diff --git a/.agents/docs/2026-10-22-issues-59-63-evidence/.gitattributes b/.agents/docs/2026-10-22-issues-59-63-evidence/.gitattributes new file mode 100644 index 00000000..500129dd --- /dev/null +++ b/.agents/docs/2026-10-22-issues-59-63-evidence/.gitattributes @@ -0,0 +1,2 @@ +# Unified diff context for an empty line is a literal single space. +engine-memory-*.patch whitespace=-blank-at-eol diff --git a/.agents/docs/2026-10-22-issues-59-63-evidence/HANDOFF.md b/.agents/docs/2026-10-22-issues-59-63-evidence/HANDOFF.md new file mode 100644 index 00000000..6c7978e3 --- /dev/null +++ b/.agents/docs/2026-10-22-issues-59-63-evidence/HANDOFF.md @@ -0,0 +1,37 @@ +# PR65 同步交接 + +此前评论里的“本地修复”没有 commit/push,这是交接缺口。本次将其实际提交到 `fix/17-connection-writer-encoding`,并附带[可恢复的精简证据包](portable-handoff/README.md)。接手者不需要访问原机器。 + +获取:`gh pr checkout 65 --repo Sunrisepeak/mcpp-language-server`;或者在已有仓库中 `git fetch origin fix/17-connection-writer-encoding` 后检查该分支。不要在 `release/0.0.16` 分支上寻找未合入的修复。 + +## 已同步文件 + +- `src/orchestrator/workspace.cpp`、`src/project/scan.cpp/.cppm`:成功 producer 回答确认 run-start 源结构,防止条件 import 导致 body-only 保存重复 emit;代数/身份/stamp、失败、inflight、ABA 和未知预处理均保守处理。仅确认扫描 opt-in 保留预处理文本。 +- `tests/test_answer_routing.cpp`、`tests/test_scan.cpp`、`conformance/fixtures/mcpp-watched-source-structure/`:对应原生及完整 70 项 LSP 对照。 +- `src/server/session.cpp/.cppm`、`tests/test_session.cpp`:拒绝已有普通文件作为 workspace directory,保留原目录回退顺序。 +- `editors/zed/tests/{smoke.sh,lsp_frames.py}`、CI workflows:真实目录与 mcppls 自身 clangd 初始化/进程检查、Zed PATH clangd、Termux 官方包源、首失败日志/producer envelope 保留。 +- `conformance/fixtures/mcpp-target-windows-real/`:真实 std 依赖修正,原 validator 未放宽。 +- `src/bin/conformance.cpp`、`tests/test_conformance.cpp`:有界 completion 请求时间线、最慢支持点及已有前后 report 的覆盖边界。 +- 本目录 `engine-memory-{proposal.md,proposal.patch,native-canary.patch,proposal-source.json}`:完整四份本地引擎提案,**未应用到引擎、未编译 canary、未发布或修改 payload lock**。 + +## 实际验证与限制 + +修复是在 `e01e1c1` 基础上的 dirty snapshot 验证,包中保留 exact patch、未跟踪 fixture/test 快照、修订和二进制 hash;不能将这些记录冒充新 commit 的远端 CI。 + +| 验证 | dev | release | +|---|---|---| +| 完整 workspace | 6 members / 58 programs / 0 failed | 6 members / 58 programs / 0 failed | +| 原 31 组 LSP + 新 fixture | 32 组 / 0 failed | 32 组 / 0 failed | +| 新 watched-source fixture | 70 checks / 0 failed | 70 checks / 0 failed | +| 重点原生 | scan 23/146、Workspace 29/399、Session 3/12,均 0 fail | 同左 | + +重点 Session 测试最初两处 string/JSON 比较编译失败,已修为显式 `.get()`;首次日志和修订均保留。规范校验 316 rules、0 failure、4 项既有 pending。新源版本 CI、真实 Windows 双目标链、新产品完整性能验收、VSIX 实际升级与发布验证仍未完成。 + +## 未解决的问题 + +- 原 `1341938` nightly 四项失败仍成立:mcpp U5/U6 慢请求、U15 在 U6 内严格 60 秒窗口 13/min、xlings idle RSS +25.33%、two-core timeout/U7a 超时;不由后来 PASS 抹去。 +- 新 trace 四阶段为 25 checks / 1 failure。本次 U5/U6 未复现历史慢请求;U7a 107.247s 来自 setup 阶段 config AST 崩溃后的 120s 隔离。其他七个 importer <6s,恢复后 AST 约 .730s;底层 crash 仍未归因。不要通过缩短冷却或改预算制造通过。 +- 引擎 .4 的周期 memo JSON 在版本判断前构造,独立无效分配已证明;尚不能归因全部 RSS 增长。引擎提案只作为候选,不代表已获授权实施。 +- 远端旧 `e01e1c1` CI/nightly 仍失败:Windows schema precheck、Termux 镜像 404、Zed 默认模式 API403 及 file-as-folder 产品缺陷;新修订需实际新 CI。 + +当前要求是同步并交接,PR 继续 draft,**不合并、不发布 0.0.17、不修改其他仓库或 issue 状态**。接手先核验本提交 diff 与证据,再按简报继续调查。发布暂缓;不能因为后来 CI 通过就自动发布。原两个实际 0.0.16 升级 profile 不重置。用户 `.zcodeignore`、`.agents/docs/2026-10.12-implementation-status-tmp` 未纳入提交。 diff --git a/.agents/docs/2026-10-22-issues-59-63-evidence/README.md b/.agents/docs/2026-10-22-issues-59-63-evidence/README.md index 9e0d041b..b4b4f65a 100644 --- a/.agents/docs/2026-10-22-issues-59-63-evidence/README.md +++ b/.agents/docs/2026-10-22-issues-59-63-evidence/README.md @@ -1,6 +1,8 @@ # #59 / #63 验证简报 -日期:2026-10-22 JST。**R1 产品补修本地已通过;新 CI 的 CPU 采样测试与原九阶段 warm 超时仍待解决,PR 保持草稿。** +交接获取修正:源码、四份引擎提案及精简证据现随 PR 分支提交。见 [HANDOFF.md](HANDOFF.md) 和[证据恢复入口](portable-handoff/README.md),不再要求接手者拥有原机器的 `.payload-cache`。两 profile 完整 workspace 各 58 程序、LSP 各 32 组(含 70 项 fixture)均已通过;新源版本远端 CI 与性能问题仍待验证。 + +日期:2026-10-22 JST。**PR #64 已合入;PR #65 的产品补修在提交 `1341938` 通过 PR CI 与本地原九阶段。最终范围审计补充真实 mcpp Windows 双目标链,新增验收仍待原生 CI;nightly 和正式发布/升级尚未完成。** 本目录只收结论、[复现步骤](reproduce.md)和 [SHA-256 索引](raw-index.tsv)。45 份原始记录共 26,769 行,已移出 PR;失败、对照、日志与 core 均保留。源码边界见 [设计方案](../2026-10-22-issues-59-63-target-and-reliability-design.md)。 @@ -56,6 +58,59 @@ R1 补修本地 dev/release 全 workspace 均为 6 members / 57 programs / 0 fai 同一新产品的原性能首轮 mcpp warm ready **15.219 s > 12 s**,真实失败;176 已有模块零重建/零首次构建、零错误/重启。producer/model/首次导航很快,慢段位于引擎 prerequisite reuse 检查与扫描。该段采样约 0.343 core;前检查点约 1.353 core,总 CPU 工作近似相当,但缺当时 IO、调度及宿主 pressure 证据,不归因外部负载或认定产品无关。其余阶段继续,不能将旧九阶段通过或后来诊断通过替代本轮失败。 +## 合入检查点与发布前发送补修 + +`38366b9` 的 PR CI 39720450692 为 61 success / 1 expected skip;proot 首次下载 VS Code 超时,单项原样重试后实际 LSP/editor 通过,首日志保留。nightly 39720447763 为 62 success / 4 expected skips,全部八组 UX 的 34 阶段 / 207 检查实际通过。固定 observer 对照四组 cold→firstwarm 全部退出 0,warm 为 3.92/4.06/4.16/3.98 秒,没有证明观察器导致首次超时。 + +另一次预登记的完整九阶段最终验收为 9 exit 0 / 53 checks / 0 failed,保留原 CPU、pins、producer、cache lineage 和门槛;mcpp warm 3.824 秒,U8 undo 均为 0。原 600 秒 xlings idle 为 0.004819 core、RSS 1258.30→1121.86 MiB,有限工作单列。此前 15.219 秒失败仍未归因,未因新通过改写。该检查点于 `fd869bd` squash 合入;原始材料及小型终态摘要保存在 `.payload-cache/59-63-performance/light-observer-final-acceptance-f6ec554/`。 + +随后 draft Release 39732244483 的 macOS dev job 119670268987 在原 `test_process.cpp` 发送检查失败:4 MiB 入队应小于 2 秒;CPU oracle 测试通过。旧断言未打印实际耗时,不能给出精确超时值或归因宿主。代码确认 JSON 编码仍在调用线程同步执行;补修将拥有所有权的 JSON 快照交给 writer 编码、写入,保持 FIFO、UTF-8 和两种 framing,并隔离 writer 编码异常。原 4 MiB / 2 秒及 stop 10 秒门槛不变;新产品必须重新验收,不能借用前述产品通过。见[复现第 11 节](reproduce.md#11-发送队列的编码与快照)。 + +## 新增状态一致性回归与证据边界 + +`072c230` 的 CI 39735383721 有真实 U15-edits 失败:最忙一分钟 13 次状态转换,超过原 12 次门槛;169 次采样、总 28 次转换、两次配置重启、零 error。其余 cold/warm/typing/graph/config undo/faults 通过不抵消该失败。原 artifact 只有汇总,旋转 server 日志也没有完整 status 轨迹,**不能确定这 13 次的原因**。 + +独立回归证明同步 apply 的一致性缺陷:真实 clangd 在安装新 primer/安排重启前会调用 `host.status_changed()`,workspace 此时已采用新 profile。相同回调时序的定制 core 在旧产品上仅一个断言失败,暴露了引擎尚未 commit 的新 profile/ready;无回调对照通过。补修只合并同一同步 apply 栈内的状态通知,所有参与引擎完成后发布;不增加延时、不扩展 hold、不隐藏异步状态。修后 dev/release 各 28 cases / 357 assertions 通过,其他验证仍在进行。这一独立缺陷不能冒充首个 13 次失败的归因。 + +runner 新增紧凑 `stateTransitionTrace` 与 `busiestMinuteWindow`:保存初始状态、重启边界和实际计数的转换,带整数纳秒及采样索引,能按原严格 `<60s` 窗口重算。只保留 issue code,不复制完整 status 或路径;原采样、计数和门槛不变。完整新产品性能链与平台 CI/nightly 尚未完成。见[复现第 12 节](reproduce.md#12-同步-apply-的状态快照与时间线证据)。 + +## 补修产品最终本地验收与真实 Windows 生产链边界 + +提交 `1341938` 的 PR CI 39742414940 为 **46 success / 1 条件跳过**。两种 profile 各 6 members / 57 programs、31 组真实 LSP、原完整 build-programs 与实际 VS Code(44 passing / 1 平台专用 pending)均零失败。原九阶段单次完整运行 **9 exit 0 / 53 checks / 0 failed**,fixture、CPU、producer、cache lineage 与预算保持原值;mcpp/xlings warm ready 为 **6.572 / 2.435 秒 <12 秒**,已有模块分别 176/111,均零重建;xlings 另有 39 首次构建。U8 撤销均零重建,U15 edits 最忙一分钟为 4/2 次。 + +原 600 秒 xlings idle 为 **0.004954 core**,RSS **1108.40→1129.73 MiB(+1.92%)**;之前 62.021 秒有限工作单列,不混入空闲值。独立重算全部状态轨迹和严格 `<60s` 窗口,9 个 observer 均正常结束,303 个观测到的 PID/birth 身份已退出;1 秒采样可能漏掉瞬时子进程。终态摘要在 `.payload-cache/59-63-performance/light-observer-final-acceptance-status-1341938/terminal-summary.json`,原 13/min 与 15.219 秒失败及其未归因边界不变。 + +范围审计发现:原 `mcpp-target-windows` 是 mock producer + 真实 clangd,它证明目标传递和条件声明,但不能代替设计要求的真实 mcpp 原生双目标生产链。新增 `mcpp-target-windows-real` 使用真实 `mcpp build --configure-only --target` 生成两份完整 CDB,不改写条目;同一会话切换 i686→x64→i686,校验 auto 来源、实际引擎命令、32/64/32 条件声明、导航与诊断。Windows 原生 CI 实际通过前,该项保持待验收;没有把 mock 通过扩大为真实生产链通过。 + +## 134 nightly 首次失败与后续修复边界 + +`1341938` 的 nightly [39742450508](https://github.com/Sunrisepeak/mcpp-language-server/actions/runs/39742450508) 最终为 **58 success / 4 failure / 4 skipped**,不能由本地九阶段或 PR CI 通过替代: + +| 首次失败 | 原始门槛与观测 | 当前结论 | +|---|---|---| +| mcpp round 2 | U5 p95 2.374 s / max 8.264 s;U6 max 6.384 s,超过 1/3/3 s 预算 | 未归因。server completion max 8.262 s 与客户端一致,不能只归为 runner 接收延迟 | +| mcpp round 1 | U15 edits 最忙一分钟 13 次 >12 | 重算跨度 59,999,840,473 ns,确实在严格 `<60s` 内;全部支持点位于 U6 autosave,不能归给后续 U8 | +| xlings two-core | U5 timeout 1;U7a p95/max 1.53 s >1.5 s | 保留实际失败,best effort 不等于通过 | +| xlings round 1 | 原 600 s idle CPU 0.006108 core;RSS 948.086→1188.250 MiB,增长 25.33% >10% | idle 前 126.419 s 有限工作单列;idle 无引擎活动事件。独立 memo 分配问题不能冒充本次全部增长的精确归因 | + +原日志和四组 measure 保存在本地 `.payload-cache/59-63-real-target-integration/first134-nightly-*`;U15 的逐点重算为 `first134-nightly-mcpp-round1-window-recompute.json`。PR 仅保留本简报和复现入口,不提交完整原始输出。 + +新的 completion trace runner 已在 dev/release 各通过 11 cases / 443 assertions;旧 runner 跑新增测试恰有两项失败,原九项通过。trace 冻结原采样回答集合后才构建 JSON,保留最多 256 条有序请求及覆盖全部请求的最慢支持点,不改变采样、超时或门槛。使用旧产品和新 trace runner 的单次四阶段诊断已结束:25 checks / 1 failure,168 个观测 PID/birth 身份均退出。U5/U6 本次 max 为 0.861/0.707 s,未复现历史慢请求,不能解释或抹去原失败;唯一 U7a failure 是 importer diagnostics 107.25 s >33 s。完整证据显示一个 importer 在保存前 clangd AST 崩溃后被隔离,其他七个约 5.6 s 内重发,迟到 importer 约两分钟后恢复;底层崩溃与安全恢复条件仍在审查。**这不是产品性能修复或新产品验收**。 + +另有独立真实 LSP 反例:合法 `#if 0` import 被 producer 正确排除,旧产品却因 lexical/model 差异,对 body-only 和相同字节保存重复 emit。新 fixture 在旧产品上为 11 次 emit、14 个失败检查(包括计数级联,非 14 个独立缺陷);修复须确认成功生成开始时的结构,保留失败重试、生成中变化、预处理变化、target/producer/manifest 的重载。产品修改仍在验证,**不宣称它解释了历史 13/min**。 + +`e01e1c1` 的 Windows job 在 schema precheck 提前失败:新真实目标 fixture 未使用 `std`,实际 producer 不输出原契约要求的 `mcpp:std`。fixture 已补真实 `import std` 和类型断言;Linux 上真实 pinned producer 的 schema 对照通过,原 validator 未放宽;Windows 真正双目标 LSP 验收仍待新 CI。新增 artifact 保存 schema 阶段最初的 producer envelope,即使尚未进入 LSP fixture 也可审查。 + +## e01 CI 与 Zed/Termux 的新增失败 + +`e01e1c1` 的 PR CI [39752365858](https://github.com/Sunrisepeak/mcpp-language-server/actions/runs/39752365858) 最终 **45 success / 1 failure / 1 skipped**;唯一失败为上述 Windows schema precheck。nightly [39752360539](https://github.com/Sunrisepeak/mcpp-language-server/actions/runs/39752360539) 尚未结束,另已保存两个实际失败: + +- Termux 随机镜像对 `proot-distro_5.6.0_all.deb` 返回 404,在 Debian login 和任何产品验证前退出。修订固定[官方主源](https://github.com/termux/termux-packages/wiki/Mirrors),不再吞 update 失败,原 bind/LSP 检查保留;静态 shell/YAML 检查通过,实际容器验证待新 CI。 +- Zed 默认模式 240 秒内没有自己的 clangd,日志记录 GitHub API 403。CI 修订预装 PATH clangd,仍要求 Zed 按默认设置亲自启动独立进程;这不再声称测试自动下载。 +- 同一 Zed artifact 还证明独立产品兼容缺口:`workspaceFolders` 指向真实 `.cppm` 文件,而 `rootUri` 是目录;mcppls 采用文件作为 clangd cwd,出现 `engine-start-failed`。修订忽略已有普通文件的 folder 项并沿原目录回退顺序选择,不把文件的 parent 猜成项目。新 native 回归及真实 Zed 复验待执行。 + +原 smoke 仅需任意 diagnostics,模块层错误也可满足,故新 smoke 同时要求 mcppls 自身 clangd 完成协议初始化并存活,不能用 Zed 的另一个 clangd 代替;这仍不宣称全部 BMI 准备完成。原始 initialize、server 日志和首次失败完整 job log 保存在本地 `zed-smoke-default-first/` 和 `nightly-{termux,zed}-*-first.log`。artifact 将补收自动诊断包,以便下一次启动失败保留更精确的错误。上述源修改均尚未经过新产品完整验证。 + ## 历史失败与反证仍保留 | 记录 | 结论边界 | @@ -82,4 +137,4 @@ R1 补修本地 dev/release 全 workspace 均为 6 members / 57 programs / 0 fai - [raw-index.tsv](raw-index.tsv) 为逐文件大小与 SHA,不按通过筛选;本地历史快照 `.payload-cache/59-63-evidence-raw/checkpoint-20261021T202025Z-5e19005.tar.gz`,SHA `674b5072ddfb27204aef5bc2de61cf13ad38176f3e5a79c00a5cda382e15a8c0`。 - 快照后的记录独立保存在上述目录、envelope 与 CI artifacts,不混入旧索引。较完整的历史叙述还可从 Git 的 `bcc9cbe` 版简报读取。 - 已结束日志的 `.log.gz` 为无损压缩,`compressed-logs.jsonl` 记录原始字节数/SHA/解压核验;core 保留本地。早期丢失的 `/tmp` 跟踪不冒充完整证据。 -- 空间不足只清理本项目过期、无进程引用的构建中间文件;当前冻结、升级 profile、性能缓存、日志及 core 不动。清理范围与保留二进制 SHA 另有 JSON 记录。 +- 空间不足只清理本项目过期、无进程引用的构建中间文件。2026-10-22 另清理 32 个历史独立性能 cache 内的 10,170 个 PCM/对象文件,释放 11,077,361,664 B;保留这些运行的 JSON、日志和输入,历史缓存已不能据此复用重跑。当前诊断与验收缓存、冻结二进制、升级 profile、日志和 core 不动。逐文件元数据和进程引用检查见本地 `cleanup-2026-10-22-obsolete-performance-{plan,report}.json`。 diff --git a/.agents/docs/2026-10-22-issues-59-63-evidence/engine-memory-native-canary.patch b/.agents/docs/2026-10-22-issues-59-63-evidence/engine-memory-native-canary.patch new file mode 100644 index 00000000..28f38a47 --- /dev/null +++ b/.agents/docs/2026-10-22-issues-59-63-evidence/engine-memory-native-canary.patch @@ -0,0 +1,126 @@ +--- a/clang-tools-extra/clangd/ModuleMemoStore.h ++++ b/clang-tools-extra/clangd/ModuleMemoStore.h +@@ -44,6 +44,10 @@ + llvm::json::Value moduleMemosJSON(); + size_t adoptModuleMemos(const llvm::json::Value &Memos); + ++/// Native-test seams. Poll is allowed only after flush joined the worker. ++size_t moduleMemosJSONBuildCountForTest(); ++bool pollModuleMemoStoreForTest(); ++ + } // namespace clang::clangd + + #endif +--- a/clang-tools-extra/clangd/ModuleMemoStore.cpp ++++ b/clang-tools-extra/clangd/ModuleMemoStore.cpp +@@ -17,6 +17,7 @@ + #include "llvm/Support/MemoryBuffer.h" + #include "llvm/Support/Path.h" + #include "llvm/Support/raw_ostream.h" ++#include + #include + #include + #include +@@ -24,6 +25,8 @@ + + namespace clang::clangd { + namespace { ++ ++std::atomic JSONBuilds{0}; + + constexpr int FormatVersion = 2; + constexpr llvm::StringLiteral FileName = "module-memos.json"; +@@ -392,6 +395,7 @@ + // ---- the whole -------------------------------------------------------------- + + Value memos(uint64_t &Verdicts, uint64_t &Scans) { ++ JSONBuilds.fetch_add(1, std::memory_order_relaxed); + auto VerdictSnapshot = getModuleValidationCache().snapshot(Verdicts); + auto ScanSnapshot = getModuleDependencyScanCache().snapshot(Scans); + Array VerdictsJSON; +@@ -443,6 +447,13 @@ + public: + explicit Store(std::string Directory) : Directory(std::move(Directory)) {} + ++ bool pollForTest() { ++ if (Worker.joinable()) ++ return false; // never race the periodic writer with the test ++ write(); ++ return true; ++ } ++ + void start() { + Worker = std::thread([this] { + load(); +@@ -578,7 +589,17 @@ + S->flush(); + } + ++size_t moduleMemosJSONBuildCountForTest() { ++ return JSONBuilds.load(std::memory_order_relaxed); ++} ++ ++bool pollModuleMemoStoreForTest() { ++ Store *S = theStore(); ++ return S && S->pollForTest(); ++} ++ + llvm::json::Value moduleMemosJSON() { ++ + uint64_t Verdicts, Scans; + return memos(Verdicts, Scans); + } +--- a/clang-tools-extra/clangd/unittests/PrerequisiteModulesTest.cpp ++++ b/clang-tools-extra/clangd/unittests/PrerequisiteModulesTest.cpp +@@ -1 +1,2 @@ + #include "ModuleMemoStore.h" ++#include "llvm/Support/MemoryBuffer.h" +--- a/clang-tools-extra/clangd/unittests/PrerequisiteModulesTest.cpp ++++ b/clang-tools-extra/clangd/unittests/PrerequisiteModulesTest.cpp +@@ -1 +1,46 @@ ++// Run this lifecycle canary in an isolated process: Store is a process singleton. ++// Poll uses the real periodic write path, after flush has joined its worker. ++TEST_F(PrerequisiteModulesTests, UnchangedMemosDoNotConstructJSON) { ++ writeTestFile(TestDir, "quiet-input.cpp", "int before;\n"); ++ writeTestFile(TestDir, "quiet.pcm", "identity-only test payload"); ++ const auto Input = getFullPath("quiet-input.cpp"); ++ const auto BMI = getFullPath("quiet.pcm"); ++ const auto File = getFullPath("quiet-memos/module-memos.json"); ++ auto Identity = ModuleValidationCache::identityOf(Input); ++ ASSERT_TRUE(Identity); ++ getModuleValidationCache().markFresh(BMI, "quiet-canary", {{Input, *Identity}}); ++ const auto Before = moduleMemosJSONBuildCountForTest(); ++ startModuleMemoStore(getFullPath("quiet-memos")); ++ flushModuleMemoStore(); ++ const auto First = moduleMemosJSONBuildCountForTest(); ++ EXPECT_EQ(First, Before + 1); ++ std::string InitialBytes; ++ { ++ auto InitialFile = llvm::MemoryBuffer::getFile(File); ++ ASSERT_TRUE(InitialFile); ++ InitialBytes = (*InitialFile)->getBuffer().str(); ++ } // close the mapping before a later rename, including on Windows ++ for (unsigned I = 0; I != 10; ++I) ++ ASSERT_TRUE(pollModuleMemoStoreForTest()); ++ EXPECT_EQ(moduleMemosJSONBuildCountForTest(), First); ++ { ++ auto QuietFile = llvm::MemoryBuffer::getFile(File); ++ ASSERT_TRUE(QuietFile); ++ EXPECT_EQ((*QuietFile)->getBuffer(), InitialBytes); ++ } ++ // Dirty data still builds JSON and reaches disk on the same periodic path. ++ writeTestFile(TestDir, "quiet-input.cpp", "int after_changed;\n"); ++ Identity = ModuleValidationCache::identityOf(Input); ++ ASSERT_TRUE(Identity); ++ getModuleValidationCache().markFresh(BMI, "quiet-canary", {{Input, *Identity}}); ++ ASSERT_TRUE(pollModuleMemoStoreForTest()); ++ EXPECT_EQ(moduleMemosJSONBuildCountForTest(), First + 1); ++ auto ChangedFile = llvm::MemoryBuffer::getFile(File); ++ ASSERT_TRUE(ChangedFile); ++ EXPECT_NE((*ChangedFile)->getBuffer(), InitialBytes); ++ // Final flush deliberately keeps the pre-fix snapshot behavior. ++ flushModuleMemoStore(); ++ EXPECT_EQ(moduleMemosJSONBuildCountForTest(), First + 2); ++} ++ + TEST_F(PrerequisiteModulesTests, MemosKeptForTheNextSession) { diff --git a/.agents/docs/2026-10-22-issues-59-63-evidence/engine-memory-proposal-source.json b/.agents/docs/2026-10-22-issues-59-63-evidence/engine-memory-proposal-source.json new file mode 100644 index 00000000..f40a5715 --- /dev/null +++ b/.agents/docs/2026-10-22-issues-59-63-evidence/engine-memory-proposal-source.json @@ -0,0 +1,10 @@ +{ + "engineForkCommit": "92a4c630f4691890e6133c87d3e4591bc5fd8258", + "engineVersion": "23.1.0-mcppls.4", + "pinned0030SHA256": "091fa15f06a736955f331c3025bf73bcd891bce7de81024c8f3826588f0b9986", + "runtimePatchSHA256": "143bebe8e0d335620ddfe18350dd8a7c22629d50050bbcb1c3f512ec4f3b0122", + "nativeCanaryPatchSHA256": "3c12e2a2753ff3560a9238d59ac78d36c0ac8a3ccf33e15fc09de770cfa1fbbf", + "status": "local proposal only; no LLVM build, no engine checkout changes, no registration/comment/publication", + "contextValidation": "Store.cpp/header reconstructed exactly from added-file content of pinned0030; cache snapshot contexts from same patch; full released source git apply --check pending authorization", + "shutdownPolicy": "Periodic writes get early-version fast path; flush forces original snapshot path because ClangdMain permits lingering cache writers" +} diff --git a/.agents/docs/2026-10-22-issues-59-63-evidence/engine-memory-proposal.md b/.agents/docs/2026-10-22-issues-59-63-evidence/engine-memory-proposal.md new file mode 100644 index 00000000..34375093 --- /dev/null +++ b/.agents/docs/2026-10-22-issues-59-63-evidence/engine-memory-proposal.md @@ -0,0 +1,74 @@ +# 引擎 memo 空闲分配:本地修复提案 + +状态:供 review、随 PR 同步的交接候选,当前不实施引擎修复。未编译 LLVM、未修改任何引擎工作树,未发引擎评论、PR 或 release。原设计首段限定“只修改 mcppls 仓库”;本提案只是存放在本仓库的评审材料。根任务尚未决定是否跨仓实施;先确认引擎修改范围是本提案的 scope 建议,不是 skill 强制的审批步骤。用户既有指令禁止 GitHub issue 评论及状态变更;即使后续允许修改引擎,也不自动授权登记上游问题或对外发布,相关外发仍须另获明确授权。不能用修改 payload lock 代替尚未完成的引擎修复和验收。 + +## 已证明的缺陷与尚未证明的归因 + +精确发布输入为 fork `92a4c630f4691890e6133c87d3e4591bc5fd8258`、`23.1.0-mcppls.4`。原始 `0030` 位于本地 `engine-memory-review/pinned-0030.patch`,其 SHA 与提案 SHA 记录于 [engine-memory-proposal-source.json](engine-memory-proposal-source.json)。 + +`Store::write()` 先调用 `memos(Verdicts, Scans)`,复制两个缓存的 snapshot、构造完整 JSON,之后才判断版本是否与 `Written*` 相同。因此,每个无变化的 30 秒周期也会构造并销毁整份 JSON;“没有写文件”不等于没有分配。该控制流缺陷可直接由发布源码证明,与任何一次 RSS 曲线是否复现无关。 + +首次 `1341938` nightly xlings round1 的原 U13 仍为 FAIL:600 秒 CPU `0.0061084758` core,RSS `948.0859375 → 1188.25 MiB`,增长 `25.3314655%`;原 CPU `0.01`、RSS `10%` 门槛不变。已知有限工作 `126.418657918s` 已单列,前后 preparation/index/request 状态一致,记录的 engine activity 为零。原始 measure 是 `.payload-cache/59-63-real-target-integration/first134-nightly-xlings-round1-measures/ux-xlings-1-long.json`。 + +这些数据**尚不能证明这次 +25.3% 全部来自 memo JSON**:原 artifact 没有分配栈、逐 PID smaps/allocator 归属或序列化期间 heap 证据;当前 activity 计数也不是全引擎分配计数。提案修复一个独立、已知的无效分配路径,不能先承诺修复全部 U13,更不能把之后的 PASS 覆盖首次 FAIL。 + +## 最小运行时改动 + +[engine-memory-proposal.patch](engine-memory-proposal.patch) 仅涉及三个发布源码文件,21 行新增、2 行修改: + +1. `ModuleValidationCache::changeCount()` 在 `Mutex` 内读取 `Changes`。 +2. `ModuleDependencyScanCache::changeCount()` 在 `Mu` 内读取 `Stores`,与现有 `snapshot()` 读取同一计数。 +3. 周期 `Store::write()` 在调用 `memos()` 前比较两个 cheap counter。均等于最近**成功发布**的版本则立即返回;有变化仍走现有 snapshot、JSON、临时文件和 rename 流程,并保留现有 snapshot 后的版本判断。 + +不改变 30 秒周期、JSON 格式、持久化开关、缓存准入、BMI/输入验证、RSS 测量起点、预算或观察频率。计数器采样不遍历 Entries,不复制 manifest,也不构造 JSON。两缓存分别短暂加锁,不同时持有两把锁;无需引入新的跨缓存锁或 Linux 专用 allocator 操作。 + +当前发布补丁中,validation 的插入/判 stale 递增 `Changes`;scan 的 `Entries.push_front` 与 `++Stores` 位于同一 `Mu` 临界区(已有 `0023` 发布补丁上下文)。实施时仍需审计完整发布树的所有持久化可见变化:这次优化复用原先写入决策的版本语义,不应顺便声称修复未计数的 eviction、adopt 或 eligibility 变化。 + +## 并发、失败与退出边界 + +| 时序 | 行为及保证 | +|---|---| +| 无变化 | 至多读取两个计数;直接返回,不调用 `memos()`。 | +| 变化恰好发生在 cheap 检查之后 | 这一 tick 可返回;`Written*` 没有前移,下一 tick 仍可发现变化。不是事件被消费掉。 | +| 构造 JSON 时又发生变化 | 成功 rename 后只保存 `snapshot()` 捕获的两个版本,不能重新读取 live counter 标记为 written。后续变化保持 dirty。 | +| 两个 snapshot 之间有写入 | 沿用原来两个独立 snapshot 的语义,不承诺跨两个缓存的事务原子性;较新的版本会再次被持久化。 | +| create/write/flush/rename 失败 | 沿用原分支,`Written*` 不更新;即便下一轮没有新缓存变化,已有 dirty 版本仍会重试。不能在 cheap 检查时提前更新 written。 | +| 最终 flush | 先 join Store worker,再显式 `write(CheckForChanges=false)`,保留原实现先做 snapshot 的退出路径。周期 fast path 不用于最终 flush。 | + +最后一项有必要:发布版 `ClangdMain` 在 `flushModuleMemoStore()` 之后仍注释可能存在 lingering background threads。Store 的 join 只停止持久化 worker,不能假定所有缓存生产者已经停止。若最终 flush 也使用 early-return,会新增“读到旧 counter 后并发产生变化就直接退出”的窗口。因此本提案保留旧最终 snapshot 路径。 + +也必须说明其上限:若缓存生产者在最终 snapshot **之后**仍产生新条目,旧实现本身就没有全量退出保证。本提案不修改 shutdown 顺序,不应宣称持久化了进程最后一个后台动作。若用户要求这个更强契约,需另审 producer drain → final snapshot 的生命周期排序;不能靠循环到版本稳定、无限等待或改周期补上。 + +## 原生 canary 与负对照 + +[engine-memory-native-canary.patch](engine-memory-native-canary.patch) 是独立的拟议测试补丁,尚未编译。它在真正的 `memos()` 入口计数,而非在成功写文件处计数;增加 test-only accessor,以及 **Store worker 已 join 后**才能调用的周期 write 测试入口。没有新的用户配置或 LSP 字段。 + +`UnchangedMemosDoNotConstructJSON` 使用真实缓存和真实 Store write: + +1. 制造一个 dirty validation 条目,启动 Store 后 flush/join,确认构造次数增加一次、文件可读。 +2. 无版本变化时调用周期 write 入口十次,要求构造计数完全不增加,磁盘 bytes 相同。旧实现虽然也不重写文件,却会再构造十次 JSON,因此同一 canary 必须在仅含测试补丁的 `.4` 实现上 FAIL。 +3. 再次 markFresh,要求下一次周期 write 构造一次并改变持久化文件 bytes;证明 fast path 不把后续 dirty 数据吞掉。 +4. 最终 flush 仍构造一次 snapshot,验证有意保留的退出策略。读取磁盘的 MemoryBuffer 在 rename 前销毁,避免 Windows 映射阻止替换。 + +Store 是进程 singleton,该生命周期 canary 应在独立原生测试进程执行;不能靠等待 30 秒、RSS 阈值或仅比较 mtime 来证明没有 JSON 构造。测试补丁可单独应用于旧实现作负对照;其 poll 入口拒绝在 worker 仍 joinable 时运行,避免测试制造双 writer。 + +合入前还必须补齐这些有控制屏障和故障注入的原生测试,不能以 sleep 近似并发顺序: + +- 在 cheap counter 读取与返回之间发布一次变化:下一次 poll 或强制 flush 包含该版本。 +- 在 snapshot 取得版本后、rename 成功前发布变化:written 仅推进至 snapshot 版本,下一次 poll 仍构造并保存新版本。 +- 分别注入 create/stream flush/rename 失败:文件保持上一有效版本;不发生新 cache mutation 时下一次 poll 仍重试。 +- producer 静止后最后一次变化由 flush 持久化;producer 在 snapshot 后继续变化的场景必须显式表述为旧 shutdown 契约边界,不写成已保证。 +- 覆盖 scan 与 validation 两个缓存的 mutation/counter 对应关系、从旧会话 adopt 后的无变化读取,以及 dirty scan 重试;若发现未计数的持久化可见变化,先修正版本协议,不能用 advisory cache 为漏测开脱。 + +## 验证与发布顺序 + +以下是跨仓实施范围获确认后的候选流程;当前只保留本地提案,外发及正式发布另按用户授权执行: + +1. 在精确发布源树检查补丁上下文,实施最小修复、原生负对照与上述并发/失败测试;仅在本地记录独立 UP 候选及已有输入/persistence 事项关联。用户既有指令禁止 GitHub issue 评论及状态变更,不能因扩大引擎修改范围而自动向 issue #24 登记;如确需外发,另获明确授权。当前没有发出任何登记或评论。 +2. 对 payload.lock 已列出的 linux-x64、linux-arm64、darwin-arm64、win32-x64 构建并运行相关原生测试;并发测试在支持的 Linux 工具链做 TSAN。新 canary 不以 Linux-only skip 规避 Windows/macOS。 +3. 先比较旧/新引擎的 JSON 构造次数;再在相同项目 pin、producer、CPU、原阶段 lineage 下运行原 U13 600 秒、0.01 core、10% RSS 预算,以及原完整场景。保留所有失败及 allocator/进程归属证据,不关闭 persistence,不延长起始等待,不改 RSS 算法,也不挑一次 PASS 当根因证明。 +4. 为候选修复分配下一未使用 engine 版本(例如仅在尚未占用时使用 `23.1.0-mcppls.5`),先完成真实跨平台构建与原生验证,准备本地或经授权的 draft 资产及 engine.json,记录 fork/LLVM commit、features、build/binary identity。此时尚不正式发布引擎,不能重用 `.4` 名称替换资产。 +5. mcppls 先 pin 上述候选资产,更新 `packaging/payload.lock.json` 候选中的 engine 版本、四个平台 URL/file/size/SHA256;从候选分发位置独立下载并按 lock 校验,刷新 payload manifest/identity,并逐条复查本地记录的 canary/WA 去留。保持 persistent-module-memos feature,kit 是否变化由实际 ABI/内容决定,不凭推测重发。 +6. 使用候选锁构建、验证完整 payload 与编辑器/发布产物,跑 dev/release native、跨平台 CI 和完整原性能验收;这些验证均使用尚未正式发布的候选资产,不依赖先发布引擎才能验证。通过后再按已获批准的发布流程正式发布相同字节的引擎资产,核对正式下载的 SHA/identity 与候选一致,再进行 mcppls 合入及正式 release。若正式 URL 变化则只迁移 lock URL 并复核下载,不重建或替换已验证字节;资产字节变化必须重新验证。更换 engine identity 导致旧 memo 候选不被采纳是兼容性边界,需按新引擎 cold→warm lineage 验证,不能要求复用已不兼容 BMI 来制造零重建。 + +当前只验证了提案补丁的 unified-diff 格式(`git apply --stat`)。Store 新文件内容从 pinned `0030` 完整重建;两个 header 的 snapshot 上下文来自同一发布补丁。没有在真实 LLVM 工作树执行 `git apply --check`、编译或运行测试,不能把本地提案称为已可发布修复。 diff --git a/.agents/docs/2026-10-22-issues-59-63-evidence/engine-memory-proposal.patch b/.agents/docs/2026-10-22-issues-59-63-evidence/engine-memory-proposal.patch new file mode 100644 index 00000000..719bbb61 --- /dev/null +++ b/.agents/docs/2026-10-22-issues-59-63-evidence/engine-memory-proposal.patch @@ -0,0 +1,51 @@ +--- a/clang-tools-extra/clangd/ModuleValidationCache.h ++++ b/clang-tools-extra/clangd/ModuleValidationCache.h +@@ -1,3 +1,9 @@ ++ /// Samples the same publication counter as snapshot(), without copying entries. ++ uint64_t changeCount() { ++ std::lock_guard Lock(Mutex); ++ return Changes; ++ } ++ + /// For the store (ModuleMemoStore.h): every entry, by key, and how many + /// entries have been made so far. + std::vector>> +--- a/clang-tools-extra/clangd/ModuleDependencyScanCache.h ++++ b/clang-tools-extra/clangd/ModuleDependencyScanCache.h +@@ -1,3 +1,9 @@ ++ /// Samples the same publication counter as snapshot(), without copying scans. ++ uint64_t changeCount() { ++ std::lock_guard Lock(Mu); ++ return Stores.load(); ++ } ++ + /// What the store writes: every live scan whose manifest can be replayed + /// in another session, and the kept ones not yet replayed in this one. + std::vector snapshot(uint64_t &StoredSoFar) { +--- a/clang-tools-extra/clangd/ModuleMemoStore.cpp ++++ b/clang-tools-extra/clangd/ModuleMemoStore.cpp +@@ -466,7 +466,7 @@ + Wake.notify_all(); + if (Worker.joinable()) + Worker.join(); +- write(); ++ write(/*CheckForChanges=*/false); + } + + private: +@@ -508,7 +508,14 @@ + .count()); + } + +- void write() { ++ void write(bool CheckForChanges = true) { ++ // Poll publication counters before copying snapshots or building JSON. ++ // Each read uses the same lock as its snapshot. A racing mutation remains ++ // dirty for the next tick. flush() retains the original snapshot path. ++ if (CheckForChanges && ++ getModuleValidationCache().changeCount() == WrittenVerdicts && ++ getModuleDependencyScanCache().changeCount() == WrittenScans) ++ return; + uint64_t Verdicts = 0, Scans = 0; + Value Root = memos(Verdicts, Scans); + if (Verdicts == WrittenVerdicts && Scans == WrittenScans) diff --git a/.agents/docs/2026-10-22-issues-59-63-evidence/portable-handoff/README.md b/.agents/docs/2026-10-22-issues-59-63-evidence/portable-handoff/README.md new file mode 100644 index 00000000..b28b15f2 --- /dev/null +++ b/.agents/docs/2026-10-22-issues-59-63-evidence/portable-handoff/README.md @@ -0,0 +1,18 @@ +# PR65 可获取的交接证据 + +此前交接评论仅引用原机器的未提交源码和 `.payload-cache`,其他机器无法获取。本次将修复、fixture、四份引擎提案提交到 PR 分支,并将精选原证据存入约 1 MB 的 `evidence.tar.gz`。`manifest.json` 逐项记录相对路径、字节数和 SHA-256;`archive.json` 记录整个包的 SHA-256。 + +在 PR checkout 根目录运行(仅依赖 Python 3.9+): + +```bash +python3 .agents/docs/2026-10-22-issues-59-63-evidence/portable-handoff/verify.py +python3 .agents/docs/2026-10-22-issues-59-63-evidence/portable-handoff/verify.py --extract --into . +``` + +第二条恢复精选证据到原相对路径;遇到已有且内容不同的文件会拒绝覆盖。也可先解到另一个空目录,避免混入当前调查。 + +包含:两 profile 全 workspace 与 32 组 LSP 的实际终态/日志、新增 70 项 fixture 的结果、旧产品重复 emit 反例、四项首次 nightly 性能失败、Windows/Termux/Zed 首次失败与原始 LSP、trace runner 对照、U7a 四阶段 measure/observer/provenance、崩溃栈及 parser context、符号化结果、memo 控制与原发布补丁、测试前源码快照。 + +不包含:ELF、PCM/对象/index 缓存、完整项目工作副本、30 MB 的生成 memo seed 和全部 verbose server 日志。摘要保留首次失败,原始数据不足的归因仍标未知。包是交接证据,不是可直接复用的完整性能运行环境;复现需按上一级 `reproduce.md` 重建工具及独立 workspace/cache。 + +两个 `state.json` 是归档时的历史快照,可能仍有 live/pending 字段;以实际 `workspace-tests.json`、`fixture-runs.json`、`fixtures-{profile}/summary.json` 和本次 `HANDOFF.md` 为准。旧路径中的绝对机器路径只用于原始 provenance,不要求新机器拥有同名目录。 diff --git a/.agents/docs/2026-10-22-issues-59-63-evidence/portable-handoff/archive.json b/.agents/docs/2026-10-22-issues-59-63-evidence/portable-handoff/archive.json new file mode 100644 index 00000000..61c524ac --- /dev/null +++ b/.agents/docs/2026-10-22-issues-59-63-evidence/portable-handoff/archive.json @@ -0,0 +1,8 @@ +{ + "archive": "evidence.tar.gz", + "bytes": 1002457, + "sha256": "af07e0600be611c59f8943c492c37743e1ab3640282d35a6b8044a4a7c95f25f", + "files": 222, + "uncompressedBytes": 8944052, + "allMembersReopenedAndSHA256Verified": true +} diff --git a/.agents/docs/2026-10-22-issues-59-63-evidence/portable-handoff/evidence.tar.gz b/.agents/docs/2026-10-22-issues-59-63-evidence/portable-handoff/evidence.tar.gz new file mode 100644 index 00000000..4fc2126c Binary files /dev/null and b/.agents/docs/2026-10-22-issues-59-63-evidence/portable-handoff/evidence.tar.gz differ diff --git a/.agents/docs/2026-10-22-issues-59-63-evidence/portable-handoff/manifest.json b/.agents/docs/2026-10-22-issues-59-63-evidence/portable-handoff/manifest.json new file mode 100644 index 00000000..659fa5aa --- /dev/null +++ b/.agents/docs/2026-10-22-issues-59-63-evidence/portable-handoff/manifest.json @@ -0,0 +1,1121 @@ +{ + "schemaVersion": 1, + "purpose": "Portable selected evidence for PR65; restores original relative paths, not an entire build/cache environment", + "testedSource": "e01e1c1 plus frozen dirty patch/source snapshots included; newly pushed head still needs its own CI", + "files": [ + { + "path": ".payload-cache/59-63-completion-trace/binaries-dev.json", + "bytes": 696, + "sha256": "7e8a49e12e3312a00c7d675e9aad9ce5a9967d5c3087b9a5b267c2da17b9139b" + }, + { + "path": ".payload-cache/59-63-completion-trace/binaries-release.json", + "bytes": 703, + "sha256": "041b92190b5a6c38353f6fd162fb67b1b5763f8ce5a9629b60d19ff27f48381a" + }, + { + "path": ".payload-cache/59-63-completion-trace/old-runner-negative-release.json", + "bytes": 527, + "sha256": "68ce18dc9561238efa6f1b5d59a1689cddcdb67441d4d4f74d25b1fd8802fd18" + }, + { + "path": ".payload-cache/59-63-completion-trace/old-runner-negative-release.log", + "bytes": 1359, + "sha256": "5d051a7671904e24fdd704219d1054a81af9c56ed67c290c6010a62b20e7325f" + }, + { + "path": ".payload-cache/59-63-completion-trace/previous-canaries.json", + "bytes": 636, + "sha256": "abf005b97f5531854f074e7a31ac39f7f1cd01f0ebaa85e71553a2eff8af8a65" + }, + { + "path": ".payload-cache/59-63-completion-trace/source-provenance.json", + "bytes": 427, + "sha256": "f22c2416bb509105b484425e19e91188f5fb2af71f2190340db620b68d19cb8c" + }, + { + "path": ".payload-cache/59-63-completion-trace/test-conformance-dev.log", + "bytes": 2679, + "sha256": "65cad58921a039356e8c92a023321a7e746b9fd8e4137b4b75043a56ddbcc68a" + }, + { + "path": ".payload-cache/59-63-completion-trace/test-conformance-release.log", + "bytes": 2787, + "sha256": "99b0be71e507d4a251c0daf7f4235511a4528db83301bf6ca9c0a3211243843a" + }, + { + "path": ".payload-cache/59-63-completion-trace/typing-trace-source-fixed.patch", + "bytes": 24945, + "sha256": "55a021e82d9d9161cd7e2d3b4fada7069d08be852b5ef0f542d0be809d0eb82e" + }, + { + "path": ".payload-cache/59-63-current-state.json", + "bytes": 7550, + "sha256": "c44cb2a9ed390e8d9e4c3f6d807ea39ce1c09d4c1b68c9e452297003eea38750" + }, + { + "path": ".payload-cache/59-63-performance/light-observer-final-acceptance-f6ec554/mcpp/cold/measure.json", + "bytes": 55565, + "sha256": "a99dc1ea02ef5b6c36ac6c15c289b2e69d031f5767c55d5d8da7c6f0ebe244fd" + }, + { + "path": ".payload-cache/59-63-performance/light-observer-final-acceptance-f6ec554/mcpp/edits/measure.json", + "bytes": 106271, + "sha256": "8418e0544dd55cbdb8c681324554b0fdfc08d2347de08d7a4bc650c0e77fbe3b" + }, + { + "path": ".payload-cache/59-63-performance/light-observer-final-acceptance-f6ec554/mcpp/faults/measure.json", + "bytes": 40059, + "sha256": "74f69e952a6eaa3038a34be9091527e0aef8bbee1e8c1685b087b02910098766" + }, + { + "path": ".payload-cache/59-63-performance/light-observer-final-acceptance-f6ec554/mcpp/warm/measure.json", + "bytes": 5113, + "sha256": "7acd998e83b9452deeaaed768103ad4079085c3dde0ed43471e4c13b8d7f9544" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/analysis-first.log", + "bytes": 782, + "sha256": "af2ad16a927d755cddfc894e905cb3c55d59f8a5b97ff4529a6e9abf41ff0ce3" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/analysis-fixed.log", + "bytes": 228, + "sha256": "ad1cd96c39a1a472039c928d6f449fd8a256578cb7ac8c6e1a027411ec492c6f" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/analysis-helper-amendment.json", + "bytes": 432, + "sha256": "08d6709cde016e9efd652a593f7f110e6005770b83287755e69430d0d50f31e9" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/analyze-terminal-first.py", + "bytes": 20549, + "sha256": "679a919eeaea42f1d53389c32a569ac23111fe3916669cd68bae32a84674bfb3" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/analyze-terminal.py", + "bytes": 20563, + "sha256": "8090e141842658d13a87bbbc8be33381601b03a03b6cf2f6ce20ced02b72f397" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/diagnostic-plan.json", + "bytes": 36815, + "sha256": "f7bbfd17ae658fcacc37d9c967796b2a75b580a774568ac1cffdb3e90475e9e6" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/mcpp/cache-a/payload-integrity.json", + "bytes": 459, + "sha256": "a4d55106807c1a95d36715b4003d292374ca7f63522785af9ebd9b520442b3af" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/mcpp/cache-b/payload-integrity.json", + "bytes": 459, + "sha256": "a4d55106807c1a95d36715b4003d292374ca7f63522785af9ebd9b520442b3af" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/mcpp/cold/binaries.json", + "bytes": 175, + "sha256": "b9186ca98c5eaf34293f19d347d40dc2427d1137c6af8f1e62a24d4cf3b6d70f" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/mcpp/cold/exit.json", + "bytes": 152, + "sha256": "fbc9e614328a42eb905b801c17bc2969fe18f629a4840a7ea0b687bad3de0cd2" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/mcpp/cold/invocation.json", + "bytes": 1665, + "sha256": "70b23dc8421022af3226c772c1dcc46ad67eb71499a35672551b0985f14b0a05" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/mcpp/cold/measure.json", + "bytes": 56661, + "sha256": "adf0e20bf1187909c64a79a39a3f1b2de421ad7190e6e5b822971218fa74ba75" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/mcpp/cold/processes.jsonl", + "bytes": 735596, + "sha256": "2ff585515868a2e62ef372750d6352032503d32729aba49489f700a6371a6cbd" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/mcpp/cold/server-log-archive.json", + "bytes": 914, + "sha256": "26ec8523b4ba38c60842c6dfbe23c211a345b2130566dbd837dca4138ae16d1b" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/mcpp/completed.json", + "bytes": 1280, + "sha256": "32991c0efa097337dd4b3de293f85915d7db5aac3567a21f069f7a508de3fd5a" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/mcpp/edits/binaries.json", + "bytes": 175, + "sha256": "b9186ca98c5eaf34293f19d347d40dc2427d1137c6af8f1e62a24d4cf3b6d70f" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/mcpp/edits/exit.json", + "bytes": 152, + "sha256": "012281ca92ca245f99dfd4546a45098deb269d29fe2c9dfe675ce259a9e4755e" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/mcpp/edits/invocation.json", + "bytes": 1688, + "sha256": "613d79fc9a2ec61c3118636a27edce70a0c0fe506ce5ccbd5fe7ecb2930cbbad" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/mcpp/edits/measure.json", + "bytes": 281259, + "sha256": "869ca265b46955815fb8e461fff7ee75eb9b59eb6ccc77d1fdd1528914e38360" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/mcpp/edits/processes.jsonl", + "bytes": 1363913, + "sha256": "8d9424ea35f93134d36ab0a2b195c1296a7c2559739fa6b26abb2773b4cdd501" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/mcpp/edits/server-log-archive.json", + "bytes": 1609, + "sha256": "042ac40062350fecab60dad82173ba1211f2a65f12848b5de0a231dd8c73347d" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/mcpp/faults/binaries.json", + "bytes": 175, + "sha256": "b9186ca98c5eaf34293f19d347d40dc2427d1137c6af8f1e62a24d4cf3b6d70f" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/mcpp/faults/exit.json", + "bytes": 155, + "sha256": "3ea69c6a981ca58e2d23c6387581533eb1977f96c2e3848e7198d822f68b591c" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/mcpp/faults/invocation.json", + "bytes": 1669, + "sha256": "ae7c6a5cf6028fd4b63738e5ecabea10af646d75102944de5e46a4f25a642aa5" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/mcpp/faults/measure.json", + "bytes": 49299, + "sha256": "6834720fba7fa995658b7aaef6bfba4636c4f2bede84ad83ba764215184324f6" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/mcpp/faults/processes.jsonl", + "bytes": 563009, + "sha256": "c40df88edd35f583ec2cb1c9fe6c838d7f3124e622dbf461d45c7e8f1d6af157" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/mcpp/faults/server-log-archive.json", + "bytes": 1782, + "sha256": "f5904aa86cb47a8d73938feb44c29e9fb823e82246e9749e5b35268705bd7cf1" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/mcpp/preflight.json", + "bytes": 959, + "sha256": "677ab2e02822aef0bf31b03e32ef1fb7ebe2aa6d524546bdb73acd7a0b823485" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/mcpp/producer-preflight.json", + "bytes": 613, + "sha256": "fe331b2f770b5fb7a687d47959e6be842c5ed9ddd327edb87023cb7ef557058f" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/mcpp/progress.json", + "bytes": 711, + "sha256": "cea4f9c4d25054e16fcc5506aa8023d598f8dd902ead2115b76c7b108baef48f" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/mcpp/started.json", + "bytes": 875, + "sha256": "aef522e5f9389290b88f38b4661cb348bbad557b034f8e1783707c6cfd90610a" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/mcpp/warm/binaries.json", + "bytes": 175, + "sha256": "b9186ca98c5eaf34293f19d347d40dc2427d1137c6af8f1e62a24d4cf3b6d70f" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/mcpp/warm/exit.json", + "bytes": 154, + "sha256": "3fd10d90ea5b4e5e79d637ae6d88e358fd0f321be58ca21d951e53bf844f5e22" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/mcpp/warm/invocation.json", + "bytes": 1686, + "sha256": "56d4bcc20c1179fd24ec9710a293294d406a9e67c52cec7c4b9a18c37ed0bd93" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/mcpp/warm/measure.json", + "bytes": 6669, + "sha256": "e1cb52ebe570dcc353d6d8480f20568c42c2af7497ce820cbe03e588d55be719" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/mcpp/warm/processes.jsonl", + "bytes": 656528, + "sha256": "743f1ff4cc68b191bd4cef2a1e14fe45ff35d13cce586b702ef9cc6a4ff7fa8c" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/mcpp/warm/server-log-archive.json", + "bytes": 1087, + "sha256": "a6844bcc3b5ed10c8a3a6afc331b51078e10ad3a2bf720b7ebb3f31596ac79ad" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/prepared-crosscheck.json", + "bytes": 1302, + "sha256": "8c4c673c29fe67344e405b10563310d2e6166a45db9caa8d54cd1aaa0c8e5b1a" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/root-registration.json", + "bytes": 605, + "sha256": "6c98f283d964d43da9985f12c31022e4cacf89a499dfcaed4d26178804726700" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/run.py", + "bytes": 13631, + "sha256": "077e293b935a280860952059d77d3805bdea647846618402ffe68eae188bc85a" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/runner-pending-pre-binding-plan.json", + "bytes": 35875, + "sha256": "0581ca1c05cd52800e031dd1eac83bb69922191c371a79e4f7abbc4e7a993bff" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/terminal-analysis.json", + "bytes": 131701, + "sha256": "aa8c81b6d111a6f321b3deac42b95c8e77829834f6f2659a9bd234aef061000a" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/u7a-crash-parser-context.txt", + "bytes": 2590, + "sha256": "0a56a7e5febb7a2b543236d6f24ecd862489a0fec6383e79031a34b319fbfd29" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/u7a-crash-stack-context.txt", + "bytes": 14183, + "sha256": "85fa9daf5a4b3926f9d6156f10a9a59e8e541d89d44595fc2ffc19a6c6b2ca1b" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/u7a-postrun-analysis.json", + "bytes": 18036, + "sha256": "23f3c48891437a6a604c6abd03935c7e4c0de1235e9f65f5849358d26e97f08b" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/u7a-postrun-analysis.md", + "bytes": 2806, + "sha256": "22bbfd685f5a2821dfb29c95df34b6a296d67d242ff8ca491c630ca180de1577" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/u7a-symbolization-summary.json", + "bytes": 6900, + "sha256": "30151877d8b42a489eeb6c825586f6f4c271cdb7f4a00544c2a54162f21c6e38" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/u7a-symbolize-addr2line.json", + "bytes": 331, + "sha256": "43c6a8db3d5c0b8f8eb80551ef50773c36a3025e920febabb8b643541a449786" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/u7a-symbolize-llvm.json", + "bytes": 329, + "sha256": "15fe4c9281a779c1dee2f058c3b061e1e365bdf22ebdf6ac190cdcb53fb3d434" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-134-nightly-round2-latency-diagnostic/u7a-symbolize-nm.json", + "bytes": 220, + "sha256": "8f96eccc8b384f2286648fafb8afb9e1abdc13c87fcc3e6348b9055c223b29cb" + }, + { + "path": ".payload-cache/59-63-performance/mcpp-staged-fixture/scenario.json", + "bytes": 23630, + "sha256": "eebddf0aed7f1d227d4f7dddee108e19b24afc85142d12f5fc30a6b99fded0f0" + }, + { + "path": ".payload-cache/59-63-performance/memo-control.py", + "bytes": 1843, + "sha256": "1b97034a7f2d8d9a1d80ba6b5ed267fe846806b6f74f7226443d77f0a6acb684" + }, + { + "path": ".payload-cache/59-63-performance/memo-only-lsp.log", + "bytes": 2071, + "sha256": "6eac28fcedad7344f4abdc222d62965deb9be0f268227ddf1fe957391ea14a90" + }, + { + "path": ".payload-cache/59-63-performance/memo-only.log", + "bytes": 3547, + "sha256": "e1f38b36de050b563e1bf7fe3c73aa6bdf9d0e5bf884e6b41331c2cbbaf98690" + }, + { + "path": ".payload-cache/59-63-performance/observe-owned-processes.py", + "bytes": 7301, + "sha256": "8410c71b3dddfa42d96ba805012c850569952b0597a56022f3789a4fda09071a" + }, + { + "path": ".payload-cache/59-63-performance/runner-cold-gdb2-memory.log", + "bytes": 7359, + "sha256": "21fc569f75f1f9b7834c76c843337ac525373712ccfe12b650ce51808e638e5f" + }, + { + "path": ".payload-cache/59-63-real-target-integration/ci-state.json", + "bytes": 188688, + "sha256": "85098f2d00b69cbce5ae48d8c5f7b1e400be657f5f0fb5f6d7da766e4dbef0e9" + }, + { + "path": ".payload-cache/59-63-real-target-integration/e01-nightly-windows1-first.log", + "bytes": 47508, + "sha256": "de87e81fbd3bd701d4fdac219c307930707dff498c3c681c0dec2f09c21b1033" + }, + { + "path": ".payload-cache/59-63-real-target-integration/engine-memory-review/pinned-0030.patch", + "bytes": 70632, + "sha256": "091fa15f06a736955f331c3025bf73bcd891bce7de81024c8f3826588f0b9986" + }, + { + "path": ".payload-cache/59-63-real-target-integration/first134-nightly-mcpp-round1-failure.log", + "bytes": 255474, + "sha256": "d1fd834da94950c205000aa18d1bd7499f5524aa6920707c18433ddf0aa9fdb4" + }, + { + "path": ".payload-cache/59-63-real-target-integration/first134-nightly-mcpp-round1-measures/ux-mcpp-1-cold.json", + "bytes": 45293, + "sha256": "ba40d219bff5bb920b5f4e0ed2511e97bb75fa494fe866d28173a73cd4d93428" + }, + { + "path": ".payload-cache/59-63-real-target-integration/first134-nightly-mcpp-round1-measures/ux-mcpp-1-edits.json", + "bytes": 105358, + "sha256": "c7455cfd6eedddac93446d7f3d54842831b78739dbc749f9240ea54ff8530447" + }, + { + "path": ".payload-cache/59-63-real-target-integration/first134-nightly-mcpp-round1-measures/ux-mcpp-1-faults.json", + "bytes": 38532, + "sha256": "5a94d32cc4c155182f27e2440be4c9bb95debf8afcb0f7f3156d4aa7363c28ec" + }, + { + "path": ".payload-cache/59-63-real-target-integration/first134-nightly-mcpp-round1-measures/ux-mcpp-1-long.json", + "bytes": 40389, + "sha256": "0199130e255f40d7ce6267dc21d5cd0a75c96b177de0739ffce92a98fd527ce2" + }, + { + "path": ".payload-cache/59-63-real-target-integration/first134-nightly-mcpp-round1-measures/ux-mcpp-1-warm.json", + "bytes": 6271, + "sha256": "2760e72904849fd3559bca31b6ef66f88d0fea08578067d53fad5590f0123199" + }, + { + "path": ".payload-cache/59-63-real-target-integration/first134-nightly-mcpp-round1-window-recompute.json", + "bytes": 4968, + "sha256": "cc4d01d09193788cee7d5e807ca7b01a6a1a4ae63d1c0498adefddeb1ddcbc39" + }, + { + "path": ".payload-cache/59-63-real-target-integration/first134-nightly-mcpp-round2-failure.log", + "bytes": 232582, + "sha256": "d1db485d6b32e26642ff5ffffbe5ce7387491e4aeaadc7556e18c68d0143f183" + }, + { + "path": ".payload-cache/59-63-real-target-integration/first134-nightly-mcpp-round2-measures/ux-mcpp-2-cold.json", + "bytes": 45661, + "sha256": "56ef639a79112ea26e22062a0e7430b77e7b7dda4c21501259ff459130e50afd" + }, + { + "path": ".payload-cache/59-63-real-target-integration/first134-nightly-mcpp-round2-measures/ux-mcpp-2-edits.json", + "bytes": 104749, + "sha256": "ea46380726f2f2aca481027db22453f4bb40ee11d016515d78eff54d652a6f01" + }, + { + "path": ".payload-cache/59-63-real-target-integration/first134-nightly-mcpp-round2-measures/ux-mcpp-2-faults.json", + "bytes": 38551, + "sha256": "388e57fbfcdbf0b891af3d93cdf0040341930961fb2bb21e550475307cc6749e" + }, + { + "path": ".payload-cache/59-63-real-target-integration/first134-nightly-mcpp-round2-measures/ux-mcpp-2-warm.json", + "bytes": 6268, + "sha256": "75c4353debba66424bf37c03705abf59e32558ab723801382bc334ad1e0165b2" + }, + { + "path": ".payload-cache/59-63-real-target-integration/first134-nightly-xlings-round1-failure.log", + "bytes": 558591, + "sha256": "6a17b4a3b813a4ae3108029d61718f316a94456ed5fa73af24e94c8ac273104d" + }, + { + "path": ".payload-cache/59-63-real-target-integration/first134-nightly-xlings-round1-measures/ux-xlings-1-cold.json", + "bytes": 42350, + "sha256": "05590c70b99a764fe1963c879478742902a62d74b4a7fc0a30a93599d1d2b72a" + }, + { + "path": ".payload-cache/59-63-real-target-integration/first134-nightly-xlings-round1-measures/ux-xlings-1-edits.json", + "bytes": 94362, + "sha256": "b34e41b266a3e7f4bac008f1266a67b3f5c1bc37771343dfe325b961417a81ae" + }, + { + "path": ".payload-cache/59-63-real-target-integration/first134-nightly-xlings-round1-measures/ux-xlings-1-faults.json", + "bytes": 37941, + "sha256": "3563080415e0c08263014edd8812647c76ac53efd9c501892dd45f7a2f82db02" + }, + { + "path": ".payload-cache/59-63-real-target-integration/first134-nightly-xlings-round1-measures/ux-xlings-1-long.json", + "bytes": 17571, + "sha256": "0d549972986e61d427a456ac03efee57d70530b257597f3c310557bb29866e13" + }, + { + "path": ".payload-cache/59-63-real-target-integration/first134-nightly-xlings-round1-measures/ux-xlings-1-warm.json", + "bytes": 15504, + "sha256": "13bf3e21ca4c30a6b6b814892d2bc70e43e931c0ecda2e3b359468c6b9e7dde6" + }, + { + "path": ".payload-cache/59-63-real-target-integration/first134-nightly-xlings-twocore-failure.log", + "bytes": 262169, + "sha256": "64c1559bf4f72f13335ca758a102c3ddb1da2876517af6ae70fad19e8aa0147d" + }, + { + "path": ".payload-cache/59-63-real-target-integration/first134-nightly-xlings-twocore-measures/ux-xlings-two-core-cold.json", + "bytes": 46922, + "sha256": "027d8c1e40a2ecb36e7383b544f19f6b4a7feb12535303228f6e0356b18de405" + }, + { + "path": ".payload-cache/59-63-real-target-integration/first134-nightly-xlings-twocore-measures/ux-xlings-two-core-edits.json", + "bytes": 95506, + "sha256": "39ed331aa81feee8002eb6d29671f880181a7e5a0f0d22bffc839afcdf5b6318" + }, + { + "path": ".payload-cache/59-63-real-target-integration/first134-nightly-xlings-twocore-measures/ux-xlings-two-core-faults.json", + "bytes": 38822, + "sha256": "f86b6f0630fdc576cfb931a74901b781a8bc847ab62196dffa34de1c78e98cbd" + }, + { + "path": ".payload-cache/59-63-real-target-integration/first134-nightly-xlings-twocore-measures/ux-xlings-two-core-warm.json", + "bytes": 9822, + "sha256": "2277aeb569def199b13a0c88e3e6ab2dd0d03b322fb12835669d49a5aa26606e" + }, + { + "path": ".payload-cache/59-63-real-target-integration/linux-std-preflight/compile_commands.json", + "bytes": 6576, + "sha256": "d1a53c60f32f2b3656755b0c1e385aca1662e34c86306a880fa275cd2604609d" + }, + { + "path": ".payload-cache/59-63-real-target-integration/linux-std-preflight/configure-host.log", + "bytes": 342, + "sha256": "94fa526f4185a270518f6ddb9dea94aaebea637d38c41a1fd63ff0051e6827a5" + }, + { + "path": ".payload-cache/59-63-real-target-integration/linux-std-preflight/no-std.json", + "bytes": 6598, + "sha256": "6df980b16a30bf041e61cc42384cd8bf253cf5f7bddab7a58e1689495e674627" + }, + { + "path": ".payload-cache/59-63-real-target-integration/linux-std-preflight/scenario.json", + "bytes": 5446, + "sha256": "2ea2c5472177e4bebd006612eb3c505b564cdf3d3fabe6415536273d322c164c" + }, + { + "path": ".payload-cache/59-63-real-target-integration/linux-std-preflight/validate-with-std.log", + "bytes": 197058, + "sha256": "7665e45d8957e782257227328223c14a7ada5ac7a50866848370f4e318871566" + }, + { + "path": ".payload-cache/59-63-real-target-integration/linux-std-preflight/with-std.json", + "bytes": 12683, + "sha256": "12365bad672cb919ab3422e8a5407904d65471c3b1ed505e9d87fb1e4d0b6567" + }, + { + "path": ".payload-cache/59-63-real-target-integration/nightly-state.json", + "bytes": 243844, + "sha256": "a17bef743615359c056cf3d7cb6343facf30d9b60fbf3ff753a4927e88d04b37" + }, + { + "path": ".payload-cache/59-63-real-target-integration/nightly-termux-119754344604-first.log", + "bytes": 34495, + "sha256": "cf42fc69cc0cb1b04517b43384971b5c933a75dc33f7c0fe7127a8c1bd784df7" + }, + { + "path": ".payload-cache/59-63-real-target-integration/nightly-zed-119754345027-first.log", + "bytes": 54344, + "sha256": "bcfc27cc1fce56e70fb7dd77f43a21130d84f2aab0c413b2969b754945daf984" + }, + { + "path": ".payload-cache/59-63-real-target-integration/runs.json", + "bytes": 50, + "sha256": "006e8a5825b3fc10f1822e0869e66785dd93e707f55ba66117e0291036cc2c48" + }, + { + "path": ".payload-cache/59-63-real-target-integration/state.json", + "bytes": 10251, + "sha256": "64e75fc77ce502e3fa82cc87a56daa5d1c4f0094babd39a3a3db2e6657b95c86" + }, + { + "path": ".payload-cache/59-63-real-target-integration/zed-lsp-directory-check/results.json", + "bytes": 729, + "sha256": "3ceea87f2c737d4c454dfa85fae9ff48945427b245a79260235dff05898496f2" + }, + { + "path": ".payload-cache/59-63-real-target-integration/zed-smoke-default-first/cache/logs/server-20261022-094609.434-c50e.log", + "bytes": 4060, + "sha256": "d49fe4db79643375c36b9d9bfd318abd2ba77ab6448c99d2ad7a64f9a6f77e9e" + }, + { + "path": ".payload-cache/59-63-real-target-integration/zed-smoke-default-first/lsp-in.log", + "bytes": 5773, + "sha256": "8f564462186b11e6ed4cf3a994b286aa08deab7b00cc48e2226bdaaad7939b9b" + }, + { + "path": ".payload-cache/59-63-real-target-integration/zed-smoke-default-first/lsp-out.log", + "bytes": 4735, + "sha256": "86bd826f4cae5ef24ec395f1a9845d71fdcde5b9a9de13476d8388948b2bdbde" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/binaries-dev.json", + "bytes": 1019, + "sha256": "c46fc47e40b782696bac9fa6a47e56cfa675ecba73f874ca8bc71bea6ca1f550" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/binaries-release.json", + "bytes": 1030, + "sha256": "d865871167b5a94e395314112357d7da6b05056a1db6c517ac63986335d5d3a4" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/build-workspace-dev.log", + "bytes": 8037, + "sha256": "c3a9bfecf465de7bdd00d7fe1cf5764b1642d32f63e701d1d5c39f8fcf0c1851" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/build-workspace-release.log", + "bytes": 8027, + "sha256": "9d7086afe2655c340e6e31ac7cb12d99fe6b2989fbe6e9f4d58f253fdfd2891e" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/build-workspaces-run.log", + "bytes": 176, + "sha256": "dec86b6722858b48fe5b54f2d8981d74fdcaba1fc26d2ab30b07a247635c3643" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/build-workspaces.py", + "bytes": 1778, + "sha256": "c09eb41fa290638a9da4703ff56017959eca985099e2b36e14425e183908e292" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/dev-test_answer_routing.log", + "bytes": 46071, + "sha256": "e4d9152a14eb4c9278d3fb593e9f919703a04cabff92a6a5c6bde10668b70a70" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/dev-test_scan.log", + "bytes": 3085, + "sha256": "c3412afe945bbca8b95d4b3441ca6c6b55f57650479bcf7562bba72c7c29ed79" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/dev-test_session-fixed.log", + "bytes": 1883, + "sha256": "09202cbc77e16c36c3fa0f2be730526bdf6bc2941fd2fe24790b0a263f519bd5" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/dev-test_session.log", + "bytes": 65036, + "sha256": "7e68feb1abd267b53b9a5b08b3827958d028b247c74c6089fba20d044d4cd058" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixture-profiles-run.log", + "bytes": 94, + "sha256": "0e3ea52a49a18d0224b6584ac233a76af203484596570a2167cf579928a03cf8" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixture-runs.json", + "bytes": 608, + "sha256": "4eec81075222edddf985d6459aa017f18826a57c799ee73495958c9ae4bcaabe" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-dev-run.log", + "bytes": 841, + "sha256": "686ec07c5c37a87141c67168ae9a322e9c7196d55bdecca6958ea83661089670" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-dev/build-program-broken.json", + "bytes": 7129, + "sha256": "ef52c1ccd366f762100efbac2ab9da5e5d803e7958d30afa2f39b53976c3407c" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-dev/build-program-gated.json", + "bytes": 4734, + "sha256": "2d338736bb143b3eeef30f2a030e15ecf70c358313aeb254a2f6f65fdb7e5a18" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-dev/build-program-manual.json", + "bytes": 4230, + "sha256": "1a0972e75bba5972218d45eb3b30f4385ecd4199ec249cdae6b9a85f072f8a91" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-dev/clangd-cannot-load.json", + "bytes": 4301, + "sha256": "b69c6c61826818ce8e09bd40fe3de2603c307c378800e7330a2743a903c33cef" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-dev/generated-module-negotiated.json", + "bytes": 3711, + "sha256": "8c7ab609ddfb913bcfae016b44331b00146b36a34d3150abe2597db942501eee" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-dev/generated-module.json", + "bytes": 3963, + "sha256": "3a3f167788953c051810cee6fe7a091f6a633bc7d50dbff1fbf924b2cf62ae50" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-dev/mcpp-emit-wait.json", + "bytes": 3355, + "sha256": "ddaba8ef148a0be5546eef64a7178fcb68291f3539a73cf3e383ba9db26b1f48" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-dev/mcpp-emit-watch@polling.json", + "bytes": 8146, + "sha256": "14c389b8a39b218e97deeb40073f6eb811a14174c11df20e9357f9e66ca8c216" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-dev/mcpp-polling-watch-inputs.json", + "bytes": 8744, + "sha256": "26dadb4680404de08ce72ec577607ea97895acf45a527faf5b2411bd9acff83b" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-dev/mcpp-producer-file-identity.json", + "bytes": 5177, + "sha256": "07248d38ff7f9dd72e9d29dc81eb14d93d5fd96234f4a125ed21009ab0696351" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-dev/mcpp-producer-identity.json", + "bytes": 7815, + "sha256": "cfc60a85506a687dd22326b9969fffd0b54b92ed2538ca1f1a05a2e87ba97f79" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-dev/mcpp-producer-identity@polling.json", + "bytes": 7818, + "sha256": "3539ef03427d5082c420f014b9ace52f2124030890958d46ef5286d3fbc5c481" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-dev/mcpp-producer-relative-path.json", + "bytes": 2657, + "sha256": "f653cae978a0b7fc9066c3642cee7d9b8adcd55ca7249655a1ec75ea534d44cc" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-dev/mcpp-target-auto-fallback.json", + "bytes": 8571, + "sha256": "2ed63f5b5ab5a184c6a542b096ea5c18df0b43099e3f53b47668df6030f5e23e" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-dev/mcpp-target-build-tool-off.json", + "bytes": 3193, + "sha256": "6037a0861a51d9cbef6bb7c9caba999091b6fe27de3dee8a9f0fe3e2fd1ef065" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-dev/mcpp-target-discovery-off.json", + "bytes": 2809, + "sha256": "85d629c4469c0ad98bde8cbe015a433a49c9b279f1dc4c3a3f8d2364d79838ee" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-dev/mcpp-target-explicit.json", + "bytes": 3210, + "sha256": "30b06e3e1531baf5888f173efef7626ac8fe49b80108b8645a7e31242b6d4b66" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-dev/mcpp-target-fallback-mismatch.json", + "bytes": 2351, + "sha256": "2931d64673f55829999d3d4e34f4d83dc7cf1e17f8c7255533f5c943424267a6" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-dev/mcpp-target-fallback-preserve.json", + "bytes": 6547, + "sha256": "226f914cc313010dc8eaaa1aadeecd9ac2f7465127872090b1f7fcce7f85ce0a" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-dev/mcpp-target-invalid.json", + "bytes": 2056, + "sha256": "cf580b150037f99293e0c6ebd7ab53498b9a98b2ad12a5ae469ddff2186cdbe1" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-dev/mcpp-target-live.json", + "bytes": 7270, + "sha256": "83365b5eb3c670c7b304705f22198d35963e24a968eb10141114ed1af803f0ef" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-dev/mcpp-target-mismatch-cold.json", + "bytes": 2934, + "sha256": "1e8372693abf57f4f10789adbe51fd4b6086dbd61952efd9d6e5ec82808143a3" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-dev/mcpp-target-mismatch.json", + "bytes": 4867, + "sha256": "19996e6643c92b23ad15631e15e018b163dc6f79e2460186d514909206962b06" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-dev/mcpp-target-project.json", + "bytes": 3092, + "sha256": "d76004854c186575c6a1225a5b1a8ae71dc707da2f3cda657a6aa59aec78fa9e" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-dev/mcpp-target-provider-excluded.json", + "bytes": 3188, + "sha256": "2fa36752f7f425cffd3a14c77357ec54eef67239afa75d60889b7fff12dce4d1" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-dev/mcpp-target-race.json", + "bytes": 6890, + "sha256": "62de78d1b6707baa69003aae99008a96a33b1bdd6745106a004c07edc9a104c4" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-dev/mcpp-target-selection.json", + "bytes": 11091, + "sha256": "043646860e3aee8a8c8e058a4b478a8f9ec5e5d2778d6fd8f5713f2811ca259e" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-dev/mcpp-target-selection@polling.json", + "bytes": 11090, + "sha256": "c970ab32219feb4553c456ca7d573698a84a18e56a47881e8b685ef4dce93c19" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-dev/mcpp-target-windows.json", + "bytes": 11719, + "sha256": "ed1dd2791de078eaed4c2e41f1f9cc7c2cfac79ba7a29884a3c704af6558154f" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-dev/mcpp-watched-source-structure.json", + "bytes": 39467, + "sha256": "cb270062908858b8cd16ffbf37c4add0d47c083fea2fea7af355d348623348ed" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-dev/mcpp-watched-source-structure.log", + "bytes": 84990, + "sha256": "9dcb112873a307e1e00bcaae2bd868a4b40e6a8589b92ffd7ff8c7425ddbacab" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-dev/provisional-no-prime.json", + "bytes": 2873, + "sha256": "83d3af88e76cfc8a0cdc8eda520b4f4f46e58957bd92cd524a2bd6b402c68a43" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-dev/summary.json", + "bytes": 27533, + "sha256": "d2e3da140560e014d0682b281677a3a2b3d5c20c7cd15da8fda04238169b5765" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-dev/untrusted.json", + "bytes": 3356, + "sha256": "3234c7d0683b13ea1a28a8dc4f54c94c5738dc2d42367837f34e76f0e9bddce2" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-release-run.log", + "bytes": 841, + "sha256": "f64cb6d53e7de50a73e3d570d63029ff0504ccd0104097e7d6efb9663146e301" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-release/build-program-broken.json", + "bytes": 7123, + "sha256": "4ee90b594683b3c014c9aa74a2aa0ce77b15ec31dc2e2d347250ae50b5d0b001" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-release/build-program-gated.json", + "bytes": 4731, + "sha256": "672a5e2ff11cf37d55fd13bbb77d08c9966e02dfc0f4082f73bdca8300d1eb37" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-release/build-program-manual.json", + "bytes": 4231, + "sha256": "36c55b617e2975ef8ba01c8068da94e7dffb9e55365e197f6846f94288a577b9" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-release/clangd-cannot-load.json", + "bytes": 4298, + "sha256": "17ee2b0f1bab2308ebb007593fbb3612a745f8f0553a1b2de93b084daac929b9" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-release/generated-module-negotiated.json", + "bytes": 3712, + "sha256": "87249f010c6a487c9263876eace5dd26e6beaba22d43c9836ade52dbe03eb593" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-release/generated-module.json", + "bytes": 3953, + "sha256": "e0a4d6d28402ad681b3b8d6d85e80677eccf8692003e97d712b25c2ec5e3a00c" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-release/mcpp-emit-wait.json", + "bytes": 3357, + "sha256": "7f7d813db4afb9df1c339e8e0f2c1edeb9a1d1e2a9168b3ce76b31b6f0bd379a" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-release/mcpp-emit-watch@polling.json", + "bytes": 8145, + "sha256": "a495be24fba968899d1c8aa906a76bb38cfe7508085d5aec8dbd5330c33b5d84" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-release/mcpp-polling-watch-inputs.json", + "bytes": 8741, + "sha256": "49ab5fe7e3084e2cf44b9f955f3f318be38e6701661567675320a33c6307b171" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-release/mcpp-producer-file-identity.json", + "bytes": 5175, + "sha256": "7864fd9fb20afac320a42ce72a81a0d1e456d8ceb00f5ace3bd46a424732a5d1" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-release/mcpp-producer-identity.json", + "bytes": 7813, + "sha256": "8601f2bba9d18b4b6f899ee33d6357b7aecdf8a6842efa78062a35c48c850f2e" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-release/mcpp-producer-identity@polling.json", + "bytes": 7816, + "sha256": "af3a9510ccd2e1ee7f06572944d813e228bdaf2cdf92ceef926b2c570299370f" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-release/mcpp-producer-relative-path.json", + "bytes": 2656, + "sha256": "1565ea396d85f4b1fe36c1456da8cfc1684eaf7c08bbf7fdfc2d9e63cae0d943" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-release/mcpp-target-auto-fallback.json", + "bytes": 8565, + "sha256": "94cc48cbe62a7918b6d356dc9e347979678f5716b706146eacdb53e7c30739e3" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-release/mcpp-target-build-tool-off.json", + "bytes": 3193, + "sha256": "306e93154f6db8c3107b0374a205f1b5dca75a2dc59b529f8705155237a3f28b" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-release/mcpp-target-discovery-off.json", + "bytes": 2803, + "sha256": "6cf225ae307ed5346821148be4404b4f211f3880aa78e7426d749fa09cc3c68b" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-release/mcpp-target-explicit.json", + "bytes": 3206, + "sha256": "8566abd43ff23a823e14f9b7842a0359b541241fc5eb4ab31c7450f69675d088" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-release/mcpp-target-fallback-mismatch.json", + "bytes": 2352, + "sha256": "95a8f474c70b210c8448bb428e650d181e4552cb4a197c08c2794fd95a0f2ec5" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-release/mcpp-target-fallback-preserve.json", + "bytes": 6546, + "sha256": "b8f013fe7999a218bebcd1bec44719e7a38958121b3011098c36b38b9fabe267" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-release/mcpp-target-invalid.json", + "bytes": 2056, + "sha256": "74cb93efc26280a4f5639f606d11626002621883c7d27831ad20151dde12c638" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-release/mcpp-target-live.json", + "bytes": 7270, + "sha256": "f07afab02d124634745c28815f490d94279aeec6539c0fc9249ad28ca4f70a73" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-release/mcpp-target-mismatch-cold.json", + "bytes": 2935, + "sha256": "10119b6a3d4c01c3ae8b71497e69ec43c0b8cff3c646f90b7432cea0096030d8" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-release/mcpp-target-mismatch.json", + "bytes": 4864, + "sha256": "37ab7f312c53d24c315a5795496d2a5b35ef7f94f0364ce7dcf2314592319873" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-release/mcpp-target-project.json", + "bytes": 3092, + "sha256": "642f500782011610bd65d36f255ddd809cf640a2cd3c45d5f9bf0c201bbe2027" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-release/mcpp-target-provider-excluded.json", + "bytes": 3186, + "sha256": "0e54fba344397ec7c933477eff66f7b7a6d33c09b69e53dd4ce42c76d786e314" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-release/mcpp-target-race.json", + "bytes": 6889, + "sha256": "1e8ad8794b06279da06e28afb3fda11a633cbc0fc84cda1be71c6ef4d2b747aa" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-release/mcpp-target-selection.json", + "bytes": 11082, + "sha256": "bfdb26430503f62b2bac3ebec902a412c5ab2efbe15bb08ebffa4bd0c6621a84" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-release/mcpp-target-selection@polling.json", + "bytes": 11077, + "sha256": "fc1c3d7979005b11693c8804f9446f17889f9f54c28af83984477d4d794e8ff2" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-release/mcpp-target-windows.json", + "bytes": 11711, + "sha256": "4fe3423e694ee258cd9581fdef4735a4ba7ca1170ff3eded431ad363c6697bf0" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-release/mcpp-watched-source-structure.json", + "bytes": 39534, + "sha256": "fbd98145203eedf4a78a25df76902e0a80c248cbdc87954c1b5d020c470b3714" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-release/mcpp-watched-source-structure.log", + "bytes": 86474, + "sha256": "26c9d5c13a95801a96e9afc0785ae989b1c435100680d2a3c3deb5df0105c07f" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-release/provisional-no-prime.json", + "bytes": 2876, + "sha256": "2b05287abb657cc5cf07c831fe0a74aa75205307fa1876a5aa3d3613858c813a" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-release/summary.json", + "bytes": 27929, + "sha256": "18d3ae41ce4df74272f525305e9971bd4496f149bd7a4e26b3b413df5257c758" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/fixtures-release/untrusted.json", + "bytes": 3358, + "sha256": "92240f14cc576e6c698ba13435c4b819339aa3a2b3b96b89c043c0ca23dbc2e4" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/focused-fixed-run.log", + "bytes": 268, + "sha256": "a2a569e8be7262a763f3d87c1d27edb75180a5adca324b2e8e71cd5fd5d6d197" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/focused-fixed-tests.json", + "bytes": 1317, + "sha256": "ae977cf3e4f4bc4abfd6ff9dc36d97afa4a1c6e199b7e36b25811f0523607cf0" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/focused-run.log", + "bytes": 149, + "sha256": "9427e4ad290ee8b4523d74dd7b555271b27ce54d3f57b0e15fbe7da68aa15aee" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/focused-terminal-summary.json", + "bytes": 557, + "sha256": "30fb56d5d550faa12eea4131e028ecbfa8129ec20fd5dc2d80f76172bcd943ff" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/focused-tests.json", + "bytes": 897, + "sha256": "348610787cc6fae8b222d8a83a24c4d3b64c6d3b14ae3ffd2cdf25148d80520d" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/release-test_answer_routing-fixed.log", + "bytes": 46071, + "sha256": "73789653ea8191418634018ae6ab59b1cebd37db75445d365665a00950f3c57e" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/release-test_scan-fixed.log", + "bytes": 3069, + "sha256": "f89600bf99420a2131eae7a3af62d1dcadc07b1bfa4a47730fe30b327665a979" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/release-test_session-fixed.log", + "bytes": 1883, + "sha256": "73fbd6a0af2cc4eddc952bdca8fbe57e424f661d8a7e90b3210951309bf8b3da" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/run-both-fixture-profiles.py", + "bytes": 785, + "sha256": "a90585b1510979c981c54583d7dbc093f4e859819ad2b5087f9e22236657fc87" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/run-fixtures.py", + "bytes": 2149, + "sha256": "ff1a741036d65200146b5f0d4c00b809e818e7a2db25d5ed1725b5bda05eaa8b" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/run-focused-fixed.py", + "bytes": 1227, + "sha256": "9f347ecd048c922d83d11de381f7b39f7dbcc2d2165d6d645e059e0dcc70900f" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/run-focused.py", + "bytes": 1079, + "sha256": "a8079145b1398a7ed2010acf753dfdd244a653e3e3096aba00dd49311f9371be" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/session-test-compilation-amendment.json", + "bytes": 422, + "sha256": "c2cca8bdb92cd94729610db3a7b15a712a4efb9359c143583933ac2c18e289e1" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/source-after-session-test-fix.json", + "bytes": 3570, + "sha256": "40b5c21b7359a46cafaa415980bce502b38315336c342d9dd64f545096654e13" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/source-before-validation-untracked.tar", + "bytes": 81920, + "sha256": "d13ef544b866a51191831e441bcfd3738a6201d9f9d2e4bda7f49f9376496276" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/source-before-validation.json", + "bytes": 3570, + "sha256": "bcd832cb12eecddb376d81311ddd05d581c1131433981e81d563749c56c49085" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/source-before-validation.patch", + "bytes": 96141, + "sha256": "095cd26959db08f4cb899c10ea89c5c86b5d6c9b9b2495bad9c8f47adca0919f" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/specs-current.log", + "bytes": 196502, + "sha256": "8ad2536e742c7be936800c9ffbc82a1c3193fc12a3dff556185a9b9f5d8b49dc" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/test-workspaces-run.log", + "bytes": 90, + "sha256": "306c8c82e5c95f96a51215cb342e024f0db648035792b77925b4ffc60851277c" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/test-workspaces.py", + "bytes": 1175, + "sha256": "40380e5d4cb0fe220fd712d2eaf2c1845a453bcb21dbf16faffb309d9dc53cc4" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/test_session-after-fix.cpp", + "bytes": 2900, + "sha256": "6e9178a9b23310628bc4af93042c1916f3ca3d5349613c2dce5df2fc5f379c0d" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/workspace-builds.json", + "bytes": 649, + "sha256": "1a7e62c1e722793cdc378ea198e8a56c481e5a44e04f010d48b83d13ab0d85a3" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/workspace-tests-dev.log", + "bytes": 133602, + "sha256": "ac877d6c69ade38af9e35c86ed9f4411b8817274affd58698fb1fe66f03acddb" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/workspace-tests-release.log", + "bytes": 133618, + "sha256": "949a56c74e88a1369fd9f1551fbdde0473658927bcacbe5fdabcffe54dd0e288" + }, + { + "path": ".payload-cache/59-63-source-watch-and-zed/workspace-tests.json", + "bytes": 895, + "sha256": "23af01284fe61b54ad87b33dcf30e6abbcf94cf040b345cbeed898c9c0e98ace" + }, + { + "path": ".payload-cache/59-63-watched-source-structure/macro-preprocess-proof.json", + "bytes": 424, + "sha256": "e9d0755a03b22d7a62950b74542ca1699fb3dd906d7cb16f70ba09d53d74666d" + }, + { + "path": ".payload-cache/59-63-watched-source-structure/old-product-exit.json", + "bytes": 1308, + "sha256": "a6936e5127399cd32a3959347818d0816e686d6c766dd855143c6fcb8af18121" + }, + { + "path": ".payload-cache/59-63-watched-source-structure/old-product-measure.json", + "bytes": 14365, + "sha256": "1d1e4ddc8da2f2646d96a0d81fec74601346774a30e4672c832beb85b4d95e9b" + }, + { + "path": ".payload-cache/59-63-watched-source-structure/old-product.log", + "bytes": 59181, + "sha256": "10758c08f3930e9e1985012f89f95ad4451da7e94b047180589f7795a4c67d28" + } + ], + "omitted": [ + "ELF executables, PCM/object/index/build caches, full workspace copies, 30MB generated module-memos.json seed, unbounded verbose server logs; selected original measures, first failures, stack/parser excerpts and provenance are retained" + ], + "historicalStateNotice": "59-63-current-state.json and real-target-integration/state.json are historical snapshots. HANDOFF.md and actual terminal JSON override stale live/pending labels." +} diff --git a/.agents/docs/2026-10-22-issues-59-63-evidence/portable-handoff/verify.py b/.agents/docs/2026-10-22-issues-59-63-evidence/portable-handoff/verify.py new file mode 100644 index 00000000..b329a878 --- /dev/null +++ b/.agents/docs/2026-10-22-issues-59-63-evidence/portable-handoff/verify.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +"""Verify the portable PR65 evidence; optionally restore its original relative paths.""" +import argparse +import hashlib +import json +from pathlib import Path +import tarfile + +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument('--extract', action='store_true') +parser.add_argument('--into', type=Path, default=Path.cwd()) +args = parser.parse_args() +base = Path(__file__).resolve().parent +metadata = json.loads((base / 'archive.json').read_text()) +manifest = json.loads((base / 'manifest.json').read_text()) +archive = base / metadata['archive'] +assert hashlib.sha256(archive.read_bytes()).hexdigest() == metadata['sha256'], 'archive digest differs' +entries = {item['path']: item for item in manifest['files']} +assert len(entries) == len(manifest['files']) == metadata['files'], 'duplicate manifest paths' +root = args.into.resolve() +with tarfile.open(archive, 'r:gz') as stream: + members = stream.getmembers() + assert len(members) == len(entries), 'archive count differs' + seen = set() + for member in members: + path = Path(member.name) + assert member.isfile() and not path.is_absolute() and '..' not in path.parts + assert path.parts[0] == '.payload-cache' and member.name not in seen + seen.add(member.name) + expected = entries[member.name] + assert member.size == expected['bytes'], 'size differs: ' + member.name + content = stream.extractfile(member).read() + assert hashlib.sha256(content).hexdigest() == expected['sha256'], 'digest differs: ' + member.name + destination = root / path + if args.extract and destination.exists(): + assert destination.is_file() and not destination.is_symlink() + assert hashlib.sha256(destination.read_bytes()).hexdigest() == expected['sha256'], 'refuse to overwrite: ' + str(destination) + if args.extract: + for member in members: + destination = root / member.name + assert destination.resolve().is_relative_to(root), 'path leaves extraction root' + if destination.exists(): + continue + destination.parent.mkdir(parents=True, exist_ok=True) + assert destination.parent.resolve().is_relative_to(root) + with destination.open('xb') as output: + output.write(stream.extractfile(member).read()) +print(json.dumps({'verifiedFiles': len(entries), 'archiveSHA256': metadata['sha256'], 'extracted': args.extract})) diff --git a/.agents/docs/2026-10-22-issues-59-63-evidence/reproduce.md b/.agents/docs/2026-10-22-issues-59-63-evidence/reproduce.md index 6888b018..df5191d7 100644 --- a/.agents/docs/2026-10-22-issues-59-63-evidence/reproduce.md +++ b/.agents/docs/2026-10-22-issues-59-63-evidence/reproduce.md @@ -166,3 +166,51 @@ Windows 原生环境仍运行完整 `ux-modules-windows` fixture,以原 `asked `mcpp test process` 和 `mcpp test process --profile release` 保留原 `busy CPU > 0.5 s`、`idle CPU < 0.2 s` 断言。旧测试固定等 2 秒 wall time,不能保证获得 0.5 秒 CPU;PR/nightly 的 macOS release 首轮分别只读到 0.09/0.08 秒增量。它们仍是真失败,不能据此排除产品 sampler 的问题。 子进程 READY 后由父进程先采样,再发送 START;子进程用独立 native process CPU 时钟确认累计至少 0.75 秒并发送 DONE,保持进程存活供父进程再次采样。父进程的工作与握手共用原 30 秒期限;sampler 每次调用另有既有界限(macOS 的 ps 为 2 秒),清理也可能越过工作期限,迟到采样仍失败。记录 native work 和两组 before/after;独立工作未完成、时钟不可用或 subject 读数不足均失败。Linux oracle 直接 native clock syscall,macOS 直接 libSystem 的 Darwin CPU clock(ID 12),不使用 subject `/proc`/`ps` 读取器;当前 OpenKAL 的 `std::clock` 实际走 monotonic,不能充当 CPU oracle。该改动只涉及测试,不更改产品 sampler 或性能输入/预算。 + +## 11. 发送队列的编码与快照 + +运行两种 profile 的完整 `mcpp test process`。原 nonreader 测试仍发送 64 条、每条 64 KiB,要求全部 send 在 2 秒内返回、stop 在 10 秒内结束;现在同时记录实际耗时。Release 39732244483 的原 macOS dev 失败日志保存在 `.payload-cache/59-63-delivery/release-macos-dev-first-failure.log`,不得以另一次通过或放宽时限替代它。 + +双 framing 的真实 echo 子进程回归检查 FIFO、发送后修改或销毁原 JSON 的快照独立性、UTF-8 replacement;退出的 peer 必须拒绝继续发送并完成 stop。新 writer 在锁外使用既有 dump/encode_frame,编码异常必须转换成连接失败,不能逃出线程;没有添加生产测试 hook 或伪造损坏 JSON 来触发 OOM。该异常隔离有代码审查,不能宣称已经完成原生 OOM 故障注入。 + +## 12. 同步 apply 的状态快照与时间线证据 + +以 `mcpp test answer_routing --profile release` 运行 Workspace 的确定性回归。先在旧 `072c230` 产品上只加入相同测试:同步 apply 回调组应恰好失败一项,输出尚未 commit 就发布的新 profile;无回调组及最终通知断言均通过。再应用产品补修,dev/release 都应通过。真实 clangd 的对应回调是 apply 内 `update_quarantine_issue_()` 调用 host;该对照证明状态提交边界,不证明原 CI 最忙一分钟的具体原因。保留两次日志、退出码、测试 patch 和 workspace 源码 SHA。 + +使用匹配 profile 的新 runner 执行 `mcpp test conformance`,其中 fake LSP 故意触发状态转换超预算,要求失败 measure 仍保留可重算的 trace/window。重复 progress 不算转换;restart 不计转换且重置前态;整数纳秒保留严格 60 秒边界。重新运行原 UX 场景时,先登记用途、产品/runner SHA、fixture/pin、CPU 和缓存谱系;不改 `maxChangesPerMinute=12`、采样或时长。首次失败保留,单次后续通过不能自行证明原因或稳定性;实际结果按源版本独立汇报。 + +## 13. 补全延迟的请求支持点 + +`MCPPLS_CONFORMANCE="$runner" mcpp test test_conformance --profile "$build_profile"` +覆盖原采样结束后才到达的回复、错误回复以及超过 256 个请求的截断。 +新 runner 在原统计完成后、额外 probe/report 前冻结回答集合,再输出请求编号、 +相对发送/回答纳秒、结果数量和 incomplete;不复制候选文本或路径。前 256 条之外 +的最慢成功/已回答请求单独保留,不能因截断丢失 max 的支持点。 +旧 runner 跑相同新增测试应失败,原有测试仍通过。 + +真实 UX 使用第 2 节原命令、输入、CPU 和 cache lineage,不增加轮询或修改预算。 +每个 typing check 的 serverStats 来自已有前后 report,recent distributions 是 +最近采样而非该 check 的精确增量;slow journal 有缺失和覆盖边界,不能据晚期 +report 的较快 p95 判断先前慢请求来自客户端。保留第一次 measure、完整 server +日志及 SHA;只有四阶段进程树全部退出后才分析日志,不在测量期间扫描 PCM。 + +## 14. 被条件编译排除的 import 与保存重载 + +完成对应 profile 构建后,运行 `mcpp test test_scan --profile "$build_profile"` +和 `mcpp test test_answer_routing --profile "$build_profile"`,然后执行完整 fixture: + +```bash +"$runner" run --server "$server" --payload "$audit_dir/released/payload" \ + --fixture conformance/fixtures/mcpp-watched-source-structure \ + --workspace-dir "$audit_dir/watched-source-workspace" \ + --cache-dir "$audit_dir/watched-source-cache" \ + --timeout 60 --measure "$audit_dir/watched-source.json" --verbose \ + > "$audit_dir/watched-source.log" 2>&1 +``` + +fixture 的 README 区分旧 28 项负对照与扩展后的 70 项。旧 `1341938` 对原 28 项 +有 11 次 emit、14 个失败检查,其中包含计数级联。扩展项保留原断言,并验证 +预处理指令变化、producer 失败后重试、生成期间结构再次变化以及 target/provider +变化。Workspace 单元回归另覆盖尚未送达的 watch 与 A→B→A 字节恢复;确认需要 +开始时结构及未变化的文件 stamp,不读取完成时结构来冒充 producer 已处理输入。 +这些是独立的正确性对照,不把它们当作历史 U15 13/min 的已证实归因。 diff --git a/.agents/docs/2026-10-22-issues-59-63-target-and-reliability-design.md b/.agents/docs/2026-10-22-issues-59-63-target-and-reliability-design.md index 1b5c33ed..26dddad8 100644 --- a/.agents/docs/2026-10-22-issues-59-63-target-and-reliability-design.md +++ b/.agents/docs/2026-10-22-issues-59-63-target-and-reliability-design.md @@ -254,28 +254,19 @@ nightly 的 proot-distro login 增加 `/mnt/scripts:/mnt/scripts` 绑定或将 - 上游 mcpp 基线 `0e79128a`:`src/build/compile_commands.cppm`(根 CDB 发布语义);`src/build/execute.cppm`(内部记录及限制);`src/build/prepare/toolchain.cpp`(默认目标);`src/cli.cppm`(emit 的 target/profile 参数)。这些是核查事实,不是新增稳定协议承诺。 - 当次复现临时材料位于 `/tmp/mcppls-issue-audit/`;临时目录不作为长期验收依赖,实施时转为仓库内 fixture。此前口头归因以本文 §2 的证据分级为准。 -## 13. 实施检查点(最终验收尚未完成) +## 13. 当前实施与验收状态 -- 已实现目标配置、自动提取、当前 producer 确认、动态切换及状态展示;补齐旧 CDB 回退校验和冷启动拒绝错误架构的竞态。 -- 已修 UTF-8 artifact 输出、Termux 脚本绑定及 polling watch 注册初期漏事件;有界 CDB 读取不依赖事前 stat 维持内存上限。 -- 当前 Linux 工作区检查点:dev/release 各 40 个单测执行体通过,22 组目标/身份/polling conformance 通过,规范和仓库检查通过。二进制 hash、范围及原始记录索引见 [验证简报](2026-10-22-issues-59-63-evidence/README.md#validation)。 -- 真实 VS Code E2E 通过:44 passing、1 项平台专用 pending;准确 release-profile 二进制 hash 与验证范围见简报。发布 VSIX 的安装/升级仍需单独验证。 -- U15 已以真实模块完成活动修复停顿判断。原 xlings cold/warm/edits/long 完整链路曾通过,U13 保留 600 秒、0.01 core、10% RSS 门槛;该次 CPU 为 0.007208 core,RSS 净变化为 -50.28%。另一个无文件/无索引的隔离实验定位到内置引擎未变 memo JSON 的周期性分配,属于已知未修局限;原 gate 通过不能证明该局限消失。详见 [简报的 memo 结论](2026-10-22-issues-59-63-evidence/README.md#memo)。最终重放中的失败也在简报中保留。 -- 原 mcpp 四核 cold/warm/edits 已通过;U8 在该链路撤销时新增实际构建为零。两逻辑核的完整 edits/faults 验收仍在进行,不以窄场景替代。不会通过放宽既有预算结束性能验收。 -- 尚未完成:全平台 CI、Termux 实际环境、性能最终重放、最终自审、squash 合入、release 与下载/升级验证。单 PR 将保持草稿直到验收完整。 +实现与验收分开记录。详细历史、原失败、复现命令与证据边界统一见[验证简报](2026-10-22-issues-59-63-evidence/README.md),不将完整原始日志放入 PR;先前检查点仍可从 Git 历史核对。 -后续自审补充:target-only 设置更新仅调度 mcpp 根,不重跑其他 provider;无关 provider 的模型比较不纳入 mcpp 选择身份。真实 LSP 回归在修前观察到额外 model-unchanged,修后不再出现。轮询生命周期对照:四次移除根目录,修前遗留四条线程,修后回到相同线程基线。对应原始记录由简报的索引定位,不在 PR 内展开 JSON。 - -只读交叉自审另发现 clang-cl 探测遗漏目标参数,会把正确 i686/ARM64 命令读成宿主 x64 并被新校验拒收。已补目标参数传递及有区分力的 host/target 单测,保留 cl.exe 原行为;补修后 Linux dev/release 各 40 个执行体通过,仍需最终提交的原生 CI 验收。 - -U8 两逻辑核完整重放随后确实未通过:undo 窗口有 28 次实际构建。原 release key 全部保留,最终 release 引擎也能复用它们。进一步 PCM 元数据与进程时序证明:旧 dev 引擎退出前读到了原地更新的 release CDB,产生 dev 依赖闭包与 release 命令混合的新 key。这里同时存在实验与驱动缺陷:runner 的 ready 判断在 dev 准备 `done=0 / wanted=140` 时已允许撤销;驱动在旧进程未完全退出时发布新 CDB。 - -补修任务:runner 在改变配置前、撤销前及撤销后检查明确的有限工作状态并有界等待,不用 CPU/RSS 或固定延时选择起点,保留等待活动与原零重建门槛;驱动对需要重启的计划保留最新待发布快照,旧进程 reaped 后才发布 CDB 和生成 prime 文件,再启动新进程。普通无需重启的增量更新仍直接发布。写入失败不得记成功或启动读取旧配置的新引擎,应保留待发布计划供明确重试。新增 native regression 后先以旧实现确认失败,再统一编译、测试并重跑原两逻辑核链路;目前这组补修尚未验收。 - -原两核完整链暴露另一个调度缺陷:普通模型 apply 重置 implementation sweep 完成状态后,没有待处理队列或新的 prime completion 时不会再调用 pump。900 s drain 最终同时看到 sweep 未完成、未调度,不能视为空闲。已在成功增量 apply 后调用既有 pump,遵循原工作/安静期判断;native 同计划重应用对照在旧实现有 4 个断言失败,补修 dev/release 各 40 个执行体及 engine 65 cases / 650 assertions 全通过,仍待完整新字节链及最终 CI。历史原链结果与独立 runner SIGSEGV 均保留在验证简报,不能由这一项修复替代验收。 - -最终自审补充恢复边界:首份或普通增量 CDB 单次写失败后,用户修复磁盘直接重启,不应依赖另一次 apply 才恢复接受流量和模块/implementation 元数据。start_process 在无接收流量状态下,发布保留的完整快照后重新应用,再启动;保留首次写失败提前返回,不让旧进程采用尚未发布的状态。新增只失败一次直接恢复的原生对照覆盖首份与增量计划,旧实现 5 个断言失败,补修 dev/release 各 40 执行体通过;其余最终验收继续。 - -原生 Windows CI 暴露 CDB 中 8.3 短名与工作区长名被当成不同项目的问题。目标提取、有效目标校验和模型 profile 现统一使用平台规范路径判断归属,保留原编译参数和嵌套项目边界。另修未信任 mcpp 根对扫描回退模型错误执行 producer 选择代数校验引起的无限重载;可信根的过期结果拒收保持有效。新 Workspace 对照在旧 guard 有 6 个断言失败;补修 dev/release 各 40 执行体通过(engine 67/693,target 9/92),24 组真实 LSP 通过。新字节原生 CI、完整性能与发布升级仍待验收,结果统一记入简报。 - -workspace 声明成员自身的别名身份也必须保留:原路径/一层 glob 匹配失败时,以声明成员目录的规范身份再核对,按一次归属快照缓存,保持嵌套与外部项目排除。对应旧实现行为对照为 8 个断言失败;正式结果与最终字节统一由简报更新。 +| 项目 | 当前事实 | 尚需完成 | +|---|---|---| +| 跟随 target、项目配置、producer 身份与缓存 | 已实现;包含 Windows 路径别名、workspace 成员归属、未信任根回退、错误架构与迟到结果拒收 | 最终提交的全部平台 CI;真实 mcpp Windows i686→x64→i686 生产链 | +| UTF-8、连接写入、状态一致性与恢复 | 已实现 UTF-8 artifact、队列写入、同步 apply 后发布状态、只读接口 rename 保护、implementation sweep 与磁盘恢复 | 新产品完整 workspace、LSP、编辑器与发布验证 | +| 源文件重复 discovery | 独立旧产品反例已证明;新实现确认有效 run-start 结构、文件 stamp、代数与 producer 身份,预处理文本仅按需采集,未知 import/header-unit 语法保守重查 | 完整 70 项 LSP fixture 与最终 CI;不能将其冒充历史 U15 的已证实归因 | +| Zed 与 Termux | 已修正 Termux 官方包源/内层绑定;新增 Zed file-as-folder 过滤及自身引擎初始化/进程检查;预装 PATH clangd 保留 Zed 默认独立启动 | 实际新 CI 的安装、原生 Zed 及产品检查,不用静态检查代替 | +| 新增原生回归 | dev/release 均通过 scan 23 cases/146 assertions、Workspace 29/399、Session 3/12;Session 首次测试编译错误已修正并保留日志 | 本地两 profile 全 workspace 各 58 程序及 LSP 各 32 组已通过,实际结果见交接;新 commit 仍需远端验证 | +| 平台与性能 | `1341938` 曾有原本地九阶段 53 项与 PR CI 通过,但其 nightly 有四项真实失败;`e01e1c1` CI/nightly 仍失败。一次新 trace 诊断为 25 项/1 失败,U7a 受保存前 AST 崩溃后的隔离影响 | 原预算不变的新产品完整性能链与所有 CI;历史慢请求、RSS 增长和底层 AST 崩溃未被完整归因 | +| 引擎范围 | [memo 提案](2026-10-22-issues-59-63-evidence/engine-memory-proposal.md) 仅供本地 review;周期无变化仍构造完整 JSON 的缺陷已证明,未修改或发布引擎 | 当前仅同步 mcppls 工作和交接,不实施引擎修复;不发布 issue 评论或变更状态,不承诺修复全部 RSS 增长 | +| 合入与交付 | PR64 已合入,发布预检后补修集中在 draft PR65;0.0.17 尚未发布,两个原 0.0.16 升级环境保持原状 | 最终 review、精确提交 squash、新 main CI/draft release、12 个实际下载资产、本地运行及两个原 profile 升级、正式发布后下载/OpenVSX 校验,最后提供上传目录 | + +依赖顺序仍为:源码冻结 → 两 profile 全量验证 → 最终源版本 CI/nightly 与完整性能验收 → 最终 review/squash → 新 main 与候选 release 验证 → 实际升级 → 正式发布及下载回验。不能以一次通过覆盖原失败,不能通过预算、等待起点、缓存谱系或冷却缩短制造通过。 diff --git a/.agents/docs/design.md b/.agents/docs/design.md index ceabdb66..ffe52bb9 100644 --- a/.agents/docs/design.md +++ b/.agents/docs/design.md @@ -168,6 +168,15 @@ nothing (#30). The build tool's hard deadline follows how long it took last time time, then three times the last run, 1 to 10 minutes), and a saved source reruns it only when its module structure changes. +A source's lexical imports can differ from the producer's preprocessed dependency graph. +After a successful, valid answer, the workspace can acknowledge the watched source structure +captured before that run, including its preprocessor directive lines. A body-only save of that +acknowledged structure need not rediscover the same graph. Confirmation is session-local and +belongs to the current model generation and native producer identity; changed file stamps, +pending reloads, failed/rejected answers and unknown preprocessing retain the conservative path. +Only already watched source paths are captured, under a shared 32 MiB read bound. Source +creation/deletion and manifest, target or producer changes keep their existing reload behavior. + **Observability.** One occurrence must be enough to see why. clangd logs at `info` into a ring in memory; crashes, stuck or spinning clangd, files set aside, backed-off restarts and a workaround whose premise is seen broken each write an incident under the workspace's cache (the newest twenty, diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ddb6491b..d9551779 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -321,7 +321,7 @@ jobs: - platform: linux-x64 part: 1 of 2 os: ubuntu-24.04 - fixtures: mcpp-split mcpp-partition-definition mcpp-all-cppm verify-changes mingw mcpp-split-gcc mcpp-watch multi-root mcpp-llvm mcpp-build-programs mcpp-watch@polling mcpp-emit mcpp-rules-generated s1-two-sets compdb-lto-msvc failure-at-base@vscode generated-module-negotiated typing-import typing-import-spin workaround-canaries typing-autosave module-edit-autosave partial-scan-standins compdb-rejected-command compdb-mixed-standards mcpp-emit-partial mcpp-emit-partial-download compdb-midwrite xmake-basic xmake-late-config xmake-user-mode xmake-no-standard xmake-needs-download ux-modules-qt mcpp-target-selection mcpp-target-explicit mcpp-target-project mcpp-target-invalid mcpp-target-windows mcpp-producer-identity mcpp-target-race mcpp-target-live mcpp-target-mismatch mcpp-target-mismatch-cold mcpp-target-build-tool-off mcpp-target-provider-excluded mcpp-target-discovery-off mcpp-target-auto-fallback mcpp-producer-file-identity mcpp-producer-relative-path mcpp-target-selection@polling mcpp-producer-identity@polling mcpp-polling-watch-inputs@polling mcpp-target-fallback-mismatch mcpp-target-fallback-preserve + fixtures: mcpp-split mcpp-partition-definition mcpp-all-cppm verify-changes mingw mcpp-split-gcc mcpp-watch multi-root mcpp-llvm mcpp-build-programs mcpp-watch@polling mcpp-emit mcpp-rules-generated s1-two-sets compdb-lto-msvc failure-at-base@vscode generated-module-negotiated typing-import typing-import-spin workaround-canaries typing-autosave module-edit-autosave partial-scan-standins compdb-rejected-command compdb-mixed-standards mcpp-emit-partial mcpp-emit-partial-download compdb-midwrite xmake-basic xmake-late-config xmake-user-mode xmake-no-standard xmake-needs-download ux-modules-qt mcpp-target-selection mcpp-target-explicit mcpp-target-project mcpp-target-invalid mcpp-target-windows mcpp-producer-identity mcpp-target-race mcpp-target-live mcpp-target-mismatch mcpp-target-mismatch-cold mcpp-target-build-tool-off mcpp-target-provider-excluded mcpp-target-discovery-off mcpp-target-auto-fallback mcpp-producer-file-identity mcpp-producer-relative-path mcpp-target-selection@polling mcpp-producer-identity@polling mcpp-polling-watch-inputs@polling mcpp-target-fallback-mismatch mcpp-target-fallback-preserve mcpp-watched-source-structure - platform: linux-x64 part: 2 of 2 os: ubuntu-24.04 @@ -333,18 +333,18 @@ jobs: - platform: darwin-arm64 os: macos-14 extras: true - fixtures: inferred inferred-bom typing-pause module-fails-in-import save-plan build-discovery-off engine-none module-faults untrusted mcpp-llvm mcpp-build-programs mcpp-split mcpp-partition-definition mcpp-all-cppm verify-changes mcpp-watch multi-root failure-at-base clangd-cannot-load failure-at-base@zed generated-module generated-module-old-mcpp generated-module-negotiated typing-import typing-import-spin workaround-canaries completion-keywords diagnostic-bundle typing-autosave module-edit-autosave partial-scan-standins clangd-crash-context mcpp-emit-wait provisional-no-prime inferred-cxx26 reset-cache cache-budget include-cleaner-modules no-modules tidy-const-views format-explicit build-program-broken build-program-gated build-program-manual mcpp-target-selection mcpp-target-explicit mcpp-target-project mcpp-target-invalid mcpp-target-windows mcpp-producer-identity mcpp-target-race mcpp-target-live mcpp-target-mismatch mcpp-target-mismatch-cold mcpp-target-build-tool-off mcpp-target-provider-excluded mcpp-target-discovery-off mcpp-target-auto-fallback mcpp-producer-file-identity mcpp-producer-relative-path mcpp-polling-watch-inputs@polling mcpp-target-fallback-mismatch mcpp-target-fallback-preserve + fixtures: inferred inferred-bom typing-pause module-fails-in-import save-plan build-discovery-off engine-none module-faults untrusted mcpp-llvm mcpp-build-programs mcpp-split mcpp-partition-definition mcpp-all-cppm verify-changes mcpp-watch multi-root failure-at-base clangd-cannot-load failure-at-base@zed generated-module generated-module-old-mcpp generated-module-negotiated typing-import typing-import-spin workaround-canaries completion-keywords diagnostic-bundle typing-autosave module-edit-autosave partial-scan-standins clangd-crash-context mcpp-emit-wait provisional-no-prime inferred-cxx26 reset-cache cache-budget include-cleaner-modules no-modules tidy-const-views format-explicit build-program-broken build-program-gated build-program-manual mcpp-target-selection mcpp-target-explicit mcpp-target-project mcpp-target-invalid mcpp-target-windows mcpp-producer-identity mcpp-target-race mcpp-target-live mcpp-target-mismatch mcpp-target-mismatch-cold mcpp-target-build-tool-off mcpp-target-provider-excluded mcpp-target-discovery-off mcpp-target-auto-fallback mcpp-producer-file-identity mcpp-producer-relative-path mcpp-polling-watch-inputs@polling mcpp-target-fallback-mismatch mcpp-target-fallback-preserve mcpp-watched-source-structure # No mcpp on the arm64 runner: the server and mock producers are cross-built. Most # fixtures use the semantic kit; the SDK-free target fixtures probe distribution clang, # installed below. Module faults, a corrupt payload and polling are included. - platform: linux-arm64 os: ubuntu-24.04-arm cross-tools: true - fixtures: inferred inferred-bom typing-pause module-fails-in-import build-discovery-off engine-none module-faults untrusted payload-corrupt clangd-cannot-load failure-at-base watch-polling typing-import typing-import-spin workaround-canaries completion-keywords diagnostic-bundle typing-autosave module-edit-autosave partial-scan-standins clangd-crash-context inferred-cxx26 reset-cache cache-budget include-cleaner-modules no-modules tidy-const-views mcpp-target-selection mcpp-target-explicit mcpp-target-project mcpp-target-invalid mcpp-target-windows mcpp-producer-identity mcpp-target-race mcpp-target-live mcpp-target-mismatch mcpp-target-mismatch-cold mcpp-target-build-tool-off mcpp-target-provider-excluded mcpp-target-discovery-off mcpp-target-auto-fallback mcpp-producer-file-identity mcpp-producer-relative-path mcpp-polling-watch-inputs@polling mcpp-target-fallback-mismatch mcpp-target-fallback-preserve + fixtures: inferred inferred-bom typing-pause module-fails-in-import build-discovery-off engine-none module-faults untrusted payload-corrupt clangd-cannot-load failure-at-base watch-polling typing-import typing-import-spin workaround-canaries completion-keywords diagnostic-bundle typing-autosave module-edit-autosave partial-scan-standins clangd-crash-context inferred-cxx26 reset-cache cache-budget include-cleaner-modules no-modules tidy-const-views mcpp-target-selection mcpp-target-explicit mcpp-target-project mcpp-target-invalid mcpp-target-windows mcpp-producer-identity mcpp-target-race mcpp-target-live mcpp-target-mismatch mcpp-target-mismatch-cold mcpp-target-build-tool-off mcpp-target-provider-excluded mcpp-target-discovery-off mcpp-target-auto-fallback mcpp-producer-file-identity mcpp-producer-relative-path mcpp-polling-watch-inputs@polling mcpp-target-fallback-mismatch mcpp-target-fallback-preserve mcpp-watched-source-structure - platform: win32-x64 part: 1 of 2 os: windows-2022 - fixtures: mcpp-split mcpp-partition-definition mcpp-all-cppm verify-changes mcpp-split-msvc cmake-msvc-std compdb-clangxx-msvc-std multi-root compdb-clang-cl-std mcpp-llvm-msvc mcpp-build-programs failure-at-base@vscode typing-import typing-import-spin workaround-canaries typing-autosave module-edit-autosave partial-scan-standins ux-modules-windows mcpp-target-selection mcpp-target-explicit mcpp-target-project mcpp-target-invalid mcpp-target-windows mcpp-producer-identity mcpp-target-race mcpp-target-live mcpp-target-mismatch mcpp-target-mismatch-cold mcpp-target-build-tool-off mcpp-target-provider-excluded mcpp-target-discovery-off mcpp-target-auto-fallback mcpp-producer-file-identity mcpp-producer-relative-path mcpp-polling-watch-inputs@polling mcpp-target-fallback-mismatch mcpp-target-fallback-preserve + fixtures: mcpp-split mcpp-partition-definition mcpp-all-cppm verify-changes mcpp-split-msvc cmake-msvc-std compdb-clangxx-msvc-std multi-root compdb-clang-cl-std mcpp-llvm-msvc mcpp-build-programs failure-at-base@vscode typing-import typing-import-spin workaround-canaries typing-autosave module-edit-autosave partial-scan-standins ux-modules-windows mcpp-target-selection mcpp-target-explicit mcpp-target-project mcpp-target-invalid mcpp-target-windows mcpp-target-windows-real mcpp-producer-identity mcpp-target-race mcpp-target-live mcpp-target-mismatch mcpp-target-mismatch-cold mcpp-target-build-tool-off mcpp-target-provider-excluded mcpp-target-discovery-off mcpp-target-auto-fallback mcpp-producer-file-identity mcpp-producer-relative-path mcpp-polling-watch-inputs@polling mcpp-target-fallback-mismatch mcpp-target-fallback-preserve mcpp-watched-source-structure - platform: win32-x64 part: 2 of 2 os: windows-2022 @@ -534,6 +534,12 @@ jobs: case "$fixture" in cmake-clang-bdb) path_prefix="$RUNNER_TEMP/cmake-4.4.2/bin:" ;; cmake-msvc-bdb|cmake-clang-cl) fixture_extra=(--msvc-env "$RUNNER_TEMP/msvc-bdb.env") ;; + # Preserve the real producer's first CDBs and server log independently + # of the diagnostic rerun: no reconstructed or rewritten target entries. + mcpp-target-windows-real) + local evidence="$RUNNER_TEMP/mcpp-target-windows-real-first" + case " $* " in *" --verbose "*) evidence="$RUNNER_TEMP/mcpp-target-windows-real-diagnostic" ;; esac + fixture_extra+=(--workspace-dir "$evidence/workspace" --cache-dir "$evidence/cache") ;; # usable plan W9.3: the fallback runs only when the client never declares # didChangeWatchedFiles.dynamicRegistration. NAME@polling runs fixture NAME that way. watch-polling|*@polling) fixture_extra+=(--no-dynamic-watch) ;; @@ -571,6 +577,15 @@ jobs: exit 1 fi + # Preserve the first producer envelopes even when schema validation stops + # the job before any LSP fixture starts. + - uses: actions/upload-artifact@v7 + if: always() && !matrix.cross-tools + with: + name: producer-envelopes-${{ matrix.platform }}-${{ matrix.part || 'full' }} + path: ${{ runner.temp }}/mcpp-emit/*.json + if-no-files-found: ignore + - uses: actions/upload-artifact@v7 if: always() with: @@ -578,6 +593,17 @@ jobs: path: ${{ runner.temp }}/conformance-measures/*.json if-no-files-found: ignore + - uses: actions/upload-artifact@v7 + if: always() && contains(matrix.fixtures, 'mcpp-target-windows-real') + with: + name: real-mcpp-target-cdb-${{ matrix.platform }} + path: | + ${{ runner.temp }}/mcpp-target-windows-real-*/workspace/mcpp-target-windows-real/recorded-*.json + ${{ runner.temp }}/mcpp-target-windows-real-*/workspace/mcpp-target-windows-real/compile_commands.json + ${{ runner.temp }}/mcpp-target-windows-real-*/cache/logs/*.log + ${{ runner.temp }}/mcpp-target-windows-real-*/cache/workspaces/**/model.mcpp.json + if-no-files-found: ignore + # Issue #23 fix plan F18: the bundles diagnostic-bundle exported on this runner's own account, whose # name is in every path of it (runneradmin and RUNNER~1 on Windows). The fixture already held each # to its manifest and found no user name and no home directory in it; here another program's zip diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 3d7df733..1ff5709b 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -178,8 +178,12 @@ jobs: EOF cat > "$RUNNER_TEMP/in-termux.sh" <<'EOF' set -eu - yes | pkg update -y || true - pkg install -y proot-distro + # Use the official seed instead of pkg's randomly selected mirrors: + # https://github.com/termux/termux-packages/wiki/Mirrors + # A stale mirror previously advertised a proot-distro package it no longer served. + printf '%s\n' 'deb https://packages.termux.dev/apt/termux-main stable main' > "$PREFIX/etc/apt/sources.list" + apt-get update + apt-get install -y proot-distro proot-distro install debian proot-distro login debian --bind /mnt/work:/work --bind /mnt/scripts:/mnt/scripts -- bash -c 'test -r /mnt/scripts/in-debian.sh && exec bash /mnt/scripts/in-debian.sh' EOF @@ -318,6 +322,13 @@ jobs: --fixture conformance/fixtures/${{ matrix.fixture }} --workspace-dir "$work/workspace" --cache-dir "$work/$cache" \ --stage "$name" --timeout 900 --measure "$results/${{ matrix.fixture }}-${{ matrix.round }}-$name.json" "$@"; then failed="$failed $name" + # Keep the original failed stage's complete server logs before later + # stages rotate them. This runs after the measured process exits. + if [ -d "$work/$cache/logs" ]; then + mkdir -p "$results/failed-$name/logs" + cp -a "$work/$cache/logs/." "$results/failed-$name/logs/" || + echo "::warning::could not preserve all $name server logs" + fi fi echo "::endgroup::" } diff --git a/.github/workflows/zed-smoke.yml b/.github/workflows/zed-smoke.yml index 1ebb827f..6f066556 100644 --- a/.github/workflows/zed-smoke.yml +++ b/.github/workflows/zed-smoke.yml @@ -50,7 +50,11 @@ jobs: set -euo pipefail sudo apt-get update sudo apt-get install -y --no-install-recommends xvfb mesa-vulkan-drivers libvulkan1 libegl1 libgl1 libgbm1 \ - libxkbcommon0 libxkbcommon-x11-0 libwayland-client0 libx11-xcb1 libxcb1 libfontconfig1 libasound2t64 + libxkbcommon0 libxkbcommon-x11-0 libwayland-client0 libx11-xcb1 libxcb1 libfontconfig1 libasound2t64 clangd + # Zed's default lookup tries PATH before downloading from GitHub. Keep its default + # server selection and require the independently started process, without testing downloads. + command -v clangd + clangd --version - name: Zed ${{ env.ZED_VERSION }} id: zed-cache uses: actions/cache@v6 @@ -81,7 +85,7 @@ jobs: MCPPLS_PAYLOAD: ${{ github.workspace }}/payload MCPPLS_DEVTOOLS: ${{ github.workspace }}/stage/mcppls-devtools ZED_SMOKE_DIR: ${{ runner.temp }}/zs - # Zed downloads its clangd from GitHub on first use; here that has to work. + # The preinstalled PATH clangd must be started by Zed beside mcppls. ZED_SMOKE_REQUIRE_CLANGD: '1' run: xvfb-run -a -s '-screen 0 1280x1024x24' editors/zed/tests/smoke.sh ${{ matrix.mode }} - uses: actions/upload-artifact@v7 @@ -92,6 +96,7 @@ jobs: ${{ runner.temp }}/zs/*.log ${{ runner.temp }}/zs/zed.out ${{ runner.temp }}/zs/cache/logs + ${{ runner.temp }}/zs/cache/bundles ${{ runner.temp }}/zs/home/.local/share/zed/logs if-no-files-found: ignore retention-days: 7 diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b9b109e..25636cc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,22 @@ The bundled engine remains [mcppls-clangd 0.0.16](https://github.com/Sunrisepeak ### Reliability and measurements +- A successful producer description can acknowledge a watched source's lexical imports even + when preprocessing excludes some of them from the active graph. Body-only saves then avoid + repeatedly describing the same structure; changed directives, failures and in-flight edits + retain conservative rediscovery. +- An existing source file named as an LSP workspace folder is ignored in favor of the client's + valid directory roots. Zed file worktrees no longer make clangd start with a source file as + its working directory. +- Child-connection messages are encoded by the writer, after an owned snapshot is queued. + Large JSON serialization no longer runs on the caller's event loop; FIFO order, framing and + UTF-8 replacement are preserved. A writer encoding failure ends the connection without terminating the server. +- A workspace publishes its status after every participating engine finishes applying a plan. + Synchronous engine callbacks no longer expose the new profile before that application completes. + Timeline measurements retain compact transition timestamps and the busiest minute's evidence. +- Typing measurements retain bounded request timestamps and the slowest successful reply, + together with the existing before/after server reports. Sampling, timeout and latency budgets + are unchanged; truncated or recent evidence is identified explicitly. - Renaming a symbol owned by mcpp's read-only engine interface is refused even from project references when the compiler returns only project edits. Definition and declaration ownership are checked before edits; unresolved or stale ownership returns an error without partial changes. diff --git a/conformance/README.md b/conformance/README.md index 824d47f1..de444006 100644 --- a/conformance/README.md +++ b/conformance/README.md @@ -44,6 +44,7 @@ checks fail at once with that reason instead of each waiting out its timeout. | `mcpp-watch` | S2 5 with mcpp itself: a new module interface, a broken `mcpp.toml` and its repair are inputs mcpp names in `watch`; the broken manifest keeps the last model, `degraded` with `model-stale` and mcpp's own `MCPP_BUILD_DATABASE_PLAN_FAILED` (S2-5-9). CI also runs it as `mcpp-watch@polling` on Linux | | `mcpp-target-selection` | A host build-program context listed first cannot replace the project's i686 profile. Root CDB target switches, deletion and recreation reach producer argv and the engine; same-target rewrites do not invoke the producer | | `mcpp-target-windows` | Windows i686/x64 target switching, including conditional module declarations reporting 32-bit semantics, without an SDK or standard-library dependency; runs on every host | +| `mcpp-target-windows-real` | On Windows, real mcpp and LLVM publish complete i686/x64 CDBs. A single session follows the unmodified databases i686 → x64 → i686, overriding the x64 project default; actual engine commands, conditional module constants, navigation and diagnostics are checked | | `mcpp-target-explicit`, `mcpp-target-project` | Explicit workspace target overrides the root CDB; project mode delegates to mcpp defaults; subsequent CDB changes do not invoke either producer again | | `mcpp-target-invalid` | An invalid explicit setting produces a configuration issue before any protocol query or producer invocation | | `mcpp-target-build-tool-off` | A disabled producer keeps the existing i686 CDB and reports the explicit x64 request as pending; no mcpp command runs | @@ -51,6 +52,7 @@ checks fail at once with that reason instead of each waiting out its timeout. | `mcpp-target-auto-fallback` | Conflicting targets and unresolved response files fall back with distinct reasons; a reason change alone does not run the producer; unique targets recover | | `mcpp-producer-identity` | An upgraded producer behind the same launcher reports its actual envelope version while protocol capabilities are cached; a changed launcher pin is watched by the provider and re-queries protocol. Also run with polling on Linux | | `mcpp-polling-watch-inputs@polling` | Watch registration retains existing non-source glob baselines, while external concrete inputs and explicitly declared files under ignored target/ directories still trigger reloads | +| `mcpp-watched-source-structure` | A successful producer answer acknowledges an unchanged lexical import excluded by preprocessing; body-only and duplicate saves avoid repeated emit, while directives, failure/retry, in-flight edits, manifest, target and native producer changes retain rediscovery | | `mcpp-target-fallback-mismatch`, `mcpp-target-fallback-preserve` | Existing-CDB fallback validates the requested target; a disabled producer preserves usable cold CDB data but cannot replace a valid model with mismatched commands | | `mcpp-producer-file-identity` | A copied native producer replaced at the same path triggers the stat-only executable timer and re-queries identity, without a build/source event | | `mcpp-producer-relative-path` | A configured relative producer path resolves against the project root and records the actual absolute invocation | diff --git a/conformance/fixtures/mcpp-target-windows-real/README.md b/conformance/fixtures/mcpp-target-windows-real/README.md new file mode 100644 index 00000000..8bf7ee9f --- /dev/null +++ b/conformance/fixtures/mcpp-target-windows-real/README.md @@ -0,0 +1,72 @@ +# Real Windows target following + +This fixture requires Windows, Visual Studio C++ tools and Windows SDK, real mcpp, +and `llvm@22.1.8`. Supply the MSVC environment captured by the Windows conformance job: + +```bash +stage/mcppls-conformance.exe run --server payload/bin/mcppls.exe --payload payload \ + --fixture conformance/fixtures/mcpp-target-windows-real \ + --msvc-env "$RUNNER_TEMP/msvc.env" \ + --workspace-dir "$RUNNER_TEMP/target-real/workspace" \ + --cache-dir "$RUNNER_TEMP/target-real/cache" --keep --verbose --timeout 180 +``` + +`MCPPLS_REAL_MCPP` can select a real mcpp executable. The scenario never fixes the +server target. Its preparation runs real configure-only builds for i686 and x64, +copies each complete published CDB unchanged, and configures i686 again. During +one LSP session it publishes these original databases in the order 32 → 64 → 32. +The manifest default is x64, so the initial 32-bit result proves that auto mode +uses the published CDB rather than that default. Checks cover the requested and +effective targets, actual engine command, architecture-dependent module constant, +module navigation, and diagnostics in both source files. The constant also has a +pointer-size assertion, making inconsistent architecture preprocessing visible. +The module imports `std` and uses `std::is_same_v`, so the generated database +also exercises the real installed standard-library module set. + +The empty `[target.i686-windows-msvc]` section is a legitimate project custom +target declaration: mcpp's built-in target list does not include this spelling. +It does not imply that every compiler triple is a built-in mcpp target. Real +mcpp versions 2026.9.30.2 and 2026.10.21.1 accepted the canonical +`i686-pc-windows-msvc` spelling supplied to `emit build-database` with this +manifest, resolving it to the declared target before checking SDK availability. + +The strict expected CDB flags are `--target=i686-pc-windows-msvc` and +`--target=x86_64-pc-windows-msvc`. In the pinned mcpp v2026.9.30.2 sources, +`src/build/prepare/toolchain.cpp` derives `crossTargetFlag` using `llvm_triple()` +whenever a target is explicitly selected with a Clang driver; this condition +does not require the selected target to differ from the host; +`modules/toolchain-model/src/triple.cppm` adds `-pc-windows-msvc`; +`src/toolchain/hostflags.cppm` passes that token into the compile flags; and +`src/build/compile_commands.cppm` writes those tokens into `arguments`. +Native execution must still verify the generated databases and effective driver +facts. This fixture contains no build program and does not test host build +context filtering. + +Keep the complete `recorded-i686.json`, `recorded-x86_64.json`, final +`compile_commands.json`, model reports and logs as native CI artifacts. For +replay evidence, compute hashes without altering any rows: + +```bash +cmake -E sha256sum "$RUNNER_TEMP/target-real/workspace/mcpp-target-windows-real/recorded-i686.json" \ + "$RUNNER_TEMP/target-real/workspace/mcpp-target-windows-real/recorded-x86_64.json" +``` + +Local preflight on Linux reached real LLVM target resolution for both targets, +then stopped because Visual Studio tools, MSVC STL and Windows SDK were absent. +The pinned producer also accepted the canonical i686 alias before reporting the +same missing SDK. No CDB was generated locally, so there are no local generated +CDB hashes or full end-to-end pass claims. Windows CI is the native acceptance +gate; a Linux SDK failure does not establish that gate has passed. + +A separate Linux preflight with pinned mcpp 2026.9.30.2 and explicit +`x86_64-linux-gnu` confirmed that this project without `import std` produces +only its project set. With the standard import and type assertion present, real +`emit build-database` produces both the project set and `mcpp:std` (two units), +and the existing specification validator accepts the complete envelope. This +checks the standard-library contract on Linux, not the Windows target chain. + +The same Linux preflight ran real `build --configure-only --target +x86_64-linux-gnu`: both project CDB entries retained the explicit +`--target=x86_64-unknown-linux-gnu` token despite selecting the host architecture. +This supports the explicit-host flag path; native Windows CDB spelling remains +a strict CI assertion. diff --git a/conformance/fixtures/mcpp-target-windows-real/mcpp.toml b/conformance/fixtures/mcpp-target-windows-real/mcpp.toml new file mode 100644 index 00000000..f39b8e7f --- /dev/null +++ b/conformance/fixtures/mcpp-target-windows-real/mcpp.toml @@ -0,0 +1,16 @@ +[package] +name = "target-real" +version = "0.1.0" +description = "Real mcpp Windows analysis target follows the published CDB" +license = "Apache-2.0" + +[toolchain] +default = "llvm@22.1.8" + +# The initial published CDB deliberately overrides this project default with i686. +[build] +target = "x86_64-windows-msvc" + +# mcpp's built-in target table does not list 32-bit Windows; projects may +# declare an explicit custom target that the selected LLVM driver supports. +[target.i686-windows-msvc] diff --git a/conformance/fixtures/mcpp-target-windows-real/scenario.json b/conformance/fixtures/mcpp-target-windows-real/scenario.json new file mode 100644 index 00000000..3775a5b3 --- /dev/null +++ b/conformance/fixtures/mcpp-target-windows-real/scenario.json @@ -0,0 +1,235 @@ +{ + "name": "mcpp-target-windows-real", + "description": "Real mcpp with LLVM and the Windows MSVC environment publishes unmodified i686 and x64 compile databases. In one LSP session, the root CDB switches i686\u2192x64\u2192i686, overriding the manifest x64 default; actual analysis targets, engine commands, conditional module constants (32/64/32), navigation and diagnostics follow. No mocked producer or rewritten CDB entries.", + "prepare-environment": "msvc", + "prepare": [ + [ + "{env:MCPPLS_REAL_MCPP|mcpp}", + "build", + "--configure-only", + "--target", + "i686-windows-msvc" + ], + [ + "cmake", + "-E", + "copy", + "compile_commands.json", + "recorded-i686.json" + ], + [ + "{env:MCPPLS_REAL_MCPP|mcpp}", + "build", + "--configure-only", + "--target", + "x86_64-windows-msvc" + ], + [ + "cmake", + "-E", + "copy", + "compile_commands.json", + "recorded-x86_64.json" + ], + [ + "{env:MCPPLS_REAL_MCPP|mcpp}", + "build", + "--configure-only", + "--target", + "i686-windows-msvc" + ] + ], + "server-arguments": [ + "--mcpp", + "{env:MCPPLS_REAL_MCPP|mcpp}" + ], + "checks": [ + { + "id": "TARGET-STATUS-1-i686", + "kind": "status", + "source": "mcpp", + "state": "ready", + "profile-kind": "build-toolchain", + "profile-compiler": "clang", + "profile-target": "i686-pc-windows-msvc", + "target-mode": "auto", + "target-requested": "i686-pc-windows-msvc", + "target-source": "compile-commands", + "target-pending": false, + "timeout": 120 + }, + { + "id": "ENGINE-1-i686", + "kind": "engine-command", + "file": "src/main.cpp", + "contains": [ + "--target=i686-pc-windows-msvc" + ], + "timeout": 60 + }, + { + "id": "ARCH-1-32", + "kind": "hover-contains", + "file": "src/main.cpp", + "at": [ + 2, + 23 + ], + "expect": "32", + "timeout": 120 + }, + { + "id": "DIAGNOSTICS-1-main", + "kind": "diagnostics-empty", + "file": "src/main.cpp", + "timeout": 120 + }, + { + "id": "DIAGNOSTICS-1-module", + "kind": "diagnostics-empty", + "file": "src/platform.cppm", + "timeout": 120 + }, + { + "id": "MODULE-1", + "kind": "definition", + "file": "src/main.cpp", + "at": [ + 0, + 12 + ], + "expect": "src/platform.cppm", + "timeout": 60 + }, + { + "id": "PUBLISH-2-x86_64", + "kind": "write-file", + "file": "compile_commands.json", + "content-from": "recorded-x86_64.json", + "expect-reload": true, + "timeout": 120 + }, + { + "id": "TARGET-STATUS-2-x86_64", + "kind": "status", + "source": "mcpp", + "state": "ready", + "profile-kind": "build-toolchain", + "profile-compiler": "clang", + "profile-target": "x86_64-pc-windows-msvc", + "target-mode": "auto", + "target-requested": "x86_64-pc-windows-msvc", + "target-source": "compile-commands", + "target-pending": false, + "timeout": 120 + }, + { + "id": "ENGINE-2-x86_64", + "kind": "engine-command", + "file": "src/main.cpp", + "contains": [ + "--target=x86_64-pc-windows-msvc" + ], + "timeout": 60 + }, + { + "id": "ARCH-2-64", + "kind": "hover-contains", + "file": "src/main.cpp", + "at": [ + 2, + 23 + ], + "expect": "64", + "timeout": 120 + }, + { + "id": "DIAGNOSTICS-2-main", + "kind": "diagnostics-empty", + "file": "src/main.cpp", + "timeout": 120 + }, + { + "id": "DIAGNOSTICS-2-module", + "kind": "diagnostics-empty", + "file": "src/platform.cppm", + "timeout": 120 + }, + { + "id": "MODULE-2", + "kind": "definition", + "file": "src/main.cpp", + "at": [ + 0, + 12 + ], + "expect": "src/platform.cppm", + "timeout": 60 + }, + { + "id": "PUBLISH-3-i686", + "kind": "write-file", + "file": "compile_commands.json", + "content-from": "recorded-i686.json", + "expect-reload": true, + "timeout": 120 + }, + { + "id": "TARGET-STATUS-3-i686", + "kind": "status", + "source": "mcpp", + "state": "ready", + "profile-kind": "build-toolchain", + "profile-compiler": "clang", + "profile-target": "i686-pc-windows-msvc", + "target-mode": "auto", + "target-requested": "i686-pc-windows-msvc", + "target-source": "compile-commands", + "target-pending": false, + "timeout": 120 + }, + { + "id": "ENGINE-3-i686", + "kind": "engine-command", + "file": "src/main.cpp", + "contains": [ + "--target=i686-pc-windows-msvc" + ], + "timeout": 60 + }, + { + "id": "ARCH-3-32", + "kind": "hover-contains", + "file": "src/main.cpp", + "at": [ + 2, + 23 + ], + "expect": "32", + "timeout": 120 + }, + { + "id": "DIAGNOSTICS-3-main", + "kind": "diagnostics-empty", + "file": "src/main.cpp", + "timeout": 120 + }, + { + "id": "DIAGNOSTICS-3-module", + "kind": "diagnostics-empty", + "file": "src/platform.cppm", + "timeout": 120 + }, + { + "id": "MODULE-3", + "kind": "definition", + "file": "src/main.cpp", + "at": [ + 0, + 12 + ], + "expect": "src/platform.cppm", + "timeout": 60 + } + ] +} diff --git a/conformance/fixtures/mcpp-target-windows-real/src/main.cpp b/conformance/fixtures/mcpp-target-windows-real/src/main.cpp new file mode 100644 index 00000000..fda9c78b --- /dev/null +++ b/conformance/fixtures/mcpp-target-windows-real/src/main.cpp @@ -0,0 +1,3 @@ +import target.real; + +int main() { return register_bits; } diff --git a/conformance/fixtures/mcpp-target-windows-real/src/platform.cppm b/conformance/fixtures/mcpp-target-windows-real/src/platform.cppm new file mode 100644 index 00000000..0acb031b --- /dev/null +++ b/conformance/fixtures/mcpp-target-windows-real/src/platform.cppm @@ -0,0 +1,13 @@ +export module target.real; + +import std; + +#if defined(_M_IX86) || defined(__i386__) +export constexpr int register_bits = 32; +#else +export constexpr int register_bits = 64; +#endif + +static_assert(register_bits == sizeof(void*) * 8); + +static_assert(std::is_same_v); diff --git a/conformance/fixtures/mcpp-watched-source-structure/README.md b/conformance/fixtures/mcpp-watched-source-structure/README.md new file mode 100644 index 00000000..da4298d6 --- /dev/null +++ b/conformance/fixtures/mcpp-watched-source-structure/README.md @@ -0,0 +1,59 @@ +# Watched C++ source acknowledgement + +Run the fixture with the normal `mcppls-conformance run --fixture` command and the +bundled mock producer beside the runner. It uses real LSP file-watch notifications, +an offline producer call log and a valid, unchanged S1 dependency graph. + +`#if 0` excludes `disabled.alpha` from the compiler's dependency graph, while the +lexical scanner still sees its import. The first structural edit must run the +producer once. Body-only edits and a repeated identical write must then avoid both +another emit and a `loading` status. A second structural edit, a manifest edit, a +live target change and replacement of the native producer must still run it. + +The original **28-check** version failed against the unchanged `1341938` product: +11 actual emits instead of the expected 6, with 14 failed checks (including count +cascades). These results are saved under +`.payload-cache/59-63-watched-source-structure/old-product-*`. They demonstrate a +repeat-scheduling defect; they do not identify the cause of the original U15 +13-transitions-per-minute failure. + +The current **70-check** version keeps those assertions and adds directive-change, +failed-description/retry and in-flight-edit controls. A failed producer response +cannot acknowledge a source. A later valid unchanged response may acknowledge only +the structure captured when that run started. A second edit while a delayed run is +active needs its own producer run. The native workspace unit regression separately +changes disk before the later watch arrives, distinguishing a run-start snapshot +from an incorrect snapshot taken when its answer completes. It also restores the +original bytes after an intervening structure change: the changed file stamp must +prevent that run from acknowledging the original structure. + +The added `src/condition.cpp` begins with an active conditional import that exactly +matches its producer dependencies. Disabling it must cause a new description both +on its first watch and after a successful acknowledgement. The producer recordings +change that unit's `requires` between `target.platform` and empty alongside those +source changes; the test does not pretend an active import has no dependency. + +Acknowledgement is session-local and conservative. Only a non-inferred producer +answer with no model issues confirms it; informational notices and the engine's +source diagnostics do not prevent confirmation. Cache restoration, rejected or +stale answers, unreadable/unknown units and input changes outside that confirmed +generation retain the existing reload path. Snapshot reads cover only previously +watched source paths, with a shared 32 MiB bound and file stamps unchanged through +the valid answer. Stamps use the existing size/time identity; this is not a claim +to detect a writer deliberately restoring those metadata. + +The key includes module/import structure and exact preprocessor logical lines, +including comments and continuations. Directive changes therefore remain inputs +even when import names are unchanged. Alternate directive spellings, pragma +operators, location/counter macros, or `#` characters not accounted for by the +scanner outside comments and literals conservatively disable acknowledgement instead of guessing. Build-program +sources retain their existing dedicated handling. + +An import the lexer cannot fully parse, including `import SELECT(active);`, is not +acknowledged: macro replacement can change its dependency while the lexical names +and macro definition remain unchanged. Header-unit imports also conservatively +disable acknowledgement; the global `required_names` meaning is unchanged. The +scanner's directive-string retention is explicitly opt-in for these two workspace +checks; ordinary index and engine scans retain no directive text. + +Unparsed module declarations (including private fragments not represented by this scanner) also conservatively forgo acknowledgement; the normal global module fragment `module;` remains supported. This is a lexical acknowledgement guard, not a full preprocessor. diff --git a/conformance/fixtures/mcpp-watched-source-structure/fixture-stdlib/std.compat.cppm b/conformance/fixtures/mcpp-watched-source-structure/fixture-stdlib/std.compat.cppm new file mode 100644 index 00000000..30fbcb79 --- /dev/null +++ b/conformance/fixtures/mcpp-watched-source-structure/fixture-stdlib/std.compat.cppm @@ -0,0 +1,2 @@ +// Minimal standard-module stand-in; the target fixture imports neither module. +export module std.compat; diff --git a/conformance/fixtures/mcpp-watched-source-structure/fixture-stdlib/std.cppm b/conformance/fixtures/mcpp-watched-source-structure/fixture-stdlib/std.cppm new file mode 100644 index 00000000..4bc8ba95 --- /dev/null +++ b/conformance/fixtures/mcpp-watched-source-structure/fixture-stdlib/std.cppm @@ -0,0 +1,2 @@ +// Minimal standard-module stand-in; the target fixture imports neither module. +export module std; diff --git a/conformance/fixtures/mcpp-watched-source-structure/mcpp-mock.json b/conformance/fixtures/mcpp-watched-source-structure/mcpp-mock.json new file mode 100644 index 00000000..05904c25 --- /dev/null +++ b/conformance/fixtures/mcpp-watched-source-structure/mcpp-mock.json @@ -0,0 +1,174 @@ +{ + "version": "2026.10.22.1", + "defaultTarget": "x86_64-unknown-linux-gnu", + "callLog": "mcpp-calls.jsonl", + "requireOffline": true, + "watch": [ + "mcpp.toml", + "src/**/*.cppm", + "src/**/*.cpp" + ], + "database": { + "version": 1, + "revision": 0, + "ide": { + "profile-version": "0.2.0", + "generator": { + "name": "mcpp", + "version": "2026.10.22.1" + }, + "toolchains": { + "target": { + "family": "clang", + "version": "22", + "driver": "${compiler}", + "target": "${target}", + "config-files": [] + } + } + }, + "sets": [ + { + "name": "app", + "family-name": "app", + "baseline-arguments": [ + "-std=c++23", + "--target=${target}" + ], + "ide": { + "configuration": "dev", + "kind": "executable", + "toolchain": "target" + }, + "visible-sets": [ + "mcpp:std" + ], + "translation-units": [ + { + "source": "${root}/src/platform.cppm", + "work-directory": "${root}", + "arguments": [ + "${compiler}", + "-std=c++23", + "--target=${target}", + "-c", + "${root}/src/platform.cppm" + ], + "local-arguments": [], + "provides": { + "target.platform": "" + }, + "requires": [], + "private": false, + "ide": { + "role": "module-interface" + } + }, + { + "source": "${root}/src/main.cpp", + "work-directory": "${root}", + "arguments": [ + "${compiler}", + "-std=c++23", + "--target=${target}", + "-c", + "${root}/src/main.cpp" + ], + "local-arguments": [], + "provides": {}, + "requires": [ + "target.platform" + ], + "private": true, + "ide": { + "role": "non-module" + } + }, + { + "source": "${root}/src/condition.cpp", + "work-directory": "${root}", + "arguments": [ + "${compiler}", + "-std=c++23", + "--target=${target}", + "-c", + "${root}/src/condition.cpp" + ], + "local-arguments": [], + "provides": {}, + "requires": [ + "target.platform" + ], + "private": true, + "ide": { + "role": "non-module" + } + } + ] + }, + { + "name": "mcpp:std", + "family-name": "mcpp:std", + "baseline-arguments": [ + "-std=c++23", + "-Wno-reserved-module-identifier", + "--target=${target}" + ], + "ide": { + "toolchain": "target", + "configuration": "dev", + "kind": "library" + }, + "visible-sets": [ + "app" + ], + "translation-units": [ + { + "source": "${root}/fixture-stdlib/std.cppm", + "work-directory": "${root}", + "arguments": [ + "${compiler}", + "-std=c++23", + "-Wno-reserved-module-identifier", + "--target=${target}", + "-c", + "${root}/fixture-stdlib/std.cppm" + ], + "local-arguments": [], + "object": "${root}/fixture-stdlib/std.cppm.o", + "provides": { + "std": "" + }, + "requires": [], + "private": false, + "ide": { + "role": "module-interface" + } + }, + { + "source": "${root}/fixture-stdlib/std.compat.cppm", + "work-directory": "${root}", + "arguments": [ + "${compiler}", + "-std=c++23", + "-Wno-reserved-module-identifier", + "--target=${target}", + "-c", + "${root}/fixture-stdlib/std.compat.cppm" + ], + "local-arguments": [], + "object": "${root}/fixture-stdlib/std.compat.cppm.o", + "provides": { + "std.compat": "" + }, + "requires": [], + "private": false, + "ide": { + "role": "module-interface" + } + } + ] + } + ] + } +} diff --git a/conformance/fixtures/mcpp-watched-source-structure/mcpp.toml b/conformance/fixtures/mcpp-watched-source-structure/mcpp.toml new file mode 100644 index 00000000..3da6496f --- /dev/null +++ b/conformance/fixtures/mcpp-watched-source-structure/mcpp.toml @@ -0,0 +1,3 @@ +[package] +name = "target-fixture" +version = "0.1.0" diff --git a/conformance/fixtures/mcpp-watched-source-structure/producer-condition-off.json b/conformance/fixtures/mcpp-watched-source-structure/producer-condition-off.json new file mode 100644 index 00000000..db6fa7dc --- /dev/null +++ b/conformance/fixtures/mcpp-watched-source-structure/producer-condition-off.json @@ -0,0 +1,172 @@ +{ + "version": "2026.10.22.3", + "defaultTarget": "x86_64-unknown-linux-gnu", + "callLog": "mcpp-calls.jsonl", + "requireOffline": true, + "watch": [ + "mcpp.toml", + "src/**/*.cppm", + "src/**/*.cpp" + ], + "database": { + "version": 1, + "revision": 0, + "ide": { + "profile-version": "0.2.0", + "generator": { + "name": "mcpp", + "version": "2026.10.22.3" + }, + "toolchains": { + "target": { + "family": "clang", + "version": "22", + "driver": "${compiler}", + "target": "${target}", + "config-files": [] + } + } + }, + "sets": [ + { + "name": "app", + "family-name": "app", + "baseline-arguments": [ + "-std=c++23", + "--target=${target}" + ], + "ide": { + "configuration": "dev", + "kind": "executable", + "toolchain": "target" + }, + "visible-sets": [ + "mcpp:std" + ], + "translation-units": [ + { + "source": "${root}/src/platform.cppm", + "work-directory": "${root}", + "arguments": [ + "${compiler}", + "-std=c++23", + "--target=${target}", + "-c", + "${root}/src/platform.cppm" + ], + "local-arguments": [], + "provides": { + "target.platform": "" + }, + "requires": [], + "private": false, + "ide": { + "role": "module-interface" + } + }, + { + "source": "${root}/src/main.cpp", + "work-directory": "${root}", + "arguments": [ + "${compiler}", + "-std=c++23", + "--target=${target}", + "-c", + "${root}/src/main.cpp" + ], + "local-arguments": [], + "provides": {}, + "requires": [ + "target.platform" + ], + "private": true, + "ide": { + "role": "non-module" + } + }, + { + "source": "${root}/src/condition.cpp", + "work-directory": "${root}", + "arguments": [ + "${compiler}", + "-std=c++23", + "--target=${target}", + "-c", + "${root}/src/condition.cpp" + ], + "local-arguments": [], + "provides": {}, + "requires": [], + "private": true, + "ide": { + "role": "non-module" + } + } + ] + }, + { + "name": "mcpp:std", + "family-name": "mcpp:std", + "baseline-arguments": [ + "-std=c++23", + "-Wno-reserved-module-identifier", + "--target=${target}" + ], + "ide": { + "toolchain": "target", + "configuration": "dev", + "kind": "library" + }, + "visible-sets": [ + "app" + ], + "translation-units": [ + { + "source": "${root}/fixture-stdlib/std.cppm", + "work-directory": "${root}", + "arguments": [ + "${compiler}", + "-std=c++23", + "-Wno-reserved-module-identifier", + "--target=${target}", + "-c", + "${root}/fixture-stdlib/std.cppm" + ], + "local-arguments": [], + "object": "${root}/fixture-stdlib/std.cppm.o", + "provides": { + "std": "" + }, + "requires": [], + "private": false, + "ide": { + "role": "module-interface" + } + }, + { + "source": "${root}/fixture-stdlib/std.compat.cppm", + "work-directory": "${root}", + "arguments": [ + "${compiler}", + "-std=c++23", + "-Wno-reserved-module-identifier", + "--target=${target}", + "-c", + "${root}/fixture-stdlib/std.compat.cppm" + ], + "local-arguments": [], + "object": "${root}/fixture-stdlib/std.compat.cppm.o", + "provides": { + "std.compat": "" + }, + "requires": [], + "private": false, + "ide": { + "role": "module-interface" + } + } + ] + } + ] + } +} diff --git a/conformance/fixtures/mcpp-watched-source-structure/producer-condition-on.json b/conformance/fixtures/mcpp-watched-source-structure/producer-condition-on.json new file mode 100644 index 00000000..d2cb37f0 --- /dev/null +++ b/conformance/fixtures/mcpp-watched-source-structure/producer-condition-on.json @@ -0,0 +1,174 @@ +{ + "version": "2026.10.22.3", + "defaultTarget": "x86_64-unknown-linux-gnu", + "callLog": "mcpp-calls.jsonl", + "requireOffline": true, + "watch": [ + "mcpp.toml", + "src/**/*.cppm", + "src/**/*.cpp" + ], + "database": { + "version": 1, + "revision": 0, + "ide": { + "profile-version": "0.2.0", + "generator": { + "name": "mcpp", + "version": "2026.10.22.3" + }, + "toolchains": { + "target": { + "family": "clang", + "version": "22", + "driver": "${compiler}", + "target": "${target}", + "config-files": [] + } + } + }, + "sets": [ + { + "name": "app", + "family-name": "app", + "baseline-arguments": [ + "-std=c++23", + "--target=${target}" + ], + "ide": { + "configuration": "dev", + "kind": "executable", + "toolchain": "target" + }, + "visible-sets": [ + "mcpp:std" + ], + "translation-units": [ + { + "source": "${root}/src/platform.cppm", + "work-directory": "${root}", + "arguments": [ + "${compiler}", + "-std=c++23", + "--target=${target}", + "-c", + "${root}/src/platform.cppm" + ], + "local-arguments": [], + "provides": { + "target.platform": "" + }, + "requires": [], + "private": false, + "ide": { + "role": "module-interface" + } + }, + { + "source": "${root}/src/main.cpp", + "work-directory": "${root}", + "arguments": [ + "${compiler}", + "-std=c++23", + "--target=${target}", + "-c", + "${root}/src/main.cpp" + ], + "local-arguments": [], + "provides": {}, + "requires": [ + "target.platform" + ], + "private": true, + "ide": { + "role": "non-module" + } + }, + { + "source": "${root}/src/condition.cpp", + "work-directory": "${root}", + "arguments": [ + "${compiler}", + "-std=c++23", + "--target=${target}", + "-c", + "${root}/src/condition.cpp" + ], + "local-arguments": [], + "provides": {}, + "requires": [ + "target.platform" + ], + "private": true, + "ide": { + "role": "non-module" + } + } + ] + }, + { + "name": "mcpp:std", + "family-name": "mcpp:std", + "baseline-arguments": [ + "-std=c++23", + "-Wno-reserved-module-identifier", + "--target=${target}" + ], + "ide": { + "toolchain": "target", + "configuration": "dev", + "kind": "library" + }, + "visible-sets": [ + "app" + ], + "translation-units": [ + { + "source": "${root}/fixture-stdlib/std.cppm", + "work-directory": "${root}", + "arguments": [ + "${compiler}", + "-std=c++23", + "-Wno-reserved-module-identifier", + "--target=${target}", + "-c", + "${root}/fixture-stdlib/std.cppm" + ], + "local-arguments": [], + "object": "${root}/fixture-stdlib/std.cppm.o", + "provides": { + "std": "" + }, + "requires": [], + "private": false, + "ide": { + "role": "module-interface" + } + }, + { + "source": "${root}/fixture-stdlib/std.compat.cppm", + "work-directory": "${root}", + "arguments": [ + "${compiler}", + "-std=c++23", + "-Wno-reserved-module-identifier", + "--target=${target}", + "-c", + "${root}/fixture-stdlib/std.compat.cppm" + ], + "local-arguments": [], + "object": "${root}/fixture-stdlib/std.compat.cppm.o", + "provides": { + "std.compat": "" + }, + "requires": [], + "private": false, + "ide": { + "role": "module-interface" + } + } + ] + } + ] + } +} diff --git a/conformance/fixtures/mcpp-watched-source-structure/producer-failure.json b/conformance/fixtures/mcpp-watched-source-structure/producer-failure.json new file mode 100644 index 00000000..4fb145ae --- /dev/null +++ b/conformance/fixtures/mcpp-watched-source-structure/producer-failure.json @@ -0,0 +1,19 @@ +{ + "version": "2026.10.22.3", + "defaultTarget": "x86_64-unknown-linux-gnu", + "callLog": "mcpp-calls.jsonl", + "requireOffline": true, + "watch": [ + "mcpp.toml", + "src/**/*.cppm", + "src/**/*.cpp" + ], + "diagnostics": [ + { + "code": "CONTROLLED_FAILURE", + "severity": "error", + "source": "mcpp", + "message": "no model for this controlled invocation" + } + ] +} diff --git a/conformance/fixtures/mcpp-watched-source-structure/producer-slow.json b/conformance/fixtures/mcpp-watched-source-structure/producer-slow.json new file mode 100644 index 00000000..bb020609 --- /dev/null +++ b/conformance/fixtures/mcpp-watched-source-structure/producer-slow.json @@ -0,0 +1,175 @@ +{ + "version": "2026.10.22.3", + "defaultTarget": "x86_64-unknown-linux-gnu", + "callLog": "mcpp-calls.jsonl", + "requireOffline": true, + "watch": [ + "mcpp.toml", + "src/**/*.cppm", + "src/**/*.cpp" + ], + "database": { + "version": 1, + "revision": 0, + "ide": { + "profile-version": "0.2.0", + "generator": { + "name": "mcpp", + "version": "2026.10.22.3" + }, + "toolchains": { + "target": { + "family": "clang", + "version": "22", + "driver": "${compiler}", + "target": "${target}", + "config-files": [] + } + } + }, + "sets": [ + { + "name": "app", + "family-name": "app", + "baseline-arguments": [ + "-std=c++23", + "--target=${target}" + ], + "ide": { + "configuration": "dev", + "kind": "executable", + "toolchain": "target" + }, + "visible-sets": [ + "mcpp:std" + ], + "translation-units": [ + { + "source": "${root}/src/platform.cppm", + "work-directory": "${root}", + "arguments": [ + "${compiler}", + "-std=c++23", + "--target=${target}", + "-c", + "${root}/src/platform.cppm" + ], + "local-arguments": [], + "provides": { + "target.platform": "" + }, + "requires": [], + "private": false, + "ide": { + "role": "module-interface" + } + }, + { + "source": "${root}/src/main.cpp", + "work-directory": "${root}", + "arguments": [ + "${compiler}", + "-std=c++23", + "--target=${target}", + "-c", + "${root}/src/main.cpp" + ], + "local-arguments": [], + "provides": {}, + "requires": [ + "target.platform" + ], + "private": true, + "ide": { + "role": "non-module" + } + }, + { + "source": "${root}/src/condition.cpp", + "work-directory": "${root}", + "arguments": [ + "${compiler}", + "-std=c++23", + "--target=${target}", + "-c", + "${root}/src/condition.cpp" + ], + "local-arguments": [], + "provides": {}, + "requires": [ + "target.platform" + ], + "private": true, + "ide": { + "role": "non-module" + } + } + ] + }, + { + "name": "mcpp:std", + "family-name": "mcpp:std", + "baseline-arguments": [ + "-std=c++23", + "-Wno-reserved-module-identifier", + "--target=${target}" + ], + "ide": { + "toolchain": "target", + "configuration": "dev", + "kind": "library" + }, + "visible-sets": [ + "app" + ], + "translation-units": [ + { + "source": "${root}/fixture-stdlib/std.cppm", + "work-directory": "${root}", + "arguments": [ + "${compiler}", + "-std=c++23", + "-Wno-reserved-module-identifier", + "--target=${target}", + "-c", + "${root}/fixture-stdlib/std.cppm" + ], + "local-arguments": [], + "object": "${root}/fixture-stdlib/std.cppm.o", + "provides": { + "std": "" + }, + "requires": [], + "private": false, + "ide": { + "role": "module-interface" + } + }, + { + "source": "${root}/fixture-stdlib/std.compat.cppm", + "work-directory": "${root}", + "arguments": [ + "${compiler}", + "-std=c++23", + "-Wno-reserved-module-identifier", + "--target=${target}", + "-c", + "${root}/fixture-stdlib/std.compat.cppm" + ], + "local-arguments": [], + "object": "${root}/fixture-stdlib/std.compat.cppm.o", + "provides": { + "std.compat": "" + }, + "requires": [], + "private": false, + "ide": { + "role": "module-interface" + } + } + ] + } + ] + }, + "delayMs": 3000 +} diff --git a/conformance/fixtures/mcpp-watched-source-structure/producer-success.json b/conformance/fixtures/mcpp-watched-source-structure/producer-success.json new file mode 100644 index 00000000..d2cb37f0 --- /dev/null +++ b/conformance/fixtures/mcpp-watched-source-structure/producer-success.json @@ -0,0 +1,174 @@ +{ + "version": "2026.10.22.3", + "defaultTarget": "x86_64-unknown-linux-gnu", + "callLog": "mcpp-calls.jsonl", + "requireOffline": true, + "watch": [ + "mcpp.toml", + "src/**/*.cppm", + "src/**/*.cpp" + ], + "database": { + "version": 1, + "revision": 0, + "ide": { + "profile-version": "0.2.0", + "generator": { + "name": "mcpp", + "version": "2026.10.22.3" + }, + "toolchains": { + "target": { + "family": "clang", + "version": "22", + "driver": "${compiler}", + "target": "${target}", + "config-files": [] + } + } + }, + "sets": [ + { + "name": "app", + "family-name": "app", + "baseline-arguments": [ + "-std=c++23", + "--target=${target}" + ], + "ide": { + "configuration": "dev", + "kind": "executable", + "toolchain": "target" + }, + "visible-sets": [ + "mcpp:std" + ], + "translation-units": [ + { + "source": "${root}/src/platform.cppm", + "work-directory": "${root}", + "arguments": [ + "${compiler}", + "-std=c++23", + "--target=${target}", + "-c", + "${root}/src/platform.cppm" + ], + "local-arguments": [], + "provides": { + "target.platform": "" + }, + "requires": [], + "private": false, + "ide": { + "role": "module-interface" + } + }, + { + "source": "${root}/src/main.cpp", + "work-directory": "${root}", + "arguments": [ + "${compiler}", + "-std=c++23", + "--target=${target}", + "-c", + "${root}/src/main.cpp" + ], + "local-arguments": [], + "provides": {}, + "requires": [ + "target.platform" + ], + "private": true, + "ide": { + "role": "non-module" + } + }, + { + "source": "${root}/src/condition.cpp", + "work-directory": "${root}", + "arguments": [ + "${compiler}", + "-std=c++23", + "--target=${target}", + "-c", + "${root}/src/condition.cpp" + ], + "local-arguments": [], + "provides": {}, + "requires": [ + "target.platform" + ], + "private": true, + "ide": { + "role": "non-module" + } + } + ] + }, + { + "name": "mcpp:std", + "family-name": "mcpp:std", + "baseline-arguments": [ + "-std=c++23", + "-Wno-reserved-module-identifier", + "--target=${target}" + ], + "ide": { + "toolchain": "target", + "configuration": "dev", + "kind": "library" + }, + "visible-sets": [ + "app" + ], + "translation-units": [ + { + "source": "${root}/fixture-stdlib/std.cppm", + "work-directory": "${root}", + "arguments": [ + "${compiler}", + "-std=c++23", + "-Wno-reserved-module-identifier", + "--target=${target}", + "-c", + "${root}/fixture-stdlib/std.cppm" + ], + "local-arguments": [], + "object": "${root}/fixture-stdlib/std.cppm.o", + "provides": { + "std": "" + }, + "requires": [], + "private": false, + "ide": { + "role": "module-interface" + } + }, + { + "source": "${root}/fixture-stdlib/std.compat.cppm", + "work-directory": "${root}", + "arguments": [ + "${compiler}", + "-std=c++23", + "-Wno-reserved-module-identifier", + "--target=${target}", + "-c", + "${root}/fixture-stdlib/std.compat.cppm" + ], + "local-arguments": [], + "object": "${root}/fixture-stdlib/std.compat.cppm.o", + "provides": { + "std.compat": "" + }, + "requires": [], + "private": false, + "ide": { + "role": "module-interface" + } + } + ] + } + ] + } +} diff --git a/conformance/fixtures/mcpp-watched-source-structure/scenario.json b/conformance/fixtures/mcpp-watched-source-structure/scenario.json new file mode 100644 index 00000000..8240885d --- /dev/null +++ b/conformance/fixtures/mcpp-watched-source-structure/scenario.json @@ -0,0 +1,633 @@ +{ + "name": "mcpp-watched-source-structure", + "description": "G-5: a lexical import inside #if 0 is correctly absent from the producer dependency graph. Describe its first structure change once, then do not re-emit for body-only saves or repeated identical watch events after the unchanged valid model answers. A second structure change, manifest input, target selection and actual producer replacement must still reload. This isolates a repeat-scheduling defect and does not assign the original U15 frequency failure a cause. Preprocessor input changes still emit; failed answers do not acknowledge their structure, and a structure changed during a delayed producer run requires its own subsequent run. A second real source starts with an active conditional import whose dependencies equal the lexical names: disabling it must emit both before and after acknowledgement, and the producer recordings update that unit requires accordingly.", + "prepare": [ + [ + "{runner-dir}/mcppls-mock-mcpp{exe}", + "--copy-self", + "producer/mcpp.exe" + ], + [ + "{runner-dir}/mcppls-mock-mcpp{exe}", + "--copy-self", + "replacement/mcpp.exe" + ] + ], + "server-arguments": [ + "--mcpp", + "{workspace}/producer/mcpp.exe" + ], + "initialization-options": { + "buildDiscovery.mcppTarget": "project" + }, + "checks": [ + { + "id": "INITIAL", + "kind": "status", + "source": "mcpp", + "state": "ready", + "timeout": 60, + "profile-target": "x86_64-unknown-linux-gnu", + "producer-version": "2026.10.22.1" + }, + { + "id": "INITIAL-EMIT", + "kind": "producer-call-log", + "file": "mcpp-calls.jsonl", + "command": "emit", + "count": 1, + "settle": 3 + }, + { + "id": "CONDITIONAL-STRUCTURE-CHANGE", + "kind": "write-file", + "file": "src/main.cpp", + "notify": true, + "timeout": 30, + "content": "import target.platform;\n#if 0\nimport disabled.alpha;\n#endif\n\nint target_user() { return architecture_bits() + 0; }\n", + "expect-reload": true + }, + { + "id": "CONDITIONAL-MODEL-READY", + "kind": "status", + "source": "mcpp", + "state": "ready", + "timeout": 60 + }, + { + "id": "STRUCTURE-DESCRIBED-ONCE", + "kind": "producer-call-log", + "file": "mcpp-calls.jsonl", + "command": "emit", + "count": 2, + "settle": 3 + }, + { + "id": "BODY-ONLY-ONE", + "kind": "write-file", + "file": "src/main.cpp", + "notify": true, + "timeout": 30, + "content": "import target.platform;\n#if 0\nimport disabled.alpha;\n#endif\n\nint target_user() { return architecture_bits() + 1; }\n", + "expect-reload": false, + "settle": 5 + }, + { + "id": "BODY-ONLY-ONE-NO-EMIT", + "kind": "producer-call-log", + "file": "mcpp-calls.jsonl", + "command": "emit", + "count": 2, + "settle": 3 + }, + { + "id": "BODY-ONLY-TWO", + "kind": "write-file", + "file": "src/main.cpp", + "notify": true, + "timeout": 30, + "content": "import target.platform;\n#if 0\nimport disabled.alpha;\n#endif\n\nint target_user() { return architecture_bits() + 2; }\n", + "expect-reload": false, + "settle": 5 + }, + { + "id": "BODY-ONLY-TWO-NO-EMIT", + "kind": "producer-call-log", + "file": "mcpp-calls.jsonl", + "command": "emit", + "count": 2, + "settle": 3 + }, + { + "id": "SAME-BYTES-AGAIN", + "kind": "write-file", + "file": "src/main.cpp", + "notify": true, + "timeout": 30, + "content": "import target.platform;\n#if 0\nimport disabled.alpha;\n#endif\n\nint target_user() { return architecture_bits() + 2; }\n", + "expect-reload": false, + "settle": 5 + }, + { + "id": "SAME-BYTES-AGAIN-NO-EMIT", + "kind": "producer-call-log", + "file": "mcpp-calls.jsonl", + "command": "emit", + "count": 2, + "settle": 3 + }, + { + "id": "NEXT-REAL-LEXICAL-STRUCTURE", + "kind": "write-file", + "file": "src/main.cpp", + "notify": true, + "timeout": 30, + "content": "import target.platform;\n#if 0\nimport disabled.alpha;\nimport disabled.beta;\n#endif\n\nint target_user() { return architecture_bits() + 2; }\n", + "expect-reload": true + }, + { + "id": "NEXT-STRUCTURE-READY", + "kind": "status", + "source": "mcpp", + "state": "ready", + "timeout": 60 + }, + { + "id": "NEXT-STRUCTURE-EMITTED", + "kind": "producer-call-log", + "file": "mcpp-calls.jsonl", + "command": "emit", + "count": 3, + "settle": 3 + }, + { + "id": "MANIFEST-INPUT-CHANGE", + "kind": "write-file", + "file": "mcpp.toml", + "notify": true, + "timeout": 30, + "content": "[package]\nname = \"target-fixture\"\nversion = \"0.1.1\"\n", + "expect-reload": true + }, + { + "id": "MANIFEST-READY", + "kind": "status", + "source": "mcpp", + "state": "ready", + "timeout": 60 + }, + { + "id": "MANIFEST-STILL-RELOADS", + "kind": "producer-call-log", + "file": "mcpp-calls.jsonl", + "command": "emit", + "count": 4, + "settle": 3 + }, + { + "id": "EXPLICIT-TARGET-CHANGE", + "kind": "configuration", + "settings": { + "mcppls": { + "buildDiscovery.mcppTarget": "x86_64-unknown-linux-gnu" + } + } + }, + { + "id": "EXPLICIT-TARGET-READY", + "kind": "status", + "source": "mcpp", + "state": "ready", + "timeout": 60, + "target-mode": "explicit", + "target-requested": "x86_64-unknown-linux-gnu", + "target-source": "configuration", + "target-pending": false + }, + { + "id": "TARGET-STILL-RELOADS", + "kind": "producer-call-log", + "file": "mcpp-calls.jsonl", + "command": "emit", + "count": 5, + "settle": 3, + "arguments-contains": [ + "--target", + "x86_64-unknown-linux-gnu" + ] + }, + { + "id": "NEXT-BODY-AFTER-TARGET", + "kind": "write-file", + "file": "src/main.cpp", + "notify": true, + "timeout": 30, + "content": "import target.platform;\n#if 0\nimport disabled.alpha;\nimport disabled.beta;\n#endif\n\nint target_user() { return architecture_bits() + 3; }\n", + "expect-reload": false, + "settle": 5 + }, + { + "id": "TARGET-BODY-NO-EMIT", + "kind": "producer-call-log", + "file": "mcpp-calls.jsonl", + "command": "emit", + "count": 5, + "settle": 3 + }, + { + "id": "UPGRADE-RECORDING-ONLY", + "kind": "write-file", + "file": "mcpp-mock.json", + "replace": { + "from": "2026.10.22.1", + "with": "2026.10.22.2" + }, + "notify": false, + "expect-reload": false, + "settle": 1 + }, + { + "id": "REPLACE-PRODUCER-IDENTITY", + "kind": "write-file", + "file": "producer/mcpp.exe", + "notify": true, + "timeout": 45, + "content-from": "replacement/mcpp.exe", + "expect-reload": true + }, + { + "id": "NEW-PRODUCER-READY", + "kind": "status", + "source": "mcpp", + "state": "ready", + "timeout": 60, + "producer-version": "2026.10.22.2", + "target-mode": "explicit" + }, + { + "id": "PRODUCER-STILL-RELOADS", + "kind": "producer-call-log", + "file": "mcpp-calls.jsonl", + "command": "emit", + "count": 6, + "settle": 3, + "version": "2026.10.22.2" + }, + { + "id": "NEXT-BODY-AFTER-PRODUCER", + "kind": "write-file", + "file": "src/main.cpp", + "notify": true, + "timeout": 30, + "content": "import target.platform;\n#if 0\nimport disabled.alpha;\nimport disabled.beta;\n#endif\n\nint target_user() { return architecture_bits() + 4; }\n", + "expect-reload": false, + "settle": 5 + }, + { + "id": "PRODUCER-BODY-NO-EMIT", + "kind": "producer-call-log", + "file": "mcpp-calls.jsonl", + "command": "emit", + "count": 6, + "settle": 3 + }, + { + "id": "PREPROCESSOR-INPUT-CHANGES", + "kind": "write-file", + "file": "src/main.cpp", + "notify": true, + "timeout": 30, + "replace": { + "from": "#if 0", + "with": "#if (0)" + }, + "expect-reload": true + }, + { + "id": "PREPROCESSOR-INPUT-READY", + "kind": "status", + "source": "mcpp", + "state": "ready", + "timeout": 60 + }, + { + "id": "DIRECTIVE-CHANGE-STILL-EMITS", + "kind": "producer-call-log", + "file": "mcpp-calls.jsonl", + "command": "emit", + "count": 7, + "settle": 3 + }, + { + "id": "BODY-AFTER-DIRECTIVE", + "kind": "write-file", + "file": "src/main.cpp", + "notify": true, + "timeout": 30, + "content": "import target.platform;\n#if (0)\nimport disabled.alpha;\nimport disabled.beta;\n#endif\n\nint target_user() { return architecture_bits() + 5; }\n", + "expect-reload": false, + "settle": 5 + }, + { + "id": "DIRECTIVE-BODY-NO-EMIT", + "kind": "producer-call-log", + "file": "mcpp-calls.jsonl", + "command": "emit", + "count": 7, + "settle": 3 + }, + { + "id": "CONTROLLED-PRODUCER-FAILURE", + "kind": "write-file", + "file": "mcpp-mock.json", + "content-from": "producer-failure.json", + "notify": false, + "expect-reload": false, + "settle": 0 + }, + { + "id": "STRUCTURE-WHICH-FAILS-TO-DESCRIBE", + "kind": "write-file", + "file": "src/main.cpp", + "notify": true, + "timeout": 30, + "content": "import target.platform;\n#if (0)\nimport disabled.alpha;\nimport disabled.beta;\nimport disabled.gamma;\n#endif\n\nint target_user() { return architecture_bits() + 5; }\n", + "expect-reload": true + }, + { + "id": "FAILED-DESCRIPTION-WAS-ATTEMPTED", + "kind": "producer-call-log", + "file": "mcpp-calls.jsonl", + "command": "emit", + "count": 8, + "settle": 3 + }, + { + "id": "FAILED-DESCRIPTION-KEEPS-MODEL", + "kind": "status", + "source": "mcpp", + "state": "degraded", + "timeout": 60 + }, + { + "id": "PRODUCER-RECOVERS", + "kind": "write-file", + "file": "mcpp-mock.json", + "content-from": "producer-success.json", + "notify": false, + "expect-reload": false, + "settle": 0 + }, + { + "id": "SAME-STRUCTURE-RETRIES-AFTER-FAILURE", + "kind": "write-file", + "file": "src/main.cpp", + "notify": true, + "timeout": 30, + "content": "import target.platform;\n#if (0)\nimport disabled.alpha;\nimport disabled.beta;\nimport disabled.gamma;\n#endif\n\nint target_user() { return architecture_bits() + 6; }\n", + "expect-reload": true + }, + { + "id": "RECOVERED-STRUCTURE-READY", + "kind": "status", + "source": "mcpp", + "state": "ready", + "timeout": 60, + "producer-version": "2026.10.22.3" + }, + { + "id": "FAILED-STRUCTURE-WAS-NOT-ACKNOWLEDGED", + "kind": "producer-call-log", + "file": "mcpp-calls.jsonl", + "command": "emit", + "count": 9, + "settle": 3 + }, + { + "id": "BODY-AFTER-SUCCESSFUL-RETRY", + "kind": "write-file", + "file": "src/main.cpp", + "notify": true, + "timeout": 30, + "content": "import target.platform;\n#if (0)\nimport disabled.alpha;\nimport disabled.beta;\nimport disabled.gamma;\n#endif\n\nint target_user() { return architecture_bits() + 7; }\n", + "expect-reload": false, + "settle": 5 + }, + { + "id": "SUCCESSFUL-RETRY-ACKNOWLEDGED", + "kind": "producer-call-log", + "file": "mcpp-calls.jsonl", + "command": "emit", + "count": 9, + "settle": 3 + }, + { + "id": "DELAY-THE-NEXT-DESCRIPTION", + "kind": "write-file", + "file": "mcpp-mock.json", + "content-from": "producer-slow.json", + "notify": false, + "expect-reload": false, + "settle": 0 + }, + { + "id": "START-INFLIGHT-STRUCTURE", + "kind": "write-file", + "file": "src/main.cpp", + "notify": true, + "timeout": 30, + "content": "import target.platform;\n#if (0)\nimport disabled.alpha;\nimport disabled.beta;\nimport disabled.gamma;\nimport disabled.delta;\n#endif\n\nint target_user() { return architecture_bits() + 7; }\n", + "expect-reload": true + }, + { + "id": "INFLIGHT-RUN-STARTED", + "kind": "producer-call-log", + "file": "mcpp-calls.jsonl", + "command": "emit", + "count": 10, + "settle": 1 + }, + { + "id": "STRUCTURE-CHANGES-WHILE-PRODUCER-RUNS", + "kind": "write-file", + "file": "src/main.cpp", + "notify": true, + "timeout": 30, + "content": "import target.platform;\n#if (0)\nimport disabled.alpha;\nimport disabled.beta;\nimport disabled.gamma;\nimport disabled.delta;\nimport disabled.epsilon;\n#endif\n\nint target_user() { return architecture_bits() + 7; }\n" + }, + { + "id": "NEXT-DESCRIPTION-IS-FAST", + "kind": "write-file", + "file": "mcpp-mock.json", + "content-from": "producer-success.json", + "notify": false, + "expect-reload": false, + "settle": 0 + }, + { + "id": "LATER-STRUCTURE-GETS-ITS-OWN-RUN", + "kind": "producer-call-log", + "file": "mcpp-calls.jsonl", + "command": "emit", + "count": 11, + "settle": 7 + }, + { + "id": "LATER-STRUCTURE-READY", + "kind": "status", + "source": "mcpp", + "state": "ready", + "timeout": 60 + }, + { + "id": "BODY-AFTER-INFLIGHT-STRUCTURE", + "kind": "write-file", + "file": "src/main.cpp", + "notify": true, + "timeout": 30, + "content": "import target.platform;\n#if (0)\nimport disabled.alpha;\nimport disabled.beta;\nimport disabled.gamma;\nimport disabled.delta;\nimport disabled.epsilon;\n#endif\n\nint target_user() { return architecture_bits() + 8; }\n", + "expect-reload": false, + "settle": 5 + }, + { + "id": "LATER-STRUCTURE-ACKNOWLEDGED", + "kind": "producer-call-log", + "file": "mcpp-calls.jsonl", + "command": "emit", + "count": 11, + "settle": 3 + }, + { + "id": "DISABLED-CONDITION-PRODUCER", + "kind": "write-file", + "file": "mcpp-mock.json", + "content-from": "producer-condition-off.json", + "notify": false, + "expect-reload": false, + "settle": 0 + }, + { + "id": "FIRST-WATCH-DISABLES-ACTIVE-IMPORT", + "kind": "write-file", + "file": "src/condition.cpp", + "content": "#if 0\nimport target.platform;\n#endif\nint condition_user() { return 0; }\n", + "notify": true, + "expect-reload": true, + "settle": 5, + "timeout": 30 + }, + { + "id": "FIRST-WATCH-DISABLED-READY", + "kind": "status", + "source": "mcpp", + "state": "ready", + "timeout": 60 + }, + { + "id": "FIRST-WATCH-DISABLED-COUNT", + "kind": "producer-call-log", + "file": "mcpp-calls.jsonl", + "command": "emit", + "count": 12, + "settle": 3 + }, + { + "id": "ENABLED-CONDITION-PRODUCER", + "kind": "write-file", + "file": "mcpp-mock.json", + "content-from": "producer-condition-on.json", + "notify": false, + "expect-reload": false, + "settle": 0 + }, + { + "id": "RESTORE-ACTIVE-IMPORT", + "kind": "write-file", + "file": "src/condition.cpp", + "content": "#if 1\nimport target.platform;\n#endif\nint condition_user() { return 0; }\n", + "notify": true, + "expect-reload": true, + "settle": 5, + "timeout": 30 + }, + { + "id": "ACTIVE-IMPORT-RESTORED-READY", + "kind": "status", + "source": "mcpp", + "state": "ready", + "timeout": 60 + }, + { + "id": "ACTIVE-IMPORT-RESTORED-COUNT", + "kind": "producer-call-log", + "file": "mcpp-calls.jsonl", + "command": "emit", + "count": 13, + "settle": 3 + }, + { + "id": "ACTIVE-CONDITIONAL-BODY-ONLY", + "kind": "write-file", + "file": "src/condition.cpp", + "content": "#if 1\nimport target.platform;\n#endif\nint condition_user() { return 1; }\n", + "notify": true, + "expect-reload": false, + "settle": 5, + "timeout": 30 + }, + { + "id": "ACTIVE-CONDITIONAL-BODY-READY", + "kind": "status", + "source": "mcpp", + "state": "ready", + "timeout": 60 + }, + { + "id": "ACTIVE-CONDITIONAL-BODY-COUNT", + "kind": "producer-call-log", + "file": "mcpp-calls.jsonl", + "command": "emit", + "count": 13, + "settle": 3 + }, + { + "id": "DISABLED-CONDITION-PRODUCER-AGAIN", + "kind": "write-file", + "file": "mcpp-mock.json", + "content-from": "producer-condition-off.json", + "notify": false, + "expect-reload": false, + "settle": 0 + }, + { + "id": "BASELINED-ACTIVE-IMPORT-DISABLED", + "kind": "write-file", + "file": "src/condition.cpp", + "content": "#if 0\nimport target.platform;\n#endif\nint condition_user() { return 1; }\n", + "notify": true, + "expect-reload": true, + "settle": 5, + "timeout": 30 + }, + { + "id": "BASELINED-CONDITION-DISABLED-READY", + "kind": "status", + "source": "mcpp", + "state": "ready", + "timeout": 60 + }, + { + "id": "BASELINED-CONDITION-DISABLED-COUNT", + "kind": "producer-call-log", + "file": "mcpp-calls.jsonl", + "command": "emit", + "count": 14, + "settle": 3 + }, + { + "id": "INACTIVE-CONDITIONAL-BODY-ONLY", + "kind": "write-file", + "file": "src/condition.cpp", + "content": "#if 0\nimport target.platform;\n#endif\nint condition_user() { return 2; }\n", + "notify": true, + "expect-reload": false, + "settle": 5, + "timeout": 30 + }, + { + "id": "INACTIVE-CONDITIONAL-BODY-READY", + "kind": "status", + "source": "mcpp", + "state": "ready", + "timeout": 60 + }, + { + "id": "INACTIVE-CONDITIONAL-BODY-COUNT", + "kind": "producer-call-log", + "file": "mcpp-calls.jsonl", + "command": "emit", + "count": 14, + "settle": 3 + } + ] +} diff --git a/conformance/fixtures/mcpp-watched-source-structure/src/condition.cpp b/conformance/fixtures/mcpp-watched-source-structure/src/condition.cpp new file mode 100644 index 00000000..54831467 --- /dev/null +++ b/conformance/fixtures/mcpp-watched-source-structure/src/condition.cpp @@ -0,0 +1,4 @@ +#if 1 +import target.platform; +#endif +int condition_user() { return 0; } diff --git a/conformance/fixtures/mcpp-watched-source-structure/src/main.cpp b/conformance/fixtures/mcpp-watched-source-structure/src/main.cpp new file mode 100644 index 00000000..d8455a8a --- /dev/null +++ b/conformance/fixtures/mcpp-watched-source-structure/src/main.cpp @@ -0,0 +1,3 @@ +import target.platform; + +int target_user() { return architecture_bits(); } diff --git a/conformance/fixtures/mcpp-watched-source-structure/src/platform.cppm b/conformance/fixtures/mcpp-watched-source-structure/src/platform.cppm new file mode 100644 index 00000000..c85e466f --- /dev/null +++ b/conformance/fixtures/mcpp-watched-source-structure/src/platform.cppm @@ -0,0 +1,3 @@ +export module target.platform; + +export int architecture_bits() { return sizeof(void*) * 8; } diff --git a/conformance/traceability.json b/conformance/traceability.json index 82c459f2..a0009427 100644 --- a/conformance/traceability.json +++ b/conformance/traceability.json @@ -894,6 +894,12 @@ } ], "S2-5-1": [ + { + "check": "mcpp-watched-source-structure/DIRECTIVE-CHANGE-STILL-EMITS" + }, + { + "check": "mcpp-watched-source-structure/LATER-STRUCTURE-GETS-ITS-OWN-RUN" + }, { "check": "mcpp-emit-watch/W1-named-input" }, diff --git a/docs/20-projects.md b/docs/20-projects.md index 48ea5287..cb3b28c6 100644 --- a/docs/20-projects.md +++ b/docs/20-projects.md @@ -33,7 +33,10 @@ A few things are worth knowing: first time, then three times what its last run took, between 1 and 10 minutes; the project's cached model serves meanwhile, or its sources when there is none (`mcppls.producerTimeout` sets a fixed limit instead). Saving a source does not run it: a saved source whose module declaration and - imports are unchanged only updates the index. The build tool is asked again when a build file, a + imports are unchanged only updates the index. When conditional compilation excludes an import, + a successful description can still confirm that source structure, so later body-only saves do + not repeatedly ask for the same graph. Preprocessing changes, failed descriptions and files + changed while discovery runs retain conservative rechecks. The build tool is asked again when a build file, a non-source input or a source's module structure changes, and edits that keep coming are one run — the wait before it grows with how long the build tool takes, up to a minute. - **The run is offline.** A build description is a question about the project, not an errand, so diff --git a/editors/zed/tests/lsp_frames.py b/editors/zed/tests/lsp_frames.py index e6fbf241..21369ab2 100755 --- a/editors/zed/tests/lsp_frames.py +++ b/editors/zed/tests/lsp_frames.py @@ -11,11 +11,16 @@ didopen from `textDocument/didOpen` diagnostics from `textDocument/publishDiagnostics` +With `--directory-workspace`, print no facts and return success only when a complete initialize +names existing directories in workspaceFolders (or rootUri if folders are empty). + A frame still being written at the end of the file is ignored; it shows up on the next read. """ import json +import pathlib import re import sys +import urllib.parse def frames(data): @@ -38,6 +43,21 @@ def frames(data): def main(): with open(sys.argv[1], "rb") as handle: data = handle.read() + if len(sys.argv) > 2 and sys.argv[2] == "--directory-workspace": + for message in frames(data): + if message.get("method") != "initialize": + continue + params = message.get("params") or {} + uris = [entry.get("uri", "") for entry in params.get("workspaceFolders") or []] + if not uris: + uris = [params.get("rootUri", "")] + + def directory(uri): + parsed = urllib.parse.urlparse(uri) + return parsed.scheme == "file" and pathlib.Path(urllib.parse.unquote(parsed.path)).is_dir() + + return 0 if uris and all(directory(uri) for uri in uris) else 1 + return 1 for message in frames(data): method = message.get("method") params = message.get("params") or {} @@ -47,6 +67,8 @@ def main(): print("didopen", (params.get("textDocument") or {}).get("uri", "")) elif method == "textDocument/publishDiagnostics": print("diagnostics", params.get("uri", ""), len(params.get("diagnostics") or [])) + return 0 -main() +if __name__ == "__main__": + sys.exit(main()) diff --git a/editors/zed/tests/smoke.sh b/editors/zed/tests/smoke.sh index 43a9badf..e8d628b3 100755 --- a/editors/zed/tests/smoke.sh +++ b/editors/zed/tests/smoke.sh @@ -21,7 +21,8 @@ # # recommended `"language_servers": ["mcppls", "!clangd"]` in the user's settings (Z-2): within # 60 s mcppls has been initialized by "Zed", been sent the file and published -# diagnostics for it, and Zed started no clangd of its own +# diagnostics for it, its own clangd completed initialization and remains alive, +# and Zed started no clangd of its own (this does not await every module BMI) # default Zed's own settings (Z-3): mcppls answers all the same, and Zed's clangd is started # beside it; that both ran is recorded, because it is what the README tells users # to turn off @@ -32,8 +33,8 @@ # ZED_EDITOR, MCPPLS_PAYLOAD, MCPPLS_DEVTOOLS required # ZED_SMOKE_DIR scratch directory (default: a new temporary one, removed on success) # ZED_SMOKE_BUDGET seconds for mcppls to answer (default 60) -# ZED_SMOKE_REQUIRE_CLANGD=1 `default` needs Zed's clangd running, not just started: Zed -# downloads it from GitHub, which a machine without network cannot do +# ZED_SMOKE_REQUIRE_CLANGD=1 `default` needs Zed's clangd running, not just attempted; +# CI preinstalls it on PATH using Zed's default binary lookup set -euo pipefail mode=${1:-recommended} @@ -151,6 +152,25 @@ has_client() { zed_said | grep -qx 'client Zed'; } has_didopen() { zed_said | grep -q "^didopen .*/$file\$"; } has_diagnostics() { mcppls_said | grep -q "^diagnostics .*/$file "; } +# Inspect the actual initialize, not just the command line that was meant to open a directory. +# Zed can otherwise make the first file a single-file worktree before opening its parent folder. +has_directory_workspace() { + python3 "$here/lsp_frames.py" "$root/lsp-in.log" --directory-workspace +} + +# Initialization is proved by the engine's own protocol log and a live direct child of mcppls, +# not by module-layer diagnostics or Zed's separate clangd. Module preparation may still continue. +has_mcppls_engine() { + local pid + grep -qsE 'clangd \(.*\): .*<-- initialized' "$root"/cache/logs/server-*.log || return 1 + for pid in $(server_pids); do + if ps -eo ppid=,comm= | awk -v parent="$pid" '$1 == parent && $2 ~ /^clangd/ { found=1 } END { exit !found }'; then + return 0 + fi + done + return 1 +} + # --------------------------------------------------------------------------------------------- # The extension, installed the way a release is checked: linked into Zed's data directory, the # payload put where the extension looks for it. @@ -190,11 +210,12 @@ say "starting Zed ($mode settings) on $workspace" XDG_CACHE_HOME="$root/home/.cache" XDG_RUNTIME_DIR="$root/run" SHELL=/bin/bash \ ZED_ALLOW_EMULATED_GPU=1 MCPPLS_CACHE_DIR="$root/cache" MCPPLS_LOG_LEVEL=debug \ PATH="$root/bin:$PATH" - exec "$editor" "$workspace/$file" "$workspace" + exec "$editor" "$workspace" "$workspace/$file" ) > "$root/zed.out" 2>&1 & zed_pid=$! wait_for 'mcppls is initialized by a client named "Zed"' has_client +wait_for 'the actual LSP workspace folders name directories' has_directory_workspace wait_for "mcppls is sent $file (didOpen)" has_didopen wait_for "mcppls publishes diagnostics for $file" has_diagnostics @@ -204,6 +225,7 @@ has_log() { ls "$root"/cache/logs/server-*.log >/dev/null 2>&1; } has_debug_log() { grep -qs '\[debug\]' "$root"/cache/logs/server-*.log; } wait_for "mcppls wrote its log under MCPPLS_CACHE_DIR" has_log wait_for "the log is at debug level (--log-level debug reached the server)" has_debug_log +wait_for "mcppls's own clangd completed initialization and is alive" has_mcppls_engine mcppls_pid=$(server_pids | head -1) test -n "$mcppls_pid" || fail "no mcppls process while Zed is showing its answers" @@ -218,7 +240,7 @@ case "$mode" in summary "Zed ($mode settings): mcppls (pid $mcppls_pid) answered within $((SECONDS - started)) s; Zed started no clangd" ;; default) - # Zed downloads clangd on first use (tens of megabytes from GitHub) before it starts it. + # Zed first checks PATH (preinstalled in CI), then downloads if none is usable. clangd_deadline=$((SECONDS + 240)) clangd='' until clangd=$(zed_clangd_pid); do diff --git a/src/bin/conformance.cpp b/src/bin/conformance.cpp index 1ccc8aa1..6d9145d4 100644 --- a/src/bin/conformance.cpp +++ b/src/bin/conformance.cpp @@ -2791,6 +2791,49 @@ class Scenario { } } + // Freeze the same response population used above before a diagnostic probe or post-check + // report can deliver late answers. This observes existing requests, without pumping the + // client, asking anything, or reading another clock. JSON is built only after sampling. + constexpr std::size_t COMPLETION_TRACE_LIMIT { 256 }; + struct CompletionTraceEntry { + std::size_t index; + std::int64_t id; + Clock::time_point sent; + std::optional answered; + bool error; + std::optional items; + std::optional incomplete; + }; + struct CompletionTrace { + std::size_t total; + std::vector entries; + std::optional slowestSuccessful; + std::optional slowestAnswered; + }; + const auto capture_trace = [&](const std::vector& ids) { + CompletionTrace trace { ids.size(), {}, std::nullopt, std::nullopt }; + trace.entries.reserve(std::min(ids.size(), COMPLETION_TRACE_LIMIT)); + for (std::size_t index { 0 }; index < ids.size(); ++index) { + const auto& request { client_.asyncRequests.at(ids[index]) }; + const Json& result { request.result }; + const Json* items { result.is_array() ? &result : result.is_object() ? lsp::find(result, "items") : nullptr }; + const Json* incomplete { result.is_object() ? lsp::find(result, "isIncomplete") : nullptr }; + CompletionTraceEntry entry { index, ids[index], request.sent, request.answered, request.isError, + items != nullptr && items->is_array() ? std::optional { items->size() } : std::nullopt, + incomplete != nullptr && incomplete->is_boolean() ? std::optional { incomplete->get() } : std::nullopt }; + if (trace.entries.size() < COMPLETION_TRACE_LIMIT) trace.entries.push_back(entry); + if (!entry.answered) continue; + const auto slower = [&](const std::optional& previous) { + return !previous || *entry.answered - entry.sent > *previous->answered - previous->sent; + }; + if (slower(trace.slowestAnswered)) trace.slowestAnswered = entry; + if (!entry.error && slower(trace.slowestSuccessful)) trace.slowestSuccessful = entry; + } + return trace; + }; + const CompletionTrace completionTrace { capture_trace(completions) }; + const CompletionTrace importerTrace { capture_trace(importerCompletions) }; + // Diagnostics after the typing stops: a real mistake is typed at once and how long its diagnostic takes is timed. std::optional refresh; if (const auto probe = check.find("diagnostic-probe"); probe != check.end() && probe->is_object()) { @@ -2896,6 +2939,96 @@ class Scenario { { "restarts", restarts }, { "filesSetAside", setAside }, { "diagnosticsRefresh", refresh ? Json(*refresh) : Json(nullptr) }, { "statesSeen", statesSeen }, { "doomed", doomed }, { "moduleFailedDiagnostics", moduleFailedDiagnostics }, { "stalledSamples", stalledSamples }, { "firstOpenSeconds", firstOpen ? Json(*firstOpen) : Json(nullptr) } }; + const auto trace_json = [&](const CompletionTrace& trace) { + const auto nanoseconds_since = [&](Clock::time_point at) { + return std::chrono::duration_cast(at - began).count(); + }; + const auto entry_json = [&](const CompletionTraceEntry& entry) { + return Json { { "index", entry.index }, { "id", entry.id }, { "sentNanoseconds", nanoseconds_since(entry.sent) }, + { "answeredNanoseconds", entry.answered ? Json(nanoseconds_since(*entry.answered)) : Json(nullptr) }, + { "elapsedNanoseconds", entry.answered ? Json(std::chrono::duration_cast(*entry.answered - entry.sent).count()) : Json(nullptr) }, + { "elapsedSeconds", entry.answered ? Json(seconds_between(entry.sent, *entry.answered)) : Json(nullptr) }, + { "error", entry.error }, { "items", entry.items ? Json(*entry.items) : Json(nullptr) }, + { "isIncomplete", entry.incomplete ? Json(*entry.incomplete) : Json(nullptr) } }; + }; + Json entries = Json::array(); + for (const auto& entry : trace.entries) entries.push_back(entry_json(entry)); + return Json { { "origin", "typing-start" }, { "population", "original-completion-statistics" }, + { "limit", COMPLETION_TRACE_LIMIT }, { "total", trace.total }, { "dropped", trace.total - trace.entries.size() }, + { "entries", std::move(entries) }, + // These are separate from the ordered prefix: a truncated trace never hides its + // slowest answer, and the successful one reproduces the original latency max. + { "slowestSuccessful", trace.slowestSuccessful ? entry_json(*trace.slowestSuccessful) : Json(nullptr) }, + { "slowestAnswered", trace.slowestAnswered ? entry_json(*trace.slowestAnswered) : Json(nullptr) } }; + }; + summary["completionTrace"] = trace_json(completionTrace); + // These are the reports already taken by the check. Keep the method's public + // counters/distributions at both boundaries: its recent engine samples can roll out + // before a later timeline report. They are cumulative/recent, not per-check percentiles. + const auto completion_stats = [](const Json& root) -> Json { + const Json* stats { lsp::find_path(root, { "requests", "textDocument/completion" }) }; + if (stats == nullptr || !stats->is_object()) return nullptr; + Json snapshot = Json::object(); + for (const std::string_view key : { "count", "empty", "errors", "cancelled", "p50Ms", "p95Ms", "maxMs", "engineAnswered", + "engineP50Ms", "engineP95Ms", "overheadP50Ms", "overheadP95Ms" }) { + const Json* value { lsp::find(*stats, key) }; + snapshot[std::string { key }] = value != nullptr && value->is_number() ? *value : Json(nullptr); + } + const Json* lastAt { lsp::find(*stats, "lastAt") }; + snapshot["lastAt"] = lastAt != nullptr && lastAt->is_string() ? *lastAt : Json(nullptr); + const Json* by { lsp::find(*stats, "answeredBy") }; + snapshot["answeredBy"] = nullptr; + if (by != nullptr && by->is_object()) { + snapshot["answeredBy"] = Json::object(); + for (const auto& item : by->items()) if (item.value().is_number()) snapshot["answeredBy"][item.key()] = item.value(); + } + return snapshot; + }; + const auto slow_total = [](const Json& root) -> std::optional { + const Json* value { lsp::find_path(root, { "eventTotals", "slow-request" }) }; + if (value == nullptr || !value->is_number_integer() || value->get() < 0) return std::nullopt; + return value->get(); + }; + const auto slowBefore { slow_total(before) }, slowAfter { slow_total(after) }; + constexpr std::size_t SLOW_TRACE_LIMIT { 256 }; + const Json* events { lsp::find(after, "events") }; + const bool eventsAvailable { events != nullptr && events->is_array() }; + std::size_t recentSlowCount { 0 }; + Json slowEvents = Json::array(); + if (eventsAvailable) { + for (const auto& event : *events) { + if (!event.is_object() || event.value("kind", std::string {}) != "slow-request") continue; + ++recentSlowCount; + Json entry = Json::object(); + const Json* at { lsp::find(event, "at") }; + entry["at"] = at != nullptr && at->is_string() ? *at : Json(nullptr); + for (const std::string_view field : { "method", "ms", "outcome", "answeredBy" }) { + const Json* value { lsp::find_path(event, { "detail", field }) }; + entry[std::string { field }] = value != nullptr && (field == "ms" ? value->is_number() : value->is_string()) ? *value : Json(nullptr); + } + if (slowEvents.size() == SLOW_TRACE_LIMIT) slowEvents.erase(slowEvents.begin()); + slowEvents.push_back(std::move(entry)); + } + } + const bool totalsComparable { slowBefore && slowAfter && *slowAfter >= *slowBefore }; + const auto added { totalsComparable ? std::optional { *slowAfter - *slowBefore } : std::nullopt }; + // Recent events are chronological suffixes of the cumulative journal. Counters expose + // omitted new events; timestamps alone cannot establish that every slow request survived. + const bool coverageKnown { added && eventsAvailable && recentSlowCount <= *slowAfter }; + const std::uint64_t retainedNew { coverageKnown ? std::min(*added, static_cast(slowEvents.size())) : 0 }; + summary["serverCompletionEvidence"] = Json { + { "requestsBefore", completion_stats(before) }, { "requestsAfter", completion_stats(after) }, + { "recentSampleLimit", 256 }, { "statisticsScope", "cumulative counters and recent samples; not per-check percentile deltas" }, + { "slowRequests", Json { + { "totalBefore", slowBefore ? Json(*slowBefore) : Json(nullptr) }, { "totalAfter", slowAfter ? Json(*slowAfter) : Json(nullptr) }, + { "added", added ? Json(*added) : Json(nullptr) }, { "limit", SLOW_TRACE_LIMIT }, + { "afterRecentCount", eventsAvailable ? Json(recentSlowCount) : Json(nullptr) }, + { "droppedByTraceLimit", eventsAvailable ? Json(recentSlowCount - slowEvents.size()) : Json(nullptr) }, + { "newRetained", coverageKnown ? Json(retainedNew) : Json(nullptr) }, + { "newNotRetained", coverageKnown ? Json(*added - retainedNew) : Json(nullptr) }, + { "eventsAfter", eventsAvailable ? std::move(slowEvents) : Json(nullptr) } + } } + }; if (!reask.empty()) { summary["wordTrace"] = std::move(wordTrace); summary["wordTraceLimit"] = WORD_TRACE_LIMIT; @@ -2904,7 +3037,8 @@ class Scenario { std::string importerBrief; if (withImporter) { summary["importer"] = Json { { "file", importerFile }, { "completions", importerCompletions.size() }, { "answered", importerAnswered.size() }, - { "empty", importerEmpty }, { "completion", importerLatency }, { "moduleFailedDiagnostics", importerModuleFailed } }; + { "empty", importerEmpty }, { "completion", importerLatency }, { "completionTrace", trace_json(importerTrace) }, + { "moduleFailedDiagnostics", importerModuleFailed } }; importerBrief = std::format(", importer {} completions p95 {:.2f}s {} empty {} module-failed", importerCompletions.size(), importerLatency["p95"].get(), importerEmpty, importerModuleFailed); } @@ -3434,22 +3568,56 @@ class Scenario { // State changes in the busiest minute, and the times the status was `error`. std::vector changes; + std::vector changeTraceIndices; + Json transitionTrace = Json::array(); + const auto analyzedSamples { samples.size() }; // root_report below may pump later statuses; these alone were counted std::string previous; int errors { 0 }; - for (const auto& sample : samples) { + for (std::size_t i { 0 }; i < samples.size(); ++i) { + const auto& sample = samples[i]; + const bool changed { !previous.empty() && sample.state != previous && sample.state != "restart" }; + // Only initial states, reset boundaries and counted changes: no full status or paths. + // Integer timestamps preserve the strict <60s boundary without JSON-double rounding. + if (previous.empty() || changed || sample.state == "restart") { + transitionTrace.push_back(Json { + { "sampleIndex", i }, { "seconds", seconds_between(begin_, sample.at) }, + { "sinceStartNanoseconds", std::chrono::duration_cast(sample.at - begin_).count() }, + { "kind", sample.state == "restart" ? "restart" : changed ? "change" : "initial" }, + { "from", previous.empty() ? Json(nullptr) : Json(previous) }, { "to", sample.state }, { "counted", changed }, + { "done", sample.done }, { "total", sample.total }, { "issues", sample.issues } + }); + } if (sample.state == "restart") { previous.clear(); continue; } if (sample.state == "error") ++errors; - if (!previous.empty() && sample.state != previous) changes.push_back(sample.at); + if (changed) { + changes.push_back(sample.at); + changeTraceIndices.push_back(transitionTrace.size() - 1); + } previous = sample.state; } std::size_t busiest { 0 }; + std::size_t busiestBegin { 0 }; for (std::size_t i { 0 }; i < changes.size(); ++i) { std::size_t inWindow { 0 }; for (std::size_t j { i }; j < changes.size() && changes[j] - changes[i] < std::chrono::seconds { 60 }; ++j) ++inWindow; - busiest = std::max(busiest, inWindow); + if (inWindow > busiest) { busiest = inWindow; busiestBegin = i; } + } + Json busiestWindow = nullptr; + if (busiest > 0) { + Json support = Json::array(); + for (std::size_t i { busiestBegin }; i < busiestBegin + busiest; ++i) support.push_back(changeTraceIndices[i]); + const auto start { changes[busiestBegin] }; + busiestWindow = Json { + { "startSeconds", seconds_between(begin_, start) }, + { "endSecondsExclusive", seconds_between(begin_, start + std::chrono::seconds { 60 }) }, + { "startNanoseconds", std::chrono::duration_cast(start - begin_).count() }, + { "endNanosecondsExclusive", std::chrono::duration_cast(start + std::chrono::seconds { 60 } - begin_).count() }, + { "lastChangeSeconds", seconds_between(begin_, changes[busiestBegin + busiest - 1]) }, + { "count", busiest }, { "traceIndices", std::move(support) } + }; } // Ready, and prepared: the first `ready` that held, and the last one after a preparation, from the start of the run. @@ -3491,6 +3659,8 @@ class Scenario { else enforce_max(check, "preparedSeconds", prepared, "preparation finished at", "s", failures); } Json summary { { "statuses", samples.size() }, { "longestStallSeconds", longestStall }, { "stateChanges", changes.size() }, { "busiestMinuteChanges", busiest }, + { "stateTransitionTrace", std::move(transitionTrace) }, { "stateTransitionStatusSamples", analyzedSamples }, + { "stateTransitionObservedThroughSeconds", seconds_between(begin_, now) }, { "busiestMinuteWindow", std::move(busiestWindow) }, { "errorStates", errors }, { "clangdRestarts", root.is_object() ? Json(clangdRestarts) : Json(nullptr) }, { "firstReadySeconds", firstReady ? Json(*firstReady) : Json(nullptr) }, { "preparedSeconds", prepared ? Json(*prepared) : Json(nullptr) }, { "serverRestarts", client_.restartMarks.size() }, diff --git a/src/lsp/connection.cpp b/src/lsp/connection.cpp index 1480746c..7626ed2a 100644 --- a/src/lsp/connection.cpp +++ b/src/lsp/connection.cpp @@ -21,7 +21,6 @@ base::Result> Connection::start(platform::SpawnOptio auto connection = std::make_unique(); connection->process_ = std::move(*process); - connection->framing_ = framing; Connection* self { connection.get() }; connection->reader_ = std::jthread { [self, framing, onMessage = std::move(onMessage), onClosed = std::move(onClosed)] { FrameReader reader; @@ -55,21 +54,35 @@ base::Result> Connection::start(platform::SpawnOptio self->closed_.store(true); if (onClosed) onClosed(); } }; - connection->writer_ = std::jthread { [self] { + connection->writer_ = std::jthread { [self, framing] { while (true) { - std::string frame; + Json message; { std::unique_lock lock { self->outMutex_ }; self->outReady_.wait(lock, [&] { return !self->outgoing_.empty() || self->outClosing_; }); - if (self->outgoing_.empty()) break; // closing, and everything queued is out - frame = std::move(self->outgoing_.front()); + if (self->closed_.load() || self->outgoing_.empty()) break; + message = std::move(self->outgoing_.front()); self->outgoing_.pop_front(); } - if (!self->process_.write(frame)) { - self->writeFailed_.store(true); - break; + // Encoding is also peer work: large JSON must not occupy the event-loop thread. + // Keep the snapshot and all framing work outside the queue lock. + try { + const std::string frame { framing == Framing::lines ? dump(message) + "\n" : encode_frame(message) }; + if (self->process_.write(frame)) continue; + } catch (...) { + // An exception escaping a writer thread would terminate the whole server. + // The failed connection closes its input just as it does after a pipe error. } + self->writeFailed_.store(true); + break; } + { + const std::lock_guard lock { self->outMutex_ }; + self->outClosing_ = true; + } + // No sender can append after observing outClosing_ under the lock. Dispose of + // any unsent snapshots without making a concurrent sender wait for their destructors. + self->outgoing_.clear(); // The input is closed here, by the one thread that writes to it: the peer reads its end and may exit by itself. self->process_.close_input(); } }; @@ -94,14 +107,13 @@ base::Result> Connection::start(platform::SpawnOptio return connection; } -base::Result Connection::send(const Json& message) { +base::Result Connection::send(Json message) { if (closed_.load()) return base::fail("connection-closed", "the peer has exited"); - if (writeFailed_.load()) return base::fail("connection-write", "the peer stopped reading its input"); - std::string frame { framing_ == Framing::lines ? dump(message) + "\n" : encode_frame(message) }; + if (writeFailed_.load()) return base::fail("connection-write", "the connection could not encode or write its queued input"); { const std::lock_guard lock { outMutex_ }; if (outClosing_) return base::fail("connection-closed", "the peer is being stopped"); - outgoing_.push_back(std::move(frame)); + outgoing_.push_back(std::move(message)); } outReady_.notify_one(); return {}; diff --git a/src/lsp/connection.cppm b/src/lsp/connection.cppm index 6fe36bfc..5797a258 100644 --- a/src/lsp/connection.cppm +++ b/src/lsp/connection.cppm @@ -30,11 +30,10 @@ private: std::jthread writer_; std::mutex outMutex_; std::condition_variable outReady_; - std::deque outgoing_; + std::deque outgoing_; bool outClosing_ { false }; // stop(): no more sends; the writer closes the input once the queue is out std::atomic writeFailed_ { false }; std::atomic closed_ { false }; - Framing framing_ { Framing::content_length }; public: Connection() = default; @@ -48,8 +47,9 @@ public: static base::Result> start(platform::SpawnOptions options, MessageHandler onMessage, ClosedHandler onClosed, ErrorLineHandler onErrorLine = {}, Framing framing = Framing::content_length); - // Queues the message for the writer thread; fails only once the peer is gone or a write failed. - base::Result send(const Json& message); + // Queues an owned snapshot; the writer encodes it and writes it in order. Success means + // accepted, not delivered. A later encoding/write failure is visible to subsequent sends. + base::Result send(Json message); // Ends the peer: closes its input, waits up to `grace`, then terminates it. void stop(std::chrono::milliseconds grace); bool closed() const { return closed_.load(); } diff --git a/src/orchestrator/workspace.cpp b/src/orchestrator/workspace.cpp index 1eec5d7c..c293e207 100644 --- a/src/orchestrator/workspace.cpp +++ b/src/orchestrator/workspace.cpp @@ -105,6 +105,15 @@ std::string structure_of(const project::ScanResult& scan) { return key; } +std::optional described_source_structure(std::string_view text, const project::ScanResult& scan) { + // The scanner recognizes ordinary # directives. Alternate directive tokens are + // deliberately unacknowledged instead of guessing how the compiler processes them. + if (text.contains("%:") || text.contains("?" "?=") || text.contains("_Pragma") || text.contains("__pragma") + || text.contains("__LINE__") || text.contains("__COUNTER__") || !scan.preprocessorComplete) return std::nullopt; + if (!scan.importsComplete || !scan.declarationsComplete || std::ranges::any_of(scan.imports, [](const auto& import) { return import.isHeaderUnit; })) return std::nullopt; + return structure_of(scan) + "\npreprocessor-inputs\n" + scan.preprocessorDirectives; +} + // The inputs a producer named in its database's watch list (S2 5), shared with the polling worker. struct WatchPatterns { std::atomic stopped { false }; @@ -478,7 +487,17 @@ struct Workspace::Impl final : engine::Host { int modelGeneration { 0 }; std::string modelDescription; // describe_model of `model`, to recognize an unchanged reload bool loading { false }; + bool applyingPlan { false }; bool reloadAfterLoad { false }; + // A lexical source structure can legitimately differ from the producer's active + // dependency graph (#if 0, for example). Remember only inputs a valid answer covered. + std::map producerSourcePaths; + std::map loadingSourceStructures; + std::map loadingSourceStamps; + std::map describedSourceStructures; + int describedSourceGeneration { -1 }; + std::string loadingSourceProducerIdentity; + std::string describedSourceProducerIdentity; project::TargetSelection targetSelection; bool targetReadPending { false }; int targetReadRetries { 0 }; @@ -2527,6 +2546,51 @@ struct Workspace::Impl final : engine::Host { } } + void capture_source_structures() { + loadingSourceStructures.clear(); + loadingSourceStamps.clear(); + loadingSourceProducerIdentity = producer_file_identity(); + // Only sources reported by the producer's watcher, never a recursive scan. + // Capture before dispatch: a later disk edit is not evidence this run read it. + std::size_t remaining { 32 * 1024 * 1024 }; + for (const auto& [key, path] : producerSourcePaths) { + if (remaining == 0) break; + const auto before = platform::fs::stamp(path); + if (!before || before->size > remaining) continue; + const auto text = platform::fs::read_file(path, remaining); + if (!text) break; // a failed bounded read may already have consumed the remaining budget + remaining -= text->size(); + if (platform::fs::stamp(path) == before) { + if (auto structure = described_source_structure(*text, project::scan_source(*text, true))) { + loadingSourceStructures.emplace(key, std::move(*structure)); + loadingSourceStamps.emplace(key, *before); + } + } + } + } + + void confirm_source_structures(const project::ProjectModel& loaded) { + if (loaded.source == project::SourceKind::inferred || !loaded.issues.empty() || reloadAfterLoad + || loadingSourceProducerIdentity != producer_file_identity() + || (!loaded.producer.empty() && !producerPath.empty() && !base::same_path(loaded.producer, producerPath))) return; + std::set units; + for (const auto& set : loaded.database.sets) { + for (const auto& unit : set.units) units.insert(base::path_key(spec::absolute_source(unit))); + } + describedSourceStructures.clear(); + for (const auto& [key, structure] : loadingSourceStructures) { + const auto path = producerSourcePaths.find(key); + const auto stamp = loadingSourceStamps.find(key); + if (units.contains(key) && path != producerSourcePaths.end() && stamp != loadingSourceStamps.end() + && platform::fs::stamp(path->second) == std::optional { stamp->second } + && matches_watch_entries(loaded.watch, root, path->second)) { + describedSourceStructures.emplace(key, structure); + } + } + describedSourceGeneration = modelGeneration; + describedSourceProducerIdentity = loadingSourceProducerIdentity; + } + void start_model_load() { if (loading) { reloadAfterLoad = true; @@ -2540,6 +2604,7 @@ struct Workspace::Impl final : engine::Host { loading = true; remember_described_inputs(true); const int generation { ++modelGeneration }; + capture_source_structures(); project::LoadOptions load; load.trusted = options.trusted; load.cacheDirectory = cacheDirectory; @@ -2791,6 +2856,7 @@ struct Workspace::Impl final : engine::Host { } keptModelNotice.clear(); staleModelReason.clear(); + confirm_source_structures(*loadedModel); adopt_model(std::move(loadedModel), "producer"); } @@ -3073,11 +3139,20 @@ struct Workspace::Impl final : engine::Host { core_wait_limit().count()); journal.add("engine-waits-for-producer", Json { { "detected", std::string { project::to_string(detectedSource) } } }); } - for (const auto& engine : engines) { - if (coreWaits && engine.get() == coreEngine) continue; - engine->apply(&plan); + { + // apply may synchronously report status before its new plan is installed. + // Publish only after every engine has applied this same workspace plan. + struct StatusBatch { + bool& active; + bool previous; + ~StatusBatch() { active = previous; } + } batch { applyingPlan, std::exchange(applyingPlan, true) }; + for (const auto& engine : engines) { + if (coreWaits && engine.get() == coreEngine) continue; + engine->apply(&plan); + } + for (const Document* document : documents_.all()) publish_diagnostics(document->uri); } - for (const Document* document : documents_.all()) publish_diagnostics(document->uri); update_status(); } @@ -3467,6 +3542,7 @@ struct Workspace::Impl final : engine::Host { void update_status() { if (!initializeAnswered) return; // see the field's own comment + if (applyingPlan) return; // replan publishes the committed snapshot after all synchronous apply calls request_auto_bundles(); const State computed { compute_state() }; State state { computed }; @@ -4250,7 +4326,35 @@ void Workspace::handle_watched_files(const Json& changes) { // pause in typing: on GalTranslPP a minute of CPU each time, beside clangd. An edit inside functions leaves // the model as it is; the index and the plan already have it. // What is on disk is what the producer reads: an open file changed under the editor (a checkout) counts too. - if (producerInput && (type != 2 || !diskText || impl_->model_structure_differs(path, project::scan_source(*diskText)))) reload = true; + if (producerInput) { + const std::string key { base::path_key(path) }; + if (type == 3) { + impl_->producerSourcePaths.erase(key); + impl_->describedSourceStructures.erase(key); + impl_->loadingSourceStructures.erase(key); + impl_->loadingSourceStamps.erase(key); + } else { + impl_->producerSourcePaths.insert_or_assign(key, path); + } + bool differs { true }; + if (type == 2 && diskText) { + const auto scan { project::scan_source(*diskText, true) }; + const auto structure { described_source_structure(*diskText, scan) }; + const auto known = impl_->describedSourceStructures.find(key); + if (impl_->describedSourceGeneration == impl_->modelGeneration && known != impl_->describedSourceStructures.end()) { + differs = !structure || known->second != *structure + || impl_->describedSourceProducerIdentity != impl_->producer_file_identity(); + } else { + // Conditional imports need one actual answer even when the old model + // happens to list the same lexical names (for example #if 1 -> #if 0). + differs = !structure || scan.uncertain || impl_->model_structure_differs(path, scan); + const auto started = impl_->loadingSourceStructures.find(key); + if (started != impl_->loadingSourceStructures.end() + && (!structure || started->second != *structure || !impl_->loading || !impl_->staleModelReason.empty())) differs = true; + } + } + if (differs) reload = true; + } if (open) continue; if (impl_->model && impl_->model->source == project::SourceKind::inferred && type != 2) reload = true; else replan = true; diff --git a/src/project/scan.cpp b/src/project/scan.cpp index 25d28db1..87a1998f 100644 --- a/src/project/scan.cpp +++ b/src/project/scan.cpp @@ -35,11 +35,17 @@ class Lexer { std::size_t at_ { 0 }; bool lineStart_ { true }; int conditionalDepth_ { 0 }; + std::string directives_; + bool retainDirectives_ { false }; + bool directivesComplete_ { true }; public: - explicit Lexer(std::string_view text) : text_ { text }, at_ { base::byte_order_mark_size(text) } {} + explicit Lexer(std::string_view text, bool retainDirectives = false) + : text_ { text }, at_ { base::byte_order_mark_size(text) }, retainDirectives_ { retainDirectives } {} int conditional_depth() const { return conditionalDepth_; } + std::string take_directives() { return std::move(directives_); } + bool directives_complete() const { return directivesComplete_; } // The next token outside comments, literals' interiors and preprocessor directives. std::optional next(bool wantHeaderName) { @@ -71,6 +77,7 @@ class Lexer { directive_(); continue; } + if (c == '#') directivesComplete_ = false; const bool startsLine { lineStart_ }; lineStart_ = false; const std::size_t begin { at_ }; @@ -164,6 +171,7 @@ class Lexer { } void directive_() { + const auto begin { at_ }; std::size_t cursor { at_ + 1 }; while (cursor < text_.size() && is_space(text_[cursor])) ++cursor; std::size_t nameEnd { cursor }; @@ -184,6 +192,10 @@ class Lexer { } ++at_; } + if (retainDirectives_) { + directives_.append(text_.substr(begin, at_ - begin)); + directives_.push_back('\n'); + } lineStart_ = true; } }; @@ -359,9 +371,9 @@ std::vector scan_syntax_tokens(std::string_view text) { return tokens; } -ScanResult scan_source(std::string_view text) { +ScanResult scan_source(std::string_view text, bool retainPreprocessor) { ScanResult result; - Lexer lexer { text }; + Lexer lexer { text, retainPreprocessor }; int braceDepth { 0 }; std::optional token { lexer.next(false) }; while (token) { @@ -387,7 +399,11 @@ ScanResult scan_source(std::string_view text) { const bool isImport { token->text == "import" }; const bool conditional { lexer.conditional_depth() > 0 }; token = lexer.next(isImport); - if (!token) break; + if (!token) { + if (isImport) result.importsComplete = false; + else result.declarationsComplete = false; + break; + } if (isImport && (token->kind == TokenKind::header_name || token->kind == TokenKind::string)) { ImportDeclaration import; @@ -402,16 +418,26 @@ ScanResult scan_source(std::string_view text) { result.imports.push_back(std::move(import)); if (conditional) result.uncertain = true; token = lexer.next(false); - } + } else result.importsComplete = false; continue; } // A contextual keyword is a declaration only when a name or partition follows. const bool nameFollows { token->kind == TokenKind::identifier || (isImport && token->kind == TokenKind::punctuation && token->text == ":") }; - if (!nameFollows) continue; + if (!nameFollows) { + if (isImport) result.importsComplete = false; + else if (token->text != ";") result.declarationsComplete = false; + continue; + } NameParse name { parse_name(lexer, token, isImport) }; - if (!name.ok) continue; + if (!name.ok) { + if (isImport) result.importsComplete = false; + else result.declarationsComplete = false; + continue; + } skip_attributes(lexer, token); if (!token || token->kind != TokenKind::punctuation || token->text != ";") { + if (isImport) result.importsComplete = false; + else result.declarationsComplete = false; // The directive's line ended first: `import hello.greet` with its `;` not typed yet. if (isImport && (!token || token->startsLine)) { const base::Range range { position_in(text, name.begin), position_in(text, name.end) }; @@ -431,6 +457,8 @@ ScanResult scan_source(std::string_view text) { result.uncertain = true; } } + result.preprocessorComplete = lexer.directives_complete(); + result.preprocessorDirectives = lexer.take_directives(); return result; } diff --git a/src/project/scan.cppm b/src/project/scan.cppm index 2625a0d6..c727de95 100644 --- a/src/project/scan.cppm +++ b/src/project/scan.cppm @@ -36,9 +36,15 @@ struct ScanResult { // diagnostics; a module a file needs all the same (required_names). std::vector unterminatedImports; bool uncertain { false }; + // Exact directive logical lines, including continuations and comments. A producer + // may preprocess imports differently even when their lexical names stay the same. + std::string preprocessorDirectives; + bool preprocessorComplete { true }; // false when a code-level # could not be classified as a directive + bool importsComplete { true }; // false when an import needs preprocessing or is not fully parsed + bool declarationsComplete { true }; // false when a module declaration is not fully parsed }; -ScanResult scan_source(std::string_view text); +ScanResult scan_source(std::string_view text, bool retainPreprocessor = false); // A minimal token for syntax highlighting a module declaration or import (design doc 2026-09-25 // K/§7): produced from the text alone, complete or not, so a person still typing `import hello.` diff --git a/src/server/session.cpp b/src/server/session.cpp index 2d15006d..19b1af63 100644 --- a/src/server/session.cpp +++ b/src/server/session.cpp @@ -29,6 +29,32 @@ import mcppls.bundle.writer; namespace mcppls::server { +std::vector workspace_folders(const nlohmann::json& params, std::string_view fallbackDirectory) { + auto folder = [](const std::string& uri) -> std::optional { + auto path = base::uri_to_path(uri); + if (!path || platform::fs::is_regular_file(*path)) return std::nullopt; + return WorkspaceFolder { *path, uri }; + }; + std::vector found; + if (const auto* folders = lsp::find(params, "workspaceFolders"); folders != nullptr && folders->is_array()) { + for (const auto& entry : *folders) { + if (auto uri = lsp::string_at(entry, "uri")) { + if (auto named = folder(*uri)) found.push_back(std::move(*named)); + } + } + if (!found.empty()) return found; + } + if (auto uri = lsp::string_at(params, "rootUri")) { + if (auto named = folder(*uri)) return { std::move(*named) }; + } + if (auto path = lsp::string_at(params, "rootPath"); path && !path->empty() && !platform::fs::is_regular_file(*path)) { + const std::string normalized { base::normalize_path(*path) }; + return { WorkspaceFolder { normalized, base::path_to_uri(normalized) } }; + } + const std::string current { fallbackDirectory }; + return { WorkspaceFolder { current, base::path_to_uri(current) } }; +} + namespace { using Json = nlohmann::json; @@ -517,38 +543,18 @@ class Session { } // A workspace folder: the path it names, and the URI the client named it by. - struct Folder { - std::string path; - std::string uri; - }; + using Folder = WorkspaceFolder; static std::optional folder_of_uri_(const std::string& uri) { auto path = base::uri_to_path(uri); - if (!path) return std::nullopt; + if (!path || platform::fs::is_regular_file(*path)) return std::nullopt; return Folder { *path, uri }; } // Every folder `initialize` names, or the single root a pre-3.6 client implies (design 13.1 // predates multi-root; usable plan W9.1 is additive over it). std::vector workspace_roots_(const Json& params) const { - std::vector found; - if (const Json* folders = lsp::find(params, "workspaceFolders"); folders != nullptr && folders->is_array()) { - for (const auto& folder : *folders) { - if (auto uri = lsp::string_at(folder, "uri")) { - if (auto named = folder_of_uri_(*uri)) found.push_back(std::move(*named)); - } - } - if (!found.empty()) return found; - } - if (auto uri = lsp::string_at(params, "rootUri")) { - if (auto named = folder_of_uri_(*uri)) return { std::move(*named) }; - } - if (auto path = lsp::string_at(params, "rootPath"); path && !path->empty()) { - const std::string normalized { base::normalize_path(*path) }; - return { Folder { normalized, base::path_to_uri(normalized) } }; - } - const std::string current { platform::fs::current_directory() }; - return { Folder { current, base::path_to_uri(current) } }; + return workspace_folders(params, platform::fs::current_directory()); } // `isFirst` wires the session's own `initialize` response to this root's engine settling diff --git a/src/server/session.cppm b/src/server/session.cppm index abe63b86..c0696e1c 100644 --- a/src/server/session.cppm +++ b/src/server/session.cppm @@ -5,12 +5,22 @@ export module mcppls.server.session; import std; +import nlohmann.json; export import mcppls.orchestrator.workspace; export namespace mcppls::server { using orchestrator::SessionOptions; +struct WorkspaceFolder { + std::string path; + std::string uri; +}; + +// Resolves initialize's workspace folders, retaining the client's URI spelling. +// Existing regular files are not workspace directories; absent directories remain valid. +std::vector workspace_folders(const nlohmann::json& params, std::string_view fallbackDirectory); + // Serves the Language Server Protocol on standard input and output until the client exits. // Returns the process exit code. int run_session(const SessionOptions& options); diff --git a/tests/test_answer_routing.cpp b/tests/test_answer_routing.cpp index f08fda85..0ffa1bea 100644 --- a/tests/test_answer_routing.cpp +++ b/tests/test_answer_routing.cpp @@ -145,14 +145,17 @@ struct Fixture { std::map versions; int nextId { 1 }; - explicit Fixture(std::string at = canonical_root(), std::string cache = "on", bool ready = true, std::string agent = "cached") : root { std::move(at) } { + explicit Fixture(std::string at = canonical_root(), std::string cache = "on", bool ready = true, std::string agent = "cached", + std::function(std::shared_ptr)> coreFactory = {}) : root { std::move(at) } { o::SessionOptions options; options.answerCache = cache; options.answerAgent = agent; options.answerPrefetch = false; - options.engineFactories = [held = held](const auto&, const auto&, bool) { + options.engineFactories = [held = held, coreFactory](const auto&, const auto&, bool) { o::EngineFactories factories; - factories.core = [held] { return std::make_unique(held); }; + factories.core = [held, coreFactory]() -> std::unique_ptr { + return coreFactory ? coreFactory(held) : std::make_unique(held); + }; return factories; }; workspace = std::make_unique(root, root, options, e::PayloadPaths {}, false, false, "", events, sink); @@ -161,15 +164,19 @@ struct Fixture { } workspace->start(Json { { "clientInfo", Json { { "name", "Visual Studio Code" } } }, { "capabilities", Json { { "experimental", Json { { "cxxModules", Json { { "version", 1 }, { "completionReady", ready } } } } } } } }, - false, false); + static_cast(coreFactory), false); load_model(); } // The project: a.cpp and b.cpp, b.cpp compiled with `extra` too. - void load_model(const std::vector& extra = {}, bool engineInterface = false) { + void load_model(const std::vector& extra = {}, bool engineInterface = false, std::string target = {}, int generation = 1) { auto model = std::make_shared(); model->root = root; model->source = mcppls::project::SourceKind::build_database; + if (!target.empty()) { + model->profile.kind = "build-toolchain"; + model->profile.target = std::move(target); + } mcppls::spec::Set set; set.name = "app"; set.hasIde = true; @@ -206,7 +213,7 @@ struct Fixture { model->database.sets.push_back(std::move(engineSet)); } // As the build tool's answer to the load start() began (generation 1); a later one replaces it. - workspace->handle_model_loaded(1, std::move(model)); + workspace->handle_model_loaded(generation, std::move(model)); } ~Fixture() { @@ -280,6 +287,7 @@ constexpr std::string_view A { "#include \nvoid f() {\n doc.size();\n s int main() { using namespace mcppls::testing; + const auto arguments = mcppls::platform::env::arguments(); if (arguments.size() == 1) { // The workspaces keep their answers under the cache: isolated, so a user's cache is never touched. @@ -304,6 +312,174 @@ int main() { std::print(std::cerr, "{}", result->error); return result->timedOut ? 1 : result->exitCode; } + "watched source acknowledgement covers successful run-start inputs, not failures or later disk edits"_test = [] { + for (const int scenario : { 0, 1, 2, 3, 4, 5 }) { + const bool failed { scenario == 1 }, restored { scenario == 2 }; + Fixture f; + const auto source = [](std::string_view hidden, int body) { + return std::format("import active;\n#if 0\nimport {};\n#endif\nint value() {{ return {}; }}\n", hidden, body); + }; + const std::string path { mcppls::base::join_path(f.root, "a.cpp") }; + const std::string initial { scenario == 5 ? "#define SELECT(x) x\nimport SELECT(active);\nint value() { return 0; }\n" + : scenario >= 3 ? "#if 1\nimport active;\n#endif\nint value() { return 0; }\n" + : "import active;\nint value() { return 0; }\n" }; + expect(fs::write_file(path, initial).has_value()); + expect(fs::write_file(mcppls::base::join_path(f.root, "b.cpp"), "export module active;\n").has_value()); + const auto loaded = [&](int generation, bool success) { + auto model = std::make_shared(); + model->root = f.root; + model->detected = mcppls::project::SourceKind::build_database; + if (success) { + model->source = mcppls::project::SourceKind::build_database; + model->watch = { "a.cpp" }; + mcppls::spec::Set set; + set.name = "app"; + for (const char* name : { "a.cpp", "b.cpp" }) { + mcppls::spec::TranslationUnit unit; + unit.source = mcppls::base::join_path(f.root, name); + unit.workDirectory = f.root; + unit.arguments = { "clang++", "-std=c++23", "-c", unit.source }; + if (std::string_view { name } == "a.cpp") unit.requiredModules = { "active" }; + else unit.providedModules = { { "active", "" } }; + set.units.push_back(std::move(unit)); + } + model->database.sets.push_back(std::move(set)); + } else model->issues.emplace_back("producer-failed", "the controlled producer returned no model"); + f.workspace->handle_model_loaded(generation, std::move(model)); + }; + const auto watched = [&] { + f.workspace->handle_watched_files(Json::array({ Json { + { "uri", f.uri("a.cpp") }, { "type", 2 } + } })); + }; + loaded(1, true); + if (scenario == 5) { + watched(); + f.tick(1600ms); + expect(fatal(f.workspace->report()["state"] == "loading")); + loaded(2, true); // SELECT(active) really imports active; this answer is valid. + expect(fs::write_file(path, "#define SELECT(x) x\nimport SELECT(other);\nint value() { return 0; }\n").has_value()); + watched(); + f.tick(1600ms); + expect(f.workspace->report()["state"] == "loading") + << "the same macro definition with another import argument needs a new producer answer"; + continue; + } + if (scenario >= 3) { + if (scenario == 4) { + // The source's active dependency exactly matches the producer's model. + // A successful refresh establishes a baseline for that same structure. + watched(); + f.tick(1600ms); + expect(fatal(f.workspace->report()["state"] == "loading")); + loaded(2, true); + } + expect(fs::write_file(path, "#if 0\nimport active;\n#endif\nint value() { return 0; }\n").has_value()); + watched(); + f.tick(1600ms); + expect(f.workspace->report()["state"] == "loading") + << "disabling the import changes effective dependencies even though lexical names still match the old model; baseline=" << (scenario == 4); + continue; + } + expect(fs::write_file(path, source("hidden.alpha", 1)).has_value()); + watched(); + const auto beforeRun = fs::stamp(path); + f.tick(1600ms); // The existing producer debounce, not a status hold. + expect(fatal(f.workspace->report()["state"] == "loading")); + if (!failed) { + // A checkout changes disk after the run began, before its watch reaches us. + expect(fs::write_file(path, source("hidden.beta", 2)).has_value()); + if (restored) { + expect(fs::write_file(path, source("hidden.alpha", 1)).has_value()); + expect(fatal(fs::stamp(path) != beforeRun)) << "the A -> B -> A control changed the original file stamp"; + } + } + loaded(2, !failed); + expect(fs::write_file(path, source(failed || restored ? "hidden.alpha" : "hidden.beta", 3)).has_value()); + watched(); + f.tick(1600ms); + expect(f.workspace->report()["state"] == "loading") + << (failed ? "a failed answer cannot acknowledge this structure: retry on the next body save" + : restored ? "a changed stamp cannot acknowledge A -> B -> A while the producer ran" + : "a run cannot acknowledge the later disk structure it did not start with"); + loaded(3, true); + expect(fs::write_file(path, source(failed || restored ? "hidden.alpha" : "hidden.beta", 4)).has_value()); + watched(); + f.tick(1600ms); + expect(f.workspace->report()["state"] != "loading") << "a valid unchanged answer acknowledges the run-start structure"; + } + }; + + "a model's status is published only after its synchronous engine apply commits"_test = [] { + struct Probe { + Sink* sink { nullptr }; + bool notifyDuringApply { false }; + unsigned callbacks { 0 }; + unsigned commits { 0 }; + std::vector intermediate; + }; + // clangd's apply calls update_quarantine_issue_ -> host.status_changed() + // before updating the primer and scheduling the new plan's restart. Model that + // permitted reentrant callback, without inventing a timer or an engine failure. + class ApplyingCore : public Core { + public: + ApplyingCore(std::shared_ptr held, std::shared_ptr probe) + : Core { std::move(held) }, probe_ { std::move(probe) } {} + void start(e::Host& host) override { + host_ = &host; + Core::start(host); + } + void apply(const mcppls::normalize::EnginePlan* plan) override { + if (plan == nullptr || probe_->sink == nullptr) return; + if (probe_->notifyDuringApply) { + const auto before = probe_->sink->sent.size(); + ++probe_->callbacks; + host_->status_changed(); + for (std::size_t i { before }; i < probe_->sink->sent.size(); ++i) { + const auto& message = probe_->sink->sent[i]; + if (message.value("method", std::string {}) == "cxxModules/status") { + probe_->intermediate.push_back(message["params"]); + } + } + } + ++probe_->commits; + } + private: + std::shared_ptr probe_; + e::Host* host_ { nullptr }; + }; + for (const bool reentrant : { false, true }) { + auto probe = std::make_shared(); + probe->notifyDuringApply = reentrant; + Fixture f { canonical_root(), "off", true, "cached", + [probe](std::shared_ptr held) -> std::unique_ptr { + return std::make_unique(std::move(held), probe); + } }; + f.workspace->allow_status_notifications(); + // Use the existing explicit refresh API: its asynchronous response is delivered + // below as generation 2. No sleep or status coalescing timeout is involved. + f.workspace->apply_build_program(); + expect(f.workspace->report()["state"] == "loading") << "the replacement model is being described"; + f.sink.sent.clear(); + probe->sink = &f.sink; + f.load_model({}, false, "aarch64-unknown-linux-gnu", 2); + expect(probe->commits == 1u) << "the model actually reached its engine"; + expect(probe->callbacks == (reentrant ? 1u : 0u)) << "the callback control was exercised"; + expect(probe->intermediate.empty()) + << "a synchronous apply callback exposed the new profile before engine commit: " << Json(probe->intermediate).dump(); + const auto statuses = f.sink.notifications("cxxModules/status"); + expect(statuses > 0u) << "the committed model still needs a status notification"; + const auto last = std::ranges::find_if(f.sink.sent.rbegin(), f.sink.sent.rend(), [](const Json& message) { + return message.value("method", std::string {}) == "cxxModules/status"; + }); + if (last != f.sink.sent.rend()) { + expect((*last)["params"]["profile"]["target"] == "aarch64-unknown-linux-gnu"); + } + // No async work or elapsed-time hold makes this assertion pass: both cases + // use the same completed apply, with only the real callback pattern toggled. + probe->sink = nullptr; + } + }; "rename refuses an engine-interface owner even when the compiler edits only project references"_test = [] { for (int shape { 0 }; shape < 3; ++shape) { Fixture f; diff --git a/tests/test_conformance.cpp b/tests/test_conformance.cpp index eda968d9..fdbde07b 100644 --- a/tests/test_conformance.cpp +++ b/tests/test_conformance.cpp @@ -53,6 +53,11 @@ int serve() { const bool never { std::ranges::find(arguments, "--drain-never") != arguments.end() }; const bool missing { std::ranges::find(arguments, "--drain-missing") != arguments.end() }; const bool restarting { std::ranges::find(arguments, "--drain-restarting") != arguments.end() }; + const bool timelineStates { std::ranges::find(arguments, "--timeline-states") != arguments.end() }; + const bool completionTrace { std::ranges::find(arguments, "--completion-trace") != arguments.end() }; + const bool completionOverflow { std::ranges::find(arguments, "--completion-trace-overflow") != arguments.end() }; + int traceReports { 0 }; + std::optional lateCompletion; int reports { 0 }, changes { 0 }, completions { 0 }; const auto send = [](const Json& message) { return platform::stdio::write_output(lsp::encode_frame(message)); }; const auto status = [&](std::string state) { return send(lsp::make_notification("cxxModules/status", Json { { "state", std::move(state) } })); }; @@ -66,6 +71,28 @@ int serve() { const Json& request { **frame }; const std::string method { request.value("method", std::string {}) }; if (method == "exit") return 0; + if (timelineStates && method == "initialized") { + const auto sample = [&](std::string state, int done, int total, std::string issue = {}) { + Json issues = Json::array(); + if (!issue.empty()) issues.push_back(Json { { "code", issue }, { "message", "private-status-text" } }); + return send(lsp::make_notification("cxxModules/status", Json { + { "state", std::move(state) }, { "progress", Json { { "done", done }, { "total", total } } }, { "issues", std::move(issues) } + })); + }; + (void)sample("loading", 0, 3); + (void)sample("loading", 1, 3); // Progress changes are not state transitions. + (void)sample("preparing", 1, 3, "preparation-stalled"); + (void)sample("preparing", 2, 3); + (void)sample("ready", 3, 3); + (void)sample("error", 3, 3, "mock-state-error"); + (void)sample("ready", 3, 3); + // Exercise the exact sentinel recorded by Client::reset without a slow OS kill. + (void)sample("restart", 0, 0); + (void)sample("ready", 0, 2); + (void)sample("loading", 0, 2); + (void)sample("ready", 2, 2); + (void)sample("ready", 2, 2); + } if (drain && method == "initialized") { if (!cacheMode.empty()) { (void)fs::create_directories(base::parent_path(readCopy)); @@ -89,6 +116,16 @@ int serve() { if (method == "initialize") result = Json { { "capabilities", Json { { "experimental", Json { { "cxxModules", Json::object() } } }, { "hoverProvider", true } } } }; if (method == "textDocument/completion") { ++completions; + if (completionTrace && completions == 1) std::this_thread::sleep_for(std::chrono::milliseconds { 30 }); + if (completionOverflow && completions == 270) std::this_thread::sleep_for(std::chrono::milliseconds { 500 }); + if (completionTrace && completions == 2) { + (void)send(lsp::make_error(request["id"], -32001, "private-completion-error")); + continue; + } + if (completionTrace && completions == 3) { + lateCompletion = request["id"]; + continue; // It arrives only with the post-sampling report, after statistics are frozen. + } result = Json { { "isIncomplete", completions == 1 }, { "items", Json::array({ Json { { "label", "private-completion-text" } } }) } }; if (completions == 2) result["items"].push_back(Json { { "label", "second-private-item" } }); @@ -101,6 +138,30 @@ int serve() { { "details", Json { { "preparation", Json { { "runningUnits", Json::array({ Json { { "module", "sample" }, { "seconds", 12 } } }) } } } } } } }) } } }) } }; + if ((completionTrace || completionOverflow) && method == "cxxModules/report") { + ++traceReports; + if (traceReports == 2 && lateCompletion) { + (void)send(lsp::make_result(*lateCompletion, Json { { "items", Json::array({ Json { { "label", "private-late-item" } } }) } })); + lateCompletion.reset(); + } + result["traceObservation"] = Json { { "completionRequests", completions }, { "reportRequests", traceReports } }; + if (completionTrace) { + result["roots"][0]["requests"] = Json { { "textDocument/completion", Json { + { "count", 10 + completions }, { "empty", 0 }, { "errors", completions > 0 ? 1 : 0 }, { "cancelled", 0 }, + { "p50Ms", 12 }, { "p95Ms", 34 }, { "maxMs", 8765 }, { "engineAnswered", 7 }, + { "engineP50Ms", 10 }, { "engineP95Ms", 30 }, { "overheadP50Ms", 2 }, { "overheadP95Ms", 4 }, + { "answeredBy", Json { { "clangd", 7 } } }, { "lastAt", "fake-at" }, { "file", "private-method-file" } + } } }; + result["roots"][0]["eventTotals"] = Json { { "slow-request", traceReports == 1 ? 5 : 8 } }; + Json slowEvents = Json::array(); + for (int index { 0 }; index < 4; ++index) slowEvents.push_back(Json { + { "at", std::format("fake-at-{}", index) }, { "kind", "slow-request" }, + { "detail", Json { { "method", "textDocument/completion" }, { "ms", 8765 + index }, + { "outcome", "result" }, { "answeredBy", "clangd" }, { "file", "private-slow-request-file" } } } + }); + result["roots"][0]["events"] = std::move(slowEvents); + } + } if (drain && method == "cxxModules/report") { const bool busy { !cacheMode.empty() ? (++reports < 2 || cacheMode == "--cache-never") : !restarting && (never || ++reports < 3) }; @@ -226,6 +287,54 @@ int main() { expect(result->output.find("engine work evidence unavailable") != std::string::npos); } }; + "a failed timeline preserves exact counted transitions and restart boundaries"_test = [&] { + const Json sample { { "name", "timing" }, { "server-arguments", Json::array({ "--timeline-states" }) }, + { "checks", Json::array({ + Json { { "id", "PUMP" }, { "kind", "hover-contains" }, { "file", "main.cpp" }, { "at", Json::array({ 0, 0 }) }, { "expect", "中文" } }, + Json { { "id", "STATES" }, { "kind", "timeline" }, { "budget", Json { { "maxChangesPerMinute", 5 } } } } + }) } + }; + expect(fs::write_file(base::join_path(fixture, "scenario.json"), sample.dump()).has_value()); + const auto result = invoke(measure, evidence); + expect(fatal(result.has_value())); + expect(result->exitCode == 1) << result->output << result->error; + const auto document = fs::read_file(measure); + expect(fatal(document.has_value())); + const Json summary = Json::parse(*document); + expect(summary["checks"][0]["ok"] == true); + expect(summary["checks"][1]["ok"] == false); + const Json& timeline = summary["checks"][1]["measure"]; + expect(timeline["stateChanges"] == 6 && timeline["busiestMinuteChanges"] == 6); + expect(fatal(timeline.contains("stateTransitionTrace") && timeline.contains("stateTransitionStatusSamples") + && timeline.contains("busiestMinuteWindow"))) << "the failing gate must retain its transition evidence"; + expect(timeline["stateTransitionStatusSamples"] == 12 && timeline["errorStates"] == 1); + const Json& trace = timeline["stateTransitionTrace"]; + expect(fatal(trace.is_array() && trace.size() == 9)); + expect(trace[0]["kind"] == "initial" && trace[0]["from"].is_null() && trace[0]["counted"] == false); + expect(trace[1]["from"] == "loading" && trace[1]["to"] == "preparing"); + expect(trace[1]["done"] == 1 && trace[1]["total"] == 3 && trace[1]["issues"] == Json::array({ "preparation-stalled" })); + expect(trace[5]["kind"] == "restart" && trace[5]["to"] == "restart" && trace[5]["counted"] == false); + expect(trace[6]["kind"] == "initial" && trace[6]["from"].is_null() && trace[6]["counted"] == false); + Json counted = Json::array(); + std::int64_t previous { -1 }; + for (std::size_t i { 0 }; i < trace.size(); ++i) { + const auto at { trace[i]["sinceStartNanoseconds"].get() }; + expect(at >= previous && trace[i]["seconds"].get() >= 0.0); + previous = at; + if (trace[i]["counted"].get()) counted.push_back(i); + } + expect(counted.size() == timeline["stateChanges"].get()); + const Json& window = timeline["busiestMinuteWindow"]; + expect(window["traceIndices"] == counted && window["count"] == counted.size()); + const auto start { window["startNanoseconds"].get() }; + const auto end { window["endNanosecondsExclusive"].get() }; + expect(end - start == 60'000'000'000LL); + for (const auto& index : window["traceIndices"]) { + const auto at { trace[index.get()]["sinceStartNanoseconds"].get() }; + expect(at >= start && at < end); + } + expect(trace.dump().find("private-status-text") == std::string::npos) << "keep only issue codes, no full status"; + }; "typing trace preserves per-word requests and records the original completion metadata"_test = [&] { const Json sample { { "name", "timing" }, { "checks", Json::array({ Json { { "id", "WORDS" }, { "kind", "typing" }, { "file", "main.cpp" }, { "seconds", 1 }, { "hz", 5 }, @@ -261,6 +370,111 @@ int main() { } expect(trace.dump().find("private") == std::string::npos) << "trace keeps counts, never completion text"; }; + "failed periodic typing keeps exact completion evidence at the original statistics boundary"_test = [&] { + const Json sample { { "name", "timing" }, { "server-arguments", Json::array({ "--completion-trace" }) }, + { "checks", Json::array({ Json { { "id", "PERIODIC" }, { "kind", "typing" }, { "file", "main.cpp" }, + { "seconds", 1 }, { "hz", 5 }, { "completion-every-ms", 100 }, { "requestTimeout", 0 }, + { "scripts", Json::array({ Json { { "line", 0 }, { "text", "abc" } } }) }, + { "budget", Json { { "max", 0 } } } + } }) } }; + expect(fs::write_file(base::join_path(fixture, "scenario.json"), sample.dump()).has_value()); + const auto result = invoke(measure, evidence); + expect(fatal(result.has_value())); + expect(result->exitCode == 1) << result->output << result->error; + const auto text = fs::read_file(measure); + expect(fatal(text.has_value())); + const Json summary = Json::parse(*text); + expect(summary["checks"][0]["ok"] == false); + const Json& typed = summary["checks"][0]["measure"]; + expect(typed["reask"] == "" && !typed.contains("wordTrace")); + expect(fatal(typed.contains("completionTrace"))); + const Json& trace = typed["completionTrace"]; + const Json& entries = trace["entries"]; + expect(trace["origin"] == "typing-start" && trace["population"] == "original-completion-statistics"); + expect(trace["limit"] == 256 && trace["dropped"] == 0); + expect(fatal(entries.size() >= 3 && entries.size() == typed["completions"].get())); + expect(trace["total"] == entries.size()); + expect(entries[0]["items"] == 1 && entries[0]["isIncomplete"] == true); + expect(entries[1]["error"] == true && entries[1]["items"].is_null()); + expect(entries[2]["answeredNanoseconds"].is_null() && entries[2]["elapsedNanoseconds"].is_null()); + expect(entries[2]["elapsedSeconds"].is_null()) << "the late response must not rewrite the original timeout"; + std::vector answered; + std::size_t errors { 0 }, unanswered { 0 }; + std::int64_t previousId { -1 }, previousSent { -1 }; + for (std::size_t index { 0 }; index < entries.size(); ++index) { + const auto& entry = entries[index]; + expect(entry["index"] == index); + expect(entry["id"].is_number_integer() && entry["sentNanoseconds"].is_number_integer()); + const auto id = entry["id"].get(); + const auto sent = entry["sentNanoseconds"].get(); + expect(id > previousId && sent >= previousSent && sent >= 0); + previousId = id; + previousSent = sent; + if (entry["answeredNanoseconds"].is_null()) { ++unanswered; continue; } + expect(entry["answeredNanoseconds"].is_number_integer() && entry["elapsedNanoseconds"].is_number_integer()); + expect(entry["answeredNanoseconds"].get() - sent == entry["elapsedNanoseconds"].get()); + expect(entry["elapsedNanoseconds"].get() >= 0); + if (entry["error"].get()) ++errors; + else answered.push_back(entry["elapsedSeconds"].get()); + } + expect(unanswered == 1 && typed["clientTimeouts"] == unanswered); + expect(errors == 1 && typed["errors"] == errors); + std::ranges::sort(answered); + expect(fatal(!answered.empty())); + expect(typed["answered"] == answered.size() && typed["completion"]["n"] == answered.size()); + for (const auto& [field, fraction] : std::array, 2> {{ { "p50", 0.5 }, { "p95", 0.95 } }}) { + const auto rank = static_cast(std::ceil(fraction * static_cast(answered.size()))); + expect(typed["completion"][std::string { field }] == answered[rank - 1]); + } + expect(typed["completion"]["max"] == answered.back()); + expect(trace["slowestSuccessful"]["elapsedSeconds"] == answered.back()); + expect(trace.dump().find("private") == std::string::npos && trace.dump().find("main.cpp") == std::string::npos); + const Json& server = typed["serverCompletionEvidence"]; + expect(server["requestsBefore"]["count"] == 10); + expect(server["requestsAfter"]["count"] == 10 + entries.size()); + expect(server["requestsAfter"]["engineP95Ms"] == 30 && server["requestsAfter"]["overheadP95Ms"] == 4); + expect(server["recentSampleLimit"] == 256); + const Json& slow = server["slowRequests"]; + expect(slow["totalBefore"] == 5 && slow["totalAfter"] == 8 && slow["added"] == 3); + expect(slow["afterRecentCount"] == 4 && slow["droppedByTraceLimit"] == 0); + expect(slow["newRetained"] == 3 && slow["newNotRetained"] == 0); + expect(slow["eventsAfter"].size() == 4 && slow["eventsAfter"][0].size() == 5); + expect(slow["eventsAfter"][0]["at"] == "fake-at-0" && slow["eventsAfter"][0]["ms"] == 8765); + expect(server.dump().find("private") == std::string::npos) << "only public method statistics and whitelisted event fields"; + const auto artifact = fs::read_file(evidence); + expect(fatal(artifact.has_value())); + const Json observed = Json::parse(*artifact)["report"]["traceObservation"]; + expect(observed["completionRequests"] == entries.size() && observed["reportRequests"] == 3) + << "tracing must add neither completion requests nor reports"; + }; + "truncated completion traces still identify the original slowest response"_test = [&] { + const Json sample { { "name", "timing" }, { "server-arguments", Json::array({ "--completion-trace-overflow" }) }, + { "checks", Json::array({ Json { { "id", "OVERFLOW" }, { "kind", "typing" }, { "file", "main.cpp" }, + { "seconds", 1 }, { "hz", 1000 }, { "completion-every-ms", 0 }, { "reask", "always" }, { "requestTimeout", 5 }, + { "scripts", Json::array({ Json { { "line", 0 }, { "text", std::string(300, 'a') } } }) } + } }) } }; + expect(fs::write_file(base::join_path(fixture, "scenario.json"), sample.dump()).has_value()); + const auto result = invoke(measure, evidence); + expect(fatal(result.has_value())); + expect(result->exitCode == 0) << result->output << result->error; + const auto text = fs::read_file(measure); + expect(fatal(text.has_value())); + const Json typed = Json::parse(*text)["checks"][0]["measure"]; + expect(fatal(typed.contains("completionTrace"))); + const Json& trace = typed["completionTrace"]; + const auto total = typed["completions"].get(); + expect(total >= 300 && trace["entries"].size() == 256); + expect(trace["total"] == total && trace["dropped"] == total - 256); + expect(trace["slowestSuccessful"]["elapsedSeconds"] == typed["completion"]["max"]); + expect(trace["slowestAnswered"]["elapsedSeconds"] == typed["completion"]["max"]); + expect(trace["slowestSuccessful"]["index"].get() >= 256) + << "the deliberately slow response beyond the prefix must survive truncation"; + expect(typed["answered"] == total && typed["completion"]["n"] == total); + expect(trace.dump().find("private") == std::string::npos); + const Json& server = typed["serverCompletionEvidence"]; + expect(server["requestsBefore"].is_null() && server["requestsAfter"].is_null()); + expect(server["slowRequests"]["eventsAfter"].is_null() && server["slowRequests"]["newRetained"].is_null()); + }; "module reuse drains partial read copies and preserves cold, rebuild and missing-state checks"_test = [&] { // Owned payloads are Linux's cache representation; its source scanner uses POSIX paths. if constexpr (mcppls::os::FAMILY != mcppls::os::Family::linux) return; diff --git a/tests/test_process.cpp b/tests/test_process.cpp index 20754d63..1c9f0996 100644 --- a/tests/test_process.cpp +++ b/tests/test_process.cpp @@ -229,10 +229,128 @@ int main() { sent = sent && (*connection)->send(nlohmann::json { { "jsonrpc", "2.0" }, { "method", "m" }, { "params", big } }).has_value(); } expect(sent); - expect(std::chrono::steady_clock::now() - started < 2s) << "4 MB to a peer that reads none of it"; + const auto sendElapsed = std::chrono::steady_clock::now() - started; + expect(sendElapsed < 2s) << "4 MB to a peer that reads none of it; actual " + << std::chrono::duration(sendElapsed).count() << " s"; const auto stopping = std::chrono::steady_clock::now(); (*connection)->stop(200ms); - expect(std::chrono::steady_clock::now() - stopping < 10s) << "the writer, blocked in a write, lets go once the peer is gone"; + const auto stopElapsed = std::chrono::steady_clock::now() - stopping; + std::println("connection nonreader evidence: 64 x 64 KiB send={:.9f}s; stop={:.9f}s", + std::chrono::duration(sendElapsed).count(), std::chrono::duration(stopElapsed).count()); + expect(stopElapsed < 10s) << "the writer, blocked in a write, lets go once the peer is gone; actual " + << std::chrono::duration(stopElapsed).count() << " s"; + }; + + "connection writer drains owned snapshots in FIFO order through both framings"_test = [&] { + using namespace std::chrono_literals; + using Json = nlohmann::json; + for (const auto framing : { mcppls::lsp::Framing::content_length, mcppls::lsp::Framing::lines }) { + const std::string_view framingName { framing == mcppls::lsp::Framing::lines ? "lines" : "content-length" }; + struct Received { + std::mutex mutex; + std::condition_variable ready; + std::vector messages; + bool release { false }; + bool gateTimedOut { false }; + bool closed { false }; + } received; + auto connection = mcppls::lsp::Connection::start( + { .program = self, .arguments = { "--echo" } }, + [&](Json message) { + std::unique_lock lock { received.mutex }; + received.messages.push_back(std::move(message)); + received.ready.notify_all(); + // Hold the first reply while later messages queue. A large following frame + // also supplies backpressure, so the writer must own queued nested values. + if (received.messages.size() == 1 + && !received.ready.wait_for(lock, 20s, [&] { return received.release; })) received.gateTimedOut = true; + }, + [&] { + const std::lock_guard lock { received.mutex }; + received.closed = true; + received.ready.notify_all(); + }, {}, framing); + expect(fatal(connection.has_value())) << framingName; + // On any assertion/exception unwind, release the callback before Connection joins it. + struct Release { + Received& received; + void open() { + { const std::lock_guard lock { received.mutex }; received.release = true; } + received.ready.notify_all(); + } + ~Release() { open(); } + } release { received }; + std::vector expected; + const auto send = [&](Json message) { + expected.push_back(message); + return (*connection)->send(std::move(message)).has_value(); + }; + bool sent { send(Json { { "jsonrpc", "2.0" }, { "id", 0 }, { "result", "first" } }) }; + bool firstArrived { false }; + { + std::unique_lock lock { received.mutex }; + firstArrived = received.ready.wait_for(lock, 20s, [&] { return !received.messages.empty() || received.closed; }) + && !received.messages.empty(); + } + expect(firstArrived) << framingName << " first echo did not arrive"; + sent = send(Json { { "jsonrpc", "2.0" }, { "id", 1 }, { "result", std::string(4 * 1024 * 1024, 'b') } }) && sent; + Json original { { "jsonrpc", "2.0" }, { "id", 2 }, + { "result", { { "nested", { { "text", "h\xC3\xA9llo \xF0\x9F\x98\x80\n" }, + { "values", Json::array({ 1, 2, 3 }) } } } } } }; + expected.push_back(original); + sent = (*connection)->send(original).has_value() && sent; + original["result"]["nested"]["text"] = "changed after send"; + original["result"]["nested"]["values"][0] = 99; + original.clear(); + { + Json temporary { { "jsonrpc", "2.0" }, { "id", 3 }, { "result", Json::array({ "temporary", 7 }) } }; + expected.push_back(temporary); + sent = (*connection)->send(std::move(temporary)).has_value() && sent; + } // The moved-from source is destroyed before the writer is released. + Json invalid { { "jsonrpc", "2.0" }, { "id", 4 }, { "result", std::string { "A\xFF\xFE" "Z" } } }; + expected.push_back(Json { { "jsonrpc", "2.0" }, { "id", 4 }, { "result", "A\xEF\xBF\xBD\xEF\xBF\xBD" "Z" } }); + sent = (*connection)->send(std::move(invalid)).has_value() && sent; + sent = send(Json { { "jsonrpc", "2.0" }, { "id", 5 }, { "result", "last" } }) && sent; + release.open(); + // Stop without first waiting for all replies: normal shutdown must drain the queue, + // close stdin, then allow --echo to finish and the reader to deliver every reply. + (*connection)->stop(20s); + expect(sent) << framingName; + expect(!received.gateTimedOut) << framingName << " first-reply gate timed out"; + expect(received.closed && (*connection)->closed()) << framingName; + expect(received.messages.size() == expected.size()) << framingName << " expected " << expected.size() + << " replies, received " << received.messages.size(); + for (std::size_t i { 0 }; i < std::min(received.messages.size(), expected.size()); ++i) { + expect(received.messages[i] == expected[i]) << framingName << " reply " << i << " differs from its sent snapshot"; + } + expect(!(*connection)->send(Json { { "after", "stop" } }).has_value()) << framingName; + (*connection)->stop(200ms); // Repeated cleanup must remain harmless after a normal drain. + } + }; + + "connection reports a departed peer and stopping it cleans up"_test = [&] { + using namespace std::chrono_literals; + std::mutex mutex; + std::condition_variable ready; + bool closed { false }; + auto connection = mcppls::lsp::Connection::start( + { .program = self, .arguments = { "--exit", "0" } }, [](nlohmann::json) {}, + [&] { const std::lock_guard lock { mutex }; closed = true; ready.notify_all(); }); + expect(fatal(connection.has_value())); + bool observedClosed { false }; + { + std::unique_lock lock { mutex }; + observedClosed = ready.wait_for(lock, 20s, [&] { return closed; }); + } + expect(observedClosed) << "the real child exited without closing the connection"; + const auto sent = (*connection)->send(nlohmann::json { { "jsonrpc", "2.0" }, { "method", "after-exit" } }); + expect(!sent.has_value()); + if (!sent) expect(sent.error().code == "connection-closed" || sent.error().code == "connection-write") << sent.error().code; + const auto stopping = std::chrono::steady_clock::now(); + (*connection)->stop(200ms); + (*connection)->stop(200ms); + expect(std::chrono::steady_clock::now() - stopping < 10s) << "departed peer cleanup"; + expect((*connection)->closed()); }; "writing after the child exits returns an error and preserves the parent"_test = [&] { diff --git a/tests/test_scan.cpp b/tests/test_scan.cpp index d009c9df..79b01b49 100644 --- a/tests/test_scan.cpp +++ b/tests/test_scan.cpp @@ -12,6 +12,45 @@ using mcppls::spec::Role; int main() { using namespace mcppls::testing; + "source acknowledgement preserves preprocessor inputs independently of lexical import names"_test = [] { + const auto disabled = scan_source("/* prefix */ #if /* condition\n continued */ 0\nimport hidden;\n#endif\nint value() { return 1; }\n", true); + const auto enabled = scan_source("/* prefix */ #if /* condition\n continued */ 1\nimport hidden;\n#endif\nint value() { return 1; }\n", true); + expect(required_names(disabled) == required_names(enabled)); + expect(disabled.preprocessorDirectives != enabled.preprocessorDirectives) + << "the same import names can have different effective dependency graphs"; + expect(disabled.preprocessorDirectives == "#if /* condition\n continued */ 0\n#endif\n"); + const auto body = scan_source("/* prefix */ #if /* condition\n continued */ 0\nimport hidden;\n#endif\nint value() { return 2; }\n", true); + expect(disabled.preprocessorDirectives == body.preprocessorDirectives); + const auto macro = scan_source("#define FLAG \\\n 0\n#if FLAG\nimport hidden;\n#endif\n", true); + expect(macro.preprocessorDirectives == "#define FLAG \\\n 0\n#if FLAG\n#endif\n"); + const auto literal = scan_source("// #define COMMENT 1\nconst char* text = \"#if 1\";\n", true); + expect(literal.preprocessorDirectives.empty() && literal.preprocessorComplete); + expect(!scan_source("int x; /* across\n lines */ #if 0\nimport hidden;\n#endif\n").preprocessorComplete) + << "an unclassified code-level # must not acknowledge a source"; + const std::string_view text { "#define FEATURE 1\nexport module sample;\n#if FEATURE\nimport hidden;\n#endif\n" }; + const auto ordinary = scan_source(text), retained = scan_source(text, true); + expect(ordinary.preprocessorDirectives.empty() && !retained.preprocessorDirectives.empty()); + expect(fatal(ordinary.declaration.has_value() && retained.declaration.has_value())); + expect(ordinary.declaration->module == retained.declaration->module + && ordinary.declaration->isExported == retained.declaration->isExported + && ordinary.declaration->nameRange == retained.declaration->nameRange); + expect(required_names(ordinary) == required_names(retained) && role_of(ordinary) == role_of(retained)); + const auto active = scan_source("#define SELECT(x) x\nimport SELECT(active);\n", true); + const auto other = scan_source("#define SELECT(x) x\nimport SELECT(other);\n", true); + expect(active.preprocessorDirectives == other.preprocessorDirectives && required_names(active) == required_names(other)); + expect(!active.importsComplete && !other.importsComplete) + << "macro arguments after import can change real dependencies without changing lexical names"; + expect(!scan_source("export module unfinished").declarationsComplete); + expect(!scan_source("export module SELECT(active);\n", true).declarationsComplete); + expect(scan_source("module;\nexport module complete;\n").declarationsComplete); + const auto oneHeader = scan_source("import \"one.hpp\";\n", true); + const auto twoHeaders = scan_source("import \"two.hpp\";\n", true); + expect(required_names(oneHeader) == required_names(twoHeaders)); + expect(fatal(oneHeader.imports.size() == 1 && twoHeaders.imports.size() == 1)); + expect(oneHeader.imports[0].isHeaderUnit && twoHeaders.imports[0].isHeaderUnit + && oneHeader.imports[0].header != twoHeaders.imports[0].header); + }; + "the fixture's primary interface"_test = [] { const auto result = scan_source("export module hello.greet;\nexport import :detail;\nimport std;\n\n" "export namespace hello {\n std::string greet(std::string_view who) { return detail::prefix() + std::string(who); }\n}\n"); diff --git a/tests/test_session.cpp b/tests/test_session.cpp new file mode 100644 index 00000000..98f40e97 --- /dev/null +++ b/tests/test_session.cpp @@ -0,0 +1,61 @@ +import std; +import nlohmann.json; +import mcppls.testing; +import mcppls.base.path; +import mcppls.base.uri; +import mcppls.platform.fs; +import mcppls.platform.dirs; +import mcppls.server.session; + +using Json = nlohmann::json; +namespace fs = mcppls::platform::fs; +namespace b = mcppls::base; + +int main() { + using namespace mcppls::testing; + const std::string root { b::join_path(mcppls::platform::dirs::temp_directory(), + std::format("mcppls-test-session-{}", std::chrono::steady_clock::now().time_since_epoch().count())) }; + const std::string directory { b::join_path(root, "src/greet") }; + const std::string source { b::join_path(directory, "greet.cppm") }; + (void)fs::create_directories(directory); + (void)fs::write_file(source, "export module hello.greet;\n"); + auto folders = [](const std::string& path) { + return Json::array({ Json { { "uri", b::path_to_uri(path) }, { "name", b::file_name(path) } } }); + }; + + "Zed file worktree falls back to its directory rootUri"_test = [&] { + const Json params { { "rootUri", b::path_to_uri(directory) }, { "rootPath", directory }, + { "workspaceFolders", folders(source) } }; + const auto chosen = mcppls::server::workspace_folders(params, root); + expect(fatal(chosen.size() == 1)); + expect(chosen[0].path == directory); + expect(chosen[0].uri == b::path_to_uri(directory)); + }; + + "a file-only initialize never makes the source its working directory"_test = [&] { + Json params { { "rootUri", b::path_to_uri(source) }, { "rootPath", source }, { "workspaceFolders", folders(source) } }; + auto chosen = mcppls::server::workspace_folders(params, root); + expect(fatal(chosen.size() == 1)); + expect(chosen[0].path == root) << "use the existing cwd fallback, without guessing a parent project"; + params["rootPath"] = directory; + chosen = mcppls::server::workspace_folders(params, root); + expect(fatal(chosen.size() == 1)); + expect(chosen[0].path == directory); + }; + + "valid and not-yet-created folders retain precedence and spelling"_test = [&] { + const std::string absent { b::join_path(root, "not-created") }; + Json params { { "rootUri", b::path_to_uri(root) }, { "workspaceFolders", folders(source) } }; + params["workspaceFolders"].push_back(folders(directory)[0]); + params["workspaceFolders"].push_back(folders(absent)[0]); + const auto chosen = mcppls::server::workspace_folders(params, root); + expect(fatal(chosen.size() == 2)); + expect(chosen[0].path == directory); + expect(chosen[0].uri == params["workspaceFolders"][1]["uri"].get()); + expect(chosen[1].path == absent); + expect(chosen[1].uri == params["workspaceFolders"][2]["uri"].get()); + }; + + fs::remove_all(root); + return report(); +}