From 0aa152a79610d48c4cfaa98286c4275f79025189 Mon Sep 17 00:00:00 2001 From: linnnn89 <216342082+linnnn89@users.noreply.github.com> Date: Wed, 9 Sep 2026 17:12:54 +0800 Subject: [PATCH] feat: checkpoint direct Roslyn delivery and E4 migration --- .github/workflows/ci.yml | 12 +- CONTRIBUTING.md | 6 +- README.md | 24 +- ...43\350\256\241\345\210\222\344\271\246.md" | 105 +- ...56\346\265\201\350\257\264\346\230\216.md" | 38 +- ...43\350\267\257\347\272\277\345\233\276.md" | 21 +- docs/codex_worklog.md | 69 + package-lock.json | 4 +- package.json | 9 +- scripts/benchmark-agent-efficiency.ts | 6 +- scripts/check.mjs | 10 +- scripts/delivery-manifest.mjs | 42 +- scripts/lib/dotnet.mjs | 60 + scripts/lib/owned-processes.mjs | 23 + scripts/roslyn/gateway-lifecycle.mjs | 46 + scripts/roslyn/host-inputs.mjs | 161 +++ scripts/roslyn/host-semantics.mjs | 97 ++ scripts/serena-isolated-launcher.py | 26 - scripts/test-mcp-client.ts | 2 +- scripts/verify-error-contracts.ts | 11 +- scripts/verify-failure-recovery.ts | 8 +- scripts/verify-mixed-load.ts | 44 +- scripts/verify-product-tasks.ts | 2 +- scripts/verify-roslyn-gateway.mjs | 144 +- scripts/verify-roslyn-host.mjs | 218 +-- scripts/verify-serena-real.ts | 181 --- scripts/verify-tavern-context.ts | 2 +- skills/wincode/SKILL.md | 4 +- skills/wincode/references/code.md | 29 +- skills/wincode/references/diagnostics.md | 17 +- src/Adapters/FlaUiAdapter.ts | 65 +- src/Adapters/IAdapter.ts | 4 +- src/Adapters/LocalTextAdapter.ts | 103 ++ src/Adapters/RoslynAdapter.ts | 54 +- src/Adapters/SerenaAdapter.ts | 1105 -------------- src/CompositeTools/ArchitectureAnalyzer.ts | 5 +- src/CompositeTools/ImpactAnalyzer.ts | 127 +- src/CompositeTools/ProjectDiagnostics.ts | 26 +- src/CompositeTools/RefactorAssistant.ts | 17 +- src/Core/AdapterStatus.ts | 11 - src/Core/Cache.ts | 235 +-- src/Core/CodeQueries.ts | 4 +- src/Core/Config.ts | 18 +- src/Core/Context.ts | 208 +-- src/Core/LocalTextScanner.ts | 104 ++ src/Core/ProjectDiscovery.ts | 382 +++++ src/Core/TextDeclarations.ts | 108 ++ src/Core/ToolRouter.ts | 84 +- src/Core/Workspace.ts | 690 +-------- src/Core/WorkspaceBrowser.ts | 165 +++ src/Core/WorkspaceContracts.ts | 168 +++ src/Core/WorkspaceFingerprint.ts | 227 +++ src/Gateway/CodeTools.ts | 37 +- src/Gateway/ContextRangeCoverage.ts | 81 ++ src/Gateway/ContextResponse.ts | 82 +- src/Gateway/McpServer.ts | 29 +- src/Gateway/ToolDefinition.ts | 31 +- src/Gateway/UiResponse.ts | 14 +- src/Gateway/UiTools.ts | 2 +- src/Gateway/WorkspaceTools.ts | 7 +- tests/architecture-boundaries.test.ts | 2 +- tests/cache-budgets.test.ts | 101 ++ tests/composite-tools.test.ts | 325 +++++ tests/context-coverage.test.ts | 4 +- tests/context-efficiency.test.ts | 28 +- tests/context-packing.test.ts | 141 ++ tests/core-cache.test.ts | 134 ++ tests/delivery-contract.test.ts | 36 +- tests/evidence-confidence.test.ts | 49 + tests/failure-recovery.test.ts | 39 +- tests/fixtures/mock-serena-mcp.mjs | 142 -- tests/lifecycle-cancellation.test.ts | 63 +- ...na-fallback.test.ts => local-text.test.ts} | 50 +- tests/mcp-stdio.test.ts | 292 ++++ tests/process-failures.test.ts | 108 ++ tests/request-concurrency.test.ts | 88 ++ tests/resource-cleanup.test.ts | 251 ++++ tests/roslyn-contracts.test.ts | 174 ++- tests/runtime-contract.test.ts | 2 +- tests/semantic-identity.test.ts | 83 ++ tests/serena-identity.test.ts | 259 ---- tests/stability-lifecycle.test.ts | 89 ++ tests/stage1-cleanup.test.ts | 9 +- tests/tdd-suite.test.ts | 1278 ----------------- tests/text-symbols.test.ts | 77 + tests/tool-contracts.test.ts | 8 +- tests/ui-code-runtime.test.ts | 2 +- tests/v05-stability.test.ts | 711 --------- tests/verify.ts | 8 +- tests/watch-invalidation.test.ts | 123 ++ tests/workspace-files.test.ts | 202 +++ tests/workspace-lifecycle.test.ts | 2 +- tests/workspace-summary.test.ts | 2 +- tools/WinCode.Code.Host/HostBuildIdentity.cs | 13 + tools/WinCode.Code.Host/Program.cs | 19 +- .../WinCode.Code.Host.csproj | 1 + tools/WinCode.Code.Host/WorkspaceInputs.cs | 74 +- tools/WinCode.Code.Host/WorkspaceSession.cs | 68 +- tools/WinCode.UIA.Host/NativeWindows.cs | 81 ++ tools/WinCode.UIA.Host/Program.cs | 759 +--------- tools/WinCode.UIA.Host/UiHostContracts.cs | 165 +++ tools/WinCode.UIA.Host/UiTreeReader.cs | 220 +++ .../WinCode.UIA.Host/WinCode.UIA.Host.csproj | 2 +- tools/WinCode.UIA.Host/WindowCapture.cs | 198 +++ tools/WinCode.UIA.Host/WindowResolver.cs | 187 +++ 105 files changed, 5935 insertions(+), 6348 deletions(-) create mode 100644 scripts/lib/dotnet.mjs create mode 100644 scripts/lib/owned-processes.mjs create mode 100644 scripts/roslyn/gateway-lifecycle.mjs create mode 100644 scripts/roslyn/host-inputs.mjs create mode 100644 scripts/roslyn/host-semantics.mjs delete mode 100644 scripts/serena-isolated-launcher.py delete mode 100644 scripts/verify-serena-real.ts create mode 100644 src/Adapters/LocalTextAdapter.ts delete mode 100644 src/Adapters/SerenaAdapter.ts create mode 100644 src/Core/LocalTextScanner.ts create mode 100644 src/Core/ProjectDiscovery.ts create mode 100644 src/Core/TextDeclarations.ts create mode 100644 src/Core/WorkspaceBrowser.ts create mode 100644 src/Core/WorkspaceContracts.ts create mode 100644 src/Core/WorkspaceFingerprint.ts create mode 100644 src/Gateway/ContextRangeCoverage.ts create mode 100644 tests/cache-budgets.test.ts create mode 100644 tests/composite-tools.test.ts create mode 100644 tests/context-packing.test.ts create mode 100644 tests/core-cache.test.ts create mode 100644 tests/evidence-confidence.test.ts delete mode 100644 tests/fixtures/mock-serena-mcp.mjs rename tests/{serena-fallback.test.ts => local-text.test.ts} (70%) create mode 100644 tests/mcp-stdio.test.ts create mode 100644 tests/process-failures.test.ts create mode 100644 tests/request-concurrency.test.ts create mode 100644 tests/resource-cleanup.test.ts create mode 100644 tests/semantic-identity.test.ts delete mode 100644 tests/serena-identity.test.ts create mode 100644 tests/stability-lifecycle.test.ts delete mode 100644 tests/tdd-suite.test.ts create mode 100644 tests/text-symbols.test.ts delete mode 100644 tests/v05-stability.test.ts create mode 100644 tests/watch-invalidation.test.ts create mode 100644 tests/workspace-files.test.ts create mode 100644 tools/WinCode.Code.Host/HostBuildIdentity.cs create mode 100644 tools/WinCode.UIA.Host/NativeWindows.cs create mode 100644 tools/WinCode.UIA.Host/UiHostContracts.cs create mode 100644 tools/WinCode.UIA.Host/UiTreeReader.cs create mode 100644 tools/WinCode.UIA.Host/WindowCapture.cs create mode 100644 tools/WinCode.UIA.Host/WindowResolver.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3aa1666..25bc0a4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,11 +43,21 @@ jobs: run: npm ci - name: Build and verify delivery run: npm run check + - name: Verify real Roslyn semantics and process cleanup + if: matrix.node == '22' + run: | + npm run test:roslyn-host + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + npm run test:roslyn-gateway + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - name: Preserve bounded check report if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: check-node-${{ matrix.node }} - path: test-tmp/check/**/report.json + path: | + test-tmp/check/**/report.json + test-tmp/roslyn-host/**/report.json + test-tmp/roslyn-gateway/**/report.json if-no-files-found: warn retention-days: 7 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 93a4643..8154686 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,13 +9,13 @@ npm run check:desktop npm run delivery:verify ``` -`check` inventories every `*.test.ts`, type-checks, builds the Gateway, restores native dependencies in locked mode, builds the Release Host and console fixtures, runs non-interactive regression and fresh-process stdio checks, then creates and verifies `dist/delivery-manifest.json`. `check:desktop` verifies that delivery, publishes the isolated WPF fixture and runs UI plus UI-to-source tests in an interactive Windows session. `test:all` runs both. Real Serena and TavernDesk checks are opt-in and do not form part of CI. No check installs global prerequisites or changes client configuration. +`check` inventories every `*.test.ts`, type-checks, builds the Gateway, restores native dependencies in locked mode, builds the Release Host and console fixtures, runs non-interactive regression and fresh-process stdio checks, then creates and verifies `dist/delivery-manifest.json`. `check:desktop` verifies that delivery, publishes the isolated WPF fixture and runs UI plus UI-to-source tests in an interactive Windows session. `test:all` runs both. Real Roslyn Host and MCP checks run on Node 22 in CI; TavernDesk checks remain opt-in. No check installs global prerequisites or changes client configuration. Reports and bounded stage logs are under `test-tmp/check//`. CI uploads only the compact report, including failures; it does not upload local workspaces or screenshots. A passing core check does not establish desktop or real-upstream acceptance. -`npm run test:serena-real -- [launcher arguments]` performs an opt-in real C# upstream acceptance in a generated `test-tmp/serena-acceptance/` fixture. Supply a command that accepts Serena CLI arguments and an isolated SERENA_HOME launcher; configure/download its C# language server only within an authorized setup. The script uses normal production adapter timeouts, records result/error/cleanup and PID exit, and never installs prerequisites. Body retrieval is a direct upstream oracle, not a claim that WinCode returns whole method bodies. Native reference coordinates may identify the containing class rather than the exact call line. This probe does not register or reload the user's Codex MCP connection. +`npm run test:roslyn-host` and `npm run test:roslyn-gateway` use generated C# projects and an already installed SDK selected by `scripts/lib/dotnet.mjs`. The gateway check copies the entire published Code Host into a Chinese path with spaces and checks real overloads, stale identities and owned MSBuild descendants. This is not a clean-machine test or verification of the current Codex connection. -For opt-in fixed-profile TavernDesk acceptance, use `npm run test:tavern-context -- --ui-pid= --ui-hwnd=` and `npm run test:product -- `. The latter checks the fixed profile receipt and performs six navigation-to-source tasks without source filenames supplied in advance. Native candidate discovery is counted, source reads used only as the oracle are separate, and all returned bodies are checked against current file hashes. Source candidates remain distinct from verified runtime bindings. The scripts do not install Serena, activate a language server, launch the target application or use personal databases. +For opt-in fixed-profile TavernDesk acceptance, use `npm run test:tavern-context -- --ui-pid= --ui-hwnd=` and `npm run test:product -- `. The latter checks the fixed profile receipt and performs six navigation-to-source tasks without source filenames supplied in advance. Native candidate discovery is counted, source reads used only as the oracle are separate, and all returned bodies are checked against current file hashes. Source candidates remain distinct from verified runtime bindings. The scripts do not install prerequisites, launch the target application or use personal databases. The delivery manifest covers Gateway JavaScript, all published Host files including dependency sidecars, four managed Skill documents, and package/SDK/Host lock configuration. It records the Git revision and toolchains. Timestamps and checkout paths do not participate in content identity. Hashes detect local mismatches; they are not signatures. Run a complete check after changing delivery inputs. Keep a complete previous checkout/artifact set for rollback; do not mix old DLLs with a new Gateway. diff --git a/README.md b/README.md index 4c8a0e5..b9682f4 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.12.5**. All UI tools are strictly read-only and non-destructive. See [CHANGELOG](CHANGELOG.md) for full version history. +Current source version: **0.13.0**. All UI tools are strictly read-only and non-destructive. See [CHANGELOG](CHANGELOG.md) for full version history. ### Quick start @@ -117,7 +117,7 @@ Optionally add `candidateCodeFiles: ["ViewModels/MainWindowViewModel.cs"]` (1– ### Tool reference -Serena results retain full `namePath`, including containers and overload indices. Pass it as `symbolName` together with its defining `relativePath`. Simple names require complete unique semantic resolution; ambiguity returns at most 20 candidates plus the count. Malformed/shortened responses are incomplete; valid empty results stay empty. Coordinates are one-based; `lineKind: "containing-symbol"` marks a containing declaration, not an exact call site. Controlled upstream tests do not establish actual language-server availability. +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. @@ -154,7 +154,7 @@ See the [architecture, data-flow and verification-gate guide](WinCode-架构与 ```text Coding agent ── stdio MCP ── WinCode - ├─ Code adapters: Serena / Repomix / Built-in text fallbacks + ├─ Code adapters: Direct Roslyn / Repomix / Local text ├─ Workspace analysis, context, and impact tools └─ FlaUiAdapter ── stdin/stdout JSON ── .NET UIA helper └─ Window tree + screenshot @@ -180,7 +180,7 @@ Coding agent ── stdio MCP ── WinCode - **Background capture:** `backgroundOnly: true` requires both PID and HWND. It uses `PrintWindow` without focus shifts or screen fallbacks. Minimized windows are rejected. `captureQuality` samples up to 1024 raw pixels before annotation: `suspect-low-variation` means the sampled RGB channel ranges are at most 3 and may reflect either blank output or a legitimate uniform/low-contrast view. `unknown` never certifies visual usability. Hints retain both image and UIA evidence and do not change the capture policy. Older helpers without this field leave quality unverified. - **UI coverage:** Inspection depends on the application's underlying UIA provider. Verified against WPF; WinUI, WinForms, and custom-rendered controls may expose differing levels of UIA detail. - **Source evidence:** Matches literal attribute declarations in supplied `.xaml` files (`runtimeSourceVerified: false`). Dynamic bindings, runtime templates, and resource dictionaries are not evaluated. -- **Project analysis:** Extracted directly from project file XML without invoking MSBuild evaluations. Serena and Repomix are optional upstreams; local fallbacks explicitly label reduced semantic coverage. +- **Project analysis:** Extracted directly from project file XML without invoking MSBuild evaluations. Direct Roslyn requires explicit project-evaluation authorization; Repomix is optional. Local text results explicitly label reduced semantic coverage. - **Visual indicator:** A non-activating, semi-transparent `REC / WinCoding` overlay is painted in the top-right corner of the primary display during UI inspection to ensure complete visibility. - **Local audit:** Lightweight start/end records are flushed to `%LOCALAPPDATA%/WinCode/logs/ui-audit` (1 MiB triggers cleanup reminders; 2 MiB blocks new access with reserved end-record space). The [audit checker script](scripts/check-ui-audit.ps1) enables manual inspections. @@ -204,7 +204,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. Interactive desktop/UI and real Serena acceptance remain 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 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. ```powershell npm ci @@ -218,7 +218,7 @@ npm run benchmark:agent -- 1 # Opt-in pilot; -- 3 for three repetitions Live UI suites require an interactive Windows desktop session. In a 222-node test fixture, targeted queries reduced response text from 62 KB to ~1.6 KB while completing in ~0.78 seconds. Detailed test records are maintained in the [work log](docs/codex_worklog.md). -`npm run test:product -- ` explicitly runs six navigation-to-source tasks against an already running fixed test profile. It discovers source files, checks the live control and verifies literal command/method candidates, recording native and MCP calls, response characters and repeated source lines under `test-tmp/product-tasks`. It neither launches the application nor changes its data or source. This scripted acceptance does not establish runtime bindings, full-method coverage, native-only speedup or real Serena integration; see the acceptance matrix in the work log. +`npm run test:product -- ` explicitly runs six navigation-to-source tasks against an already running fixed test profile. It discovers source files, checks the live control and verifies literal command/method candidates, recording native and MCP calls, response characters and repeated source lines under `test-tmp/product-tasks`. It neither launches the application nor changes its data or source. This scripted acceptance does not establish runtime bindings, full-method coverage, native-only speedup or semantic completeness; see the acceptance matrix in the work log. The agent benchmark covers ten scripted scenarios, including existing `dotnet-mini` C# fixtures and four levels of initial location knowledge. It validates returned files, ranges, bodies and status against current fixture contents. Tool/transport/response/cleanup failures remain in the JSON report under `test-tmp/agent-efficiency`; failed cases produce a nonzero exit code. Unchanged-evidence reuse is tested only under trusted, controlled fixture writes; edits require a new request. Reports measure MCP calls, output characters, repeated displayed lines and call time using local fallback with upstreams and GUI disabled. They do not establish real-agent completion rates, model-token savings or production cache benefits. Schema v2 results should not be compared directly with the earlier six-scenario report. @@ -232,7 +232,7 @@ WinCode 是面向 Windows 与 .NET 工程研发的本地 MCP 服务。它将项 - **观察实际界面:**发现系统可见窗口,按条件定向查询目标控件或子树,并在不激活、不抢占前台焦点的前提下获取数字标注截图。 - **源码双向印证:**将运行时抓取的控件关联回 XAML 源码声明的起始行号、代码片段与文件哈希,清晰报告歧义、截断与降级状态。 -当前源码版本为 **0.12.5**。所有 UI 取证工具均为纯只读与非侵入设计。版本历史见 [CHANGELOG](CHANGELOG.md)。 +当前源码版本为 **0.13.0**。所有 UI 取证工具均为纯只读与非侵入设计。版本历史见 [CHANGELOG](CHANGELOG.md)。 ### 快速上手 @@ -327,7 +327,7 @@ npm run delivery:verify ### 工具一览 -Serena 结果保留完整 `namePath`(容器及重载索引);将其作为 `symbolName` 并附定义文件 `relativePath` 续查引用。简单名称只有完整、唯一语义定位才继续;歧义最多返回 20 个候选及总数。损坏/缩略响应不完整,合法空结果保持为空。坐标统一一基;`lineKind:"containing-symbol"` 表示所在声明起点,不是精确调用行。受控上游测试不替代真实语言服务器验收。 +默认以 `local-text` 启动,并明确报告语义能力未配置。显式配置直接 Roslyn 后,将搜索返回的完整 `location` 作为 `symbolLocation` 传给引用、影响分析或重构工具,名称使用原结果的简单名称。外部 Serena 启动配置及 namePath 身份已退役;过期快照须重新显式搜索。 `wincode_hello_world` 返回启动时固定的实例 ID、构建指纹及当前注册工具定义的 hash。传 `toolName: "wincode_prepare_context"` 可按需查看单个工具参数,与同一连接的 `tools/list` 对照。`npm run build` 生成 manifest;直接运行 `tsc`、产物缺失/失配或源码开发模式会明确报告 `unknown`。构建指纹校验本地产物一致性,不证明发布来源可信;切换分析工作区不会改变运行构建。 @@ -362,7 +362,7 @@ Serena 结果保留完整 `namePath`(容器及重载索引);将其作为 ` ```text Coding Agent ── stdio MCP ── WinCode - ├─ 代码适配器:Serena / Repomix / 内置文本降级引擎 + ├─ 代码适配器:直接 Roslyn / Repomix / 本地文本 ├─ 工作区分析、上下文提取与影响面分析工具 └─ FlaUiAdapter ── stdin/stdout JSON ── .NET UIA Helper └─ 控件树遍历 + 截图渲染 @@ -388,7 +388,7 @@ Coding Agent ── stdio MCP ── WinCode - **后台截图适用性:**`backgroundOnly: true` 仅支持非最小化窗口且需同时指定 PID 与 HWND。`captureQuality` 在标注前最多采样 1024 个原始像素;suspect-low-variation 表示采样 RGB 各通道范围不超过 3,可能为空图或正常纯色/低对比界面。unknown 也不能证明图片可用。提示保留图像和 UIA,不自动改变截图策略;旧 Host 缺少该字段时按未验证处理。 - **UI 自动化覆盖度:**取证效果取决于目标应用本身的 UIA Provider 完备性。项目针对 WPF 提供了隔离测试夹具;对于 WinUI、WinForms 或自绘渲染程序,UIA 支持度视其实现而定。 - **源码证据边界:**仅匹配指定 `.xaml` 文件内的字面量属性声明(`runtimeSourceVerified: false`),不求值动态 Binding、模板或全局资源字典。 -- **项目分析边界:**直接解析 `.sln` 与 `.csproj` 文件结构,不执行 MSBuild 动态属性计算。Serena 与 Repomix 均为可选上游,降级运行时会在结果中明确声明。 +- **项目分析边界:**直接解析 `.sln` 与 `.csproj` 文件结构,不执行 MSBuild 动态属性计算。直接 Roslyn 需要显式项目求值授权;Repomix 为可选上游。本地文本结果明确声明语义范围不足。 - **视觉指示器:**在 UI 取证期间,主屏幕右上角会强制浮现半透明置顶标志(`REC / WinCoding`),保障操作对用户完全透明可见。 - **本地审计记录:**仅记录时间、PID、耗时等结构化元数据至 `%LOCALAPPDATA%/WinCode/logs/ui-audit`。达到 1 MiB 提示清理,达到 2 MiB 拦截新访问以预留结束记录空间。日志不自动删除,支持通过 [检测脚本](scripts/check-ui-audit.ps1) 手动审查。 @@ -412,7 +412,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 和交付校验,失败时也上传有界报告。交互桌面/UI 和真实 Serena 验收仍单独执行。通过与否以实际运行结果为准。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 @@ -426,7 +426,7 @@ npm run benchmark:agent -- 1 # 显式小样本;三轮对照使用 -- 3 实机 UI 测试需要交互式 Windows 桌面会话。实测在包含 222 个节点的测试夹具中,定向查询将返回文本由 62 KB 降至约 1.6 KB,单次耗时稳定在 0.78 秒左右。详尽的测试记录参见 [工作日志](docs/codex_worklog.md)。 -`npm run test:product -- <专用测试PID> ` 显式运行六项导航到源码任务,要求固定测试 profile 已启动。它发现候选文件、核对实际控件及命令/方法文字候选,将原生和 MCP 调用、返回字符、重复源码行写入 `test-tmp/product-tasks`;不启动应用、不修改数据或源码。此脚本验收不证明运行时绑定、完整方法覆盖、相对纯原生工具提速或真实 Serena 集成;详见工作日志验收矩阵。 +`npm run test:product -- <专用测试PID> ` 显式运行六项导航到源码任务,要求固定测试 profile 已启动。它发现候选文件、核对实际控件及命令/方法文字候选,将原生和 MCP 调用、返回字符、重复源码行写入 `test-tmp/product-tasks`;不启动应用、不修改数据或源码。此脚本验收不证明运行时绑定、完整方法覆盖、相对纯原生工具提速或语义完整性;详见工作日志验收矩阵。 Agent 基准包含 10 类脚本场景,复用现有 `dotnet-mini` C# 夹具,并按四种初始位置信息分层。返回的文件、行号、正文及状态均与当前夹具核对;工具错误、传输异常、响应损坏和清理失败会保留在 `test-tmp/agent-efficiency` 下的 JSON 报告中,失败返回非零退出码。无变化复用只在夹具写入受控、变化事件可信的条件下测试,修改后必须重新请求。测量使用本地回退,关闭上游与 GUI,记录 MCP 调用、返回字符、重复显示行和调用耗时,不代表真实 Agent 完成率、模型 Token 节省或生产缓存收益。Schema v2 场景与旧版六场景报告不同,不能直接比较两版总量。 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 bb0115f..8243dfe 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,12 +1,14 @@ # WinCode 下一轮工程化迭代计划书 -更新日期:2026-09-09(北京时间)。核对基线:**0.12.5 / main bbc20ff**。状态:**E1/E2 补修与 E3 有界及真实上游验收完成。显式配置的直接 Roslyn 路径已接入现有 MCP,最新端到端 13 场景、核心回归 344/344 通过。默认后端迁移、E4 公共迁移、Code Host 正式打包及干净环境验收尚未完成。变更尚未提交,最新范围与限制见末尾实施记录。** +更新日期: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 或切换实际客户端。 + +历史基线结果与默认 SDK 发现失败回执保留;维护脚本已统一选择现有锁定 SDK,不改系统环境。E4 已获准直接采用方案二,当前为实施中快照;真实客户端项目/配置和求值范围仍需明确。早期章节描述对应阶段历史,以本段及末尾更新为准。 已完成的 WP1–WP5、Repomix 安全修复和真实 Serena 隔离验收已从待办移除,历史见 [CHANGELOG](CHANGELOG.md) 与 [工作记录](docs/codex_worklog.md)。方向总览见 [路线图](WinCode-迭代路线图.md),现状见 [架构说明](WinCode-架构与数据流说明.md)。 ## 目标与边界 -下一轮集中验证失败恢复与长期运行可靠性,沿用 Gateway → Registry/Router → Core/能力接口 → Adapter/Host 的结构。先证明具体缺口,再做局部修复;不因 Router 较大就机械拆层,不增加微服务、插件框架、消息队列或新数据库。 +当前下一轮集中修正已经复现的误导性结果、输入处理问题及 Roslyn 交付缺口,沿用 Gateway → Registry/Router → Core/能力接口 → Adapter/Host 的结构。此前失败恢复与有界负载验收保留,不重复当作未完成任务;不因 Router 较大就机械拆层,不增加微服务、插件框架、消息队列或新数据库。 已确认策略继续有效:Node 24 主支持、22 兼容;未知字段容忍并忽略,已声明字段严格校验且在 Skill 列出;hello 不主动探测;UI 取证不操作目标应用;安装与真实上游使用隔离目录。输出范围、候选与已证实事实必须分开表达。 @@ -295,3 +297,102 @@ E4 建议进一步精简:新增公共事实优先限于 errorCode、errorMessa **剩余范围:** 本轮只在生成的 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 尚未实现”已被本节取代。 + +尚未完成:远端 CI 实际运行、发布/客户端启用;E4 文本兼容方案等待用户本次选择。不得用本地验收替代以上关口。实际客户端项目/配置及求值范围须明确后才启用 Roslyn。 + + +2026-09-09 发布开发快照更新:用户确认可直接采用 E4 方案二后,基础实现已开始;随后要求先将当前状态上传 GitHub。本次保存所有相关源码/测试/文档,以草稿 PR 交付,不将 E4 或整体发布判为完成。 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 00ead5b..ec79676 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,8 +1,8 @@ # WinCode 架构、数据流与检查关口 -**基线:0.12.5,main `10496e0`;核对日期:2026-09-08(北京时间)。** +**本地源码:0.13.0,基于 main@2235a42;结构更新日期:2026-09-09(北京时间)。未发布。** -本说明描述当前源码中已实现的结构。GitHub 分支保护已在本次只读核查中确认;历史实测结果见[工作记录](docs/codex_worklog.md)。源码版本、磁盘构建和客户端当前连接是三个不同对象,不能互相替代。 +本说明描述当前源码中已实现的结构。GitHub 分支保护的历史只读核查日期为 2026-09-08,本轮未重新查询远端;历史实测结果见[工作记录](docs/codex_worklog.md)。源码版本、磁盘构建和客户端当前连接是三个不同对象,不能互相替代。 ## 1. 整体定位与结构 @@ -18,7 +18,7 @@ flowchart TB Router["ToolRouter\n组件装配 · 用例入口 · 工作区切换 · 生命周期"] Use["用例与证据处理\nContext / Architecture / Impact / Refactor / UiReview"] State["横向状态与资源\nWorkspace · Session · Cache · Watch · ResourceManager"] - Adapters["适配器\nSerenaAdapter · RepomixAdapter · FlaUiAdapter"] + Adapters["适配器\nLocalTextAdapter / RoslynAdapter · RepomixAdapter · FlaUiAdapter"] Gate --> Router Router --> Use Router --> State @@ -26,13 +26,13 @@ flowchart TB Router --> Adapters end Client <-->|"MCP / stdio"| Gate - Adapters <-->|"MCP / stdio"| Serena["Serena 进程\n语言服务器:C# 使用 Roslyn"] + Adapters <-->|"有界 JSONL / stdio"| CodeHost["WinCode.Code.Host\n直接 Roslyn / MSBuildWorkspace"] Adapters <-->|"Node 直启 JS / 输出文件"| Repomix["已安装的 Repomix CLI\n缺失时使用内置打包器"] Adapters <-->|"stdin 请求 / stdout JSON"| Host[".NET UIA Host\nFlaUI · Win32 · 截图 · 审计"] Host -->|"只读取证"| App["目标 Windows 应用\n独立 PID / HWND"] State <--> Disk["本地文件系统\n源码 · 项目文件 · 缓存 · trash"] Use -->|"有界读取"| Disk - Serena --> Disk + CodeHost --> Disk Repomix --> Disk ``` @@ -45,7 +45,7 @@ flowchart TB | ToolRouter | 创建并组合组件,提供用例入口,协调请求与工作区生命周期 | [ToolRouter](src/Core/ToolRouter.ts);这是装配与协调中心,不只是名称路由表 | | 核心能力契约 | 定义符号、引用、打包、UI、操作取消等数据类型 | [CodeQueries](src/Core/CodeQueries.ts)、[ContextPacking](src/Core/ContextPacking.ts)、[UiContracts](src/Core/UiContracts.ts)、[OperationContext](src/Core/OperationContext.ts) | | 用例层 | 项目结构分析、上下文组织、影响评估、重构建议、UI→源码候选 | [Context](src/Core/Context.ts)、[CompositeTools](src/CompositeTools);消费窄接口,保留来源与不完整状态 | -| 适配器层 | 上游协议、响应解析、超时、失败降级及子进程管理 | [Adapters](src/Adapters);Serena 的语义结果与本地文本结果分开标识 | +| 适配器层 | 上游协议、响应解析、超时、失败降级及子进程管理 | [Adapters](src/Adapters);Roslyn 与 local-text 的来源分开标识 | | 原生 Host | 按 PID/HWND 取证,执行有界 UIA 搜索及截图 | [Program.cs](tools/WinCode.UIA.Host/Program.cs)、[BoundedUiSearch](tools/WinCode.UIA.Host/BoundedUiSearch.cs)、[UiAudit](tools/WinCode.UIA.Host/UiAudit.cs) | | 构建交付层 | 锁定构建、回归、stdio 验证、产物身份、Skill 一致性 | [check.mjs](scripts/check.mjs)、[delivery-manifest](scripts/delivery-manifest.mjs)、[sync-skill](scripts/sync-skill.mjs) | @@ -96,7 +96,7 @@ flowchart LR Route -->|"lineRanges"| Lines["按指定文件/行范围读取"] Route -->|"scopeFiles + symbol"| Local["文件内声明匹配\n局部窗口,语义覆盖不完整"] Route -->|"scopeFiles"| Files["指定文件预览\n或有预算的全文"] - Route -->|"尚无明确范围"| Discover["任务关键词 / 候选 / focusAreas\nSerena 查询或文本降级"] + Route -->|"尚无明确范围"| Discover["任务关键词 / 候选 / focusAreas\nRoslyn 查询或本地文本"] Discover --> Select["候选排序与去重\n选取有限文件"] Lines --> Evidence["Evidence\n文件 · 实际行范围 · 正文 · 定位方式"] Local --> Evidence @@ -111,7 +111,7 @@ flowchart LR | 数据 | 生产者 → 使用者 | 必须随数据保留的信息 | |---|---|---| -| 符号 / 引用 | SerenaAdapter → Context、Impact、Refactor | `source`、`namePath`、文件、行、`queryComplete`、歧义、截断;引用行的 `lineKind` | +| 符号 / 引用 | LocalTextAdapter / RoslynAdapter → Context、Impact、Refactor | `source`、快照绑定的 `location`、文件、行、`queryComplete`、歧义及截断 | | 源码正文 | 文件读取 / 打包 → ContextResponse → Agent | 实际起止行、末行是否完整、`locationKind`、省略原因与范围覆盖 | | 项目结构 | Workspace / DotNetGraph → ArchitectureAnalyzer | 从 `.sln`、`.csproj` 等文件提取的声明关系;不代表 MSBuild 动态求值后的实际编译图 | @@ -119,11 +119,11 @@ flowchart LR ### 3.2 语义链与降级链 -SerenaAdapter 懒连接真实上游,先握手、获取工具列表,再查询。状态分为命令已发现、握手成功、项目激活、语义查询可用;前一层成功不自动推导后一层成功。 +默认只启用本地文本能力;显式配置直接 Roslyn 后,首次语义搜索才启动自有 Code Host。ready 核对版本、协议、配置、输入策略及进程树保障;不启动外部 Serena,也不在 Roslyn 出错时切换提供方。 -真实符号结果保留完整 `namePath` 和重载标识。简名对应多个身份时返回 ambiguous,引用查询不擅自选择第一项;指定身份查询仍要核对完成状态。上游失败时可使用有文件数、字节数和时间预算的本地正则扫描,结果明确为文本降级。 +真实符号结果返回 snapshotId/project/file/position 身份,选定后传给引用、影响分析和重构。旧定位先校验,再分析;不按名字重选重载。本地文本扫描仍有文件数、字节数和时间预算,并明确语义能力未配置。 -0.12.5 处理了真实 Serena/FastMCP 的 `structuredContent.result` 字符串包装;合法空数组和零引用保留语义来源,错误或不支持的结构不会被当作成功。ImpactAnalyzer 对身份不唯一或查询不完整的情况保留 `UNKNOWN`;零引用不构成“可以安全删除”的证明。 +0.13.0 退役外部 Serena 配置、连接及旧 source;local-text 与 roslyn 均不能仅凭来源证明完整性。ImpactAnalyzer 对身份不唯一或查询不完整保留 UNKNOWN;零引用不构成可安全删除的证明。 ### 3.3 输出预算位于最后一公里 @@ -185,7 +185,7 @@ flowchart TB ### 5.3 取消与退出 -代码用例把客户端 signal 与操作 deadline 传入扫描/上游路径。当前代码用例总预算由 Serena 连接、调用和文件扫描预算合成(默认 43 秒);具体外部操作还有各自超时。UI 另有 Helper 超时,默认 10 秒。原生调用或单次磁盘 I/O 不一定能立即中断。 +代码用例把客户端 signal 与操作 deadline 传入扫描/上游路径。本地文本用例采用文件扫描预算;Roslyn 用例采用显式加载、查询与文件扫描预算;具体外部操作还有各自超时。UI 另有 Helper 超时,默认 10 秒。原生调用或单次磁盘 I/O 不一定能立即中断。 关闭时拒绝新请求、取消代码操作、等待在途请求,并依次尝试停止 watcher、各适配器、扩展兼容项,刷新缓存写入、关闭 session 和资源管理器。主要关闭路径保留聚合错误,重复 dispose 共享结果;不能仅凭进程计数为零证明所有清理成功。ResourceManager 保存有限的进程内清理记录,真实验收另检查已知自有 PID 是否退出。 @@ -197,13 +197,13 @@ flowchart TB | G2 请求与工作区 | Gateway / ToolRouter | 取消/关闭检查;切换互斥与在途排空 | 不是所有请求统一串行,也不是多租户隔离 | | G3 文件与范围 | Workspace、Context、UI 源码 mapper | 相对/真实路径、工作区边界、候选数量、文件/读取预算 | 路径检查不是 OS 沙盒或完整文件事务 | | G4 上游启动 | 各 Adapter | 配置禁用、可用性、超时;Repomix Node 直启 JS | 已安装脚本本身的可信性没有因此被证明 | -| G5 语义身份 | SerenaAdapter / ImpactAnalyzer | 完整身份、重载、歧义、协议错误、完成状态 | fallback、零引用或非空结果不等于安全重构 | +| G5 语义身份 | LocalTextAdapter / RoslynAdapter / ImpactAnalyzer | 完整身份、重载、歧义、协议错误、完成状态 | fallback、零引用或非空结果不等于安全重构 | | G6 UI 准入 | Host / UiAudit | PID-HWND 归属、后台策略、审计容量、搜索预算 | computer-use 其他链路的窗口归属不是本模块证据 | | G7 UI 返回 | Host / FlaUiAdapter | 文本/图片/管道预算、协议与 inspectionVersion | 像素有变化不等于画面可用,候选不等于绑定已证实 | | G8 最终正文 | ContextResponse / UiResponse | 最终序列化预算、截断、省略与范围信息 | 正文覆盖不等于任务推理充分,估算字符不等于精确 token | | G9 生命周期 | OperationContext / ResourceManager / Router | deadline、取消、自有进程关闭、缓存写入排空 | 单一 dispose 返回或资源计数不是全部外部进程的证据 | | G10 交付一致性 | 构建清单 / delivery verify | Gateway、完整 Host 发布文件、配置与 Skill 的版本/哈希 | 内容一致性不是数字签名,磁盘新版不等于连接新版 | -| G11 合并 | GitHub 保护与 CI | Node 22/24 + 三项 CodeQL、PR、管理员约束、禁止 force push/删除 | CI 绿色不证明真实桌面/Serena已验收,也不证明所有安全告警关闭 | +| G11 合并 | GitHub 保护与 CI | Node 22/24 + 三项 CodeQL、PR、管理员约束、禁止 force push/删除 | CI 绿色不证明真实桌面已验收,也不证明所有安全告警关闭 | G1–G10 分布在运行时和本地交付工具中;G11 依赖远端仓库配置。人工授权、是否接受重构方案、是否安装真实上游等,仍属于客户端/维护流程的决策,不能把 Skill 的文字说明当成服务器权限系统。 @@ -219,14 +219,14 @@ flowchart LR Merge --> Disk["主分支构建 / Skill 同步"] Disk --> Reconnect["客户端重新建立连接"] Reconnect --> Identity["hello:实例 / buildId / schemaHash\n核对实际请求行为"] - Build -.-> Desktop["独立验收\n桌面 WPF / TavernDesk / 真实 Serena"] + Build -.-> Desktop["独立验收\n桌面 WPF / TavernDesk"] ``` `hello` 从 0.12.1 起只读已知状态;主动检查使用 `diagnose_project`。未知值明确保留为 unknown/null,历史健康结果可能陈旧。Gateway 初始化仍会初始化适配器,轻量 hello 不表示整个启动过程没有探测成本。 当前分支保护强制 Node 22/24 回归和 CodeQL 的 JavaScript/TypeScript、C#、Actions 三项检查,对管理员生效;按单维护者政策要求的 GitHub approval 数量为 0。**因此独立审核仍是额外流程,不是仓库规则已保证的事实。** -真实 Serena、交互桌面验收分别是 opt-in 命令,没有被普通 CI 自动覆盖。0.12.5 已有真实 Serena/Roslyn 的七项隔离实测;源码正文其中使用了直接上游 oracle,不应推广成 WinCode 已提供完整方法正文接口。 +真实 Roslyn Host/MCP 验收纳入 Node 22 CI,使用生成项目并核对 BuildHost 与目标子进程清理;桌面验收仍单独显式执行。是否通过以该次报告为准。 ## 8. 当前设计的工程成熟度与明确边界 @@ -243,3 +243,9 @@ flowchart LR 本说明的架构图、数据表与关口表共同描述当前实现;新增功能应说明接入哪条数据流、使用哪个现有契约、在哪个关口拒绝或降级,以及如何留下真实验收证据。 下一轮可靠性工作见[待实施计划](WinCode-下一轮工程化迭代计划书.md):工作区切换后续步骤失败的一致性、trash 移动后元数据失败的部分完成语义、有界混合负载验收,以及错误契约渐进整理。前两项来自静态调用链审查,仍需故障注入确认;后两项是验证和一致性改进,不能据此断言当前已有泄漏或必须整体重构。 + +## 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 个套件,各自拥有缓存目录。 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 537b9aa..95ff9bc 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,6 +1,6 @@ # WinCode 迭代路线图 -更新日期:2026-09-09(北京时间)。当前提交基线:**0.12.5 / main bbc20ff**;E1/E2 修复和 E3 有界/真实上游验收完成。显式 Roslyn 配置已接入现有 MCP,最新端到端 13 场景、核心回归 344/344 通过;默认迁移、E4 和 Code Host 正式交付未完成,尚未提交。 +更新日期:2026-09-09(北京时间)。本地契约 0.13.0,工作分支 codex/roslyn-correctness,尚未提交/推送。A1/A2、外部 Serena 退役、默认 local-text、职责拆分及 C 已完成本地实现;B 的构建/清单、异地发布目录与真实 Host/MCP 验收已通过,远端 CI 及实际客户端启用仍未验收。最新证据见工作记录末尾;早期章节保留为历史。 本文件只保留未完成方向与进入条件。已完成的 R1–R6、WP1–WP5 不再作为待办重复执行;版本变更见 [CHANGELOG](CHANGELOG.md),过程与验收边界见 [工作记录](docs/codex_worklog.md)。 @@ -10,20 +10,23 @@ 0.12.4 的 Repomix 无 shell 启动修复已合并,GitHub 安全告警 #1 已自动标记 fixed;0.12.5 的 Serena/FastMCP 兼容修复已合并,固定 Serena 1.7.0/Roslyn 的七项隔离验收通过。主分支保护已启用,单维护者策略 approval=0,不能据此宣称独立审核已完成。 -0.12.5 主分支核心回归为 313 通过、1 项可选跳过;这不等于所有真实应用或长时间运行场景均已验证。历史 Yuki/TavernDesk 导航到源码验收已经完成,不再列为未开始;角色聊天等应用业务行为不属于该证据范围。 +PR #30 之前的 0.12.5 主分支核心回归为 313 通过、1 项可选跳过,接入阶段回执更新为 344/344;这些都不等于所有真实应用或长时间运行场景均已验证。另一个提交准备流程的默认 `npm test` 因未发现锁定 SDK 10.0.303 失败,需统一已有 SDK 的选择与传递;本轮没有用历史成功覆盖该失败。历史 Yuki/TavernDesk 导航到源码验收已经完成,不再列为未开始;角色聊天等应用业务行为不属于该证据范围。 ## 下一轮优先级 -本轮 E1(工作区失败后阻止业务请求,区分重新打开与重启 Gateway)和 E2(trash 部分完成、实际路径及长文件名)已完成本地实现与复核补修。E3 的 97 秒/70 次调用采样、固定 Serena 8 项和 Repomix 10 项真实验收通过;实测另修复 Repomix 文件数误报。核心回归 337/337、生产 stdio 通过;没有改动系统 SDK 或 global.json,没有提交/推送,也不代表真实桌面或当前 Codex 连接已验收。 +E1(工作区失败恢复)、E2(trash 部分完成)与 E3 有界验收已经完成,不重复列为待办。本次 Pro 与本地复核认为现有分层可继续使用,应先修具体逻辑和输入问题,再补交付、增加能力;完整证据、范围和决策点见计划书末尾。 | 顺序 | 未完成方向 | 进入与完成标准 | | --- | --- | --- | -| 1 | 逐步统一错误与证据输出 | E1/E2 所需错误码已局部实现;10 个失败/不完整场景已盘点。社区复核建议同一错误对象生成 JSON 文本及可选 structuredContent,旧文本仅按实际兼容需求过渡;具体公共策略待确认,独立于 Roslyn 验收 | -| 2 | 完成 Roslyn 发布与默认迁移,移除 C# 语义路径对 Serena 的运行依赖 | 显式选择 Roslyn 的 MCP 接入、精确定位/source、编辑失效、A→B→A 和实际 MSBuild 取消/崩溃/超时清理已验收,13 场景通过。后续为 Code Host/BuildHost 正式打包及指纹、无 Serena/Python 干净环境、实际客户端验收,再决定默认切换 | +| 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)。本地实现、针对性验证、完整交付和真实上游验收分别记录,不将其中一项替代其他关口。 -2026-09-09 的直接 Roslyn 设计已更新后续语义方向:目标是随 WinCode 提供分析组件,无须用户另装 Serena/Python/独立语言服务器;这不消除加载目标项目所需 SDK、引用包等前置条件。E4 另补三种兼容方式的利弊,修订建议暂不增加统一 retryable 布尔值,不混改未知工具的协议层行为;仍待用户选择文本策略。 +## 阶段回顾与保持的边界 + +以下为同日实施过程;当前优先级以上表和计划书末尾为准。直接 Roslyn 的方向仍是随 WinCode 提供分析组件,无须用户另装 Serena/Python/独立语言服务器;这不消除加载目标项目所需 SDK、引用包等前置条件。E4 已讨论三种兼容方式的利弊,修订建议暂不增加统一 retryable 布尔值,不混改未知工具的协议层行为;仍待用户选择文本策略。 同日社区与第一性原理复核进一步收缩了原型范围,保留文本探索能力,后置公共符号句柄设计及跨客户端共享服务。现有监听忽略 obj 并防抖,不能原样作为语义状态失效保证。本机 MCP SDK 隔离探针 5/5 验证工具错误可绕过成功 outputSchema 校验;这不等于真实客户端兼容已验证。最新处理意见见计划书末尾复核节;没有新增生产依赖或切换提供方。 @@ -35,7 +38,8 @@ ## 尚需补齐的验收 -- **实际客户端重连**:最近一次观测的 Codex 实例仍为 0.11.2;不能用新 stdio 测试替代宿主连接验证。重连后核对 hello 的版本、实例、构建与 schema,再执行代表性工具请求。该历史观察不是对任意当前客户端的实时判断。 +- **实际客户端 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。 @@ -48,3 +52,6 @@ | 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 或整体发布判为完成。 diff --git a/docs/codex_worklog.md b/docs/codex_worklog.md index 16c938b..b0a1943 100644 --- a/docs/codex_worklog.md +++ b/docs/codex_worklog.md @@ -664,3 +664,72 @@ - 用户明确要求将当前版本推送 GitHub;纳入当前 README、稳定性/恢复机制、Roslyn Host/Gateway 接入、验证脚本与既有文档改动,不升级版本号。目标 origin/main。 - 推送前 npm run typecheck 通过,git diff --check 通过;43 个待提交文件的常见凭据标记及大文件筛查未发现命中,构建产物和测试临时目录由现有忽略规则排除。 - npm test 执行失败:日志报告当前可发现的 .NET SDK 为 10.0.302,缺少 global.json 锁定的 10.0.303,ui-query-check 无法启动。未安装 SDK、放宽版本锁或将历史通过结果作为本轮验证。日志位于本地 test-tmp/pre-push-tests.log。本次推送保存当前工作版本,不表示完整回归或发布验收通过。 + +## 2026-09-09 — Pro 最新分析对照复核与下一轮规划(北京时间) + +- 目标:结合用户提供的网页版 Pro 全文、当前实现和 GitHub 一手经验,复核架构与下一轮优先级。会话读取工具不可用后由用户补充附件全文,现已完成对照;没有把预览截断处当成完整结论。本轮只修改既有计划/路线图/本日志,并在 test-tmp 创建隔离探针与回执,没有修复生产代码或替用户批准公共接口/输入政策变化。 +- 基线:[PR #30](https://github.com/linnnn89/WinCode/pull/30) 已于 13:56:43 合并,远端 main 为 `2235a4200c117a1c1389afce1fecd90c45908a73`。本轮开始时本地 bbc20ff 加工作区修改,收尾复查已为 `codex/current-version-20260909@a2d76f6` 且本轮编辑前干净;保留上节提交准备记录。本轮未运行提交/推送/分支切换。两次核对 13 个关键生产/构建文件 Git blob,均与远端基线一致;[比对回执](../test-tmp/review-20260909/pro-baseline-comparison.json)保留初始 HEAD 与各文件哈希,不声称整个工作区完全相同。 +- Pro 路径问题已用实际 ImpactAnalyzer 加受控提供方复现:[探针](../test-tmp/review-20260909/impact-identity-audit.mjs)、[回执](../test-tmp/review-20260909/impact-identity-report.json)。四个外部引用文件全部出现在 affectedFiles,但同名 Service.cs 和后缀 NewService.cs 被组件摘要遗漏,两个 Handler.cs 合为一个;不同启动目录下工作区内绝对目标无法解析。此证据验证实际聚合逻辑,不冒充真实 Roslyn 提供方验收。 +- [真实 Roslyn/MCP 探针](../test-tmp/review-20260909/roslyn-post-integration-audit.mjs)及[回执](../test-tmp/review-20260909/roslyn-audit-ebBSx0/report.json)仅使用现有构建、项目内 SDK 10.0.303 和生成项目;夹具 NuGet 源清空,未新增持久依赖或执行用户真实项目。正常 Save 引用有一处、source=roslyn,但重构建议误称文本降级和查询中断;新增无关 README 使旧定位 SNAPSHOT_STALE,生成无关 33 MiB 文件使查询 INPUT_BUDGET_EXCEEDED,该文件已移除。 +- 新增正确性发现:显式 CodePage=1252 的 Café 类在 dotnet build 中 0 警告/错误,Host 搜 Café 得到零结果和非法字符诊断,搜 Caf 却返回错误类名。源码核对指向 WorkspaceSession 冻结文档时强制 UTF-8,故提升为优先修复;不把这种精确位置结果视为正确符号的充分证据。 +- 反证结果:原先怀疑 Gateway 非正常退出可能留下 Code Host/BuildHost。受控 MSBuild 阻塞期间,Gateway 强制退出和客户端关闭两场景各观测 9 个相关进程,3 秒后均无残留,无额外清理动作才通过的情况。此次没有复现孤儿进程问题,不能据上游故障帖子宣称 WinCode 存在该缺陷。源代码显示上游互斥等待未受 Host 队列上限覆盖,但尚未负载验证,只登记为有界探针候选。 +- 实际 Codex 连接于 14:39 被动 hello:[回执](../test-tmp/review-20260909/current-client-1439.json)。版本 0.12.5,instanceId=`26920008-5f2c-40d5-85c9-ef63a3e41e6d`,buildId=`1c62d2c8e4b10ff345eba1721745a81998368ef3413eef399b000b9fbdac1aa6`,schemaHash=`d685329e95f1cc93087ea3f62ff96d1647e93dc0c4ad9f683d476ce79b8f3bdc`,15 工具;当前 codeProvider=serena、mode=degraded、semanticQueryUsable=false,Roslyn 未启用。更新旧客户端 0.11.2 待办,区分身份已验证与 Roslyn 功能未验收,未修改用户客户端配置。 +- 架构意见:保留现有 Registry/Router/CodeQueries/Adapter/独立 Code Host,不新建能力注册平台、全局图或数据库。清理对象限定为旧 Serena 专用判断/中性依赖命名、纯文本解析耦合、无用注入。ArchitectureAnalyzer 的 `_queries` 尚未使用,不能把静态项目声明图称为已经实现的 Roslyn 语义架构图;不同用途的 watcher 暂不合并。 +- 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)、[VuDZ/RoslynMcpServer](https://github.com/VuDZ/RoslynMcpServer)、[MadQ 作者实测](https://github.com/MadQ/RoslynMcp/blob/dev/docs/battle-test-results.md)。仅采纳具体职责和验证方法;不照搬 AddDocument、单一版本失效或他人的性能数字,也不把模型诊断/源码对应关系当成构建或运行时因果证据。 +- 计划更新:A1 先修路径与来源/覆盖说明;A2 修编码及无关大文件阻断,明确输入集合并保留真实依赖失效;B 把真实脚本接入 CI、统一 SDK 选择、补 Code Host/BuildHost 清单及实际客户端验收;C 贯通可选 symbolLocation,并同步中文注释、Skill 和契约。E4 独立待决;全面性能优化与 WPF 精确导航在这些步骤后按实测需要进入。 +- 验证边界:本轮运行上述隔离诊断并完成清理,没有重跑整套 npm check。344/344 和 MCP 13 场景仍是前一接入阶段回执;另一个提交准备流程的默认 npm test SDK 发现失败已核对原日志,纳入 B,不删历史失败、不放宽 global.json、不宣称本轮默认环境通过。Pro 与本轮为不同证据来源,本轮反证自审仍不等于独立模型/人工审核。 +- **USER_DECISION_REQUIRED:** 本次为复核与规划,新的修复尚未实施。A2 输入覆盖政策、C 公共参数扩展、E4 文本策略、B 的可选交付形态及实际项目求值范围须在对应实施前明确;已批准直接 Roslyn 方向及项目内依赖权限不重复申请。下一步推荐从 A1/A2 开始,不为完成规划额外申请安装、客户端控制或发布权限。 +- 文档收尾:三份文件的代码围栏、新增的 10 个本地链接和 git diff --check 通过,13 个关键实现/构建文件哈希仍与复核基线一致,受管改动仅这三份文档。范围检查首次因 Git 默认将中文路径转义而误报;改用 NUL 分隔的原始路径输出后通过,未放宽范围断言。测试探针/回执仍在既有忽略目录 test-tmp,不列入发布包。 + +## 2026-09-09 — A1/A2 正确性与可补充输入实施(北京时间) + +- 授权与基线:用户要求开始,随后选择“编译相关输入之外允许显式补充文件”。只读 fetch 更新 origin/main 到 2235a42,确认其树与本地原 a2d76f6 一致后建立 codex/roslyn-correctness,保留三份已有计划修改。没有提交、推送、全局安装、客户端配置修改或真实用户项目求值。 +- A1:ImpactAnalyzer 改用工作区完整文件身份、可用项目身份作聚合键,名称只展示;修正绝对路径、分隔符/大小写、同名/后缀文件及链接源码组件计数。RefactorAssistant 不把正常 Roslyn 有限结果称为文本回退或中断;未解析的 Roslyn 结果不再附加正则回退说明。Roslyn 已知加载/查询/清理失败按现有 AdapterLastError 汇总,清理失败保留不可恢复标记,不主动探测上游。 +- 编码:WorkspaceSession 的冻结文本使用 document.GetTextAsync 所提供的 Encoding,遵循 MSBuild CodePage/BOM;四种生成夹具验证 Café 符号、引用和 UTF-16 偏移,未改写用户源码或引入编码依赖。新增函数/接口与关键身份、缓存语义补充中文注释,Program 的“尚未接入 Gateway”过时说明已修正。 +- A2:WorkspaceInputs 采用约定输入候选 + 实际文档/AdditionalFiles/分析配置/程序集 + 祖先配置 + 显式 additionalInputs。非标准 Import 和隐式数据须显式补充,未声称自动识别所有依赖。自动发现 .cs 后仍由 MSBuild Compile 决定加载;未加载的普通二进制和 README 不计输入字节。目录枚举和输入预算保留,必要输入超限不截断。每个文件流式散列,仅冻结源码保存正文,global.json 签名单独保留,不把 Files.Count 当成总输入数。 +- 配置与边界:additionalInputs 最多 32 个根内相对文件,JSON 最长 4096;Node 与 Host 均校验字面路径、重复及边界,Host 验证链接和每次存在性。缺失阻断重载,恢复后显式搜索;切换根后按新根解释。只经显式启动 JSON/原生 argv 传递,不接受 MCP 工具参数或自动读取目标仓库配置。ready 的 inputPolicy.version=1 和实际列表必须匹配,旧/漏配 Host 拒绝并清理,协议 v2 和 15 个工具名不变。 +- 针对性失败:首轮 36 项有 1 项因 Windows 大写文件名推导符号名而失败,修正匹配后 36/36。中间 [Host 回执](../test-tmp/roslyn-host/fixture-fif5GD/report.json)在恢复补充文件后于第 23 场景返回 INPUTS_CHANGED;当时信息未区分指纹与事件,不能事后断言唯一根因。[八轮隔离恢复](../test-tmp/review-20260909/input-race-kqPHuT/report.json)未再次复现。保留原失败,拆分错误说明,增加每次加载前最多四个 50 ms 的事件稳定观察窗;没有增加业务重放或放松加载后检查。 +- 反证自审:非标准 custom.rules 真正作为 MSBuild Import 改变条件与引用数;AdditionalFiles 的非标准后缀仍被跟踪;丢失补充项后 reload 不得通过过滤旧文件而丢掉要求。实际 MSBuild Touch 在内容哈希不变时仍触发拒绝,证明稳定等待没有吞掉求值期间的事件。移除该写入 target 后可显式恢复。此为作者自审,不冒充独立审核或全磁盘原子一致证明。 +- 验收:[核心回执](../test-tmp/check/2026-09-09T07-20-21-008Z-core/report.json)12 阶段通过,350/350、0 失败/取消/跳过,覆盖最终 TypeScript、类型检查、锁定构建、生产 stdio 和现有清单。其后 Host 等待补修由[最终 Host 58 场景](../test-tmp/roslyn-host/fixture-sDJxDM/report.json)及[最终真实 MCP 16 场景](../test-tmp/roslyn-gateway/run-fLDNDI/report.json)验证,构建 0 警告/错误;MCP 覆盖补充配置端到端传递、无关 33 MiB 文件、缺失/恢复、绝对目标、有限重构说明、A→B→A 和真实 MSBuild 取消/崩溃/超时后已观测自有进程退出。中间 46/56/14/16 结果不累加为终验数。 +- 环境与交付:复用现有项目内 SDK 10.0.303、锁定 NuGet 缓存及已有 Node 依赖,仅当前测试子进程设置 SDK 路径,不改系统 PATH/global.json。先前默认入口发现不到 SDK 的失败仍保留,B 才统一普通入口/CI 选择。仓内 SKILL.md、code/diagnostics 手册、计划和路线图同步;未部署全局 Skill 或声称当前 Codex 已加载新实现。正式清单仍不含 Code Host,不能用现有 delivery matched 代替 B。 +- 资料核对:[MSBuild 增量构建](https://learn.microsoft.com/en-us/visualstudio/msbuild/incremental-builds?view=vs-2022)与[Exec](https://learn.microsoft.com/en-us/visualstudio/msbuild/exec-task?view=vs-2022)用于解释自定义输入无法凭扩展名穷尽;编码 API 根据已安装 Roslyn 5.9.0 XML 契约和真实结果核对。没有下载新工具或增加依赖。 +- 本阶段 A1/A2 已完成;B/C、默认迁移、E4 仍未实施。输入补充方案已经批准,不再重复询问;后续 E4 文本策略、实际项目求值范围和正式交付形态按对应阶段对齐。持续写入仍可能拒绝,未列入的自定义输入、生成器、运行时动态调用和全规模性能不在本轮保证范围。 +- 收尾检查:六份变更文档的围栏及新增 19 个本地链接有效,Skill quick_validate 和 git diff --check 通过。重新生成清单时默认 dotnet 路径再次复现锁定 SDK 发现失败;改为本轮已使用的项目内 SDK 命令环境后生成/验证成功,未声称修好了默认入口。最终 contentId=`a0298c62eaf3c51710befaed42d1a96087902fe78371947645ad74b98184b4ab`,matched=true,回执 `test-tmp/roslyn-input-delivery.json`;仍只证明既有清单范围。最后的文档/注释更新没有改变已验收的功能逻辑,不重复扩展测试预算。 + +## 2026-09-09 — 交付、去 Serena 与职责拆分实施(北京时间) + +- 用户要求将前轮 A1/A2 后续的 B/C 与巨型模块问题逐项处理;没有把握时暂停对应事项询问。 +- 已明确三项产品决定:彻底退役外部 Serena;未提供 Roslyn 项目配置时以本地文本模式启动并说明语义未配置;文本 source 改为 local-text,旧调用方需迁移。版本拟同步为 0.13.0。 +- 实施顺序:统一 SDK 与脚本辅助函数 → Code Host 发布清单、构建身份及真实 CI → 本地文本能力独立/移除外部接入 → 精确符号贯穿影响与重构 → 工作区/UIA/上下文/历史测试职责拆分 → 真实回归与文档同步。保留前轮全部未提交改动;不发布远端、不修改真实客户端配置。 +- 当前 SDK 入口已用项目内现有 10.0.303 验证,无新安装。Code Host 身份、可选完整交付组件与 CI 步骤已编写,尚待整体验证。 +- 已提取原有文本扫描预算/路径/编码/取消规则及纯解析函数。删除外部连接实现及专用启动/协议夹具;外部上游格式/握手专属测试随接口退役,通用身份、UNKNOWN 风险、文本边界和工作区恢复测试迁移保留,最终测试数量会相应变化。 +- 初次针对性运行 58 项中 57 通过,1 项仍修改旧适配器私有 timeouts 字段;正在迁移该测试注入位置,未削弱扫描截止断言。完整回归、发布目录语义验收与桌面验证尚未完成。 + + +## 2026-09-09 — B/C 与职责拆分本地验收更新(北京时间 17:02) + +- 用户已明确选择:彻底退役外部 Serena;缺少 Roslyn 配置时默认 local-text 并提示语义未配置;旧 source 改为 local-text,同步契约、测试与 Skill。版本标记 0.13.0 为本地待发布。旧配置/启动器/外部专用夹具删除;本地文本扫描和明确退化边界保留。 +- B 已实现:维护脚本统一选择已安装 SDK(精确 global.json;无下载/全局改动);check 发布 Code Host 全目录,交付清单覆盖 deps/runtimeconfig/Roslyn/BuildHost 并验证版本、Release、协议。CI 的 Node 22 增加真实 Host/MCP;尚未推送,因此没有本轮远端运行结果。 +- C 已实现:影响/重构接受可选 symbolLocation,保留简单名称入口及别名。先验证已选快照再搜索该声明,拒绝过期/错名,保持 UNKNOWN/不完整语义;本地文本模式在扫描前拒绝精确定位。 +- 职责拆分:Workspace 保留根变更/Git/trash,分出 Browser/Discovery/Contracts;本地文本分出扫描/声明解析;Cache 分出 WorkspaceFingerprint;Context 拆出符号收集/格式化,响应拆出纯范围覆盖。UIA Host 分出 Win32、窗口定位、抓图、树读取及契约;FlaUiAdapter 提取纯协议解析,保留集中进程生命周期。删除 Architecture/Refactor 未使用的查询注入。Router 锁、排空、恢复仍集中,未机械拆散事务状态。 +- 测试和脚本:两份历史大测试按功能拆成 13 份并隔离缓存;退役外部协议测试,保留通用取消/恢复/边界证据。验收脚本共享 SDK/进程观察,Host 和 Gateway 场景分组。没有为追求测试数量保留不存在的连接接口。 +- 本轮失败与修复:首轮核心 302/303,TTL 50ms 被并发 I/O 提前耗尽,改用可控时钟保留前后断言;中文异地 Host 查询正常,但 PowerShell 进程观察非 UTF-8 导致匹配失败,已明确编码并通过完整复验。新增版本测试发现旧输入策略 mock 缺少 id:null,纠正该夹具,避免提前协议错误形成假阳性。Skill 校验器默认 GBK 解码失败后以 Python -X utf8 通过,未改全局设置。 +- 当前已通过:[核心 307/307](../test-tmp/check/2026-09-09T08-57-16-594Z-core/report.json),[桌面夹具 35/35](../test-tmp/check/2026-09-09T08-58-14-312Z-desktop/report.json),[Host 58 场景](../test-tmp/roslyn-host/fixture-sPovtl/report.json),[真实 MCP 19 场景](../test-tmp/roslyn-gateway/run-NqA7IS/report.json),[混合负载 70 调用/10 轮](../test-tmp/mixed-load/run-BcmGpg/report.json)。MCP 使用完整发布目录的中文/空格异地副本和不同 cwd,观测真实 BuildHost/targets 子进程退出;仍复用本机 SDK,不能称为干净机器安装验收。末尾无用构造参数清理另由类型检查与针对性测试验证,最终清单将在收尾重建。 +- 反证自审:不仅断言成功;验证缺失/混合 BuildHost 文件清单、错误版本/Debug Host、旧重载定位不可被自动搜索替换,以及被拒绝的文本定位不触发扫描。此为作者自审,非独立审核。 +- 文档:README、贡献指南、架构图、仓内 Skill 的默认后端/定位/错误/发布目录说明同步,保留 Serena 致谢及历史日志。 +- USER_DECISION_REQUIRED:E4 普通错误文本迁移仍未批准,已请求具体选择;本轮不改变该行为。实际 Codex 连接尚未变更:启用 Roslyn 需明确目标项目、Configuration、TFM 和求值授权;发布/推送及全局 Skill 部署尚未执行。 + +### 同轮最终核对(北京时间 17:05) + +- 无用注入清理后[最终核心 307/307](../test-tmp/check/2026-09-09T09-01-27-993Z-core/report.json)通过;此前 53 项针对性回归亦通过。 +- 修正 Gateway 验收发布输出写入本轮 fixture 目录,避免改写正式交付 publish;[最终 MCP 19 场景](../test-tmp/roslyn-gateway/run-R15auS/report.json)通过,随后原交付清单仍 matched=true(contentId=53fcfb01f044dd0acb71b9d086397f30d960208d678308ae48e0bd3697e1910c)。 +- 48 个本地 TypeScript 模块静态 import/export 图无环;43 个测试文件各登记一次;Skill 校验、相对链接/代码围栏及 git diff --check 通过。桌面验收后仅 UIA Program 尾部空白整理,最终 check 已重新发布。 +- 用户回复要求先详细解释 E4 两方案利弊,尚未选择;已保持现有普通错误 content/isError,不提前新增 structuredContent。后续等待明确选择。 + + +## 2026-09-09 — 当前开发状态上传 GitHub(北京时间 17:13) + +- 用户确认尚未广泛分发,可以直接采用 E4 方案二;普通 Gateway 异常已开始统一 JSON 文本与 structuredContent,错误码/恢复动作不从消息猜测,UI/trash 领域载荷保留。随后用户要求先上传当前状态,因此停止扩展实现,以草稿 PR 保存当前完整相关变更。 +- 本次验证:[错误契约 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 未改动。 diff --git a/package-lock.json b/package-lock.json index ea00e49..b3b1e04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wincode-mcp", - "version": "0.12.5", + "version": "0.13.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wincode-mcp", - "version": "0.12.5", + "version": "0.13.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/client": "2.0.0", diff --git a/package.json b/package.json index 2bea98c..b860a91 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wincode-mcp", - "version": "0.12.5", + "version": "0.13.0", "description": "Windows-first MCP gateway: .NET project graph, evidence-bounded context, honest change-impact, long-running process hygiene", "main": "dist/index.js", "type": "module", @@ -12,12 +12,11 @@ "build": "node scripts/build.mjs", "start": "node dist/index.js", "dev": "tsx src/index.ts --development", - "test": "tsx --test tests/tdd-suite.test.ts tests/v05-stability.test.ts tests/stage1-cleanup.test.ts tests/ui-hardening.test.ts tests/ui-source-review.test.ts tests/v071-acceptance.test.ts tests/ui-background.test.ts tests/ui-audit.test.ts tests/ui-query.test.ts tests/context-efficiency.test.ts tests/agent-efficiency-benchmark.test.ts tests/workspace-summary.test.ts tests/runtime-identity.test.ts tests/runtime-contract.test.ts tests/context-coverage.test.ts tests/serena-identity.test.ts tests/ui-code-candidates.test.ts tests/skill-sync.test.ts tests/serena-fallback.test.ts tests/repomix-disabled.test.ts tests/workspace-watch-close.test.ts tests/workspace-lifecycle.test.ts tests/failure-recovery.test.ts tests/tool-contracts.test.ts tests/architecture-boundaries.test.ts tests/lifecycle-cancellation.test.ts tests/delivery-contract.test.ts tests/roslyn-contracts.test.ts", + "test": "tsx --test tests/core-cache.test.ts tests/workspace-files.test.ts tests/text-symbols.test.ts tests/context-packing.test.ts tests/composite-tools.test.ts tests/mcp-stdio.test.ts tests/stability-lifecycle.test.ts tests/cache-budgets.test.ts tests/process-failures.test.ts tests/request-concurrency.test.ts tests/evidence-confidence.test.ts tests/watch-invalidation.test.ts tests/resource-cleanup.test.ts tests/stage1-cleanup.test.ts tests/ui-hardening.test.ts tests/ui-source-review.test.ts tests/v071-acceptance.test.ts tests/ui-background.test.ts tests/ui-audit.test.ts tests/ui-query.test.ts tests/context-efficiency.test.ts tests/agent-efficiency-benchmark.test.ts tests/workspace-summary.test.ts tests/runtime-identity.test.ts tests/runtime-contract.test.ts tests/context-coverage.test.ts tests/semantic-identity.test.ts tests/ui-code-candidates.test.ts tests/skill-sync.test.ts tests/local-text.test.ts tests/repomix-disabled.test.ts tests/workspace-watch-close.test.ts tests/workspace-lifecycle.test.ts tests/failure-recovery.test.ts tests/tool-contracts.test.ts tests/architecture-boundaries.test.ts tests/lifecycle-cancellation.test.ts tests/delivery-contract.test.ts tests/roslyn-contracts.test.ts", "test:verify": "tsx tests/verify.ts", "benchmark:agent": "tsx scripts/benchmark-agent-efficiency.ts", "test:benchmark": "tsx --test tests/agent-efficiency-benchmark.test.ts", "test:product": "tsx scripts/verify-product-tasks.ts", - "test:serena-real": "tsx scripts/verify-serena-real.ts", "test:repomix-real": "tsx scripts/verify-repomix-real.ts", "test:mixed-load": "tsx scripts/verify-mixed-load.ts", "test:error-contracts": "tsx scripts/verify-error-contracts.ts", @@ -38,9 +37,9 @@ "windows", "ai-agent", "coding-agent", - "serena", "repomix", - "code-intelligence" + "code-intelligence", + "roslyn" ], "author": "WinCode Team", "license": "MIT", diff --git a/scripts/benchmark-agent-efficiency.ts b/scripts/benchmark-agent-efficiency.ts index 20c6498..0ade7bb 100644 --- a/scripts/benchmark-agent-efficiency.ts +++ b/scripts/benchmark-agent-efficiency.ts @@ -120,7 +120,7 @@ async function createFixture(root: string) { async function runCase(testCase: Case, policy: Policy, repetition: number, hooks: BenchmarkHooks) { const root = await fs.mkdtemp(path.join(os.tmpdir(), 'wincode-benchmark-')); const config = getDefaultConfig(root); - config.adapters.serena.enabled = false; + config.adapters.flaui.enabled = false; config.adapters.repomix.useCli = false; const router = new ToolRouter(config); @@ -128,9 +128,9 @@ async function runCase(testCase: Case, policy: Policy, repetition: number, hooks const client = new Client({ name: 'agent-efficiency-benchmark', version: '1' }); const overlap = new EvidenceOverlap(); let symbolQueries = 0; - const originalFind = router.serena.findSymbolsDetailed.bind(router.serena); + const originalFind = router.text.findSymbolsDetailed.bind(router.text); // Count invocations, including cache hits; keep production results and cache behavior intact. - router.serena.findSymbolsDetailed = async (...args) => { symbolQueries++; return originalFind(...args); }; + router.text.findSymbolsDetailed = async (...args) => { symbolQueries++; return originalFind(...args); }; const calls: { args: Args; elapsedMs: number; returnedCharacters: number; status: string; accepted: boolean; error?: string }[] = []; const errors: { phase: string; message: string }[] = []; const actions: { action: string; revision: number; accepted?: boolean }[] = []; diff --git a/scripts/check.mjs b/scripts/check.mjs index 840fa00..b3e862f 100644 --- a/scripts/check.mjs +++ b/scripts/check.mjs @@ -2,6 +2,7 @@ import fs from 'node:fs/promises'; import path from 'node:path'; import { spawnSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; +import { resolveDotnet } from './lib/dotnet.mjs'; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const pkg = JSON.parse(await fs.readFile(path.join(root, 'package.json'), 'utf8')); @@ -26,6 +27,7 @@ if (new Set(declared).size !== declared.length || found.length !== declared.leng if (inventoryOnly) { console.log(JSON.stringify({ tests: declared.length, groups: Object.fromEntries(Object.entries(groups).map(([name, files]) => [name, files.length])) })); } else { + const toolchain = resolveDotnet(root); const directory = path.join(root, 'test-tmp/check', `${new Date().toISOString().replace(/[:.]/g, '-')}-${desktop ? 'desktop' : 'core'}`); await fs.mkdir(directory, { recursive: true }); const report = { version: pkg.version, mode: desktop ? 'desktop' : 'core', startedAt: new Date().toISOString(), @@ -33,7 +35,9 @@ if (inventoryOnly) { async function run(name, command, args) { console.log(`[check] ${name}`); const started = Date.now(); - const result = spawnSync(command, args, { cwd: root, encoding: 'utf8', windowsHide: true, timeout: 300000, maxBuffer: 8 * 1024 * 1024 }); + const result = spawnSync(command === 'dotnet' ? toolchain.dotnet : command, args, { + cwd: root, env: toolchain.env, encoding: 'utf8', windowsHide: true, timeout: 300000, maxBuffer: 8 * 1024 * 1024, + }); const output = `${result.stdout ?? ''}${result.stderr ?? ''}`; await fs.writeFile(path.join(directory, `${name}.log`), output); const success = !result.error && result.status === 0; @@ -53,6 +57,7 @@ if (inventoryOnly) { const tsc = path.join(root, 'node_modules/typescript/bin/tsc'); const tsx = path.join(root, 'node_modules/tsx/dist/cli.mjs'); const native = 'tools/WinCode.UIA.Host/WinCode.UIA.Host.csproj'; + const codeHost = 'tools/WinCode.Code.Host/WinCode.Code.Host.csproj'; const audit = 'tests/fixtures/ui-audit-check/ui-audit-check.csproj'; const query = 'tests/fixtures/ui-query-check/ui-query-check.csproj'; const wpf = 'tests/fixtures/wpf-ui-review/wpf-ui-review.csproj'; @@ -67,9 +72,10 @@ if (inventoryOnly) { } else { await node('typecheck', [tsc, '-p', 'tsconfig.test.json']); await node('build-gateway', ['scripts/build.mjs']); - for (const [name, project] of [['host', native], ['audit', audit], ['query', query]]) + for (const [name, project] of [['host', native], ['code-host', codeHost], ['audit', audit], ['query', query]]) await run(`restore-${name}`, 'dotnet', ['restore', project, '--locked-mode']); await run('publish-host', 'dotnet', ['publish', native, '-c', 'Release', '-r', 'win-x64', '--no-self-contained', '--no-restore', ...deterministic]); + await run('publish-code-host', 'dotnet', ['publish', codeHost, '-c', 'Release', '--no-self-contained', '--no-restore', ...deterministic]); await run('build-audit', 'dotnet', ['build', audit, '-c', 'Debug', '--no-restore', ...deterministic]); await run('build-query', 'dotnet', ['build', query, '-c', 'Release', '--no-restore', ...deterministic]); report.tests = testTotals(await node('regression', [tsx, '--test', '--test-reporter=tap', ...groups.test])); diff --git a/scripts/delivery-manifest.mjs b/scripts/delivery-manifest.mjs index 466f62d..2c62875 100644 --- a/scripts/delivery-manifest.mjs +++ b/scripts/delivery-manifest.mjs @@ -5,8 +5,10 @@ import { spawnSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; import { collectBuildInputs, createBuildManifest, fingerprint } from './build.mjs'; import { managedFiles } from './sync-skill.mjs'; +import { resolveDotnet } from './lib/dotnet.mjs'; export const hostDirectory = 'tools/WinCode.UIA.Host/bin/Release/net10.0-windows/win-x64/publish'; +export const codeHostDirectory = 'tools/WinCode.Code.Host/bin/Release/net10.0/publish'; const rootDirectory = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const sha256 = data => createHash('sha256').update(data).digest('hex'); const settings = ['package.json', 'package-lock.json', 'global.json', @@ -54,7 +56,7 @@ async function records(root, files) { return result; } -export async function collectDelivery(root, hostIdentity, toolchains) { +export async function collectDelivery(root, hostIdentity, toolchains, codeHostIdentity) { const pkg = JSON.parse(await fs.readFile(path.join(root, 'package.json'), 'utf8')); const gateway = JSON.parse(await fs.readFile(path.join(root, 'dist/build-manifest.json'), 'utf8')); if (gateway.version !== pkg.version || hostIdentity?.version !== pkg.version || hostIdentity.configuration !== 'Release') @@ -68,11 +70,28 @@ export async function collectDelivery(root, hostIdentity, toolchains) { const hostFiles = await inventory(root, hostDirectory); for (const required of ['WinCode.UIA.Host.exe', 'WinCode.UIA.Host.dll', 'WinCode.UIA.Host.deps.json', 'WinCode.UIA.Host.runtimeconfig.json']) if (!hostFiles.includes(`${hostDirectory}/${required}`)) throw new Error(`Missing Host sidecar: ${required}`); + // 可选组件必须整体交付;只复制入口 DLL 会遗漏真实求值使用的 BuildHost 子进程。 + let codeHost; + if (codeHostIdentity !== undefined) { + if (codeHostIdentity.version !== pkg.version || codeHostIdentity.configuration !== 'Release' || codeHostIdentity.protocolVersion !== 2) + throw new Error('Code Host version, Release configuration and protocol must agree with the Gateway.'); + const files = await inventory(root, codeHostDirectory); + for (const required of ['WinCode.Code.Host.dll', 'WinCode.Code.Host.deps.json', 'WinCode.Code.Host.runtimeconfig.json', + 'Microsoft.CodeAnalysis.Workspaces.MSBuild.dll', 'BuildHost-netcore/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.dll', + 'BuildHost-netcore/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.deps.json', + 'BuildHost-netcore/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.runtimeconfig.json']) { + if (!files.includes(`${codeHostDirectory}/${required}`)) throw new Error(`Missing Code Host sidecar: ${required}`); + } + codeHost = { identity: codeHostIdentity, files: await records(root, files) }; + } return { version: pkg.version, toolchains, components: { gateway: { buildId: gateway.buildId, files: gatewayFiles }, host: { identity: hostIdentity, files: await records(root, hostFiles) }, + ...(codeHost ? { codeHost } : {}), skill: { files: await records(root, managedFiles.map(file => `skills/wincode/${file}`)) }, - configuration: { files: await records(root, settings) }, + configuration: { files: await records(root, [...settings, ...(codeHost ? [ + 'tools/WinCode.Code.Host/WinCode.Code.Host.csproj', 'tools/WinCode.Code.Host/packages.lock.json', + ] : [])]) }, } }; } @@ -81,24 +100,33 @@ export function deliveryId(delivery) { return sha256(JSON.stringify(delivery)); export async function verifyDelivery(root, manifest) { if (manifest.formatVersion !== 1 || !manifest.delivery || deliveryId(manifest.delivery) !== manifest.contentId) throw new Error('Invalid delivery manifest identity.'); - const actual = await collectDelivery(root, manifest.delivery.components.host.identity, manifest.delivery.toolchains); + const actual = await collectDelivery(root, manifest.delivery.components.host.identity, manifest.delivery.toolchains, + manifest.delivery.components.codeHost?.identity); if (deliveryId(actual) !== manifest.contentId) throw new Error('Delivery contents changed or are incomplete.'); return { contentId: manifest.contentId, version: actual.version, matched: true }; } -function output(command, args, root, input) { - const result = spawnSync(command, args, { cwd: root, input, encoding: 'utf8', windowsHide: true, timeout: 10000, maxBuffer: 65536 }); +function output(command, args, root, input, env = process.env) { + const result = spawnSync(command, args, { cwd: root, env, input, encoding: 'utf8', windowsHide: true, timeout: 10000, maxBuffer: 65536 }); if (result.error || result.status !== 0) throw new Error(`Delivery probe failed: ${result.error?.message ?? result.stderr ?? result.status}`); return result.stdout.trim(); } export async function writeDelivery(root = rootDirectory) { + const sdk = resolveDotnet(root); const hostResponse = JSON.parse(output(path.join(root, hostDirectory, 'WinCode.UIA.Host.exe'), [], root, JSON.stringify({ schemaVersion: '1.0', requestId: 'delivery-check', action: 'health' }) + '\n')); if (hostResponse.success !== true || hostResponse.status !== 'healthy') throw new Error('Published Host health failed.'); - const toolchains = { node: process.versions.node, dotnet: output('dotnet', ['--version'], root), + const toolchains = { node: process.versions.node, dotnet: sdk.sdkVersion, npm: process.env.npm_execpath ? output(process.execPath, [process.env.npm_execpath, '--version'], root) : null }; - const delivery = await collectDelivery(root, hostResponse.hostIdentity, toolchains); + const codeHostPath = path.join(root, codeHostDirectory, 'WinCode.Code.Host.dll'); + const installed = await fs.stat(path.join(root, codeHostDirectory)).catch(error => { + if (error.code === 'ENOENT') return null; + throw error; + }); + const codeResponse = installed ? JSON.parse(output(sdk.dotnet, [codeHostPath, '--identity'], root, undefined, sdk.env)) : undefined; + if (codeResponse && codeResponse.success !== true) throw new Error('Code Host identity probe failed.'); + const delivery = await collectDelivery(root, hostResponse.hostIdentity, toolchains, codeResponse?.hostIdentity); const git = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8', windowsHide: true, timeout: 3000 }); const revision = git.status === 0 && /^[a-f0-9]{40,64}$/.test(git.stdout.trim()) ? git.stdout.trim() : null; const manifest = { formatVersion: 1, contentId: deliveryId(delivery), delivery, revision, createdAt: new Date().toISOString() }; diff --git a/scripts/lib/dotnet.mjs b/scripts/lib/dotnet.mjs new file mode 100644 index 0000000..f67daba --- /dev/null +++ b/scripts/lib/dotnet.mjs @@ -0,0 +1,60 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; + +/** + * 统一构建与验收脚本的 SDK 选择。显式 WINCODE_DOTNET_PATH 优先,其次项目内固定版本, + * 最后使用现有 DOTNET_HOST_PATH / PATH。只探测已安装程序,绝不下载或放宽 global.json。 + */ +export function resolveDotnet(root, inherited = process.env) { + const expected = JSON.parse(fs.readFileSync(path.join(root, 'global.json'), 'utf8')).sdk?.version; + if (typeof expected !== 'string' || !/^\d+\.\d+\.\d+$/.test(expected)) { + throw new Error('global.json must declare an exact stable SDK version.'); + } + const executable = process.platform === 'win32' ? 'dotnet.exe' : 'dotnet'; + const local = path.join(root, '.deps', `dotnet-${expected}`, executable); + const explicit = inherited.WINCODE_DOTNET_PATH; + let dotnet; + if (explicit !== undefined) { + if (!explicit || !path.isAbsolute(explicit)) throw new Error('WINCODE_DOTNET_PATH must be an absolute installed dotnet path.'); + dotnet = explicit; + } else if (fs.existsSync(local)) { + dotnet = local; + } else if (inherited.DOTNET_HOST_PATH) { + dotnet = inherited.DOTNET_HOST_PATH; + } else { + dotnet = (inherited.PATH ?? inherited.Path ?? '').split(path.delimiter) + .map(directory => path.join(directory.replace(/^"|"$/g, ''), executable)) + .find(candidate => path.isAbsolute(candidate) && fs.existsSync(candidate)); + } + if (!dotnet || !path.isAbsolute(dotnet) || !fs.statSync(dotnet, { throwIfNoEntry: false })?.isFile()) { + throw new Error(`Installed .NET SDK ${expected} not found; set WINCODE_DOTNET_PATH to its dotnet executable.`); + } + dotnet = path.resolve(dotnet); + const env = { ...inherited, DOTNET_ROOT: path.dirname(dotnet), DOTNET_HOST_PATH: dotnet, + PATH: `${path.dirname(dotnet)}${path.delimiter}${inherited.PATH ?? inherited.Path ?? ''}`, + DOTNET_CLI_HOME: inherited.DOTNET_CLI_HOME ?? path.join(root, '.deps/dotnet-cli-home'), + NUGET_PACKAGES: inherited.NUGET_PACKAGES ?? path.join(root, '.deps/nuget-packages'), + NUGET_HTTP_CACHE_PATH: inherited.NUGET_HTTP_CACHE_PATH ?? path.join(root, '.deps/nuget-http-cache'), + DOTNET_NOLOGO: '1', DOTNET_CLI_TELEMETRY_OPTOUT: '1' }; + // Windows 环境变量不区分大小写;只保留一个 PATH,避免子进程选回另一套 SDK。 + for (const key of Object.keys(env)) if (key.toUpperCase() === 'PATH' && key !== 'PATH') delete env[key]; + const result = spawnSync(dotnet, ['--version'], { + cwd: root, env, encoding: 'utf8', windowsHide: true, timeout: 10000, maxBuffer: 65536, + }); + if (result.error || result.status !== 0 || result.stdout.trim() !== expected) { + throw new Error(`Selected dotnet cannot provide SDK ${expected}: ${result.error?.message || result.stderr || result.stdout}`); + } + return { dotnet, env, sdkVersion: expected }; +} + +/** 执行有界 SDK 命令;失败保留原因,不自动安装依赖或换 SDK 重试。 */ +export function runDotnet(toolchain, args, cwd, timeout = 180000) { + const result = spawnSync(toolchain.dotnet, args, { + cwd, env: toolchain.env, encoding: 'utf8', windowsHide: true, timeout, maxBuffer: 2 * 1024 * 1024, + }); + if (result.error || result.status !== 0) { + throw new Error(`dotnet ${args[0]} failed: ${result.error ?? ''}\n${result.stdout ?? ''}\n${result.stderr ?? ''}`); + } + return result.stdout; +} diff --git a/scripts/lib/owned-processes.mjs b/scripts/lib/owned-processes.mjs new file mode 100644 index 0000000..26f605e --- /dev/null +++ b/scripts/lib/owned-processes.mjs @@ -0,0 +1,23 @@ +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; + +/** 只读记录测试所有进程树;命令只插入正整数 PID,创建时间用于排除 PID 复用。 */ +export function ownedProcesses(pid) { + assert.ok(Number.isSafeInteger(pid) && pid > 0); + const command = `[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false); $all = @(Get-CimInstance Win32_Process); $ids = @(${pid}); do { $more = @($all | Where-Object { $_.ParentProcessId -in $ids -and $_.ProcessId -notin $ids }); $ids += @($more | ForEach-Object { $_.ProcessId }) } while ($more.Count -gt 0); @($all | Where-Object { $_.ProcessId -in $ids } | Select-Object ProcessId,ParentProcessId,CreationDate,Name,CommandLine) | ConvertTo-Json -Compress`; + const result = spawnSync('powershell.exe', ['-NoProfile', '-Command', command], { + encoding: 'utf8', windowsHide: true, timeout: 20000, + }); + assert.equal(result.status, 0, result.error?.message ?? result.stderr); + const value = JSON.parse(result.stdout || '[]'); + return Array.isArray(value) ? value : [value]; +} + +/** 同时核对 PID 和创建时间;此函数只断言退出,不终止任何进程。 */ +export function assertExited(processes) { + for (const process of processes) { + const alive = ownedProcesses(process.ProcessId).some(current => + current.ProcessId === process.ProcessId && current.CreationDate === process.CreationDate); + assert.ok(!alive, `Owned process survived: ${process.ProcessId}`); + } +} diff --git a/scripts/roslyn/gateway-lifecycle.mjs b/scripts/roslyn/gateway-lifecycle.mjs new file mode 100644 index 0000000..ee70dca --- /dev/null +++ b/scripts/roslyn/gateway-lifecycle.mjs @@ -0,0 +1,46 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { assertExited } from '../lib/owned-processes.mjs'; + +/** 仅对入口生成的隔离项目注入 MSBuild 故障,复用真实客户端及自有进程观察。 */ +export async function verifyGatewayLifecycle({ root, a, host, appProject, client, call, markerReady, codeProcesses, report, references, integerTarget }) { + // 获准的隔离 targets 只启动测试脚本;标记写入 .cache,避免用写入结果假装另一个业务输入。 + const blocker = path.join(root, 'blocker.mjs'); + await fs.writeFile(blocker, "import fs from 'node:fs'; fs.writeFileSync(process.argv[2], String(process.pid)); setInterval(() => {}, 1000);\n"); + const marker = path.join(a, '.cache/block.started'); + const escape = value => value.replaceAll('&', '&').replaceAll('"', '"').replaceAll('<', '<'); + const targetXml = ``; + for (const mode of ['cancel', 'crash', 'timeout']) { + console.log(`[roslyn-gateway] active MSBuild ${mode}`); + await call('workspace_open', { path: a }); + await fs.rm(marker, { force: true }); + await fs.writeFile(path.join(a, 'App/App.csproj'), appProject.replace('', targetXml + '')); + const controller = new AbortController(); + const pending = client.callTool({ name: 'wincode_find_code_symbol', arguments: { query: 'Api' } }, { timeout: 30000, signal: controller.signal }); + const settled = pending.then(value => ({ value }), error => ({ error: String(error) })); + await markerReady(marker); + const processes = codeProcesses(); + assert.ok(processes.some(item => item.CommandLine?.includes('BuildHost')), 'actual BuildHost must be observed during design-time work'); + assert.ok(processes.some(item => item.CommandLine?.includes(blocker)), 'blocking target child must be observed'); + report.processes.push({ mode, processes }); + if (mode === 'cancel') controller.abort(); + if (mode === 'crash') { + const hostProcess = processes.find(item => item.CommandLine?.includes(host)); + assert.ok(hostProcess); + process.kill(hostProcess.ProcessId, 'SIGKILL'); + } + const outcome = await settled; + if (mode === 'cancel') assert.ok(outcome.error); + else { + assert.equal(outcome.value?.isError, true, JSON.stringify(outcome)); + assert.equal(JSON.parse(outcome.value.content[0].text).errorCode, mode === 'timeout' ? 'HOST_TIMEOUT' : 'HOST_CRASHED'); + } + // 客户端取消会先结束本地等待;同根打开等待 Gateway 占用清理完成后,才应确认恢复。 + await call('workspace_open', { path: a }); + assertExited(processes); + await fs.writeFile(path.join(a, 'App/App.csproj'), appProject); + await references(await integerTarget(), 1, a); + report.scenarios.push(`${mode} during real MSBuild work releases observed Host, BuildHost and target descendants; explicit recovery succeeds`); + } +} diff --git a/scripts/roslyn/host-inputs.mjs b/scripts/roslyn/host-inputs.mjs new file mode 100644 index 0000000..939bff0 --- /dev/null +++ b/scripts/roslyn/host-inputs.mjs @@ -0,0 +1,161 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs/promises'; +import path from 'node:path'; + +/** verifyInputChanges: 使用同一自有会话,入口负责顺序、快照代次及最终清理。 */ +export async function verifyInputChanges(ctx) { + const { root, source, code, child, next, query, reload, assertStale, report, ready, burstQuery } = ctx; + // 非约定后缀的实际 MSBuild Import 用显式列表补齐;其变化必须重载真实编译条件。 + await fs.writeFile(path.join(root, 'build-inputs/custom.rules'), '$(DefineConstants);EXTRA'); + await assertStale('explicit custom-extension import invalidates its semantic snapshot'); + await reload(); + assert.equal((await query(source.indexOf('Save(int'))).totalReferences, 3); + await fs.writeFile(path.join(root, 'build-inputs/custom.rules'), ''); + await reload(); + assert.equal((await query(source.indexOf('Save(int'))).totalReferences, 2); + report.scenarios.push('reload applies and removes conditions from the explicitly tracked import'); + await fs.appendFile(path.join(root, 'App/details.data'), 'changed\n'); + await assertStale('loaded AdditionalFiles are tracked regardless of extension'); + await reload(); + await fs.unlink(path.join(root, 'schema.yaml')); + const missingInput = await query(source.indexOf('Save(int')); + assert.equal(missingInput.errorCode, 'INPUT_UNAVAILABLE'); + assert.equal(missingInput.references, undefined); + child.stdin.write(JSON.stringify({ id: 'missing-input-reload', operation: 'reload' }) + '\n'); + assert.equal((await next()).errorCode, 'INPUT_UNAVAILABLE'); + await fs.writeFile(path.join(root, 'schema.yaml'), 'mode: restored\n'); + await reload(); + assert.equal((await query(source.indexOf('Save(int'))).totalReferences, 2); + report.scenarios.push('missing explicit input blocks reload until restored, without silently dropping the requirement'); + // 反证:预加载等待不能吞掉求值期间的输入事件,即使文件内容散列没有变化。 + await fs.writeFile(path.join(root, 'build-inputs/custom.rules'), ''); + child.stdin.write(JSON.stringify({ id: 'changed-during-load', operation: 'reload' }) + '\n'); + const changedDuringLoad = await next(); + assert.equal(changedDuringLoad.success, false, JSON.stringify(changedDuringLoad)); + assert.equal(changedDuringLoad.errorCode, 'INPUTS_CHANGED'); + assert.equal((await query(source.indexOf('Save(int'))).errorCode, 'SNAPSHOT_STALE'); + report.scenarios.push('actual MSBuild input touch during reload refuses publication even when content hashes match'); + await fs.writeFile(path.join(root, 'build-inputs/custom.rules'), ''); + await reload(); + assert.equal((await query(source.indexOf('Save(int'))).totalReferences, 2); + report.scenarios.push('removing the input-writing target allows explicit recovery without replaying a failed query'); + await fs.writeFile(path.join(root, 'App/Use.cs'), code['App/Use.cs'].replace('Api.Save(3)', 'Other.Save(3)')); + await assertStale('immediate query after source edit refuses old references'); + const firstSnapshot = ctx.snapshot; + await reload(); + assert.equal((await query(source.indexOf('Save(int'))).totalReferences, 1); + assert.equal((await query(source.indexOf('Save(int'), { snapshot: firstSnapshot })).errorCode, 'SNAPSHOT_STALE'); + report.scenarios.push('reload reflects changed call and permanently expires old snapshot'); + + await fs.writeFile(path.join(root, 'App/Extra.cs'), 'class Extra { void Run() { Demo.Api.Save(9); } }'); + await assertStale('new source file invalidates the original reference file set'); + await reload(); + const added = await query(source.indexOf('Save(int')); + assert.equal(added.totalReferences, 2); + assert.ok(added.references.some(item => item.file.endsWith('Extra.cs'))); + report.scenarios.push('reloaded MSBuild Compile glob includes new call sites'); + + await fs.rename(path.join(root, 'App/Extra.cs'), path.join(root, 'App/Moved.cs')); + await assertStale('renamed file invalidates old locations'); + await reload(); + const renamed = await query(source.indexOf('Save(int')); + assert.ok(renamed.references.some(item => item.file.endsWith('Moved.cs'))); + assert.ok(renamed.references.every(item => !item.file.endsWith('Extra.cs'))); + await fs.unlink(path.join(root, 'App/Moved.cs')); + await assertStale('deleted file invalidates old references'); + await reload(); + assert.equal((await query(source.indexOf('Save(int'))).totalReferences, 1); + report.scenarios.push('rename and delete reloads return only current paths'); + + await fs.appendFile(path.join(root, 'App/obj/project.assets.json'), '\n'); + await assertStale('obj assets changes are tracked before another query'); + await reload(); + assert.equal((await query(source.indexOf('Save(int'))).totalReferences, 1); + await fs.writeFile(path.join(root, 'Directory.Build.props'), 'TRACE;EXTRA'); + await assertStale('Directory.Build.props change invalidates compiled conditions'); + await reload(); + const conditional = await query(source.indexOf('Save(int')); + assert.equal(conditional.totalReferences, 2); + assert.ok(conditional.references.some(item => item.file.endsWith('Conditional.cs'))); + report.scenarios.push('reload applies actual MSBuild preprocessor configuration'); + + const appProject = path.join(root, 'App/App.csproj'); + const originalProject = await fs.readFile(appProject, 'utf8'); + await fs.writeFile(appProject, originalProject.replace('', '')); + await assertStale('project Compile changes invalidate the loaded project graph'); + await reload(); + assert.equal((await query(source.indexOf('Save(int'))).totalReferences, 1); + report.scenarios.push('reload respects project file exclusions'); + + await fs.writeFile(appProject, '', `${codePage}`)); + await fs.writeFile(path.join(root, 'Lib/Encoded.cs'), bytes); + const encodedReady = await reload(); + assert.deepEqual(encodedReady.compilationErrors, [], label); + const symbolId = `encoded-${ctx.nextId()}`; + child.stdin.write(JSON.stringify({ id: symbolId, operation: 'symbols', snapshot: ctx.snapshot, query: 'Café' }) + '\n'); + const symbols = await next(); + assert.equal(symbols.id, symbolId); + assert.equal(symbols.success, true, JSON.stringify(symbols)); + assert.equal(symbols.symbols.length, 1, label); + assert.equal(symbols.symbols[0].name, 'Café'); + assert.equal(symbols.symbols[0].location.position, encodedSource.indexOf('Café')); + const refs = await query(encodedSource.indexOf('Café'), { file: 'Lib/Encoded.cs', symbolName: 'Café' }); + assert.equal(refs.success, true, JSON.stringify(refs)); + assert.equal(refs.totalReferences, 2, label); + for (const reference of refs.references) assert.equal(encodedSource.slice(reference.start, reference.start + reference.length), 'Café'); + report.scenarios.push(`${label} preserves compiler symbol and exact UTF-16 references`); + } + await fs.unlink(path.join(root, 'Lib/Encoded.cs')); + await fs.writeFile(libraryProject, libraryXml); + await reload(); + +} diff --git a/scripts/roslyn/host-semantics.mjs b/scripts/roslyn/host-semantics.mjs new file mode 100644 index 0000000..3a4c0db --- /dev/null +++ b/scripts/roslyn/host-semantics.mjs @@ -0,0 +1,97 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs/promises'; +import path from 'node:path'; + +/** verifySemantics: 使用同一自有会话,入口负责顺序、快照代次及最终清理。 */ +export async function verifySemantics(ctx) { + const { root, source, code, child, next, query, reload, assertStale, report, ready, burstQuery } = ctx; + const timedOut = await query(source.indexOf('Save(int'), { timeoutMs: 1 }); + assert.equal(timedOut.success, false, 'cold semantic operation should exceed the 1 ms test budget'); + assert.equal(timedOut.errorCode, 'CANCELLED'); + report.scenarios.push('1 ms cold-query budget cancels without losing the session'); + const integers = await query(source.indexOf('Save(int')); + assert.equal(integers.success, true, JSON.stringify(integers)); + assert.equal(integers.freshness.files, ready.freshness.files); + assert.equal(integers.freshness.fingerprint, ready.freshness.fingerprint); + assert.equal(integers.totalReferences, 2); + assert.deepEqual(integers.references.map(r => r.line).sort((a, b) => a - b), [4, 7]); + for (const reference of integers.references) { + assert.equal(reference.file.replaceAll('\\', '/'), 'App/Use.cs'); + assert.equal(reference.column, 7); + assert.equal(code['App/Use.cs'].slice(reference.start, reference.start + reference.length), 'Save'); + } + report.scenarios.push('integer overload resolves exact cross-project call spans and columns'); + const strings = await query(source.indexOf('Save(string')); + assert.equal(strings.success, true); + assert.deepEqual(strings.references.map(r => r.line), [5]); + report.scenarios.push('string overload excludes integer overload and same-name other type'); + const empty = await query(source.indexOf('Unused')); + assert.equal(empty.success, true); + assert.equal(empty.totalReferences, 0); + report.scenarios.push('valid symbol with zero references remains successful bounded evidence'); + const repeated = await query(source.indexOf('Save(int')); + assert.deepEqual(repeated.references, integers.references); + report.metrics.push({ firstQueryMs: integers.queryMs, warmQueryMs: repeated.queryMs, workingSetBytes: repeated.workingSetBytes }); + report.scenarios.push('warm query reuses snapshot and preserves exact evidence'); + const truncated = await query(source.indexOf('Save(int'), { limit: 1 }); + assert.equal(truncated.totalReferences, 2); + assert.equal(truncated.references.length, 1); + assert.equal(truncated.truncated, true); + assert.equal(truncated.queryComplete, false); + report.scenarios.push('output cap preserves total and marks incomplete'); + for (const [label, extra, errorCode] of [ + ['stale snapshot', { snapshot: 'stale' }, 'SNAPSHOT_STALE'], ['outside source', { file: '../outside.cs' }, 'OUTSIDE_WORKSPACE'], + ['invalid position', { position: -1 }, 'INVALID_ARGUMENT'], ['wrong project context', { project: 'App/App.csproj' }, 'INVALID_ARGUMENT'], + ['invalid time budget', { timeoutMs: 0 }, 'INVALID_ARGUMENT'], + ['fractional position', { position: 1.5 }, 'INVALID_ARGUMENT'], ['fractional time budget', { timeoutMs: 1.5 }, 'INVALID_ARGUMENT'], + ]) { + const rejected = await query(source.indexOf('Save(int'), extra); + assert.equal(rejected.success, false, label); + assert.equal(rejected.errorCode, errorCode, label); + report.scenarios.push(`${label} rejected`); + } +} + +/** verifyQueue: 使用同一自有会话,入口负责顺序、快照代次及最终清理。 */ +export async function verifyQueue(ctx) { + const { root, source, code, child, next, query, reload, assertStale, report, ready, burstQuery } = ctx; + child.stdin.write(JSON.stringify({ id: 'cancel-target', operation: 'references', snapshot: ctx.snapshot, + project: 'Lib/Lib.csproj', file: 'Lib/Api.cs', position: source.indexOf('Save(int') }) + '\n' + + JSON.stringify({ id: 'cancel-control', operation: 'cancel', targetId: 'cancel-target' }) + '\n'); + const cancelled = new Map((await Promise.all([next(), next()])).map(result => [result.id, result])); + assert.equal(cancelled.get('cancel-control').cancellationRequested, true); + assert.equal(cancelled.get('cancel-target').errorCode, 'CANCELLED'); + assert.equal((await query(source.indexOf('Save(int'))).totalReferences, 2); + report.scenarios.push('explicit cancellation reaches queued or active work without closing the session'); + + // 单次突发同时检验身份冲突、排队截止和背压;每个输入都必须收到独立结果,不静默丢队列项。 + /** 构造当前夹具快照的引用请求,允许突发与 EOF 验收复用同一定位。 */ + + // 到期的排队 reload 必须在触碰工作区前退出;若错误地到执行时才计时,会使后续旧身份查询失败。 + const burst = [burstQuery('burst-first'), burstQuery('burst-first'), + { id: 'burst-deadline', operation: 'reload', timeoutMs: 1 }, + ...Array.from({ length: 16 }, (_, index) => burstQuery(`burst-${index}`))]; + child.stdin.write(burst.map(request => JSON.stringify(request)).join('\n') + '\n'); + const burstResults = await Promise.all(burst.map(() => next())); + const duplicated = burstResults.filter(result => result.id === 'burst-first'); + assert.equal(duplicated.length, 2); + assert.equal(duplicated.filter(result => result.errorCode === 'DUPLICATE_REQUEST').length, 1); + assert.equal(duplicated.filter(result => result.success === true).length, 1); + report.scenarios.push('duplicate active id is rejected without cancelling its original request'); + assert.equal(burstResults.find(result => result.id === 'burst-deadline').errorCode, 'CANCELLED'); + report.scenarios.push('expired queued reload is cancelled before invalidating the valid snapshot'); + assert.ok(burstResults.some(result => result.errorCode === 'BUSY')); + for (const request of burst.slice(2)) assert.equal(burstResults.filter(result => result.id === request.id).length, 1); + for (const result of burstResults) { + if (result.success) assert.equal(result.totalReferences, 2); + else assert.ok(['CANCELLED', 'BUSY', 'DUPLICATE_REQUEST'].includes(result.errorCode)); + } + report.queue = { submitted: burst.length, completed: burstResults.filter(result => result.success).length, + rejectedBusy: burstResults.filter(result => result.errorCode === 'BUSY').length }; + report.scenarios.push('bounded queue reports backpressure and accounts for every submitted frame'); + child.stdin.write(JSON.stringify({ id: 'cancel-missing', operation: 'cancel', targetId: 'absent-request' }) + '\n'); + assert.equal((await next()).cancellationRequested, false); + assert.equal((await query(source.indexOf('Save(int'))).totalReferences, 2); + report.scenarios.push('cancelling an absent request reports no cancellation and preserves the session'); + +} diff --git a/scripts/serena-isolated-launcher.py b/scripts/serena-isolated-launcher.py deleted file mode 100644 index 6d85f5f..0000000 --- a/scripts/serena-isolated-launcher.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Explicit environment for the opt-in, project-local Serena installation.""" -import os -from pathlib import Path - -repo = Path(__file__).resolve().parent.parent -deps = repo / ".deps" -sdk = deps / "dotnet-10.0.303" -os.environ.update({ - "SERENA_HOME": str(deps / "serena-home"), - "DOTNET_ROOT": str(sdk), - "DOTNET_ROOT_X64": str(sdk), - "DOTNET_CLI_HOME": str(deps / "dotnet-cli-home"), - "NUGET_PACKAGES": str(deps / "nuget-packages"), - "NUGET_HTTP_CACHE_PATH": str(deps / "nuget-http-cache"), - "DOTNET_CLI_TELEMETRY_OPTOUT": "1", - "DOTNET_NOLOGO": "1", - "DOTNET_GENERATE_ASPNET_CERTIFICATE": "false", - "DOTNET_ADD_GLOBAL_TOOLS_TO_PATH": "false", - "PATH": str(sdk) + os.pathsep + os.environ.get("PATH", ""), -}) - -# Import only after setting paths; upstream modules resolve directories on import. -from serena.cli import top_level - -if __name__ == "__main__": - top_level() diff --git a/scripts/test-mcp-client.ts b/scripts/test-mcp-client.ts index c6fbb97..49bcb61 100644 --- a/scripts/test-mcp-client.ts +++ b/scripts/test-mcp-client.ts @@ -25,7 +25,7 @@ import { getDefaultConfig } from ${distUrl('Core/Config.js')}; import { ToolRouter } from ${distUrl('Core/ToolRouter.js')}; import { WinCodeMcpServer } from ${distUrl('Gateway/McpServer.js')}; const config = getDefaultConfig(${JSON.stringify(root)}); -config.adapters.serena.enabled = false; + config.adapters.flaui.enabled = false; config.adapters.repomix.useCli = false; const server = new WinCodeMcpServer(new ToolRouter(config)); diff --git a/scripts/verify-error-contracts.ts b/scripts/verify-error-contracts.ts index cf4f8fa..787d39a 100644 --- a/scripts/verify-error-contracts.ts +++ b/scripts/verify-error-contracts.ts @@ -7,7 +7,7 @@ import { ToolRouter } from '../src/Core/ToolRouter.js'; import { WinCodeMcpServer } from '../src/Gateway/McpServer.js'; import { AbortError } from '../src/Core/ResourceManager.js'; -// Inventory current public responses before proposing an additive contract. +// Verify the unified JSON tool-error contract, including domain outcomes. // Generated inputs only; native UI and external adapters are disabled. const parent = path.resolve('test-tmp/error-contracts'); await fs.mkdir(parent, { recursive: true }); @@ -16,7 +16,7 @@ await fs.writeFile(path.join(root, 'A.cs'), 'class Same {}\n'); await fs.writeFile(path.join(root, 'B.cs'), 'class Same {}\n'); await fs.writeFile(path.join(root, 'Long.cs'), Array.from({ length: 100 }, (_, i) => `// ${i} ${'x'.repeat(100)}`).join('\n')); const config = getDefaultConfig(root); -config.adapters.serena.enabled = false; + config.adapters.flaui.enabled = false; config.adapters.repomix.useCli = false; const router = new ToolRouter(config); @@ -28,7 +28,8 @@ let failure: string | undefined; async function observe(scenario: string, name: string, args: Record, check: (result: any, body: any) => void) { const result: any = await client.callTool({ name, arguments: args }); let body: any = null; - try { body = JSON.parse(result.content[0].text); } catch { /* Plain-text errors are part of this inventory. */ } + try { body = JSON.parse(result.content[0].text); } catch { throw new Error('Every tool response in this acceptance must contain JSON text.'); } + if (result.isError) { assert.deepEqual(result.structuredContent, body); assert.equal(body.success, false); } observations.push({ scenario, tool: name, result }); check(result, body); } @@ -48,7 +49,7 @@ try { assert.equal(body.truncated, true); assert.ok(result.content[0].text.length <= 2048); }); await observe('unavailable semantic upstream with empty local result', 'wincode_find_code_symbol', { query: 'Absent' }, (_result, body) => { - assert.equal(body.source, 'serena-adapter-fallback'); assert.equal(body.analysisCompleteness, 'degraded'); + assert.equal(body.source, 'local-text'); assert.equal(body.analysisCompleteness, 'degraded'); assert.equal(body.totalFound, 0); }); const original = router.findCodeSymbols; @@ -62,7 +63,7 @@ try { } finally { router.findCodeSymbols = original; } 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.reason, 'cancelled'); assert.equal(body.recoverable, false); + assert.equal(result.isError, true); assert.equal(body.errorCode, 'SHUTDOWN'); assert.equal(body.recoveryAction, 'restart_gateway'); }); } catch (error) { failure = String(error); process.exitCode = 1; } finally { diff --git a/scripts/verify-failure-recovery.ts b/scripts/verify-failure-recovery.ts index a22e3b9..25440de 100644 --- a/scripts/verify-failure-recovery.ts +++ b/scripts/verify-failure-recovery.ts @@ -21,7 +21,7 @@ async function fixture(name: string, work: (router: ToolRouter, a: string, b: st await fs.writeFile(path.join(a, 'OnlyA.cs'), 'class OnlyA {}'); await fs.writeFile(path.join(b, 'OnlyB.cs'), 'class OnlyB {}'); const config = getDefaultConfig(a); - config.adapters.serena.enabled = false; + config.adapters.flaui.enabled = false; config.adapters.repomix.useCli = false; const router = new ToolRouter(config); @@ -36,7 +36,7 @@ async function fixture(name: string, work: (router: ToolRouter, a: string, b: st } const stages = ['root-before', 'root-after', 'namespace', 'session', 'watch', - 'repomix-dispose', 'serena-reset', 'repomix-initialize', 'serena-initialize', 'composites', 'cancel-after-root']; + 'repomix-dispose', 'text-reset', 'repomix-initialize', 'text-initialize', 'composites', 'cancel-after-root']; for (const stage of stages) { await fixture(stage, async (router, a, b) => { @@ -48,9 +48,9 @@ for (const stage of stages) { namespace: [router.cache, 'setNamespace'], session: [router.session, 'open'], watch: [router as any, 'bindWatch'], 'repomix-dispose': [router.repomix, 'dispose'], - 'serena-reset': [router.serena, 'resetConnection'], + 'text-reset': [router.text, 'resetConnection'], 'repomix-initialize': [router.repomix, 'initialize'], - 'serena-initialize': [router.serena, 'initialize'], + 'text-initialize': [router.text, 'initialize'], composites: [router as any, 'bindCompositeTools'], }; const [target, method] = targets[stage]; diff --git a/scripts/verify-mixed-load.ts b/scripts/verify-mixed-load.ts index 9c756bb..fd8655e 100644 --- a/scripts/verify-mixed-load.ts +++ b/scripts/verify-mixed-load.ts @@ -38,11 +38,9 @@ for (const [index, directory] of roots.entries()) { ...Array.from({ length: 10 }, (_, round) => `class Probe${round}Only${index} {}`)].join('\n')); } const config = getDefaultConfig(roots[0]); -config.adapters.serena.enabled = false; + config.adapters.flaui.enabled = false; config.adapters.repomix.useCli = false; -config.timeouts.serenaConnectMs = 3000; -config.timeouts.serenaCallMs = 3000; const router = new ToolRouter(config); const children: cp.ChildProcess[] = []; const originalSpawn = cp.spawn; @@ -78,33 +76,21 @@ try { const index = round % 2; await call('switch', () => router.openWorkspace(roots[index])); await call('query-before-interleaving', () => query(index)); - // Real owned Node fixture: hang an RPC for cancellation, or exit during it. - // Keep the production RPC/reset implementation intact; the gate controls scheduling only. + // 调度门只控制文本查询开始时刻;保留真实扫描及工作区排空逻辑。 const cancel = round % 2 === 0; - router.config.adapters.serena.enabled = true; - router.config.adapters.serena.customCommand = process.execPath; - router.config.adapters.serena.customArgs = [path.resolve('tests/fixtures/mock-serena-mcp.mjs'), cancel ? '--hang' : '--crash']; - await router.serena.initialize(); - assert.equal(await router.serena.ensureConnected(), true); - const upstreamMetrics = processMetrics([process.pid, ...children - .filter(child => child.exitCode === null && child.signalCode === null && child.pid) - .map(child => child.pid!)]); + const upstreamMetrics = processMetrics([process.pid]); const controller = new AbortController(); let entered!: () => void, release!: () => void; const ready = new Promise(resolve => { entered = resolve; }); const gate = new Promise(resolve => { release = resolve; }); - const adapter = router.serena as any; - const originalCall = adapter.callSerenaTool; - adapter.callSerenaTool = async (...args: unknown[]) => { - const outcome = Promise.resolve(originalCall.apply(adapter, args)).then( - value => ({ ok: true as const, value }), error => ({ ok: false as const, error })); + const adapter = router.text as any; + const originalCall = adapter.findSymbolsDetailed; + adapter.findSymbolsDetailed = async (...args: unknown[]) => { entered(); await gate; - const result = await outcome; - if (!result.ok) throw result.error; - return result.value; + return originalCall.apply(adapter, args); }; - const queryWork = call(cancel ? 'cancel-in-flight' : 'exit-in-flight', async () => { + const queryWork = call(cancel ? 'cancel-in-flight' : 'query-in-flight', async () => { await router.acquireRequestSlot(); try { const pending = router.findCodeSymbols(`Probe${round}`, undefined, controller.signal); @@ -113,7 +99,7 @@ try { return { cancelled: true }; } const result = await pending; - assert.notEqual(result.source, 'serena-mcp'); + assert.notEqual(result.source, 'roslyn'); assert.ok(result.symbols.some(symbol => symbol.name === `Probe${round}Only${index}`)); return result; } finally { router.endRequest(); } @@ -129,18 +115,18 @@ try { assert.equal(router.isSwitchingWorkspace, true); assert.equal(router.inFlightRequests, 1); assert.equal(router.config.workspaceRoot, roots[index], 'root cannot change while the old query owns its slot'); - interleavings.push({ round, mode: cancel ? 'cancel' : 'upstream-exit', - rpcStarted: true, switchWaiting: true, oldRootPreserved: true }); + interleavings.push({ round, mode: cancel ? 'cancel' : 'text-completion', + queryStarted: true, switchWaiting: true, oldRootPreserved: true }); if (cancel) controller.abort(); release(); await Promise.all([queryWork, switching]); } finally { controller.abort(); release(); await Promise.allSettled([queryWork, ...(switching ? [switching] : [])]); - adapter.callSerenaTool = originalCall; + adapter.findSymbolsDetailed = originalCall; } - router.config.adapters.serena.enabled = false; - await router.serena.initialize(); + + await router.text.initialize(); await Promise.all([call('query-after-interleaving-1', () => query(1 - index)), call('query-after-interleaving-2', () => query(1 - index))]); const health = await call('health', () => router.getRuntimeHealth()); @@ -168,7 +154,7 @@ if (liveOwnedPids.length) error = `${error ?? ''}\nOwned child processes still l const report = { success: !error, node: process.version, elapsedMs: Date.now() - started, callCount: calls.length, sampleIntervalMs, finalMetrics: processMetrics([process.pid]), budget: { maxCalls: 100, maxMs: 300000 }, calls, samples, interleavings, liveOwnedPids, error, - limitations: ['Local generated workspaces and mock upstream only; real Serena/Repomix compatibility untested.', + limitations: ['Generated workspaces and real local text scans; Roslyn process faults are covered by the separate real Host/Gateway suites.', 'Bounded paced sample is not an endurance or leak proof; no forced GC or continuous high-load claim.', 'Windows WorkingSet64 is a point-in-time working set, not peak RSS. Metrics availability/missing PIDs are recorded explicitly.', 'outputChars are serialized UTF-16 characters, not model tokens.'], diff --git a/scripts/verify-product-tasks.ts b/scripts/verify-product-tasks.ts index dcb55d9..11f6d45 100644 --- a/scripts/verify-product-tasks.ts +++ b/scripts/verify-product-tasks.ts @@ -88,7 +88,7 @@ import { getDefaultConfig } from ${url('Core/Config.js')}; import { ToolRouter } from ${url('Core/ToolRouter.js')}; import { WinCodeMcpServer } from ${url('Gateway/McpServer.js')}; const config = getDefaultConfig(${JSON.stringify(temporary)}); -config.adapters.serena.enabled = false; + config.adapters.repomix.useCli = false; const server = new WinCodeMcpServer(new ToolRouter(config)); process.stdin.on('end', () => { void server.stop(); }); diff --git a/scripts/verify-roslyn-gateway.mjs b/scripts/verify-roslyn-gateway.mjs index 0def7e8..2b2c04a 100644 --- a/scripts/verify-roslyn-gateway.mjs +++ b/scripts/verify-roslyn-gateway.mjs @@ -1,27 +1,27 @@ +import { verifyGatewayLifecycle } from './roslyn/gateway-lifecycle.mjs'; /** * 直接 Roslyn 的真实 stdio MCP 验收。只生成/求值 test-tmp 下两套 C# 项目,保留失败与进程证据。 * 依赖项目内已批准 SDK 与已构建 Gateway/Code Host;不安装、不运行真实用户项目或目标应用。 */ import assert from 'node:assert/strict'; +import { resolveDotnet, runDotnet } from './lib/dotnet.mjs'; +import { ownedProcesses as owned, assertExited } from './lib/owned-processes.mjs'; import fs from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { spawnSync } from 'node:child_process'; import { Client } from '@modelcontextprotocol/client'; import { StdioClientTransport } from '@modelcontextprotocol/client/stdio'; const repo = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); -const sdk = path.join(repo, '.deps/dotnet-10.0.303'); -const dotnet = path.join(sdk, 'dotnet.exe'); -const host = path.join(repo, 'tools/WinCode.Code.Host/bin/Release/net10.0/WinCode.Code.Host.dll'); -const env = { ...process.env, DOTNET_ROOT: sdk, DOTNET_HOST_PATH: dotnet, DOTNET_CLI_HOME: path.join(repo, '.deps/dotnet-cli-home'), - NUGET_PACKAGES: path.join(repo, '.deps/nuget-packages'), NUGET_HTTP_CACHE_PATH: path.join(repo, '.deps/nuget-http-cache'), - DOTNET_NOLOGO: '1', DOTNET_CLI_TELEMETRY_OPTOUT: '1' }; +const toolchain = resolveDotnet(repo); +const { dotnet, env } = toolchain; +let host; +const dotnetRun = args => runDotnet(toolchain, args, repo, 120000); const parent = path.join(repo, 'test-tmp/roslyn-gateway'); await fs.mkdir(parent, { recursive: true }); const root = await fs.mkdtemp(path.join(parent, 'run-')); const report = { root, scenarios: [], processes: [], success: false, - limitations: ['Generated C# projects and a fresh local stdio client; not the currently configured Codex connection or a clean machine release test.'] }; + limitations: ['Generated C# projects and a fresh local stdio client; relocated published Code Host with the installed SDK; not the current Codex connection or a clean machine test.'] }; const library = 'namespace Demo;\npublic partial class Api { public static void Save(int x) {} public static void Save(string x) {} public static void Unused() {} }\npublic class Other { public static void Save(int x) {} }\n'; const calls = tag => `using Demo;\n// 😀 中文 UTF-16 ${tag}\npublic class Use { public void Run() { Api.Save(1); Api.Save("x"); Other.Save(2); ${tag === 'A' ? 'Api.Save(3);' : ''} } }\n// Api.Save(777)\n`; const project = 'net10.013.0falseEXTRA'; @@ -30,27 +30,8 @@ let transport; let client; let stderr = ''; -/** 执行限定时长的本地 SDK 命令;失败保留输出,不把失败当成缺包后自动安装。 */ -function dotnetRun(args) { - const result = spawnSync(dotnet, args, { cwd: repo, env, windowsHide: true, encoding: 'utf8', timeout: 120000 }); - assert.equal(result.status, 0, `${result.error ?? ''}\n${result.stdout}\n${result.stderr}`); - return result.stdout; -} -/** 获取测试所有进程树;只拼接经正整数校验的 PID,记录创建时间以排除 PID 复用。 */ -function owned(pid) { - assert.ok(Number.isSafeInteger(pid) && pid > 0); - const command = `$all = @(Get-CimInstance Win32_Process); $ids = @(${pid}); do { $more = @($all | Where-Object { $_.ParentProcessId -in $ids -and $_.ProcessId -notin $ids }); $ids += @($more | ForEach-Object { $_.ProcessId }) } while ($more.Count -gt 0); @($all | Where-Object { $_.ProcessId -in $ids } | Select-Object ProcessId,ParentProcessId,CreationDate,Name,CommandLine) | ConvertTo-Json -Compress`; - const result = spawnSync('powershell.exe', ['-NoProfile', '-Command', command], { encoding: 'utf8', windowsHide: true, timeout: 15000 }); - assert.equal(result.status, 0, result.stderr); - const value = JSON.parse(result.stdout || '[]'); - return Array.isArray(value) ? value : [value]; -} -/** 每个进程按 PID/创建时间核对退出;不终止不属于本次测试的对象。 */ -function assertExited(processes) { - for (const process of processes) assert.ok(!owned(process.ProcessId).some(current => current.ProcessId === process.ProcessId && current.CreationDate === process.CreationDate), `Owned process survived: ${process.ProcessId}`); -} /** 工作区切换只关闭 Code Host 子树;Gateway 自己的控制台宿主应保持到 Gateway 退出。 */ function codeProcesses() { @@ -109,7 +90,17 @@ async function markerReady(marker) { try { console.log('[roslyn-gateway] build and generated fixtures'); - report.hostBuild = dotnetRun(['build', 'tools/WinCode.Code.Host', '-c', 'Release', '-p:RestoreLockedMode=true', '--nologo']); + // 验收构建只写入本轮目录,不能改写 check 已散列的正式 publish 交付件。 + const fixturePublish = path.join(root, 'fixture-publish'); + report.hostBuild = dotnetRun(['publish', 'tools/WinCode.Code.Host', '-c', 'Release', '-p:RestoreLockedMode=true', '--nologo', '--output', fixturePublish]); + // 复制完整发布目录,使用含中文和空格的新路径;不能依赖原 bin 旁的 BuildHost。 + const relocated = path.join(root, '交付 Code Host'); + await fs.cp(fixturePublish, relocated, { recursive: true, errorOnExist: true, force: false }); + host = path.join(relocated, 'WinCode.Code.Host.dll'); + report.publishedHost = host; + const identity = JSON.parse(runDotnet(toolchain, [host, '--identity'], root)); + assert.equal(identity.hostIdentity.configuration, 'Release'); + report.scenarios.push('complete published Code Host relocates to a Chinese path with spaces and starts from a different working directory'); for (const tag of ['A', 'B']) { const workspace = path.join(root, tag); for (const folder of ['Lib', 'App', '.cache']) await fs.mkdir(path.join(workspace, folder), { recursive: true }); @@ -119,20 +110,22 @@ try { await fs.writeFile(path.join(workspace, 'Lib/Partial.cs'), 'namespace Demo; public partial class Api { public int Value { get; set; } }'); await fs.writeFile(path.join(workspace, 'App/Use.cs'), calls(tag)); await fs.writeFile(path.join(workspace, 'Helper.ts'), 'export function localHelp() { return 3; }\n'); + await fs.writeFile(path.join(workspace, 'schema.yaml'), 'mode: original\n'); dotnetRun(['restore', path.join(workspace, 'App/App.csproj'), '--nologo']); } const a = path.join(root, 'A'), b = path.join(root, 'B'); const config = path.join(root, 'roslyn.json'); await fs.writeFile(config, JSON.stringify({ enabled: true, allowProjectEvaluation: true, project: 'App/App.csproj', - configuration: 'Debug', targetFramework: 'net10.0', dotnetPath: dotnet, hostPath: host, loadTimeoutMs: 15000, queryTimeoutMs: 10000 })); + configuration: 'Debug', targetFramework: 'net10.0', dotnetPath: dotnet, hostPath: host, loadTimeoutMs: 15000, queryTimeoutMs: 10000, + additionalInputs: ['schema.yaml'] })); client = new Client({ name: 'roslyn-gateway-acceptance', version: '1' }); - transport = new StdioClientTransport({ command: process.execPath, args: [path.join(repo, 'dist/index.js'), '--workspace', a, '--roslyn-config', config], env, stderr: 'pipe' }); + transport = new StdioClientTransport({ command: process.execPath, args: [path.join(repo, 'dist/index.js'), '--workspace', a, '--roslyn-config', config], cwd: root, env, stderr: 'pipe' }); await client.connect(transport); transport.stderr?.on('data', chunk => { stderr = (stderr + chunk).slice(-16384); }); const initial = await call('wincode_hello_world'); assert.equal(initial.codeProvider, 'roslyn'); assert.equal(initial.health.roslyn.processAlive, false); - assert.equal(initial.health.serena.handshakeOk, false); + assert.equal(initial.health.text.semanticConfigured, false); report.scenarios.push('explicit production CLI selects Roslyn; hello does not load a project'); const listed = await client.listTools(); assert.equal(listed.tools.length, 15); @@ -140,6 +133,16 @@ try { report.scenarios.push('existing tools expose the validated optional symbolLocation contract'); const target = await integerTarget(); await references(target, 2, a); + for (const name of ['analyze_change_impact', 'wincode_plan_refactoring']) { + assert.ok(listed.tools.find(tool => tool.name === name).inputSchema.properties.symbolLocation); + const result = await call(name, { target: target.name, symbolLocation: target.location, ...(name.includes('refactoring') ? { goal: 'Simplify this overload' } : {}) }); + const evidence = result.evidence ?? result; + assert.deepEqual(evidence.symbolLocation, target.location); + assert.equal(evidence.queryComplete, false); + if (!result.evidence) { assert.equal(result.referencesCount, 2); assert.equal(result.matchedSymbols.length, 1); } + assert.equal((await call(name, { target: 'Other', symbolLocation: target.location, goal: 'Simplify' }, true)).errorCode, 'SYMBOL_MISMATCH'); + } + report.scenarios.push('selected overload continues into impact and refactoring without mixing other Save declarations'); const ambiguous = await call('wincode_find_references', { symbolName: 'Save' }); assert.equal(ambiguous.resolution, 'ambiguous'); assert.equal(ambiguous.candidateCount, 3); @@ -158,6 +161,14 @@ try { assert.equal(impact.confidence, 'UNCERTAIN'); assert.ok(impact.referencesCount > 0, JSON.stringify(impact)); report.scenarios.push('partial declarations deduplicate; impact keeps real references and incomplete confidence'); + const absoluteImpact = await call('analyze_change_impact', { target: path.join(a, 'Lib/Api.cs') }); + assert.equal(absoluteImpact.uniqueResolution, true); + assert.equal(absoluteImpact.referencesCount, impact.referencesCount); + const refactor = await call('wincode_plan_refactoring', { target: 'Api', goal: 'Simplify the implementation' }); + assert.equal(refactor.evidence.source, 'roslyn'); + assert.equal(refactor.evidence.queryComplete, false); + assert.ok(refactor.recommendedSteps.every(step => !/interrupted|textual matches|degraded retrieval/.test(step))); + report.scenarios.push('absolute workspace target resolves and real Roslyn refactoring preserves bounded semantic evidence'); const context = await call('wincode_prepare_context', { task: 'Inspect Save', scopeFiles: ['Lib/Api.cs'], lineRanges: [{ file: 'Lib/Api.cs', startLine: 1, endLine: 3 }], maxTokens: 2000 }); assert.ok(context.evidence.length > 0); const textContext = await call('wincode_prepare_context', { task: 'Inspect localHelp', scopeFiles: ['Helper.ts'], symbol: 'localHelp', maxTokens: 2000 }); @@ -167,11 +178,16 @@ try { assert.equal((await call('wincode_find_references', { symbolName: 'Save', symbolLocation: badLocation }, true)).errorCode, 'OUTSIDE_WORKSPACE'); assert.equal((await call('wincode_find_references', { symbolName: 'Other', symbolLocation: target.location }, true)).errorCode, 'SYMBOL_MISMATCH'); assert.equal((await call('wincode_find_references', { symbolName: 'Api/Save[0]' }, true)).errorCode, 'LEGACY_SYMBOL_ID'); - report.scenarios.push('outside location, mismatched name and legacy Serena identity are rejected'); + report.scenarios.push('outside location, mismatched name and retired Serena identity are rejected'); await fs.writeFile(path.join(a, 'App/Use.cs'), calls('A').replace('Api.Save(3);', '')); const stale = await call('wincode_find_references', { symbolName: 'Save', symbolLocation: target.location }, true); assert.ok(['SNAPSHOT_STALE', 'INPUTS_CHANGED'].includes(stale.errorCode)); assert.equal(stale.references, undefined); + for (const name of ['analyze_change_impact', 'wincode_plan_refactoring']) { + const rejected = await call(name, { target: target.name, symbolLocation: target.location, goal: 'Simplify' }, true); + assert.ok(['SNAPSHOT_STALE', 'INPUTS_CHANGED'].includes(rejected.errorCode)); + } + report.scenarios.push('impact and refactoring reject stale selected locations before search can reload'); const edited = await integerTarget(); assert.notEqual(edited.location.snapshotId, target.location.snapshotId); await references(edited, 1, a); @@ -190,6 +206,29 @@ try { assert.equal((await call('wincode_find_references', { symbolName: 'Save', symbolLocation: inB.location }, true)).errorCode, 'SNAPSHOT_STALE'); report.scenarios.push('A to B to A closes the old Host and rejects identities from both prior sessions'); + // 配置经生产 CLI/Adapter/Host 三层传递;无关文件和显式输入必须产生相反的失效行为。 + await fs.writeFile(path.join(a, 'README.md'), '# Unrelated notes\n'); + const unrelated = path.join(a, 'unrelated.bin'); + const unrelatedHandle = await fs.open(unrelated, 'wx'); + try { await unrelatedHandle.truncate(33 * 1024 * 1024); } finally { await unrelatedHandle.close(); } + try { await references(againA, 1, a); } finally { await fs.unlink(unrelated); } + report.scenarios.push('real MCP keeps the same snapshot after README and unrelated 33 MiB file creation'); + await fs.writeFile(path.join(a, 'schema.yaml'), 'mode: changed\n'); + const additionalStale = await call('wincode_find_references', { symbolName: 'Save', symbolLocation: againA.location }, true); + assert.ok(['SNAPSHOT_STALE', 'INPUTS_CHANGED'].includes(additionalStale.errorCode)); + assert.equal(additionalStale.references, undefined); + const inputReloaded = await integerTarget(); + assert.notEqual(inputReloaded.location.snapshotId, againA.location.snapshotId); + await references(inputReloaded, 1, a); + await fs.unlink(path.join(a, 'schema.yaml')); + const inputMissing = await call('wincode_find_references', { symbolName: 'Save', symbolLocation: inputReloaded.location }, true); + assert.equal(inputMissing.errorCode, 'INPUT_UNAVAILABLE'); + assert.equal((await call('wincode_find_code_symbol', { query: 'Api' }, true)).errorCode, 'INPUT_UNAVAILABLE'); + await fs.writeFile(path.join(a, 'schema.yaml'), 'mode: restored\n'); + await references(await integerTarget(), 1, a); + assert.equal((await call('wincode_hello_world')).health.lastAdapterError.provider, 'roslyn'); + report.scenarios.push('configured extra input changes and absence invalidate evidence; explicit repair and search recover'); + await call('workspace_open', { path: a }); await fs.writeFile(path.join(a, 'App/App.csproj'), ' {}, 1000);\n"); - const marker = path.join(a, '.cache/block.started'); - const escape = value => value.replaceAll('&', '&').replaceAll('"', '"').replaceAll('<', '<'); - const targetXml = ``; - for (const mode of ['cancel', 'crash', 'timeout']) { - console.log(`[roslyn-gateway] active MSBuild ${mode}`); - await call('workspace_open', { path: a }); - await fs.rm(marker, { force: true }); - await fs.writeFile(path.join(a, 'App/App.csproj'), appProject.replace('', targetXml + '')); - const controller = new AbortController(); - const pending = client.callTool({ name: 'wincode_find_code_symbol', arguments: { query: 'Api' } }, { timeout: 30000, signal: controller.signal }); - const settled = pending.then(value => ({ value }), error => ({ error: String(error) })); - await markerReady(marker); - const processes = codeProcesses(); - assert.ok(processes.some(item => item.CommandLine?.includes('BuildHost')), 'actual BuildHost must be observed during design-time work'); - assert.ok(processes.some(item => item.CommandLine?.includes(blocker)), 'blocking target child must be observed'); - report.processes.push({ mode, processes }); - if (mode === 'cancel') controller.abort(); - if (mode === 'crash') { - const hostProcess = processes.find(item => item.CommandLine?.includes(host)); - assert.ok(hostProcess); - process.kill(hostProcess.ProcessId, 'SIGKILL'); - } - const outcome = await settled; - if (mode === 'cancel') assert.ok(outcome.error); - else { - assert.equal(outcome.value?.isError, true, JSON.stringify(outcome)); - assert.equal(JSON.parse(outcome.value.content[0].text).errorCode, mode === 'timeout' ? 'HOST_TIMEOUT' : 'HOST_CRASHED'); - } - // 客户端取消会先结束本地等待;同根打开等待 Gateway 占用清理完成后,才应确认恢复。 - await call('workspace_open', { path: a }); - assertExited(processes); - await fs.writeFile(path.join(a, 'App/App.csproj'), appProject); - await references(await integerTarget(), 1, a); - report.scenarios.push(`${mode} during real MSBuild work releases observed Host, BuildHost and target descendants; explicit recovery succeeds`); - } + await verifyGatewayLifecycle({ root, a, host, appProject, client, call, markerReady, codeProcesses, report, references, integerTarget }); const final = await call('wincode_hello_world'); - assert.equal(final.health.serena.handshakeOk, false); + assert.equal(final.health.text.semanticConfigured, false); const processes = owned(transport.pid); assert.ok(processes.every(item => !/python|serena/i.test(`${item.Name} ${item.CommandLine}`))); await client.close(); diff --git a/scripts/verify-roslyn-host.mjs b/scripts/verify-roslyn-host.mjs index 1be215a..a40a8e7 100644 --- a/scripts/verify-roslyn-host.mjs +++ b/scripts/verify-roslyn-host.mjs @@ -1,9 +1,13 @@ +import { verifySemantics, verifyQueue } from './roslyn/host-semantics.mjs'; +import { verifyInputChanges, verifyBudgetsAndEncoding } from './roslyn/host-inputs.mjs'; /** * 自有 Roslyn Host 的隔离验收入口:只写 test-tmp 下生成的两项目夹具。 - * 使用项目内 SDK/NuGet,验证语义结果、失败边界及进程退出;不启动 Gateway/Serena。 + * 使用项目内 SDK/NuGet,验证语义结果、失败边界及进程退出;不启动 Gateway/Local text。 * 返回非零退出码表示验收失败,详细结果和失败原因保留到夹具目录 report.json。 */ import assert from 'node:assert/strict'; +import { resolveDotnet, runDotnet } from './lib/dotnet.mjs'; +import { ownedProcesses } from './lib/owned-processes.mjs'; import fs from 'node:fs/promises'; import path from 'node:path'; import { spawn, spawnSync } from 'node:child_process'; @@ -11,23 +15,15 @@ import { createInterface } from 'node:readline'; import { fileURLToPath } from 'node:url'; const repo = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); -const localSdk = path.join(repo, '.deps/dotnet-10.0.303'); -await fs.access(path.join(localSdk, 'dotnet.exe')); -const dotnet = path.join(localSdk, 'dotnet.exe'); -const env = { ...process.env, DOTNET_ROOT: localSdk, DOTNET_HOST_PATH: dotnet, - DOTNET_CLI_HOME: path.join(repo, '.deps/dotnet-cli-home'), NUGET_PACKAGES: path.join(repo, '.deps/nuget-packages'), - NUGET_HTTP_CACHE_PATH: path.join(repo, '.deps/nuget-http-cache'), DOTNET_NOLOGO: '1', DOTNET_CLI_TELEMETRY_OPTOUT: '1' }; +const toolchain = resolveDotnet(repo); +const { dotnet, env } = toolchain; +const run = (args, cwd = repo) => runDotnet(toolchain, args, cwd); const parent = path.join(repo, 'test-tmp/roslyn-host'); await fs.mkdir(parent, { recursive: true }); const root = await fs.mkdtemp(path.join(parent, 'fixture-')); const report = { root, scenarios: [], metrics: [], limitations: ['Generated SDK C# fixture only; checkpoints cover tracked inputs, not arbitrary external target inputs or live Gateway migration.'] }; const host = path.join(repo, 'tools/WinCode.Code.Host/bin/Release/net10.0/WinCode.Code.Host.dll'); -/** 执行有 180 秒上限的 dotnet 命令;失败包含构建输出,成功返回 stdout。 */ -function run(args, cwd = repo) { - const result = spawnSync(dotnet, args, { cwd, env, encoding: 'utf8', windowsHide: true, timeout: 180000, maxBuffer: 2 * 1024 * 1024 }); - if (result.error || result.status !== 0) throw new Error(`dotnet ${args[0]} failed: ${result.error ?? ''}\n${result.stdout}\n${result.stderr}`); - return result.stdout; -} + const code = { 'Lib/Api.cs': 'namespace Demo;\npublic class Api {\n public static void Save(int x) {}\n public static void Save(string x) {}\n public static void Unused() {}\n}\npublic class Other { public static void Save(int x) {} }\n', 'App/Use.cs': 'using Demo;\npublic class Use {\n public void Run() {\n Api.Save(1);\n Api.Save("x");\n Other.Save(2);\n Api.Save(3);\n }\n}\n', @@ -38,8 +34,13 @@ const project = (extra = '') => `')); -const args = [host, '--allow-project-evaluation', root, path.join(root, 'App/App.csproj'), 'Debug', 'net10.0']; +await fs.mkdir(path.join(root, 'build-inputs')); +await fs.writeFile(path.join(root, 'build-inputs/custom.rules'), ''); +await fs.writeFile(path.join(root, 'schema.yaml'), 'mode: original\n'); +await fs.writeFile(path.join(root, 'App/details.data'), 'additional document\n'); +await fs.writeFile(path.join(root, 'App/App.csproj'), project('')); +const additionalInputs = ['schema.yaml', 'build-inputs/custom.rules']; +const args = [host, '--allow-project-evaluation', root, path.join(root, 'App/App.csproj'), 'Debug', 'net10.0', JSON.stringify(additionalInputs)]; let child; let exit; /** @@ -73,18 +74,7 @@ function startHost() { }); return { process, exited, next, stderr: () => stderr }; } -/** - * 只读抓取给定 PID 及后代的身份;退出检查同时比对 CreationDate,避免 PID 复用误判。 - * PowerShell 命令只插入经过正整数校验的 PID,不插入路径或任意用户文本。 - */ -function ownedProcesses(pid) { - assert.ok(Number.isInteger(pid) && pid > 0); - const command = `$all = @(Get-CimInstance Win32_Process); $ids = @(${pid}); do { $more = @($all | Where-Object { $_.ParentProcessId -in $ids -and $_.ProcessId -notin $ids }); $ids += @($more | ForEach-Object { $_.ProcessId }) } while ($more.Count -gt 0); @($all | Where-Object { $_.ProcessId -in $ids } | Select-Object ProcessId,ParentProcessId,CreationDate,Name,CommandLine) | ConvertTo-Json -Compress`; - const result = spawnSync('powershell.exe', ['-NoProfile', '-Command', command], { encoding: 'utf8', windowsHide: true, timeout: 20000 }); - assert.equal(result.status, 0, result.stderr); - const value = JSON.parse(result.stdout || '[]'); - return Array.isArray(value) ? value : [value]; -} + try { // Only Host and generated fixture dependencies are restored, never a user's target application. report.hostBuild = run(['build', 'tools/WinCode.Code.Host', '-c', 'Release', '-p:RestoreLockedMode=true', '--nologo']); @@ -100,6 +90,18 @@ try { assert.equal(denied.status, 1); assert.match(denied.stdout, /Explicit project evaluation permission required/); report.scenarios.push('missing evaluation permission rejected before load'); + for (const [label, inputs, errorCode] of [ + ['outside additional input', ['../outside.yaml'], 'OUTSIDE_WORKSPACE'], + ['wildcard additional input', ['*.yaml'], 'INVALID_ARGUMENT'], + ['duplicate additional input', ['schema.yaml', './schema.yaml'], 'INVALID_ARGUMENT'], + ['missing additional input', ['missing.yaml'], 'INPUT_UNAVAILABLE'], + ['directory additional input', ['App'], 'INPUT_UNAVAILABLE'], + ]) { + const rejected = spawnSync(dotnet, [...args.slice(0, -1), JSON.stringify(inputs)], { cwd: repo, env, encoding: 'utf8', windowsHide: true, timeout: 10000 }); + assert.equal(rejected.status, 1, label); + assert.equal(JSON.parse(rejected.stdout.trim()).errorCode, errorCode, label); + report.scenarios.push(`${label} rejected before project loading`); + } const started = performance.now(); const session = startHost(); @@ -110,6 +112,9 @@ try { const ready = await next(150000); assert.equal(ready.type, 'ready', JSON.stringify(ready)); assert.equal(ready.protocolVersion, 2); + assert.equal(ready.inputPolicy.version, 1); + assert.deepEqual(ready.inputPolicy.additionalInputs.map(file => file.replaceAll('\\', '/')), additionalInputs); + assert.equal(ready.freshness.scope, 'compilation-inputs-and-explicit-files'); assert.equal(ready.projects, 2); assert.deepEqual(ready.loadDiagnostics, []); assert.deepEqual(ready.compilationErrors, []); @@ -130,51 +135,6 @@ try { return response; }; const source = code['Lib/Api.cs']; - const timedOut = await query(source.indexOf('Save(int'), { timeoutMs: 1 }); - assert.equal(timedOut.success, false, 'cold semantic operation should exceed the 1 ms test budget'); - assert.equal(timedOut.errorCode, 'CANCELLED'); - report.scenarios.push('1 ms cold-query budget cancels without losing the session'); - const integers = await query(source.indexOf('Save(int')); - assert.equal(integers.success, true, JSON.stringify(integers)); - assert.equal(integers.freshness.files, ready.freshness.files); - assert.equal(integers.freshness.fingerprint, ready.freshness.fingerprint); - assert.equal(integers.totalReferences, 2); - assert.deepEqual(integers.references.map(r => r.line).sort((a, b) => a - b), [4, 7]); - for (const reference of integers.references) { - assert.equal(reference.file.replaceAll('\\', '/'), 'App/Use.cs'); - assert.equal(reference.column, 7); - assert.equal(code['App/Use.cs'].slice(reference.start, reference.start + reference.length), 'Save'); - } - report.scenarios.push('integer overload resolves exact cross-project call spans and columns'); - const strings = await query(source.indexOf('Save(string')); - assert.equal(strings.success, true); - assert.deepEqual(strings.references.map(r => r.line), [5]); - report.scenarios.push('string overload excludes integer overload and same-name other type'); - const empty = await query(source.indexOf('Unused')); - assert.equal(empty.success, true); - assert.equal(empty.totalReferences, 0); - report.scenarios.push('valid symbol with zero references remains successful bounded evidence'); - const repeated = await query(source.indexOf('Save(int')); - assert.deepEqual(repeated.references, integers.references); - report.metrics.push({ firstQueryMs: integers.queryMs, warmQueryMs: repeated.queryMs, workingSetBytes: repeated.workingSetBytes }); - report.scenarios.push('warm query reuses snapshot and preserves exact evidence'); - const truncated = await query(source.indexOf('Save(int'), { limit: 1 }); - assert.equal(truncated.totalReferences, 2); - assert.equal(truncated.references.length, 1); - assert.equal(truncated.truncated, true); - assert.equal(truncated.queryComplete, false); - report.scenarios.push('output cap preserves total and marks incomplete'); - for (const [label, extra, errorCode] of [ - ['stale snapshot', { snapshot: 'stale' }, 'SNAPSHOT_STALE'], ['outside source', { file: '../outside.cs' }, 'OUTSIDE_WORKSPACE'], - ['invalid position', { position: -1 }, 'INVALID_ARGUMENT'], ['wrong project context', { project: 'App/App.csproj' }, 'INVALID_ARGUMENT'], - ['invalid time budget', { timeoutMs: 0 }, 'INVALID_ARGUMENT'], - ['fractional position', { position: 1.5 }, 'INVALID_ARGUMENT'], ['fractional time budget', { timeoutMs: 1.5 }, 'INVALID_ARGUMENT'], - ]) { - const rejected = await query(source.indexOf('Save(int'), extra); - assert.equal(rejected.success, false, label); - assert.equal(rejected.errorCode, errorCode, label); - report.scenarios.push(`${label} rejected`); - } /** 主动重载应生成新身份;默认只用于预期成功的稳定夹具状态。 */ const reload = async () => { const id = `reload-${++count}`; @@ -194,115 +154,15 @@ try { assert.equal(result.references, undefined); report.scenarios.push(label); }; - await fs.writeFile(path.join(root, 'App/Use.cs'), code['App/Use.cs'].replace('Api.Save(3)', 'Other.Save(3)')); - await assertStale('immediate query after source edit refuses old references'); - const firstSnapshot = activeSnapshot; - await reload(); - assert.equal((await query(source.indexOf('Save(int'))).totalReferences, 1); - assert.equal((await query(source.indexOf('Save(int'), { snapshot: firstSnapshot })).errorCode, 'SNAPSHOT_STALE'); - report.scenarios.push('reload reflects changed call and permanently expires old snapshot'); - - await fs.writeFile(path.join(root, 'App/Extra.cs'), 'class Extra { void Run() { Demo.Api.Save(9); } }'); - await assertStale('new source file invalidates the original reference file set'); - await reload(); - const added = await query(source.indexOf('Save(int')); - assert.equal(added.totalReferences, 2); - assert.ok(added.references.some(item => item.file.endsWith('Extra.cs'))); - report.scenarios.push('reloaded MSBuild Compile glob includes new call sites'); - - await fs.rename(path.join(root, 'App/Extra.cs'), path.join(root, 'App/Moved.cs')); - await assertStale('renamed file invalidates old locations'); - await reload(); - const renamed = await query(source.indexOf('Save(int')); - assert.ok(renamed.references.some(item => item.file.endsWith('Moved.cs'))); - assert.ok(renamed.references.every(item => !item.file.endsWith('Extra.cs'))); - await fs.unlink(path.join(root, 'App/Moved.cs')); - await assertStale('deleted file invalidates old references'); - await reload(); - assert.equal((await query(source.indexOf('Save(int'))).totalReferences, 1); - report.scenarios.push('rename and delete reloads return only current paths'); - - await fs.appendFile(path.join(root, 'App/obj/project.assets.json'), '\n'); - await assertStale('obj assets changes are tracked before another query'); - await reload(); - assert.equal((await query(source.indexOf('Save(int'))).totalReferences, 1); - await fs.writeFile(path.join(root, 'Directory.Build.props'), 'TRACE;EXTRA'); - await assertStale('Directory.Build.props change invalidates compiled conditions'); - await reload(); - const conditional = await query(source.indexOf('Save(int')); - assert.equal(conditional.totalReferences, 2); - assert.ok(conditional.references.some(item => item.file.endsWith('Conditional.cs'))); - report.scenarios.push('reload applies actual MSBuild preprocessor configuration'); - - const appProject = path.join(root, 'App/App.csproj'); - const originalProject = await fs.readFile(appProject, 'utf8'); - await fs.writeFile(appProject, originalProject.replace('', '')); - await assertStale('project Compile changes invalidate the loaded project graph'); - await reload(); - assert.equal((await query(source.indexOf('Save(int'))).totalReferences, 1); - report.scenarios.push('reload respects project file exclusions'); - - await fs.writeFile(appProject, ' [result.id, result])); - assert.equal(cancelled.get('cancel-control').cancellationRequested, true); - assert.equal(cancelled.get('cancel-target').errorCode, 'CANCELLED'); - assert.equal((await query(source.indexOf('Save(int'))).totalReferences, 2); - report.scenarios.push('explicit cancellation reaches queued or active work without closing the session'); - - // 单次突发同时检验身份冲突、排队截止和背压;每个输入都必须收到独立结果,不静默丢队列项。 - /** 构造当前夹具快照的引用请求,允许突发与 EOF 验收复用同一定位。 */ const burstQuery = id => ({ id, operation: 'references', snapshot: activeSnapshot, project: 'Lib/Lib.csproj', file: 'Lib/Api.cs', position: source.indexOf('Save(int') }); - // 到期的排队 reload 必须在触碰工作区前退出;若错误地到执行时才计时,会使后续旧身份查询失败。 - const burst = [burstQuery('burst-first'), burstQuery('burst-first'), - { id: 'burst-deadline', operation: 'reload', timeoutMs: 1 }, - ...Array.from({ length: 16 }, (_, index) => burstQuery(`burst-${index}`))]; - child.stdin.write(burst.map(request => JSON.stringify(request)).join('\n') + '\n'); - const burstResults = await Promise.all(burst.map(() => next())); - const duplicated = burstResults.filter(result => result.id === 'burst-first'); - assert.equal(duplicated.length, 2); - assert.equal(duplicated.filter(result => result.errorCode === 'DUPLICATE_REQUEST').length, 1); - assert.equal(duplicated.filter(result => result.success === true).length, 1); - report.scenarios.push('duplicate active id is rejected without cancelling its original request'); - assert.equal(burstResults.find(result => result.id === 'burst-deadline').errorCode, 'CANCELLED'); - report.scenarios.push('expired queued reload is cancelled before invalidating the valid snapshot'); - assert.ok(burstResults.some(result => result.errorCode === 'BUSY')); - for (const request of burst.slice(2)) assert.equal(burstResults.filter(result => result.id === request.id).length, 1); - for (const result of burstResults) { - if (result.success) assert.equal(result.totalReferences, 2); - else assert.ok(['CANCELLED', 'BUSY', 'DUPLICATE_REQUEST'].includes(result.errorCode)); - } - report.queue = { submitted: burst.length, completed: burstResults.filter(result => result.success).length, - rejectedBusy: burstResults.filter(result => result.errorCode === 'BUSY').length }; - report.scenarios.push('bounded queue reports backpressure and accounts for every submitted frame'); - child.stdin.write(JSON.stringify({ id: 'cancel-missing', operation: 'cancel', targetId: 'absent-request' }) + '\n'); - assert.equal((await next()).cancellationRequested, false); - assert.equal((await query(source.indexOf('Save(int'))).totalReferences, 2); - report.scenarios.push('cancelling an absent request reports no cancellation and preserves the session'); - - const excessive = path.join(root, 'oversized-input.bin'); - const handle = await fs.open(excessive, 'wx'); - try { await handle.truncate(33 * 1024 * 1024); } finally { await handle.close(); } - assert.equal((await query(source.indexOf('Save(int'))).errorCode, 'INPUT_BUDGET_EXCEEDED'); - await fs.unlink(excessive); - await reload(); - report.scenarios.push('input byte cap rejects oversized input without accepting a partial fingerprint'); + const scenario = { root, source, code, child, next, query, reload, assertStale, report, ready, burstQuery, + get snapshot() { return activeSnapshot; }, nextId: () => ++count }; + // 场景有显式前置状态;不为拆文件而改变执行顺序或并发修改同一夹具。 + await verifySemantics(scenario); + await verifyInputChanges(scenario); + await verifyQueue(scenario); + await verifyBudgetsAndEncoding(scenario); await fs.writeFile(path.join(root, 'global.json'), JSON.stringify({ sdk: { version: '10.0.303', rollForward: 'disable' } })); assert.equal((await query(source.indexOf('Save(int'))).errorCode, 'HOST_RESTART_REQUIRED'); diff --git a/scripts/verify-serena-real.ts b/scripts/verify-serena-real.ts deleted file mode 100644 index 48f4e46..0000000 --- a/scripts/verify-serena-real.ts +++ /dev/null @@ -1,181 +0,0 @@ -import assert from 'node:assert/strict'; -import fs from 'node:fs/promises'; -import path from 'node:path'; -import { SerenaAdapter } from '../src/Adapters/SerenaAdapter.js'; -import { CacheManager } from '../src/Core/Cache.js'; -import { getDefaultConfig } from '../src/Core/Config.js'; -import { killProcessTree } from '../src/Core/ResourceManager.js'; -import { ToolRouter } from '../src/Core/ToolRouter.js'; - -// Explicit opt-in: use an already installed, isolated Serena command. Never install prerequisites here. -const [command, ...prefixArgs] = process.argv.slice(2); -if (!command || !path.isAbsolute(command)) throw new Error('Usage: test:serena-real -- [launcher arguments]. The command must accept Serena CLI arguments.'); -await fs.access(command); -const root = path.resolve('test-tmp/serena-acceptance', `${Date.now()}-${process.pid}`); -await fs.mkdir(path.join(root, '.serena'), { recursive: true }); -await fs.copyFile('global.json', path.join(root, 'global.json')); -await fs.writeFile(path.join(root, '.serena/project.yml'), 'project_name: wincode-real-acceptance\nlanguage_servers: [csharp]\nread_only: true\n'); -await fs.writeFile(path.join(root, 'Fixture.csproj'), 'net10.0'); -const source = `namespace Fixture; -public class Service { - public int Save(int value) { return value + 1; } - public string Save(string value) { return value + "!"; } - public int Unused() { return 42; } -} -public class Other { - public int Save(int value) { return value - 1; } -} -public class Caller { - public int Run() { return new Service().Save(7); } - public string Text() { return new Service().Save("y"); } -} -`; -await fs.writeFile(path.join(root, 'Service.cs'), source); -const report: any = { startedAt: new Date().toISOString(), command, prefixArgs, fixture: root, stages: [], passed: false }; -const adapters: SerenaAdapter[] = []; -async function create(active: boolean) { - const config = getDefaultConfig(root); - config.adapters.serena.customCommand = command; - config.adapters.serena.customArgs = [...prefixArgs, 'start-mcp-server', - ...(active ? ['--project', root] : []), '--enable-web-dashboard', 'false', '--open-web-dashboard', 'false', - '--enable-gui-log-window', 'false', '--log-level', 'WARNING']; - const cache = new CacheManager(path.join(root, active ? 'active-cache' : 'inactive-cache')); - await cache.initialize(); - const adapter = new SerenaAdapter(config, cache); - adapters.push(adapter); - await adapter.initialize(); - return { adapter, config }; -} -async function stage(name: string, run: () => Promise) { - const start = Date.now(); - try { const result = await run(); report.stages.push({ name, passed: true, ms: Date.now() - start, result }); } - catch (error) { report.stages.push({ name, passed: false, ms: Date.now() - start, error: String(error) }); throw error; } -} -try { - const { adapter, config } = await create(true); - await stage('same-name and overload identities through WinCode adapter', async () => { - const found = await adapter.findSymbolsDetailed('Save', undefined, 'Service.cs'); - assert.equal(found.source, 'serena-mcp'); assert.equal(found.queryComplete, true); - assert.deepEqual(found.symbols.map(s => [s.namePath, s.line]), [ - ['Fixture/Service/Save[0]', 3], ['Fixture/Service/Save[1]', 4], ['Fixture/Other/Save', 8], - ]); - assert.equal(adapter.getUpstreamStatus().semanticQueryUsable, true); - return { found, health: adapter.getUpstreamStatus() }; - }); - await stage('ambiguous name never selects an overload', async () => { - const refs = await adapter.findReferencesDetailed('Save', 'Service.cs'); - assert.equal(refs.resolution, 'ambiguous'); assert.equal(refs.candidateCount, 3); - assert.equal(refs.queryComplete, false); return refs; - }); - await stage('explicit overload references retain containing-symbol coordinates', async () => { - const results = []; - for (const [name, preview, zeroReferenceLine] of [ - ['Fixture/Service/Save[0]', 'Save(7)', 10], ['Fixture/Service/Save[1]', 'Save("y")', 11], - ] as const) { - const refs = await adapter.findReferencesDetailed(name, 'Service.cs'); - assert.equal(refs.source, 'serena-mcp'); assert.equal(refs.queryComplete, true); - assert.equal(refs.resolution, 'resolved'); assert.equal(refs.totalReferences, 1); - assert.equal(refs.target?.namePath, name); assert.ok(refs.references[0].preview.includes(preview)); - const marked = refs.references[0].preview.split('\n').find(line => /^\s*>\s*\d+:/.test(line)); - assert.ok(marked?.includes(`> ${zeroReferenceLine}:`) && marked.includes(preview), 'marked reference, not merely surrounding context, matches this overload'); - assert.equal(refs.references[0].lineKind, 'containing-symbol'); - assert.equal(refs.references[0].line, 10); results.push(refs); - } - return results; - }); - await stage('real upstream body matches fixture source (direct upstream oracle)', async () => { - // Test-only access to the actual connected upstream; do not add a public product API for this oracle. - const result = await (adapter as any).serenaClient.callTool({ name: 'find_symbol', arguments: { - name_path_pattern: '/Fixture/Service/Save[0]', relative_path: 'Service.cs', include_body: true, - } }); - assert.notEqual(result.isError, true); - const body = JSON.parse(result.content[0].text); - assert.equal(body.length, 1); assert.equal(body[0].body, source.split('\n')[2].trim()); - return result; - }); - await stage('valid empty symbols and references stay semantic', async () => { - const empty = await adapter.findSymbolsDetailed('AbsentSymbol', undefined, 'Service.cs'); - const unused = await adapter.findReferencesDetailed('Fixture/Service/Unused', 'Service.cs'); - assert.equal(empty.source, 'serena-mcp'); assert.equal(empty.queryComplete, true); assert.equal(empty.totalFound, 0); - assert.equal(unused.source, 'serena-mcp'); assert.equal(unused.queryComplete, true); assert.equal(unused.totalReferences, 0); - return { empty, unused }; - }); - await stage('actual process interruption and unavailable restart degrade honestly', async () => { - const pid = (adapter as any).serenaPid as number; - assert.ok(pid > 0); - // Only the test-owned Serena tree is terminated. Its restart command is made unavailable in this fixture. - config.adapters.serena.customCommand = path.join(root, 'missing-serena.exe'); - await killProcessTree({ pid }); - for (let i = 0; i < 100 && adapter.getUpstreamStatus().handshakeOk; i++) await new Promise(r => setTimeout(r, 20)); - const fallback = await adapter.findSymbolsDetailed('Unused', undefined, 'Service.cs'); - assert.equal(fallback.source, 'serena-adapter-fallback'); - assert.equal(adapter.getUpstreamStatus().semanticQueryUsable, false); - assert.throws(() => process.kill(pid, 0), { code: 'ESRCH' }); - return { fallback, health: adapter.getUpstreamStatus(), pidExited: pid }; - }); - await stage('real unactivated project remains inactive', async () => { - const { adapter: inactive } = await create(false); - const found = await inactive.findSymbolsDetailed('Save', undefined, 'Service.cs'); - assert.equal(found.source, 'serena-adapter-fallback'); assert.equal(found.queryComplete, false); - assert.equal(inactive.getUpstreamStatus().projectActive, false); - return { found, health: inactive.getUpstreamStatus() }; - }); - await stage('real Router A-B-A switches rebind upstream and cached query evidence', async () => { - const workspaces = [path.join(root, 'switch-a'), path.join(root, 'switch-b')]; - for (const [index, directory] of workspaces.entries()) { - await fs.mkdir(path.join(directory, '.serena'), { recursive: true }); - await fs.copyFile('global.json', path.join(directory, 'global.json')); - await fs.writeFile(path.join(directory, '.serena/project.yml'), `project_name: switch-${index}\nlanguage_servers: [csharp]\nread_only: true\n`); - await fs.writeFile(path.join(directory, 'Fixture.csproj'), 'net10.0'); - await fs.writeFile(path.join(directory, `Unique${index}.cs`), 'public class Marker {}\n' + - [0, 1, 2].map(round => `public class Probe${round} {}\n`).join('')); - } - const routeConfig = getDefaultConfig(workspaces[0]); - routeConfig.adapters.serena.customCommand = command; - routeConfig.adapters.serena.customArgs = [...prefixArgs, 'start-mcp-server', '--project-from-cwd', - '--enable-web-dashboard', 'false', '--open-web-dashboard', 'false', '--enable-gui-log-window', 'false', '--log-level', 'WARNING']; - routeConfig.adapters.flaui.enabled = false; - routeConfig.adapters.repomix.useCli = false; - const router = new ToolRouter(routeConfig); - const results = [], ownedPids: number[] = []; - try { - await router.initialize(); - for (const [round, index] of [0, 1, 0].entries()) { - await router.openWorkspace(workspaces[index]); - await router.acquireRequestSlot(); - try { - const result = await router.findCodeSymbols('Marker'); - // Returning to A may legitimately reuse A's cache without a process. - // A fresh query additionally proves that the new connection binds A. - const fresh = await router.findCodeSymbols(`Probe${round}`); - const pid = (router.serena as any).serenaPid as number; - if (pid && !ownedPids.includes(pid)) ownedPids.push(pid); - assert.equal(result.source, 'serena-mcp'); assert.equal(result.queryComplete, true); - assert.deepEqual(result.symbols.map(symbol => [symbol.name, symbol.file]), [['Marker', `Unique${index}.cs`]]); - assert.equal(fresh.source, 'serena-mcp'); assert.equal(fresh.queryComplete, true); - assert.deepEqual(fresh.symbols.map(symbol => [symbol.name, symbol.file]), [[`Probe${round}`, `Unique${index}.cs`]]); - assert.equal(router.workspaceRecoveryState, null); - results.push({ workspace: workspaces[index], pid, result, fresh }); - } finally { router.endRequest(); } - } - assert.equal(ownedPids.length, 3); - return { results, ownedPids }; - } finally { - await router.dispose(); - for (const pid of ownedPids) assert.throws(() => process.kill(pid, 0), { code: 'ESRCH' }); - } - }); - assert.equal(await fs.readFile(path.join(root, 'Service.cs'), 'utf8'), source); - report.passed = true; -} catch (error) { report.error = String(error); process.exitCode = 1; } -finally { - const pids = adapters.map(a => (a as any).serenaPid as number | null).filter((p): p is number => Boolean(p)); - const cleanup = await Promise.allSettled(adapters.map(a => a.dispose())); - report.cleanup = cleanup.map(r => r.status === 'fulfilled' ? { closed: true } : { closed: false, error: String(r.reason) }); - if (cleanup.some(r => r.status === 'rejected')) { report.passed = false; process.exitCode = 1; } - report.pidExit = pids.map(pid => { try { process.kill(pid, 0); return { pid, exited: false }; } catch (e: any) { return {pid, exited: e.code === 'ESRCH'}; } }); - if (report.pidExit.some((p: any) => !p.exited)) { report.passed = false; process.exitCode = 1; } - report.completedAt = new Date().toISOString(); - await fs.writeFile(path.join(root, 'report.json'), JSON.stringify(report, null, 2)); - console.log(JSON.stringify({ passed: report.passed, stages: report.stages.map((s: any) => ({ name: s.name, passed: s.passed })), report: path.join(root, 'report.json') })); -} diff --git a/scripts/verify-tavern-context.ts b/scripts/verify-tavern-context.ts index 8039e8a..12b6692 100644 --- a/scripts/verify-tavern-context.ts +++ b/scripts/verify-tavern-context.ts @@ -43,7 +43,7 @@ import { getDefaultConfig } from ${url('Core/Config.js')}; import { ToolRouter } from ${url('Core/ToolRouter.js')}; import { WinCodeMcpServer } from ${url('Gateway/McpServer.js')}; const config = getDefaultConfig(${JSON.stringify(temporary)}); -config.adapters.serena.enabled = false; + config.adapters.flaui.enabled = ${Boolean(uiPid)}; config.adapters.repomix.useCli = false; const server = new WinCodeMcpServer(new ToolRouter(config)); diff --git a/skills/wincode/SKILL.md b/skills/wincode/SKILL.md index 8859a4b..f8a8d1a 100644 --- a/skills/wincode/SKILL.md +++ b/skills/wincode/SKILL.md @@ -5,9 +5,9 @@ description: 使用 WinCode MCP 分析 Windows/.NET 工作区,或读取桌面 # WinCode -发布基线:0.12.5;手册修订:2026-09-09(含本地 E1/E2 修复与可选直接 Roslyn MCP 接入,不代表新版本已发布)。安装内容可用 `node scripts/sync-skill.mjs <安装目录绝对路径>` 核对;仅维护时执行,不在每个任务中例行检查。以当前连接实际 Schema 为准,手册版本不证明 MCP 已重连。 +源码契约:0.13.0(含 E4 开发中错误 JSON 迁移,状态见诊断手册);手册修订:2026-09-09(本地待发布)。外部 Serena 入口与旧 source 已退役,不能将此版本号当作当前连接已升级。安装内容可用 `node scripts/sync-skill.mjs <安装目录绝对路径>` 核对;仅维护时执行。以当前连接实际 Schema 为准。 -默认代码路径仍使用 Serena;显式配置 Roslyn 的实例已通过相同 MCP 工具接入 WinCode.Code.Host。读取实际 codeProvider/source:Roslyn 搜索返回的 location 可作为引用工具的 symbolLocation;不要猜测定位、复用过期快照或把 Serena namePath 当作 Roslyn 身份。内部 position/snapshot/project 顶层参数及 reload/cancel 不是 MCP 工具字段。配置与维护验收边界见代码手册。 +默认以本地文本模式启动,source=local-text;明确配置 Roslyn 后,才通过 WinCode.Code.Host 提供 C# 语义证据。搜索返回的 location 可作为引用、影响分析和重构工具的 symbolLocation;不要猜测定位、复用旧快照或使用已退役的 namePath。内部 reload/cancel 不是 MCP 工具字段。配置与验收边界见代码手册。 仅按当前任务读取对应手册,不预读全部文件: - 代码、上下文、引用、影响分析:[code](references/code.md)。 diff --git a/skills/wincode/references/code.md b/skills/wincode/references/code.md index 3da23ff..cd8f673 100644 --- a/skills/wincode/references/code.md +++ b/skills/wincode/references/code.md @@ -4,11 +4,11 @@ ## 后端与实验接口边界 -默认 Gateway 使用 SerenaAdapter 或明确标记的本地文本降级;显式启用 Roslyn 的实例通过相同工具提供 C# 声明、引用和影响证据,不启动 Serena/Python,也不在失败后偷偷切回 Serena。`hello.codeProvider` 标明实例选择;`source` 按实际响应读取,不能根据仓库中存在 Host 推断当前连接已经更新。 +默认 Gateway 使用 WinCode 内置文本能力,`source=local-text`,健康状态明确 semanticConfigured=false。显式启用 Roslyn 后使用直接 Code Host,失败会报错,不会偷偷改换提供方。外部 Serena 连接配置、启动器及旧 `serena-adapter-fallback` 来源已退役;旧调用方须适配。`hello.codeProvider` 标明实例选择,不能根据仓库中存在 Host 推断当前连接已更新。 Roslyn 调用顺序:用 wincode_find_code_symbol 搜索(query 最长 256 字符),根据 signature、file 和 location.project 选择具体声明;再把该项的 name 作为 symbolName、完整 location 对象作为 symbolLocation 传给 wincode_find_references。location 包含 snapshotId(32 位小写十六进制)、project/file(工作区内相对路径)和 position(非负零基 UTF-16)。不手工猜偏移;同名/重载返回候选,不能自动选第一项。简单名称查询在当前不完整范围下只返回候选,单候选也需明确定位;candidatesTruncated=true 时 candidateCount 可能缺省,不能当作全量计数。 -编辑、重载或工作区切换会使 location 失效。SNAPSHOT_STALE/INPUTS_CHANGED 后,下一次显式符号搜索执行所需重载;失败请求不自动重放。若编辑后直接搜索,首个请求也可能报告过期,再显式搜索恢复。HOST_RESTART_REQUIRED 按诊断手册重新打开工作区。Serena 实例明确拒绝 symbolLocation;Serena 的 namePath/重载序号不能迁移为 Roslyn 身份。semanticContext 保留快照、输入检查点、排除生成器数和范围,queryComplete=false 时零引用仍不能证明可删除。 +受跟踪输入的变化、重载或工作区切换会使 location 失效;无关文件编辑不等于编译输入变化。SNAPSHOT_STALE/INPUTS_CHANGED 后,下一次显式符号搜索执行所需重载;失败请求不自动重放。若源码编辑后直接搜索,首个请求也可能报告过期,再显式搜索恢复。HOST_RESTART_REQUIRED 按诊断手册重新打开工作区。本地文本实例明确拒绝 symbolLocation;旧 namePath/重载序号不能迁移为 Roslyn 身份。semanticContext 保留快照、输入检查点、排除生成器数和范围,queryComplete=false 时零引用仍不能证明可删除。 维护者可用启动参数 `--roslyn-config <配置 JSON 的绝对路径>` 显式选择;不从目标仓库自动发现执行配置。JSON 对应宿主 WinCodeConfig.adapters.roslyn,最多 16 KiB,示例路径须替换成已安装/已构建的实际文件: @@ -20,15 +20,18 @@ Roslyn 调用顺序:用 wincode_find_code_symbol 搜索(query 最长 256 字 "configuration": "Debug", "targetFramework": "net10.0", "dotnetPath": "C:/dotnet/dotnet.exe", - "hostPath": "C:/WinCode/tools/WinCode.Code.Host/bin/Release/net10.0/WinCode.Code.Host.dll" + "hostPath": "C:/WinCode/tools/WinCode.Code.Host/bin/Release/net10.0/publish/WinCode.Code.Host.dll", + "additionalInputs": [] } ``` allowProjectEvaluation 表示允许 MSBuild 设计时求值执行项目 targets,须符合用户授权;不会自动 restore 或下载 SDK。project 是相对当前工作区的固定入口;A→B 切换后使用 B 中同一路径,缺失就报错,不猜其他项目。配置和 TFM 当前固定于实例,要改变它们需更新启动配置并重启 Gateway。dotnetPath/hostPath 必须为绝对普通文件,重解析路径不支持;子进程使用指定 dotnet 的安装根,不改系统环境。可选 loadTimeoutMs 为 1–120000(默认 120000),queryTimeoutMs 为 1–60000(默认 30000),不属于 MCP 请求参数。 -维护验收使用 `npm run test:roslyn-host`(独立 Host)和 `npm run test:roslyn-gateway`(已构建 Gateway 的真实 stdio MCP)。要求已有项目内 SDK `.deps/dotnet-10.0.303`,会构建 Host、还原生成夹具并写入 test-tmp;Gateway 脚本还会在生成的 targets 中启动受控测试子进程,验证取消/崩溃/超时。它们不是日常工具不可用时的替代调用,不证明发布包或当前 Codex 连接已更新。环境变更须在用户授权范围内。 +维护验收使用 `npm run test:roslyn-host`(独立 Host)和 `npm run test:roslyn-gateway`(真实 stdio MCP)。维护脚本按显式 WINCODE_DOTNET_PATH、项目 .deps、DOTNET_HOST_PATH、PATH 顺序寻找已安装 SDK,并核对 global.json 的精确版本;不下载安装。当前要求 10.0.303。这些脚本只还原生成夹具,保留 test-tmp 报告;Gateway 验收使用完整发布目录的异地副本。此验收不证明当前 Codex 连接已更新或无 SDK 的机器可运行。 -原型通过独立进程的 JSON 行协议 v2 工作,非 MCP tools/call:启动参数为 `--allow-project-evaluation ROOT PROJECT CONFIGURATION FRAMEWORK`;加载后 ready 帧给出 protocolVersion=2 和 snapshot。项目求值可能执行 targets,不自动 restore;本维护验收只使用获准的生成夹具。协议及启动方式以源码 `tools/WinCode.Code.Host/Program.cs` 注释为准,尚非稳定公共接口。 +`additionalInputs` 是可选启动配置,默认空数组。例如自定义构建读取现存的 `schema.yaml` 和非标准导入 `build-inputs/custom.rules`,可填 `["schema.yaml","build-inputs/custom.rules"]`。最多 32 个工作区相对文件路径,数组 JSON 最长 4096 个 UTF-16 字符;不接受根外/绝对路径、重复项、目录、通配符或链接。缺失项报 INPUT_UNAVAILABLE,不静默删除;创建或恢复文件后再显式搜索。切换工作区后列表按新根解释,各根均须具备所列文件。修改列表需更新启动配置并重启 Gateway,普通 MCP 参数不能添加输入或获取项目执行许可。 + +Host 通过独立进程的 JSON 行协议 v2 工作,非 MCP tools/call:启动参数为 `--allow-project-evaluation ROOT PROJECT CONFIGURATION FRAMEWORK [ADDITIONAL_INPUTS_JSON]`;加载后 ready 帧给出 protocolVersion=2、snapshot 及 inputPolicy={version:1,additionalInputs:[...]}。Gateway 必须核对实际列表;旧 Host 缺少输入策略确认或列表不一致时拒绝接入,即使同为协议 v2 也不能假定兼容。项目求值可能执行 targets,不自动 restore;本维护验收只使用获准的生成夹具。协议及启动方式以源码 `tools/WinCode.Code.Host/Program.cs` 注释为准,尚非稳定公共接口。 | 内部 operation | 请求与结果 | | --- | --- | @@ -40,7 +43,9 @@ allowProjectEvaluation 表示允许 MSBuild 设计时求值执行项目 targets 每帧还须包含 operation;id 为 1–128 字符且活动期间不可重复。队列最多等待 8 项,满时 BUSY;timeoutMs 从接纳起计算,包含排队,Host 本身执行协作取消。Gateway 超时/取消先等待目标收尾,超过 1 秒宽限才回收自有 Host 进程树;初次加载尚不能接收 cancel 时直接回收。Windows Host 在加载前绑定自有 Job,以覆盖普通子进程继承的退出行为;这不是沙盒,也不约束 targets 通过外部服务启动的进程。请求帧最多 65536 个 UTF-16 字符,Node 接收帧最多 1 Mi 字符,超长使通道失效。SDK/global.json、监听或资源释放故障可能要求新进程,不能循环 reload。 -Host 监听变化并在查询前后比较输入内容指纹,变化时丢弃结果并要求显式 reload。freshness.status=checked 仅覆盖其声明的工作区文件、已加载文档/元数据及祖先常规配置;包括新增文件与 obj/assets,默认排除 bin/node_modules 等目录,但显式加载的输入仍检查。预算为最多 20000 个枚举条目、5000 个文件、总计 128 MiB、单文件 32 MiB;超过即失败,不接受截断快照。不支持重解析路径。 +Host 监听变化并在查询前后比较内容指纹,变化时丢弃结果并要求显式 reload。每次加载尝试前用最多四个 50 ms 观察窗收敛输入事件,受请求取消预算约束;持续写入仍失败。求值期间的内容/事件检查继续保留,不自动重放请求。freshness.scope=compilation-inputs-and-explicit-files,自动候选包括 .cs/.csproj/.props/.targets、.xaml/.resx/.resw/.resources、.config/.ruleset,global.json、project.assets.json、packages.lock.json、.editorconfig/.globalconfig 及 *.nuget.dgspec.json。同时跟踪实际加载的文档、AdditionalFiles、分析配置、程序集引用和祖先常规构建配置;新增源码仍经 MSBuild 的 Compile 规则决定是否加载,不因发现 .cs 就直接加入项目。 + +默认枚举排除 .git、node_modules、.deps、bin、dist、build、.cache、.vs、.packages、test-tmp、trash;实际加载或显式补充的文件优先于目录排除。非标准扩展名导入、排除目录中的自定义配置,以及自定义 targets 隐式读取的数据,应通过 additionalInputs 补充;不能承诺自动发现任意构建依赖。普通 README、视频和未加载的二进制文件不占输入字节预算。文件清单仍需有界枚举,极大目录仍可能超限;预算为最多 20000 个枚举条目、5000 个输入文件、总计 128 MiB、单个输入 32 MiB。必要输入或显式补充文件超限仍失败,不接受截断快照。内容核查流式计算摘要,仅冻结文档时保留源码正文;不支持重解析路径。 自定义 targets 的任意外部输入和整个磁盘原子快照尚未验证,所以仍保留 diskFreshnessVerified=false、externalCustomInputsVerified=false。queryComplete 当前为 false;排除的分析器/生成器、加载及编译诊断须保留,零引用不证明安全删除。普通 MCP 请求使用下方规范字段;snapshotId 仅出现在 symbolLocation/semanticContext 内,不单独作为顶层参数发送。未知字段可能被忽略,成功响应不证明新参数生效。TS/JS/Python 的限定文件文本取证仍走 prepare_context,不把 Roslyn 声明搜索当成多语言语义服务。 @@ -55,8 +60,8 @@ Host 监听变化并在查询前后比较输入内容指纹,变化时丢弃结 | `wincode_analyze_workspace` | 无 | `maxDepth`: 数字,默认 2 | | `wincode_find_code_symbol` | `query`: 非空字符串 | `kind`: 字符串,常用 `class/interface/method/function/type/enum`;此工具未声明文件范围参数,指定文件取证改用下面的 `scopeFiles` | | `wincode_find_references` | `symbolName`: 非空字符串 | `relativePath`: 定义文件相对路径;`symbolLocation`: Roslyn 搜索返回的 location 对象(snapshotId/project/file/position 均必填,路径各最长 4096);同时提供 relativePath 时必须与 location.file 一致 | -| `analyze_change_impact` | `target`: 非空字符串 | 无 | -| `wincode_plan_refactoring` | `target`、`goal`: 非空字符串 | 无 | +| `analyze_change_impact` | `target`: 非空字符串 | `symbolLocation`: 搜索返回的完整定位;提供时 target 必须是该符号的简单名称 | +| `wincode_plan_refactoring` | `target`、`goal`: 非空字符串 | `symbolLocation`: 同影响分析 | | `wincode_safe_move_to_trash` | `filePath`: 工作区内相对路径字符串 | `reason`: 字符串;该工具实际移动文件,须符合用户授权 | `wincode_analyze_change_impact` 是 `analyze_change_impact` 的公布别名;`wincode_workspace_open` 是 `workspace_open` 的历史兼容别名。别名共享参数和执行规则,优先使用本连接 tools/list 公布的名称。其余字段名不接受自动拼写纠正。 @@ -97,7 +102,7 @@ lineRanges 查看最终 coverage.allRequestedCovered、completeLines 和 details 按目标选工具,不顺序执行整张表。已知文件范围时直接限定: -上游结果有 namePath 时保留原值(如 Service/Save[0]),续查用 symbolName:namePath 加 relativePath:file;不要还原成短名或删除重载索引。简单名称歧义检查 resolution/candidateCount/candidatesTruncated,不能选第一项。queryComplete=false 或解析失败不能解释为零引用;lineKind=containing-symbol 不是精确调用点。 +选定 Roslyn 重载后,将其 name 和 location 原样传给后续工具:引用使用 symbolName,影响分析及重构使用 target,同时传 symbolLocation。后两者先验证定位再分析,不按名字重选目标;SNAPSHOT_STALE/INPUTS_CHANGED 时须重新搜索。简单名称歧义检查 resolution/candidateCount/candidatesTruncated,不能选第一项。queryComplete=false 不等于零引用。 workspace_open 默认返回项目摘要和最多 8 个入口,整份 JSON 默认不超过 8000 个 UTF-16 字符;不生成目录树或统计全仓大小。检查 projectScanComplete,null 统计不等于零。需要目录时用 wincode_list_directory 指定窄路径,查看 scanComplete/truncated/omissions。includeTree:true 可显式取得有界兼容树,不能当成完整仓库清单。maxOutputChars 为 2048–32768;目录 maxDepth 为 1–5,maxEntries 为 1–500。需要生成目录时显式 includeIgnored:true,但不能越过工作区边界。 @@ -133,7 +138,9 @@ bodyStatusScope 明确该字段描述 displayed-snippet 或 packed-file。symbol 保留 queryComplete、truncated、metadataTruncated、limitationsOmitted、omittedFiles/omittedFileCount 等字段的含义;预算裁剪后不得把缺失当成不存在。根据缺口收窄候选或增加预算,勿例行拉取全文。 -检查 source、queryComplete、uniqueResolution/uniqueTypeMatch 与 limitations。文本回退不保证语义引用完整;零引用、UNKNOWN 或未找到均不证明可安全删除。 +检查 source、queryComplete、uniqueResolution/uniqueTypeMatch 与 limitations。source=roslyn 是编译器语义来源,不是文本回退;queryComplete=false 可以表示生成器或加载图等覆盖缺口,不能直接解释为执行中断或要求原样重试。文本回退不保证语义引用完整;零引用、UNKNOWN 或未找到均不证明可安全删除。 + +影响分析用完整工作区文件路径及可用的项目身份区分组件,targetFile 和组件 name 仍是展示名称;不同目录同名组件可以分别出现,不要按 name 再合并。提供目录的 target 按工作区解析;只有纯文件名才用于候选匹配。Host 冻结源码沿用 Roslyn/MSBuild 的 CodePage 与 BOM 编码,返回位置仍按解码后的 UTF-16 文本计算,不按原始文件字节偏移定位。 若已有影响报告,直接据此规划,不为获得通用清单再次调用 plan_refactoring。该工具仍会做影响分析;它返回的 evidence 保留歧义、降级和 UNKNOWN,不代表已经执行重构。 @@ -142,3 +149,5 @@ bodyStatusScope 明确该字段描述 displayed-snippet 或 packed-file。symbol trash 响应保留 success/trashPath/message,并用 outcome 区分 completed(移动及元数据完成)、not_moved(本次未移动)、partial(已移动但元数据未完成)。partial 的 errorCode=TRASH_METADATA_FAILED、failureStage=metadata,originalPath/trashPath/metadataPath 给出原位置、实际移动位置及预期元数据位置;metadataPath 不证明元数据完整。立即保留并告知用户实际 trashPath,不把 success=false 当作未执行,不重复移动或自动移回。not_moved 的 trashPath 为空,errorCode=TRASH_NOT_MOVED;先检查 failureStage 和文件实际状态。重启不会自动补写元数据或推断原路径;丢失 partial 响应时,本实现不保证自动恢复原目录映射。 回收站目标名含唯一标识,过长的原文件名展示部分会截短,以给元数据文件名预留空间;完整原路径保存在 originalPath 和成功写入的元数据中。恢复时使用这些路径,不从截短的目标名推断原文件名或扩展名。 + +交付时保留 Code Host 整个 publish 目录,包括 deps/runtimeconfig、Roslyn 依赖及 BuildHost-netcore 子目录。`npm run check` 生成并核对交付清单;Host ready 身份必须与 Gateway 版本一致且为 Release、协议 v2,否则 HOST_VERSION_MISMATCH。不要仅复制入口 DLL,也不要把版本握手等同于运行时文件防篡改。 diff --git a/skills/wincode/references/diagnostics.md b/skills/wincode/references/diagnostics.md index bac7af7..5562de7 100644 --- a/skills/wincode/references/diagnostics.md +++ b/skills/wincode/references/diagnostics.md @@ -1,12 +1,12 @@ # 诊断与审计 -0.12.5 已兼容 Serena 1.7/FastMCP 的 structuredContent.result 字符串包装。真实上游验收入口是维护命令 `npm run test:serena-real -- <已安装命令绝对路径> [启动器参数]`,只在用户要求验收且环境已准备时执行;它创建独立 C# 夹具,记录重载、引用、空结果、未激活、断连降级及 PID 退出。脚本不自动安装,也不把 commandFound/握手成功当作语义可用。安装在 test-tmp 的上游仅用于隔离验收,不表示 Codex 默认连接已启用 Serena。 +0.13.0 彻底退役外部 Serena。默认本地文本模式可用,但不提供编译器语义;需要 C# 语义时按代码手册显式配置直接 Roslyn。维护入口为 test:roslyn-host 与 test:roslyn-gateway,不再有 test:serena-real。 从 0.12.4 起 Repomix 健康探测和打包都由当前 Node 可执行文件直接启动已安装的 JavaScript CLI;不经过 cmd、npx 或 PATH 包装脚本,也不下载包。默认按目标工作区和 WinCode 安装目录的 Node 模块路径读取 repomix/package.json 的 bin 入口;不搜索 npx 缓存或 npm 自定义全局前缀。非标准安装需在宿主 WinCodeConfig.adapters.repomix.customCliPath 提供绝对 .js/.cjs/.mjs 路径;该字段不是 MCP 工具参数,不能传给 hello/prepare_context。显式路径无效时返回 builtin fallback,不执行另一份安装;useCli=false 仍完全禁止探测和启动。执行已安装脚本不提供沙盒或脚本可信性保证。 `hello` 从 0.12.1 起只读取版本、能力和已知状态,不启动上游、CLI 或 UI Host 探测进程。`health.healthObservation` 区分 `known/unknown` 并给出 `observedAt`;`unknown`、`available:null` 或 `commandFound:null` 表示尚未探测,不能解释为不可用。配置禁用属于已知策略,但观察时间可为 null。已知健康结果可能陈旧,需要当前检查时调用现有 `wincode_diagnose_project({})`,不向 hello 添加未声明的 force/probe 字段。 -代码查询、引用、上下文、影响分析和重构建议接收 MCP 客户端取消信号;停止后续扫描/打包,等待当前读操作或自有上游进程清理后释放请求占用。上游 RPC 取消可能重置共享 Serena 连接,其他上游调用可能失败或降级;不保证外部服务器的单请求取消实现。磁盘单次 OS I/O 不能保证瞬时中断。工作区切换在等待和提交前可取消;已开始提交切换时完成一致性收尾,不声称已回滚。 +代码查询、引用、上下文、影响分析和重构建议接收 MCP 客户端取消信号;停止后续扫描/打包,等待当前读操作或自有上游进程清理后释放请求占用。Roslyn 取消会传播到自有 Host;若合作取消未及时完成,则按既有超时策略清理自有进程树,不宣称其他请求已成功。磁盘单次 OS I/O 不能保证瞬时中断。工作区切换在等待和提交前可取消;已开始提交切换时完成一致性收尾,不声称已回滚。 `health.resourceCleanup` 是最多 100 条资源关闭记录(owner、kind、closed/failed 与最多 1024 字符错误),`omitted` 表示更早记录被省略。进程数量为零不能替代这些结果或真实 PID 退出证据。关闭失败会向调用方抛出,重复关闭保留失败;初始化失败会尝试释放已取得资源。记录只保存在当前进程内,不是持久审计或防篡改证明。 @@ -14,7 +14,7 @@ 更新仓库后,先用 `npm run skill:check -- <已安装 wincode 目录的绝对路径>` 核对四份受管手册;不一致退出码为 2。明确更新时使用 `npm run skill:sync -- <同一路径>`,先在同级 .wincode-backup-* 目录以 .bak 后缀备份旧手册(避免备份被发现为重复 Skill),再写入并校验哈希;其他文件保持原样。此操作不注册 MCP、不改客户端配置、不重启运行实例。检查本机安装内容与仓库一致也不证明当前连接加载了新版。 -仅遇到故障或用户要求时调用 wincode_hello_world({}) 查看适配器、工作区及 runtime;环境问题再用 wincode_diagnose_project({})。健康成功不证明 Serena 语义连接成功;watcher 停止、最近超时和清理错误如实报告,不自动安装依赖或循环重启。 +仅遇到故障或用户要求时调用 wincode_hello_world({}) 查看适配器、工作区及 runtime;环境问题再用 wincode_diagnose_project({})。本地文本健康成功不证明 Roslyn 已配置或项目已加载;watcher 停止、最近超时和清理错误如实报告,不自动安装依赖或循环重启。 工具不可用:先确认客户端是否启用了 wincode MCP;已保存配置通常需重新加载客户端/会话。Skill 不负责注册 MCP。安装路径取实际客户端配置,不沿用历史机器的 I:/WinCode。STDIO 配置结构(占位路径需替换): - 命令:node @@ -42,10 +42,19 @@ E4 统一错误表达尚未实施:当前可能收到 isError=true 的纯文本 直接 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 运行中已观察到的加载、查询或清理错误也纳入 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;不会重放失败引用。 -INPUT_UNAVAILABLE/HOST_UNAVAILABLE 先检查明确的配置文件、SDK/Host/项目路径;HOST_PROTOCOL_ERROR 检查 Host 与 Gateway 的协议版本,不绕过校验。LEGACY_SYMBOL_ID 要求重新搜索 Roslyn 身份;UNSUPPORTED_SYMBOL_LOCATION 表示该实例使用 Serena;SYMBOL_MISMATCH 表示名称和定位不一致。INPUT_BUDGET_EXCEEDED 先缩小受支持范围,不能接受截断指纹。内部 BUSY 表示队列已满,DUPLICATE_REQUEST 要求新的 id;CANCELLED 是目标终止结果,取消确认不替代它。OUTSIDE_WORKSPACE/UNSUPPORTED_LINK 拒绝越界或链接路径,不放松校验来恢复。 +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 专项回归和最终手册核对尚待完成,当前连接是否已更新须查看运行身份。 diff --git a/src/Adapters/FlaUiAdapter.ts b/src/Adapters/FlaUiAdapter.ts index 5fa4c83..137d22d 100644 --- a/src/Adapters/FlaUiAdapter.ts +++ b/src/Adapters/FlaUiAdapter.ts @@ -379,6 +379,39 @@ export class FlaUiAdapter implements IAdapter { } } + /** 纯协议解释:版本不匹配时返回明确错误,不改变自有进程的生命周期。 */ + private parseHostResponse(stdoutData: string, request: UiInspectRequest & { requestId: string }): UiInspectResult { + try { + const parsed = JSON.parse(stdoutData.trim()) as UiInspectResult; + if (parsed.protocolVersion && parsed.protocolVersion !== '1.0') { + return { + schemaVersion: '1.0', + protocolVersion: '1.0', + requestId: request.requestId, + success: false, + errorCode: UiErrorCodes.VERSION_MISMATCH, + errorMessage: `Host returned unsupported protocol version: ${parsed.protocolVersion}`, + }; + } + // Old/custom helpers must not silently ignore a scoped query and return a whole window. + if ((request.query || request.readStates) && parsed.success && parsed.inspectionVersion !== 2) { + return { schemaVersion: '1.0', protocolVersion: '1.0', requestId: request.requestId, + success: false, errorCode: UiErrorCodes.VERSION_MISMATCH, + errorMessage: 'Query/state inspection requires a v0.9 helper (inspectionVersion 2).', auditNotice: parsed.auditNotice }; + } + return parsed; + } catch (jsonErr) { + return { + schemaVersion: '1.0', + protocolVersion: '1.0', + requestId: request.requestId, + success: false, + errorCode: UiErrorCodes.HOST_ERROR, + errorMessage: `Failed to parse host JSON output: ${(jsonErr as Error).message}. Output head: ${stdoutData.slice(0, 300)}`, + }; + } + } + private async executeHost( request: UiInspectRequest & { requestId: string }, timeoutMs: number, @@ -558,37 +591,7 @@ export class FlaUiAdapter implements IAdapter { return; } - try { - const parsed = JSON.parse(stdoutData.trim()) as UiInspectResult; - if (parsed.protocolVersion && parsed.protocolVersion !== '1.0') { - resolve({ - schemaVersion: '1.0', - protocolVersion: '1.0', - requestId: request.requestId, - success: false, - errorCode: UiErrorCodes.VERSION_MISMATCH, - errorMessage: `Host returned unsupported protocol version: ${parsed.protocolVersion}`, - }); - return; - } - // Old/custom helpers must not silently ignore a scoped query and return a whole window. - if ((request.query || request.readStates) && parsed.success && parsed.inspectionVersion !== 2) { - resolve({ schemaVersion: '1.0', protocolVersion: '1.0', requestId: request.requestId, - success: false, errorCode: UiErrorCodes.VERSION_MISMATCH, - errorMessage: 'Query/state inspection requires a v0.9 helper (inspectionVersion 2).', auditNotice: parsed.auditNotice }); - return; - } - resolve(parsed); - } catch (jsonErr) { - resolve({ - schemaVersion: '1.0', - protocolVersion: '1.0', - requestId: request.requestId, - success: false, - errorCode: UiErrorCodes.HOST_ERROR, - errorMessage: `Failed to parse host JSON output: ${(jsonErr as Error).message}. Output head: ${stdoutData.slice(0, 300)}`, - }); - } + resolve(this.parseHostResponse(stdoutData, request)); }); // Write request payload to stdin diff --git a/src/Adapters/IAdapter.ts b/src/Adapters/IAdapter.ts index 8fd495f..b264bb1 100644 --- a/src/Adapters/IAdapter.ts +++ b/src/Adapters/IAdapter.ts @@ -1,11 +1,11 @@ /** - * Adapter contract for optional upstreams (Serena, Repomix). + * Adapter contract for optional upstreams (Roslyn, Repomix). * FlaUI provides bounded read-only inspection. Upstream implementations remain separate. * initialize/dispose are owned by ToolRouter + ResourceManager, not by composite tools. */ import { AdapterHealth } from '../Core/AdapterStatus.js'; -export type { AdapterHealth, AdapterLastError, UpstreamConnectionStatus } from '../Core/AdapterStatus.js'; +export type { AdapterHealth, AdapterLastError } from '../Core/AdapterStatus.js'; export interface IAdapter { readonly name: string; diff --git a/src/Adapters/LocalTextAdapter.ts b/src/Adapters/LocalTextAdapter.ts new file mode 100644 index 0000000..ea10e79 --- /dev/null +++ b/src/Adapters/LocalTextAdapter.ts @@ -0,0 +1,103 @@ +import path from 'node:path'; +import type { WinCodeConfig } from '../Core/Config.js'; +import type { AdapterHealth } from '../Core/AdapterStatus.js'; +import type { CacheManager } from '../Core/Cache.js'; +import { checkOperation, type OperationContext } from '../Core/OperationContext.js'; +import { scanLocalFiles } from '../Core/LocalTextScanner.js'; +import { parseTextDeclarations } from '../Core/TextDeclarations.js'; +import { CodeQueryError, LOCAL_TEXT_LIMITATIONS, computeTypeMatchStats, + type CodeSymbol, type SymbolReference, type FindSymbolsResult, type FindReferencesResult } from '../Core/CodeQueries.js'; + +export type { CodeSymbol, SymbolReference, FindSymbolsResult, FindReferencesResult } from '../Core/CodeQueries.js'; + +/** 本体文本能力:不创建子进程或网络连接;扫描完整性与语义完整性分开报告。 */ +export class LocalTextAdapter { + readonly name = 'LocalTextAdapter'; + private observedAt: string | null = null; + + constructor(private readonly config: WinCodeConfig, private readonly cache: CacheManager) {} + + /** 本地能力始终可用;不把未配置 Roslyn 伪装为语义后端就绪。 */ + private health(): AdapterHealth { + return { available: true, source: 'fallback', + details: 'Local text search only. Semantic analysis is not configured; explicitly configure Roslyn for C# semantic queries.' }; + } + + async initialize(): Promise { this.observedAt = new Date().toISOString(); } + async checkHealth(): Promise { await this.initialize(); return this.health(); } + getKnownHealth() { return { health: this.health(), observedAt: this.observedAt }; } + /** 没有外部连接;缓存和扫描请求的生命周期分别由 CacheManager 和调用方负责。 */ + async dispose(): Promise {} + + /** 只解析调用方已经读取的文本;不声称正则结果具有 Roslyn 的快照身份。 */ + findSymbolsInContent(content: string, file: string): CodeSymbol[] { + return parseTextDeclarations(content, file, path.extname(file).toLowerCase()); + } + + async findSymbols(query: string, kind?: string, operation?: OperationContext): Promise { + return (await this.findSymbolsDetailed(query, kind, undefined, operation)).symbols; + } + + /** 精确文件范围在读取正文前应用;受限或失败结果不写入缓存。 */ + 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 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 => + symbol.name.toLowerCase().includes(query.toLowerCase()) && (!kind || symbol.kind.toLowerCase() === kind.toLowerCase())), + relativePath, operation); + const stats = computeTypeMatchStats(scan.items, query); + const result: FindSymbolsResult = { + query, kindFilter: kind, totalFound: scan.items.length, symbols: scan.items, source: 'local-text', + analysisCompleteness: scan.complete ? 'degraded' : 'incomplete', + limitations: [...(scan.error ? [`查询不完整: ${scan.error}`] : []), ...LOCAL_TEXT_LIMITATIONS], + queryComplete: scan.complete, queryError: scan.error, truncated: scan.truncated, + uniqueTypeMatch: scan.complete && !scan.truncated && stats.uniqueTypeMatch, typeMatchCount: stats.typeMatchCount, + }; + checkOperation(operation); + if (scan.complete) await this.cache.set(key, result, { fingerprint, ttlMs: 300000 }); + return result; + } + + async findReferences(symbolName: string, relativePath?: string, operation?: OperationContext): Promise { + return (await this.findReferencesDetailed(symbolName, relativePath, operation)).references; + } + + /** defining file 仅为提示;文本引用仍扫描工作区,不把旧 Serena 重载身份静默降为简单名。 */ + async findReferencesDetailed(symbolName: string, relativePath?: string, operation?: OperationContext): Promise { + checkOperation(operation); + if (symbolName.includes('/') || /\[\d+\]/.test(symbolName)) throw new CodeQueryError('LEGACY_SYMBOL_ID', 'Legacy Serena identities are retired; supply a plain name or configure Roslyn and search again.'); + const fingerprint = await this.cache.computeWorkspaceFingerprint(this.config.workspaceRoot); + const key = `local_text_references_v1_${JSON.stringify([symbolName, relativePath, this.config.workspaceRoot])}`; + const cached = await this.cache.get(key, fingerprint); + checkOperation(operation); + if (cached?.queryComplete) return cached; + const escaped = symbolName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const match = new RegExp(`\\b${escaped}\\b`); + const declaration = new RegExp(`(^|\\s)(class|interface|struct|enum)\\s+${escaped}\\b`); + const scan = await scanLocalFiles(this.config.workspaceRoot, this.config.timeouts.fileScanMs, + ['.cs', '.ts', '.tsx', '.js', '.jsx', '.py', '.xaml', '.xml', '.csproj', '.sln'], 200, + function* (content, file) { + const lines = content.split(/\r?\n/); + for (let index = 0; index < lines.length; index++) { + const preview = lines[index].trim(); + if (/^(?:\/\/|\*|\/\*|#|