diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 25bc0a4..4b5d9bb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,6 +43,9 @@ jobs: run: npm ci - name: Build and verify delivery run: npm run check + - name: Verify tool errors and recovery contracts + if: matrix.node == '22' + run: npm run test:error-contracts - name: Verify real Roslyn semantics and process cleanup if: matrix.node == '22' run: | @@ -57,6 +60,7 @@ jobs: name: check-node-${{ matrix.node }} path: | test-tmp/check/**/report.json + test-tmp/error-contracts/**/report.json test-tmp/roslyn-host/**/report.json test-tmp/roslyn-gateway/**/report.json if-no-files-found: warn diff --git a/CHANGELOG.md b/CHANGELOG.md index 10fde2d..9c0844e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 0.13.1 + +- Mask comments/literals before local declarations, add TSX/JSX scanning and scoped context, preserve original signatures/lines and invalidate legacy declaration caches. Uncertain lexical boundaries are incomplete and uncached; text references remain heuristic. +- Share the existing C# masker with UI source mapping; bound script interpolation/JSX nesting and preserve cancellation checkpoints. +- Report unknown tools as JSON-RPC InvalidParams during normal admission. Known tool errors retain matching JSON text/structuredContent and domain recovery details. Extend error acceptance and run it in Node 22 CI. +- Return impact analysis once as JSON, retaining formattedReport and both tool names. Consumers reading a second Markdown block must use formattedReport instead. +- Align guides with direct Roslyn delivery and current error contracts. Actual-client Roslyn acceptance remains explicitly deferred by the user. + +## 0.13.0 + +- Retire external Serena and default to local-text, with explicitly configured direct Roslyn for C# semantics. Exact snapshot locations flow through references, impact and refactoring. +- Include the complete Code Host/BuildHost delivery and real Roslyn verification in Windows CI. Add workspace recovery, trash partial outcomes and input/encoding consistency checks. +- Begin unified JSON tool errors; protocol and continuous acceptance follow-up is recorded in 0.13.1. + ## 0.12.5 - Handle the real Serena 1.7/FastMCP structuredContent.result string envelope before parsing symbols or references. Preserve error, empty-result, malformed and shortened-response semantics; do not discard unknown envelope metadata or silently prefer text over unsupported structured data. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8154686..35ff41c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,3 +32,5 @@ On 2026-09-08, the owner authorized applying main protection. The read-back conf SDK policy follows [Microsoft global.json guidance](https://learn.microsoft.com/en-us/dotnet/core/tools/global-json); dependency locking uses [NuGet locked restore](https://learn.microsoft.com/en-us/nuget/consume-packages/package-references-in-project-files#locking-dependencies). Current implementation is described in the [architecture guide](WinCode-架构与数据流说明.md). Remaining work is maintained in the [active engineering plan](WinCode-下一轮工程化迭代计划书.md); completed work belongs in CHANGELOG and the append-only work log. For documentation-only changes, verify local links, commands, version claims and evidence boundaries; do not claim a new runtime regression without running it. + +Node 22 CI runs `npm run test:error-contracts` and uploads its bounded report. It exercises protocol errors, matching tool-error text/structured payloads, real generated-file trash failures and workspace recovery; injected UI images test serialization only. Run it locally after changes to these boundaries. diff --git a/README.md b/README.md index b9682f4..028260c 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ WinCode is a local MCP server built for Windows and .NET engineering. It bridges - **Inspect the running app:** Enumerate visible windows, query specific controls or subtrees, and capture numbered visual overlays without activating or stealing focus from the target. - **Review with evidence:** Trace on-screen widgets back to literal XAML declaration tags, line numbers, and file hashes, with transparent reporting for ambiguity, truncation, or degraded upstreams. -Current source version: **0.13.0**. All UI tools are strictly read-only and non-destructive. See [CHANGELOG](CHANGELOG.md) for full version history. +Current source version: **0.13.1**. All UI tools are strictly read-only and non-destructive. See [CHANGELOG](CHANGELOG.md) for full version history. ### Quick start @@ -117,6 +117,8 @@ Optionally add `candidateCodeFiles: ["ViewModels/MainWindowViewModel.cs"]` (1– ### Tool reference +Local declaration search supports C#/TS/TSX/JS/JSX/Python with bounded comment/literal/JSX masking; uncertain lexical boundaries are reported as incomplete. Text references remain heuristic. Impact analysis returns one JSON text block, including formattedReport once. Known tool errors expose matching JSON text and structuredContent; unknown tools use JSON-RPC -32602 during normal admission. + The default provider is `local-text`, with an explicit semantic-unconfigured status. Configure direct Roslyn to obtain compiler-backed identities. Pass a returned `location` unchanged as `symbolLocation` to references, impact, or refactoring, and use the returned plain symbol name. Old Serena namePath identities and external startup settings are retired; stale snapshots require a new explicit search. `wincode_hello_world` reports a frozen running instance ID and build fingerprint, plus a hash of the tool definitions actually registered by that instance. Pass `toolName: "wincode_prepare_context"` to inspect just that tool's input schema. Compare it with `tools/list` on the same connection. `npm run build` emits a manifest; direct `tsc`, missing/mismatched artifacts or source development mode can report `unknown`. The build fingerprint checks local output consistency, not release authenticity. Workspace changes do not change the running build. @@ -204,7 +206,7 @@ The default `compact` response contains one JSON text block; `responseFormat: "l ### Development and validation -The [CI workflow](.github/workflows/ci.yml) runs `npm run check` on pull requests and main pushes using Windows, Node.js 22/24 and .NET SDK 10.0.303. It performs locked builds, core regression, production stdio and delivery verification, and uploads bounded reports even on failure. Node 22 also runs real Roslyn Host and MCP acceptance on generated projects. Interactive desktop/UI acceptance remains separate. Check the actual run result. Main protection was verified on 2026-09-08 with required Node 22/24 and three CodeQL checks; approvals are zero under the single-maintainer policy. See [CONTRIBUTING](CONTRIBUTING.md) for enforcement and evidence boundaries. +The [CI workflow](.github/workflows/ci.yml) runs `npm run check` on pull requests and main pushes using Windows, Node.js 22/24 and .NET SDK 10.0.303. It performs locked builds, core regression, production stdio and delivery verification, and uploads bounded reports even on failure. Node 22 also runs error/recovery contracts and real Roslyn Host/MCP acceptance on generated projects. Interactive desktop/UI acceptance remains separate. Check the actual run result. Main protection was verified on 2026-09-08 with required Node 22/24 and three CodeQL checks; approvals are zero under the single-maintainer policy. See [CONTRIBUTING](CONTRIBUTING.md) for enforcement and evidence boundaries. ```powershell npm ci @@ -232,7 +234,7 @@ WinCode 是面向 Windows 与 .NET 工程研发的本地 MCP 服务。它将项 - **观察实际界面:**发现系统可见窗口,按条件定向查询目标控件或子树,并在不激活、不抢占前台焦点的前提下获取数字标注截图。 - **源码双向印证:**将运行时抓取的控件关联回 XAML 源码声明的起始行号、代码片段与文件哈希,清晰报告歧义、截断与降级状态。 -当前源码版本为 **0.13.0**。所有 UI 取证工具均为纯只读与非侵入设计。版本历史见 [CHANGELOG](CHANGELOG.md)。 +当前源码版本为 **0.13.1**。所有 UI 取证工具均为纯只读与非侵入设计。版本历史见 [CHANGELOG](CHANGELOG.md)。 ### 快速上手 @@ -327,6 +329,8 @@ npm run delivery:verify ### 工具一览 +本地声明扫描支持 C#/TS/TSX/JS/JSX/Python,有界屏蔽注释、字符串及 JSX;词法边界不确定时报告不完整。引用仍为文本线索。影响分析仅返回一个 JSON 文本块(含一份 formattedReport);已知工具失败的 JSON 文本与 structuredContent 一致,正常受理的未知工具走 JSON-RPC -32602。 + 默认以 `local-text` 启动,并明确报告语义能力未配置。显式配置直接 Roslyn 后,将搜索返回的完整 `location` 作为 `symbolLocation` 传给引用、影响分析或重构工具,名称使用原结果的简单名称。外部 Serena 启动配置及 namePath 身份已退役;过期快照须重新显式搜索。 `wincode_hello_world` 返回启动时固定的实例 ID、构建指纹及当前注册工具定义的 hash。传 `toolName: "wincode_prepare_context"` 可按需查看单个工具参数,与同一连接的 `tools/list` 对照。`npm run build` 生成 manifest;直接运行 `tsc`、产物缺失/失配或源码开发模式会明确报告 `unknown`。构建指纹校验本地产物一致性,不证明发布来源可信;切换分析工作区不会改变运行构建。 @@ -412,7 +416,7 @@ Coding Agent ── stdio MCP ── WinCode ### 本地开发与测试验证 -[CI 工作流](.github/workflows/ci.yml) 在 PR 和 main 推送时使用 Windows、Node.js 22/24 与 .NET SDK 10.0.303 执行 `npm run check`,覆盖锁定构建、核心回归、生产 stdio 和交付校验,失败时也上传有界报告。Node 22 另运行生成项目的真实 Roslyn Host 与 MCP 验收;交互桌面/UI 验收仍单独执行。通过与否以实际运行结果为准。2026-09-08 已核对 main 保护要求 Node 22/24 和三项 CodeQL 检查;单维护者策略要求 approval=0,不代表已获独立审核。详见 [贡献指南](CONTRIBUTING.md)。 +[CI 工作流](.github/workflows/ci.yml) 在 PR 和 main 推送时使用 Windows、Node.js 22/24 与 .NET SDK 10.0.303 执行 `npm run check`,覆盖锁定构建、核心回归、生产 stdio 和交付校验,失败时也上传有界报告。Node 22 另运行错误/恢复契约专项与生成项目的真实 Roslyn Host/MCP 验收;交互桌面/UI 验收仍单独执行。通过与否以实际运行结果为准。2026-09-08 已核对 main 保护要求 Node 22/24 和三项 CodeQL 检查;单维护者策略要求 approval=0,不代表已获独立审核。详见 [贡献指南](CONTRIBUTING.md)。 ```powershell npm ci diff --git a/SECURITY.md b/SECURITY.md index 0be82f9..4b7f0bc 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,8 +1,8 @@ # Security policy / 安全策略 -The latest 0.12.x version and current `main` are maintained. Older versions do not have a separate backport commitment. Supported runtimes are Node 24 (primary) and Node 22 (compatibility), on Windows x64; build requirements are in [CONTRIBUTING](CONTRIBUTING.md). +The latest 0.13.x version and current `main` are maintained. Older versions do not have a separate backport commitment. Supported runtimes are Node 24 (primary) and Node 22 (compatibility), on Windows x64; build requirements are in [CONTRIBUTING](CONTRIBUTING.md). -目前维护最新 0.12.x 版本与 `main`,不承诺对旧版本单独回补。Windows x64 上以 Node 24 为主要环境、22 为兼容环境;构建要求见贡献指南。 +目前维护最新 0.13.x 版本与 `main`,不承诺对旧版本单独回补。Windows x64 上以 Node 24 为主要环境、22 为兼容环境;构建要求见贡献指南。 Report suspected vulnerabilities through [GitHub private vulnerability reporting](https://github.com/linnnn89/WinCode/security/advisories/new). Include the affected version/build identity, reproduction steps, expected and observed behavior, and a minimal sanitized example. Do not include credentials, personal databases or private source unnecessarily. Avoid publishing exploit details in a public issue before coordination with the maintainer. diff --git "a/WinCode-Skill\345\210\266\344\275\234\344\270\216MCP\351\205\215\347\275\256\346\214\207\345\215\227.md" "b/WinCode-Skill\345\210\266\344\275\234\344\270\216MCP\351\205\215\347\275\256\346\214\207\345\215\227.md" index 6ebace5..304d97f 100644 --- "a/WinCode-Skill\345\210\266\344\275\234\344\270\216MCP\351\205\215\347\275\256\346\214\207\345\215\227.md" +++ "b/WinCode-Skill\345\210\266\344\275\234\344\270\216MCP\351\205\215\347\275\256\346\214\207\345\215\227.md" @@ -1,6 +1,6 @@ # WinCode Skill 安装、维护与 MCP 配置指南 -适用于 **0.12.5**,核对日期 2026-09-08(北京时间)。以下使用本机 `I:/WinCode` 路径举例;其他机器必须替换路径。客户端界面名称随版本变化,以实际界面为准。 +适用于 **0.13.1**,核对日期 2026-09-08(北京时间)。以下使用本机 `I:/WinCode` 路径举例;其他机器必须替换路径。客户端界面名称随版本变化,以实际界面为准。 ## 1. 三个独立对象 @@ -22,7 +22,7 @@ npm run check npm run delivery:verify ``` -`check` 进行类型检查、Gateway 构建、锁定 NuGet restore、Release Host/控制台夹具构建、核心回归、新 stdio 验证与交付清单生成。交互桌面验收另执行 `npm run check:desktop`,需要可用 Windows 桌面。真实 Serena/TavernDesk 验收单独选择,详见 [CONTRIBUTING](CONTRIBUTING.md)。 +`check` 进行类型检查、Gateway 构建、锁定 NuGet restore、Release UIA/Code Host 与控制台夹具构建、核心回归、新 stdio 验证与交付清单生成。交互桌面验收另执行 `npm run check:desktop`,需要可用 Windows 桌面。真实 Roslyn/TavernDesk 验收按对应入口执行,详见 [CONTRIBUTING](CONTRIBUTING.md)。 `npm run build` 只构建 Gateway,不能单独证明原生 Host、Skill 和整个交付物一致。生产使用发布的 Release Host;`npm run dev` 才显式启用开发回退。报告位于 `test-tmp/check/`,内容哈希不是发布签名。 @@ -77,7 +77,7 @@ npm run skill:check -- C:/Users/40218/.agents/skills/wincode 0.12.1 起 hello 不主动启动探测进程;unknown/null 表示未探测,不代表不可用。已知健康结果也可能陈旧。旧版本 hello 的行为不能套用新版说明。 -0.12.5 已通过固定 Serena 1.7.0/Roslyn 的隔离真实验收;隔离安装不表示默认客户端已启用上游。0.12.4 起 Repomix 直接执行已安装 JavaScript bin,不再使用 cmd/npx 包装链,也不会自动下载;非标准安装和降级边界见诊断手册。 +0.13 系列已经退役外部 Serena,默认 local-text;C# 语义使用随产品交付的 Code Host,通过显式 --roslyn-config 配置入口项目、Configuration、TFM、SDK 与求值许可,字段示例见代码手册。真实 Host/MCP 验收通过也不代表实际客户端已启用 Roslyn。0.12.4 起 Repomix 直接执行已安装 JavaScript bin,不再使用 cmd/npx 包装链,也不会自动下载;非标准安装和降级边界见诊断手册。 ## 6. 常见偏差 @@ -86,7 +86,9 @@ npm run skill:check -- C:/Users/40218/.agents/skills/wincode | 源码是新版,hello 返回旧版 | 核对实际命令、路径、instanceId 和启动时间;通过客户端重连,不以强杀宿主或复制文件冒充完成 | | 字段被忽略,结果不像预期 | 对照规范字段表和实际工具 schema;容忍未知字段并不赋予其语义 | | Host 缺失或身份不符 | 完整执行锁定构建和 delivery:verify;不混用旧 DLL、新 Gateway 或开发 Host | -| Serena/Repomix 不可用 | 先区分策略禁用、尚未探测、命令缺失、握手成功但语义不可用;检查 source/fallbackReason,不把降级当语义验收成功 | +| Roslyn/Repomix 不可用 | 先核对 provider、显式配置、项目求值许可、已知健康和恢复动作;Roslyn 失败不会暗中换成本地文本,Repomix 降级不代表语义验收成功 | | UI 查不到或出现多个目标 | 核对 PID/HWND 和大小写准确的查询;只有 complete 且 unique 才能声称唯一定位 | 更新后仍无法核对客户端身份时,保留“客户端未验收”状态与实际证据,不反复尝试未声明参数。 + +0.13.1 的已知工具失败以同源 JSON 文本和 structuredContent 表达,UI/trash 保留领域信息;未知工具在正常受理时返回 JSON-RPC -32602。未知字段容忍策略与未知工具的协议错误是两回事。影响分析只返回一个 JSON 文本块,formattedReport 在对象内。 diff --git "a/WinCode-\344\270\213\344\270\200\350\275\256\345\267\245\347\250\213\345\214\226\350\277\255\344\273\243\350\256\241\345\210\222\344\271\246.md" "b/WinCode-\344\270\213\344\270\200\350\275\256\345\267\245\347\250\213\345\214\226\350\277\255\344\273\243\350\256\241\345\210\222\344\271\246.md" index 8243dfe..55cdb10 100644 --- "a/WinCode-\344\270\213\344\270\200\350\275\256\345\267\245\347\250\213\345\214\226\350\277\255\344\273\243\350\256\241\345\210\222\344\271\246.md" +++ "b/WinCode-\344\270\213\344\270\200\350\275\256\345\267\245\347\250\213\345\214\226\350\277\255\344\273\243\350\256\241\345\210\222\344\271\246.md" @@ -1,398 +1,52 @@ # WinCode 下一轮工程化迭代计划书 -更新日期:2026-09-09(北京时间)。本地版本 0.13.0,工作分支 codex/roslyn-correctness,基于 main@2235a42。A1/A2、外部 Serena 退役、默认 local-text、职责拆分及 C 已完成本地实现与验收;B 的构建、交付清单、中文异地发布目录及真实 Host/MCP 验收已通过。最新核心 307/307、桌面 35/35、Host 58、MCP 19 场景,回执见工作记录末尾。尚未提交/推送、执行远端 CI 或切换实际客户端。 +更新日期:2026-09-09(北京时间)。本轮实现版本 **0.13.1**,从 main@a23740c 推进;代码、专项验证及 CI 配置已完成,远端检查和合并以对应 PR 回执为准。实际客户端 Roslyn 验收按用户决定暂缓,不能宣称消费端已经验收。 -历史基线结果与默认 SDK 发现失败回执保留;维护脚本已统一选择现有锁定 SDK,不改系统环境。E4 已获准直接采用方案二,当前为实施中快照;真实客户端项目/配置和求值范围仍需明确。早期章节描述对应阶段历史,以本段及末尾更新为准。 +已完成实现从待办移除:LocalText 非代码区屏蔽、有限 TSX/JSX 声明、旧缓存失效、E4 恢复与领域失败专项、未知工具协议错误、影响报告去重及仓内手册同步。过程和失败修复保留在[工作日志](docs/codex_worklog.md),对外变化见 [CHANGELOG](CHANGELOG.md)。 -已完成的 WP1–WP5、Repomix 安全修复和真实 Serena 隔离验收已从待办移除,历史见 [CHANGELOG](CHANGELOG.md) 与 [工作记录](docs/codex_worklog.md)。方向总览见 [路线图](WinCode-迭代路线图.md),现状见 [架构说明](WinCode-架构与数据流说明.md)。 +## 1. 工作方针与稳定边界 -## 目标与边界 +先通过实际消费者验证 0.13 系列,再用真实任务决定下一项能力。沿用[当前分层](WinCode-架构与数据流说明.md)的 Registry、Router、适配器及独立 UIA/Code Host;不为潜在需求新增通用平台、共享服务或持久语义数据库。 -当前下一轮集中修正已经复现的误导性结果、输入处理问题及 Roslyn 交付缺口,沿用 Gateway → Registry/Router → Core/能力接口 → Adapter/Host 的结构。此前失败恢复与有界负载验收保留,不重复当作未完成任务;不因 Router 较大就机械拆层,不增加微服务、插件框架、消息队列或新数据库。 +已经确认的边界继续有效: -已确认策略继续有效:Node 24 主支持、22 兼容;未知字段容忍并忽略,已声明字段严格校验且在 Skill 列出;hello 不主动探测;UI 取证不操作目标应用;安装与真实上游使用隔离目录。输出范围、候选与已证实事实必须分开表达。 +- 默认 local-text;C# 语义通过显式配置的直接 Roslyn,不恢复 Serena 安装链。 +- Node 24 主支持、22 兼容;保持锁定依赖和 SDK,新增依赖与环境变化另行确认。 +- 未知请求字段容忍并忽略,规范字段及类型继续校验;以[仓内 Skill](skills/wincode/SKILL.md)说明为准。 +- hello 只读取已知观察,不主动求值或探测。工作区恢复失败时阻断业务请求;trash 部分完成保留实际位置,不自动移回。 +- 未知工具在正常受理状态返回 JSON-RPC -32602;已知工具失败保留 isError,JSON 文本与 structuredContent 同源。影响分析只有一个 JSON 文本块,保留 formattedReport 和全部证据字段。 +- LocalText 仍是有限文本分析:插值/JSX 内表达式省略,复杂语法可能漏检;无法确定词法边界的文件标记 lexical-uncertainty 且不缓存。文本引用不等于语义引用,完整扫描不等于全语言语义完整。 -## E1:工作区切换失败一致性(优先) +## 2. 待恢复:实际客户端 Roslyn 验收 -**基线风险与本轮证据:** [ToolRouter.openWorkspace](src/Core/ToolRouter.ts) 在工作区根更新后还会重置、初始化和绑定适配器。本轮故障注入已复现失败后仍准入请求、部分根/会话/watcher 不一致和提交后取消未识别;修复后原 13 个 E1/E2 故障用例不再报告这些问题。 +用户已决定“先完成代码与 CI,客户端验收暂缓”。本轮不修改客户端 MCP 配置,不部署全局 Skill;新 stdio、InMemory、磁盘清单及真实 Host/Gateway 测试均不能代替当前 Codex 连接验收。 -1. 在根切换、缓存/会话更新、适配器 dispose/reset/initialize/rebind 各阶段注入失败与取消。 -2. 检查失败后的根路径、缓存命名空间、watcher、上游绑定、请求占用和下一次请求行为。 -3. 根据复现选择最小恢复策略,并补充回归;禁止错误发生后以“已成功切换”继续返回混合证据。 +恢复时先明确客户端、隔离工作区、入口 csproj、Configuration、单一 TFM、SDK 和 MSBuild 求值范围。推荐使用仓内生成的受控 C# 夹具;若改用 TavernDesk,另行确认真实项目配置和求值授权。 -**验收:** 各故障点结果可解释;后续请求只读同一工作区,或明确拒绝并给出恢复动作;无旧缓存串入、重复 watcher 或自有进程残留。覆盖成功、失败、取消及再次切换。 +验收顺序: -**2026-09-09 用户已确认并实施:** 变更前失败保留旧工作区;变更后无法确认一致性时拒绝业务请求,重新 workspace_open 完成恢复。hello 可被动读取恢复状态。同根恢复不走快速路径;恢复再次失败仍保持拒绝。取消发生于根变更后同样进入恢复状态。不实施跨适配器自动回滚。 +1. 建立新连接,hello 核对版本、构建、实例、Schema 和 provider;未探测状态保持 unknown。 +2. 搜索重载/同名声明,明确选择返回的 symbolLocation。 +3. 同一连接执行引用、影响分析与重构建议,验证始终使用同一声明;歧义、零引用或部分结果不能解释成可安全删除。 +4. 仅修改隔离夹具,用旧 location 验证明确的 stale/输入变化错误;按 recoveryAction 处理并重新搜索,新位置应恢复成功。 +5. 记录实际连接身份、位置、错误恢复及结果。手册同步与磁盘文件一致不能替代重连;不得用终止 Codex 或修改真实项目制造验收条件。 -**复核补修:** 内部 Serena 清理或旧 watcher 关闭失败会保留失败状态,返回 recoveryAction=restart_gateway,提示检查自有资源清理后重启 Gateway;不再让 workspace_open 重复修改会话或承诺可以恢复。可重试的 watcher 创建失败仍返回 workspace_open。绑定结束及切换提交前均核对 watcher;创建失败或初始化中 watcher 出错不会成功提交切换。已覆盖内部关闭异常、底层 watcher 创建/关闭失败及初始化期间事件错误。 +**USER_DECISION_REQUIRED:** 恢复验收时确认上述项目和求值范围。当前暂缓,不影响已经授权的代码、CI、PR 和合并工作。 -## E2:trash 部分完成与恢复 +## 3. 条件性下一轮工作 -**基线风险与本轮证据:** [Workspace.moveToTrash](src/Core/Workspace.ts) 先 rename 再写元数据。本轮已用生成文件复现元数据失败但文件已移动;已增加 completed/not_moved/partial、失败阶段和实际路径,保留旧字段。测试覆盖目录准备失败、移动失败、元数据失败、重复请求、重启后文件保留及同名文件在同一时间戳下分别保留。 - -1. 注入 rename 失败、rename 成功后元数据失败、恢复步骤失败。 -2. 保留源路径、实际目标位置与失败阶段,使已移动文件可找回。 -3. 验证再次请求不会把“失败”误当成完全未执行;恢复动作不能覆盖已有文件。 - -**验收:** 任何结果均能解释文件实际位置;不丢失或覆盖内容;重复请求和重启后恢复有明确边界。 - -**2026-09-09 用户已确认并实施:** 准确报告部分完成和实际位置,不自动移回;保留 success/trashPath/message 并增加状态字段。仓内 Skill 已补充恢复说明,未部署至用户全局 Skill。丢失部分完成响应且元数据未完成时,不保证自动恢复原目录映射;本次不新增恢复数据库或自动回滚。 - -**复核补修:** UUID 与时间戳保留唯一性,展示用原文件名按 Unicode 码点截短,为 .meta.json 留出空间;最终元数据文件名不超过 255 UTF-8 字节(同时约束 Windows UTF-16 长度)。完整 originalPath 保留不变。183–255 字符 ASCII 边界及中文/emoji 文件名夹具均验证正文和元数据完整。 - -## E3:有界混合负载验收 - -现有有限次数的顺序/并发生命周期测试不能证明真实上游长时间运行稳定,也没有证据据此断言存在泄漏。 - -先运行一个不超过 5 分钟、100 次调用的小样本,在隔离的两个工作区交替查询、切换、取消,并模拟自有上游退出。记录调用延迟、输出量、Gateway/自有子进程 PID、可取得的内存和句柄趋势、清理结果。预算扩大或安装其他上游前另行确认。 - -**验收:** 无跨工作区证据污染、请求占用永久不释放、自有进程残留;区分启动增长、缓存稳定平台与持续增长趋势。报告实际采样条件和不可观察项目,不把一次内存峰值当泄漏或把短测当耐久证明。复用现有脚本和报告目录,不建大型基准平台。 - -固定 Serena 1.7.0/Roslyn 的真实验收已在本轮复测并增加切换检查;固定 Repomix 1.18.0 已完成下述实际打包验收。普通 CI 不自动安装或运行这些上游。 - -**2026-09-09 本地小样本:** scripts/verify-mixed-load.ts 记录 80 次 Core 操作(另有夹具初始化/握手)、10 轮采样、10 个真实 Node 模拟上游进程退出,结束时无自有子进程残留,查询/切换检查未发现跨根证据。采样仅约 1 秒,RSS 从约 134 MiB 增至 137 MiB、heapUsed 从约 34 MiB 增至 42 MiB;尚未观察稳定平台,不能判断长期增长或宣称无泄漏。Windows 句柄数、子进程 RSS 和真实上游兼容性仍未覆盖。 - -**复核后交错小样本:** 原脚本的调用前取消和顺序切换不足以验证运行中交错,已改为实际进入模拟上游 RPC 后再发起切换;5 轮运行中取消、5 轮上游退出,均断言切换等待旧请求、旧根在请求占用期间不变、结束后新根查询正确。共记录 70 次 Core 操作、10 个自有进程,1934 ms,结束时无自有进程残留。报告 test-tmp/mixed-load/run-ftSuQB/report.json;此结果只补齐受控交错场景,不是耐久性或真实语义上游验收。 - -**后续完成(2026-09-09):** `npm run test:mixed-load -- --sample-interval-ms=10000` 在同一 100 次/5 分钟预算内完成 70 次操作、10 轮、97235 ms。Windows 指定 PID 采样均成功:每轮结束 Gateway 句柄均为 234,dispose 后 233;工作集启动阶段下降后,第 2–9 轮约 103.4→105.2 MiB,仍有小幅增长,不能把这一短窗判为长期稳定平台。10 个上游工作集约 53.9–56.4 MiB,结束时均无残留。报告 `test-tmp/mixed-load/run-NUo0VL/report.json`。指标含采样点而非峰值,无强制 GC、持续高负载或耐久性证明;E3 按原有有界标准完成,不把未授权的长时间压力测试提升为本轮新增验收条件。 - -用户随后授权补齐环境,所有持久组件最终位于 `.deps`:Python 3.13.15、Serena 1.7.0 固定提交 949a27e、上游锁定 Roslyn 5.5.0-2.26078.4、Repomix 1.18.0。新增组件和下载缓存逻辑大小合计约 716 MiB(硬链接可能重复计数,非物理分配量),已有 SDK/NuGet 不计入。环境回执 `.deps/environment-receipt-20260909.json`;不改系统 PATH、全局 SDK、项目主依赖锁或 Codex 注册。 - -- Serena:`npm run test:serena-real -- "绝对项目路径/.deps/serena-venv/Scripts/python.exe" "绝对项目路径/scripts/serena-isolated-launcher.py"`。固定 8 项全部通过,包括原 7 项语义检查和 Router A→B→A 的缓存/新查询切换、每次实际重连与自有 PID 退出;报告 `test-tmp/serena-acceptance/1788923903497-28124/report.json`。专用启动器在导入上游之前设置隔离环境,避免 MCP SDK 默认环境白名单丢弃 SERENA_HOME。首轮误生成的用户目录配置/日志已依据创建时间和上游“原文件不存在”日志核对后归档 `.deps/serena-first-attempt`,不遗留全局 Serena 配置。 -- Repomix:`npm run test:repomix-real -- "绝对项目路径/.deps/repomix/node_modules/repomix/bin/repomix.cjs"`。10 项通过:安装握手、Markdown/XML/plain、真实压缩、空包、watcher 后缓存失效、封闭候选集、实际 CLI 取消、启动超时与输出清理;报告 `test-tmp/repomix-acceptance/中文 & (real)-hCTcgs/report.json`。实测修复了说明文字被误计为正文文件的问题;使用独立 CLI 摘要,缺少受支持摘要时降级而不猜测数量。未宣称覆盖任意版本、用户配置或所有打包功能。 - -## E4:结果与错误契约渐进整理 - -当前 UI、代码和 Gateway 错误格式不同。先盘点高频失败:范围无效、目标歧义、预算截断、取消、上游不可用、部分完成;为每类明确稳定错误码、来源、可重试条件和恢复提示。 - -**验收:** 现有成功响应和调用方式兼容,客户端无需解析自然语言识别已纳入的失败;未知/不完整状态不被改写成成功或否定结论。只处理实际用例涉及的字段,不一次性替换所有结果信封。 - -**USER_DECISION_REQUIRED:** 新公共响应字段及兼容策略需在盘点后确认;不把 MCP 的可选结构化输出能力当作必须全面重写接口的理由。 - -E1/E2 所需 WORKSPACE_RECOVERY_REQUIRED、TRASH_NOT_MOVED、TRASH_METADATA_FAILED 已按用户确认的兼容方案局部实现;不等于所有工具的错误与证据模型已完成统一。其余范围、歧义、预算和上游错误已完成下述盘点,公共字段兼容方案待确认。 - -### 2026-09-09 E4 盘点与待确认兼容方案 - -`scripts/verify-error-contracts.ts` 已通过 InMemory MCP 实测 10 个场景,报告 `test-tmp/error-contracts/run-0L5l8H/report.json`。取消/普通异常使用隔离 Router 的操作错误注入,其余为实际校验、读取或关闭路径;没有操作真实 UI。 - -| 场景 | 当前可机器读取的事实 | 缺口与处理意见 | -| --- | --- | --- | -| 未知工具、代码参数/范围无效 | MCP isError=true;content 为普通错误文本 | 增加独立 structuredContent,分别使用 UNKNOWN_TOOL / INVALID_ARGUMENT;修正工具名/参数后才能再试 | -| UI 参数无效 | success=false、INVALID_ARGUMENT、errorMessage | 保留现有 content,补相同的附加元数据;不调用原生 UI | -| 目标歧义 | 引用 resolution=ambiguous、candidates;context 的 fileIssues.reason 标识歧义且 evidence 为空 | 保持现有候选/范围信息,不伪造成功命中;按候选缩小范围,暂不重写成功结果信封 | -| 预算不足 | truncated、queryComplete、evidenceInsufficient;行范围还含 missingRanges/nextRequest | 这是部分证据,不统一变成执行失败;继续使用最终序列化后计算的预算和覆盖信息 | -| 上游不可用 | source=serena-adapter-fallback、analysisCompleteness=degraded/incomplete;health 的 upstream/lastError | 来源能区分降级,但并不说明唯一故障原因;不从 queryError 自然语言猜码,不把空本地结果说成语义零结果 | -| 执行中取消 | success=false、CANCELLED;必要时 workspaceRecovery | 保留现有字段;恢复状态优先于一般重试提示 | -| 关闭时拒绝 | status=failed、reason=cancelled、provider=wincode、recoverable=false | 与执行中取消不同;附加 GATEWAY_SHUTTING_DOWN,提示重启,而不是盲目重试 | -| 普通执行异常 | MCP isError=true;content 为普通错误文本 | 附加 TOOL_EXECUTION_FAILED;原因未知时标记 inspect_error,不承诺自动重试 | -| trash 部分完成、切换恢复 | outcome/failureStage/实际路径;WORKSPACE_RECOVERY_REQUIRED/recoveryAction | 继续使用已确认字段,不覆盖实际位置或永久恢复动作 | - -**建议的首批公共兼容方案(USER_DECISION_REQUIRED,尚未实施):** 只给 Gateway 已失败响应增加 `structuredContent={success:false,errorCode,errorMessage,provider:"wincode",retryable:false,recoveryAction}`,旧 content、isError、成功响应及工具参数不变。retryable=false 表示不推荐原样自动重发;recoveryAction 表示先修正参数、检查错误、重新打开或重启后再请求。已执行部分副作用的取消使用现有 workspaceRecovery 动作;trash 不被通用提示覆盖。未知原因仅用 TOOL_EXECUTION_FAILED,绝不凭文字猜测上游错误类型。 - -例如无效 query 当前仍返回 `Tool Execution Error: ...`;附加结构将为 `{"success":false,"errorCode":"INVALID_ARGUMENT","errorMessage":"原错误信息","provider":"wincode","retryable":false,"recoveryAction":"fix_arguments"}`。取消、关闭、未知工具、普通异常分别用 CANCELLED、GATEWAY_SHUTTING_DOWN、UNKNOWN_TOOL、TOOL_EXECUTION_FAILED;未知工具动作使用 fix_arguments,取消使用 retry_after_cancellation(若已有工作区恢复动作则优先)。此批不增加统一成功信封,不新增工具,不改变预算,也不实施语义图/UI 绑定路线。 - -验收:旧 content 逐项保持一致;新客户端无需解析自然语言识别上述失败;无效参数在副作用前拒绝;取消/关闭/永久恢复动作不混淆;成功、歧义、预算截断、空降级结果原样保留。未知扩展字段继续容忍并忽略。 - -## 交付与检查关口 - -每个工作包独立形成可审查变更,先记录触发问题和失败样例,再做最小修复。影响交付输入时执行 `npm run check`;UI 路径变化增加 `npm run check:desktop`,上游路径变化增加对应 opt-in 实测。文档单独修改只做链接、命令、版本和事实一致性核对。 - -沿用已授权的版本流程:针对性复测与 debug → 对应版本和 Skill 同步 → PR 精确提交的 Node 22/24 与三项 CodeQL 检查 → 合并 → 主分支交付核对。实际客户端重连另行核对,不能用磁盘版本或新测试会话代替。每步写入既有工作日志;作者自审不等同独立审核。 - -每包验收失败即停在该包定位原因,不叠加下一包掩盖失败。新依赖、运行环境、重要公共接口或恢复政策超出既有决定时先确认。本计划不预设版本号或工期,避免把尚未复现的风险包装成已确定修复规模。 - -## 2026-09-09:对照“架构分析优化建议”的后续计划 - -来源:[架构分析优化建议](chatgpt-conversation://6aa0185d-0bb8-83e8-9281-594f16e8c6d2)。已读取两轮完整问答,并与上述源码基线对照。对话中的架构判断作为建议输入;优先级与实施范围以下述核对为准,尚未获得实施新架构的授权。 - -### 建议与当前实现的差距 - -| 对话建议 | 当前源码证据 | 真正待完成的工作 | -| --- | --- | --- | -| Capability Registry | Gateway/ToolRegistry.ts 已集中注册、发布、校验、别名和契约哈希;ToolDefinition.ts 已定义执行接口 | 先盘点现有描述和状态信息能否表达适用任务、前置条件、降级路径;仅为真实选择困难补元数据,不另建重复注册体系 | -| Evidence Model | Core/CodeQueries.ts 已表达来源、完整性、截断和局限;UI 映射也有候选状态和文件哈希 | 在 E4 中对齐共有语义及错误码;逐类兼容迁移,不把供应方名称或任意数值置信度当正确性保证 | -| Semantic Graph / Impact Analysis | Core/DotNetGraph.ts 有声明级项目图;Serena 提供符号/引用查询;CompositeTools/ImpactAnalyzer.ts 已有影响报告 | 验证能否复用这些能力构造有来源的有限符号关系;声明依赖、引用和真实调用关系分别标注,不能把当前实现说成全仓调用图 | -| Code ↔ UI Mapping | UiSourceMapper.ts / UiCodeMapper.ts 已提供 XAML 与 C# 候选链 | 当前明确 runtimeSourceVerified=false、运行构建与源码身份未知、模板解析不支持;优先改善候选消歧与语义关联,真实 Binding/DataContext 解析仍是条件性研究 | -| Incremental Index | Cache.ts / WorkspaceWatch.ts 已有缓存、指纹和变化失效 | 这些不等于持久化符号/引用增量索引;先测重复扫描成本,再决定是否需要新索引及存储 | -| 统一错误与 CI | McpServer.ts 取消错误已结构化,普通异常仍返回文本;ci.yml 已运行 Windows Node 22/24 的 npm run check | 错误整理沿用 E4;CI 继续补与改动相匹配的回归,不重复建设已有流水线,真实桌面和上游实测保持独立边界 | - -### 建议执行顺序与验收 - -1. **先处理可靠性底座(E1 → E2 → E3)。** 用隔离夹具证明工作区切换和 trash 部分完成问题,再按确认后的恢复政策修复。验收沿用各工作包,尤其检查“请求报错但状态已改变”的场景。混合负载遵守既有小样本预算;本轮未执行故障注入,也未判定风险已复现。 -2. **渐进统一证据、错误与能力描述(E4)。** 先交付字段/失败场景对照表及兼容方案,再做局部实现。优先涵盖取消、上游不可用、歧义、截断和部分完成。验收要求旧客户端仍能调用,新增字段可机器判读,空结果与不完整查询不混淆。能力声明复用既有 Registry 和 Skill;不额外增加同义 MCP 工具。 -3. **语义关系最小验证。** 建议先以 C# 小型多项目夹具验证,复用 Serena 符号身份与引用、现有项目图和 ImpactAnalyzer;先支持唯一符号的一跳关系及影响证据。覆盖同名/重载、跨项目、缺失上游、查询截断,确保每条关系可定位来源,未知关系保持未知。先评估可行性,再决定是否扩展关系类型、引入新解析器或持久化图;不承诺完整调用图或确定性“会不会坏”。 -4. **深化 UI 到代码的证据链。** 在现有 XAML/C# 候选基础上,验证一个明确 WPF 场景的 AutomationId → XAML → Command 候选 → 语义声明/引用链。覆盖重复标识、多个候选、模板和运行二进制与源码不一致;无法证明运行时绑定时继续标记候选。只有实际任务被阻塞,再评估 R8 的应用内诊断路线。 -5. **按测量结果决定增量索引及语言扩展。** 固定任务比较冷/热查询、少量文件变更后的耗时、扫描量和资源趋势;只有现有缓存/上游复用仍不足时才提出索引方案。增量方案须验证文件修改、删除、重命名与工作区切换后不返回旧证据。SQLite、新服务、TypeScript/Python 扩展均不预先列入必做实现。 - -对话强调的长期价值仍是语义关系、UI 到代码映射和可追溯证据;实施顺序建议先稳住工作区一致性,再逐步增强这些已有能力。继续维持 MCP 能力层定位,当前计划不包含自建 Agent、向量数据库或大量扩增工具。 - -**USER_DECISION_REQUIRED:** E1/E2 的恢复政策、E4 的公共字段兼容方案,以及后续是否采用 C# 优先的最小语义范围,均在调查形成具体方案后确认。新的依赖、持久化存储或应用内注入另行确认。本次拉取与计划整理不包含安装、构建部署、客户端重连或远端提交授权。 - -## 2026-09-09:直接集成 Roslyn 的设计稿 - -**授权与状态:** 用户要求开始设计绕过 Serena、直接集成 Roslyn,并进一步解释 E4 利弊。本节取代上述后续语义路线中“继续经 Serena 实现”的默认建议;历史验收仍保留。当前仅完成设计,尚未新增 Roslyn 生产依赖、编译语义 Host、切换提供方或删除现有 Serena 实现。 - -**后续复核:** 本文末尾“社区实践与第一性原理复核”修订了首个原型范围、身份设计顺序、监听失效要求及 E4 推荐顺序;涉及这些取舍时以该复核为最新建议,以下保留为原设计记录。 - -### 目标与第一版能力 - -WinCode 自行管理 C# 语义查询,使用 Microsoft.CodeAnalysis 系列库;用户不再为这条能力安装 Serena、Python 或独立 Roslyn 语言服务器。产品包携带 WinCode 自有语义 Host 和所需 Roslyn 组件。加载真实项目仍可能需要匹配的 .NET SDK、目标框架引用包和已恢复的项目依赖;“随产品提供分析组件”不等于任意项目零前置条件。 - -首版建议限于 Windows 上 SDK 风格的 C# `.csproj`/`.sln`:声明查找、重载/同名符号消歧、指定符号的跨项目源码引用、精确引用位置、向现有影响报告提供有范围和完整性标记的证据。无 Serena 条件下完成验收是必要条件。TS/JS/Python 保持已有文本能力,明确不提供 Roslyn 语义分析;VB/.NET Framework 特殊项目、自动重命名/写代码、完整动态调用图、WPF 运行时 Binding、持久化图索引不纳入首版。 - -### 模块边界与部署 - -建议调用链:`现有 MCP 工具 → ToolRouter / CodeQueries → RoslynAdapter → WinCode.Code.Host → Roslyn 库`。这是随 WinCode 分发、按需启动的本地子进程,不新增用户注册的 MCP、不监听网络、不要求安装另一款工具。Roslyn 库直接在自有 .NET Host 中执行;采用进程边界是因为当前 Gateway 为 Node.js,也便于超时回收和释放 .NET 工作区资源。 - -| 方案 | 收益 | 代价/判断 | -| --- | --- | --- | -| 加入现有 UIA Host | 交付上少一个可执行入口 | UI Host 当前为一次请求读取 stdin 到 EOF,并含 DPI、桌面通知、FlaUI;语义 Workspace 需要跨查询驻留。改造成混合生命周期会耦合桌面和编译资源,不推荐 | -| 新增 WinCode.Code.Host(推荐) | C# 工作区独立生命周期,随同一个产品包交付,故障不会占用 UIA 请求 | 增加一个可执行组件及内部协议;MSBuildWorkspace 还可能启动自身 BuildHost,须把后代进程纳入清理,不能声称整个功能只有一个 OS 进程 | -| Node 进程内直接加载 .NET | 减少显式子进程通信 | 引入 FFI/运行时桥接和额外兼容链,现有工具链无此基础,不推荐 | - -复用 CodeSymbolQuery/CodeReferenceQuery/ContextCodeQuery、ResourceManager、现有请求占用/切换锁、WorkspaceWatch 和交付指纹。不另建泛化插件框架;只有实际共用逻辑才提取。拟新增 `src/Adapters/RoslynAdapter.ts` 与 `tools/WinCode.Code.Host/`;调整 CodeQueries、ToolRouter、ImpactAnalyzer 和健康报告中的提供方耦合,Gateway 保留现有工具名。 - -候选依赖为 Microsoft.CodeAnalysis.CSharp.Workspaces、Microsoft.CodeAnalysis.Workspaces.MSBuild、Microsoft.Build.Locator,统一选择兼容版本并锁定 NuGet。版本、完整传递依赖、发布字节数和包许可证清单在实现首个最小原型时核验,不从 Serena 的语言服务器包版本推断 Roslyn 库版本,也不直接依赖 SDK 私有目录内的 DLL。构建沿用项目既有 SDK 策略;分析目标的 SDK 选择另遵从该项目 global.json,缺失时报告,不静默下载或挑任意新版。MSBuild 定位规则参考[微软文档](https://learn.microsoft.com/en-us/visualstudio/msbuild/find-and-use-msbuild-versions?view=visualstudio)。 - -### 项目加载与事实边界 - -不能靠枚举 `.cs` 文件和补几个引用就承诺完整语义。真实编译还涉及 Compile 条目、条件符号、引用、imports、目标框架、生成文件。建议使用 MSBuildWorkspace 读取实际项目配置,显式记录 Configuration、Platform、TargetFramework 和加载诊断。多目标框架不能任选一个后把结果说成覆盖全部;首版限定一个明确配置,存在多种且未指定时要求选择。 - -**重要取舍:** 项目加载通常涉及 design-time build。微软说明其用途是获得源文件/引用/选项,会调用额外 MSBuild targets,并随配置/框架而变化。[设计时构建说明](https://github.com/dotnet/project-system/blob/main/docs/design-time-builds.md)。因此“不调用 dotnet build/restore”不能保证任意用户项目绝无执行副作用;项目自定义 targets 仍是需要信任的代码。 - -建议政策:未获项目执行信任时仅提供语法/文本证据;明确允许设计时求值后才进入项目语义模式,许可按工作区及加载策略保存,普通查询不重复询问。默认不自动 restore、不编译目标程序、不运行目标程序、不主动运行项目分析器/源生成器;若 MSBuild 自定义目标本身执行代码,这一政策不能当作沙盒保证。源码生成相关引用缺失时标记不完整;已有 obj 生成文件也不能未经身份核对就当作当前源码。首版在已授权的生成夹具中实现,再确认真实用户项目的信任交互。 - -外部项目引用、链接文件、SDK/NuGet 元数据有不同用途:源码取证范围必须经过根目录包含性校验;根外源码不自动展开,列出范围缺口。授权使用的 SDK/包元数据可参与类型分析,不等于授权读取任意根外源码或加载其中分析器。项目加载失败、依赖缺失、条件配置不明确时不输出 queryComplete=true。 - -### 查询、身份和变化一致性 - -1. 声明查找使用语法树与编译符号,显示名只用于展示。重载、泛型、partial、接口实现不能按字符串等同。引用查询采用 Roslyn 的 SymbolFinder.FindReferencesAsync,范围为实际加载的 Solution;精确位置由源码 Location/span 得到,行/列对外统一一基,不能把所在方法起点冒充调用点。[官方引用 API](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.findsymbols.symbolfinder.findreferencesasync?view=roslyn-dotnet-4.13.0)。这仍不覆盖反射、运行时动态绑定或仓外调用。 -2. 建议引入不透明 `symbolId` 与 `snapshotId`,身份绑定工作区、项目/TFM 和文档版本;由当前快照的符号映射解析,不依赖 Serena 的 `/Save[0]` 序号。名称/签名用于显示与可读消歧。ID 在编辑、切换或 Host 重启后可能过期,返回明确失效状态并要求重新定位,不承诺跨版本永久 ID。ID 表有界且随快照释放。 -3. 每个 Gateway 最多维护一个活动语义工作区;按需启动 Host,持有可复用 Solution 快照。内部 stdio 协议包含 requestId、workspaceGeneration、snapshotId 和协议版本,stdout 仅协议,stderr 为有界日志;有限帧长度、并发数、队列、取消和超时,不另建网络服务。 -4. WorkspaceWatch 收到 `.cs` 变化时更新/失效文档快照;首版可保守重载,优化增量复用后置。`.csproj`、global.json、Directory.Build.*、assets 文件变化触发项目重载。watcher 本身不保证原子文件快照;响应提交时核对代次和参与文档的版本/哈希,发现变化返回不完整或取消,禁止把不同快照的身份与引用拼成一个完整结果。 -5. 切换沿用 E1:排空旧请求后释放旧 Host/子进程,再初始化新工作区;释放失败不能假装恢复成功。取消是请求级操作,超时无法收敛时关闭自有 Host 树并废弃快照,后续只读查询再按明确状态重建。预算包括加载与查询时间、返回条数/字节及快照资源;初始值根据生成多项目夹具测量,不宣称仅裁剪输出就限制了内部计算内存。 - -### 兼容迁移与交付验收 - -- 保留现有工具名和普通参数;拟为精确引用新增可选 symbolId/snapshotId。旧简单名称继续支持,但歧义必须重新选择。已有 Serena namePath/序号不可静默套到 Roslyn 顺序上,旧身份请求提示重新定位。 -- `source` 目前是 Serena 专用枚举,ImpactAnalyzer 也按 serena-mcp 判断语义来源。必须明确引入 Roslyn 来源并修改类型、消费方和回归;不返回假的 serena-mcp。旧客户端若穷举 source,新增值仍可能不兼容,不能宣传为完全无损替换。建议先显式选择 roslyn 验证,再决定切为默认的版本迁移;不自动偷偷回退到 Serena。 -- 查询完整性由项目加载、查询范围、诊断和预算共同决定;Roslyn 来源不自动提高 confidence。零引用继续不能推出安全删除。错误契约可复用 E4,但 E4 不需要等待 Roslyn 才实施,Roslyn 原型也不需要先改全部工具信封。 -- 首个验证阶段只做自有 Code Host + 生成的两项目 C# 夹具,覆盖同名/重载、泛型、partial、接口、跨项目、合法空结果和精确坐标;以明确预期源码位置为判据,Serena 仅可作可选对照,不能作唯一正确性判据。 -- 接入阶段覆盖冷/热查询、编辑/删除/重命名、A→B→A、旧 ID、配置/TFM 切换、缺失 SDK/引用、未信任项目零设计时执行、取消/崩溃/关闭、预算截断。保留 E1/E2 行为与当前核心回归;不运行目标应用。 -- 交付阶段将 Code Host、Roslyn 与必要 BuildHost 文件、NuGet 锁、协议/提供方契约纳入版本和交付指纹。现有交付清单有 512 目录条目、单文件 64 MiB、合计 256 MiB 等界限,须按实物发布包审核,不能直接关掉校验。验证无 Python/Serena 可用的干净环境仍能完成首版 C# 验收后,才迁移默认提供方和移除运行依赖。此阶段不自动删除当前 .deps 中用于对照的安装。 - -**落地前待确认:** 建议首版 C# SDK 项目/单配置、WinCode 自有 Code Host、显式项目设计时求值信任、先可选后默认的迁移。用户已确认直接集成方向;上述范围、执行边界和身份/source 公共字段属于本设计的具体取舍,当前没有把它们当作已获实现批准。 - -## 2026-09-09:E4 兼容方案的利弊与修订建议 - -E4 解决的是“不同工具报错方式不同,调用方不得不猜文字”,不提升代码理解能力。当前真实样例包括普通 `Tool Execution Error: ...`、CANCELLED JSON、关闭时 reason=cancelled 的旧对象。成功结果已有自己的范围/歧义/截断事实,这些不应为了统一而删改。 - -### 三种迁移方式 - -| 方式 | 好处 | 代价/风险 | -| --- | --- | --- | -| 保留原 content,失败时附加 structuredContent(此前建议,推荐作首批过渡) | 老的文本消费路径变化最小;新客户端可按固定错误码分支;改动集中 Gateway,可独立回退 | 老客户端若只读 content,得不到新收益;严格拒绝未知字段的客户端仍可能不兼容;两个表达必须由同一分类事实生成;需要实测实际宿主是否把结构化部分交给模型 | -| 错误 content 改成规范 JSON,同时提供同一 structuredContent | 文本与结构一致;只读文本的模型也能看到错误码;便于统一 schema | 原来按固定前缀/纯文本解析的客户端要迁移;不属于“旧错误文本完全不变” | -| 保留第一条旧文本,再追加 JSON 文本及 structuredContent | 保留旧首条文本,同时让只读 content 的消费者看到机器字段 | 额外文本与字段重复,模型输入可能更长;只允许一个文本块的客户端仍可能不兼容;只能承诺保留首块,不能承诺 content 数组逐项不变 | - -MCP 官方将 structuredContent 与 outputSchema 设为可选,并建议返回 structuredContent 时同时提供其 JSON 文本表示。[工具规范](https://modelcontextprotocol.io/specification/2025-11-25/server/tools)。因此第一种仅保留旧自然语言的方案是为了迁移而做的取舍,不能声称已经满足“同一 JSON 文本镜像”的兼容建议。也不能因为 SDK 能解析 structuredContent,就保证当前 Codex 会按它执行恢复。上线前需明确选定文本策略并验收实际客户端;测试连接不能冒充用户当前连接。 - -### 收益、维护成本与语义风险 - -- 收益:INVALID_ARGUMENT 可提示改参数;WORKSPACE_RECOVERY_REQUIRED 可区分 workspace_open/restart_gateway;关闭状态与普通取消可区分。错误文案修改/翻译不必导致程序分支改变。后续 Roslyn 的加载失败与过期身份可以沿用同一表达办法。字段只帮助调用者选择动作,不能保证 AI 正确执行,也不会自动实施恢复。 -- 成本:需要维护错误码、分类映射、字段类型、手册和兼容回归;structuredContent 不是加一个 JSON.stringify 就结束。不得从自然语言匹配关键字猜故障类型,只在明确校验分支/已知错误类型分类。额外结构增加传输字节,实际 token 增量由客户端如何渲染决定,暂不宣称固定 token 成本。 -- 成功与副作用:success=false/isError=true 只表示请求没完成预期结果,绝不表示文件没动、工作区没变。trash 的 partial、实际路径及 workspaceRecovery 必须保留且优先,不能被通用恢复提示覆盖。预算截断/候选歧义可能是有用但不完整的结果,不能一律改成失败或自动重试。 -- 重试语义:原提案统一 retryable=false 仅想禁止原样自动重发,但很容易被理解为“永远不能再调用”。**修订建议是首批省略这个新增布尔字段,使用明确 recoveryAction;如未来确有自动重试需求,再依据幂等性、执行阶段与副作用设计 retryable。**取消后恢复动作建议 inspect_state(若已有 workspaceRecovery 则用其动作),不笼统写 retry_after_cancellation。已存在的 recoverable 字段保留,不静默重定义。 -- provider=wincode 仅表示错误由 Gateway 报告,不代表根因一定在 WinCode;底层原因未知时不可冒充 roslyn/serena 故障。未知普通异常保持 TOOL_EXECUTION_FAILED。错误信息不新增完整栈、凭据或未返回的用户源码。 -- outputSchema 若只描述失败对象,会与成功结果不匹配;首批暂不为整工具声明这种不完整 schema,用内部类型/测试校验新增字段。以后明确成功/失败联合结构再发布工具输出 schema。 -- 未知工具和真实协议层失败是另一个边界:MCP 规范将未知工具列为协议错误,当前 Gateway 实际返回 isError 文本。此前把 UNKNOWN_TOOL 一并列入只是兼容盘点,不应以 E4 名义悄悄改传输语义;修订后的首批建议先保持它的现有行为,协议规范化单独审查。 - -**推荐的缩小版首批(未实施):** 对已知工具的参数错误、执行异常、取消/关闭、工作区恢复,保留旧 content/isError,附加 success、errorCode、errorMessage、provider、recoveryAction;不新增统一 retryable,不重写成功结果,不改未知工具传输行为,不覆盖领域部分完成信息。先验证字段与原文本一致、旧客户端仍能调用、实际模型能否看到新增信息。若需要只读 content 的消费者也获得全部错误码,再由用户选择 JSON 文本迁移或附加第二块,而不是声称第一种已经覆盖所有客户端。 - -当前用户要求详细解释利弊,尚未批准此修订字段集合或具体文本策略;先保留设计稿。直接 Roslyn 集成和 E4 分别验收,不打包成一次不可分割的大改。 - -## 2026-09-09:社区实践与第一性原理复核 - -**状态:** 用户要求复核此前建议、参考优秀社区经验。本节是对设计的修订建议,未实施生产架构或 E4 迁移。只核对当前代码、官方资料、社区作者的一手记录,并使用项目现有依赖执行隔离 SDK 探针。没有以帖子热度、工具数量或其他项目的性能宣传替代 WinCode 验收。 - -### 从需求推导必要部分 - -WinCode 要交付的是:在明确的项目配置与源码版本内,确定查询指向哪个符号,返回可核对的声明/引用,并说明未覆盖的范围。三个必要条件是编译上下文正确、符号身份明确、证据没有混用版本;“去掉 Serena”“统一 JSON”是服务这一目标的手段。性能比较还应先保证任务正确完成,再比较冷/热耗时、调用次数、输出量和资源,不用减少依赖层数推导一定更快。 - -据此保留直接 Roslyn 库 + 随产品交付的 WinCode.Code.Host。当前 Gateway 是 Node,现有 UIA Host 是带桌面状态的一次请求进程,独立 C# 工作区生命周期有具体用途。WinCode 只承担加载、查询、生命周期和证据输出;类型解析、重载匹配、引用查找仍交给 Roslyn。减少 Serena/Python 的部署环节会把项目加载、版本兼容和故障恢复的维护责任转给 WinCode,不等于维护成本归零,也不保证完整语义超越同样使用 Roslyn 的 Serena。 - -### 采纳的社区经验及适用边界 - -| 一手资料 | 可采纳经验 | WinCode 的处理意见 | +| 方向 | 进入条件与最小实验 | 验收边界 | | --- | --- | --- | -| [csharp-ls 项目说明](https://github.com/razzmatazz/csharp-language-server) | 使用 Roslyn 实现语言服务;诊断分析器可单独关闭,并说明开启的 CPU/延迟成本 | 复用编译器能力,分开查询所需语义、诊断分析器和源生成器的职责;不因要找引用就默认运行全部诊断扩展 | -| [RoslynMcp 作者实测](https://github.com/MadQ/RoslynMcp/blob/dev/docs/battle-test-results.md) | 作者记录了冷启动负担、简单名称搜索的低成本,以及只取指定方法可能漏看邻近代码问题的案例 | 文本检索/文件浏览与语义查询互补;不强制所有查询先加载完整 Solution;精确引用附有界上下文,不把精准片段说成完整任务覆盖。该文为作者测试,性能数值不移植到 WinCode | -| [RoslynMcp 工作区模式](https://github.com/MadQ/RoslynMcp/blob/dev/docs/reference/WORKSPACE_MODES.md) | 自建源码工作区缺少项目配置、NuGet 与项目引用等上下文 | 源码扫描可以给语法证据,不能冒充真实项目语义;缺依赖时说明缺口,不能靠换成 AdhocWorkspace 获得“完整”结果 | -| [共享工作区设计稿](https://github.com/MadQ/RoslynMcp/blob/dev/docs/plans/multi-instance-architecture.md) | 讨论多个客户端重复加载工作区的成本;页面明确仍是后续设计 | 首轮仅在一个 Gateway 内复用一个工作区;没有 WinCode 多进程重复加载的实测需求前,不照搬 named pipe 守护服务、共享缓存或跨客户端资源系统 | -| [MCP SDK 问题 #654](https://github.com/modelcontextprotocol/typescript-sdk/issues/654) 与[已合并修复 #655](https://github.com/modelcontextprotocol/typescript-sdk/pull/655) | 成功输出校验曾遮蔽工具原本的失败信息;修复选择对工具错误跳过该校验 | 错误应完整到达调用者;区分协议文字、当前 SDK 行为及真实宿主行为,不从其中一层推断所有客户端兼容 | - -这些是可检查的实现经验,不构成社区共识或推荐安装上述产品。库/API 的行为另由[微软 Workspace 模型](https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/work-with-workspace)、[符号位置查询 API](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.findsymbols.symbolfinder.findsymbolatpositionasync?view=roslyn-dotnet-4.13.0)及前述 MSBuild 文档核对。 - -### 原方案需要纠正或收缩的部分 - -1. **先证明加载与引用闭环,再冻结公共身份协议。** 前案把 symbolId/snapshotId 及映射表提前列入首版公共接口;其必要性还没有原型证据。首个 Host 原型建议只使用内部的项目上下文、Document、声明标识符的 UTF-16 位置及当前 Solution 代次取得 ISymbol。路径/行号本身不够:同一文件可在多个项目配置中编译,同一行也可有多个重载。这个内部定位方式需验证后才能决定对外短期句柄或位置参数;不把跨编辑永久 ID、独立符号注册系统或新的公共字段作为原型前置条件。必要的快照代次、请求关联、取消和帧边界仍保留。 -2. **不能原样复用现有监听作为语义正确性保证。** 当前 `src/Core/WorkspaceWatch.ts` 忽略 obj/bin,并在默认 150 ms 防抖结束后才调用无路径参数的 onChange。前案同时要求 assets 变化重载,二者不一致。将来接入时,应在相关变更到达即标记语义状态待更新,只对重载防抖;按实际加载输入识别 project.assets.json、生成源码、imports、项目配置和 Compile 文件集合的变化,避免简单去掉所有忽略项引入输出目录事件风暴。文件新增/删除、未知文件名事件、监听失败或无法确认来源的新旧状态,不能当作“无变更”。 -3. **不透明 ID 和文件哈希不等于完整性。** Roslyn Solution 的不可变模型可避免查询内部混用逻辑快照,但不能证明磁盘始终没变化。仅复核返回的文件,会漏掉“另一个新文件新增了引用”这种负面证据失效。引用范围的文件集合与加载输入也属于待验证上下文;旧快照必须注明范围/代次,不能宣称磁盘实时完整。源码生成缺失、加载诊断和范围缺口继续显式报告,不能因为 provider=roslyn 就提高 confidence。这里指出的是待实现设计的缺口,未声称已复现一个尚不存在的 RoslynAdapter 故障。 -4. **项目执行边界保留,但先在原型中证明。** MSBuild 设计时构建会执行 targets;“不主动 build/restore”或“关闭诊断分析器”均不等于禁止项目代码执行。诊断分析器与为编译贡献源码的生成器也不能混为一谈。首个已授权生成夹具使用不依赖外部生成器的明确配置,并核对加载副作用;对真实项目是否允许设计时求值及生成器的政策仍待用户决定,不预建复杂信任管理系统。对不支持的生成来源如实标记缺口,不以手工拼装引用弥补后宣称完整。 -5. **E4 的默认过渡建议需要调整。** “原文本不变 + 新 structuredContent”只有已知消费者确实依赖旧文本时才有明确价值;不能为假设中的旧客户端永久保留两套表达。仓内检查发现多数测试客户端从第一个 text 块 JSON.parse,未找到已知工具必须保留 `Tool Execution Error:` 前缀的消费分支;未知工具另有文本断言,仍单独保持。此调查不证明所有外部客户端都兼容。推荐终态为同一个错误对象生成一份 JSON 文本及可选 structuredContent;只读 content 的调用者也能看到错误码,errorMessage 保留可读解释。是否直接迁移还是短期保留旧文本,由真实兼容要求决定,仍是公共契约待决事项。 -6. **撤回“以后必须先有成功/失败联合 schema”的过强推断。** 当前 client/server 2.0.0 的 Client + 项目所用低层 Server,在有成功 outputSchema 时,isError=true 的错误无 structuredContent 或携带不同形状均能原样收到;成功结果的缺失/不匹配仍被拒绝。局部实测 5/5,通过[探针脚本](test-tmp/review-20260909/e4-sdk-output-schema.mjs)与[回执](test-tmp/review-20260909/e4-sdk-output-schema-report.json)可复核。因此 E4 不必绑定全工具成功输出重构。原来“仅失败对象的 schema 不能覆盖成功结果”仍成立;也不能把本机 SDK 的错误豁免说成所有宿主的保证。[2025-11-25 工具规范](https://modelcontextprotocol.io/specification/2025-11-25/server/tools)有结构化内容的 JSON 文本镜像建议;实际协商版本及宿主展示仍需针对部署验收。 - -E4 建议进一步精简:新增公共事实优先限于 errorCode、errorMessage,以及有明确定义时的恢复动作;既有 success、recoverable、workspaceRecovery、trash outcome/实际路径按原含义保留。provider=wincode、统一 retryable 和新通用成功包装都不作为必加字段。领域对象已说明实际发生什么时,不重复制造一个可能相互矛盾的恢复结论;未知普通错误不猜测可自动重试。即使请求失败,已移动的文件也不能重移,已变更的工作区也不能忽略恢复状态。 - -### 修订后的进入顺序与验收 - -1. **最小独立 Host 验证。** 拟在隔离的两项目、单配置/TFM 夹具完成“加载 → 定位具体重载 → 跨项目找引用 → 返回精确位置与少量上下文”。同名干扰、合法空结果和缺失引用必须表现不同;测冷/热耗时、资源、超时和关闭。使用声明位置及人工定义的调用点为真值,避免只与 Serena 比较。这个阶段不迁移公共参数、不切默认提供方;仍需按已确认方向对具体实现及依赖选择对齐。 -2. **一致性与现有入口接入。** 原型证明后,再确定所需身份/source 字段并接到现有 CodeQueries/ToolRouter;覆盖编辑后立即查询、obj/assets 改动、新文件新增引用、A→B→A、旧定位失效、取消/崩溃与 E1 清理失败。文本浏览继续可用,不创建另一套通用插件层。公共范围标记及完整性必须与实际支持的项目配置相符。 -3. **独立迁移 E4 与发布验收。** E4 可与 Host 分别实施,不强制先完成全工具重构。按选定的文本策略跑现有失败样例、部分完成样例和真实目标客户端;最后核对完整交付包、必要 BuildHost 文件和无 Serena/Python 环境。是否完成由可运行证据决定,设计稿、SDK 探针、历史 337 项回归均不替代直接 Roslyn 功能验收。 - -**USER_DECISION_REQUIRED:** 直接集成方向已确认;本文未替用户批准首版具体项目执行政策、公共身份/source 迁移、E4 文本兼容取舍。最新推荐是先做上述小型 Host 验证,E4 以单一错误事实和可见 JSON 为目标,旧文本兼容仅在实际需要时短期保留。本次复核未安装依赖、运行真实项目求值、修改生产代码、切换客户端或提交远端。 - -## 2026-09-09:第一阶段 Host 原型已实现 - -用户同意按复核方向开始。本阶段实现并验收自有 C# Host 的最小引用闭环;没有将它切为 Gateway 默认后端,E4 公共错误迁移也尚未实施。此前的“未新增 Host/依赖”为当时状态,当前进展以本节为准。 - -- 实现位于 [Program.cs](tools/WinCode.Code.Host/Program.cs),启动参数显式要求允许项目求值、工作区根、入口 csproj、Configuration 和单一 TargetFramework。一次加载后复用 Solution;引用查询使用指定项目中的文档和 UTF-16 偏移,返回准确源码 span、一基行/列和有界上下文。内部 JSON 行协议尚不作为稳定公共 API。 -- Roslyn 库固定 5.9.0、Build.Locator 1.11.2;Framework 17.11.48 只作编译引用,设置 ExcludeAssets=runtime/PrivateAssets=all,避免与 Locator 加载的 MSBuild 冲突。依赖通过 [packages.lock.json](tools/WinCode.Code.Host/packages.lock.json)锁定,使用现有项目内 SDK 10.0.303 和 NuGet 路径;没有安装全局 SDK。回执记录 22 个锁定包及其声明的 MIT 许可;本次构建目录为 112 文件、26,859,048 字节,含必要辅助文件,但不是最终发布包或新增磁盘占用的测量。 -- 复现入口:`npm run test:roslyn-host`,对应[验收脚本](scripts/verify-roslyn-host.mjs)。脚本仅生成 test-tmp 两项目夹具、还原夹具依赖和构建 Host;不运行 Serena、目标应用或真实用户项目。当前入口要求已具备 `.deps/dotnet-10.0.303`,不是面向任意新机器的安装器。 -- 最新[回执](test-tmp/roslyn-host/fixture-4E9UyF/report.json):18 场景通过。覆盖显式许可缺失、两项目加载、重载/同名类型隔离、精确引用位置、合法零引用、热查询、截断、旧快照、根外文件、无效位置/项目/预算、1 ms 冷查询取消及后续可用、缺失依赖、不生成目标编译文件、关闭/EOF。Build 0 警告、0 错误。最后一次冷就绪约 2.83 秒,后续有效查询 382 ms,热查询低于毫秒整数计时分辨率,工作集约 125.6 MB;这是单个小夹具样本,不是性能承诺。 -- 资源检查按 PID 和创建时间核对已观测进程退出;本次采样捕获 Host 与 conhost,未捕获 BuildHost,因此不声称已经验证全部短寿命辅助进程。进程树硬回收和真正 Gateway 取消/切换仍属于接入验收。 -- 按用户新增要求,所有新增 C# 函数及主要 JS 验收函数已补充中文 XML/JSDoc 注释;协议注释覆盖必填字段、UTF-16 坐标单位、返回值、失败行为、超时及快照生命周期。后续新增函数和接口沿用此要求,注释应解释契约及非显然约束。 - -**验收边界:** 这是固定语义快照原型,没有 watcher、重载或磁盘新鲜度保证,响应明确 diskFreshnessVerified=false。编译前移除 AnalyzerReference,以防引用查询间接执行生成器;当前夹具排除了 12 个 SDK 分析器/生成器引用,完整性因而保守标为 false。源生成覆盖、配置扩展、真实项目执行政策和根外导入不作为本阶段已完成能力;自定义 MSBuild targets 仍是用户批准执行的项目代码,源码路径校验不构成执行沙盒。取消为协作超时,返回 limit 不约束 Roslyn 内部搜索内存。 - -下一阶段先处理变化失效与接入生命周期,再定公共定位/source 字段并连接 CodeQueries/ToolRouter;E4 可独立迁移。完整发布清单、默认后端切换及真正无 Serena 环境验收尚未完成,不把本阶段 18 项结果替代这些工作。 - -## 2026-09-09:Host 输入一致性、重载与取消已实现 - -用户同意继续后,本次完成独立 Host 的变化失效与请求生命周期。该进展更新上一节的固定快照限制;公共定位/source、Gateway 提供方和 E4 保持尚未迁移的状态。 - -- 新增 [WorkspaceInputs.cs](tools/WinCode.Code.Host/WorkspaceInputs.cs) 与 [WorkspaceSession.cs](tools/WinCode.Code.Host/WorkspaceSession.cs):查询前后检查输入文件集合与内容,包括源码增删改名、obj/assets、csproj、祖先常规配置及实际加载的文档/元数据;文档文本在编译前固定。监听事件直接推进代次,不依赖现有 Gateway watcher 的 150 ms 防抖。结果只能描述检查点覆盖范围,不能证明任意自定义 targets 的外部输入或整个磁盘原子一致。 -- 内部协议升级 v2:显式 reload 生成新快照,开始重载后失败保持失效;不自动运行第二次业务请求。MSBuild 返回部分项目而未抛异常时,按 WorkspaceDiagnosticKind.Failure 返回 PROJECT_LOAD_FAILED;源码编译错误仍可随不完整引用保留。global.json 变化、监听或清理失败要求重启 Host。 -- [Program.cs](tools/WinCode.Code.Host/Program.cs) 分离输入控制与串行工作队列:最多等待 8 项、重复活动 id 拒绝、预算从接纳时开始、主动 cancel、shutdown/EOF 取消并排空后清理。排队时已到期的 reload 在改动状态前退出,旧快照仍可用;已开始重载后失败不恢复旧身份。取消仍为协作机制,生产进程树硬回收未实现。 -- 输入预算为 20000 个枚举条目、5000 个文件、总计 128 MiB、单文件 32 MiB;超限拒绝,不生成部分指纹。freshness 明确覆盖范围;diskFreshnessVerified=false、externalCustomInputsVerified=false、queryComplete=false 保留。排除生成器的限制也保留,不以准确的现有调用位置证明全局覆盖。 -- 最终[验收回执](test-tmp/roslyn-host/fixture-09LFFy/report.json) 42 场景通过,锁定构建 0 警告/0 错误。覆盖编辑后立即查询、新文件、重命名/删除、assets 和真实条件编译变化、Compile 排除、损坏项目失败与修复、过期身份、队列超时/冲突/背压、主动取消、活动请求 EOF 清理及 SDK 变更重启要求。突发 19 帧得到 8 个成功、9 个 BUSY、1 个重复 id 和 1 个取消,全部有回执。 -- 本次单夹具冷就绪约 4.06 秒、有效首查 484 ms、热查 136 ms、工作集 184123392 字节;初始跟踪 195 文件/6163683 字节。热查询现在包含前后内容校验,不能用此前无校验的亚毫秒样本作同口径性能比较。构建输出仍为 112 文件,26891268 字节,不是最终发布包测量;仅对采样到的 Host/conhost 验证退出,BuildHost 未观测。 -- 中文函数/接口注释和仓内 [Skill](skills/wincode/SKILL.md)、代码及诊断手册已同步;手册校验、现有接口/同步测试 11/11、脚本语法与 diff 检查通过。未找到已安装 wincode Skill 的受查目标,没有创建全局安装或声称当前客户端已更新。 - -后续仍需完成 RoslynAdapter/CodeQueries/ToolRouter 接入、实际 A→B→A 切换与崩溃/硬取消回收、公共身份/source 契约、E4 及完整交付和无 Serena/Python 环境验收。监听溢出与 Dispose 异常的真实注入、任意外部 targets 输入、非当前单配置和生成器覆盖未验证。此前核心 337/337 未在本次重跑,独立 Host 的 42 项不能代替生产入口验收。 - -## 2026-09-09 13:46:Roslyn 接入现有 MCP 与生命周期验收完成(北京时间) - -用户同意开始后,完成公共定位契约、RoslynAdapter/CodeQueries/ToolRouter 接入与真实 MCP 生命周期验证。上节“尚未接入/未硬回收”为当时状态;本节是当前进展。保持 15 个工具名,默认配置仍使用 Serena,只有显式 Roslyn 配置才启用新路径。 - -- 新增 [RoslynAdapter](src/Adapters/RoslynAdapter.ts) 和 [RoslynHostClient](src/Adapters/RoslynHostClient.ts),连接自有 Code Host。启动时通过 `--roslyn-config` 指定绝对 JSON 配置路径,显式给出项目求值许可、根内入口 csproj、Configuration、单一 TFM、已有 dotnet 和 Host 路径;配置示例见 [Skill 代码手册](skills/wincode/references/code.md)。不自动读取仓内配置以获得执行许可,也不允许普通 MCP 查询改可执行路径。 -- `wincode_find_code_symbol` 返回 `source:"roslyn"` 与精确 `location={snapshotId,project,file,position}`;`wincode_find_references` 新增可选 `symbolLocation`,仍保留 `symbolName`。position 为零基 UTF-16 偏移,行/列仍为一基;同名/重载返回候选供选择,旧 Serena 序号身份明确拒绝,名字与位置不匹配不会被静默忽略。内部 v2 协议补充 symbols 操作,partial 去重且指定文件范围时返回该文件中的真实声明位置。 -- 两类查询均携带 `semanticContext`,说明加载快照、排除的分析器/生成器和有界输入检查点。ImpactAnalyzer 可使用已定位符号的真实引用,但保持 `queryComplete=false`、`UNCERTAIN/UNKNOWN`,不因来源为 Roslyn 推断完整。显式 TS/JS/Python 范围仍可使用现有本地文本能力,Roslyn 模式不启动或回退到 Serena。 -- 编辑后拒绝旧证据;下一次显式符号搜索才重载并产生新定位,不自动重放失败请求。`workspace_open` 同根重开及 A→B→A 均关闭旧 Host、失效旧身份。hello 只报告已知提供方/状态,不触发项目求值。清理失败保留 E1 的 `WORKSPACE_RECOVERY_REQUIRED/restart_gateway`,不能用重复打开掩盖失败。 -- 取消先发请求取消,超时或无响应时硬回收自有进程树;启动阶段未进入协议循环也能取消。Windows Host 在 MSBuild 初始化前进入自有 Job,Host 崩溃时由系统关闭 Job 清理其继承的子进程;这是资源所有权机制,不是任意项目代码的执行沙盒。实现依据 [Windows Job Objects](https://learn.microsoft.com/en-us/windows/win32/procthread/job-objects) 和 [扩展限制结构](https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-jobobject_extended_limit_information)。 -- 新增 [真实 MCP 验收脚本](scripts/verify-roslyn-gateway.mjs) 与 [契约回归](tests/roslyn-contracts.test.ts)。最终 `npm run test:roslyn-gateway` [回执](test-tmp/roslyn-gateway/run-nP7SpF/report.json) 13 场景通过,覆盖真实重载/引用、编辑、切换、加载失败修复、实际 MSBuild 执行中的取消/崩溃/超时及最终关闭。后三类各捕获 7 个自有进程,包含 Host、BuildHost、测试 target 的 cmd/node 和控制台,按 PID/创建时间确认退出;这次已实际观测 BuildHost,不再沿用此前未捕获的证明缺口。 -- 最终 `npm run check` [回执](test-tmp/check/2026-09-09T05-41-07-067Z-core/report.json) 12 阶段通过,344/344、0 失败/0 跳过;生产 stdio 的 15 工具契约和现有交付指纹匹配。该交付清单仍覆盖现有 Gateway/UIA Host/受管 Skill,不代表 Code Host 已纳入正式发布。独立 Host 在本轮早期另通过 42 场景([回执](test-tmp/roslyn-host/fixture-whR9f2/report.json));后续位置范围及接入修订以最终 13 场景和核心回归为证,未把中间结果重复计数。 -- 新函数、接口及生命周期约束配中文注释;仓内 Skill、代码与诊断手册同步完成。相关定向测试 18/18、Skill UTF-8 验证通过。实现期间发现并修复旧 Serena 调用多传一个 undefined 的兼容问题;进程验收初次误把 Gateway 自身控制台计入切换时必须退出的 Code Host 树,按真实父子关系修正后通过。失败回执和自审细节见工作日志,没有削弱 Code Host 子树的退出断言。 - -**剩余范围:** 本轮只在生成的 SDK 风格 C# 两项目、单入口/配置/TFM 夹具中验证;入口 ProjectReference 可达图不等于完整仓库、所有反向依赖或 `.sln`。生成器、任意外部 targets 输入、通过外部服务创建的进程、非 Windows 平台和真实用户项目未在本轮证明。此为作者自审,没有独立审核;新 stdio 测试连接也不是当前 Codex 连接。 - -**下一阶段:** 按已讨论方向分别推进 E4 公共错误迁移及 Code Host 正式交付,把 Roslyn/BuildHost、锁文件、协议和 Skill 纳入可核对的安装包,再完成无 Serena/Python 的干净环境与实际客户端验收。默认后端切换及旧运行依赖移除在这些证据齐备后处理;具体 E4 文本兼容策略和真实项目求值授权仍需按实际范围确认。本轮未引入新依赖、修改全局环境/客户端配置或提交推送。 - -## 2026-09-09:Pro 最新分析对照复核与下一轮工作计划(北京时间) - -本节更新上一节的执行顺序,状态为**已完成复核与规划,代码修复尚未开始**。依据为用户提供的 Pro 分析全文、当前源码、GitHub 一手记录,以及本轮生成夹具的实际结果。Pro 未在 Windows 完整运行项目;本轮补充验证也不是全配置验收或独立人工审核。 - -### 基线与证据范围 - -- GitHub [PR #30](https://github.com/linnnn89/WinCode/pull/30) 于 2026-09-09 13:56:43 合并,远端 main 为 `2235a4200c117a1c1389afce1fecd90c45908a73`。本轮早期本地是 `bbc20ff` 加工作区修改;收尾时已变为该 PR 的 head `a2d76f6`,原有提交准备记录保留。本轮没有执行提交、推送或分支切换。 -- 两次核对 ImpactAnalyzer、RefactorAssistant、Router、两个 Roslyn Adapter/Client、CodeTools、三个 Code Host 文件、check、delivery-manifest、package.json 与 CI,共 13 个文件的 Git blob;均与 Pro 基线相同。[比对回执](test-tmp/review-20260909/pro-baseline-comparison.json)记录初始状态及文件哈希;这不表示整份工作区与远端完全相同。后续实施先按远端合并基线建立工作分支并保留本地计划修改,不能盲目 reset 或重复合入 PR #30。 -- [实际 ImpactAnalyzer 探针](test-tmp/review-20260909/impact-identity-report.json)使用生产分析类和受控查询提供方,验证聚合逻辑;[真实 Roslyn/MCP 探针](test-tmp/review-20260909/roslyn-audit-ebBSx0/report.json)使用现有构建和项目内 SDK,只求值本轮生成的项目。两类证据不互相冒充。 -- [14:39 实际客户端观测](test-tmp/review-20260909/current-client-1439.json)确认 Codex 已连接 0.12.5、15 个工具,构建身份 verified,支持引用工具的 symbolLocation;当前提供方仍为 Serena 文本降级,Roslyn 未启用。旧“当前客户端 0.11.2”已经过时;新版身份核对已完成,实际客户端的 Roslyn 功能验收仍待完成。 - -### Pro 建议的取舍及新增发现 - -| 项目 | 本轮核对结果 | 处理意见 | -| --- | --- | --- | -| 影响分析的文件身份 | 已复现:`src/B/Service.cs` 及 `src/C/NewService.cs` 被误排除;两个目录中的 Handler.cs 合成一个组件;启动目录不同的绝对目标无法解析。affectedFiles 仍含全部四个引用文件,错误发生在组件摘要/目标解析 | 优先修复。完整规范路径负责身份,短名称只展示;有项目身份时保留项目维度,避免同一链接文件跨项目被误合并 | -| Roslyn 被称为文本降级、有限覆盖被称为中断 | 真实 Roslyn 查询正常返回后,RefactorAssistant 仍给出这两类错误说明;源码条件与 Pro 判断一致 | 优先修正消费逻辑。分开来源、执行状态和覆盖范围;不把 queryComplete 全改为 true,不由覆盖不足推导自动重试 | -| Roslyn 健康汇总遗漏 | Router 的聚合 lastAdapterError 未纳入 Roslyn;它已有独立状态 | 复用现有健康错误模型,补齐聚合,不另造监控层 | -| **新增:源码编码被改写** | 带 CodePage=1252 的 Café 类在真实 dotnet build 中 0 警告/错误;Host 搜 Café 得到零结果及错误字符诊断,搜 Caf 却返回错误名称。WorkspaceSession 冻结正文时强制 UTF-8 | 提前修复正确性。遵循项目/Roslyn 选定的编码冻结源码;不支持的编码明确失败,禁止静默替换字符再声称找到了精确符号 | -| 输入指纹范围和成本 | 新增无关 README 使旧身份 SNAPSHOT_STALE;无关 33 MiB bin 文件使符号查询 INPUT_BUDGET_EXCEEDED。当前扫描/保留所有非排除文件正文的代码与 Pro 描述一致 | 大文件阻断已是可用性问题,提前处理;重复 I/O 和瞬时内存成本仍需基准,不称为已证明的泄漏 | -| 真实验收与交付 | 真实 Roslyn 脚本未接入标准 CI;Code Host 不在正式清单内。默认 npm test 的 SDK 发现失败又说明本地专用路径和普通入口不一致 | 保留模拟协议测试,复用真实脚本补 CI;统一显式 SDK 选择并纳入 Code Host/BuildHost 身份与完整性校验 | -| 已选符号向组合工具传递 | find_references 已接受位置;影响分析/重构公共入口仍只有名称。内部唯一目标已会传位置 | 后续增加可选精确目标,保留字符串调用;属于接口扩展,不描述成整个 Roslyn 组合路径尚未接入 | -| 进程退出疑虑 | 真实 MSBuild 阻塞期间强制退出 Gateway、关闭客户端,两场景各观测 9 个相关进程,3 秒后均无残留,未靠额外清理才能通过 | 本轮未复现孤儿进程缺陷;保留回归场景,不据猜测重写生命周期管理 | - -### 架构判断与应控制的冗余 - -保留 MCP → ToolRouter/CodeQueries → RoslynAdapter → RoslynHostClient → 自有 C# Host → Roslyn。Node/C# 运行时边界、语义工作区生命周期和进程回收各有明确责任;现有 ToolRegistry、领域证据与 SymbolLocation 已具备基础能力。当前没有证据支持另造注册平台、全局语义图、数据库或公共状态机框架。 - -需要收敛的是接入遗留:以 serena 命名的中性查询依赖、仅为纯文本解析仍构造 SerenaAdapter、未使用的 `_queries` 构造参数。ArchitectureAnalyzer 当前主要输出项目文件声明图;注入查询接口不等于已经用 Roslyn 得到语义架构图。优先在相关变更内清理命名、提取现有纯函数和删除确认无用的注入,避免以“脱离 Serena”为由删除仍有价值的文本探索或兼容路径。 - -Gateway watcher 服务仓库/文本缓存,Host watcher 与指纹服务语义输入,包括 obj 和配置;它们职责不同,暂不机械合并。详细查询接口与旧数组接口并存则需要调用方盘点:旧无位置引用入口在 Roslyn 下可能只返回空数组,当前主要消费者已用详细结果,但不能把这种潜在误用风险当作已复现的现行业务漏报。 - -另有尚未实测的准入风险:Host 的 8 项队列不能约束在 Adapter 互斥锁外等待的请求数。先用 16–32 个受控并发请求记录排队、取消及恢复;只有确认缺口后,才在现有准入层增加明确上限,不新增队列服务,也不据静态代码宣称内存泄漏。 - -### GitHub 经验的具体用途 - -- Serena [#1718 维护者复核](https://github.com/oraios/serena/issues/1718#issuecomment-5033051492)缩小了原帖所称的失效范围;[讨论](https://github.com/oraios/serena/issues/1718#issuecomment-5032705578)强调在语言服务管理层处理同步。采纳“先复现具体调用、在工作区生命周期层集中维护新鲜度”,不把原帖标题当成全部查询都会过期的事实。 -- csharp-ls [#401](https://github.com/razzmatazz/csharp-language-server/issues/401)展示了只看项目版本会遗漏文档变化对依赖项目结果的影响。WinCode 优化指纹时必须覆盖源码集合及依赖变化,不能简单改为时间戳或单一版本号判断。 -- [VuDZ/RoslynMcpServer](https://github.com/VuDZ/RoslynMcpServer)区分文档编辑和项目图变化,值得借鉴;不能直接把磁盘新增 .cs 一律 AddDocument,否则会破坏 Compile 排除与条件配置。仍以 MSBuild/Roslyn 的实际项目语义为准。 -- [MadQ/RoslynMcp 的作者实测](https://github.com/MadQ/RoslynMcp/blob/dev/docs/battle-test-results.md)用于设计任务对照:同时看正确完成、冷/热耗时、调用和输出成本。其样本收益不能成为 WinCode 的性能承诺,普通文本搜索仍有适用场景。 - -### 下一轮三个里程碑 - -**里程碑 A:结果正确、输入可靠。** 建议先做 A1,再做 A2,分别保持可审查的变更范围。 - -- **A1:组合结果修补。** 修改 ImpactAnalyzer、RefactorAssistant 及 Roslyn 健康聚合相关位置。验收不同目录同名/后缀文件、不同启动目录、Windows 分隔符/大小写;同一份 affectedFiles 与 affectedComponents 一致。覆盖 Roslyn/Serena/文本、正常有限结果/截断/超时/取消,说明与实际状态一致,保留已有公共字段和恢复语义。 -- **A2:Host 源码及输入处理。** 先修编码,复用真实项目夹具覆盖 UTF-8 有无 BOM、UTF-16 与 CodePage=1252,比较编译器和查询所得符号及 UTF-16 位置。再将输入清单、指纹和冻结正文分开:正文保留给编译文档,其他必要输入采用有界流式摘要;以实际文档/引用、项目/导入/配置、assets 及新增源码发现规则界定覆盖。源码增删改名、Compile 排除、条件配置、监听异常及过期身份仍需正确失效。 -- **A2 的重要边界:** “不是 .cs”不等于“与编译无关”,自定义 targets 可能读取资源。实施前明确输入覆盖政策和无法验证的范围;不能简单忽略所有非 C# 文件、放大预算或只信 watcher。验收生成夹具中的无关 README/33 MiB 文件不再无谓失效或阻断查询,真实依赖变化仍可检测;必要输入超限依旧明确失败。完整增量索引及大规模性能改造后置。 - -**里程碑 B:持续验收与正式 Roslyn 交付。** - -- 复用 `test:roslyn-host`、`test:roslyn-gateway`,使已有 SDK 路径可显式传入并用于相关构建/夹具子进程;保留 global.json 和锁文件,不靠放宽版本或跳过测试消除 SDK 失败。至少一个 Windows CI 任务真实构建 Code Host 并完成语义闭环;Node 22/24 网关兼容矩阵保留,是否两组都跑完整真实套件按耗时决定。 -- 把 Code Host、Roslyn/BuildHost 运行依赖和协议/构建身份纳入对应交付清单,覆盖缺文件、错配版本、哈希不符和缺 SDK 的明确诊断。复用现有 manifest/build-info 机制;内部握手是否加字段在协议边界内评估,不引入通用插件安装器。 -- 在隔离解压目录验证中文/空格路径、启动目录不同、无 Serena/Python 情况下的真实符号与引用;记录仍需的目标 SDK/引用包。再在实际 Codex 连接显式启用 Roslyn,核对身份并跑代表性调用。干净目录 smoke 与真正新机器环境分别记录,不互相替代。 - -**里程碑 C:已经选中的符号贯穿操作。** - -- 影响分析和重构建议新增可选 `symbolLocation`,沿用引用工具身份结构和原 target/goal;不增加一组重复工具。位置与名称、项目、快照不一致时明确拒绝,过期后要求重新定位,不静默切成另一个同名目标。 -- 验收同名类、重载、多项目/链接源码、正常零引用、修改后旧位置失败,以及旧字符串客户端的原有行为。特别验证“搜索选中的 Save(string)”进入后续报告时仍是该重载。 -- 新增函数/接口沿用中文契约注释;同步仓内 Skill、code/diagnostics 手册、schema/契约测试及交付清单,再报告客户端实际加载状态。计划文档不提前把尚未实现的字段写成可调用接口。 - -**E4 与条件性后续:** E4 仍作为独立兼容迁移处理,不阻塞 A 的说明纠错。沿用已有建议:稳定错误码、明确 recoveryAction、成功与副作用结果保留,不新增含糊 retryable;JSON 文本/旧文本及 structuredContent 的具体组合仍待选择。完成 A–C 后,先测冷启动、热查询、单文件变化恢复、峰值内存及输入读取量,再决定增量优化;UI 只考虑一个固定 WPF 应用的 Click/Command 候选到精确符号,不由源码关联推断运行时 CanExecute 故障原因。 - -### 决策点、退出标准与本轮交付 - -**USER_DECISION_REQUIRED:** 当前请求授权复核与规划;上述新一轮代码修改尚未实施。建议首先实施 A1/A2;A2 输入覆盖政策须在实际改动前明确。B 推荐基础交付保留、Roslyn 为明确可选组件,默认切换放在验收之后;C 的可选参数为公共接口扩展。E4 文本兼容策略、实际用户项目/配置及其求值范围在进入对应工作包前确认,已有直接集成方向和项目内依赖授权不重复申请。 - -各里程碑独立验收,按修改点运行针对性测试并完成必要回归;不能用“有注入接口”“模拟 Host 通过”“已有 manifest matched”分别冒充语义能力、真实编译器验收或 Code Host 完整交付。反证重点是:精确位置仍可能来自错误解码;完整文件列表仍可能配有错误组件摘要;有界 Host 队列仍可能留下上游无界等待;构建身份正确也不证明当前客户端选择了 Roslyn。 - -本次只新增隔离探针/回执并修改既有计划、路线图和日志,没有修改生产代码、安装依赖、求值真实用户项目、改变客户端配置或执行外部发布。已完成探针清理,两个退出场景未见自有残留;344/344 是此前指定环境的成功回执,本轮不重新宣称全套通过,默认入口 SDK 发现失败保留在后续验收范围。 - -## 2026-09-09:第一阶段 A1/A2 实施与验收(北京时间) - -用户授权开始实施后,从更新后的 origin/main@2235a42 建立 `codex/roslyn-correctness`,保留上一轮三份计划文档的修改。用户随后明确选择“在编译相关输入之外允许显式补充文件”;该项不再待决。此节更新上一节“尚未修复”的状态,未实施 B/C、E4 或默认客户端迁移。 - -- **A1 已完成。** ImpactAnalyzer 以工作区根解析完整文件身份,按可用项目身份区分组件;展示名保留。同名/后缀文件不再误判内部引用,不同目录的 Handler.cs 不再合并;绝对/相对路径和 Windows 分隔符/大小写别名有回归。RefactorAssistant 保留 queryComplete=false 的覆盖限制,不把 Roslyn 称为文本降级或把有限结果称为中断;Roslyn 加载、查询、清理错误纳入已有 lastAdapterError。 -- **编码已修正。** WorkspaceSession 冻结正文时沿用 Roslyn/MSBuild 选定的编码和 BOM,验证 UTF-8 有无 BOM、UTF-16 BOM 和 CodePage=1252 下 Café 的声明及引用 UTF-16 位置;没有把源码改写成 UTF-8 文件。 -- **A2 已完成。** 约定编译输入、实际文档/AdditionalFiles/分析配置/程序集及祖先配置自动跟踪;非标准后缀导入和自定义数据通过可选 `additionalInputs` 补齐。数组最多 32 个根内相对文件,JSON 最长 4096 字符;拒绝重复、通配符、目录、越界和链接,缺失项明确失败。配置只通过显式启动 JSON 传入,切换后按新根解释,不增加 MCP 业务参数。ready 的 inputPolicy.version=1 及实际列表必须匹配,旧 Host 不能静默漏用补充配置。 -- **内容成本与边界。** 非加载的 README/视频/普通二进制不占输入字节预算;实际候选集和排除目录见[代码手册](skills/wincode/references/code.md)。保留 20000 个枚举条目、5000 个输入、128 MiB 总量和 32 MiB 单输入上限,必要/补充文件超限仍失败。元数据等流式散列,只有冻结编译文档时保留正文;未测完整性能收益,不宣称全磁盘覆盖、任意自定义依赖自动发现或无内存泄漏。 -- **重载边界。** 每次加载尝试前最多四个 50 ms 事件稳定观察窗,继续受请求取消预算约束;持续变化则失败。没有增加业务自动重试,加载后的指纹/配置事件检查仍在。真实 MSBuild Touch 在内容哈希不变时也会触发拒绝,不能把等待窗口当成跳过新鲜度校验。 -- **验收。** [核心回执](test-tmp/check/2026-09-09T07-20-21-008Z-core/report.json)350/350、0 失败/跳过,含类型检查、构建、生产 stdio 及现有交付验证。后续 Host 收敛补修由[58 场景回执](test-tmp/roslyn-host/fixture-sDJxDM/report.json)及[最终 MCP 16 场景](test-tmp/roslyn-gateway/run-fLDNDI/report.json)验证;覆盖实际非标准 Import 条件改变、缺失补充输入阻断/修复、AdditionalFiles、无关 33 MiB 文件、源码集合与 Compile 排除、编码、旧身份、切换及真实 MSBuild 取消/崩溃/超时清理。场景数量不跨套件累加。 -- **失败与限制保留。** Windows 文件名大小写首轮曾使唯一解析失败,已修复并复测。另一轮 Host 在恢复补充文件后拒绝发布快照,旧回执未区分内容变化和事件变化,不能断言唯一根因;八轮隔离恢复未再次复现。保留该失败,拆分诊断原因,加入有界事件收敛和真实求值期 Touch 反证后,58/16 终验通过;仍可能在持续写入时返回 INPUTS_CHANGED,需稳定输入后显式恢复。 - -中文函数/接口注释与仓内 Skill/代码/诊断手册已经同步。当前改动尚未提交或推送;未安装新依赖、求值真实用户项目、写入全局 Skill 或改变客户端配置。现有 delivery 清单仍不包含 Code Host,不能把 matched=true 当成 B 的正式交付完成。下一步建议实施 B,C 的公共可选定位参数、E4 文本策略和实际用户项目求值范围仍在对应阶段明确。 - -## 2026-09-09:B/C、外部 Serena 退役与职责拆分开始实施 - -用户已批准外部 Serena 完全退役、默认本地文本/显式 Roslyn、source=local-text 三项取舍。以上旧章节的待决状态由本节更新。实现和验证进展持续记录于 [工作日志](docs/codex_worklog.md),发布及当前客户端切换尚未实施。 - - -## 2026-09-09 B/C、退役与拆分验收更新 - -用户三项迁移选择均已实施,本地版本 0.13.0。B 的构建、完整 Code Host 交付身份、异地发布目录和真实 MCP 验收已通过;C 的精确重载连续分析及职责拆分已完成。核心 307、桌面 35、Host 58、MCP 19、混合负载 70 调用的回执与失败过程见 [工作记录末尾](docs/codex_worklog.md)。早期“默认 Serena”“Code Host 不在清单”“C 尚未实现”已被本节取代。 +| UI → 源码候选 → Roslyn | 消费端验收后,以一个固定 WPF 场景比较手动续查和候选续查的调用数、证据及定位正确性 | 保留 XAML/C# 哈希、位置、歧义和快照;静态声明不证明运行时 Binding/CanExecute | +| 等待与背压 | 实际出现等待堆积,或需要声明并发容量时,用 16–32 个受控请求测取消、等待上界和恢复 | Host 队列有界不等于整个入口有界;复用现有准入层,不凭推测重构 | +| 增量扫描与 Repo Map | 固定任务证明冷/热查询、变化恢复或未知文件定位是主要成本后,再比较读取量、正确率、时延和资源 | 不降低输入新鲜度、不忽略 Compile 排除、不预建大型索引或向量库 | +| 更完整的文本解析 | 真实源码持续暴露现有词法/声明限制,并影响任务完成时,收集最小反例后评估成熟 parser | 新依赖、支持语言和资源成本须先确认;不把局部修补宣传成完整语法分析 | -尚未完成:远端 CI 实际运行、发布/客户端启用;E4 文本兼容方案等待用户本次选择。不得用本地验收替代以上关口。实际客户端项目/配置及求值范围须明确后才启用 Roslyn。 +正式对照保持相同初始信息与任务,字符数不当作实际 token,不将不同模型/提示词的耗时归因于工具。 +## 4. 持续关口 -2026-09-09 发布开发快照更新:用户确认可直接采用 E4 方案二后,基础实现已开始;随后要求先将当前状态上传 GitHub。本次保存所有相关源码/测试/文档,以草稿 PR 交付,不将 E4 或整体发布判为完成。 +- 每个版本先针对性验证和 debug,再执行完整 check、必要的桌面/真实 Roslyn 验收,经过 PR 检查后合并。保留失败证据,不用旧成功覆盖新失败。 +- Node 22 CI 执行 E4 专项与真实 Roslyn,Node 22/24 执行核心与交付检查;对应提交的有效 CodeQL 语言均须通过。当前 default-setup 已移除无源码 Python,核对新运行而非为历史红叉添加假源码。 +- 当前历史中文文件指纹测试曾偶发失败,后续复现时保留现场,区分测试竞争与真实失效漏洞;不靠删测试或延长 sleep 掩盖问题。 +- 版本、Host、仓内 Skill 与完整磁盘交付必须一致。短时有界负载不等于长期耐久性,异地发布目录不等于干净机器安装,客户端暂缓期间保留“消费者尚未验收”的限制。 diff --git "a/WinCode-\346\236\266\346\236\204\344\270\216\346\225\260\346\215\256\346\265\201\350\257\264\346\230\216.md" "b/WinCode-\346\236\266\346\236\204\344\270\216\346\225\260\346\215\256\346\265\201\350\257\264\346\230\216.md" index ec79676..99d320c 100644 --- "a/WinCode-\346\236\266\346\236\204\344\270\216\346\225\260\346\215\256\346\265\201\350\257\264\346\230\216.md" +++ "b/WinCode-\346\236\266\346\236\204\344\270\216\346\225\260\346\215\256\346\265\201\350\257\264\346\230\216.md" @@ -1,6 +1,6 @@ # WinCode 架构、数据流与检查关口 -**本地源码:0.13.0,基于 main@2235a42;结构更新日期:2026-09-09(北京时间)。未发布。** +**源码契约:0.13.1;基于已合并的 main@a23740c,结构更新日期:2026-09-09(北京时间)。交付验收见工作记录。** 本说明描述当前源码中已实现的结构。GitHub 分支保护的历史只读核查日期为 2026-09-08,本轮未重新查询远端;历史实测结果见[工作记录](docs/codex_worklog.md)。源码版本、磁盘构建和客户端当前连接是三个不同对象,不能互相替代。 @@ -235,17 +235,19 @@ flowchart LR 维护时仍应认识以下边界: 1. **ToolRouter 同时承担装配、状态和生命周期协调。** 当前职责集中且可定位;扩展功能应走既有用例与契约,不继续把具体上游访问塞进 Gateway。 -2. **结果协议有工具族差异。** UI 使用 success/errorCode 等字段,代码结果侧重 source/completeness,部分 Gateway 错误仍为文本;目前不能宣称全软件已有单一错误信封。 +2. **结果协议有工具族差异。** UI 使用 success/errorCode 等字段,代码结果侧重 source/completeness,Gateway 失败由同一对象生成 JSON 文本与 structuredContent;UI/trash 保留领域形状,未知工具走协议错误,不能把所有结果说成同一信封。 3. **检查是分路径落实的。** 范围读取、候选 mapper、目录扫描和 trash 各自设边界;不能把某条路径的检查推广到所有低层文件调用。 4. **生成计划与执行修改分开。** 重构工具提供建议与检查清单;代码修改、编译、Git 提交与 PR 操作由外部工程协作工具执行。trash 是需要特别识别的实际文件写入口。 5. **运行时一致性仍需客户端参与。** Gateway 实例身份、原生 Host 身份与交付清单提供核对依据,但系统没有自动替客户端重连旧 MCP 实例的能力。 本说明的架构图、数据表与关口表共同描述当前实现;新增功能应说明接入哪条数据流、使用哪个现有契约、在哪个关口拒绝或降级,以及如何留下真实验收证据。 -下一轮可靠性工作见[待实施计划](WinCode-下一轮工程化迭代计划书.md):工作区切换后续步骤失败的一致性、trash 移动后元数据失败的部分完成语义、有界混合负载验收,以及错误契约渐进整理。前两项来自静态调用链审查,仍需故障注入确认;后两项是验证和一致性改进,不能据此断言当前已有泄漏或必须整体重构。 +工作区失败恢复、trash 部分完成、有界负载和基础错误迁移已落实;当前待办见[计划](WinCode-下一轮工程化迭代计划书.md)。实际客户端 Roslyn 验收由用户明确暂缓;条件性性能研究不表示已发现泄漏。 ## 2026-09-09 职责拆分 WorkspaceManager 保留可变根、Git 与回收站事务;WorkspaceBrowser 和 ProjectDiscovery 负责只读发现。LocalTextAdapter 委托 LocalTextScanner 与 TextDeclarations;CacheManager 委托 WorkspaceFingerprint(文本缓存提示,不冒充语义快照)。ContextManager 拆出符号收集及格式化方法,ContextResponse 委托纯范围覆盖计算。UIA Host 将 Win32、窗口解析、抓图、树读取及 DTO 分离;FlaUiAdapter 的协议解析与自有进程调度分离。ToolRouter 的工作区锁、排空与恢复状态继续集中,避免把同一事务拆成多个状态源。 验收脚本共享 SDK 选择及进程观察函数;Host 场景分为语义/队列与输入变化模块,Gateway 将真实 MSBuild 生命周期故障独立。两份历史混合大测试按功能拆成 13 个套件,各自拥有缓存目录。 + +0.13.1 的 TextDeclarations 在声明匹配前使用 CSharpLexicalMask/ScriptLexicalMask,前者与 UiCodeMapper 共用;未闭合/不支持词法结构令本地扫描不完整且不缓存。TSX/JSX 的 scoped context 使用同一规则。影响报告只保留一份 JSON,E4 领域/恢复专项进入 Node 22 CI。 diff --git "a/WinCode-\350\277\255\344\273\243\350\267\257\347\272\277\345\233\276.md" "b/WinCode-\350\277\255\344\273\243\350\267\257\347\272\277\345\233\276.md" index 95ff9bc..ffa7e41 100644 --- "a/WinCode-\350\277\255\344\273\243\350\267\257\347\272\277\345\233\276.md" +++ "b/WinCode-\350\277\255\344\273\243\350\267\257\347\272\277\345\233\276.md" @@ -1,57 +1,17 @@ # WinCode 迭代路线图 -更新日期:2026-09-09(北京时间)。本地契约 0.13.0,工作分支 codex/roslyn-correctness,尚未提交/推送。A1/A2、外部 Serena 退役、默认 local-text、职责拆分及 C 已完成本地实现;B 的构建/清单、异地发布目录与真实 Host/MCP 验收已通过,远端 CI 及实际客户端启用仍未验收。最新证据见工作记录末尾;早期章节保留为历史。 +更新:2026-09-09(北京时间)。实现版本 **0.13.1**;远端 CI/合并状态查看对应 PR,实际客户端 Roslyn 验收按用户决定暂缓。 -本文件只保留未完成方向与进入条件。已完成的 R1–R6、WP1–WP5 不再作为待办重复执行;版本变更见 [CHANGELOG](CHANGELOG.md),过程与验收边界见 [工作记录](docs/codex_worklog.md)。 +详细方针和验收条件以[下一轮工程化迭代计划书](WinCode-下一轮工程化迭代计划书.md)为准;完成内容记在 [CHANGELOG](CHANGELOG.md)和[工作日志](docs/codex_worklog.md),不再作为待办重复实施。 -## 当前基线 +## Next:恢复消费端验收 -已落地工作区摘要、运行身份与契约核对、精准范围证据、统一参数校验、取消与资源回收、锁定构建及交付清单。未知字段保持容忍并忽略,规范字段见 [Skill](skills/wincode/SKILL.md);hello 读取已知状态,需要主动探测时使用 diagnose_project。 +先明确隔离 C# 项目的配置与 MSBuild 求值许可,再通过实际客户端完成版本核对、精确符号、引用、影响和旧快照拒绝闭环。当前只完成代码及可自动化的验收,不修改实际客户端或全局 Skill。 -0.12.4 的 Repomix 无 shell 启动修复已合并,GitHub 安全告警 #1 已自动标记 fixed;0.12.5 的 Serena/FastMCP 兼容修复已合并,固定 Serena 1.7.0/Roslyn 的七项隔离验收通过。主分支保护已启用,单维护者策略 approval=0,不能据此宣称独立审核已完成。 +## Later:由真实任务决定 -PR #30 之前的 0.12.5 主分支核心回归为 313 通过、1 项可选跳过,接入阶段回执更新为 344/344;这些都不等于所有真实应用或长时间运行场景均已验证。另一个提交准备流程的默认 `npm test` 因未发现锁定 SDK 10.0.303 失败,需统一已有 SDK 的选择与传递;本轮没有用历史成功覆盖该失败。历史 Yuki/TavernDesk 导航到源码验收已经完成,不再列为未开始;角色聊天等应用业务行为不属于该证据范围。 +- UI → 源码候选 → Roslyn:一个固定 WPF 场景验证定位正确性及续查成本。 +- 等待/性能:出现具体问题后测量冷、热查询、变化恢复和准入等待,再决定局部优化。 +- 更完整文本解析/Repo Map:收集现有能力不足的反例及成本,明确范围后设计;新增依赖单独确认。 -## 下一轮优先级 - -E1(工作区失败恢复)、E2(trash 部分完成)与 E3 有界验收已经完成,不重复列为待办。本次 Pro 与本地复核认为现有分层可继续使用,应先修具体逻辑和输入问题,再补交付、增加能力;完整证据、范围和决策点见计划书末尾。 - -| 顺序 | 未完成方向 | 进入与完成标准 | -| --- | --- | --- | -| 1 / B 剩余 | 远端 CI 与实际客户端验收 | 复用真实 Host/MCP 脚本,统一已有 SDK 选择,纳入 Windows CI;Code Host/BuildHost 的文件与构建身份可校验;隔离解压、无 Serena/Python、缺失/错配故障可验收,再完成实际客户端的 Roslyn 调用 | -| 已完成 / C | 精确目标贯通 | 影响分析与重构接受 symbolLocation;真实 MCP 验证重载及旧快照拒绝,仓内 Skill 同步 | -| 实施中 | E4 错误 JSON 迁移 | 用户已批准方案二;基础实现进入当前快照,专项回归及最终手册仍待完成。默认 local-text,Roslyn 显式配置 | - -具体工作包、验收与待决策略见 [下一轮工程化迭代计划书](WinCode-下一轮工程化迭代计划书.md)。现有分层见 [架构与数据流说明](WinCode-架构与数据流说明.md)。本地实现、针对性验证、完整交付和真实上游验收分别记录,不将其中一项替代其他关口。 - -## 阶段回顾与保持的边界 - -以下为同日实施过程;当前优先级以上表和计划书末尾为准。直接 Roslyn 的方向仍是随 WinCode 提供分析组件,无须用户另装 Serena/Python/独立语言服务器;这不消除加载目标项目所需 SDK、引用包等前置条件。E4 已讨论三种兼容方式的利弊,修订建议暂不增加统一 retryable 布尔值,不混改未知工具的协议层行为;仍待用户选择文本策略。 - -同日社区与第一性原理复核进一步收缩了原型范围,保留文本探索能力,后置公共符号句柄设计及跨客户端共享服务。现有监听忽略 obj 并防抖,不能原样作为语义状态失效保证。本机 MCP SDK 隔离探针 5/5 验证工具错误可绕过成功 outputSchema 校验;这不等于真实客户端兼容已验证。最新处理意见见计划书末尾复核节;没有新增生产依赖或切换提供方。 - -用户随后批准开始,现已新增 tools/WinCode.Code.Host、锁定 NuGet 依赖及 npm run test:roslyn-host。最小语义闭环和函数/协议注释已完成;固定快照、生成器排除、辅助进程采样等限制详见计划书最新实施节。尚未改变现有 Gateway 的 Serena 路径,E4 公共迁移也未落地。 - -继续实施后,Host 内部协议 v2 已补上源码/配置/obj 变化失效、reload/cancel、有界队列和活动请求关闭;仓内 Skill 同步更新。输入指纹只验证声明范围,任意外部 targets 与全磁盘原子一致仍不保证;生成器排除和未观测 BuildHost 的边界保留。最新 42 项证据与性能成本见计划书末尾;旧“没有 watcher/重载”的描述属于上一阶段历史,不再作为当前 Host 状态。 - -2026-09-09 13:46 接入阶段完成后,`--roslyn-config` 可显式启用 RoslynAdapter,现有 MCP 符号/引用、上下文和影响报告均能使用该路径;默认 Serena 配置仍保留。公共 `symbolLocation` 绑定快照/项目/文件/UTF-16 位置,失效后需重新搜索;source 不代替完整性或置信度。真实 MCP 验收已捕获并确认 BuildHost 及受控 target 子进程退出,更新上述历史采样限制。最终核心回归 344/344、stdio 与现有交付清单通过;仓内 Skill 和中文注释同步。Code Host 尚不在正式交付清单内,真实客户端与干净环境未验收,详见计划书最新实施节及工作日志。 - -## 尚需补齐的验收 - -- **实际客户端 Roslyn 验收**:2026-09-09 14:39 被动 hello 已确认 0.12.5、15 工具、构建 verified 及 symbolLocation 契约;旧 0.11.2 记录过时。当前仍选 Serena 文本降级,Roslyn 未启用。待 B 阶段显式配置并核对身份,再按确认的项目/配置执行代表性 Roslyn 调用,不把 stdio 或版本核对替代功能验收。 -- **已补齐的 A1/A2 回归**:同名路径、来源/覆盖、编码、无关大文件和补充输入已修复并纳入测试;补充文件缺失不能忽略,实际求值期间输入变化仍拒绝。Gateway 强制退出和客户端关闭的复核场景未见自有进程残留。上游等待队列与真实项目性能尚未实测,不作为已证实泄漏或必须重构的理由。 -- **更长时间或其他上游版本**:当前固定版本、有界样本已经通过,不等于全配置兼容或耐久性证明。只有出现实际需求或持续增长证据后再扩大预算;不作为 E3 原有有界验收的缺项。 -- **成本对照**:现有固定任务验证不等于 8–12 个真实任务的完整对照。只有决定继续优化检索成本时才补齐;比较正确完成率、调用数、输出量、重复取证和耗时,字符数不冒充 token。 - -## 条件性研究,不列入近期必做版本 - -| 方向 | 触发证据 | 最小实验及限制 | -| --- | --- | --- | -| R7:按需 MSBuild 求值 | Condition、imports、Directory.Build.* 或多 TFM 导致声明图与实际依赖出现可复现差异 | 保留快速声明图,实验按配置/TFM 求值并标注来源;不自动 restore/build,不隐式下载 SDK,不增加常驻服务 | -| R8:WPF 深层诊断 | 实际任务被 Binding、DataContext 或模板信息阻塞,现有 UIA/源码证据不足 | 在专用测试应用验证一个明确诊断问题及退出清理;应用内接入/注入是新路线,须另行确认 | -| R9:未知位置任务 Repo Map | 对照证明文件定位仍是主要成本 | 先用既有项目和符号关系验证有限排名收益;已知范围继续直达,不默认全仓预扫描 | - -研究入口沿用 [MSBuild 求值文档](https://learn.microsoft.com/en-us/visualstudio/msbuild/evaluate-items-and-properties?view=vs-2022)、[SnoopWPF](https://github.com/snoopwpf/snoopwpf)、[Aider Repo Map](https://aider.chat/docs/repomap.html)。它们是后续复核入口,不表示本轮已检索最新实现或完成集成。实施前固定上游版本,先证明收益再决定引入依赖。 - - -2026-09-09 发布开发快照更新:用户确认可直接采用 E4 方案二后,基础实现已开始;随后要求先将当前状态上传 GitHub。本次保存所有相关源码/测试/文档,以草稿 PR 交付,不将 E4 或整体发布判为完成。 +持续执行已确认的工程约束:未知字段容忍、hello 被动、显式 Roslyn、诚实的不完整结果、锁定交付、验证后 PR/合并。架构继续沿用[现有分层](WinCode-架构与数据流说明.md),不预建共享 Host、向量库或额外平台。 diff --git a/docs/codex_worklog.md b/docs/codex_worklog.md index b0a1943..7c0c6a8 100644 --- a/docs/codex_worklog.md +++ b/docs/codex_worklog.md @@ -733,3 +733,28 @@ - 本次验证:[错误契约 10 场景通过](../test-tmp/error-contracts/run-Daj6o7/report.json);[最新核心检查](../test-tmp/check/2026-09-09T09-11-00-925Z-core/report.json)的类型检查、Gateway 构建及 .NET 构建通过,核心回归 306/307。失败为 tests/resource-cleanup.test.ts 的中文文件原地修改指纹未变化,根因尚未定位;不以此前 307/307 覆盖此失败。此轮 check 在 regression 失败后停止,未执行后续 stdio 与清单阶段。 - E4 待完成:错误/恢复分支专项测试、当前 UI 错误双载荷的验收及完整手册核对。旧的桌面 35、Host 58、MCP 19 场景属于 E4 之前的成功基线,不表示该开发快照已全部复验。 - 上传范围为工作分支 codex/roslyn-correctness 的源码、测试、脚本、仓内文档与 CI;.deps、node_modules、dist、test-tmp、缓存继续忽略,不上传本地依赖或生成证据。Serena 专用目录仍未实际删除。main 未合并,实际客户端与全局 Skill 未改动。 + +## 2026-09-09 20:40 — 结合最新架构评估收敛下一轮计划(北京时间) + +- 用户要求读取“架构分析优化建议”最新分析并结合本地实际更新计划;已读取引用聊天,核对本地/远端 main@a23740c、当前声明解析/扫描、Gateway 错误/影响输出、CI、Skill 与既有验收记录。此次规划不修改生产代码、依赖、实际客户端或远端配置,不派生子代理。 +- 直接调用当前 parseTextDeclarations 做六个最小观察:C# `// class Ghost {}`、TS `const note = "class Ghost {}";`、Python 三引号内 `class Ghost:` 都错误返回 Ghost;真实 UserCard 函数在 .ts 返回声明,在 .tsx/.jsx 返回空。源码确认符号/引用扩展名集合不一致。将其列为 LocalText 优先修补,同时保留文本引用、复杂词法和降级完整性的边界。 +- 本轮实际执行 `npm run test:error-contracts`,10 场景通过;[报告](../test-tmp/error-contracts/run-uZMb3F/report.json)。它证明当前实现的已测分支,不证明标准一致性:unknown tool 的现有断言本身要求 isError,需按协议层修订。持续 CI 和恢复/领域分支仍待补齐。 +- 只读核对 [CI 34339009649](https://github.com/linnnn89/WinCode/actions/runs/34339009649):Node 22/24 成功,Node 22 真实 Host/MCP 步骤成功。CodeQL 34339009257 的三个有效语言成功、Python 历史 job 失败,失败注释明确 exit 32/没有 Python 源码;附带增量缓存提示不能替代该直接证据。 +- 与聊天结论的一处更新:GitHub default-setup 当前语言列表已排除 Python,updated_at=2026-09-09T10:13:02Z。将“修改 Python 配置”从待做事项撤下,改为下一次新扫描确认;本轮未改远端设置或触发扫描。当前仓库没有本地 CodeQL workflow,不为历史红叉另建一套流程。 +- 对照 MCP 2025-11-25 工具规范确认未知工具属于协议错误、业务输入值校验属于工具错误;保留已批准的 E4 方案二,不重新要求用户选择旧方案。影响报告去掉第二文本块与 unknown tool 的对外兼容调整列为实施前明确项,暂不扩展全工具响应抽象。 +- 重写根目录计划和路线图,只保留 0.13 修补方针、实际客户端验收及条件性 UI→Roslyn/性能研究。移除 E1–E3、A1/A2、B/C、Serena 退役和职责拆分的已完成步骤;所有历史实现/失败仍保留本日志与 Git,未把旧失败静默删成成功。 +- 本轮未执行完整核心、桌面或真实 Roslyn 套件,也未声称当前客户端已启用。仅用已有远端结果建立版本基线;计划保留隔离项目、求值范围与实际消费者验证关口。 +- 文档终验:两份计划的 15 个本地链接、围栏、版本和维护命令核对通过,git diff --check 通过。校验脚本首次按系统默认 GBK 读取含中文 JSON 失败;改为显式 UTF-8 后回执校验通过,未改全局环境。 + + +## 2026-09-09 21:18 — 0.13.1 LocalText 与 E4 稳定化实施(北京时间) + +- 用户要求按计划实施,沿用既有逐版本复测/debug、PR 与合并授权。针对客户端验收的询问,用户明确选择“先完成代码与 CI,客户端验收暂缓”;未修改真实 MCP 配置或已安装 Skill,未安装新依赖/SDK,未分出子代理。 +- LocalText:复用并提取现有 C# 非代码区屏蔽纯函数;增加有深度/取消边界的 JS/TS/Python 屏蔽。修复注释、字符串、模板、正则及 JSX 展示内容产生假声明;增加 TSX/JSX 扫描与指定文件符号取证。行号/展示签名保留原文;词法边界不确定时文件结果不完整且不缓存,旧 v1 声明缓存失效。插值/JSX 表达式省略,复杂语法仍可能漏检,不主张完整语法或精确引用。 +- E4:正常受理时未知工具走 SDK ProtocolError -32602;已知工具保留 isError/领域载荷,关闭/取消的入口优先级不变。专项由 10 扩展为 16 场景,覆盖真实隔离 trash metadata partial 及重试位置、工作区提交失败/阻断/恢复、注入 UI 失败与独立图片。后者只验证序列化,不冒充真实截图。Node 22 CI 新增专项及有界回执上传。 +- 影响输出:删除第二份 Markdown 文本,保留 JSON 中 formattedReport、证据字段和别名。固定 dotnet-mini/MemoryService 的同一结果文本从 3194 减至 2511 个 UTF-16 字符,减少 683;[量化回执](../test-tmp/impact-0131-size.json)。这不是实际 token 测量或跨任务性能结论。 +- RED/DEBUG 记录:各类 Ghost、TSX、未知工具和输出去重先复现失败后修复。第一次完整 check 的两个失败来自仍要求旧 stdio 结果的断言,按新契约迁移后通过;测试编写中的 evidence.content/impact.data 错误访问由类型/运行检查指出并更正。自审又复现代码块后的正则字面量泄漏,补充反例并修复后重跑完整检查。未删减生产边界或用旧成功覆盖失败。 +- 最终本地验收:[核心检查](../test-tmp/check/2026-09-09T13-15-21-646Z-core/report.json)318 项、317 通过、0 失败、1 跳过(未配置固定 TavernDesk 工作区的可选集成);覆盖类型、锁定构建、生产 stdio 与完整交付。独立[桌面夹具 35/35](../test-tmp/check/2026-09-09T13-09-20-368Z-desktop/report.json)、[真实 Host 58 场景](../test-tmp/roslyn-host/fixture-cB6uto/report.json)、[真实 MCP 19 场景](../test-tmp/roslyn-gateway/run-j1oS63/report.json)、[E4 16 场景](../test-tmp/error-contracts/run-6c5rEY/report.json)通过。后续仅手册/换行整理,最终核心已重新构建核对交付;无实际客户端验收。 +- 文档:版本统一 0.13.1;README、CHANGELOG、SECURITY、架构/配置指南与仓内 Skill 对齐;计划仅保留暂缓的客户端关口和条件性后续工作。77 个变更文档本地链接、代码围栏核对通过。历史日志追加而非改写,生成证据仍在忽略目录内。 +- 作者反证自审覆盖旧缓存假阳性、词法不确定的假零结果、长字面量取消、未知工具后的连接可用、impact 字段/别名保持,以及部分移动后真实文件位置;不等同独立模型或人工审核。剩余边界:有限词法、文本引用、干净机器/长期耐久性及实际消费者均不扩大声明。 +- 发布流程:工作分支 codex/local-text-e4-stabilization;本地通过后推送本版本 PR,等待对应 head 的 Node 22/24、真实 Roslyn/E4 及有效 CodeQL 全部通过再合并。远端完成情况以 PR/Actions 回执为准,此条写入时尚未推送,不提前声明 CI 已绿。 diff --git a/package-lock.json b/package-lock.json index b3b1e04..efa8027 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wincode-mcp", - "version": "0.13.0", + "version": "0.13.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wincode-mcp", - "version": "0.13.0", + "version": "0.13.1", "license": "MIT", "dependencies": { "@modelcontextprotocol/client": "2.0.0", diff --git a/package.json b/package.json index b860a91..c6f88f5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wincode-mcp", - "version": "0.13.0", + "version": "0.13.1", "description": "Windows-first MCP gateway: .NET project graph, evidence-bounded context, honest change-impact, long-running process hygiene", "main": "dist/index.js", "type": "module", diff --git a/scripts/test-mcp-client.ts b/scripts/test-mcp-client.ts index 49bcb61..066526f 100644 --- a/scripts/test-mcp-client.ts +++ b/scripts/test-mcp-client.ts @@ -35,6 +35,8 @@ await server.start(); `); transport = new StdioClientTransport({ command: process.execPath, args: [bootstrap], cwd: root, stderr: 'pipe' }); await client.connect(transport); + await assert.rejects(client.callTool({ name: 'missing_tool', arguments: {} }), + (error: any) => error.code === -32602, 'unknown tool must travel through the stdio protocol-error channel'); const tools = (await client.listTools()).tools; const call = async (name: string, args: Record = {}) => { const result = await client.callTool({ name, arguments: args }); diff --git a/scripts/verify-error-contracts.ts b/scripts/verify-error-contracts.ts index 787d39a..cf5d3ad 100644 --- a/scripts/verify-error-contracts.ts +++ b/scripts/verify-error-contracts.ts @@ -1,4 +1,5 @@ import assert from 'node:assert/strict'; +import { mock } from 'node:test'; import fs from 'node:fs/promises'; import path from 'node:path'; import { Client, InMemoryTransport } from '@modelcontextprotocol/client'; @@ -36,7 +37,11 @@ async function observe(scenario: string, name: string, args: Record assert.equal(result.isError, true)); + await assert.rejects(client.callTool({ name: 'missing_tool', arguments: {} }), (error: any) => { + assert.equal(error.code, -32602); + observations.push({ scenario: 'unknown tool protocol error', code: error.code }); + return true; + }); await observe('invalid code arguments', 'wincode_find_code_symbol', { query: 5 }, result => assert.equal(result.isError, true)); await observe('outside workspace scope', 'wincode_prepare_context', { task: 'read', scopeFiles: ['../outside.cs'] }, result => assert.equal(result.isError, true)); await observe('invalid UI arguments before native access', 'wincode_ui_inspect', {}, (result, body) => { @@ -61,6 +66,50 @@ try { router.findCodeSymbols = async () => { throw new Error('inventory execution failure'); }; await observe('unclassified execution exception', 'wincode_find_code_symbol', { query: 'Same' }, result => assert.equal(result.isError, true)); } finally { router.findCodeSymbols = original; } + // 保留领域载荷,不用人工构造的统一错误对象替代真实移动/恢复行为。 + await fs.writeFile(path.join(root, 'Trash.cs'), 'class Trash {}'); + const write = fs.writeFile; + const metadataFault = mock.method(fs, 'writeFile', async (...args: Parameters) => { + if (String(args[0]).endsWith('.meta.json')) throw new Error('isolated metadata failure'); + return write(...args); + }); + let movedPath = ''; + try { + await observe('trash metadata partial failure', 'wincode_safe_move_to_trash', { filePath: 'Trash.cs' }, (result, body) => { + assert.equal(result.isError, true); assert.equal(body.errorCode, 'TRASH_METADATA_FAILED'); + assert.equal(body.outcome, 'partial'); assert.equal(body.failureStage, 'metadata'); + assert.equal(body.originalPath, path.join(root, 'Trash.cs')); movedPath = body.trashPath; + }); + } finally { metadataFault.mock.restore(); } + assert.equal(await fs.readFile(movedPath, 'utf8'), 'class Trash {}'); + await assert.rejects(fs.stat(path.join(root, 'Trash.cs')), { code: 'ENOENT' }); + await observe('trash retry does not move again', 'wincode_safe_move_to_trash', { filePath: 'Trash.cs' }, (result, body) => { + assert.equal(result.isError, true); assert.equal(body.outcome, 'not_moved'); assert.equal(body.trashPath, ''); + }); + const uiFault = mock.method(router, 'inspectUi', async () => ({ success: false, errorCode: 'CAPTURE_FAILED', + errorMessage: 'isolated capture failure', auditNotice: { fixture: true }, + screenshotPngBase64: 'ZmFrZQ==' }) as any); + try { + await observe('UI failure preserves metadata and separate image (injected adapter result)', 'wincode_ui_inspect', { pid: 123 }, (result, body) => { + assert.equal(result.isError, true); assert.equal(body.errorCode, 'CAPTURE_FAILED'); + assert.equal(body.hasScreenshot, true); assert.deepEqual(body.auditNotice, { fixture: true }); + assert.equal(body.screenshotPngBase64, undefined); assert.equal(result.content[1].type, 'image'); + }); + } finally { uiFault.mock.restore(); } + const nextRoot = path.join(root, 'next'); + await fs.mkdir(nextRoot); + const switchFault = mock.method(router.text, 'initialize', async () => { throw new Error('isolated rebind failure'); }); + try { + await observe('workspace commit failure', 'workspace_open', { path: nextRoot }, (result, body) => { + assert.equal(result.isError, true); assert.equal(body.errorCode, 'WORKSPACE_RECOVERY_REQUIRED'); + assert.equal(body.recoveryAction, 'workspace_open'); assert.equal(body.workspaceRecovery.recoveryAction, 'workspace_open'); + }); + } finally { switchFault.mock.restore(); } + await observe('recovery state blocks subsequent business calls', 'wincode_find_code_symbol', { query: 'Same' }, (result, body) => { + assert.equal(result.isError, true); assert.equal(body.errorCode, 'WORKSPACE_RECOVERY_REQUIRED'); + assert.equal(body.workspaceRecovery.recoveryAction, body.recoveryAction); + }); + await observe('explicit workspace recovery', 'workspace_open', { path: root }, result => assert.notEqual(result.isError, true)); await router.dispose(); await observe('shutdown rejection before admission', 'wincode_find_code_symbol', { query: 'Same' }, (result, body) => { assert.equal(result.isError, true); assert.equal(body.errorCode, 'SHUTDOWN'); assert.equal(body.recoveryAction, 'restart_gateway'); diff --git a/skills/wincode/SKILL.md b/skills/wincode/SKILL.md index f8a8d1a..cf8c344 100644 --- a/skills/wincode/SKILL.md +++ b/skills/wincode/SKILL.md @@ -5,7 +5,7 @@ description: 使用 WinCode MCP 分析 Windows/.NET 工作区,或读取桌面 # WinCode -源码契约:0.13.0(含 E4 开发中错误 JSON 迁移,状态见诊断手册);手册修订:2026-09-09(本地待发布)。外部 Serena 入口与旧 source 已退役,不能将此版本号当作当前连接已升级。安装内容可用 `node scripts/sync-skill.mjs <安装目录绝对路径>` 核对;仅维护时执行。以当前连接实际 Schema 为准。 +源码契约:0.13.1(LocalText 词法边界与 E4 错误契约,见对应手册);手册修订:2026-09-09。外部 Serena 入口与旧 source 已退役,不能将此版本号当作当前连接已升级。安装内容可用 `node scripts/sync-skill.mjs <安装目录绝对路径>` 核对;仅维护时执行。以当前连接实际 Schema 为准。 默认以本地文本模式启动,source=local-text;明确配置 Roslyn 后,才通过 WinCode.Code.Host 提供 C# 语义证据。搜索返回的 location 可作为引用、影响分析和重构工具的 symbolLocation;不要猜测定位、复用旧快照或使用已退役的 namePath。内部 reload/cancel 不是 MCP 工具字段。配置与验收边界见代码手册。 diff --git a/skills/wincode/references/code.md b/skills/wincode/references/code.md index cd8f673..395a1ff 100644 --- a/skills/wincode/references/code.md +++ b/skills/wincode/references/code.md @@ -2,7 +2,11 @@ 以下为 MCP 工具名和参数;以客户端实际 Schema 为准。 -## 后端与实验接口边界 +0.13.1 的本地声明扫描覆盖 .cs/.ts/.tsx/.js/.jsx/.py,屏蔽注释、字符串以及整个 JSX 元素(含其中的表达式),签名与行号仍来自原文。无法可靠定界、未闭合或嵌套超限的文件标记 lexical-uncertainty,queryComplete=false 且不缓存完整空结果;这不是完整语法解析,复杂声明可能省略。引用搜索仍为文本线索,不提供编译器语义或精确身份。 + +`analyze_change_impact` 及其别名返回一个 JSON 文本块,formattedReport 保留在对象中,不再返回第二份重复 Markdown。没有新增 responseFormat 参数,不要给该工具传 context 专用的格式字段。 + +## 后端与能力边界 默认 Gateway 使用 WinCode 内置文本能力,`source=local-text`,健康状态明确 semanticConfigured=false。显式启用 Roslyn 后使用直接 Code Host,失败会报错,不会偷偷改换提供方。外部 Serena 连接配置、启动器及旧 `serena-adapter-fallback` 来源已退役;旧调用方须适配。`hello.codeProvider` 标明实例选择,不能根据仓库中存在 Host 推断当前连接已更新。 diff --git a/skills/wincode/references/diagnostics.md b/skills/wincode/references/diagnostics.md index 5562de7..8b86413 100644 --- a/skills/wincode/references/diagnostics.md +++ b/skills/wincode/references/diagnostics.md @@ -38,23 +38,19 @@ pwsh -NoProfile -File "/scripts/check-ui-audit.ps1" WORKSPACE_RECOVERY_REQUIRED 表示切换中途失败后工作区一致性尚未确认。此时业务工具被拒绝;被动 hello 仍可读取 health.workspaceRecovery,status=recovery_required。先检查 recoveryAction:workspace_open 表示可按原任务指定路径重新打开,只有完整重置/初始化及 watcher 绑定成功才恢复请求;同一路径也执行完整恢复。restart_gateway 表示清理失败被当前实例保留,重新打开无法恢复;先检查 Gateway 自有资源的清理情况,再按客户端正常流程重启 Gateway,不自动重启或终止目标应用。永久失败后的 workspace_open 不再反复改变根或会话。不要只修改路径字段、反复重试业务请求或把旧适配器状态当成已切换成功。CANCELLED 若附带 workspaceRecovery,同样按其 recoveryAction 处理;切换变更前失败且状态未改变时仍保留旧工作区。 -E4 统一错误表达尚未实施:当前可能收到 isError=true 的纯文本,也可能是 content 中的 JSON;不能要求所有失败都含 structuredContent、统一 recoveryAction 或 retryable。先保留 isError 和原始内容,只在实际存在时读取 errorCode、workspaceRecovery、trash outcome/实际位置。结构化字段缺失不等于成功,取消或失败也不代表副作用已回滚;部分完成不原样重试。JSON 文本与 structuredContent 同源的方案是后续迁移方向,不能套用到旧连接。 +0.13.1 中,已知工具执行失败的 JSON 文本与 structuredContent 同源;Gateway 异常含 success=false、errorCode、errorMessage、provider 和 recoveryAction。UI/trash 保留领域字段及实际位置,不要求所有领域错误具有 Gateway 字段;图片保持独立 image 块。未知工具在正常受理状态下返回 JSON-RPC -32602 协议错误,不返回 isError 结果;关闭/取消的入口拒绝优先于工具查找。旧连接不能套用此契约,先核对实际版本。恢复动作不表示已经回滚或允许原样重试。 -直接 Roslyn Host 与 UIA Host 是不同组件。新 Gateway 的 hello.codeProvider 和 health.roslyn 报告显式选择的提供方、已知观察、processAlive、snapshotId 及重载/重启/清理状态;hello 不启动 Roslyn 或执行项目,进程存活不等于当前磁盘语义已验证。ready 是内部握手帧,UIA 的 VERSION_MISMATCH、inspectionVersion 等不能套到 Code Host。当前 npm run check / delivery:verify 不替代 test:roslyn-host/test:roslyn-gateway,也不证明 Code Host 已纳入正式发布包。 +直接 Roslyn Host 与 UIA Host 是不同组件。新 Gateway 的 hello.codeProvider 和 health.roslyn 报告显式选择的提供方、已知观察、processAlive、snapshotId 及重载/重启/清理状态;hello 不启动 Roslyn 或执行项目,进程存活不等于当前磁盘语义已验证。ready 是内部握手帧,UIA 的 VERSION_MISMATCH、inspectionVersion 等不能套到 Code Host。当前 npm run check / delivery:verify 不替代 test:roslyn-host/test:roslyn-gateway;当前交付清单已覆盖 Code Host 完整发布目录,但不证明实际客户端已启用 Roslyn。 Roslyn 运行中已观察到的加载、查询或清理错误也纳入 health.lastAdapterError,provider=roslyn;health.roslyn.health.lastError 保留对应观察。lastError 是历史最后一次失败,不表示每次 hello 都执行了健康探测,也不能据此自行重放业务请求。工作区完整重置后观察清空。 Code Host 内部协议 v2 的失败包含 success=false、errorCode 和 error,且不附带旧引用。SNAPSHOT_STALE/INPUTS_CHANGED 要求等写入稳定后显式 reload,再用新身份定位;PROJECT_LOAD_FAILED 表示结构化 MSBuild 加载失败,先修复项目输入,再 reload,不能继续使用最后一次成功快照。源码的 compilationErrors 可随有用的部分引用返回,不能据此宣称完整。 -Roslyn 的已知领域错误通过 MCP 的 isError=true 和 JSON 文本 success=false/errorCode/errorMessage 返回,不代表 E4 已覆盖所有工具。HOST_RESTART_REQUIRED(SDK/监听状态)应对当前路径执行 workspace_open,再显式搜索;同根打开也关闭旧 Host 后重新选择 SDK。清理失败则按 WORKSPACE_RECOVERY_REQUIRED 的 restart_gateway 处理,不能通过再次打开恢复。HOST_TIMEOUT/HOST_CRASHED 后旧定位不可用,下一次显式搜索才启动新 Host;不会重放失败引用。 +Roslyn 的已知领域错误通过 MCP 的 isError=true 和 JSON 文本 success=false/errorCode/errorMessage 返回;失败的 JSON 文本与 structuredContent 一致,仍保留领域差异。HOST_RESTART_REQUIRED(SDK/监听状态)应对当前路径执行 workspace_open,再显式搜索;同根打开也关闭旧 Host 后重新选择 SDK。清理失败则按 WORKSPACE_RECOVERY_REQUIRED 的 restart_gateway 处理,不能通过再次打开恢复。HOST_TIMEOUT/HOST_CRASHED 后旧定位不可用,下一次显式搜索才启动新 Host;不会重放失败引用。 INPUT_UNAVAILABLE/HOST_UNAVAILABLE 先检查明确的配置文件、SDK/Host/项目路径,以及 additionalInputs 中的文件是否存在;补充文件缺失时,重载也会失败,恢复文件后再显式搜索。不要为恢复查询而静默移除真实构建输入。HOST_VERSION_MISMATCH 先核对 Code Host 与 Gateway 的版本、Release 配置和协议;不要继续使用混合交付。HOST_PROTOCOL_ERROR 同时检查协议 v2、inputPolicy.version=1 和实际补充列表;旧 Host 没有确认新策略时不能绕过。LEGACY_SYMBOL_ID 要求重新搜索 Roslyn 身份;UNSUPPORTED_SYMBOL_LOCATION 表示该实例未配置 Roslyn;SYMBOL_MISMATCH 表示名称和定位不一致。INPUT_BUDGET_EXCEEDED 区分枚举规模与受跟踪输入字节限制,先缩小受支持范围,不能接受截断指纹。内部 BUSY 表示队列已满,DUPLICATE_REQUEST 要求新的 id;CANCELLED 是目标终止结果,取消确认不替代它。OUTSIDE_WORKSPACE/UNSUPPORTED_LINK 拒绝越界或链接路径,不放松校验来恢复。 维护接口变更时,同步检查 Gateway 工具定义、相应 references 手册、实际客户端 Schema 和已安装四份受管文件;更新源码手册后运行 skill:sync,再以 skill:check 校验。仍须单独确认 MCP 实例的版本/构建/Schema,不能用手册同步代替重连。公共接口尚未发布时,只记录实验边界,不提前把新参数加入 MCP 规范字段表。 -## 2026-09-09 E4 当前开发快照 - -用户已确认尚未广泛分发,可直接迁移到方案二。Gateway 普通错误已改为 JSON 文本并同步 structuredContent,使用稳定 errorCode、errorMessage、provider 和 recoveryAction;原文本前缀不再是兼容接口。UI/trash 保留领域结果字段并附同内容结构化载荷,尤其 partial 仍表示文件已经移动,不自动重试或移回。成功响应不在本次迁移范围。 - -这是未发布、未完成专项验收的开发状态;恢复动作只表示先处理的步骤,不授予执行、安装或自动重试权限。完整错误码/恢复状态矩阵、E4 专项回归和最终手册核对尚待完成,当前连接是否已更新须查看运行身份。 +`npm run test:error-contracts` 使用生成夹具验证错误、部分完成与恢复;Node 22 CI 执行该专项并保存有界报告。UI 图片场景使用注入响应,只验证序列化,不冒充真实屏幕验收。 diff --git a/src/Adapters/LocalTextAdapter.ts b/src/Adapters/LocalTextAdapter.ts index ea10e79..e24079f 100644 --- a/src/Adapters/LocalTextAdapter.ts +++ b/src/Adapters/LocalTextAdapter.ts @@ -42,13 +42,13 @@ export class LocalTextAdapter { async findSymbolsDetailed(query: string, kind?: string, relativePath?: string, operation?: OperationContext): Promise { checkOperation(operation); const fingerprint = await this.cache.computeWorkspaceFingerprint(this.config.workspaceRoot); - const key = `local_text_symbols_v1_${JSON.stringify([query, kind, relativePath, this.config.workspaceRoot])}`; + const key = `local_text_symbols_v2_${JSON.stringify([query, kind, relativePath, this.config.workspaceRoot])}`; const cached = await this.cache.get(key, fingerprint); checkOperation(operation); if (cached?.queryComplete) return cached; const scan = await scanLocalFiles(this.config.workspaceRoot, this.config.timeouts.fileScanMs, - ['.cs', '.ts', '.js', '.py'], 500, - (content, file, extension) => parseTextDeclarations(content, file, extension).filter(symbol => + ['.cs', '.ts', '.tsx', '.js', '.jsx', '.py'], 500, + (content, file, extension) => parseTextDeclarations(content, file, extension, () => checkOperation(operation)).filter(symbol => symbol.name.toLowerCase().includes(query.toLowerCase()) && (!kind || symbol.kind.toLowerCase() === kind.toLowerCase())), relativePath, operation); const stats = computeTypeMatchStats(scan.items, query); diff --git a/src/Core/CSharpLexicalMask.ts b/src/Core/CSharpLexicalMask.ts new file mode 100644 index 0000000..62b105f --- /dev/null +++ b/src/Core/CSharpLexicalMask.ts @@ -0,0 +1,89 @@ +/** 屏蔽 C# 注释与字符串/字符字面量;保持偏移和行号,无法可靠定界时返回 null。 */ +export function maskCSharpNonCode(source: string, checkpoint: () => void, maxDepth: number): string | null { + const output = source.split(''); + let steps = 0; + const tick = () => { if ((++steps & 1023) === 0) checkpoint(); }; + const mask = (start: number, end: number) => { + for (let index = start; index < end; index++) { + tick(); + if (output[index] !== '\n' && output[index] !== '\r') output[index] = ' '; + } + }; + const commentEnd = (index: number): number | undefined => { + if (source.startsWith('//', index)) { + const end = source.indexOf('\n', index + 2); return end < 0 ? source.length : end; + } + if (source.startsWith('/*', index)) { + const end = source.indexOf('*/', index + 2); return end < 0 ? -1 : end + 2; + } + return undefined; + }; + // The expression is skipped as text, including its own strings and comments, never evaluated. + const expressionEnd = (start: number, depth: number): number => { + if (depth > maxDepth) return -1; + let braces = 1; + for (let index = start; index < source.length;) { + tick(); + const comment = commentEnd(index); + if (comment !== undefined) { if (comment < 0) return -1; index = comment; continue; } + const literal = literalEnd(index, depth + 1); + if (literal !== undefined) { if (literal < 0) return -1; index = literal; continue; } + if (source[index] === '{') { braces++; if (depth + braces - 1 > maxDepth) return -1; } + if (source[index] === '}' && --braces === 0) return index + 1; + index++; + } + return -1; + }; + const literalEnd = (start: number, depth: number): number | undefined => { + let quoteIndex = start; + let verbatim = false; + let interpolated = false; + if (source.startsWith('$@"', start) || source.startsWith('@$"', start)) { + quoteIndex += 2; verbatim = true; interpolated = true; + } else if (source.startsWith('$"', start)) { quoteIndex++; interpolated = true; } + else if (source.startsWith('@"', start)) { quoteIndex++; verbatim = true; } + else if (source[start] !== '"' && source[start] !== "'") { + if (source[start] === '$' && /^\$+"{3,}/.test(source.slice(start))) return -1; + return undefined; + } + if (depth > maxDepth) return -1; + const quote = source[quoteIndex]; + const count = quote === '"' ? /^"+/.exec(source.slice(quoteIndex))![0].length : 1; + if (count >= 3) { + if (interpolated || verbatim) return -1; + const end = source.indexOf('"'.repeat(count), quoteIndex + count); + return end < 0 ? -1 : end + count; + } + for (let index = quoteIndex + 1; index < source.length;) { + tick(); + if (source[index] === quote) { + if (verbatim && source[index + 1] === quote) { index += 2; continue; } + return index + 1; + } + if (!verbatim && (source[index] === '\n' || source[index] === '\r')) return -1; + if (!verbatim && source[index] === '\\') { index += 2; continue; } + if (interpolated && source[index] === '{') { + if (source[index + 1] === '{') { index += 2; continue; } + const end = expressionEnd(index + 1, depth + 1); + if (end < 0) return -1; + index = end; continue; + } + if (interpolated && source[index] === '}') { + if (source[index + 1] !== '}') return -1; + index += 2; continue; + } + index++; + } + return -1; + }; + checkpoint(); + for (let index = 0; index < source.length; index++) { + tick(); + const end = commentEnd(index) ?? literalEnd(index, 0); + if (end !== undefined) { + if (end < 0) return null; + mask(index, end); index = end - 1; + } + } + return output.join(''); +} diff --git a/src/Core/CodeQueries.ts b/src/Core/CodeQueries.ts index 6a4c70b..73ee108 100644 --- a/src/Core/CodeQueries.ts +++ b/src/Core/CodeQueries.ts @@ -25,6 +25,7 @@ export class CodeQueryError extends Error { constructor(readonly errorCode: string, message: string) { super(message); this.name = 'CodeQueryError'; } } export const LOCAL_TEXT_LIMITATIONS: string[] = [ + '声明扫描屏蔽注释、字符串及 JSX 元素(含插值);复杂词法/语法不保证完整,无法可靠定界的文件标记 lexical-uncertainty。引用仍为文本线索。', '本地正则扫描仅作为文本检索降级方案,不保证符号身份、重载区分、跨文件引用完整性或安全重命名。', '本地正则扫描无法替代完整 Roslyn/TypeScript LSP 语义层面的跨文件重命名与重载解析。', '未找到引用不得直接解释为“无影响”或“低风险”。', diff --git a/src/Core/Config.ts b/src/Core/Config.ts index 9946de7..adc9974 100644 --- a/src/Core/Config.ts +++ b/src/Core/Config.ts @@ -1,6 +1,6 @@ import path from 'node:path'; -export const WINCODE_VERSION = '0.13.0'; +export const WINCODE_VERSION = '0.13.1'; /** * Bounded waits for every external process/RPC. None of these may be Infinity. diff --git a/src/Core/Context.ts b/src/Core/Context.ts index 0577302..a73b697 100644 --- a/src/Core/Context.ts +++ b/src/Core/Context.ts @@ -399,7 +399,7 @@ export class ContextManager { const stat = await fs.stat(full); if (!stat.isFile()) { fileIssues.push({ path: file, reason: 'not-file' }); continue; } if (stat.size >= 500_000) { fileIssues.push({ path: file, reason: 'file-too-large' }); continue; } - if (!['.cs', '.ts', '.js', '.py'].includes(path.extname(file).toLowerCase())) { + if (!['.cs', '.ts', '.tsx', '.js', '.jsx', '.py'].includes(path.extname(file).toLowerCase())) { fileIssues.push({ path: file, reason: 'unsupported-symbol-language' }); continue; } const source = await fs.readFile(full, { encoding: 'utf8', signal: operation?.signal }); @@ -571,6 +571,7 @@ export class ContextManager { } private readIssue(error: unknown): string { + if ((error as NodeJS.ErrnoException)?.code === 'TEXT_LEXICAL_UNCERTAINTY') return 'lexical-uncertainty'; if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') return 'not-found'; return error instanceof Error && error.message.includes('outside-workspace') ? 'outside-workspace' : 'unreadable'; } diff --git a/src/Core/LocalTextScanner.ts b/src/Core/LocalTextScanner.ts index afcb5da..c21a0b1 100644 --- a/src/Core/LocalTextScanner.ts +++ b/src/Core/LocalTextScanner.ts @@ -77,7 +77,10 @@ export async function scanLocalFiles(root: string, timeoutMs: number, extensi if (items.length >= maxResults) { mark('result-limit', true, true); break; } } } finally { await handle.close(); } - } catch (error) { rethrowOperationError(error, operation); mark('read-error'); } + } catch (error) { + rethrowOperationError(error, operation); + mark((error as NodeJS.ErrnoException)?.code === 'TEXT_LEXICAL_UNCERTAINTY' ? 'lexical-uncertainty' : 'read-error'); + } }; const walk = async (dir: string): Promise => { if (!canContinue()) return; diff --git a/src/Core/ScriptLexicalMask.ts b/src/Core/ScriptLexicalMask.ts new file mode 100644 index 0000000..86fd54f --- /dev/null +++ b/src/Core/ScriptLexicalMask.ts @@ -0,0 +1,142 @@ +/** 屏蔽 JS/TS 与 Python 非代码区;保留 UTF-16 偏移和换行,不提供语法/语义完整性。 */ +export function maskScriptNonCode(source: string, python: boolean, checkpoint: () => void, jsx = false): string | null { + const output = source.split(''); + let steps = 0; + const tick = () => { if ((++steps & 1023) === 0) checkpoint(); }; + const commentEnd = (start: number): number | undefined => { + if (python ? source[start] === '#' : source.startsWith('//', start)) { + const end = source.indexOf('\n', start); return end < 0 ? source.length : end; + } + if (!python && source.startsWith('/*', start)) { + const end = source.indexOf('*/', start + 2); return end < 0 ? -1 : end + 2; + } + return undefined; + }; + const regexEnd = (start: number): number => { + let inClass = false; + for (let i = start + 1; i < source.length; i++) { + tick(); + if (source[i] === '\\') { i++; continue; } + if (source[i] === '\n' || source[i] === '\r') return -1; + if (source[i] === '[') inClass = true; + if (source[i] === ']') inClass = false; + if (source[i] === '/' && !inClass) return i + 1; + } + return -1; + }; + // 插值表达式整体省略,包括其中的字符串;不把嵌套正文误当成外部声明。 + const expressionEnd = (start: number, depth: number): number => { + if (depth > 16) return -1; + let braces = 1; + let operand = true; + for (let i = start; i < source.length;) { + tick(); + const end = commentEnd(i) ?? literalEnd(i, depth + 1) ?? + (!python && source[i] === '/' && operand ? regexEnd(i) : undefined); + if (end !== undefined) { if (end < 0) return -1; i = end; operand = false; continue; } + const ch = source[i]; + if (ch === '{' && ++braces + depth > 16) return -1; + if (ch === '}' && --braces === 0) return i + 1; + if (!/\s/.test(ch)) operand = /[({[}=,:;!?&|+*%~<>-]/.test(ch); + i++; + } + return -1; + }; + const literalEnd = (start: number, depth: number): number | undefined => { + const quote = source[start]; + if (quote !== '"' && quote !== "'" && (python || quote !== '`')) return undefined; + if (depth > 16) return -1; + const triple = python && source.startsWith(quote.repeat(3), start); + const delimiter = quote.repeat(triple ? 3 : 1); + const formatted = python && /(?:^|[^\w])(?:f|fr|rf)$/i.test(source.slice(Math.max(0, start - 3), start)); + for (let i = start + delimiter.length; i < source.length;) { + tick(); + if (source[i] === '\\') { i += 2; continue; } + if (source.startsWith(delimiter, i)) return i + delimiter.length; + if (!triple && quote !== '`' && (source[i] === '\n' || source[i] === '\r')) return -1; + if ((!python && quote === '`' && source.startsWith('${', i)) || (formatted && source[i] === '{')) { + if (formatted && source[i + 1] === '{') { i += 2; continue; } + const end = expressionEnd(i + (python ? 1 : 2), depth + 1); + if (end < 0) return -1; + i = end; continue; + } + i++; + } + return -1; + }; + // JSX 整个元素是文本扫描的非声明区;包含其中的表达式,避免把展示内容当代码。 + const jsxEnd = (start: number, depth: number): number => { + const tags: string[] = []; + for (let i = start; i < source.length;) { + tick(); + if (depth + tags.length > 16) return -1; + if (source[i] === '{') { + const end = expressionEnd(i + 1, depth + tags.length + 1); + if (end < 0) return -1; + i = end; continue; + } + if (source[i] !== '<') { i++; continue; } + const closing = source[i + 1] === '/'; + i += closing ? 2 : 1; + const nameStart = i; + while (i < source.length && /[\w$:.\-]/.test(source[i])) { tick(); i++; } + const name = source.slice(nameStart, i); + if (!name && source[i] !== '>') return -1; + let last = ''; + for (; i < source.length && source[i] !== '>';) { + tick(); + const ch = source[i]; + if (ch === '"' || ch === "'") { + const end = source.indexOf(ch, i + 1); + if (end < 0) return -1; + i = end + 1; last = ch; continue; + } + if (ch === '{') { + const end = expressionEnd(i + 1, depth + tags.length + 1); + if (end < 0) return -1; + i = end; last = '}'; continue; + } + if (!/\s/.test(ch)) last = ch; + i++; + } + if (i >= source.length) return -1; + i++; + if (closing) { if (last || tags.pop() !== name) return -1; } + else if (last !== '/') tags.push(name); + if (!tags.length) return i; + } + return -1; + }; + let operand = true; + let control = false; + const parentheses: boolean[] = []; + checkpoint(); + for (let i = 0; i < source.length;) { + tick(); + const comment = commentEnd(i); + const end = comment ?? literalEnd(i, 0) ?? + (jsx && operand && source[i] === '<' && /[a-zA-Z_$>]/.test(source[i + 1] ?? '') ? jsxEnd(i, 0) : undefined) ?? (!python && source[i] === '/' && operand ? regexEnd(i) : undefined); + if (end !== undefined) { + if (end < 0) return null; + for (let j = i; j < end; j++) { tick(); if (source[j] !== '\n' && source[j] !== '\r') output[j] = ' '; } + i = end; + if (comment === undefined) operand = false; + continue; + } + if (/[a-zA-Z_$]/.test(source[i])) { + const start = i++; + while (i < source.length && /[\w$]/.test(source[i])) { tick(); i++; } + control = /^(if|while|for|with|switch|catch)$/.test(source.slice(start, i)); + operand = /^(return|throw|case|delete|void|typeof|new|in|of|instanceof|yield|await|else|do)$/.test(source.slice(start, i)); + continue; + } + if (source[i] === '(') { + if (parentheses.length >= 128) return null; + parentheses.push(control); control = false; operand = true; + } else if (source[i] === ')') operand = parentheses.pop() ?? false; + else if (!/\s/.test(source[i])) { control = false; operand = /[({[}=,:;!?&|+*%~<>-]/.test(source[i]); } + i++; + } + checkpoint(); + return output.join(''); +} diff --git a/src/Core/TextDeclarations.ts b/src/Core/TextDeclarations.ts index 7ae88fb..2364076 100644 --- a/src/Core/TextDeclarations.ts +++ b/src/Core/TextDeclarations.ts @@ -1,11 +1,22 @@ +import { maskScriptNonCode } from './ScriptLexicalMask.js'; +import { maskCSharpNonCode } from './CSharpLexicalMask.js'; import type { CodeSymbol } from './CodeQueries.js'; +export class TextLexicalError extends Error { + readonly code = 'TEXT_LEXICAL_UNCERTAINTY'; + constructor() { super('Cannot reliably delimit comments/literals; no declarations returned for this file.'); } +} + /** 解析已提供的正文,不读取磁盘、不启动进程;正则匹配不提供语义身份或完整引用保证。 */ -export function parseTextDeclarations(content: string, relPath: string, ext: string): CodeSymbol[] { +export function parseTextDeclarations(content: string, relPath: string, ext: string, checkpoint: () => void = () => {}): CodeSymbol[] { const symbols: CodeSymbol[] = []; - const lines = content.split(/\r?\n/); + const originalLines = content.split(/\r?\n/); + const masked = ext === '.cs' ? maskCSharpNonCode(content, checkpoint, 16) : maskScriptNonCode(content, ext === '.py', checkpoint, ext === '.tsx' || ext === '.jsx'); + if (masked === null) throw new TextLexicalError(); + const lines = masked.split(/\r?\n/); for (let i = 0; i < lines.length; i++) { + checkpoint(); const line = lines[i]; const trimmed = line.trim(); const lineNum = i + 1; @@ -14,76 +25,76 @@ export function parseTextDeclarations(content: string, relPath: string, ext: str if (ext === '.cs') { const classMatch = trimmed.match(/(?:public|private|protected|internal)?\s*(?:static|abstract|sealed|partial)?\s*class\s+([A-Za-z0-9_]+)/); if (classMatch) { - symbols.push({ name: classMatch[1], kind: 'class', file: relPath, line: lineNum, signature: trimmed }); + symbols.push({ name: classMatch[1], kind: 'class', file: relPath, line: lineNum, signature: originalLines[i].trim() }); continue; } const structMatch = trimmed.match(/(?:public|private|protected|internal)?\s*(?:readonly|ref)?\s*struct\s+([A-Za-z0-9_]+)/); if (structMatch) { - symbols.push({ name: structMatch[1], kind: 'struct', file: relPath, line: lineNum, signature: trimmed }); + symbols.push({ name: structMatch[1], kind: 'struct', file: relPath, line: lineNum, signature: originalLines[i].trim() }); continue; } const enumMatch = trimmed.match(/(?:public|private|protected|internal)?\s*enum\s+([A-Za-z0-9_]+)/); if (enumMatch) { - symbols.push({ name: enumMatch[1], kind: 'enum', file: relPath, line: lineNum, signature: trimmed }); + symbols.push({ name: enumMatch[1], kind: 'enum', file: relPath, line: lineNum, signature: originalLines[i].trim() }); continue; } const interfaceMatch = trimmed.match(/(?:public|private|protected|internal)?\s*interface\s+([A-Za-z0-9_]+)/); if (interfaceMatch) { - symbols.push({ name: interfaceMatch[1], kind: 'interface', file: relPath, line: lineNum, signature: trimmed }); + symbols.push({ name: interfaceMatch[1], kind: 'interface', file: relPath, line: lineNum, signature: originalLines[i].trim() }); continue; } const methodMatch = trimmed.match(/(?:public|private|protected|internal)\s+(?:async\s+)?(?:static\s+|virtual\s+|override\s+|sealed\s+)?([A-Za-z0-9_<>?, \[\]]+)\s+([A-Za-z0-9_]+)\s*\(/); if (methodMatch && !['if', 'for', 'while', 'switch', 'using', 'catch'].includes(methodMatch[2])) { - symbols.push({ name: methodMatch[2], kind: 'method', file: relPath, line: lineNum, signature: trimmed }); + symbols.push({ name: methodMatch[2], kind: 'method', file: relPath, line: lineNum, signature: originalLines[i].trim() }); continue; } } // TypeScript / JavaScript symbol patterns - if (ext === '.ts' || ext === '.js') { + if (ext === '.ts' || ext === '.js' || ext === '.tsx' || ext === '.jsx') { const classMatch = trimmed.match(/(?:export\s+)?(?:abstract\s+)?class\s+([A-Za-z0-9_]+)/); if (classMatch) { - symbols.push({ name: classMatch[1], kind: 'class', file: relPath, line: lineNum, signature: trimmed }); + symbols.push({ name: classMatch[1], kind: 'class', file: relPath, line: lineNum, signature: originalLines[i].trim() }); continue; } const interfaceMatch = trimmed.match(/(?:export\s+)?interface\s+([A-Za-z0-9_]+)/); if (interfaceMatch) { - symbols.push({ name: interfaceMatch[1], kind: 'interface', file: relPath, line: lineNum, signature: trimmed }); + symbols.push({ name: interfaceMatch[1], kind: 'interface', file: relPath, line: lineNum, signature: originalLines[i].trim() }); continue; } const enumMatch = trimmed.match(/(?:export\s+)?(?:const\s+)?enum\s+([A-Za-z0-9_]+)/); if (enumMatch) { - symbols.push({ name: enumMatch[1], kind: 'enum', file: relPath, line: lineNum, signature: trimmed }); + symbols.push({ name: enumMatch[1], kind: 'enum', file: relPath, line: lineNum, signature: originalLines[i].trim() }); continue; } const typeMatch = trimmed.match(/(?:export\s+)?type\s+([A-Za-z0-9_]+)\s*=/); if (typeMatch) { - symbols.push({ name: typeMatch[1], kind: 'type', file: relPath, line: lineNum, signature: trimmed }); + symbols.push({ name: typeMatch[1], kind: 'type', file: relPath, line: lineNum, signature: originalLines[i].trim() }); continue; } const funcMatch = trimmed.match(/(?:export\s+)?(?:async\s+)?function\s+([A-Za-z0-9_]+)\s*\(/); if (funcMatch) { - symbols.push({ name: funcMatch[1], kind: 'function', file: relPath, line: lineNum, signature: trimmed }); + symbols.push({ name: funcMatch[1], kind: 'function', file: relPath, line: lineNum, signature: originalLines[i].trim() }); continue; } const arrowFuncMatch = trimmed.match(/(?:export\s+)?const\s+([A-Za-z0-9_]+)\s*=\s*(?:async\s*)?\([^)]*\)\s*(?::\s*[^=]+)?\s*=>/); if (arrowFuncMatch) { - symbols.push({ name: arrowFuncMatch[1], kind: 'function', file: relPath, line: lineNum, signature: trimmed }); + symbols.push({ name: arrowFuncMatch[1], kind: 'function', file: relPath, line: lineNum, signature: originalLines[i].trim() }); continue; } const classMethodMatch = trimmed.match(/^(?:public|private|protected)?\s*(?:static\s+)?(?:async\s+)?([A-Za-z0-9_]+)\s*\([^)]*\)\s*(?::\s*[^;{]+)?\s*\{/); if (classMethodMatch && !['if', 'for', 'while', 'switch', 'constructor', 'catch'].includes(classMethodMatch[1])) { - symbols.push({ name: classMethodMatch[1], kind: 'method', file: relPath, line: lineNum, signature: trimmed }); + symbols.push({ name: classMethodMatch[1], kind: 'method', file: relPath, line: lineNum, signature: originalLines[i].trim() }); continue; } } @@ -92,13 +103,13 @@ export function parseTextDeclarations(content: string, relPath: string, ext: str if (ext === '.py') { const classMatch = trimmed.match(/^class\s+([A-Za-z0-9_]+)/); if (classMatch) { - symbols.push({ name: classMatch[1], kind: 'class', file: relPath, line: lineNum, signature: trimmed }); + symbols.push({ name: classMatch[1], kind: 'class', file: relPath, line: lineNum, signature: originalLines[i].trim() }); continue; } const defMatch = trimmed.match(/^(?:async\s+)?def\s+([A-Za-z0-9_]+)\s*\(/); if (defMatch) { - symbols.push({ name: defMatch[1], kind: 'function', file: relPath, line: lineNum, signature: trimmed }); + symbols.push({ name: defMatch[1], kind: 'function', file: relPath, line: lineNum, signature: originalLines[i].trim() }); continue; } } diff --git a/src/Core/UiCodeMapper.ts b/src/Core/UiCodeMapper.ts index 20190e1..69fb004 100644 --- a/src/Core/UiCodeMapper.ts +++ b/src/Core/UiCodeMapper.ts @@ -1,3 +1,4 @@ +import { maskCSharpNonCode } from './CSharpLexicalMask.js'; import fs from 'node:fs/promises'; import path from 'node:path'; import { createHash } from 'node:crypto'; @@ -50,96 +51,6 @@ export function validateCandidateCodeFiles(value: unknown): asserts value is str const CSHARP_KEYWORDS = new Set(('abstract as base bool break byte case catch char checked class const continue decimal default delegate do double else enum event explicit extern false finally fixed float for foreach goto if implicit in int interface internal is lock long namespace new null object operator out override params private protected public readonly ref return sbyte sealed short sizeof stackalloc static string struct switch this throw true try typeof uint ulong unchecked unsafe ushort using virtual void volatile while').split(' ')); -/** Mask comments and string/character literals without changing offsets or line numbers. */ -function maskNonCode(source: string, checkpoint: () => void, maxDepth: number): string | null { - const output = source.split(''); - let steps = 0; - const tick = () => { if ((++steps & 1023) === 0) checkpoint(); }; - const mask = (start: number, end: number) => { - for (let index = start; index < end; index++) { - tick(); - if (output[index] !== '\n' && output[index] !== '\r') output[index] = ' '; - } - }; - const commentEnd = (index: number): number | undefined => { - if (source.startsWith('//', index)) { - const end = source.indexOf('\n', index + 2); return end < 0 ? source.length : end; - } - if (source.startsWith('/*', index)) { - const end = source.indexOf('*/', index + 2); return end < 0 ? -1 : end + 2; - } - return undefined; - }; - // The expression is skipped as text, including its own strings and comments, never evaluated. - const expressionEnd = (start: number, depth: number): number => { - if (depth > maxDepth) return -1; - let braces = 1; - for (let index = start; index < source.length;) { - tick(); - const comment = commentEnd(index); - if (comment !== undefined) { if (comment < 0) return -1; index = comment; continue; } - const literal = literalEnd(index, depth + 1); - if (literal !== undefined) { if (literal < 0) return -1; index = literal; continue; } - if (source[index] === '{') { braces++; if (depth + braces - 1 > maxDepth) return -1; } - if (source[index] === '}' && --braces === 0) return index + 1; - index++; - } - return -1; - }; - const literalEnd = (start: number, depth: number): number | undefined => { - let quoteIndex = start; - let verbatim = false; - let interpolated = false; - if (source.startsWith('$@"', start) || source.startsWith('@$"', start)) { - quoteIndex += 2; verbatim = true; interpolated = true; - } else if (source.startsWith('$"', start)) { quoteIndex++; interpolated = true; } - else if (source.startsWith('@"', start)) { quoteIndex++; verbatim = true; } - else if (source[start] !== '"' && source[start] !== "'") { - if (source[start] === '$' && /^\$+"{3,}/.test(source.slice(start))) return -1; - return undefined; - } - if (depth > maxDepth) return -1; - const quote = source[quoteIndex]; - const count = quote === '"' ? /^"+/.exec(source.slice(quoteIndex))![0].length : 1; - if (count >= 3) { - if (interpolated || verbatim) return -1; - const end = source.indexOf('"'.repeat(count), quoteIndex + count); - return end < 0 ? -1 : end + count; - } - for (let index = quoteIndex + 1; index < source.length;) { - tick(); - if (source[index] === quote) { - if (verbatim && source[index + 1] === quote) { index += 2; continue; } - return index + 1; - } - if (!verbatim && (source[index] === '\n' || source[index] === '\r')) return -1; - if (!verbatim && source[index] === '\\') { index += 2; continue; } - if (interpolated && source[index] === '{') { - if (source[index + 1] === '{') { index += 2; continue; } - const end = expressionEnd(index + 1, depth + 1); - if (end < 0) return -1; - index = end; continue; - } - if (interpolated && source[index] === '}') { - if (source[index + 1] !== '}') return -1; - index += 2; continue; - } - index++; - } - return -1; - }; - checkpoint(); - for (let index = 0; index < source.length; index++) { - tick(); - const end = commentEnd(index) ?? literalEnd(index, 0); - if (end !== undefined) { - if (end < 0) return null; - mask(index, end); index = end - 1; - } - } - return output.join(''); -} - /** Closed-file textual navigation. It neither evaluates bindings nor establishes runtime causality. */ export async function mapUiCodeCandidates( workspaceRoot: string, candidateCodeFiles: string[], sourceEvidence: UiSourceEvidence, signal?: AbortSignal @@ -224,7 +135,7 @@ export async function mapUiCodeCandidates( try { source = new TextDecoder('utf-8', { fatal: true }).decode(buffer.subarray(0, bytesRead)); } catch { result.files.push({ file, status: 'unsupported-encoding' }); continue; } if (source.includes('\0')) { result.files.push({ file, status: 'unsupported-encoding' }); continue; } - const code = maskNonCode(source, checkpoint, result.limits.maxInterpolationDepth); + const code = maskCSharpNonCode(source, checkpoint, result.limits.maxInterpolationDepth); if (code === null) { result.files.push({ file, status: 'unsupported-or-unclosed-literal' }); continue; } const fileSha256 = createHash('sha256').update(buffer.subarray(0, bytesRead)).digest('hex'); const relativeFile = relative.replace(/\\/g, '/'); diff --git a/src/Gateway/CodeTools.ts b/src/Gateway/CodeTools.ts index 0d5aa3b..2b14fc5 100644 --- a/src/Gateway/CodeTools.ts +++ b/src/Gateway/CodeTools.ts @@ -153,7 +153,7 @@ export const CODE_TOOLS = [ validate: args => { if (!args.target) throw new Error('target is required.'); }, execute: async (args, { router, signal }) => { const impact = await (args.symbolLocation ? router.analyzeChangeImpact(args.target, signal, args.symbolLocation) : router.analyzeChangeImpact(args.target, signal)); - return { content: [{ type: 'text', text: JSON.stringify(impact, null, 2) }, { type: 'text', text: impact.formattedReport }] }; + return jsonResult(impact, true); }, }), defineTool<{ target: string; goal: string; symbolLocation?: SymbolLocation }>({ diff --git a/src/Gateway/McpServer.ts b/src/Gateway/McpServer.ts index 22916a3..edc531a 100644 --- a/src/Gateway/McpServer.ts +++ b/src/Gateway/McpServer.ts @@ -1,4 +1,4 @@ -import { Server } from '@modelcontextprotocol/server'; +import { Server, ProtocolError, ProtocolErrorCode } from '@modelcontextprotocol/server'; import { StdioServerTransport } from '@modelcontextprotocol/server/stdio'; import { ToolRouter, WorkspaceRecoveryRequiredError } from '../Core/ToolRouter.js'; import { WINCODE_VERSION } from '../Core/Config.js'; @@ -28,22 +28,22 @@ export class WinCodeMcpServer { this.router.isShuttingDown ? 'restart_gateway' : 'none'); } const definition = this.registry.resolve(name); + if (!definition) throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Unknown tool: ${name}`); const context: ToolExecutionContext = { router: this.router, signal, tools: this.registry.list(), schemaHash: this.registry.schemaHash }; let args: Record; try { args = this.registry.prepare(name, input, context); } catch (error) { const message = error instanceof Error ? error.message : String(error); - return definition?.invalidArguments?.(message) ?? - toolErrorResult(definition ? 'INVALID_ARGUMENT' : 'UNKNOWN_TOOL', message, definition ? 'correct_arguments' : 'select_tool'); + return definition.invalidArguments?.(message) ?? toolErrorResult('INVALID_ARGUMENT', message, 'correct_arguments'); } let acquired = false; try { - if (!definition!.switchesWorkspace) { - await this.router.acquireRequestSlot(signal, definition!.allowDuringWorkspaceRecovery); + if (!definition.switchesWorkspace) { + await this.router.acquireRequestSlot(signal, definition.allowDuringWorkspaceRecovery); acquired = true; } - return await definition!.execute(args, context); + return await definition.execute(args, context); } catch (error) { // 根变化后的失败必须携带真实恢复状态;取消不能掩盖已发生的部分状态变更。 const recovery = error instanceof WorkspaceRecoveryRequiredError ? error.recovery : this.router.workspaceRecoveryState; diff --git a/tests/local-text.test.ts b/tests/local-text.test.ts index c3a1cb1..c4b656c 100644 --- a/tests/local-text.test.ts +++ b/tests/local-text.test.ts @@ -1,3 +1,6 @@ +import { ContextManager } from '../src/Core/Context.js'; +import { WorkspaceManager } from '../src/Core/Workspace.js'; +import { parseTextDeclarations } from '../src/Core/TextDeclarations.js'; import { it } from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs/promises'; @@ -130,3 +133,126 @@ it('does not turn a directory read failure into a complete empty scan', async (t assert.match(result.queryError!, /read-error/); } finally { t.mock.restoreAll(); } })); + +it('ignores C# comment and literal declarations while preserving real declaration lines and signatures', async () => fixture(async (adapter, root) => { + const source = [ + '// class GhostComment {}', + '/* public class GhostBlock {} */', + 'var sample = "class GhostString {}";', + 'var multi = @"text', + 'class GhostVerbatim {}', + '";', + 'var raw = """', + 'class GhostRaw {}', + '""";', + 'public class Actual {} // remains in signature', + ].join('\r\n'); + await fs.writeFile(path.join(root, 'Sample.cs'), source); + const ghosts = await adapter.findSymbolsDetailed('Ghost'); + assert.deepEqual(ghosts.symbols, []); + const actual = await adapter.findSymbolsDetailed('Actual'); + assert.equal(actual.symbols[0]?.line, 10); + assert.equal(actual.symbols[0]?.signature, 'public class Actual {} // remains in signature'); + assert.equal(actual.source, 'local-text'); + assert.equal(actual.analysisCompleteness, 'degraded'); +})); + +it('ignores JS templates/regex and Python multiline strings without hiding following real declarations', async () => fixture(async (adapter, root) => { + await fs.writeFile(path.join(root, 'Sample.ts'), [ + 'const note = "class GhostString {}";', + '/* class GhostComment {} */', + 'const re = /class GhostRegex[\\/]/;', + 'const sample = `', + 'class GhostTemplate {}', + '${(() => "class GhostNested {}")()}', + '`;', + 'export function ActualTs() {}', + ].join('\n')); + await fs.writeFile(path.join(root, 'Sample.py'), [ + '# class GhostComment:', + 'doc = r"""', + 'class GhostPython:', + ' pass', + '"""', + 'def ActualPy():', + ' return "ok"', + ].join('\n')); + assert.deepEqual((await adapter.findSymbolsDetailed('Ghost')).symbols, []); + const actual = (await adapter.findSymbolsDetailed('Actual')).symbols; + assert.deepEqual(actual.map(s => [s.name, s.line]).sort(), [['ActualPy', 6], ['ActualTs', 8]]); +})); + +it('finds TSX/JSX declarations while excluding JSX text and attributes', async () => fixture(async (adapter, root) => { + for (const ext of ['tsx', 'jsx']) { + await fs.writeFile(path.join(root, `Card.${ext}`), [ + `export function UserCard${ext}() {`, + ' return
', + ' class GhostText {}', + ' <>{"class GhostExpression {}"}', + '
;', + '}', + `export class After${ext} {}`, + ].join('\n')); + } + assert.deepEqual((await adapter.findSymbolsDetailed('Ghost')).symbols, []); + const actual = (await adapter.findSymbolsDetailed('UserCard')).symbols; + assert.deepEqual(actual.map(s => [s.name, s.line]).sort(), [['UserCardjsx', 1], ['UserCardtsx', 1]]); + assert.equal((await adapter.findSymbolsDetailed('After')).totalFound, 2); + assert.equal(adapter.findSymbolsInContent('export function Direct() { return

class Ghost {}

; }', 'Direct.tsx')[0]?.name, 'Direct'); +})); + +it('does not report a complete empty scan or cache results when lexical boundaries are uncertain', async () => fixture(async (adapter, root, cached) => { + await fs.writeFile(path.join(root, 'Broken.ts'), 'const note = `unterminated\nclass Ghost {}'); + const result = await adapter.findSymbolsDetailed('Ghost'); + assert.deepEqual(result.symbols, []); + assert.equal(result.queryComplete, false); + assert.match(result.queryError!, /lexical-uncertainty/); + assert.equal(cached.size, 0); +})); + +it('keeps declarations after escaped/interpolated literals and ordinary division', async () => fixture(async (adapter, root) => { + const samples: Record = { + 'Nested.cs': 'var text = $"{string.Join("class Ghost {}", items)}";\npublic class ActualCs {}', + 'Nested.ts': 'const text = `outer ${`inner ${"class Ghost {}"}`} tail`;\nconst ratio = 12 / 3;\nexport function ActualTs() {}', + 'Nested.py': 'text = f"outer {"class Ghost {}"}"\ndef ActualPy(): pass', + }; + for (const [file, source] of Object.entries(samples)) await fs.writeFile(path.join(root, file), source); + assert.deepEqual((await adapter.findSymbolsDetailed('Ghost')).symbols, []); + assert.equal((await adapter.findSymbolsDetailed('Actual')).totalFound, 3); +})); + +it('does not reuse declaration results cached by the retired unmasked parser', async () => fixture(async (adapter, root, cached) => { + await fs.writeFile(path.join(root, 'Only.cs'), '// class Ghost {}'); + cached.set(`local_text_symbols_v1_${JSON.stringify(['Ghost', undefined, undefined, root])}`, { + queryComplete: true, symbols: [{ name: 'Ghost' }], totalFound: 1, + }); + assert.deepEqual((await adapter.findSymbolsDetailed('Ghost')).symbols, []); +})); + +it('excludes regex literals used as control-flow statements', async () => fixture(async (adapter, root) => { + await fs.writeFile(path.join(root, 'Regex.js'), 'if (ready && check()) /class GhostRegex/.test(input);\nif (ready) {} /class GhostBlockRegex/.test(input);\nexport class Actual {}'); + assert.deepEqual((await adapter.findSymbolsDetailed('Ghost')).symbols, []); + assert.equal((await adapter.findSymbolsDetailed('Actual')).totalFound, 1); +})); + + +it('scoped TSX context returns the real declaration and reports uncertain lexical input', async () => fixture(async (adapter, root) => { + await fs.writeFile(path.join(root, 'Card.tsx'), 'export function UserCard() { return

class Ghost {}

; }'); + const config = getDefaultConfig(root); + const manager = new ContextManager(config, new WorkspaceManager(config), null as any, adapter); + const result = await manager.prepareContext({ task: 'Review', scopeFiles: ['Card.tsx'], symbol: 'UserCard' }); + assert.ok(result.evidence.some(item => item.file === 'Card.tsx' && item.snippet.includes('UserCard'))); + await fs.writeFile(path.join(root, 'Broken.ts'), 'const text = `open'); + const broken = await manager.prepareContext({ task: 'Review', scopeFiles: ['Broken.ts'], symbol: 'Ghost' }); + assert.ok(broken.fileIssues.some(item => item.reason === 'lexical-uncertainty')); + assert.equal(broken.evidence.length, 0); +})); + +it('checks cancellation during long literal processing, not only between files', () => { + const source = 'const note = `' + 'text '.repeat(10000) + '`;\nexport class Actual {}'; + let checks = 0; + const cancelled = new Error('cancelled while processing source'); + assert.throws(() => parseTextDeclarations(source, 'Long.ts', '.ts', () => { + if (++checks === 3) throw cancelled; + }), error => error === cancelled); +}); diff --git a/tests/mcp-stdio.test.ts b/tests/mcp-stdio.test.ts index 25f9643..8023f64 100644 --- a/tests/mcp-stdio.test.ts +++ b/tests/mcp-stdio.test.ts @@ -220,7 +220,8 @@ describe('mcp-stdio', () => { assert.ok(impact.targetFile.includes('ToolRouter.ts')); assert.ok(impact.riskLevel); assert.ok(impact.recommendations.length >= 3); - assert.ok(res.result?.content?.[1]?.text.includes('# Impact Analysis')); + assert.equal(res.result.content.length, 1); + assert.ok(impact.formattedReport.includes('# Impact Analysis')); // Alias verification const aliasRes = await callMcp('tools/call', { @@ -280,13 +281,14 @@ describe('mcp-stdio', () => { assert.strictEqual(escapeRes.result?.isError, true); }); - it('Error Handling: Unknown tool name returns isError without crashing server', async () => { + it('Error Handling: Unknown tool name returns a protocol error without crashing server', async () => { const res = await callMcp('tools/call', { name: 'non_existent_tool_12345', arguments: {}, }); - assert.strictEqual(res.result?.isError, true); - assert.ok(res.result?.content?.[0]?.text.includes('Unknown tool: non_existent_tool_12345')); + assert.equal(res.error?.code, -32602); + assert.equal(res.result, undefined); + assert.ok(res.error?.message.includes('Unknown tool: non_existent_tool_12345')); }); }); }); diff --git a/tests/tool-contracts.test.ts b/tests/tool-contracts.test.ts index 8e032f3..7eebc56 100644 --- a/tests/tool-contracts.test.ts +++ b/tests/tool-contracts.test.ts @@ -193,7 +193,6 @@ it('rejects declared enum, range and nested type violations before admission', a ['wincode_prepare_context', { task: 'x', lineRanges: [{ file: 'A.cs', startLine: '1', endLine: 3 }] }], ['wincode_ui_inspect', { pid: 5, capture: 'interactive' }], ['wincode_ui_inspect', { pid: 5, query: { name: 'Save', maxMatches: 21 } }], ['wincode_ui_inspect', { pid: 5, query: { name: 42 } }], ['wincode_ui_review', { pid: 5, candidateFiles: ['../View.xaml'] }], - ['unknown_tool', {}], ]; for (const [name, args] of cases) { const result = await client.callTool({ name, arguments: args }); @@ -255,3 +254,22 @@ it('ignores unknown UI query properties without weakening the required known con assert.equal(bad.isError, true); assert.equal(admissions(), before); })); + +it('returns unknown tools as protocol errors before admission and keeps the connection usable', async () => fixture(async (client, router, admissions) => { + await assert.rejects(client.callTool({ name: 'missing_tool', arguments: {} }), (error: any) => error.code === -32602); + assert.equal(admissions(), 0); + router.findCodeSymbols = async () => ({ symbols: [], queryComplete: true }) as any; + const result = await client.callTool({ name: 'wincode_find_code_symbol', arguments: { query: 'Known' } }); + assert.notEqual(result.isError, true); +})); + +it('returns impact evidence once for both canonical name and alias', async () => fixture(async (client, router) => { + const impact = { target: 'Same', references: [{ file: 'Use.cs', line: 5 }], risk: 'UNKNOWN', formattedReport: 'REPORT_ONCE' }; + router.analyzeChangeImpact = async () => impact as any; + for (const name of ['analyze_change_impact', 'wincode_analyze_change_impact']) { + const result: any = await client.callTool({ name, arguments: { target: 'Same' } }); + assert.equal(result.content.length, 1); + assert.deepEqual(JSON.parse(result.content[0].text), impact); + assert.equal(result.content[0].text.split('REPORT_ONCE').length, 2); + } +})); diff --git a/tools/WinCode.Code.Host/WinCode.Code.Host.csproj b/tools/WinCode.Code.Host/WinCode.Code.Host.csproj index 20e0ed2..fb4a4c9 100644 --- a/tools/WinCode.Code.Host/WinCode.Code.Host.csproj +++ b/tools/WinCode.Code.Host/WinCode.Code.Host.csproj @@ -1,6 +1,6 @@ - 0.13.0 + 0.13.1 Exe net10.0 enable diff --git a/tools/WinCode.UIA.Host/README.md b/tools/WinCode.UIA.Host/README.md index 22a7396..1e6852a 100644 --- a/tools/WinCode.UIA.Host/README.md +++ b/tools/WinCode.UIA.Host/README.md @@ -1,6 +1,6 @@ # WinCode.UIA.Host -0.12.5 的 Windows UI Automation(FlaUI.UIA3)一次性取证进程。实现入口为 [Program.cs](Program.cs),面向 Agent 的规范参数见 [UI 手册](../../skills/wincode/references/ui.md),整体数据流见 [架构说明](../../WinCode-架构与数据流说明.md)。 +0.13.1 的 Windows UI Automation(FlaUI.UIA3)一次性取证进程。实现入口为 [Program.cs](Program.cs),面向 Agent 的规范参数见 [UI 手册](../../skills/wincode/references/ui.md),整体数据流见 [架构说明](../../WinCode-架构与数据流说明.md)。 ## 职责和边界 diff --git a/tools/WinCode.UIA.Host/WinCode.UIA.Host.csproj b/tools/WinCode.UIA.Host/WinCode.UIA.Host.csproj index bd1f6a7..01c2339 100644 --- a/tools/WinCode.UIA.Host/WinCode.UIA.Host.csproj +++ b/tools/WinCode.UIA.Host/WinCode.UIA.Host.csproj @@ -1,7 +1,7 @@ - 0.13.0 + 0.13.1 true Exe net10.0-windows