diff --git a/docs/EVIDENCE.md b/docs/EVIDENCE.md index 1dd6efd1..11049ca6 100644 --- a/docs/EVIDENCE.md +++ b/docs/EVIDENCE.md @@ -9,7 +9,7 @@ | 配置 | 证据定位 | |---|---| | main 默认:自研核心 + 系统 ConPTY | R1 限定总验收和有限 VT/Unicode 子集;默认构建不需要 Rust | -| main 显式候选:`termforge.5` + 系统 ConPTY | [固定来源/补丁台账](../experiments/candidates/alacritty/PROVENANCE.md)、[适配边界](../experiments/core/CANDIDATE.md)、[#39 字素合同](research/streaming-graphemes.md);仅按已有合同验收 | +| 当前源码显式候选:`termforge.6` + 系统 ConPTY | [固定来源/补丁台账](../experiments/candidates/alacritty/PROVENANCE.md)、[适配边界](../experiments/core/CANDIDATE.md)、[#39 字素合同](research/streaming-graphemes.md);仅按已有合同验收 | | PR #50 实验:`termforge.4` + 固定官方 ConPTY | [Draft PR](https://github.com/JavaZeroo/termforge/pull/50) / [#47](https://github.com/JavaZeroo/termforge/issues/47);[联合基线](research/joint-conpty-baseline.md)已固定,产品回归未全绿;[resize #51](https://github.com/JavaZeroo/termforge/issues/51)、[GUI Vim #52](https://github.com/JavaZeroo/termforge/issues/52)、[工作区 #63](https://github.com/JavaZeroo/termforge/issues/63)仍开放。[#53 的未完成粘贴、负例及原八会话证据](https://github.com/JavaZeroo/termforge/blob/848cb0e621a291f732cc0e046d0b2fab6a77d962/docs/research/resize-replay-cost.md)已按限定合同整合到实验父分支;不包含同次真实 CF/IME 或完整官方矩阵验收 | R1 固定总出口的源码为 `ce9b24d`,早期 `2dc8d6d` / `9b16acc` 仍分别只代表原记录。源 VT、原生控制台 CHAR_INFO、transport、末端状态、自有 DIB、绘制提交和真实呈现是不同检查点;未知端点保持未知。 @@ -36,7 +36,7 @@ R1 固定总出口的源码为 `ce9b24d`,早期 `2dc8d6d` / `9b16acc` 仍分 | R2 流式字素与固定 emoji(#39) | [合同](research/streaming-graphemes.md)、[红/绿及实际场景验证](../experiments/shell/results/2026-10-26-streaming-graphemes/validation.json) | 已合并 #39:.4 的固定 ZWJ/keycap 原子格/选择/绘制和单列 resize。766 个规范用例只验证分段 helper;64 标量超限按明确无损分片,不宣称完整字素布局或完整 Unicode/IME。 | | 真实备用屏切换(#27 限定切片) | [五阶段原生快照与候选验证](../experiments/shell/results/2026-10-26-alternate-screen/validation.json)、[候选接口](../experiments/core/CANDIDATE.md) | 真实 1049h/1049l 五阶段的源 VT、CHAR_INFO、样式/光标恢复及候选历史隔离;origin/wrap、margin/resize 交叉与 #27 整体验收另做。 | | 局部滚动区域(#27 限定切片) | [原生阶段快照、独立 transport 重放与候选验证](../experiments/shell/results/2026-10-26-scroll-region/validation.json)、[候选接口](../experiments/core/CANDIDATE.md) | 真实 DECSTBM 局部滚动的源、原生快照与 transport 独立重放;区域外保持/历史隔离通过,不要求控制字节原样回显。 | -| ConPTY resize 重放、历史与 Find/选区(#29) | [限定合同](adr/0005-physical-row-resize.md)、[重放与历史验证](../experiments/core/results/2026-10-26-resize-replay/validation.json)、[文字列位置回归](../experiments/core/results/2026-10-27-replay-coordinates/README.md)、[快照/清空修复](../experiments/core/results/2026-10-27-resize-snapshot/README.md)、[候选接口](../experiments/core/CANDIDATE.md) | 固定 861B 重放和独立列位置回归保留;`.5` 验证 resize 后 pending 可见网格一致及主屏 ED3/RIS 清空不复活。跨宽度历史、缓存/备用屏、完整 resize/reflow 与真实 child/呈现仍未完成。 | +| ConPTY resize 重放、历史与 Find/选区(#29) | [限定合同](adr/0005-physical-row-resize.md)、[重放与历史验证](../experiments/core/results/2026-10-26-resize-replay/validation.json)、[文字列位置回归](../experiments/core/results/2026-10-27-replay-coordinates/README.md)、[快照/清空修复](../experiments/core/results/2026-10-27-resize-snapshot/README.md)、[候选接口](../experiments/core/CANDIDATE.md) | 固定 861B 重放和独立列位置回归保留;`.5` 验证 resize 后 pending 可见网格一致及主屏 ED3/RIS 清空不复活。`.6` 的[缓存与淘汰修复](../experiments/core/results/2026-10-28-history-cache/README.md)覆盖透明输入、主/备用屏归属、原生前缀和旧回放缓存失效。混宽历史视口、完整 resize/reflow 与真实 child/呈现仍未完成。 | | WindTerm resize 文本保留与回带(#29) | [方法与边界](research/windterm-resize-text.md)、[两次事实/哈希](../experiments/core/results/2026-10-27-resize-snapshot/windterm-observations.json) | 原版 2.7.0 的隔离 Telnet 五阶段观察:42 条已知文本保留,恢复宽/高后采集投影恢复;不把文本索引当物理格,不验证候选冻结/清空、ConPTY child、实际呈现或性能。 | | R2 复杂标量序列输入(#30 限定切片) | [真实键盘接收验证](../experiments/shell/results/2026-10-26-unicode-keyboard/validation.json)、[场景与复现](../experiments/shell/README.md) | 固定 ZWJ/keycap 通过 WM_CHAR/ConPTY/ReadConsoleW 精确接收;只证明输入保真,不证明绘制、字素宽度或完整应用键盘模式。 | | 自有窗口绘制正确性 | [呈现探针](../experiments/presentation/README.md)、[共享 renderer](../experiments/presentation/render.hpp)、[度量测试](../experiments/presentation/renderer_metrics_test.cpp) | 自有 DIB 的 retained/full RGB、字体/光标/缺字负例与覆盖;BitBlt/GdiFlush 提交不等于实际上屏,presented_done=null。 | diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 2a548465..9c425cef 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -1,6 +1,6 @@ # 当前能力与目标矩阵 -本文件维护当前 main 的能力与已知差距。R1 已按限定合同验收;R2 仍在实现与整合,尚未达到日常开发版出口。路标见[总体计划](PLAN.md),验收合同见 [R2 出口检查表](milestones/R2-local-terminal.md),执行状态见 [GitHub 任务入口](TRACKING.md),结果与来源见[证据索引](EVIDENCE.md)。每份证据只证明其固定源码、构建配置和场景;局部测试、未合并分支及历史成功不自动代表当前产品整体验收。 +本文件维护所读源码版本的能力与已知差距;分支证据不代表 main 已包含相应实现。R1 已按限定合同验收;R2 仍在实现与整合,尚未达到日常开发版出口。路标见[总体计划](PLAN.md),验收合同见 [R2 出口检查表](milestones/R2-local-terminal.md),执行状态见 [GitHub 任务入口](TRACKING.md),结果与来源见[证据索引](EVIDENCE.md)。每份证据只证明其固定源码、构建配置和场景;局部测试、未合并分支及历史成功不自动代表当前产品整体验收。 比较目标固定为 **WindTerm 2.7.0**。功能范围依据[固定提交的官方 README](https://github.com/kingToolbox/WindTerm/blob/a8336c1d1e3dd1a981500dae8ef9eb42066de95f/README.md)、[2.7.0 release](https://github.com/kingToolbox/WindTerm/releases/tag/2.7.0)及仓库内的发行物/运行时研究。官方声明说明目标包含什么,不证明已经在本机验证了其行为,更不证明 TermForge 已实现。各平台、版本限制和具体交互仍需逐项形成可重复场景。 @@ -13,10 +13,10 @@ | 配置 | 当前定位 | 可以引用的证据与限制 | |---|---|---| | 默认 main:自研核心 + 系统 ConPTY | 默认构建与已发布 Alpha 的基础路线,不需要 Rust | 已有 R1 限定验收;VT/Unicode 能力仍为有限子集,不能引用候选核心的字素或 TUI 结果 | -| 显式候选 main:`termforge.5` + 系统 ConPTY | `TERMFORGE_BUILD_CANDIDATE_ADAPTER=ON`;按 [ADR-0003](adr/0003-core-reference-and-reuse.md)保留为可评估实验,正式核心选型待用户确认 | [固定来源](../experiments/candidates/alacritty/PROVENANCE.md)、[适配边界](../experiments/core/CANDIDATE.md)与 [#39 字素合同](research/streaming-graphemes.md);候选正确性通过不关闭系统 ConPTY 或 R2 父项 | +| 当前源码显式候选:`termforge.6` + 系统 ConPTY | `TERMFORGE_BUILD_CANDIDATE_ADAPTER=ON`;按 [ADR-0003](adr/0003-core-reference-and-reuse.md)保留为可评估实验,正式核心选型待用户确认 | [固定来源](../experiments/candidates/alacritty/PROVENANCE.md)、[适配边界](../experiments/core/CANDIDATE.md)与 [#39 字素合同](research/streaming-graphemes.md);候选正确性通过不关闭系统 ConPTY 或 R2 父项 | | 实验分支:`termforge.4` + 固定官方 ConPTY | [PR #50(Draft)](https://github.com/JavaZeroo/termforge/pull/50),联合源码 `3e5b96d` 的[原基线](research/joint-conpty-baseline.md)保留;后续限定修复已整合,尚未合入 main 或通过产品整体验收 | [#47](https://github.com/JavaZeroo/termforge/issues/47) 跟踪后端整合;终端 margin 与原生状态分列。[resize #51](https://github.com/JavaZeroo/termforge/issues/51)、[GUI Vim #52](https://github.com/JavaZeroo/termforge/issues/52)、[工作区 #63](https://github.com/JavaZeroo/termforge/issues/63) 仍开放。[#53](https://github.com/JavaZeroo/termforge/issues/53) 的原 3MB/768B/16KiB 未完成粘贴取消/关闭、负例及八会话 data-only 两轮已限定验证并整合到实验父分支;同次真实 CF/IME 与完整官方矩阵未通过。fence 默认关闭,不由实验结果选择产品后端 | -以下能力默认指已合并实现;某个配置的通过结果不能转用于另一个配置。R1 的真实系统 CF/拼音验收属于固定自研核心配置,不外推到候选 `.4` 或实验后端联合配置。 +以下能力以所读源码版本及各自固定证据为准;某个配置的通过结果不能转用于另一个配置。R1 的真实系统 CF/拼音验收属于固定自研核心配置,不外推到当前候选 或实验后端联合配置。 ## 基础文字交互与本地终端 @@ -31,9 +31,9 @@ | 查找与定位 | **限定验收**:有界增量字面量搜索、原生查找框、方向/循环/高亮、中文、追加与淘汰;持续输出时可显式使用有效局部命中,运行中查询替换/取消/关闭均有[托管两轮独立证据](../experiments/shell/results/2026-10-24-r1-find-accepted/validation.json) | 局部结果不表示全局末端/循环;动作与受控 DPI 交叉见统一文字动作,组合与资源见 #18 的独立验收。正则、跨会话、完整 Unicode caseless 和大历史规模归 R4 | R1 / R4 | | 统一文字动作 | **限定验收**:共用目标/可用状态,实际终端右键/快捷键、原生 Find/rename 编辑、busy/closing/失效选区反馈及受控 DPI/zoom 保持;[最终系统审计](../experiments/shell/results/2026-10-24-r1-actions-accepted/validation.json)与[逐项对照](research/r1-action-comparison.md)覆盖 Find 实际 Ctrl+C/V、中文重选、默认 Return 和会话隔离 | 复杂 busy/closing 参考行为仍未知,采用明确项目策略;完整流程与八会话有限资源已由 #18 另验收,真实跨显示器及完整 WindTerm 等价未验收 | R1 | | 中文 IME 与键盘 | **限定验收**:基础按键/修饰键/UTF-16 与实际接收字节;[固定 ZWJ/keycap 输入](../experiments/shell/results/2026-10-26-unicode-keyboard/validation.json)在默认/候选各两轮精确接收。固定 Microsoft 拼音[产品场景](../research/ime/results/2026-10-24-product-ime-contexts/validation.json)覆盖两区域组合/提交/取消、候选、输出/resize/zoom/受控 DPI、隐藏/关闭、历史阅读和原生控件 | 限定 Windows Server 2025 固定拼音及已有场景;同次组合由八会话工作流另验收。固定字素绘制/选择已由 #39 限定完成,但所有输入法、应用键盘模式、完整 IME 和真实不同 DPI 跨屏仍未完成 | R1 / R2 | -| VT 核心 | **实现中,未验收**:默认自研支持基础光标/擦除(含 ECH)、CR/LF/BS/TAB、有限 SGR 和有界恢复。显式候选 `.5` 接入 `Term/Processor`、C++ 拥有值适配、稳定行元数据、每缓冲区 margin 与流式字素;[共同轨迹](../experiments/differential/results/2026-10-26-candidate-common-v6/summary.json)及[源 margin 合同](../experiments/shell/results/2026-10-26-margin-state/validation.json)为各自固定版本结果 | [#27](https://github.com/JavaZeroo/termforge/issues/27) 的 origin/wrap、插删、模式/resize 交叉未闭合;[#41](https://github.com/JavaZeroo/termforge/issues/41) 系统 ConPTY 已丢失的 margin 不能由末端补回。ECH pending-wrap 差异见[限定合同](research/more-capture-ech.md);#36/#38、完整呈现和平台支持独立验收 | R2 / R4 | -| Unicode 与文字显示 | **实现中,未验收**:默认自研保留 Unicode 15。显式候选 `.5` 保留 [#39 Unicode 17 流式字素合同](research/streaming-graphemes.md)已限定验收:固定 ZWJ/keycap 的单 lead/两列、光标、SGR/查询、原子选择复制、单列 resize 与有界无损分片;实际 ConPTY/DWrite 核对固定样本的单 glyph、字体和网格 | [#30](https://github.com/JavaZeroo/termforge/issues/30) 仍需完整输入/IME 交叉;所有脚本/整形、通用 fallback、所有原生重放、真彩色/更多属性、Powerline 和真实跨屏未验收。分段 helper 的规范数据通过不代表完整终端 Unicode 正确性;[#54](https://github.com/JavaZeroo/termforge/issues/54) 保留已观察的性能成本 | R2 / R4 | -| resize 与 reflow | **限定验收**:默认按 ADR-0005 物理行裁剪;候选保留[固定 861B 重放](../experiments/core/results/2026-10-26-resize-replay/validation.json)的身份/历史与[列位置回归](../experiments/core/results/2026-10-27-replay-coordinates/README.md)。`.5` 的[快照与清空修复](../experiments/core/results/2026-10-27-resize-snapshot/README.md)验证四种可见尺寸的 pending 快照,以及主屏 ED3/RIS 清空期间不复活旧历史、选择/Find 失效 | [#29](https://github.com/JavaZeroo/termforge/issues/29) 的跨宽度缓存显示、零原生历史启发式、备用屏与完整 resize 交叉未完成;[原版两次文本观察](research/windterm-resize-text.md)仅证明固定输入保留及恢复尺寸后文本投影恢复。完整 reflow、child ack、实际呈现、系统/实验后端联合配置及性能分别验收;默认缩窄不可恢复 | R2 | +| VT 核心 | **实现中,未验收**:默认自研支持基础光标/擦除(含 ECH)、CR/LF/BS/TAB、有限 SGR 和有界恢复。显式候选 `.6` 接入 `Term/Processor`、C++ 拥有值适配、稳定行元数据、每缓冲区 margin 与流式字素;[共同轨迹](../experiments/differential/results/2026-10-26-candidate-common-v6/summary.json)及[源 margin 合同](../experiments/shell/results/2026-10-26-margin-state/validation.json)为各自固定版本结果 | [#27](https://github.com/JavaZeroo/termforge/issues/27) 的 origin/wrap、插删、模式/resize 交叉未闭合;[#41](https://github.com/JavaZeroo/termforge/issues/41) 系统 ConPTY 已丢失的 margin 不能由末端补回。ECH pending-wrap 差异见[限定合同](research/more-capture-ech.md);#36/#38、完整呈现和平台支持独立验收 | R2 / R4 | +| Unicode 与文字显示 | **实现中,未验收**:默认自研保留 Unicode 15。显式候选 `.6` 保留 [#39 Unicode 17 流式字素合同](research/streaming-graphemes.md)已限定验收:固定 ZWJ/keycap 的单 lead/两列、光标、SGR/查询、原子选择复制、单列 resize 与有界无损分片;实际 ConPTY/DWrite 核对固定样本的单 glyph、字体和网格 | [#30](https://github.com/JavaZeroo/termforge/issues/30) 仍需完整输入/IME 交叉;所有脚本/整形、通用 fallback、所有原生重放、真彩色/更多属性、Powerline 和真实跨屏未验收。分段 helper 的规范数据通过不代表完整终端 Unicode 正确性;[#54](https://github.com/JavaZeroo/termforge/issues/54) 保留已观察的性能成本 | R2 / R4 | +| resize 与 reflow | **限定验收**:默认按 ADR-0005 物理行裁剪;候选保留[固定 861B 重放](../experiments/core/results/2026-10-26-resize-replay/validation.json)的身份/历史与[列位置回归](../experiments/core/results/2026-10-27-replay-coordinates/README.md)。`.5` 的[快照与清空修复](../experiments/core/results/2026-10-27-resize-snapshot/README.md)验证四种可见尺寸的 pending 快照,以及主屏 ED3/RIS 清空期间不复活旧历史、选择/Find 失效;`.6` 的[缓存回归](../experiments/core/results/2026-10-28-history-cache/README.md)覆盖透明输入、主屏缓存归属及重复淘汰 | [#29](https://github.com/JavaZeroo/termforge/issues/29) 的混宽历史视口、完整 resize/备用屏/滚动区交叉仍未完成;[原版两次文本观察](research/windterm-resize-text.md)仅证明固定输入保留及恢复尺寸后文本投影恢复。完整 reflow、child ack、实际呈现、系统/实验后端联合配置及性能分别验收;默认缩窄不可恢复 | R2 | | 本地 shell 选择 | **实现中,未验收**:`ShellLaunch` 分离可执行文件、参数、工作目录及 shell 命令差异;cmd、Windows PowerShell 5.1、PowerShell 7、显式 Ubuntu WSL 各有[两轮真实启动/目录/尺寸/正常退出证据](../experiments/shell/results/2026-10-24-r2-shell-lifecycle/validation.json),无效程序启动错误可见且可关闭后恢复 cmd。四 shell 测试已补 [WSL 原生尺寸程序返回后的新提示符同步](research/wsl-size-return.md),正常退出证据只在实际全部回收后写入;默认/system 的事件反例、原两轮及完整回归分别留证 | 仅声明上述固定入口和隔离目录;#80 原 hosted stage6 是否同因仍未知,#81 hosted 准备超时另行定位;任意 profile/参数、通用分屏/排序、全部运行时错误提示及管理员 shell 未完成 | R2;权限场景 R4 | | 常用 TUI | **实现中,未验收**:显式候选 + 系统 ConPTY 有固定 Vim 进入、编辑、60×12 resize、退出写盘、主屏恢复及光标回复的[产品证据](../experiments/core/CANDIDATE.md);More 尾部 ECH 与捕获端点有[限定修复](research/more-capture-ech.md) | [#28](https://github.com/JavaZeroo/termforge/issues/28) 的更多应用及完整输入/输出模式未完成;PR #50 实验后端的 GUI Vim [#52](https://github.com/JavaZeroo/termforge/issues/52) 仍有失败;[native 捕获目录复用](research/vim-fixture-isolation.md)已独立定位并修正,不能代替 GUI 或完整 TUI 验收。headless 比较与自有 DIB 不等于真实呈现,`presented_done=null` | R2 | diff --git a/docs/PROVENANCE.md b/docs/PROVENANCE.md index a1634315..c2d4d351 100644 --- a/docs/PROVENANCE.md +++ b/docs/PROVENANCE.md @@ -24,7 +24,7 @@ - `experiments/core/unicode_width.hpp`:通过原创生成器从 Python 3.12.10 `unicodedata` 所带 Unicode 15.0.0 属性数据生成,含项目自己的宽度/拒绝策略。未复制 CPython 实现源码。Unicode 数据保留[官方许可](../experiments/core/UNICODE-LICENSE.txt)与归属,来源为 [Unicode](https://www.unicode.org/);正常构建使用固定生成文件。此数据不归项目 MIT 许可覆盖。 -- 外部核心候选当前使用 `alacritty_terminal 0.26.1-dev+termforge.5`(Apache-2.0),固定上游 commit `542147067dd016fc5f49120ef0ba605a98a4e3a7` 的 manifest/changelog/许可证与 `src/`,并保留 ED、稳定行身份/淘汰、安全 swap、每缓冲区滚动区域、流式字素和显式历史清空代次补丁;精确传递依赖及校验值由 `Cargo.lock` 固定。未复制上游 46 MiB 行为录制,候选不进入当前产品包;默认 CMake 不依赖 Rust,显式适配器构建使用 staticlib。原始 0.26.0 身份、当前来源、许可证和补丁清单见[候选来源台账](../experiments/candidates/alacritty/PROVENANCE.md)。 +- 外部核心候选当前使用 `alacritty_terminal 0.26.1-dev+termforge.6`(Apache-2.0),固定上游 commit `542147067dd016fc5f49120ef0ba605a98a4e3a7` 的 manifest/changelog/许可证与 `src/`,并保留 ED、稳定行身份/淘汰、安全 swap、每缓冲区滚动区域、流式字素、显式历史清空代次和原生历史前缀淘汰补丁;精确传递依赖及校验值由 `Cargo.lock` 固定。未复制上游 46 MiB 行为录制,候选不进入当前产品包;默认 CMake 不依赖 Rust,显式适配器构建使用 staticlib。原始 0.26.0 身份、当前来源、许可证和补丁清单见[候选来源台账](../experiments/candidates/alacritty/PROVENANCE.md)。 - 候选新增精确锁定 `unicode-segmentation 1.13.3`(Unicode 17,MIT OR Apache-2.0),官方来源为 [unicode-rs](https://github.com/unicode-rs/unicode-segmentation) 与 [crates.io 固定版本](https://crates.io/crates/unicode-segmentation/1.13.3)。保留原依赖许可证;与既有 `unicode-width 0.2.2` 同使用 Unicode 17,默认自研核心仍使用 Unicode 15。版本、包哈希、MSRV、依赖成本和修改边界见[候选来源台账](../experiments/candidates/alacritty/PROVENANCE.md)。 diff --git a/docs/adr/0005-physical-row-resize.md b/docs/adr/0005-physical-row-resize.md index 39376f54..f872fa43 100644 --- a/docs/adr/0005-physical-row-resize.md +++ b/docs/adr/0005-physical-row-resize.md @@ -49,4 +49,4 @@ API 依据是 [Microsoft ResizePseudoConsole](https://learn.microsoft.com/en-us/ 命令、哈希、默认/候选完整回归及未满足项见[重放与历史验证记录](../../experiments/core/results/2026-10-26-resize-replay/validation.json)。 -相同文字左右移位后的选区失效和 Find 重新定位另见[列位置回归](../../experiments/core/results/2026-10-27-replay-coordinates/README.md)。`.5` 在主屏 ED3/RIS 实际执行时递增清空代次,经 C ABI 立即释放旧回放和历史缓存;原生历史为空也报告,ED2 不清历史,清空不冒充淘汰。[快照与清空回归](../../experiments/core/results/2026-10-27-resize-snapshot/README.md)覆盖固定尺寸、清空前/中/后/同 feed、选择/Find 失效与后续不复活。跨宽度历史的显示、零历史缓存启发式、备用屏和完整交叉仍未闭合;这些修复不裁决 WindTerm 的完整重排、清空或选择保留策略。 +相同文字左右移位后的选区失效和 Find 重新定位另见[列位置回归](../../experiments/core/results/2026-10-27-replay-coordinates/README.md)。`.5` 在主屏 ED3/RIS 实际执行时递增清空代次,经 C ABI 立即释放旧回放和历史缓存;原生历史为空也报告,ED2 不清历史,清空不冒充淘汰。[快照与清空回归](../../experiments/core/results/2026-10-27-resize-snapshot/README.md)覆盖固定尺寸、清空前/中/后/同 feed、选择/Find 失效与后续不复活。`.6` 的[缓存归属与淘汰回归](../../experiments/core/results/2026-10-28-history-cache/README.md)移除零原生历史的清空推断,保持主屏缓存跨备用屏,并将容量淘汰同步到原生存储及旧回放快照。跨宽度历史显示和完整 resize/备用屏/滚动区交叉仍未闭合;这些修复不裁决 WindTerm 的完整重排、清空或选择保留策略。 diff --git a/experiments/candidates/alacritty/Cargo.lock b/experiments/candidates/alacritty/Cargo.lock index 7a002fd0..80d1c626 100644 --- a/experiments/candidates/alacritty/Cargo.lock +++ b/experiments/candidates/alacritty/Cargo.lock @@ -13,7 +13,7 @@ dependencies = [ [[package]] name = "alacritty_terminal" -version = "0.26.1-dev+termforge.5" +version = "0.26.1-dev+termforge.6" dependencies = [ "arrayvec", "base64", diff --git a/experiments/candidates/alacritty/PROVENANCE.md b/experiments/candidates/alacritty/PROVENANCE.md index 0a2ca5d2..659f7d13 100644 --- a/experiments/candidates/alacritty/PROVENANCE.md +++ b/experiments/candidates/alacritty/PROVENANCE.md @@ -1,6 +1,6 @@ # 固定候选来源与本地补丁 -当前适配版本为 `alacritty_terminal 0.26.1-dev+termforge.5`,不是 crates.io 原始 `0.26.0`。 +当前适配版本为 `alacritty_terminal 0.26.1-dev+termforge.6`,不是 crates.io 原始 `0.26.0`。 ## 上游身份 @@ -59,6 +59,13 @@ 2. C ABI 与 C++ 适配器同步读取代次,在 resize 回放期间释放旧历史和身份缓存,避免显式清空后复活。公开 `feed/resize/Screen/TextSelection/TextSearch` 接口回归覆盖该行为,不依靠对原始字节搜索 `CSI 3J`。 3. 版本升为 `0.26.1-dev+termforge.5`,未改变固定上游 commit、第三方依赖版本或许可证。此补丁仍是候选实验,不代表正式产品选型或 WindTerm 清空行为已对照。 +## termforge.6 补丁 + +1. `src/grid/mod.rs`:增加最旧历史前缀的容量淘汰操作,保持配置容量,裁掉实际存储中的旧行并累计原生淘汰数;与不计淘汰的显式清空分开。 +2. C ABI 在修改前核对待淘汰主屏原生前缀的全部稳定 ID。C++ 合并缓存达到容量时同步移除仍驻留的原生旧行,避免空同步重复导入/淘汰或增高时复活。缓存独有行的淘汰单独累计;公开接口覆盖交错历史、重复空同步及增高不复活。 +3. 候选 C++ 快照保留原生身份映射及淘汰端点,主屏缓存跨备用屏切换挂起/恢复,空原生历史不再推断为清空。容量淘汰也使旧回放快照中的缓存行失效,在行版本比较结束后才修改来源快照。相关实现不是 WindTerm 私有代码;原版完整 resize/显示语义仍需对照。 +4. 升为 `0.26.1-dev+termforge.6`,同步当前报告身份;固定上游、注册表依赖与许可证未变。 + ## 历史基线 #24–#26 的原始候选为 crates.io `0.26.0`,crate SHA-256 `bda177466b9524d59f1b12f0dd30b68696788e9992a7e959021c4a0ed96fcf59`,对应上游 commit `94e7c8874e526b1e67b349d9ba30ddf81669119e`。当时的 90 match + 5 reference_difference、project/interactive 计量和平台/维护审查保持原批次身份,不改写为 termforge.1/2 结果。 diff --git a/experiments/candidates/alacritty/README.md b/experiments/candidates/alacritty/README.md index bdbaf120..54b1a3bf 100644 --- a/experiments/candidates/alacritty/README.md +++ b/experiments/candidates/alacritty/README.md @@ -6,7 +6,7 @@ | 项目 | 当前固定值 | 来源 | |---|---|---| -| `alacritty_terminal` | `0.26.1-dev+termforge.5`,Apache-2.0 | [上游 commit `5421470`](https://github.com/alacritty/alacritty/commit/542147067dd016fc5f49120ef0ba605a98a4e3a7)与[来源/补丁台账](PROVENANCE.md) | +| `alacritty_terminal` | `0.26.1-dev+termforge.6`,Apache-2.0 | [上游 commit `5421470`](https://github.com/alacritty/alacritty/commit/542147067dd016fc5f49120ef0ba605a98a4e3a7)与[来源/补丁台账](PROVENANCE.md) | | 本地补丁 | ED1/ED2、稳定 Row ID/换行元数据/淘汰、安全完整 Row swap、每缓冲区滚动区域、流式字素与明确剪裁 | `vendor/alacritty_terminal/src/`;所有文件哈希进入结果 manifest | | 工具链 | Rust/cargo 1.92.0 | 本机与候选 CI 显式选择;当前锁定图最高声明 Rust 1.85 | | 传递依赖 | 59 个注册表包 + 根适配器与固定 vendor 两个 path crate,共 61 个包 | `Cargo.lock` 与 `cargo metadata --locked --format-version 1` | diff --git a/experiments/candidates/alacritty/include/termforge_candidate.h b/experiments/candidates/alacritty/include/termforge_candidate.h index 21fffc27..53ba25ba 100644 --- a/experiments/candidates/alacritty/include/termforge_candidate.h +++ b/experiments/candidates/alacritty/include/termforge_candidate.h @@ -40,6 +40,10 @@ int tf_terminal_feed(void* terminal, const uint8_t* data, size_t len, uint8_t* e int tf_terminal_finish(void* terminal, uint8_t* error, size_t error_capacity); int tf_terminal_resize(void* terminal, size_t rows, size_t columns, uint8_t* error, size_t error_capacity); int tf_terminal_meta(void* terminal, TfTerminalMeta* output); +// Evict an exact oldest primary-history prefix. Validate all IDs before mutation; +// return the cumulative native eviction count without changing history capacity. +int tf_terminal_discard_history(void* terminal, const uint64_t* ids, size_t count, + uint64_t* discarded, uint8_t* error, size_t error_capacity); int tf_terminal_row_meta(void* terminal, size_t index, TfRowMeta* output); int tf_terminal_cell_meta(void* terminal, size_t index, size_t column, TfCellMeta* output); int tf_terminal_cell_text(void* terminal, size_t index, size_t column, uint8_t* output, diff --git a/experiments/candidates/alacritty/run.py b/experiments/candidates/alacritty/run.py index 25bf3584..2a6a29d6 100644 --- a/experiments/candidates/alacritty/run.py +++ b/experiments/candidates/alacritty/run.py @@ -11,7 +11,7 @@ ROOT = Path(__file__).resolve().parents[3] HERE = Path(__file__).resolve().parent -VERSION = "0.26.1-dev+termforge.5" +VERSION = "0.26.1-dev+termforge.6" UPSTREAM_COMMIT = "542147067dd016fc5f49120ef0ba605a98a4e3a7" BASE_CRATE_SHA256 = "bda177466b9524d59f1b12f0dd30b68696788e9992a7e959021c4a0ed96fcf59" diff --git a/experiments/candidates/alacritty/src/ffi.rs b/experiments/candidates/alacritty/src/ffi.rs index 5a70d6e1..43cf6139 100644 --- a/experiments/candidates/alacritty/src/ffi.rs +++ b/experiments/candidates/alacritty/src/ffi.rs @@ -9,7 +9,7 @@ use std::panic::{AssertUnwindSafe, catch_unwind}; use alacritty_terminal::event::Event; use alacritty_terminal::grid::{Dimensions, GridCell}; use alacritty_terminal::index::{Column, Line}; -use alacritty_terminal::term::{Config, Term, cell::{Cell, Flags}}; +use alacritty_terminal::term::{Config, Term, TermMode, cell::{Cell, Flags}}; use alacritty_terminal::vte::ansi::{Color, NamedColor, Processor}; use trace::{Listener, Size}; @@ -216,6 +216,40 @@ pub unsafe extern "C" fn tf_terminal_meta(value: *mut c_void, output: *mut Termi 0 } +#[unsafe(no_mangle)] +pub unsafe extern "C" fn tf_terminal_discard_history( + value: *mut c_void, + ids: *const u64, + count: usize, + discarded: *mut u64, + error: *mut c_uchar, + error_capacity: usize, +) -> c_int { + guard(error, error_capacity, || { + if count > 10000 || discarded.is_null() || (count != 0 && ids.is_null()) { + return Err("invalid history eviction buffer".into()); + } + let value = unsafe { terminal(value)? }; + if value.term.mode().contains(TermMode::ALT_SCREEN) { + return Err("primary history eviction on alternate screen".into()); + } + let grid = value.term.grid(); + let history = grid.history_size(); + if count > history { + return Err("history eviction exceeds retained rows".into()); + } + let ids = if count == 0 { &[] } else { unsafe { std::slice::from_raw_parts(ids, count) } }; + for (index, id) in ids.iter().enumerate() { + if grid[Line(index as i32 - history as i32)].id() != *id { + return Err("history eviction prefix identity differs".into()); + } + } + value.term.grid_mut().discard_oldest_history(count); + unsafe { *discarded = value.term.grid().discarded() }; + Ok(()) + }) +} + fn row_at<'a>(value: &'a Terminal, index: usize) -> Result<(Line, &'a alacritty_terminal::grid::Row), String> { let grid = value.term.grid(); let history = grid.history_size(); @@ -370,3 +404,94 @@ pub unsafe extern "C" fn tf_terminal_next_event( value.events.pop_front(); 1 } + +#[cfg(test)] +mod tests { + use super::*; + use std::mem::MaybeUninit; + use std::ptr::null_mut; + + struct Handle(*mut c_void); + + impl Drop for Handle { + fn drop(&mut self) { + unsafe { tf_terminal_free(self.0) }; + } + } + + impl Handle { + fn feed(&self, bytes: &[u8]) { + assert_eq!(unsafe { tf_terminal_feed(self.0, bytes.as_ptr(), bytes.len(), null_mut(), 0) }, 0); + } + + fn meta(&self) -> TerminalMeta { + let mut meta = MaybeUninit::uninit(); + assert_eq!(unsafe { tf_terminal_meta(self.0, meta.as_mut_ptr()) }, 0); + unsafe { meta.assume_init() } + } + + fn rows(&self) -> Vec<(u64, u8)> { + let meta = self.meta(); + (0..meta.history + meta.rows).map(|index| { + let mut row = MaybeUninit::::uninit(); + assert_eq!(unsafe { tf_terminal_row_meta(self.0, index, row.as_mut_ptr()) }, 0); + let mut text = [0; 8]; + let mut needed = 0; + assert_eq!(unsafe { + tf_terminal_cell_text(self.0, index, 0, text.as_mut_ptr(), text.len(), &mut needed) + }, 0); + (unsafe { row.assume_init() }.id, text[0]) + }).collect() + } + } + + #[test] + fn history_eviction_checks_entire_prefix_before_mutating() { + let value = Handle(tf_terminal_new(2, 8, 4)); + assert!(!value.0.is_null()); + value.feed(b"A\r\nB\r\nC\r\nD\r\nE\r\nF"); + let before = value.rows(); + let prefix = [before[0].0, before[1].0]; + let mut discarded = u64::MAX; + let wrong_second = [prefix[0], prefix[0]]; + assert_eq!(unsafe { + tf_terminal_discard_history(value.0, wrong_second.as_ptr(), 2, &mut discarded, null_mut(), 0) + }, 1); + assert_eq!(discarded, u64::MAX); + assert_eq!(value.rows(), before); + assert_eq!(value.meta().discarded, 0); + + value.feed(b"\x1b[?1049hALT"); + let alternate = value.rows(); + assert_eq!(unsafe { + tf_terminal_discard_history(value.0, prefix.as_ptr(), 2, &mut discarded, null_mut(), 0) + }, 1); + assert_eq!(value.rows(), alternate); + value.feed(b"\x1b[?1049l"); + assert_eq!(value.rows(), before); + + let initial = value.meta(); + assert_eq!(unsafe { + tf_terminal_discard_history(value.0, prefix.as_ptr(), 2, &mut discarded, null_mut(), 0) + }, 0); + let after = value.meta(); + assert_eq!(discarded, 2); + assert_eq!(after.history, 2); + assert_eq!(value.rows(), before[2..]); + assert_eq!((after.row, after.column, after.history_clear_epoch), + (initial.row, initial.column, initial.history_clear_epoch)); + assert_eq!(unsafe { + tf_terminal_discard_history(value.0, std::ptr::null(), 0, &mut discarded, null_mut(), 0) + }, 0); + assert_eq!(discarded, 2); + + value.feed(b"\r\nG\r\nH"); + assert_eq!(value.meta().history, 4, "eviction must not reduce configured capacity"); + assert_eq!(value.meta().discarded, 2); + value.feed(b"\r\nI"); + assert_eq!(value.meta().discarded, 3); + assert_eq!(unsafe { tf_terminal_resize(value.0, 8, 8, null_mut(), 0) }, 0); + assert!(value.rows().iter().all(|(_, text)| !b"ABC".contains(text)), + "height growth must not recover evicted storage rows"); + } +} diff --git a/experiments/candidates/alacritty/test_candidate.py b/experiments/candidates/alacritty/test_candidate.py index 8e3f56a9..5335ab93 100644 --- a/experiments/candidates/alacritty/test_candidate.py +++ b/experiments/candidates/alacritty/test_candidate.py @@ -32,7 +32,7 @@ def test_fixed_streaming_grapheme_samples(self): self.assertFalse(summary["performance_measured"]) def test_identity_and_limitations_are_recorded(self): - self.assertEqual(self.summary["version"], "0.26.1-dev+termforge.5") + self.assertEqual(self.summary["version"], "0.26.1-dev+termforge.6") self.assertEqual(self.summary["license"], "Apache-2.0") self.assertEqual(self.summary["upstream_commit"], "542147067dd016fc5f49120ef0ba605a98a4e3a7") self.assertEqual(len(self.summary["trace_sha256"]), 64) diff --git a/experiments/candidates/alacritty/vendor/alacritty_terminal/Cargo.toml b/experiments/candidates/alacritty/vendor/alacritty_terminal/Cargo.toml index 0755589b..0ea94088 100644 --- a/experiments/candidates/alacritty/vendor/alacritty_terminal/Cargo.toml +++ b/experiments/candidates/alacritty/vendor/alacritty_terminal/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "alacritty_terminal" -version = "0.26.1-dev+termforge.5" +version = "0.26.1-dev+termforge.6" authors = ["Christian Duerr ", "Joe Wilm "] license = "Apache-2.0" description = "Library for writing terminal emulators" diff --git a/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/mod.rs b/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/mod.rs index 8c3fbed8..ca8ddd1e 100644 --- a/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/mod.rs +++ b/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/mod.rs @@ -405,6 +405,15 @@ impl Grid { self.display_offset = 0; } + /// Evict an oldest history prefix without changing the configured capacity. + pub fn discard_oldest_history(&mut self, count: usize) { + assert!(count <= self.history_size(), "history eviction exceeds retained rows"); + let discarded = self.discarded.checked_add(count as u64).expect("history discarded exhausted"); + self.raw.shrink_lines(count); + self.display_offset = min(self.display_offset, self.history_size()); + self.discarded = discarded; + } + /// This is used only for initializing after loading ref-tests. #[inline] pub fn initialize_all(&mut self) diff --git a/experiments/core/CANDIDATE.md b/experiments/core/CANDIDATE.md index 726fc697..2f8fcbcb 100644 --- a/experiments/core/CANDIDATE.md +++ b/experiments/core/CANDIDATE.md @@ -2,7 +2,7 @@ 按 [ADR-0006](../../docs/adr/0006-confirmed-project-direction.md),用户确认保留本候选代码继续评估,尚未确认最终采用。下述实现、来源与回归结果继续保留;与 WindTerm 2.7.0 的重要差异、性能容差及正式选型分别提交用户确认。 -`CandidateTerminal` 是 #27 的拥有值适配层,不是自研核心的替代品声明。它把固定 `0.26.1-dev+termforge.5` 的 `Term/Processor` 接到现有 `Screen`/`Line`;默认 CMake 保留自研核心且不需要 Rust,候选产品由 `TERMFORGE_BUILD_CANDIDATE_ADAPTER=ON` 显式启用。 +`CandidateTerminal` 是 #27 的拥有值适配层,不是自研核心的替代品声明。它把固定 `0.26.1-dev+termforge.6` 的 `Term/Processor` 接到现有 `Screen`/`Line`;默认 CMake 保留自研核心且不需要 Rust,候选产品由 `TERMFORGE_BUILD_CANDIDATE_ADAPTER=ON` 显式启用。 ## 构建与验证 @@ -28,7 +28,9 @@ Linux 使用相应 CMake 配置。候选 workflow 在 Windows/Ubuntu 构建 `can 回放行键与后续缓存匹配同时保留文字的起始列,只忽略末尾未写 padding。相同 `ABC` 从第 0 列移到第 2 列(或反向)不能沿用旧版本供选择/Find 使用;原选区失效,Find 重扫后命中实际列。独立[列位置回归](results/2026-10-27-replay-coordinates/README.md)覆盖回放中/回放后、整段/逐字节和原位对照。 -等待回放时冻结最近一份完整的 resize 后状态,旧尺寸快照只用于完成时匹配身份。解析器实际执行主屏 ED3(含 `03J`)或 RIS 后,`.5` 的清空代次经 C ABI 释放旧回放/缓存,不等 `?25h` 才显示清空;ED2 不清历史,清空不计为淘汰。[快照与清空回归](results/2026-10-27-resize-snapshot/README.md)记录固定可见网格和清空边界。跨宽度历史行的显示、零原生历史缓存启发式及备用屏缓存仍未完整验证;不能把这些局部结果称为完整历史合同。[原版文本接口观察](../../docs/research/windterm-resize-text.md)仅证明固定序列的文本保留和投影恢复,不验证本项目清空或冻结策略。 +等待回放时冻结最近一份完整的 resize 后状态,旧尺寸快照只用于完成时匹配身份。解析器实际执行主屏 ED3(含 `03J`)或 RIS 后,`.5` 的清空代次经 C ABI 释放旧回放/缓存,不等 `?25h` 才显示清空;ED2 不清历史,清空不计为淘汰。[快照与清空回归](results/2026-10-27-resize-snapshot/README.md)记录固定可见网格和清空边界。跨宽度历史行的显示仍有行宽不一致反例,完整历史合同未完成。[原版文本接口观察](../../docs/research/windterm-resize-text.md)仅证明固定序列的文本保留和投影恢复,不验证本项目清空或冻结策略。 + +`.6` 将主屏缓存、原生身份映射与淘汰端点一起保存在拥有值快照中,跨备用屏切换挂起/恢复;空原生历史不再推断为显式清空。容量淘汰同时失效旧回放中的缓存行;仍驻留 Rust 的旧行经 C ABI 核对整个最旧前缀的 ID 后从实际历史删除,原生与缓存独有行的淘汰各计一次。ED3/RIS 的解析器清空代次继续优先。相关[缓存与淘汰回归](results/2026-10-28-history-cache/README.md)覆盖透明输入、主/备用屏切换、回放中断、清空和实际淘汰;它不验证完整 reflow 或混宽历史的绘制。 ## 真实产品场景 diff --git a/experiments/core/candidate_terminal.cpp b/experiments/core/candidate_terminal.cpp index d90332ea..7dc18eb7 100644 --- a/experiments/core/candidate_terminal.cpp +++ b/experiments/core/candidate_terminal.cpp @@ -99,7 +99,7 @@ std::vector replay_keys(const History& history, const Lines& lines) } // namespace void CandidateTerminal::begin_replay() { - replay_origin_ = std::make_unique(screen); + replay_origin_ = std::make_unique(screen, identity_aliases_, native_discarded_, preserve_history_); replay_bytes_ = 0; replay_pending_ = replay_hide_ = replay_show_ = false; } @@ -148,7 +148,7 @@ std::unordered_set CandidateTerminal::reconcile_replay(std::vectorscreen : screen; const auto expected = replay_keys(source.history, source.lines); const auto actual = replay_keys(std::vector{}, retained); std::unordered_map old_counts, new_counts; @@ -172,16 +172,25 @@ std::unordered_set CandidateTerminal::reconcile_replay(std::vector& retained, std::vector& original_ids, size_t& history, uint64_t& discarded) { - if (!preserve_history_) return; - const auto& source = replay_origin_ ? *replay_origin_ : screen; +std::unordered_set CandidateTerminal::merge_history(std::vector& retained, + std::vector& original_ids, size_t& history, uint64_t& discarded, uint64_t& native_discarded) { + if (!preserve_history_) return {}; + const auto& source = replay_origin_ ? replay_origin_->screen : screen; + const auto& aliases = replay_origin_ ? replay_origin_->aliases : identity_aliases_; + const auto source_discarded = replay_origin_ ? replay_origin_->native_discarded : native_discarded_; std::deque previous = source.history; - if (discarded > source.discarded) { - const auto removed = discarded - source.discarded; - if (removed >= previous.size()) previous.clear(); - else previous.erase(previous.begin(), previous.begin() + static_cast(removed)); + if (native_discarded > source_discarded) { + // Native eviction cannot identify cache-only rows by their ordinal. + // Remove old rows backed by native identities that are actually gone. + const std::unordered_set current(original_ids.begin(), original_ids.end()); + std::unordered_set removed; + for (const auto& alias : aliases) + if (current.find(alias.first) == current.end()) removed.insert(alias.second.id); + previous.erase(std::remove_if(previous.begin(), previous.end(), [&](const Line& line) { + return removed.find(line.id) != removed.end(); + }), previous.end()); } - if (previous.empty()) return; + if (previous.empty()) return {}; std::unordered_set raw_ids; for (size_t index = 0; index < history; ++index) raw_ids.insert(retained[index].id); std::unordered_set candidates, unchanged_history, unchanged_visible; @@ -214,14 +223,26 @@ void CandidateTerminal::merge_history(std::vector& retained, std::vector evicted_native; + std::unordered_set evicted; while (history > screen.history_limit) { if (discarded == (std::numeric_limits::max)()) throw std::overflow_error("candidate discarded history exhausted"); ++discarded; + evicted.insert(retained.front().id); + if (original_ids.front()) evicted_native.push_back(original_ids.front()); retained.erase(retained.begin()); original_ids.erase(original_ids.begin()); --history; } + if (!evicted_native.empty()) { + // Remove still-resident native rows too: otherwise they reappear on the + // next import or height growth and get counted as evicted repeatedly. + std::array error{}; + check(tf_terminal_discard_history(impl_, evicted_native.data(), evicted_native.size(), + &native_discarded, error.data(), error.size()), "candidate history eviction failed"); + } + return evicted; } void CandidateTerminal::feed(std::string_view bytes) { @@ -276,15 +297,41 @@ std::vector CandidateTerminal::take_events() { void CandidateTerminal::synchronize() { TfTerminalMeta meta{}; check(tf_terminal_meta(impl_, &meta), "candidate metadata failed"); - if (meta.history_clear_epoch != history_clear_epoch_) { + const bool history_cleared = meta.history_clear_epoch != history_clear_epoch_; + if (history_cleared) { // A parsed clear invalidates cached history even in an incomplete replay. // The generation also covers clears when the native history is empty. history_clear_epoch_ = meta.history_clear_epoch; replay_origin_.reset(); + suspended_primary_.reset(); + preserve_history_ = false; + identity_aliases_.clear(); + replay_pending_ = replay_hide_ = replay_show_ = false; + replay_bytes_ = 0; + } + // mode_bits carries the pinned candidate's TermMode::ALT_SCREEN bit. + constexpr uint64_t alternate_mask = uint64_t{1} << 12; + const bool alternate = (meta.mode_bits & alternate_mask) != 0; + bool restoring_primary = false; + if ((meta.mode_bits ^ mode_bits_) & alternate_mask) { + if (alternate && !history_cleared) { + // A buffer switch ends the primary replay. Its old history must + // survive even if the pending repaint has already replaced it. + suspended_primary_ = replay_pending_ && replay_origin_ ? std::move(replay_origin_) : + std::make_unique(screen, identity_aliases_, native_discarded_, preserve_history_); + if (replay_pending_) suspended_primary_->preserve_history = true; + } + replay_origin_.reset(); preserve_history_ = false; identity_aliases_.clear(); replay_pending_ = replay_hide_ = replay_show_ = false; replay_bytes_ = 0; + replay_tail_.clear(); + if (!alternate && suspended_primary_) { + replay_origin_ = std::move(suspended_primary_); + preserve_history_ = replay_origin_->preserve_history; + restoring_primary = true; + } } if (replay_pending_) { // Keep the last complete publication, including its resized dimensions. @@ -294,8 +341,9 @@ void CandidateTerminal::synchronize() { mode_bits_ = meta.mode_bits; grapheme_overflows_ = meta.grapheme_overflows; std::unordered_map previous; - for (const auto& line : screen.history) previous.emplace(line.id, &line); - for (const auto& line : screen.lines) previous.emplace(line.id, &line); + const auto& previous_screen = restoring_primary ? replay_origin_->screen : screen; + for (const auto& line : previous_screen.history) previous.emplace(line.id, &line); + for (const auto& line : previous_screen.lines) previous.emplace(line.id, &line); std::vector retained(meta.history + meta.rows); std::vector raw_ids; @@ -320,10 +368,19 @@ void CandidateTerminal::synchronize() { } const auto preserved = reconcile_replay(retained); if (replay_show_) preserve_history_ = true; - if (preserve_history_ && !replay_origin_ && meta.history == 0 && meta.discarded == screen.discarded) - preserve_history_ = false; - merge_history(retained, raw_ids, meta.history, meta.discarded); - if (replay_show_) replay_origin_.reset(); + auto native_discarded = meta.discarded; + std::unordered_set evicted; + if (!alternate) { + // The native counter cannot include rows evicted only by our cache. + // Explicit clears preserve this count; alternate output has its own. + if (cached_history_discarded_ > (std::numeric_limits::max)() - meta.discarded) + throw std::overflow_error("candidate discarded history exhausted"); + meta.discarded += cached_history_discarded_; + const auto before_merge = meta.discarded; + const auto native_before_merge = native_discarded; + evicted = merge_history(retained, raw_ids, meta.history, meta.discarded, native_discarded); + cached_history_discarded_ += (meta.discarded - before_merge) - (native_discarded - native_before_merge); + } screen.normalize_identities(retained); for (size_t r = 0; r + 1 < retained.size(); ++r) if (retained[r].ending == LineEnding::soft_wrap) retained[r].next_id = retained[r + 1].id; @@ -352,6 +409,7 @@ void CandidateTerminal::synchronize() { screen.column = static_cast(meta.column); screen.wrap_pending = meta.wrap_pending; screen.discarded = meta.discarded; + native_discarded_ = native_discarded; screen.style = {meta.fg, meta.bg, meta.bold}; screen.history.assign(retained.begin(), retained.begin() + meta.history); screen.lines.assign(retained.begin() + meta.history, retained.end()); @@ -359,5 +417,19 @@ void CandidateTerminal::synchronize() { identity_aliases_.clear(); for (size_t index = 0; index < retained.size(); ++index) if (raw_ids[index]) identity_aliases_.emplace(raw_ids[index], IdentityAlias{retained[index].id, retained[index].version}); + if (replay_origin_ && !evicted.empty()) { + // Retire evicted cache rows from the old replay too. Delay mutation until + // version comparisons have finished using pointers into this snapshot. + auto& history = replay_origin_->screen.history; + history.erase(std::remove_if(history.begin(), history.end(), [&](const Line& line) { + return evicted.find(line.id) != evicted.end(); + }), history.end()); + auto& aliases = replay_origin_->aliases; + for (auto entry = aliases.begin(); entry != aliases.end();) { + if (evicted.find(entry->second.id) != evicted.end()) entry = aliases.erase(entry); + else ++entry; + } + } + if (replay_show_ || restoring_primary) replay_origin_.reset(); } } // namespace termforge diff --git a/experiments/core/candidate_terminal.hpp b/experiments/core/candidate_terminal.hpp index 3cfe92cd..9b228a9d 100644 --- a/experiments/core/candidate_terminal.hpp +++ b/experiments/core/candidate_terminal.hpp @@ -18,11 +18,24 @@ class CandidateTerminal { void* impl_ = nullptr; uint64_t mode_bits_ = 0, grapheme_overflows_ = 0; uint64_t history_clear_epoch_ = 0; + uint64_t cached_history_discarded_ = 0; std::unordered_map versions_; std::vector events_; struct IdentityAlias { uint64_t id = 0, version = 0; }; std::unordered_map identity_aliases_; - std::unique_ptr replay_origin_; + uint64_t native_discarded_ = 0; + struct Publication { + Screen screen; + std::unordered_map aliases; + uint64_t native_discarded; + bool preserve_history; + Publication(const Screen& value, const std::unordered_map& ids, + uint64_t discarded, bool preserve) + : screen(value), aliases(ids), native_discarded(discarded), preserve_history(preserve) {} + }; + std::unique_ptr replay_origin_; + // The alternate grid has no primary scrollback; retain its owner separately. + std::unique_ptr suspended_primary_; std::string replay_tail_; size_t replay_bytes_ = 0; bool replay_pending_ = false, replay_hide_ = false, replay_show_ = false; @@ -31,7 +44,8 @@ class CandidateTerminal { void begin_replay(); void scan_replay(std::string_view bytes); std::unordered_set reconcile_replay(std::vector& retained); - void merge_history(std::vector& retained, std::vector& original_ids, size_t& history, uint64_t& discarded); + std::unordered_set merge_history(std::vector& retained, std::vector& original_ids, + size_t& history, uint64_t& discarded, uint64_t& native_discarded); void synchronize(); static void check(int status, const char* message); diff --git a/experiments/core/candidate_terminal_test.cpp b/experiments/core/candidate_terminal_test.cpp index 787c5ddd..ae72b282 100644 --- a/experiments/core/candidate_terminal_test.cpp +++ b/experiments/core/candidate_terminal_test.cpp @@ -523,6 +523,185 @@ void erase_screen_keeps_replay_history(bool fragmented) { "ED2 did not erase the visible screen"); } +void cached_history_survives_noop() { + termforge::CandidateTerminal terminal(2, 8, 4); + terminal.feed("A\r\nB\r\nC\r\nD"); + terminal.resize(4, 4); + terminal.feed("\x1b[?25l\x1b[2J\x1b[HC\r\nD\x1b[?25h"); + require(terminal.screen.history.size() == 2 && text(terminal.screen.history[0]) == "A" && + text(terminal.screen.history[1]) == "B", "cached history no-op fixture differs"); + termforge::TextDocument before(terminal.screen); + termforge::TextSelection selection; + require(selection.select_line(before, 0), "cached history selection fixture failed"); + termforge::TextSearch search; + const auto generation = search.start(before, "A"); + for (int step = 0; step < 100 && search.status() == termforge::SearchStatus::running; ++step) + search.step(before, generation, 64); + require(search.match(before, 0).has_value(), "cached history Find fixture failed"); + const auto expected = state(terminal); + for (const auto bytes : {std::string_view{}, std::string_view("\0", 1), std::string_view("\x1b[6n")}) { + terminal.feed(bytes); + require(state(terminal) == expected, "transparent feed discarded cached history"); + } + termforge::TextDocument after(terminal.screen); + require(selection.copy(after, 64).status == termforge::SelectionCopyStatus::ok && + selection.copy(after, 64).text == "A", "empty feed invalidated cached history selection"); + require(search.match(after, 0).has_value(), "transparent feed invalidated cached history Find hit"); + terminal.feed("\x1b[3J"); + termforge::TextDocument cleared(terminal.screen); + require(terminal.screen.history.empty() && !search.match(cleared, 0) && + selection.copy(cleared, 64).status == termforge::SelectionCopyStatus::invalidated, + "clear did not invalidate cached history text actions"); +} + +void cached_history_stays_with_primary(bool fragmented) { + termforge::CandidateTerminal terminal(2, 8, 4); + terminal.feed("A\r\nB\r\nC\r\nD"); + terminal.resize(4, 4); + terminal.feed("\x1b[?25l\x1b[2J\x1b[HC\r\nD\x1b[?25h"); + require(terminal.screen.history.size() == 2, "primary cached history fixture differs"); + const auto first = terminal.screen.history[0].id, second = terminal.screen.history[1].id; + const auto feed = [&](std::string_view bytes) { + if (fragmented) for (const char byte : bytes) terminal.feed(std::string_view(&byte, 1)); + else terminal.feed(bytes); + }; + feed("\x1b[?1049h"); + require(terminal.screen.history.empty(), "primary cached history leaked into alternate screen"); + feed("\x1b[HALT\r\nX\r\nY\r\nZ\r\nQ\x1b[3J"); + require(terminal.screen.history.empty(), "alternate output retained primary history"); + feed("\x1b[?1049l"); + require(terminal.screen.history.size() == 2 && text(terminal.screen.history[0]) == "A" && + text(terminal.screen.history[1]) == "B" && terminal.screen.history[0].id == first && + terminal.screen.history[1].id == second, "alternate switch lost primary cached history"); + termforge::TextDocument restored(terminal.screen); + termforge::TextSelection selection; + require(selection.select_line(restored, 0) && selection.copy(restored, 64).text == "A", + "restored history cannot be selected"); + termforge::TextSearch search; + const auto generation = search.start(restored, "A"); + for (int step = 0; step < 100 && search.status() == termforge::SearchStatus::running; ++step) + search.step(restored, generation, 64); + const auto match = search.match(restored, 0); + require(match && match->range.begin.line_id == first && match->range.begin.column == 0, + "restored history Find coordinates differ"); + feed("\x1b[?1049h\x1b[HALT\x1b[?1049l"); + require(terminal.screen.history.size() == 2 && text(terminal.screen.history[0]) == "A" && + text(terminal.screen.history[1]) == "B", "coalesced buffer switch lost cached history"); +} + +void alternate_interrupts_pending_history_replay(bool fragmented) { + termforge::CandidateTerminal terminal(2, 8, 4); + terminal.feed("A\r\nB\r\nC\r\nD"); + terminal.resize(4, 4); + const auto feed = [&](std::string_view bytes) { + if (fragmented) for (const char byte : bytes) terminal.feed(std::string_view(&byte, 1)); + else terminal.feed(bytes); + }; + feed("\x1b[?25l\x1b[2J\x1b[HC\r\nD"); + feed("\x1b[?1049h\x1b[HALT"); + require(terminal.screen.history.empty(), "interrupted primary history leaked into alternate screen"); + feed("\x1b[?1049l\x1b[?25h"); + require(terminal.screen.history.size() == 2 && text(terminal.screen.history[0]) == "A" && + text(terminal.screen.history[1]) == "B", "alternate interrupted replay lost primary history"); +} + +void cached_history_eviction_count_is_stable(bool fragmented) { + termforge::CandidateTerminal terminal(2, 8, 4); + terminal.feed("A\r\nB\r\nC\r\nD"); + terminal.resize(4, 4); + terminal.feed("\x1b[?25l\x1b[2J\x1b[HC\r\nD\x1b[?25h"); + const auto discarded = terminal.screen.discarded; + const auto feed = [&](std::string_view bytes) { + if (fragmented) for (const char byte : bytes) terminal.feed(std::string_view(&byte, 1)); + else terminal.feed(bytes); + }; + feed("\x1b[4;1HE\r\nF\r\nG\r\n"); + require(terminal.screen.history.size() == 4 && text(terminal.screen.history[0]) == "B" && + text(terminal.screen.history[1]) == "C" && text(terminal.screen.history[2]) == "D" && + text(terminal.screen.history[3]).empty(), "cached eviction retained the wrong rows"); + require(terminal.screen.discarded == discarded + 1, "cached eviction count differs"); + terminal.feed(""); + require(terminal.screen.discarded == discarded + 1, "empty feed reset cached eviction count"); + feed("H\r\nI\r\n"); + require(terminal.screen.discarded == discarded + 3, "native and cached eviction counts were not combined"); + require(terminal.screen.history.size() == 4 && text(terminal.screen.history[0]) == "D" && + text(terminal.screen.history[1]).empty() && text(terminal.screen.history[2]) == "E" && + text(terminal.screen.history[3]) == "F", "native and cached eviction retained the wrong rows"); + feed("\x1b[3J"); + require(terminal.screen.history.empty() && terminal.screen.discarded == discarded + 3, + "explicit clear changed accumulated eviction count"); +} + +void cached_history_resize_and_reset_in_alternate(bool reset, bool fragmented) { + termforge::CandidateTerminal terminal(2, 8, 4); + terminal.feed("A\r\nB\r\nC\r\nD"); + terminal.resize(4, 4); + terminal.feed("\x1b[?25l\x1b[2J\x1b[HC\r\nD\x1b[?25h"); + const auto feed = [&](std::string_view bytes) { + if (fragmented) for (const char byte : bytes) terminal.feed(std::string_view(&byte, 1)); + else terminal.feed(bytes); + }; + feed("\x1b[?1049h"); + terminal.resize(3, 6); + feed("\x1b[?25l\x1b[HALT\x1b[?25h"); + require(terminal.screen.history.empty(), "alternate resize leaked cached primary history"); + feed(reset ? "\x1b" "c" : "\x1b[3J"); + feed("\x1b[?1049l"); + require(!(terminal.mode_bits() & (1 << 12)), "primary screen was not restored"); + if (reset) require(terminal.screen.history.empty(), "RIS resurrected suspended primary history"); + else require(terminal.screen.history.size() >= 2 && text(terminal.screen.history[0]) == "A" && + text(terminal.screen.history[1]) == "B", "alternate ED3 or resize cleared primary history"); +} + +void cached_history_evicts_native_prefix(bool fragmented) { + termforge::CandidateTerminal terminal(2, 8, 4); + terminal.feed("A\r\nB\r\nC\r\nD\r\nE\r\nF"); + terminal.resize(3, 8); + terminal.feed("\x1b[?25l\x1b[2J\x1b[HE\r\nF\x1b[?25h"); + const std::string output = "\x1b[3;1HG\r\n"; + if (fragmented) for (const char byte : output) terminal.feed(std::string_view(&byte, 1)); + else terminal.feed(output); + require(terminal.screen.history.size() == 4 && text(terminal.screen.history[0]) == "B" && + text(terminal.screen.history[1]) == "C" && text(terminal.screen.history[2]) == "D" && + text(terminal.screen.history[3]) == "E" && terminal.screen.discarded == 1, + "interleaved history eviction differs"); + const auto expected = state(terminal); + for (int repeat = 0; repeat < 3; ++repeat) { + terminal.feed(""); + require(state(terminal) == expected, "native prefix was reimported and evicted again"); + } + terminal.resize(7, 8); + for (size_t index = 0; index < terminal.screen.retained_size(); ++index) + require(text(terminal.screen.retained_line(index)) != "A", "height growth resurrected an evicted native row"); +} + +void cached_eviction_invalidates_replay_origin(bool fragmented) { + termforge::CandidateTerminal terminal(2, 8, 4); + const auto feed = [&](std::string_view bytes) { + if (fragmented) for (const char byte : bytes) terminal.feed(std::string_view(&byte, 1)); + else terminal.feed(bytes); + }; + feed("A\r\nB\r\nC\r\nD"); + terminal.resize(4, 8); + feed("\x1b[?25l\x1b[2J\x1b[HC\r\nD\x1b[?25h"); + require(terminal.screen.history.size() == 2, "old replay cache fixture differs"); + feed("\x1b[4;1H"); + terminal.resize(1, 8); + require(terminal.screen.history.size() == 4 && text(terminal.screen.history[0]) == "B" && + text(terminal.screen.history[1]) == "C" && text(terminal.screen.history[2]) == "D" && + text(terminal.screen.history[3]).empty() && terminal.screen.discarded == 1, + "resize cache eviction differs"); + const auto expected = state(terminal); + for (int repeat = 0; repeat < 3; ++repeat) { + terminal.feed(""); + require(state(terminal) == expected, "old replay restored an evicted cache row"); + } + feed("\x1b[?25l\x1b[?1049hALT\x1b[?1049l\x1b[?25h"); + require(terminal.screen.discarded == 1, "buffer switch re-evicted an old replay cache row"); + for (const auto& line : terminal.screen.history) + require(text(line) != "A", "buffer switch restored an evicted old replay row"); +} + void replay_text_coordinates(int from, int to, bool during_replay, bool fragmented) { termforge::CandidateTerminal terminal(2, 8, 4); terminal.feed(from == 0 ? "H\r\nABC\r\nTAIL" : "H\r\n\x1b[3GABC\r\nTAIL"); @@ -576,6 +755,17 @@ void conpty_resize_replay(const std::filesystem::path& path) { replay_case(path, true); incomplete_replay_is_bounded(); pending_replay_keeps_resized_grid(); + cached_history_survives_noop(); + cached_history_stays_with_primary(false); + cached_history_stays_with_primary(true); + for (const bool fragmented : {false, true}) { + alternate_interrupts_pending_history_replay(fragmented); + cached_history_eviction_count_is_stable(fragmented); + cached_history_resize_and_reset_in_alternate(false, fragmented); + cached_history_resize_and_reset_in_alternate(true, fragmented); + cached_history_evicts_native_prefix(fragmented); + cached_eviction_invalidates_replay_origin(fragmented); + } history_clear_does_not_resurrect(); for (const bool fragmented : {false, true}) { for (const auto clear : {"\x1b[3J", "\x1b[03J", "\x1b" "c"}) diff --git a/experiments/core/results/2026-10-28-history-cache/README.md b/experiments/core/results/2026-10-28-history-cache/README.md new file mode 100644 index 00000000..7037e075 --- /dev/null +++ b/experiments/core/results/2026-10-28-history-cache/README.md @@ -0,0 +1,43 @@ +# #29:主屏历史缓存归属与容量淘汰 + +本切片修复候选的透明输入丢历史、主/备用屏缓存归属和重复淘汰问题。基点为 PR #109 的 `6a108e1d15f00dc9497dac79a8bf75a3559469f8`,候选升至 `0.26.1-dev+termforge.6`;固定上游、59 个注册表依赖及许可证不变。它不关闭 #29,不代表正式采用 Alacritty,也不改变默认核心或 PR #50 实验官方后端。 + +## 原因与行为 + +- 原生历史为零曾被推断为显式清空,导致 resize 重绘后只输入空串就丢失缓存 A/B。现在由解析器清空代次裁决 ED3/RIS,空串、NUL、DSR 保持历史及已有选择/Find;ED3 仍使历史与文字动作失效。 +- 主屏缓存与回放来源现在共同保存拥有值 Screen、原生行别名及淘汰端点,进入备用屏时挂起,退出时恢复。备用屏不显示主屏历史;备用屏 ED3 保留主屏缓存,RIS 使两屏旧缓存失效。覆盖整段/逐字节、合并切换、pending 回放中断与备用屏 resize。 +- 原生淘汰按实际消失的原生身份去除旧行;缓存独有行的淘汰另行累计。容量裁掉仍驻留 Rust 的行时,C ABI 先核对整个最旧前缀的 ID,再删除实际历史,保持配置容量。已淘汰的缓存行还须从旧回放快照退休;删除发生在行版本比较之后,避免使旧行指针失效。 + +两项独立审查反例均保留原始 red 与修复后 green: + +| 反例 | 修复前 | 修复后 | +|---|---|---| +| 旧原生 A 与较新缓存 D 交错;容量留下 B/C/D/E | 在 `b966adc` 上,一次空 feed 使淘汰数 1→2 | 内容保持,计数保持 1;整段/逐字节及增高不复活 A 的回归通过 | +| 缓存 A/B 后缩高至 1 行,旧回放仍含已淘汰 A | 在 `93b18db` 上,连续三次空 feed 使计数 1→2→3→4 | 历史 B/C/D/空行与计数 1 保持;后续 pending/备用屏切换不恢复 A | + +公开 `CandidateTerminal::feed/resize` 回归保留原 861B 重放、列位置和显式清空检查。另一个 Rust FFI 测试核对错误第二行 ID 时完全不修改、备用屏拒绝、正确前缀移除、零数量 no-op、容量保持和增高不复活;它只验证接口合同。 + +## 验证与复现 + +Windows 11 x64 候选/system 完整 CTest **49/49**;其中 `paste_system` 内部有 **1 项跳过**:私有 window station 的 `OpenClipboard` 返回 5,真实系统剪贴板门槛仍未验证。Rust 工作区 **157 上游单元 + 7 适配/程序单元 + 1 doc test** 通过,重建 trace 后候选报告测试 **6/6** 通过。Standards 与 Spec 最终代码审查均为 0 项。 + +源码提交、tree、每个源码 blob/工作文件 SHA-256、二进制及本地原始日志哈希见 [validation.json](validation.json)。本地审查代码为 `4940050`,公开代码 `1c1a64e59803e2cd0be6e07a4cd4a052705f5a7e` 与它使用相同 tree;最终结果只对应候选/system/Release,不能转用于默认配置或实验官方后端。早期两轮完整套件虽通过,随后仍发现覆盖外反例,已分列保存。 + +```powershell +$env:RUSTUP_TOOLCHAIN = '1.92.0' +# 本次使用已有 Git for Windows Vim;仅修改测试进程环境。 +$env:PATH = 'C:\Program Files\Git\usr\bin;' + $env:PATH +cmake --build build/candidate-adapter --config Release +cargo test --manifest-path experiments/candidates/alacritty/Cargo.toml --locked --workspace +cargo build --manifest-path experiments/candidates/alacritty/Cargo.toml --locked --release +python -m unittest experiments.candidates.alacritty.test_candidate -v +ctest --test-dir build/candidate-adapter -C Release --output-on-failure +``` + +构建配置与前置见[候选说明](../../CANDIDATE.md)。过程日志留在本地;公开内容只有合成场景、脱敏观察与哈希。新增逻辑是按已有历史/清空合同独立编写的 TermForge 适配及固定 Apache-2.0 Alacritty 补丁,没有引入 WindTerm 私有源码、反编译输出或资源。本轮未运行原版,也未做性能测量。 + +## 仍失败及未完成 + +在同一最终代码上,2×8、history=4 输入 A/B/C/D,resize(4,4) 后重绘 C/D,滚至历史首行的快照仍为 **8、8、4、4** 列,当前屏幕宽度为 **4**;独立形状检查退出 1。随后空 feed 的历史数保持 **2**,明确区分已修复的丢历史与未修复的混宽视口。未执行该反例的实际渲染,不宣称已观察到崩溃,也不通过永久裁剪文本掩盖它。 + +完整逻辑/物理行映射、增长后的全部顺序与 reflow、连续 resize、备用屏/滚动区组合、原版选择/Find 保留行为及真实 child ack 继续归 #29。真实不同 DPI、完整系统输入组合、实验官方后端、实际呈现、性能及历史可靠性缺口各自验收;局部通过不构成 R2 出口或原版等价。 diff --git a/experiments/core/results/2026-10-28-history-cache/validation.json b/experiments/core/results/2026-10-28-history-cache/validation.json new file mode 100644 index 00000000..41fe27cc --- /dev/null +++ b/experiments/core/results/2026-10-28-history-cache/validation.json @@ -0,0 +1,507 @@ +{ + "scope": "R2 #29 primary history cache ownership, transparent feed and capacity eviction; incomplete resize acceptance", + "source_commit": "1c1a64e59803e2cd0be6e07a4cd4a052705f5a7e", + "reviewed_local_code_commit": "4940050655e94cb445c88e3b08d9675788c0bc61", + "identical_source_tree": "ea13b9db473eb456225fa953e5c1c684d6b1bc46", + "base_commit": "6a108e1d15f00dc9497dac79a8bf75a3559469f8", + "source_files": [ + { + "path": "experiments/candidates/alacritty/Cargo.lock", + "git_blob": "80d1c6262af07dc6dbb8036b8652de757555b580", + "worktree_sha256": "64a6e71b8972f05e0710c1a9bb876fb29648dfdfef1de7dcd067d1ecdfcf3b6d" + }, + { + "path": "experiments/candidates/alacritty/PROVENANCE.md", + "git_blob": "659f7d130af0d78e379f173fe005fb40f592d7ab", + "worktree_sha256": "c07b79501fcadda65baf966902edadbe5933ccbbf7c10c2ed235dbc236786754" + }, + { + "path": "experiments/candidates/alacritty/include/termforge_candidate.h", + "git_blob": "53ba25baabffb501aaf5e35f001ce974b67b103d", + "worktree_sha256": "ea78826c5be2194e42266e0d34ba6378d826b860014e7c5d221c370afc95f664" + }, + { + "path": "experiments/candidates/alacritty/run.py", + "git_blob": "2a6a29d6a9804288958b389479ec3b7b73105c43", + "worktree_sha256": "2c8efbbd0ad2c72d2e2c0a543b94ffda96f1da1fa570d4a97193c3bf96c13c3c" + }, + { + "path": "experiments/candidates/alacritty/src/ffi.rs", + "git_blob": "43cf61391518d1477ede9fa4f764ec33a0b6606d", + "worktree_sha256": "78cc39dbada42a55ce7422032e30852bb322d5bb63777bbba3e798bea8ec245f" + }, + { + "path": "experiments/candidates/alacritty/test_candidate.py", + "git_blob": "5335ab93181bde5cf6bdad62d943b84aed5de0a0", + "worktree_sha256": "07860a851ec44b25c0f2b24c8d84855e9f62ad0335ce0761687c012940fbc7ed" + }, + { + "path": "experiments/candidates/alacritty/vendor/alacritty_terminal/Cargo.toml", + "git_blob": "0ea940886fd7111ac7eec58a36d3176ed5202a35", + "worktree_sha256": "32dac1a8741538ddbeb4a7f7099f2b61ff01a8e67e61496e4634fd990e86338e" + }, + { + "path": "experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/mod.rs", + "git_blob": "ca8ddd1ea48a66608d1b3b2e4aee839e97e51fde", + "worktree_sha256": "0c9c18ef9df4f59ec4e16a3175a09070c0366816ffaa8b67d78c3c4b3db9d799" + }, + { + "path": "experiments/core/candidate_terminal.cpp", + "git_blob": "7dc18eb72ac63e3f00ebc4df24fb96087a3c21b2", + "worktree_sha256": "e0d856ce47510df93baf950a8ba36d1ab8e3db313f501cfa7207e707099e3f11" + }, + { + "path": "experiments/core/candidate_terminal.hpp", + "git_blob": "9b228a9d95c39151d6bd71ba2974f39c9857b4a5", + "worktree_sha256": "85a9860695edfed329699a07e9613f79ad2de16292524c7a395673092f72b9dc" + }, + { + "path": "experiments/core/candidate_terminal_test.cpp", + "git_blob": "ae72b282088b975bcca0499337a3836d5078ee92", + "worktree_sha256": "37462e25a7911d09f1e8d3cc7d0278665cf6efec0e58587df25b92a5d4609ef6" + } + ], + "configuration": { + "os": "Windows 11 Pro 10.0.26200 x64", + "candidate": "0.26.1-dev+termforge.6", + "adapter": true, + "backend": "system ConPTY", + "build_type": "Release", + "rust": "1.92.0", + "python": "3.12.10", + "msvc": "19.42.34433.0", + "windows_sdk": "10.0.26100.0", + "registered_dependencies": 59, + "path_crates": 2, + "vim": { + "version": "9.1 patches 1-532", + "sha256": "2e0fda63a89171adfc46c9417d5105cc39ed778cc69a890b83a4eabc34e7646a", + "path_policy": "Existing Git for Windows usr/bin prepended only in test process environment" + } + }, + "binaries": [ + { + "path": "build/candidate-adapter/bin/Release/candidate_core_test.exe", + "sha256": "488bba51708779ffd8317c09946fb9182acc7313f73ee7c3e09ec2fa6f1112dd" + }, + { + "path": "build/candidate-adapter/bin/Release/termforge_shell.exe", + "sha256": "92705678c27406f0cd7779e16910fb5b67953bb3ebbfc642eb3d563beb643a57" + }, + { + "path": "experiments/candidates/alacritty/target/release/termforge_alacritty_core.lib", + "sha256": "c2f9769629caa45794d49bfb256484748e5c5719fa8e32cc76b0654665efef71" + }, + { + "path": "experiments/candidates/alacritty/target/release/termforge-alacritty-trace.exe", + "sha256": "7c20a69c2450ce55209def718064c8f25581fd0c6eb6d07389a2f2a87caff620" + } + ], + "validation": { + "commands": [ + { + "log": "local/r29-history/build-final.txt", + "finished_utc": "2026-10-27T16:28:48.9072391Z", + "started_utc": "2026-10-27T16:27:50.0260715Z", + "name": "build-final", + "exit_code": 0, + "sha256": "5e10b2c2eef7a83e90ecdb317811983fc30c60c115b69110380cde720b6ad604" + }, + { + "log": "local/r29-history/rust-final.txt", + "finished_utc": "2026-10-27T16:28:53.6216294Z", + "started_utc": "2026-10-27T16:28:48.9516626Z", + "name": "rust-final", + "exit_code": 0, + "sha256": "56560f85a08370a4c036525a99351ba9c3a21d87fdd0a1be7a01684b78a72596" + }, + { + "log": "local/r29-history/build-trace-final.txt", + "finished_utc": "2026-10-27T16:28:53.8677439Z", + "started_utc": "2026-10-27T16:28:53.6249355Z", + "name": "build-trace-final", + "exit_code": 0, + "sha256": "dbc2c037c3ab59d8b8aa7c39426e1a2bc5a3640f1666ff60efa0c197a5c60686" + }, + { + "log": "local/r29-history/runner-final.txt", + "finished_utc": "2026-10-27T16:28:54.2853320Z", + "started_utc": "2026-10-27T16:28:53.8706619Z", + "name": "runner-final", + "exit_code": 0, + "sha256": "4c10d89bb8d4e85573fa1706c57a740cc090471465f53759579c4e53262b5c6b" + }, + { + "log": "local/r29-history/ctest-final.txt", + "finished_utc": "2026-10-27T16:33:58.9842335Z", + "started_utc": "2026-10-27T16:28:54.2879219Z", + "name": "ctest-final", + "exit_code": 0, + "sha256": "b851dceceb541f2f0edd2a2f84555537e100483f2500b651a1acd9c2ccbe7e04" + } + ], + "ctest": { + "tests": 49, + "failures": 0, + "skipped": 0, + "cases": [ + { + "name": "shell_launch", + "status": "run" + }, + { + "name": "candidate_title", + "status": "run" + }, + { + "name": "runtime_observation", + "status": "run" + }, + { + "name": "ime_input_expectation", + "status": "run" + }, + { + "name": "sent_message_wait", + "status": "run" + }, + { + "name": "find_bar", + "status": "run" + }, + { + "name": "renderer_metrics", + "status": "run" + }, + { + "name": "ime_context", + "status": "run" + }, + { + "name": "layout_state", + "status": "run" + }, + { + "name": "workspace_state", + "status": "run" + }, + { + "name": "transport_completion", + "status": "run" + }, + { + "name": "scenario_tick_gate", + "status": "run" + }, + { + "name": "candidate_core", + "status": "run" + }, + { + "name": "resize_core", + "status": "run" + }, + { + "name": "history_core", + "status": "run" + }, + { + "name": "selection_core", + "status": "run" + }, + { + "name": "search_core", + "status": "run" + }, + { + "name": "paste", + "status": "run" + }, + { + "name": "clipboard_policy", + "status": "run" + }, + { + "name": "keyboard", + "status": "run" + }, + { + "name": "text_action_policy", + "status": "run" + }, + { + "name": "candidate_shell_tui", + "status": "run" + }, + { + "name": "core", + "status": "run" + }, + { + "name": "recovery", + "status": "run" + }, + { + "name": "measure", + "status": "run" + }, + { + "name": "binary_paths", + "status": "run" + }, + { + "name": "conpty_margin_contract", + "status": "run" + }, + { + "name": "benchmark_tools", + "status": "run" + }, + { + "name": "differential", + "status": "run" + }, + { + "name": "ime_inventory", + "status": "run" + }, + { + "name": "text_actions", + "status": "run" + }, + { + "name": "workflow_fixture", + "status": "run" + }, + { + "name": "workflow_sessions", + "status": "run" + }, + { + "name": "workflow_resources", + "status": "run" + }, + { + "name": "workflow_resource_phases", + "status": "run" + }, + { + "name": "workflow_system_content", + "status": "run" + }, + { + "name": "workflow_system_runner", + "status": "run" + }, + { + "name": "copy_error_frame", + "status": "run" + }, + { + "name": "research_guards", + "status": "run" + }, + { + "name": "receive", + "status": "run" + }, + { + "name": "conpty", + "status": "run" + }, + { + "name": "conpty_ctrl_c", + "status": "run" + }, + { + "name": "conpty_margin", + "status": "run" + }, + { + "name": "presentation", + "status": "run" + }, + { + "name": "shell", + "status": "run" + }, + { + "name": "selection_bytes", + "status": "run" + }, + { + "name": "selection_context", + "status": "run" + }, + { + "name": "raw_console_receiver", + "status": "run" + }, + { + "name": "paste_system", + "status": "run" + } + ], + "internal_skips": [ + { + "ctest": "paste_system", + "test": "test_real_system_clipboard (experiments.shell.test_paste.PasteTests.test_real_system_clipboard)", + "reason": "Private window station OpenClipboard denied (5); system API gate remains unverified." + } + ], + "junit": { + "path": "build/candidate-adapter/local-final.xml", + "sha256": "f81d530522d011417ad36c643ba15e35659b053d8e95958a2a296f5cb1a318a8" + } + }, + "rust": { + "vendor_unit": 157, + "adapter_and_program_unit": 7, + "doc": 1 + }, + "runner": { + "tests": 6, + "failures": 0 + }, + "adapter_after_origin_fix": { + "path": "local/r29-history/origin-green.txt", + "sha256": "f249f16080e7539f36f1dc3103a9b03e5366fbdc5ef7166c21bea21f1a334a71" + } + }, + "counterexamples": [ + { + "name": "resident native prefix reimport", + "product_red_commit": "b966adcc7f71178ec7803c4e8753b11141ed45f7", + "probe_source_sha256": "767696f173de3cae54aed56de035ac21d6224bddf02fd6113eaa1519233ba785", + "red": { + "path": "local/r29-interleaved/red.txt", + "sha256": "9783bf36a2a705c7ae7a54766dd1139556b44df1e375c4d6181bc1d4744e8bf1", + "exit_code": 1, + "observed": "B|C|D|E| discarded 1 -> 2 after empty feed" + }, + "green": { + "path": "local/r29-interleaved/green.txt", + "sha256": "217ddf2aa56216497e2c1027fa7de9be73dc4f433d80d10281105363ad097e55", + "source_commit": "4940050655e94cb445c88e3b08d9675788c0bc61", + "exit_code": 0, + "observed": "B|C|D|E| discarded 1 -> 1 after empty feed" + } + }, + { + "name": "cache-only row retained by old replay origin", + "product_red_commit": "93b18db094310599c93bb1579640800058607021", + "probe_source_sha256": "79dbadc992b422cf9aeac8380a640f4cbd0ef01a72ec74ece6e46a954d560247", + "red": { + "path": "local/r29-spec-cache-origin/result.txt", + "sha256": "6e0827477011e1169ea5991759313bd1cdec8f8b16826201eed0b4b9e1aa27ac", + "exit_code": 1, + "observed": "B|C|D|| discarded 1 -> 2 -> 3 -> 4 after three empty feeds" + }, + "green": { + "path": "local/r29-spec-cache-origin/green.txt", + "sha256": "f964600058ce4ddcac00717a70ab63705fb98097bcca752daf949cacd8551506", + "source_commit": "4940050655e94cb445c88e3b08d9675788c0bc61", + "exit_code": 0, + "observed": "B|C|D|| discarded remains 1 after three empty feeds" + }, + "independent_review_green": { + "path": "local/r29-spec-cache-origin/fixed-result.txt", + "sha256": "f964600058ce4ddcac00717a70ab63705fb98097bcca752daf949cacd8551506" + } + } + ], + "incremental_diagnostics": { + "identity_limit": "Intermediate working-tree snapshots were not all independently frozen; these logs are diagnostic history, not separate pinned acceptance runs.", + "failure_logs": [ + { + "path": "local/r29-history/noop-red.txt", + "sha256": "d24cce242cce0943d27a35bf195f34784fbd5d150070d55ed27968f2041628ac" + }, + { + "path": "local/r29-history/alt-red.txt", + "sha256": "3dfd9adbcd5fc29ec075bdbe66c47cba4b9a6c7fbda502ff0c46f0f19c8fcee0" + }, + { + "path": "local/r29-history/interrupted-red.txt", + "sha256": "3319e7af926e1e64057fe9c0acc7cf108f2138c08474be87a9b234bb2ce3dd21" + }, + { + "path": "local/r29-history/eviction-red.txt", + "sha256": "05309c92e31bc94568248a30419a17e2b78e09d464b3c32783b4c08547c2ccdd" + }, + { + "path": "local/r29-history/eviction-green.txt", + "sha256": "be19abab14693305b9dace97abd343d1be972be9f459de163eacfce3e748ac00" + }, + { + "path": "local/r29-history/eviction-diagnostic.txt", + "sha256": "999060a3da70f7a8ce04dc9e2003a6dbe42040a161a43b8c02d0fcafad368ef9" + }, + { + "path": "local/r29-history/eviction-identities.txt", + "sha256": "c1190a7582d6c0ec06139b5e4a7d92a0be89e862ef3d0e3095e4e23007be4066" + }, + { + "path": "local/r29-history/native-prefix-red.txt", + "sha256": "c85a2f4f7c54d612836cee526abe7206cb1bc745e23ba8b6e15c8f3068519d77" + } + ], + "note": "eviction-green.txt is a retained failed attempt despite its filename." + }, + "earlier_full_runs": [ + { + "source_commit": "b966adcc7f71178ec7803c4e8753b11141ed45f7", + "ctest_passed": 49, + "record": { + "path": "local/r29-history/pre-review-validation.json", + "sha256": "08738458db8100b0897a2b6623a0e7f97c21370f63f40a804bd4f5f5bb4894c6" + }, + "limitation": "Did not cover the native-prefix counterexample; not final acceptance." + }, + { + "source_commit": "93b18db094310599c93bb1579640800058607021", + "ctest_passed": 49, + "record": { + "path": "local/r29-history/review93/frozen-validation.json", + "sha256": "7554af16fd8f2043b2d051dadf012e8d6e3dddaec724d0590062e79fd5e9efdb" + }, + "limitation": "Did not cover the cache-origin counterexample; not final acceptance." + } + ], + "known_failure": { + "source_commit": "4940050655e94cb445c88e3b08d9675788c0bc61", + "probe_source_sha256": "4d7bbf2004775095db4353d249b050876d08bbede1517297b9bce529b2612f66", + "binary_sha256": "8de021490aed198052ee8124dd06cbe95a1ceafff63229675221e328153705c8", + "result": { + "path": "local/r29-shape-final/result.txt", + "sha256": "90b755b98cc0809c7ab62cdd85a69a6cb5b3fdb24e8af2cc2901a873f2233f15" + }, + "exit_code": 1, + "observed_columns": 4, + "viewport_row_widths": [ + 8, + 8, + 4, + 4 + ], + "history_after_empty_feed": 2, + "actual_rendering_executed": false, + "note": "Probe initially failed to compile because it used Screen.columns instead of width(); corrected probe then reproduced the shape failure. This is not an observed rendering crash." + }, + "review": { + "code_source": "4940050655e94cb445c88e3b08d9675788c0bc61", + "standards_findings": 0, + "spec_findings": 0, + "earlier_spec_findings": "Two separate P2 eviction counterexamples reproduced and repaired; red evidence retained." + }, + "provenance": "Independent TermForge cache/ABI implementation and changes to pinned Apache-2.0 Alacritty. No WindTerm code, decompiled output, private resources or new dependencies.", + "windterm_runtime_executed_this_slice": false, + "performance_measured": false, + "limitations": [ + "History viewport mixed-width row shape still fails; no clipping or rendering workaround claimed.", + "Full chronological resize/reflow, all buffer/scroll-region/continuous-resize crossings and original behavior mapping remain open.", + "No acceptance of experimental official ConPTY backend, real different-DPI behavior, complete IME/clipboard combination or actual presentation.", + "Prior baseline/core/reliability failures remain separate; #29 and R2 remain open; Alacritty is a candidate.", + "Local raw logs contain environment paths and remain private; only hashes and sanitized observations are published." + ] +}