diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e82e67..8e3c719 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## 0.15.0 (unreleased) +- Report Roslyn snapshot diagnostic totals before sample limits, bounded error-code/project counts and explicit sample omissions; preserve unknown totals for legacy Hosts and conservative semantic coverage. Update Skill guidance to distinguish dependency loading from excluded generator coverage. + +- Preserve single-target referenced projects' own framework during Roslyn design-time loading, preventing a Windows entry framework from hiding restored library dependencies. Keep explicit entry/multi-target selection, existing import hooks and per-Host output ownership; add a mixed-framework package-reference regression. - Add an optional Skill-managed session using the existing MCP client SDK and a persistent interactive terminal. Start the Gateway on the first tool call, validate workspace/build/provider, reuse the connection and Roslyn snapshot, preserve complete MCP results as local JSON/image attachments, and close owned resources explicitly or on owner exit. Disable the client's native WinCode MCP entry to prevent its automatic startup; no automatic retry/reconnection, new service or dependency is introduced. - Preserve completed/partial trash outcomes and actual file locations when the request deadline expires during metadata finalization. Propagate cancellation before the move and keep interruption accounting; client cancellation or disconnection still does not guarantee delivery of a final response. - Add optional Roslyn reference `limit` (1–1000, default 100, requires `symbolLocation`). Impact reports retain `totalReferences` and `referencesTruncated` alongside the existing returned `referencesCount`; unknown totals remain null and semantic risk/coverage rules stay unchanged. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 886d248..034cb72 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,6 +21,8 @@ Reports and bounded stage logs are under `test-tmp/check//`. CI retains the Windows CI jobs have a 20-minute overall budget for cold setup and native acceptance. Node 22 runs shared-cache checks early, then gives SDK concurrency and design-time output ownership separate steps. Design-time readiness uses the production Roslyn load budget of 120 seconds; it does not impose an additional cold-start performance target. Production request deadlines remain unchanged. +`node scripts/verify-design-time-concurrency.mjs --phase semantics --filter semantics/mixed` checks a multi-target entry selected as Windows against a plain .NET library with a package dependency and an existing early props hook. It reuses a dependency already locked by the Code Host, restores the generated fixture offline, and verifies the framework-specific reference and compilation diagnostics through the published Host. The complete design-time acceptance includes this scenario. + `npm run test:owner-death` kills only a generated Gateway during confirmed initial MSBuild work and checks all previously observed process identities for survivors. `node scripts/verify-owner-death.mjs --repomix` audits the actual adapter with a controlled Node CLI, without installing Repomix. `check:desktop` also runs the `--desktop` owner-death scenario against the isolated WPF fixture: the UIA Helper must exit while the target remains alive; the fixture is closed separately after recording that result. Reports are kept under `test-tmp/owner-death/`. These checks never identify client applications by process name. `node scripts/measure-runtime-baseline.mjs` creates a small C# project, runs three fresh stdio clients, measures unused/cold/warm/exit phases, and profiles source Router startup separately. It records snapshots under `test-tmp/runtime-baseline/`, using the existing SDK. Run it without competing check jobs for a comparison; samples share OS/SDK caches and do not establish a p95, a clean-machine benchmark, or installed-client acceptance. Instrumented startup I/O counts cover the selected asynchronous Node filesystem methods and `child_process.spawn`, not all native/kernel I/O. 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 e412059..24794ed 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" @@ -9,7 +9,7 @@ | 事项 | 已知情况 | 下一步 | | --- | --- | --- | | 已安装 Codex 的冷启动确认 | Skill 按需入口已通过真实终端连续 Roslyn 查询、关闭和所属进程退出;Node 22.23.2 与 24.19.0 本地核心回归均为 462 通过、1 项可选夹具跳过。Node 24 已在 I:/New-tarven 完成后台 UI 和真实图片附件验证,Node 22 补验了该项目的实际终端声明/引用流程。原生 wincode 已设 enabled=false,已安装 Skill 四文件一致 | 正常刷新/重新启动 Codex,确认未调用时没有新建 WinCode 进程。当前任务未重启宿主;保存配置不会清除旧连接。Node 22 证据见 test-tmp/node22-validation/report.json | -| Tavern 的 Roslyn 语义覆盖 | 本地 App 项目 Release/net10.0-windows 快照正确返回导航声明及两处引用,并跨 UI 调用保持身份;同时排除 24 个分析器/生成器,报告 CS8795 和部分 Microsoft.Data/Microsoft.ML 的 CS0234。源码正常构建/隔离启动不能消除这些设计时诊断 | 如需扩大到完整项目语义,先核对设计时项目引用、TFM 和依赖解析,再区分生成器策略造成的缺口;当前维持 incomplete,不根据局部零引用判断删除安全。证据:test-tmp/tavern-skill-JNZ7Wu/report.json | +| Tavern 的生成源码覆盖 | 单目标引用框架被入口覆盖的问题已在本地修复;实际 Host 不再报告 Microsoft.Data/Microsoft.ML 依赖缺失。当前 Codex 终端验证了 Tokenizer、SQLite 参数类型及具体引用,导航两处引用仍正确。完整 Host 诊断现为 11 项 CS8795,均涉及生成正则;24 个分析器/生成器引用仍排除 | 单独评估生成源码的受控支持、定位和快照失效,不直接启用全部生成器。queryComplete=false/incomplete 仍是保守契约,局部零引用不能证明删除安全。最新证据:test-tmp/roslyn-dependency-debug/tavern-report.json;原验收报告作为历史保留 | | UI 和托盘的间歇性失败 | PR #41 将真实端点启动检查移到顺序阶段,main 两个 Node 版本均通过,3000 ms 生产超时未变;隐藏/唤出、截图/提示窗及原生启动延迟的根因仍未全部定位 | 再现时保存错误码、原生日志和步骤耗时,再针对原因修复;不把调度修正及重跑通过称为已解决所有 UI/托盘问题 | | SDK 突发请求警告 | main 的 128 请求阶段仍记录 11 个 drain 监听器警告,阶段结束后为 0;此次实际客户端连续查询未出现调用失败,但未采集该客户端的监听器统计 | 若普通负载也出现问题,再检查发送背压及监听器释放。不提高监听器上限来隐藏警告,也不据此认定内存泄漏 | 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 384e82a..3d19de2 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" @@ -6,6 +6,8 @@ N4 实现:`DesignTimeBuild` 使用已有 SDK 的 ProjectCollection 做原项目求值,保留原中间目录的 Compile 排除规则及自定义导入;目标运行仍交给 MSBuildWorkspace,使用每 Host UUID 的私有 IntermediateOutputPath。Configuration/TargetFramework 必须为字面目录段,规范化后的输出必须位于所属 UUID 内;原求值的无效项目异常保持 `PROJECT_LOAD_FAILED`。输入扫描仅过滤已判定不参与默认编译的原中间产物,实际文档/显式输入仍校验,自定义 Compile 保守处理。`OwnedBuildOutputs` 记录原生所有权清单,正常关闭回收;`RoslynHostClient` 在实际退出后调用 `DesignTimeArtifacts` 回收所属命名空间。内部 inputPolicy 为 2,缺失/旧策略 Host 的拒绝和进程回收已有专项测试。当前发布目录通过交付身份核验;复杂 target、任意动态项目图及断电后的孤儿产物仍不在本地通过范围内。 +项目引用加载:入口及多目标项目继续使用显式指定的框架;单目标引用项目先按自身声明求值,再通过所属 Host 目录中的 props 将框架覆盖限定到该引用项目。覆盖在 Directory.Build.props/SDK 包导入前生效,并保留原 CustomBeforeDirectoryBuildProps 导入,避免入口的全局 Windows 框架污染普通 .NET 类库的依赖解析。此策略不承诺为多目标引用自动挑选最近兼容框架。分析器/生成器仍被排除;queryComplete=false 和 incomplete 是保守契约,不能因编译诊断清零就改为全项目完整。 + ## 1. 整体定位与结构 WinCode 是一个运行在本机的 **MCP 工具网关**:接收编码 Agent 的结构化请求,组织代码或桌面证据,再把正文与证据边界一起返回。Agent 的模型推理在客户端侧;WinCode 自身没有模型推理服务或向量数据库。 diff --git a/docs/codex_worklog.md b/docs/codex_worklog.md index 693e996..9077dbc 100644 --- a/docs/codex_worklog.md +++ b/docs/codex_worklog.md @@ -1430,3 +1430,24 @@ - 随后仅在检查子进程环境中前置便携版 PATH,并以同一 Node 执行 `scripts/check.mjs`;父进程和通过 PATH 启动的子 Node 均读回 v22.23.2。完整核心检查通过:463 项中 462 通过、0 失败、1 项跳过(原有可选 Tavern 固定路径不存在),类型检查、Gateway/原生构建、生产 stdio 和交付核验全部成功。报告 `test-tmp/check/2026-09-12T09-20-46-120Z-core/report.json`。Gateway buildId 与 Node 24 构建一致,仍为 `7491337da2426a3ccc4f2df4a11a5f28982fc86852ae5485305ffeb78bceec7e`;记录 Node 22 工具链的新交付 contentId=`7592124791ab0b901a432405cbcdb4b87f2c7312bfe093c4ebca2a553576e1a0`,matched=true。 - 在当前 Codex 交互终端使用便携版启动真实 Skill 入口,绑定 `I:/New-tarven` 和上一轮显式授权的 Roslyn 配置。4 次显式请求覆盖 hello、声明搜索、精确引用和再次 hello;Gateway 两次自报 v22.23.2,实际入口 PID=26740、Gateway PID=9412 的命令行均指向便携版 Node 22。instanceId=`87e949f7-776c-4380-86ed-3623bcc09f42` 保持一致,snapshotId=`6c1db72b22ea42ec85a3a87959ffdda1` 从声明传到引用;第 127、130 行两处引用按实际源码 UTF-16 位置核验成功。Tavern 已知设计时语义缺口仍返回 incomplete,没有把版本兼容性通过解释为全项目语义完整。 - 会话显式关闭并退出;记录的 5 个所属进程全部消失,survivors=[]。新执行环境读回系统默认 Node 仍为 v24.19.0。汇总报告 `test-tmp/node22-validation/report.json`,原始 MCP 响应位于 `test-tmp/skill-sessions/run-lRbqki/`。本轮未重启 Codex、未复跑桌面或 CI 的全部附加专项,未提交/推送;现有待验表移除 Node 22 未验状态,保留宿主冷启动观察。 + +## 2026-09-12 18:13 — 修复单目标引用项目的设计时依赖解析(北京时间) + +- 用户批准先修复依赖解析并用 Tavern 实测,生成器支持单独处理。基于已合并的 `c229198` 新建 `codex/roslyn-project-reference-loading`;本轮未提交或推送。改动限定在 Code Host 的 DesignTimeBuild、现有设计时验收脚本和相关文档,未修改 Tavern 源码/依赖/数据库、公共 MCP 接口或分析器策略。 +- 最小复现:Windows App 引用 net10.0 类库,类库使用本机已有且 Code Host 锁定的 System.Composition.AttributedModel 10.0.1。离线正常构建成功,旧发布 Host 却报告 CS0234。隔离诊断副本显示类库带有 WINDOWS 预处理符号且缺少该包的 MetadataReference;只移除工作区全局 TargetFramework 后,依赖和编译诊断恢复正常,输出隔离未变。证据依次为 `test-tmp/roslyn-dependency-debug/case-c7jw7W/report.json`、`case-N4wxIN/report.json`、`case-wDsTsn/report.json`。生产代码没有加入诊断开关。 +- 根因是工作区全局框架覆盖了单目标引用项目。对照安装的 Roslyn 5.9.0 对应源码确认:MSBuildWorkspace 的 properties 等同 MSBuild 全局参数,并传到每个项目。参考:https://github.com/dotnet/roslyn/blob/35d9211b841e7613c1d2f8f5af6d628ace696c4c/src/Workspaces/MSBuild/Core/MSBuild/MSBuildWorkspace.cs 。MSBuild 导入中的 TreatAsLocalProperty 作用域参考:https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-properties#local-properties-and-imports 。最初直接移除全局框架仅作为诊断,正式修复仍保留入口及多目标项目的显式选择。 +- 正式实现先按单目标引用自身声明求值,在所属 Host UUID 目录生成限定到该项目的早期 props 覆盖;保留原 CustomBeforeDirectoryBuildProps 导入、原中间目录排除规则及所有权回收。没有为多目标引用添加自动选择最近兼容框架的规则,也没有扫描 DLL 补引用或自动还原用户项目。 +- 新增恰好一个回归场景 `semantics/mixed/production`:多目标入口选择 Windows、普通类库保留 net10.0、锁定包引用可见、原早期导入保留、跨项目引用正确。旧实现失败报告 `test-tmp/design-time-production/run-PBaQ0n/report.json`;首次修复通过 `run-VdaJNe/report.json`。补充原早期导入断言后的语义阶段 6/6 通过(`run-kQFKMX`),并发/取消/崩溃阶段 5/5(`run-y3fFYK`)、外部构建/源码失效阶段 6/6(`run-V37sRQ`)、输入变化阶段 5/5(`run-VQ4JN1`),合计 22 个实际生产 Host 场景,均无所属进程残留。没有失败的生产修复循环,也没有跳过失败断言。 +- 当前 Codex 的真实 Skill 交互终端在 `I:/New-tarven` 执行 7 次工具调用。Tokenizer 参数签名、SQLite connection/transaction 参数签名均正确解析;EstimateTiktokenMessages 第 25 行、RepairCharactersAsync 第 151 行,以及 ShowCharactersAsync 第 127/130 行引用逐项与磁盘 UTF-16 位置核对一致。同一 Gateway instanceId=`c64b1f1f-3889-45aa-b208-7460c92cf4b6`、snapshotId=`c30b726a198b46ada192eb212ce72d46` 被连续复用,关闭后记录的 5 个进程均退出。实际生产 Host 完整诊断不再含缺失依赖的 CS0234,剩余 11 项均为生成正则实现缺失的 CS8795;原 MCP 简略响应仅展示前五条,不能拿以前显示的两处正则错误当作总数。汇总 `test-tmp/roslyn-dependency-debug/tavern-report.json`;原始响应 `test-tmp/skill-sessions/run-aXAiN9/`。Tavern 前后 Git 状态仅有原有 `.publish-verify/`,本轮未启动目标应用或接触个人资料库。 +- Node 22.23.2 执行一次完整核心检查,通过 463 项中的 462 项,0 失败,1 项原有可选 Tavern 固定夹具跳过;类型检查、构建、生产 stdio 和交付核验通过。报告 `test-tmp/check/2026-09-12T10-10-53-659Z-core/report.json`。Gateway buildId 仍为 `7491337da2426a3ccc4f2df4a11a5f28982fc86852ae5485305ffeb78bceec7e`(本轮仅修改原生加载实现),交付 contentId=`6902e19593c6b55b4712cb2a5e31d8dcecc6adab27a185b87a7d7aacf23a5987`。上述 22 个专项及 Tavern 真实终端使用 Node 24.19.0;未重复 Node 24 全套核心检查或运行本轮云端 CI。 +- 边界:24 个分析器/生成器引用仍排除,queryComplete=false 和 analysisCompleteness=incomplete 的保守契约未变;本次解决依赖加载缺口,不声称全项目语义完整。作者自查不等于独立审查,当前 Codex 宿主冷启动观察也未在本轮补验。诊断副本及原始附件集中保留在忽略的 `test-tmp/roslyn-dependency-debug/`,不作为交付文件。 + +## 2026-09-12 18:45 — 诊断计数与 Skill 防误判规则(北京时间) + +- 用户授权与上述依赖修复一起提交 PR、通过必需检查后合并,并同步本地 wincode Skill。Host 在每项目二十条样例裁剪前统计当前快照编译错误,单列加载诊断;直接符号/引用响应 semanticContext.diagnosticSummary 返回总数、最多各十组的错误码/项目计数及分组省略数,Gateway 给出五条展示样例与省略条数。旧 Host 未提供统计时总数为 null、countsComplete=false;不补写零。limitations 保留简短计数,使组合工具也能带出覆盖边界。 +- countsComplete 仅指已加载快照的编译错误和加载诊断统计,不覆盖编译警告、被排除分析器/生成器或未加载项目。queryComplete=false、analysisCompleteness=incomplete 保持不变。必要元数据超出引用预算仍按原契约拒绝;未增加工具、依赖、服务、自动 restore 或生成器执行。 +- 扩展既有 120 引用集成用例:固定 25 条 CS1029 加一条编译警告,验证计数先于 Host 的二十条和 Gateway 的五条上限,警告不计入错误。旧发布 Host 失败见 test-tmp/diagnostic-red.log;首次发布新 Host 后通过见 diagnostic-green.log,120 个引用仍正确且最终输出遵守预算。另加一个旧 Host 未知/不一致统计拒绝测试;连同上一轮 mixed 夹具,本任务新增两个自动化场景,未超过三项。 +- Node 22.23.2 本轮完整核心检查:464 项、463 通过、0 失败、0 取消、1 项原有可选 Tavern 固定夹具跳过;类型检查、Gateway/原生构建、生产 stdio、交付核验通过。报告 test-tmp/check/2026-09-12T10-39-52-054Z-core/report.json。buildId=2072ef38b631017d6f82fc404a3fa1a75a4619cf53422ea87f6098b3d6f58b8d,schemaHash/17 工具未变;Node 24 的聚焦真实 Host 回归通过。未重复运行无关桌面套件。 +- 当前 Codex 的 Node 24 真实 Skill 终端对 Tavern 搜索 ShowCharactersAsync 并查询引用,返回诊断总数 11、CS8795=11、App=2、Infrastructure=9、Core=0、加载诊断=0、展示=5、省略=6、排除分析器引用=24。第 127/130 行两处引用按磁盘 UTF-16 位置核验;两个响应复用 snapshotId=d394f23675ab42e382d304601f647b72。原始结果 test-tmp/skill-sessions/run-nolmHH/,入口/Gateway instanceId=0cb7610a-f126-4f76-943f-858fb598f7e2。显式关闭后五个记录所属进程全部退出;Tavern Git 仍仅有原有 .publish-verify/,没有业务源码或资料库改动。 +- Skill 保持短入口,只更新 references/code.md 与 references/diagnostics.md:先看完整统计,再区分框架/依赖加载与生成器覆盖;不凭错误码自动安装包或反复重试。skill-creator quick_validate 以 Python UTF-8 模式通过。现有 sync-skill.mjs --apply 已备份后同步 C:/Users/40218/.agents/skills/wincode,备份目录 .wincode-backup-17cc7064-c86b-4973-b869-b0f34d2f1e9d;再次只读检查四份受管文件 matched=true。合并后复核。 +- 尚待本次 PR 的精确提交 CI 和合并;作者自查不是独立审查。生成器覆盖与宿主冷启动观察仍是已有明确边界,不将本轮通过描述为已解决它们。 diff --git a/scripts/roslyn/design-time-prototypes.mjs b/scripts/roslyn/design-time-prototypes.mjs index f94a5e6..7680840 100644 --- a/scripts/roslyn/design-time-prototypes.mjs +++ b/scripts/roslyn/design-time-prototypes.mjs @@ -65,7 +65,18 @@ export async function fixture(root, sdk, name, type = name) { const api = 'namespace Probe; public static class Api { public static void Save(int x) {} }'; let project = 'App.csproj', framework = 'net10.0', projects = 1, references = 1; const use = 'namespace Probe; public class Use { public void Run() { Api.Save(1); } }'; - if (type === 'graph') { + if (type === 'mixed') { + project = 'App/App.csproj'; framework = 'net10.0-windows'; projects = 2; + // Reuse a locked Host dependency from the local package cache; no new package or network restore. + const lock = JSON.parse(await fs.readFile(path.resolve(import.meta.dirname, '../../tools/WinCode.Code.Host/packages.lock.json'), 'utf8')); + const version = lock.dependencies['net10.0']['System.Composition.AttributedModel'].resolved; + await write('Lib/early.props', '$(DefineConstants);PRESERVED_EARLY_HOOK'); + await write('Lib/Lib.csproj', `$(MSBuildThisFileDirectory)early.props${props}`); + await write('Lib/Api.cs', api.replace('public static void Save(int x) {}', 'public static void Save(int x) { _ = new System.Composition.ExportAttribute(); }')); + await write('Lib/Framework.cs', '#if WINDOWS\n#error The net10.0 library must not inherit the entry Windows framework.\n#endif\n#if !PRESERVED_EARLY_HOOK\n#error Preserve the original early props hook.\n#endif\n'); + await write(project, `${props.replace('net10.0', 'net10.0;net10.0-windows')}`); + await write('App/Use.cs', 'namespace Probe; public class Use { public void Run() {\n#if WINDOWS\nApi.Save(1);\n#endif\n} }'); + } else if (type === 'graph') { project = 'App/App.csproj'; projects = 2; await write('Lib/Lib.csproj', `${props}`); await write('Lib/Api.cs', api); @@ -91,7 +102,8 @@ export async function fixture(root, sdk, name, type = name) { if (type === 'graph') runDotnet(sdk, ['restore', path.join(directory, 'Peer/Peer.csproj'), '--configfile', path.join(root, 'NuGet.Config'), '--nologo'], root, 30000); await fs.writeFile(path.join(root, `${name}-restore.log`), restore); return { root: directory, project, framework, projects, references, type, - projectDirectories: type === 'graph' ? ['App', 'Lib', 'Peer'].map(p => path.join(directory, p)) : [directory] }; + projectDirectories: type === 'graph' ? ['App', 'Lib', 'Peer'].map(p => path.join(directory, p)) + : type === 'mixed' ? ['App', 'Lib'].map(p => path.join(directory, p)) : [directory] }; } /** Real MSBuild Exec descendant, enabled only in the selected prototype child environment. */ diff --git a/scripts/verify-design-time-concurrency.mjs b/scripts/verify-design-time-concurrency.mjs index 115dba1..1665bc9 100644 --- a/scripts/verify-design-time-concurrency.mjs +++ b/scripts/verify-design-time-concurrency.mjs @@ -404,7 +404,7 @@ try { report.build = { ...delivery, buildId: gateway.buildId, revision: deliveryManifest.revision, host, assemblyHash: hash(await fs.readFile(host)) }; await fs.copyFile(path.join(repo, 'global.json'), path.join(root, 'global.json')); await fs.writeFile(path.join(root, 'NuGet.Config'), ''); - if (phase === 'all' || phase === 'semantics') for (const type of ['basic', 'graph', 'wpf', 'custom']) { + if (phase === 'all' || phase === 'semantics') for (const type of ['basic', 'graph', 'wpf', 'custom', 'mixed']) { for (const mode of modes) { await trial(`semantics/${type}/${mode}`, async item => { const project = await fixture(root, sdk, type); diff --git a/skills/wincode/references/code.md b/skills/wincode/references/code.md index a4cd60b..255b63d 100644 --- a/skills/wincode/references/code.md +++ b/skills/wincode/references/code.md @@ -184,6 +184,8 @@ bodyStatusScope 明确该字段描述 displayed-snippet 或 packed-file。symbol 检查 source、queryComplete、uniqueResolution/uniqueTypeMatch 与 limitations。source=roslyn 是编译器语义来源,不是文本回退;queryComplete=false 可以表示生成器或加载图等覆盖缺口,不能直接解释为执行中断或要求原样重试。文本回退不保证语义引用完整;零引用、UNKNOWN 或未找到均不证明可安全删除。 +Roslyn 查询先读 `semanticContext.diagnosticSummary`,再读 limitations 中的诊断样例:`compilationErrorCount` 是当前加载快照的编译错误总数,`loadDiagnosticCount` 单列加载诊断;`samplesDisplayed/samplesOmitted` 区分展示与省略条数。`byCode/byProject` 各最多十组,`codesOmitted/projectsOmitted` 是未展示的组数;总数在裁剪前统计。`countsComplete=true` 只说明该快照的这两类诊断计数完整,不包括被排除的分析器、编译警告或未加载项目,也不提高 queryComplete。旧 Host 无统计时总数为 null、countsComplete=false,不从五条样例推断总数。诊断排查顺序见 [diagnostics.md](diagnostics.md)。 + 影响分析用完整工作区文件路径及可用的项目身份区分组件,targetFile 和组件 name 仍是展示名称;不同目录同名组件可以分别出现,不要按 name 再合并。提供目录的 target 按工作区解析;只有纯文件名才用于候选匹配。Host 冻结源码沿用 Roslyn/MSBuild 的 CodePage 与 BOM 编码,返回位置仍按解码后的 UTF-16 文本计算,不按原始文件字节偏移定位。 若已有影响报告,直接据此规划,不为获得通用清单再次调用 plan_refactoring。该工具仍会做影响分析;它返回的 evidence 保留歧义、降级和 UNKNOWN,不代表已经执行重构。 diff --git a/skills/wincode/references/diagnostics.md b/skills/wincode/references/diagnostics.md index 4eede55..e40dc85 100644 --- a/skills/wincode/references/diagnostics.md +++ b/skills/wincode/references/diagnostics.md @@ -1,5 +1,13 @@ # 诊断与审计 +## 设计时语义诊断 + +连接成功、精确符号定位和全项目语义覆盖分别判断。读取 `semanticContext.diagnosticSummary` 的总数、错误码和项目分组,再结合 `excludedAnalyzers`、limitations 样例与实际源码判断影响;展示五条不等于只有五条错误。分组省略数大于零时,未展示的项目或错误码不能当作不存在。引用响应预算不足可显式提高 maxOutputChars;不能容纳必要元数据时会返回 OUTPUT_BUDGET_EXCEEDED,不静默删除统计。 + +出现 CS0234 等缺失命名空间错误,先核对入口及引用项目各自的 TargetFramework、已有 restore assets 与实际 MetadataReference,再判断是否真缺少包。正常构建成功也不证明设计时加载一致;不要仅凭错误码安装依赖、自动 restore、修改项目框架或扫描 DLL 补引用。单目标依赖应保留自身框架,多目标引用仍遵循现有显式配置,本实现不承诺自动选择最近兼容目标。 + +出现 CS8795 等 partial 实现缺失,结合声明、项目生成器配置及排除数量核实原因;错误码本身不是生成器故障的充分证据。WinCode 仍排除分析器/生成器,不自行开启或执行它们。queryComplete=false / incomplete 是保守覆盖契约,即使诊断为零也保留;有效局部引用可继续使用,但不能据此主张全局无引用或安全删除。计数未知时先核对已连接 Host 与本地交付版本,不原样反复调用或擅自重启服务。 + ## Skill 按需会话 适用于能保留交互终端会话的 Codex。首次确实需要 WinCode 才启动入口;不要因加载 Skill、开始聊天或查看配置而预热。目标工作区取当前任务的绝对目录,安装目录取已核实的本机交付位置(已有 MCP 配置中的 `dist/index.js` 路径也可用于定位);不照抄其他机器或历史测试项目的路径。 diff --git a/src/Adapters/RoslynAdapter.ts b/src/Adapters/RoslynAdapter.ts index 0b52555..26091d7 100644 --- a/src/Adapters/RoslynAdapter.ts +++ b/src/Adapters/RoslynAdapter.ts @@ -176,12 +176,40 @@ export class RoslynAdapter implements CodeReferenceQuery, ContextCodeQuery { private limitations(reply: HostReply): string[] { if (reply.queryComplete !== false || !Array.isArray(reply.compilationErrors) || !Array.isArray(reply.loadDiagnostics) || !Number.isSafeInteger(reply.excludedAnalyzers)) throw new CodeQueryError('HOST_PROTOCOL_ERROR', 'Missing Host completeness evidence.'); + const summary = this.diagnostics(reply); return ['范围仅为当前入口加载的 C# 项目及单配置快照;不覆盖动态调用或仓外调用。', `排除 ${reply.excludedAnalyzers} 个分析器/生成器引用,生成源码覆盖未证明。`, '输入校验覆盖声明的文件集合,不保证任意外部 targets 输入或全磁盘原子一致。', + summary.countsComplete + ? `当前快照编译错误 ${summary.compilationErrorCount} 条,加载诊断 ${summary.loadDiagnosticCount} 条;展示 ${summary.samplesDisplayed} 条样例,省略 ${summary.samplesOmitted} 条。` + : `Host 未提供完整诊断统计;展示 ${summary.samplesDisplayed} 条样例,总数未知。`, ...[...reply.loadDiagnostics, ...reply.compilationErrors].slice(0, 5).map(value => String(value).slice(0, 1024))]; } + private diagnostics(reply: HostReply): NonNullable { + const samplesDisplayed = Math.min(5, (reply.loadDiagnostics as unknown[]).length + (reply.compilationErrors as unknown[]).length); + const raw = reply.diagnosticSummary as any; + if (raw === undefined) return { countsComplete: false, compilationErrorCount: null, loadDiagnosticCount: null, + samplesDisplayed, samplesOmitted: null, byCode: [], codesOmitted: null, byProject: [], projectsOmitted: null }; + const count = (value: unknown): value is number => Number.isSafeInteger(value) && (value as number) >= 0; + const groups = (values: any, total: number, omitted: number) => Array.isArray(values) && values.length <= 10 && + values.every((v: any) => v && typeof v.key === 'string' && v.key.length > 0 && v.key.length <= 4096 && count(v.count)) && + new Set(values.map((v: any) => v.key)).size === values.length && + (omitted === 0 ? values.reduce((n: number, v: any) => n + v.count, 0) === total : values.reduce((n: number, v: any) => n + v.count, 0) <= total); + if (!raw || !count(raw.compilationErrorCount) || !count(raw.loadDiagnosticCount) || + !count(raw.codesOmitted) || !count(raw.projectsOmitted) || + !groups(raw.byCode, raw.compilationErrorCount, raw.codesOmitted) || + !groups(raw.byProject, raw.compilationErrorCount, raw.projectsOmitted) || + raw.loadDiagnosticCount !== (reply.loadDiagnostics as unknown[]).length || + raw.compilationErrorCount < (reply.compilationErrors as unknown[]).length || + !Number.isSafeInteger(raw.compilationErrorCount + raw.loadDiagnosticCount)) + throw new CodeQueryError('HOST_PROTOCOL_ERROR', 'Invalid diagnostic summary.'); + for (const item of raw.byProject) this.localPath(item.key); + return { countsComplete: true, compilationErrorCount: raw.compilationErrorCount, loadDiagnosticCount: raw.loadDiagnosticCount, + samplesDisplayed, samplesOmitted: raw.compilationErrorCount + raw.loadDiagnosticCount - samplesDisplayed, + byCode: raw.byCode, codesOmitted: raw.codesOmitted, byProject: raw.byProject, projectsOmitted: raw.projectsOmitted }; + } + /** 传递经校验的实际检查点,不把缺失的校验结果补写成 verified。 */ private evidence(reply: HostReply): SemanticContext { const freshness = reply.freshness as SemanticContext['freshness']; @@ -192,7 +220,7 @@ export class RoslynAdapter implements CodeReferenceQuery, ContextCodeQuery { !Number.isSafeInteger(freshness.bytes) || freshness.bytes < 0 || freshness.bytes > 128 * 1024 * 1024) throw new CodeQueryError('HOST_PROTOCOL_ERROR', 'Invalid semantic checkpoint evidence.'); return { snapshotId: this.snapshot!, scope: 'loaded-solution-snapshot', diskFreshnessVerified: false, - excludedAnalyzers: reply.excludedAnalyzers as number, freshness }; + excludedAnalyzers: reply.excludedAnalyzers as number, diagnosticSummary: this.diagnostics(reply), freshness }; } /** 校验定位的坐标和范围;过期身份在启动或查询 Host 前拒绝。 */ diff --git a/src/Core/CodeQueries.ts b/src/Core/CodeQueries.ts index db487c1..296034f 100644 --- a/src/Core/CodeQueries.ts +++ b/src/Core/CodeQueries.ts @@ -17,6 +17,17 @@ export interface SemanticContext { scope: 'loaded-solution-snapshot'; diskFreshnessVerified: false; excludedAnalyzers: number; + diagnosticSummary?: { + countsComplete: boolean; + compilationErrorCount: number | null; + loadDiagnosticCount: number | null; + samplesDisplayed: number; + samplesOmitted: number | null; + byCode: { key: string; count: number }[]; + codesOmitted: number | null; + byProject: { key: string; count: number }[]; + projectsOmitted: number | null; + }; freshness: { status: 'checked'; scope: string; fingerprint: string; files: number; bytes: number; externalCustomInputsVerified: false }; } diff --git a/tests/roslyn-contracts.test.ts b/tests/roslyn-contracts.test.ts index d51bc9a..681982c 100644 --- a/tests/roslyn-contracts.test.ts +++ b/tests/roslyn-contracts.test.ts @@ -38,7 +38,8 @@ it('MCP preserves 120 known Roslyn references and accepts bounded limits without try { await fs.writeFile(path.join(root, 'Probe.csproj'), 'net10.0false'); const source = 'public static class Api { public static void Call() {} }\npublic class Consumer { public void Run() {\n' + - Array.from({ length: 120 }, () => 'Api.Call();').join('\n') + '\n} }\n'; + Array.from({ length: 120 }, () => 'Api.Call();').join('\n') + '\n} }\n' + + Array.from({ length: 25 }, (_, i) => `#error diagnostic_${i}`).join('\n') + '\n#warning load_is_not_compilation_warning\n'; await fs.writeFile(path.join(root, 'Probe.cs'), source); await fs.copyFile(path.join(repo, 'global.json'), path.join(root, 'global.json')); const packages = path.join(root, 'empty-package-source'); await fs.mkdir(packages); @@ -49,9 +50,16 @@ it('MCP preserves 120 known Roslyn references and accepts bounded limits without const found = await call('wincode_find_code_symbol', { query: 'Call', kind: 'method' }); const selected = found.symbols.find((symbol: any) => symbol.name === 'Call'); assert.ok(selected?.location); + assert.deepEqual(found.semanticContext.diagnosticSummary, { + countsComplete: true, compilationErrorCount: 25, loadDiagnosticCount: 0, + samplesDisplayed: 5, samplesOmitted: 20, + byCode: [{ key: 'CS1029', count: 25 }], codesOmitted: 0, + byProject: [{ key: 'Probe.csproj', count: 25 }], projectsOmitted: 0, + }); const args = { symbolName: 'Call', symbolLocation: selected.location }; const defaults = await call('wincode_find_references', args); assert.equal(defaults.totalReferences, 120); assert.ok(defaults.references.length > 0 && defaults.references.length < 100); + assert.deepEqual(defaults.semanticContext.diagnosticSummary, found.semanticContext.diagnosticSummary); assert.equal(defaults.truncated, true); assert.deepEqual(defaults.outputOmissions, ['references']); assert.equal(defaults.returnedReferences, defaults.references.length); @@ -83,6 +91,22 @@ it('MCP preserves 120 known Roslyn references and accepts bounded limits without } }); +it('diagnostic evidence keeps legacy totals unknown and rejects inconsistent Host counts', () => { + const adapter = Object.create(RoslynAdapter.prototype) as any; + adapter.localPath = (value: string) => value; + const reply = { queryComplete: false, excludedAnalyzers: 0, loadDiagnostics: [], compilationErrors: ['CS1029: example'] }; + const legacy = adapter.diagnostics(reply); + assert.equal(legacy.countsComplete, false); assert.equal(legacy.compilationErrorCount, null); + assert.equal(legacy.samplesOmitted, null); assert.equal(legacy.samplesDisplayed, 1); + const valid = { compilationErrorCount: 11, loadDiagnosticCount: 0, + byCode: [{ key: 'CS1029', count: 1 }], codesOmitted: 10, + byProject: [{ key: 'App.csproj', count: 11 }], projectsOmitted: 0 }; + assert.equal(adapter.diagnostics({ ...reply, diagnosticSummary: valid }).samplesOmitted, 10); + for (const invalid of [{ ...valid, codesOmitted: 0 }, { ...valid, compilationErrorCount: -1 }, + { ...valid, byProject: [{ key: 'App.csproj', count: 12 }] }, { ...valid, loadDiagnosticCount: 1 }]) + assert.throws(() => adapter.diagnostics({ ...reply, diagnosticSummary: invalid }), { errorCode: 'HOST_PROTOCOL_ERROR' }); +}); + it('same-root confirmations preserve warm identity, reload state and perform a required restart only once', async t => { const root = await fs.mkdtemp(path.join(os.tmpdir(), 'wincode-warm-中文 空格-')); const config = getDefaultConfig(root); diff --git a/tools/WinCode.Code.Host/DesignTimeBuild.cs b/tools/WinCode.Code.Host/DesignTimeBuild.cs index fc42079..8d0b602 100644 --- a/tools/WinCode.Code.Host/DesignTimeBuild.cs +++ b/tools/WinCode.Code.Host/DesignTimeBuild.cs @@ -11,12 +11,14 @@ internal sealed class DesignTimeBuild private readonly List<(string Path, bool CustomCompile, IMSBuildGlob[] Globs, HashSet Explicit)> outputs = []; internal readonly List PrivateDirectories = []; internal string Hook { get; private set; } = ""; + private string frameworkHook = ""; private string intermediateOutputPath = ""; internal static DesignTimeBuild? Current; internal static Dictionary Properties(string configuration, string framework) => new() { ["Configuration"] = configuration, ["TargetFramework"] = framework, ["RunAnalyzers"] = "false", ["RunAnalyzersDuringBuild"] = "false", ["IntermediateOutputPath"] = Current!.intermediateOutputPath, + ["CustomBeforeDirectoryBuildProps"] = Current!.frameworkHook, ["CustomBeforeMicrosoftCommonTargets"] = Current!.Hook }; internal static bool IsCandidate(string file) @@ -41,10 +43,15 @@ internal static DesignTimeBuild Prepare(string root, string projectPath, string if (!Guid.TryParseExact(identity, "N", out _)) throw new ArgumentException("Invalid build output identity."); var result = new DesignTimeBuild { intermediateOutputPath = $".cache/wincode-msbuild/{identity}/{configuration}/{framework}/" }; var xml = new XElement("Project"); + var earlyImports = new XElement("Project"); + var frameworkOverrides = new XElement("Project", new XAttribute("TreatAsLocalProperty", "TargetFramework")); + var storage = WorkspaceInputs.Inside(root, Path.Combine(root, ".cache/wincode-build", identity)); + var overridesFile = Path.Combine(storage, "reference-frameworks.props"); using var collection = new ProjectCollection(new Dictionary { - ["Configuration"] = configuration, ["TargetFramework"] = framework, + ["Configuration"] = configuration, ["DesignTimeBuild"] = "true", ["BuildingInsideVisualStudio"] = "true" }); + var selectedProperties = new Dictionary(collection.GlobalProperties) { ["TargetFramework"] = framework }; var pending = new Stack(); pending.Push(projectPath); var seen = new HashSet(StringComparer.OrdinalIgnoreCase); while (pending.TryPop(out var path)) @@ -53,7 +60,15 @@ internal static DesignTimeBuild Prepare(string root, string projectPath, string if (!seen.Add(path)) continue; if (seen.Count > 64) throw new HostFailure("INPUT_BUDGET_EXCEEDED", "More than 64 project layouts."); Project project; - try { project = collection.LoadProject(path); } + var entry = string.Equals(path, projectPath, StringComparison.OrdinalIgnoreCase); + try { + project = collection.LoadProject(path, entry ? selectedProperties : collection.GlobalProperties, null); + // A single-target reference owns its framework. Retain the existing explicit selection for multi-target projects. + if (!entry && string.IsNullOrWhiteSpace(project.GetPropertyValue("TargetFramework"))) { + collection.UnloadProject(project); + project = collection.LoadProject(path, selectedProperties, null); + } + } catch (InvalidProjectFileException error) { throw new HostFailure("PROJECT_LOAD_FAILED", error.Message); } var directory = Path.GetDirectoryName(path)!; var privateDirectory = WorkspaceInputs.Inside(root, Path.Combine(directory, ".cache/wincode-msbuild", identity)); @@ -75,6 +90,18 @@ internal static DesignTimeBuild Prepare(string root, string projectPath, string if (!string.Equals(baseIntermediate, directory, StringComparison.OrdinalIgnoreCase)) result.outputs.Add((baseIntermediate.TrimEnd('\\', '/') + Path.DirectorySeparatorChar, customCompile, globs, explicitFiles)); var condition = $"'$(MSBuildProjectFullPath)' == '{ProjectCollection.Escape(path)}'"; + var projectFramework = project.GetPropertyValue("TargetFramework"); + if (!entry && !string.Equals(projectFramework, framework, StringComparison.OrdinalIgnoreCase)) { + // Roslyn forwards workspace globals to every project. Scope this override before SDK/package props, + // without relaxing the entry's explicit framework or changing the target project's files. + earlyImports.Add(new XElement("Import", new XAttribute("Project", overridesFile), new XAttribute("Condition", condition))); + frameworkOverrides.Add(new XElement("PropertyGroup", new XAttribute("Condition", condition), + new XElement("TargetFramework", ProjectCollection.Escape(projectFramework)))); + } + var originalEarlyHook = project.Imports.FirstOrDefault(import => + import.ImportingElement.Project == "$(CustomBeforeDirectoryBuildProps)").ImportedProject?.FullPath; + if (originalEarlyHook != null) + earlyImports.Add(new XElement("Import", new XAttribute("Project", originalEarlyHook), new XAttribute("Condition", condition))); var originalHook = project.Imports.FirstOrDefault(import => import.ImportingElement.Project == "$(CustomBeforeMicrosoftCommonTargets)").ImportedProject?.FullPath; if (originalHook != null) @@ -85,9 +112,11 @@ internal static DesignTimeBuild Prepare(string root, string projectPath, string result.PrivateDirectories.Add(privateDirectory); foreach (var reference in project.GetItems("ProjectReference")) pending.Push(Path.GetFullPath(reference.EvaluatedInclude, directory)); } - var storage = WorkspaceInputs.Inside(root, Path.Combine(root, ".cache/wincode-build", identity)); OwnedBuildOutputs.Record(root, identity, result.PrivateDirectories); Directory.CreateDirectory(storage); + result.frameworkHook = Path.Combine(storage, "framework.props"); + File.WriteAllText(result.frameworkHook, earlyImports.ToString(), new UTF8Encoding(false)); + File.WriteAllText(overridesFile, frameworkOverrides.ToString(), new UTF8Encoding(false)); result.Hook = Path.Combine(storage, "preserve.targets"); File.WriteAllText(result.Hook, xml.ToString(), new UTF8Encoding(false)); Current = result; diff --git a/tools/WinCode.Code.Host/WorkspaceSession.cs b/tools/WinCode.Code.Host/WorkspaceSession.cs index 427f83d..9c84f3c 100644 --- a/tools/WinCode.Code.Host/WorkspaceSession.cs +++ b/tools/WinCode.Code.Host/WorkspaceSession.cs @@ -31,6 +31,7 @@ internal sealed class WorkspaceSession : IDisposable private long configurationGeneration; private int excludedAnalyzers; private string[] loadDiagnostics = [], compilationErrors = []; + private object? diagnosticSummary; /// 绑定固定根及配置并启动监听;不在构造时执行 MSBuild,求值由 ReloadAsync 显式启动。 public WorkspaceSession(string root, string projectPath, string configuration, string framework, string[]? additionalInputs = null) @@ -169,10 +170,15 @@ public async Task ReloadAsync(string? id, CancellationToken token) candidate = candidate.WithDocumentText(document.Id, SourceText.From(content, loadedText.Encoding, throwIfBinaryDetected: true)); } var errors = new List(); + var byCode = new Dictionary(); + var byProject = new Dictionary(); foreach (var project in candidate.Projects) { var compilation = await project.GetCompilationAsync(token); - errors.AddRange(compilation!.GetDiagnostics(token).Where(d => d.Severity == DiagnosticSeverity.Error).Take(20).Select(d => d.ToString())); + var diagnostics = compilation!.GetDiagnostics(token).Where(d => d.Severity == DiagnosticSeverity.Error).ToArray(); + byProject[Path.GetRelativePath(root, project.FilePath!)] = diagnostics.Length; + foreach (var diagnostic in diagnostics) byCode[diagnostic.Id] = byCode.GetValueOrDefault(diagnostic.Id) + 1; + errors.AddRange(diagnostics.Take(20).Select(d => d.ToString())); } var after = await CaptureAsync(token, checkEvents: false); if (captured.Fingerprint != after.Fingerprint) @@ -186,12 +192,20 @@ public async Task ReloadAsync(string? id, CancellationToken token) snapshot = Guid.NewGuid().ToString("N"); loadDiagnostics = completedDiagnostics; compilationErrors = errors.ToArray(); + // Counts precede sample limits; bounded breakdowns retain explicit omitted totals. + diagnosticSummary = new { + compilationErrorCount = byCode.Values.Sum(), loadDiagnosticCount = loadDiagnostics.Length, + byCode = byCode.OrderBy(p => p.Key, StringComparer.Ordinal).Take(10).Select(p => new { key = p.Key, count = p.Value }).ToArray(), + codesOmitted = Math.Max(0, byCode.Count - 10), + byProject = byProject.OrderBy(p => p.Key, StringComparer.Ordinal).Take(10).Select(p => new { key = p.Key, count = p.Value }).ToArray(), + projectsOmitted = Math.Max(0, byProject.Count - 10) + }; invalidated = false; return new { id, type = "ready", success = true, protocolVersion = 2, snapshot, hostIdentity = HostBuildIdentity.Current, projects = candidate.ProjectIds.Count, configuration, framework, loadMs = clock.ElapsedMilliseconds, inputPolicy = new { version = 2, additionalInputs = additionalInputs.Select(file => Path.GetRelativePath(root, file)).ToArray() }, - loadDiagnostics, compilationErrors, excludedAnalyzers, scope = "loaded-solution-snapshot", + loadDiagnostics, compilationErrors, diagnosticSummary, excludedAnalyzers, scope = "loaded-solution-snapshot", processTreeGuard = OperatingSystem.IsWindows(), diskFreshnessVerified = false, freshness = Freshness(after) }; } catch { ReleaseWorkspace(); throw; } @@ -281,7 +295,7 @@ public async Task SymbolsAsync(JsonElement request, CancellationToken to } await EnsureCurrentAsync(requestedSnapshot, token); return new { id = request.GetProperty("id").GetString(), success = true, snapshot, symbols, totalFound, - truncated = totalFound > symbols.Count, queryComplete = false, loadDiagnostics, compilationErrors, excludedAnalyzers, + truncated = totalFound > symbols.Count, queryComplete = false, loadDiagnostics, compilationErrors, diagnosticSummary, excludedAnalyzers, scope = "loaded-solution-snapshot", diskFreshnessVerified = false, freshness = Freshness(inputs!) }; } @@ -332,7 +346,7 @@ public async Task ReferencesAsync(JsonElement request, CancellationToken await EnsureCurrentAsync(requestedSnapshot, token); return new { id = request.GetProperty("id").GetString(), success = true, snapshot, symbol = symbol.ToDisplayString(), references, totalReferences = locations.Length, truncated = locations.Length > limit, - queryComplete = false, loadDiagnostics, compilationErrors, excludedAnalyzers, + queryComplete = false, loadDiagnostics, compilationErrors, diagnosticSummary, excludedAnalyzers, scope = "loaded-solution-snapshot", diskFreshnessVerified = false, freshness = Freshness(inputs!), queryMs = clock.ElapsedMilliseconds, workingSetBytes = Environment.WorkingSet }; }