From 67ac9c12447884584098657f06742dcd3e0f3d59 Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Wed, 28 Oct 2026 01:25:51 +0800 Subject: [PATCH 01/14] wip: preserve native resize boundaries and expose cached-text duplication (#29) --- docs/EVIDENCE.md | 3 +- docs/FEATURES.md | 8 +- docs/research/windterm-history-layout.md | 42 + docs/research/windterm-resize-text.md | 2 + experiments/candidates/alacritty/Cargo.lock | 2 +- .../candidates/alacritty/PROVENANCE.md | 9 +- experiments/candidates/alacritty/README.md | 2 +- experiments/candidates/alacritty/run.py | 2 +- experiments/candidates/alacritty/src/ffi.rs | 57 ++ .../candidates/alacritty/test_candidate.py | 2 +- .../vendor/alacritty_terminal/Cargo.toml | 2 +- .../alacritty_terminal/src/grid/resize.rs | 33 + .../alacritty_terminal/src/grid/tests.rs | 145 ++- experiments/core/CANDIDATE.md | 4 +- experiments/core/candidate_terminal_test.cpp | 39 + .../windterm-input.json | 58 ++ .../windterm-observations.json | 912 ++++++++++++++++++ 17 files changed, 1309 insertions(+), 13 deletions(-) create mode 100644 docs/research/windterm-history-layout.md create mode 100644 experiments/core/results/2026-10-28-history-layout/windterm-input.json create mode 100644 experiments/core/results/2026-10-28-history-layout/windterm-observations.json diff --git a/docs/EVIDENCE.md b/docs/EVIDENCE.md index 11049ca6..4d523770 100644 --- a/docs/EVIDENCE.md +++ b/docs/EVIDENCE.md @@ -9,7 +9,7 @@ | 配置 | 证据定位 | |---|---| | main 默认:自研核心 + 系统 ConPTY | R1 限定总验收和有限 VT/Unicode 子集;默认构建不需要 Rust | -| 当前源码显式候选:`termforge.6` + 系统 ConPTY | [固定来源/补丁台账](../experiments/candidates/alacritty/PROVENANCE.md)、[适配边界](../experiments/core/CANDIDATE.md)、[#39 字素合同](research/streaming-graphemes.md);仅按已有合同验收 | +| 当前源码显式候选:`termforge.7` + 系统 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、绘制提交和真实呈现是不同检查点;未知端点保持未知。 @@ -38,6 +38,7 @@ R1 固定总出口的源码为 `ce9b24d`,早期 `2dc8d6d` / `9b16acc` 仍分 | 局部滚动区域(#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 清空不复活。`.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、实际呈现或性能。 | +| WindTerm 长历史行的视图折行(#29) | [逐字符观察与边界](research/windterm-history-layout.md)、[两次事实/哈希](../experiments/core/results/2026-10-28-history-layout/windterm-observations.json) | 独立的两次七阶段观察:185 字符软行由 80/80/25 变为 62/62/61,两条 77 字符硬行各变为 62/15;文档文本与位置不变,恢复宽度后投影恢复。仅 ASCII 文本坐标接口;覆盖写入、选择/Find、物理格及呈现仍未知。 | | 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。 | | 进程生命周期和平台边界 | [ConPTY 探针](../experiments/conpty/README.md)、[跨环境记录](research/conpty-platform-check-2026-10-22.md)、[旧平台探针](../.github/workflows/conpty-compat.yml) | 受控后代、取消/超时和短批次回收;Windows 11/Server 2025 有固定记录,Server 2022 稳态句柄门槛仍失败。长期无泄漏未知。 | diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 9c425cef..58a374c9 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -13,7 +13,7 @@ | 配置 | 当前定位 | 可以引用的证据与限制 | |---|---|---| | 默认 main:自研核心 + 系统 ConPTY | 默认构建与已发布 Alpha 的基础路线,不需要 Rust | 已有 R1 限定验收;VT/Unicode 能力仍为有限子集,不能引用候选核心的字素或 TUI 结果 | -| 当前源码显式候选:`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.7` + 系统 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/拼音验收属于固定自研核心配置,不外推到当前候选 或实验后端联合配置。 @@ -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 和有界恢复。显式候选 `.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 | +| VT 核心 | **实现中,未验收**:默认自研支持基础光标/擦除(含 ECH)、CR/LF/BS/TAB、有限 SGR 和有界恢复。显式候选 `.7` 接入 `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。显式候选 `.7` 保留 [#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)仅证明固定输入保留及恢复尺寸后文本投影恢复;[逐字符历史观察](research/windterm-history-layout.md)另证明固定 ASCII 长行随宽度重新分段,未验证覆盖写入或交互。完整 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/research/windterm-history-layout.md b/docs/research/windterm-history-layout.md new file mode 100644 index 00000000..8add86d4 --- /dev/null +++ b/docs/research/windterm-history-layout.md @@ -0,0 +1,42 @@ +# WindTerm 2.7.0:历史长文本的字符坐标与视图折行 + +这是 #29 的原版研究输入,不是产品策略确认或验收结果。[此前 resize 观察](windterm-resize-text.md)只有逻辑文本起止矩形,没有逐字符映射和主动历史滚动。本次使用新计划、新合成输入完成两次各 45 秒的有界观察,两个运行的七阶段文本坐标投影一致,随后停止。 + +固定官方 WindTerm 2.7.0 Windows x64,Windows 11 Pro 10.0.26200;新建合成 profile、自有非输入窗口站/desktop、loopback Telnet peer。输入为 1143 字节 ASCII、45 条逻辑文本,其中索引 6/7/8 分别是 185 字符软长行和两条 77 字符 CRLF 硬行。随后 35 条短尾行使三个长行全部进入历史,初始 bottom 阶段其字符矩形 y 均为负。[合成输入](../../experiments/core/results/2026-10-28-history-layout/windterm-input.json)可重建原始字节;[事实与哈希](../../experiments/core/results/2026-10-28-history-layout/windterm-observations.json)记录两次来源身份、阶段、坐标和清理。 + +## 观察 + +每阶段读取三个长行的全部 339 个 ASCII 字符位置及 3 个行尾位置,记录文档位置、矩形、`visibleLineFromPosition`、`lineFromPosition` 和 `visibleLineStart`。以下分组由逐字符坐标与视图行映射得出,没有把 `lineText` 条目解释为物理格。 + +| 阶段 | peer 实收 NAWS | scrollTop | visibleLineCount | 软长行字符分组 | 两条硬行各自分组 | +|---|---|---|---|---|---| +| 初始底部 | 80×26 | 22 | 47 | 80 / 80 / 25 | 77 | +| 历史位置 3 | 80×26 | 3 | 47 | 80 / 80 / 25 | 77 | +| 缩宽 | 62×26 | 3 | 49 | 62 / 62 / 61 | 62 / 15 | +| 滚动到 10 | 62×26 | 10 | 49 | 62 / 62 / 61 | 62 / 15 | +| 回到 3 | 62×26 | 3 | 49 | 62 / 62 / 61 | 62 / 15 | +| 恢复宽度 | 80×26 | 3 | 47 | 80 / 80 / 25 | 77 | +| 回到底部 | 80×26 | 22 | 47 | 80 / 80 / 25 | 77 | + +初次 NAWS 是 80×25,输入按该尺寸生成,随后基线稳定为 80×26。外框请求为 1230×887 → 1010×887 → 1230×887,实际尺寸以 peer 收到的 NAWS 为准。 + +全部 45 条文本及文档行、字符位置始终不变。软长行的文档位置范围是 108–293;缩宽时视图段起点从 108/188/268 变为 108/170/232。两条硬行仍分别属于逻辑行 7、8,同时各自在窄视图内分成两段。以 `scrollTop=3` 为例: + +| 软行 ASCII offset | 文档位置 | 80列 x,y | 62列 x,y | +|---|---|---|---| +| 62 | 170 | 643,54 | 147,72 | +| 80 | 188 | 147,72 | 291,72 | +| 124 | 232 | 499,72 | 147,90 | +| 160 | 268 | 147,90 | 435,90 | + +本环境全部已采样 ASCII 字符矩形为 8×18,各视图段内相邻字符 x 步长为 8。窄视图 `scrollTop` 从 3 到 10 再回 3 时,342 个采样位置的 y 统一 −126/+126,其余点字段不变。恢复宽度后,文本坐标投影精确等于原宽度的历史位置 3;回底部后精确等于初始底部。 + +这些连续 ASCII 样本支持“原版历史长逻辑行按当前视图宽度重新分段”的解释;显式 CRLF 仍保留独立逻辑行边界。内部存储方式和完整 VT reflow 语义不能由这些接口直接推出。 + +## 边界与来源 + +- 这是 Telnet 文本坐标接口的观察,不是物理格、实际呈现、ConPTY child 确认或性能测量;250 ms 稳定采样间隔不是验收阈值。 +- 未验证 selection 保留、复制、Find、XY 命中、Unicode 字素/宽度、词边界和空白。resize 后再覆盖写入/重绘、物理光标和完整 VT 行为仍未知。 +- 两次均核对固定 EXE、4047 文件规范 manifest 和全部复制文件哈希;输入从公开 JSON 重建后与两份原始 payload 字节及 SHA-256 完全一致。 +- 两次 Job 最终 active=0,peer 停止,desktop/station 句柄关闭,profile 各只有一个合成会话。未操作个人剪贴板、DPI 或输入桌面。 +- 发布内容仅含合成输入、白名单事实与原件哈希;app/profile、私有路径、桌面名、进程标识和原始提取脚本留在本地。研究结果不构成产品策略的用户确认。 diff --git a/docs/research/windterm-resize-text.md b/docs/research/windterm-resize-text.md index 6266db40..99310aae 100644 --- a/docs/research/windterm-resize-text.md +++ b/docs/research/windterm-resize-text.md @@ -29,3 +29,5 @@ ## 对实现的约束 现有默认物理裁剪及候选缓存只描述本项目实现。原版上述文本保留事实要求继续调查当前跨宽度历史的表示和显示,不能用永久裁掉缓存文本来消除行宽不一致。修正候选 pending 快照与显式清空属于[独立不变量修复](../../experiments/core/results/2026-10-27-resize-snapshot/README.md),不等于本次原版观察已经验证这些候选行为。 + +后续独立的[长历史行逐字符观察](windterm-history-layout.md)补充了主动滚动和 80→62→80 列的视图分段证据。它保留本次原始结果,不扩大为覆盖写入、物理格或完整 reflow 的验证。 diff --git a/experiments/candidates/alacritty/Cargo.lock b/experiments/candidates/alacritty/Cargo.lock index 80d1c626..9347966e 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.6" +version = "0.26.1-dev+termforge.7" dependencies = [ "arrayvec", "base64", diff --git a/experiments/candidates/alacritty/PROVENANCE.md b/experiments/candidates/alacritty/PROVENANCE.md index 659f7d13..264979d8 100644 --- a/experiments/candidates/alacritty/PROVENANCE.md +++ b/experiments/candidates/alacritty/PROVENANCE.md @@ -1,6 +1,6 @@ # 固定候选来源与本地补丁 -当前适配版本为 `alacritty_terminal 0.26.1-dev+termforge.6`,不是 crates.io 原始 `0.26.0`。 +当前适配版本为 `alacritty_terminal 0.26.1-dev+termforge.7`,不是 crates.io 原始 `0.26.0`。 ## 上游身份 @@ -66,6 +66,13 @@ 3. 候选 C++ 快照保留原生身份映射及淘汰端点,主屏缓存跨备用屏切换挂起/恢复,空原生历史不再推断为清空。容量淘汰也使旧回放快照中的缓存行失效,在行版本比较结束后才修改来源快照。相关实现不是 WindTerm 私有代码;原版完整 resize/显示语义仍需对照。 4. 升为 `0.26.1-dev+termforge.6`,同步当前报告身份;固定上游、注册表依赖与许可证未变。 +## termforge.7 补丁 + +1. `src/grid/resize.rs`:原硬断行跟随拆分后的最终片段;内部软折行使用当前宽度。拓宽吞入空 continuation 时,将其硬边界转给接收行,保留后续独立空硬行;不改变已有单元格文字或光标重排算法。 +2. 缩宽使物理行数量超过历史容量时,实际删除的最旧行计入原生淘汰数。相同尺寸、空同步和显式清空不重复计数;物理行合并不视作容量淘汰。 +3. Grid 回归覆盖硬行/软链拆分、buffered continuation、完整/部分合并和空硬边界;公开 C ABI 的真实 feed/resize 校验导出边界及容量淘汰。C++ 长行复制回归仍失败,旧整行缓存与原生重排片段的来源归属尚未修复;本版本不宣称混宽历史、复制/Find、完整 reflow 或 #29 验收通过。 +4. 升为 `0.26.1-dev+termforge.7`,固定上游、注册表依赖和许可证未变。新增补丁为独立实现,没有引入 WindTerm 私有源码或资源。 + ## 历史基线 #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 54b1a3bf..249394d6 100644 --- a/experiments/candidates/alacritty/README.md +++ b/experiments/candidates/alacritty/README.md @@ -6,7 +6,7 @@ | 项目 | 当前固定值 | 来源 | |---|---|---| -| `alacritty_terminal` | `0.26.1-dev+termforge.6`,Apache-2.0 | [上游 commit `5421470`](https://github.com/alacritty/alacritty/commit/542147067dd016fc5f49120ef0ba605a98a4e3a7)与[来源/补丁台账](PROVENANCE.md) | +| `alacritty_terminal` | `0.26.1-dev+termforge.7`,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/run.py b/experiments/candidates/alacritty/run.py index 2a6a29d6..ef004e3d 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.6" +VERSION = "0.26.1-dev+termforge.7" UPSTREAM_COMMIT = "542147067dd016fc5f49120ef0ba605a98a4e3a7" BASE_CRATE_SHA256 = "bda177466b9524d59f1b12f0dd30b68696788e9992a7e959021c4a0ed96fcf59" diff --git a/experiments/candidates/alacritty/src/ffi.rs b/experiments/candidates/alacritty/src/ffi.rs index 43cf6139..c274a34b 100644 --- a/experiments/candidates/alacritty/src/ffi.rs +++ b/experiments/candidates/alacritty/src/ffi.rs @@ -445,6 +445,63 @@ mod tests { } } + #[test] + fn width_reflow_exports_logical_line_ends() { + let value = Handle(tf_terminal_new(2, 8, 8)); + assert!(!value.0.is_null()); + value.feed(b"ABCDEFGH\r\nIJKLMNOP\r\nQRSTUVWX"); + for (columns, expected) in [ + (4, vec![(false, true, 4), (true, false, 0), + (false, true, 4), (true, false, 0), + (false, true, 4), (false, false, 0)]), + (8, vec![(true, false, 0), (true, false, 0), (false, false, 0)]), + ] { + assert_eq!(unsafe { tf_terminal_resize(value.0, 2, columns, null_mut(), 0) }, 0); + let meta = value.meta(); + let actual: Vec<_> = (0..meta.history + meta.rows).map(|index| { + let mut row = MaybeUninit::::uninit(); + assert_eq!(unsafe { tf_terminal_row_meta(value.0, index, row.as_mut_ptr()) }, 0); + let row = unsafe { row.assume_init() }; + (row.hard_break, row.wrap, row.wrap_columns) + }).collect(); + assert_eq!(actual, expected); + assert_eq!(meta.discarded, 0, "merging physical containers is not history eviction"); + } + } + + #[test] + fn width_reflow_counts_capacity_eviction_once() { + let value = Handle(tf_terminal_new(2, 8, 1)); + assert!(!value.0.is_null()); + value.feed(b"ABCDEFGH\r\nIJKLMNOP\r\nQRSTUVWX"); + assert_eq!(value.meta().history, 1); + assert_eq!(value.meta().discarded, 0); + + assert_eq!(unsafe { tf_terminal_resize(value.0, 2, 4, null_mut(), 0) }, 0); + let meta = value.meta(); + assert_eq!((meta.history, meta.rows, meta.columns), (1, 2, 4)); + let mut actual = Vec::new(); + for row in 0..3 { + for column in 0..4 { + let mut text = [0; 8]; + let mut needed = 0; + assert_eq!(unsafe { + tf_terminal_cell_text(value.0, row, column, text.as_mut_ptr(), text.len(), &mut needed) + }, 0); + actual.extend_from_slice(&text[..needed]); + } + } + assert_eq!(actual, b"MNOPQRSTUVWX"); + assert_eq!(meta.discarded, 3, "three oldest reflowed rows exceeded history capacity"); + + value.feed(b""); + assert_eq!(unsafe { tf_terminal_resize(value.0, 2, 4, null_mut(), 0) }, 0); + assert_eq!(value.meta().discarded, 3, "unchanged geometry must not recount eviction"); + value.feed(b"\x1b[3J"); + assert_eq!(value.meta().history, 0); + assert_eq!(value.meta().discarded, 3, "explicit history clear is not capacity eviction"); + } + #[test] fn history_eviction_checks_entire_prefix_before_mutating() { let value = Handle(tf_terminal_new(2, 8, 4)); diff --git a/experiments/candidates/alacritty/test_candidate.py b/experiments/candidates/alacritty/test_candidate.py index 5335ab93..8144d944 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.6") + self.assertEqual(self.summary["version"], "0.26.1-dev+termforge.7") 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 0ea94088..5e3008bc 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.6" +version = "0.26.1-dev+termforge.7" 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/resize.rs b/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/resize.rs index aa70c4ec..2a6202fb 100644 --- a/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/resize.rs +++ b/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/resize.rs @@ -182,6 +182,9 @@ impl Grid { let line_delta = self.cursor.point.line - target.line; if line_delta != 0 && row.is_clear() { + // The consumed continuation can carry a hard boundary even + // when its remaining cells are empty. + last_row.set_hard_break(row.hard_break()); continue; } @@ -197,6 +200,10 @@ impl Grid { self.cursor.point.line += 1; } + // A physical row may disappear; its logical end must survive + // on the row which received its cells. + last_row.set_hard_break(row.hard_break()); + // Don't push line into the new buffer. continue; } @@ -232,6 +239,13 @@ impl Grid { if row.len() < columns { row.grow(columns); } + if reflow { + let wrapped = row.last().is_some_and(|cell| cell.flags().contains(Flags::WRAPLINE)); + row.set_wrap_columns(if wrapped { columns } else { 0 }); + if wrapped { + row.set_hard_break(false); + } + } new_raw.push(row); } @@ -256,6 +270,10 @@ impl Grid { let mut rows = self.raw.take_all(); for (i, mut row) in rows.drain(..).enumerate().rev() { + // The logical end belongs to the final fragment, not to the + // physical container retained for the first fragment. + let hard_break = row.hard_break(); + // Append lines left over from the previous row. if let Some(buffered) = buffered.take() { // Add a column for every cell added before the cursor, if it goes beyond the new @@ -380,6 +398,7 @@ impl Grid { wrapped.resize_with(columns, T::default); } row = Row::from_vec(wrapped, occ); + row.set_hard_break(hard_break); if i < self.display_offset { // Since we added a new line, rotate up the viewport. @@ -391,7 +410,21 @@ impl Grid { // Reverse iterator and use it as the new grid storage. let mut reversed: Vec> = new_raw.drain(..).rev().collect(); + // Reflow can create more physical rows than the history capacity holds. + // These are real oldest-history evictions, not merged row identities. + let evicted = reversed.len().saturating_sub(self.max_scroll_limit + self.lines); + self.discarded = self.discarded.checked_add(evicted as u64) + .expect("history discarded exhausted"); reversed.truncate(self.max_scroll_limit + self.lines); + if reflow { + for row in &mut reversed { + let wrapped = row.last().is_some_and(|cell| cell.flags().contains(Flags::WRAPLINE)); + row.set_wrap_columns(if wrapped { columns } else { 0 }); + if wrapped { + row.set_hard_break(false); + } + } + } self.raw.replace_inner(reversed); // Clamp display offset in case some lines went off. diff --git a/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/tests.rs b/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/tests.rs index f052c75a..11840ed2 100644 --- a/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/tests.rs +++ b/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/tests.rs @@ -156,7 +156,10 @@ fn test_iter() { assert_eq!(&4, iter.cell()); // Test that iter ends at end of grid. - let mut final_iter = grid.iter_from(Point { line: Line(4), column: Column(4) }); + let mut final_iter = grid.iter_from(Point { + line: Line(4), + column: Column(4), + }); assert_eq!(None, final_iter.next()); assert_indexed(23, final_iter.prev()); } @@ -348,6 +351,146 @@ fn shrink_reflow_disabled() { assert_eq!(grid[Line(0)][Column(1)], cell('2')); } +#[test] +fn resize_boundary_shrink_hard_line() { + let mut grid = Grid::::new(1, 8, 8); + boundary_row(&mut grid, 0, "ABCDEFGH", true, false); + + grid.resize(true, 1, 3); + + assert_boundary_rows( + &grid, + &[ + ("ABC", false, true, 3), + ("DEF", false, true, 3), + ("GH ", true, false, 0), + ], + ); +} + +#[test] +fn resize_boundary_shrink_soft_chain() { + let mut grid = Grid::::new(2, 8, 8); + boundary_row(&mut grid, 0, "ABCDEFGH", false, true); + boundary_row(&mut grid, 1, "IJKL", true, false); + + grid.resize(true, 2, 4); + + assert_boundary_rows( + &grid, + &[ + ("ABCD", false, true, 4), + ("EFGH", false, true, 4), + ("IJKL", true, false, 0), + ], + ); +} + +#[test] +fn resize_boundary_shrink_buffered_continuation() { + let mut grid = Grid::::new(2, 5, 8); + boundary_row(&mut grid, 0, "ABCDE", false, true); + boundary_row(&mut grid, 1, "FG", true, false); + + grid.resize(true, 2, 3); + + assert_boundary_rows( + &grid, + &[ + ("ABC", false, true, 3), + ("DEF", false, true, 3), + ("G ", true, false, 0), + ], + ); +} + +#[test] +fn resize_boundary_grow_complete_line() { + let mut grid = Grid::::new(2, 4, 8); + boundary_row(&mut grid, 0, "ABCD", false, true); + boundary_row(&mut grid, 1, "EFGH", true, false); + + grid.resize(true, 2, 8); + + assert_boundary_rows( + &grid, + &[("ABCDEFGH", true, false, 0), (" ", false, false, 0)], + ); +} + +#[test] +fn resize_boundary_grow_partial_line() { + let mut grid = Grid::::new(3, 4, 8); + boundary_row(&mut grid, 0, "ABCD", false, true); + boundary_row(&mut grid, 1, "EFGH", false, true); + boundary_row(&mut grid, 2, "IJKL", true, false); + + grid.resize(true, 3, 6); + + assert_boundary_rows( + &grid, + &[ + ("ABCDEF", false, true, 6), + ("GHIJKL", true, false, 0), + (" ", false, false, 0), + ], + ); +} + +#[test] +fn resize_boundary_grow_empty_hard_continuation() { + let mut grid = Grid::::new(3, 4, 8); + boundary_row(&mut grid, 0, "ABCD", false, true); + boundary_row(&mut grid, 1, "", true, false); + boundary_row(&mut grid, 2, "", true, false); + + grid.resize(true, 3, 8); + + // The empty continuation ends ABCD. The next hard empty row is a separate + // logical line, so there must still be two distinct hard boundaries. + assert_boundary_rows( + &grid, + &[ + ("ABCD ", true, false, 0), + (" ", true, false, 0), + (" ", false, false, 0), + ], + ); +} + +fn boundary_row(grid: &mut Grid, line: i32, text: &str, hard: bool, soft: bool) { + let columns = grid.columns(); + let row = &mut grid[Line(line)]; + for (column, c) in text.chars().enumerate() { + row[Column(column)] = cell(c); + } + row.set_hard_break(hard); + row.set_wrap_columns(if soft { columns } else { 0 }); + if soft { + row[Column(columns - 1)].flags.insert(Flags::WRAPLINE); + } +} + +fn assert_boundary_rows(grid: &Grid, expected: &[(&str, bool, bool, usize)]) { + let actual: Vec<_> = (-(grid.history_size() as i32)..grid.screen_lines() as i32) + .map(|line| { + let row = &grid[Line(line)]; + let text: String = row.into_iter().map(|cell| cell.c).collect(); + ( + text, + row.hard_break(), + row.last().unwrap().flags.contains(Flags::WRAPLINE), + row.wrap_columns(), + ) + }) + .collect(); + let expected: Vec<_> = expected + .iter() + .map(|&(text, hard, soft, columns)| (text.to_owned(), hard, soft, columns)) + .collect(); + assert_eq!(actual, expected); +} + #[test] fn accurate_size_hint() { let grid = Grid::::new(5, 5, 2); diff --git a/experiments/core/CANDIDATE.md b/experiments/core/CANDIDATE.md index 2f8fcbcb..1b001d9e 100644 --- a/experiments/core/CANDIDATE.md +++ b/experiments/core/CANDIDATE.md @@ -2,7 +2,9 @@ 按 [ADR-0006](../../docs/adr/0006-confirmed-project-direction.md),用户确认保留本候选代码继续评估,尚未确认最终采用。下述实现、来源与回归结果继续保留;与 WindTerm 2.7.0 的重要差异、性能容差及正式选型分别提交用户确认。 -`CandidateTerminal` 是 #27 的拥有值适配层,不是自研核心的替代品声明。它把固定 `0.26.1-dev+termforge.6` 的 `Term/Processor` 接到现有 `Screen`/`Line`;默认 CMake 保留自研核心且不需要 Rust,候选产品由 `TERMFORGE_BUILD_CANDIDATE_ADAPTER=ON` 显式启用。 +`CandidateTerminal` 是 #27 的拥有值适配层,不是自研核心的替代品声明。它把固定 `0.26.1-dev+termforge.7` 的 `Term/Processor` 接到现有 `Screen`/`Line`;默认 CMake 保留自研核心且不需要 Rust,候选产品由 `TERMFORGE_BUILD_CANDIDATE_ADAPTER=ON` 显式启用。 + +当前 `.7` 修复原生 resize 的硬/软边界传递及缩宽容量淘汰计数,来源见[补丁台账](../candidates/alacritty/PROVENANCE.md#termforge7-补丁)。C++ 长行 resize/repaint 的整段复制回归仍失败:旧历史行与原生片段重复,来源归属和统一视图映射仍由 #29 继续处理。这些原生修复不构成候选完整正确性通过。 ## 构建与验证 diff --git a/experiments/core/candidate_terminal_test.cpp b/experiments/core/candidate_terminal_test.cpp index ae72b282..56e47595 100644 --- a/experiments/core/candidate_terminal_test.cpp +++ b/experiments/core/candidate_terminal_test.cpp @@ -554,6 +554,44 @@ void cached_history_survives_noop() { "clear did not invalidate cached history text actions"); } +void cached_history_resize_replay_preserves_long_text() { + const std::string expected = "ABCDEFGH\r\nB\r\nC\r\nD"; + for (const bool fragmented : {false, true}) { + termforge::CandidateTerminal terminal(2, 8, 16); + 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(expected); + terminal.resize(4, 4); + feed("\x1b[?25l\x1b[2J\x1b[HC\r\nD\x1b[?25h"); + + termforge::TextDocument document(terminal.screen); + termforge::TextSearch search; + const auto generation = search.start(document, "EFGH"); + for (int step = 0; step < 100 && search.status() == termforge::SearchStatus::running; ++step) + search.step(document, generation, 64); + const auto match = search.match(document, 0); + require(search.status() == termforge::SearchStatus::complete && search.matches().size() == 1 && match, + "resize replay lost or duplicated the long history row's Find result"); + termforge::TextSelection found; + require(found.set(document, match->range) && found.copy(document, 64).text == "EFGH", + "resize replay Find cannot select the original text beyond the new width"); + + // The repaint writes C/D into the first two visible rows. Include all + // retained history, but stop at D rather than copying unused screen rows. + const auto begin = document.position(0, 0); + const auto end = document.position(terminal.screen.history.size() + 1, 1); + termforge::TextSelection whole; + require(begin && end && whole.set(document, {*begin, *end}), + "resize replay whole-text selection failed"); + const auto copied = whole.copy(document, 64); + require(copied.status == termforge::SelectionCopyStatus::ok && copied.text == expected, + fragmented ? "bytewise resize replay duplicated or lost original history text" : + "whole-feed resize replay duplicated or lost original history text"); + } +} + void cached_history_stays_with_primary(bool fragmented) { termforge::CandidateTerminal terminal(2, 8, 4); terminal.feed("A\r\nB\r\nC\r\nD"); @@ -756,6 +794,7 @@ void conpty_resize_replay(const std::filesystem::path& path) { incomplete_replay_is_bounded(); pending_replay_keeps_resized_grid(); cached_history_survives_noop(); + cached_history_resize_replay_preserves_long_text(); cached_history_stays_with_primary(false); cached_history_stays_with_primary(true); for (const bool fragmented : {false, true}) { diff --git a/experiments/core/results/2026-10-28-history-layout/windterm-input.json b/experiments/core/results/2026-10-28-history-layout/windterm-input.json new file mode 100644 index 00000000..cc677fc9 --- /dev/null +++ b/experiments/core/results/2026-10-28-history-layout/windterm-input.json @@ -0,0 +1,58 @@ +{ + "encoding": "ASCII", + "columns_at_payload_generation": 80, + "rows_at_payload_generation": 25, + "preamble_hex": "1b5b324a1b5b481b5b3f3768", + "line_separator_hex": "0d0a", + "final_newline": false, + "payload_bytes": 1143, + "payload_sha256": "0d4997d10e0c1c051dae19fbb59f5f3316967c487359953132403a21a4ce8f13", + "reconstruction": "bytes.fromhex(preamble_hex) + bytes.fromhex(line_separator_hex).join(line.encode(encoding) for line in logical_input_lines)", + "logical_input_lines": [ + "V29-H0000|history", + "V29-H0001|history", + "V29-H0002|history", + "V29-H0003|history", + "V29-H0004|history", + "V29-H0005|history", + "V29-SOFT|0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefg|SOFT-END", + "V29-HARD-A|0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwx|A-END", + "V29-HARD-B|zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA98765432|B-END", + "V29-TAIL0000|tail", + "V29-TAIL0001|tail", + "V29-TAIL0002|tail", + "V29-TAIL0003|tail", + "V29-TAIL0004|tail", + "V29-TAIL0005|tail", + "V29-TAIL0006|tail", + "V29-TAIL0007|tail", + "V29-TAIL0008|tail", + "V29-TAIL0009|tail", + "V29-TAIL0010|tail", + "V29-TAIL0011|tail", + "V29-TAIL0012|tail", + "V29-TAIL0013|tail", + "V29-TAIL0014|tail", + "V29-TAIL0015|tail", + "V29-TAIL0016|tail", + "V29-TAIL0017|tail", + "V29-TAIL0018|tail", + "V29-TAIL0019|tail", + "V29-TAIL0020|tail", + "V29-TAIL0021|tail", + "V29-TAIL0022|tail", + "V29-TAIL0023|tail", + "V29-TAIL0024|tail", + "V29-TAIL0025|tail", + "V29-TAIL0026|tail", + "V29-TAIL0027|tail", + "V29-TAIL0028|tail", + "V29-TAIL0029|tail", + "V29-TAIL0030|tail", + "V29-TAIL0031|tail", + "V29-TAIL0032|tail", + "V29-TAIL0033|tail", + "V29-TAIL0034|tail", + "V29-END" + ] +} diff --git a/experiments/core/results/2026-10-28-history-layout/windterm-observations.json b/experiments/core/results/2026-10-28-history-layout/windterm-observations.json new file mode 100644 index 00000000..87c0c0d0 --- /dev/null +++ b/experiments/core/results/2026-10-28-history-layout/windterm-observations.json @@ -0,0 +1,912 @@ +{ + "scope": "Original runtime research input; not a confirmed product policy or acceptance result.", + "observed_version": "WindTerm 2.7.0 Windows x64", + "os": "Windows 11 Pro 10.0.26200 x64", + "transport": "Synthetic loopback Telnet; NAWS received independently by the owned peer.", + "planned_runs": 2, + "actual_runs": 2, + "instrumentation_limit_seconds_per_run": 45, + "scenario_deadline_seconds": 43, + "cli_timeout_seconds": 60, + "settle_rule": "Two identical bounded projections at least 250 ms apart; not a performance threshold.", + "target_sha256": "7b10531dccc7cd614ae559f36f1ef518e0c96335637431b4646354823b2804d0", + "distribution_files": 4047, + "distribution_manifest_sha256": "e459adf2f12adb6a1b7f55e66a9da1c22794185e66e848057d21e0c60d669203", + "payload_sha256": "0d4997d10e0c1c051dae19fbb59f5f3316967c487359953132403a21a4ce8f13", + "probe_template_sha256": "1c0a5d4399ea28169bed29d3af21ab6c864db2c81a3c44ffa1fbb01e0309265a", + "peer_script_sha256": "112217343bc7d663b0adacb7231bb4cd313be11e281b9ecb3a0365445af2b586", + "frozen_plan_sha256": "a17e98ceec0fe5eb833d0d2e9fa34c3d082ebd0da14bc917f65ce387c7d54e44", + "frozen_probe_manifest_sha256": "5f345e2a95e6d76daeb72935dab850ffcfecbc3bf0b99a900f49892409717bf7", + "same_input_and_phase_text_projections_in_both_runs": true, + "all_45_input_texts_and_document_positions_preserved": true, + "sampled_logical_lines": [ + 6, + 7, + 8 + ], + "characters_per_phase": 339, + "line_end_positions_per_phase": 3, + "all_long_character_rect_y_negative_at_initial_bottom": true, + "point_tuple": [ + "ascii_offset", + "document_position", + "x", + "y", + "width", + "height", + "visibleLineFromPosition", + "lineFromPosition", + "visibleLineStart" + ], + "segment_offset_end_is_exclusive": true, + "coordinates": "Script text-view coordinates; not presented pixels or physical cells.", + "common_phases": [ + { + "phase": "wide-bottom", + "requested_outer": [ + 1230, + 887 + ], + "naws": [ + 80, + 26 + ], + "scrollTop": 22, + "visibleLineCount": 47, + "screenLineCapacity": 26, + "character_groups": [ + [ + 80, + 80, + 25 + ], + [ + 77 + ], + [ + 77 + ] + ] + }, + { + "phase": "wide-history-3", + "requested_outer": [ + 1230, + 887 + ], + "naws": [ + 80, + 26 + ], + "scrollTop": 3, + "visibleLineCount": 47, + "screenLineCapacity": 26, + "character_groups": [ + [ + 80, + 80, + 25 + ], + [ + 77 + ], + [ + 77 + ] + ] + }, + { + "phase": "narrow-history", + "requested_outer": [ + 1010, + 887 + ], + "naws": [ + 62, + 26 + ], + "scrollTop": 3, + "visibleLineCount": 49, + "screenLineCapacity": 26, + "character_groups": [ + [ + 62, + 62, + 61 + ], + [ + 62, + 15 + ], + [ + 62, + 15 + ] + ] + }, + { + "phase": "narrow-history-10", + "requested_outer": [ + 1010, + 887 + ], + "naws": [ + 62, + 26 + ], + "scrollTop": 10, + "visibleLineCount": 49, + "screenLineCapacity": 26, + "character_groups": [ + [ + 62, + 62, + 61 + ], + [ + 62, + 15 + ], + [ + 62, + 15 + ] + ] + }, + { + "phase": "narrow-history-3", + "requested_outer": [ + 1010, + 887 + ], + "naws": [ + 62, + 26 + ], + "scrollTop": 3, + "visibleLineCount": 49, + "screenLineCapacity": 26, + "character_groups": [ + [ + 62, + 62, + 61 + ], + [ + 62, + 15 + ], + [ + 62, + 15 + ] + ] + }, + { + "phase": "restore-width-history", + "requested_outer": [ + 1230, + 887 + ], + "naws": [ + 80, + 26 + ], + "scrollTop": 3, + "visibleLineCount": 47, + "screenLineCapacity": 26, + "character_groups": [ + [ + 80, + 80, + 25 + ], + [ + 77 + ], + [ + 77 + ] + ] + }, + { + "phase": "restore-bottom", + "requested_outer": [ + 1230, + 887 + ], + "naws": [ + 80, + 26 + ], + "scrollTop": 22, + "visibleLineCount": 47, + "screenLineCapacity": 26, + "character_groups": [ + [ + 80, + 80, + 25 + ], + [ + 77 + ], + [ + 77 + ] + ] + } + ], + "coordinate_layouts": [ + { + "phase": "wide-history-3", + "scrollTop": 3, + "long_lines": [ + { + "logical_line": 6, + "document_start": 108, + "document_end": 293, + "ascii_characters": 185, + "segments": [ + { + "offset_begin": 0, + "offset_end_exclusive": 80, + "characters": 80, + "visible_line": 6, + "visible_line_start_position": 108, + "first_document_position": 108, + "last_document_position": 187, + "first_rect": [ + 147, + 54, + 8, + 18 + ], + "last_rect": [ + 779, + 54, + 8, + 18 + ] + }, + { + "offset_begin": 80, + "offset_end_exclusive": 160, + "characters": 80, + "visible_line": 7, + "visible_line_start_position": 188, + "first_document_position": 188, + "last_document_position": 267, + "first_rect": [ + 147, + 72, + 8, + 18 + ], + "last_rect": [ + 779, + 72, + 8, + 18 + ] + }, + { + "offset_begin": 160, + "offset_end_exclusive": 185, + "characters": 25, + "visible_line": 8, + "visible_line_start_position": 268, + "first_document_position": 268, + "last_document_position": 292, + "first_rect": [ + 147, + 90, + 8, + 18 + ], + "last_rect": [ + 339, + 90, + 8, + 18 + ] + } + ] + }, + { + "logical_line": 7, + "document_start": 294, + "document_end": 371, + "ascii_characters": 77, + "segments": [ + { + "offset_begin": 0, + "offset_end_exclusive": 77, + "characters": 77, + "visible_line": 9, + "visible_line_start_position": 294, + "first_document_position": 294, + "last_document_position": 370, + "first_rect": [ + 147, + 108, + 8, + 18 + ], + "last_rect": [ + 755, + 108, + 8, + 18 + ] + } + ] + }, + { + "logical_line": 8, + "document_start": 372, + "document_end": 449, + "ascii_characters": 77, + "segments": [ + { + "offset_begin": 0, + "offset_end_exclusive": 77, + "characters": 77, + "visible_line": 10, + "visible_line_start_position": 372, + "first_document_position": 372, + "last_document_position": 448, + "first_rect": [ + 147, + 126, + 8, + 18 + ], + "last_rect": [ + 755, + 126, + 8, + 18 + ] + } + ] + } + ], + "soft_line_boundary_samples": [ + [ + 61, + 169, + 635, + 54, + 8, + 18, + 6, + 6, + 108 + ], + [ + 62, + 170, + 643, + 54, + 8, + 18, + 6, + 6, + 108 + ], + [ + 79, + 187, + 779, + 54, + 8, + 18, + 6, + 6, + 108 + ], + [ + 80, + 188, + 147, + 72, + 8, + 18, + 7, + 6, + 188 + ], + [ + 123, + 231, + 491, + 72, + 8, + 18, + 7, + 6, + 188 + ], + [ + 124, + 232, + 499, + 72, + 8, + 18, + 7, + 6, + 188 + ], + [ + 159, + 267, + 779, + 72, + 8, + 18, + 7, + 6, + 188 + ], + [ + 160, + 268, + 147, + 90, + 8, + 18, + 8, + 6, + 268 + ], + [ + 184, + 292, + 339, + 90, + 8, + 18, + 8, + 6, + 268 + ] + ] + }, + { + "phase": "narrow-history", + "scrollTop": 3, + "long_lines": [ + { + "logical_line": 6, + "document_start": 108, + "document_end": 293, + "ascii_characters": 185, + "segments": [ + { + "offset_begin": 0, + "offset_end_exclusive": 62, + "characters": 62, + "visible_line": 6, + "visible_line_start_position": 108, + "first_document_position": 108, + "last_document_position": 169, + "first_rect": [ + 147, + 54, + 8, + 18 + ], + "last_rect": [ + 635, + 54, + 8, + 18 + ] + }, + { + "offset_begin": 62, + "offset_end_exclusive": 124, + "characters": 62, + "visible_line": 7, + "visible_line_start_position": 170, + "first_document_position": 170, + "last_document_position": 231, + "first_rect": [ + 147, + 72, + 8, + 18 + ], + "last_rect": [ + 635, + 72, + 8, + 18 + ] + }, + { + "offset_begin": 124, + "offset_end_exclusive": 185, + "characters": 61, + "visible_line": 8, + "visible_line_start_position": 232, + "first_document_position": 232, + "last_document_position": 292, + "first_rect": [ + 147, + 90, + 8, + 18 + ], + "last_rect": [ + 627, + 90, + 8, + 18 + ] + } + ] + }, + { + "logical_line": 7, + "document_start": 294, + "document_end": 371, + "ascii_characters": 77, + "segments": [ + { + "offset_begin": 0, + "offset_end_exclusive": 62, + "characters": 62, + "visible_line": 9, + "visible_line_start_position": 294, + "first_document_position": 294, + "last_document_position": 355, + "first_rect": [ + 147, + 108, + 8, + 18 + ], + "last_rect": [ + 635, + 108, + 8, + 18 + ] + }, + { + "offset_begin": 62, + "offset_end_exclusive": 77, + "characters": 15, + "visible_line": 10, + "visible_line_start_position": 356, + "first_document_position": 356, + "last_document_position": 370, + "first_rect": [ + 147, + 126, + 8, + 18 + ], + "last_rect": [ + 259, + 126, + 8, + 18 + ] + } + ] + }, + { + "logical_line": 8, + "document_start": 372, + "document_end": 449, + "ascii_characters": 77, + "segments": [ + { + "offset_begin": 0, + "offset_end_exclusive": 62, + "characters": 62, + "visible_line": 11, + "visible_line_start_position": 372, + "first_document_position": 372, + "last_document_position": 433, + "first_rect": [ + 147, + 144, + 8, + 18 + ], + "last_rect": [ + 635, + 144, + 8, + 18 + ] + }, + { + "offset_begin": 62, + "offset_end_exclusive": 77, + "characters": 15, + "visible_line": 12, + "visible_line_start_position": 434, + "first_document_position": 434, + "last_document_position": 448, + "first_rect": [ + 147, + 162, + 8, + 18 + ], + "last_rect": [ + 259, + 162, + 8, + 18 + ] + } + ] + } + ], + "soft_line_boundary_samples": [ + [ + 61, + 169, + 635, + 54, + 8, + 18, + 6, + 6, + 108 + ], + [ + 62, + 170, + 147, + 72, + 8, + 18, + 7, + 6, + 170 + ], + [ + 79, + 187, + 283, + 72, + 8, + 18, + 7, + 6, + 170 + ], + [ + 80, + 188, + 291, + 72, + 8, + 18, + 7, + 6, + 170 + ], + [ + 123, + 231, + 635, + 72, + 8, + 18, + 7, + 6, + 170 + ], + [ + 124, + 232, + 147, + 90, + 8, + 18, + 8, + 6, + 232 + ], + [ + 159, + 267, + 427, + 90, + 8, + 18, + 8, + 6, + 232 + ], + [ + 160, + 268, + 435, + 90, + 8, + 18, + 8, + 6, + 232 + ], + [ + 184, + 292, + 627, + 90, + 8, + 18, + 8, + 6, + 232 + ] + ] + } + ], + "scroll_changes": [ + { + "from": "narrow-history", + "to": "narrow-history-10", + "sampled_positions": 342, + "scrollTop_delta": 7, + "uniform_y_delta": -126, + "all_other_point_fields_unchanged": true + }, + { + "from": "narrow-history-10", + "to": "narrow-history-3", + "sampled_positions": 342, + "scrollTop_delta": -7, + "uniform_y_delta": 126, + "all_other_point_fields_unchanged": true + } + ], + "restore_width_projection_exact": true, + "restore_bottom_projection_exact": true, + "runs": [ + { + "run": "run-1", + "evidence_id": "ev_690a21bd685478bbd6595af7a8c14ec0be7ead2cbcd20f07b7c88abb5221c8f4", + "probe_sha256": "724e41efe047ae0b7160e4e0e580cd594d26cea14b03741cc9ccaa7f507b0ccd", + "completed_phases": 7, + "naws": [ + [ + 80, + 25 + ], + [ + 80, + 26 + ], + [ + 62, + 26 + ], + [ + 80, + 26 + ] + ], + "phase_text_projection_sha256": { + "wide-bottom": "7d0b13e759a95859f6d306ca9d53e163f915dc93dcd3e7b181a26cef12704ce0", + "wide-history-3": "84159e3511fccc00b8f9fc2c8633b99bd3a1f76b4a0ceb087aa96b6f33c6072d", + "narrow-history": "541a163bd2aa33e2695a567dd0dbf76cf51a5bd989e2a31d7db4f38428883476", + "narrow-history-10": "5ddcd47173bcab9a626894f9ab7301529741f5d01cbb48f420cb3aa85ab605b4", + "narrow-history-3": "541a163bd2aa33e2695a567dd0dbf76cf51a5bd989e2a31d7db4f38428883476", + "restore-width-history": "84159e3511fccc00b8f9fc2c8633b99bd3a1f76b4a0ceb087aa96b6f33c6072d", + "restore-bottom": "7d0b13e759a95859f6d306ca9d53e163f915dc93dcd3e7b181a26cef12704ce0" + }, + "verified": { + "all_4047_copied_file_hashes": true, + "payload_reconstructed_exactly": true, + "all_input_texts_and_document_positions_preserved": true, + "no_observation_errors": true, + "only_synthetic_session": true, + "peer_closed": true + }, + "cleanup": { + "job_active": 0, + "job_total": 3, + "desktop_handle_closed": true, + "station_handle_closed": true, + "original_station_restored": true + }, + "private_artifact_sha256": { + "evidence.json": "ea347ad57bc9168f50f4d0bda78d10103ba790f401536064acac263ee9209793", + "run.json": "490a65fa8452b6cf9d08811f43cbf3c2058268894f26598800210541c5345958", + "files.json": "a25c87cba935294d5bfd3bbc76c650d31a8473809564d798ad5a889e4aea00e8", + "payload.bin": "0d4997d10e0c1c051dae19fbb59f5f3316967c487359953132403a21a4ce8f13", + "payload.json": "d9da84f6bfde3771454d883dd6f9d730e4f2f62c75bd5beef2d9433e6d711d52", + "peer.json": "09ddc2fb9ef88c40be9cffe1c636dd201f7ae8b0b4ac422f2a0fb85d8f3ebf1c", + "probe.js": "724e41efe047ae0b7160e4e0e580cd594d26cea14b03741cc9ccaa7f507b0ccd", + "cleanup.json": "8400f93b6a4a4a50cde35b5d474660590ed215732f55772a9d8afac138da4f1d", + "surface-cleanup.json": "2947878016d582f50a3241f5dfaf0c923671e6ddee62cd57e00a78a988ae7954", + "profile-check.json": "5e522a64e98197722ccd084639b1ce50258b238e03be2f016e81c8af023b5ee1" + } + }, + { + "run": "run-2", + "evidence_id": "ev_cd5b7836ab6a72077113df2db3c583cb5190d65907c9e85579e9d91230ea2f6b", + "probe_sha256": "7a81455140286168274da8e5057b83bd38cc709d9e50c19ed4606d8245daf4ae", + "completed_phases": 7, + "naws": [ + [ + 80, + 25 + ], + [ + 80, + 26 + ], + [ + 62, + 26 + ], + [ + 80, + 26 + ] + ], + "phase_text_projection_sha256": { + "wide-bottom": "7d0b13e759a95859f6d306ca9d53e163f915dc93dcd3e7b181a26cef12704ce0", + "wide-history-3": "84159e3511fccc00b8f9fc2c8633b99bd3a1f76b4a0ceb087aa96b6f33c6072d", + "narrow-history": "541a163bd2aa33e2695a567dd0dbf76cf51a5bd989e2a31d7db4f38428883476", + "narrow-history-10": "5ddcd47173bcab9a626894f9ab7301529741f5d01cbb48f420cb3aa85ab605b4", + "narrow-history-3": "541a163bd2aa33e2695a567dd0dbf76cf51a5bd989e2a31d7db4f38428883476", + "restore-width-history": "84159e3511fccc00b8f9fc2c8633b99bd3a1f76b4a0ceb087aa96b6f33c6072d", + "restore-bottom": "7d0b13e759a95859f6d306ca9d53e163f915dc93dcd3e7b181a26cef12704ce0" + }, + "verified": { + "all_4047_copied_file_hashes": true, + "payload_reconstructed_exactly": true, + "all_input_texts_and_document_positions_preserved": true, + "no_observation_errors": true, + "only_synthetic_session": true, + "peer_closed": true + }, + "cleanup": { + "job_active": 0, + "job_total": 3, + "desktop_handle_closed": true, + "station_handle_closed": true, + "original_station_restored": true + }, + "private_artifact_sha256": { + "evidence.json": "1399cb1eb5a20110f2c71f4bbe1905c8087a5e43e82a9fe39daaa8312d6826e0", + "run.json": "5cd1fd459d5f442cf2b52b024e620ca91808535c9849a6737470b5ece5ba8375", + "files.json": "a25c87cba935294d5bfd3bbc76c650d31a8473809564d798ad5a889e4aea00e8", + "payload.bin": "0d4997d10e0c1c051dae19fbb59f5f3316967c487359953132403a21a4ce8f13", + "payload.json": "d9da84f6bfde3771454d883dd6f9d730e4f2f62c75bd5beef2d9433e6d711d52", + "peer.json": "09ddc2fb9ef88c40be9cffe1c636dd201f7ae8b0b4ac422f2a0fb85d8f3ebf1c", + "probe.js": "7a81455140286168274da8e5057b83bd38cc709d9e50c19ed4606d8245daf4ae", + "cleanup.json": "8400f93b6a4a4a50cde35b5d474660590ed215732f55772a9d8afac138da4f1d", + "surface-cleanup.json": "2947878016d582f50a3241f5dfaf0c923671e6ddee62cd57e00a78a988ae7954", + "profile-check.json": "5e522a64e98197722ccd084639b1ce50258b238e03be2f016e81c8af023b5ee1" + } + } + ], + "unknown": [ + "physical terminal cells and cursor semantics", + "actual presented frames", + "ConPTY child acknowledgement", + "performance", + "selection preservation or copy", + "Find", + "Unicode graphemes and widths", + "XY point-to-position hit testing", + "word boundaries and whitespace", + "overwriting or redraw after resize", + "complete VT reflow and all configurations" + ], + "provenance": "Whitelist export of synthetic input, factual coordinate summaries and hashes. No app/profile, private identifiers, paths, or extraction scripts." +} From a8fd664a27e208cecff7d4c24a3c3915cc4ff86b Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Wed, 28 Oct 2026 01:31:40 +0800 Subject: [PATCH 02/14] docs: preserve reflow metadata checks and unresolved history failures (#29) --- docs/EVIDENCE.md | 1 + docs/PROVENANCE.md | 2 +- experiments/core/CANDIDATE.md | 2 +- .../2026-10-28-reflow-metadata/README.md | 35 ++++++ .../validation.json | 102 ++++++++++++++++++ 5 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 experiments/core/results/2026-10-28-reflow-metadata/README.md create mode 100644 experiments/core/results/2026-10-28-reflow-metadata/validation.json diff --git a/docs/EVIDENCE.md b/docs/EVIDENCE.md index 4d523770..303b27b6 100644 --- a/docs/EVIDENCE.md +++ b/docs/EVIDENCE.md @@ -39,6 +39,7 @@ R1 固定总出口的源码为 `ce9b24d`,早期 `2dc8d6d` / `9b16acc` 仍分 | 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、实际呈现或性能。 | | WindTerm 长历史行的视图折行(#29) | [逐字符观察与边界](research/windterm-history-layout.md)、[两次事实/哈希](../experiments/core/results/2026-10-28-history-layout/windterm-observations.json) | 独立的两次七阶段观察:185 字符软行由 80/80/25 变为 62/62/61,两条 77 字符硬行各变为 62/15;文档文本与位置不变,恢复宽度后投影恢复。仅 ASCII 文本坐标接口;覆盖写入、选择/Find、物理格及呈现仍未知。 | +| 候选原生重排边界与未解决的源文本重复(#29) | [工作检查点与反例](../experiments/core/results/2026-10-28-reflow-metadata/README.md)、[来源与验证](../experiments/core/results/2026-10-28-reflow-metadata/validation.json) | `.7` 修复原生硬/软边界传递及缩宽容量计账;Rust 与 runner 通过,C++ 长行复制仍红。混宽历史、来源归属、选择/Find 及完整产品矩阵未完成。 | | 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。 | | 进程生命周期和平台边界 | [ConPTY 探针](../experiments/conpty/README.md)、[跨环境记录](research/conpty-platform-check-2026-10-22.md)、[旧平台探针](../.github/workflows/conpty-compat.yml) | 受控后代、取消/超时和短批次回收;Windows 11/Server 2025 有固定记录,Server 2022 稳态句柄门槛仍失败。长期无泄漏未知。 | diff --git a/docs/PROVENANCE.md b/docs/PROVENANCE.md index c2d4d351..c265f01d 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.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)。 +- 外部核心候选当前使用 `alacritty_terminal 0.26.1-dev+termforge.7`(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/experiments/core/CANDIDATE.md b/experiments/core/CANDIDATE.md index 1b001d9e..b801e233 100644 --- a/experiments/core/CANDIDATE.md +++ b/experiments/core/CANDIDATE.md @@ -4,7 +4,7 @@ `CandidateTerminal` 是 #27 的拥有值适配层,不是自研核心的替代品声明。它把固定 `0.26.1-dev+termforge.7` 的 `Term/Processor` 接到现有 `Screen`/`Line`;默认 CMake 保留自研核心且不需要 Rust,候选产品由 `TERMFORGE_BUILD_CANDIDATE_ADAPTER=ON` 显式启用。 -当前 `.7` 修复原生 resize 的硬/软边界传递及缩宽容量淘汰计数,来源见[补丁台账](../candidates/alacritty/PROVENANCE.md#termforge7-补丁)。C++ 长行 resize/repaint 的整段复制回归仍失败:旧历史行与原生片段重复,来源归属和统一视图映射仍由 #29 继续处理。这些原生修复不构成候选完整正确性通过。 +当前 `.7` 修复原生 resize 的硬/软边界传递及缩宽容量淘汰计数,来源见[补丁台账](../candidates/alacritty/PROVENANCE.md#termforge7-补丁),结果见[工作检查点](results/2026-10-28-reflow-metadata/README.md)。C++ 长行 resize/repaint 的整段复制回归仍失败:旧历史行与原生片段重复,来源归属和统一视图映射仍由 #29 继续处理。这些原生修复不构成候选完整正确性通过。 ## 构建与验证 diff --git a/experiments/core/results/2026-10-28-reflow-metadata/README.md b/experiments/core/results/2026-10-28-reflow-metadata/README.md new file mode 100644 index 00000000..3d4aca66 --- /dev/null +++ b/experiments/core/results/2026-10-28-reflow-metadata/README.md @@ -0,0 +1,35 @@ +# #29:原生重排边界修复,源历史重复仍未解决 + +本地工作分支代码 `67ac9c12447884584098657f06742dcd3e0f3d59`,tree `476e25606ce93359c4d855726de93777e752139d`,基点为 PR #110 的 `b59badb135b6ab53dcc55cd5b1a4c3493273ae6d`。这是未完成工作的检查点,不是 #29 的完成交付;尚未发布或合入 main。候选版本为 `0.26.1-dev+termforge.7`,固定上游、注册表依赖与许可未变。 + +原生 resize 只搬移单元格时,硬断行仍留在旧前缀、后缀丢失硬边界,软行宽度也可能沿用旧宽。现在硬边界跟随最终片段,合并时从被吞入的 continuation 转移到结果行;内部软边界记录当前宽度。六个 Grid 用例先全部失败,修复后通过。未改变既有单元格文字和光标重排算法。 + +另一公开 C ABI 反例从 `2×8/history=1` 输入 `ABCDEFGH\r\nIJKLMNOP\r\nQRSTUVWX`,缩到 `2×4` 后只剩 `MNOP/QRST/UVWX`,但原生淘汰计数错误地保持 0。修复后计为 3;空 feed、相同尺寸和 ED3 不重复计数,合并物理容器不计为淘汰。该计数单位是候选的物理历史行,不是原版逻辑行容量政策。 + +## 验证范围 + +Windows 11 x64、Rust 1.92.0、MSVC Release 候选构建;[机器可读身份与结果](validation.json)保存源码、二进制、日志哈希和命令。 + +| 检查 | 结果与限制 | +|---|---| +| Rust workspace | 163 个 vendor 单元测试、9 个适配/程序测试、1 个 doc test 全部通过,包含真实 feed/resize 的 C ABI 边界与淘汰检查 | +| 重建 trace 后的候选 runner | 6/6;固定输入、字素与来源报告检查,不是性能或原版等价证明 | +| CMake `candidate_core` | **0/1,仍失败**:新增长历史整段复制测试发现重复文字;没有修改断言或跳过。先运行 whole-feed 分支即失败,bytewise 分支尚未执行 | +| 12 个公开接口案例 | 36 条 before/resize/repaint 记录;源历史重复、软链错误和空硬行丢失仍存在 | +| 代码审查 | Standards 的来源登记问题已以 `.7` 修正;Spec 未发现本原生修复切片的新增缺陷。两个 resize 完成点的短元数据规范化重复保留为可选建议 | + +完整 CMake 产品矩阵尚未执行;已知核心回归为红,不能据 Rust 通过宣布候选正确性通过。没有实际 renderer、系统剪贴板、ConPTY child ack、实验官方后端或性能验证;此前版本的通过与历史失败不受此结果覆盖。 + +## 仍然失败的用户行为 + +`CandidateTerminal(2,8,16)`,输入以下文本,执行 `resize(4,4)`,随后输入 `ESC[?25l ESC[2J ESC[H C CRLF D ESC[?25h`(控制序列之间无表述用空格)。三个案例在 resize 后、repaint 前均保留完整文本;repaint 后: + +| 初始输入 | 实际完整复制文本 | +|---|---| +| `ABCDEFGH\r\nB\r\nC\r\nD` | `ABCDEFGH\r\nABCDB\r\nC\r\nD\r\n\r\n` | +| `ABCDEFGHIJKL\r\nC\r\nD` | `ABCDEFGHABCDIJKL\r\nC\r\nD\r\n\r\n`;`GHIJ` 从一个 Find 命中变为零 | +| `\r\nB\r\nC\r\nD` | `B\r\nC\r\nD\r\n\r\n`,首个空硬行消失 | + +末尾空行是剩余可见空行;整段复制回归明确止于重绘 D 的末尾,仍因前面的重复内容失败。混宽历史视口也仍失败,不能用裁剪文本解决。下一步需要明确旧历史与原生重排片段的来源和顺序,再统一显示、选择与 Find 的映射。 + +另行发布的[原版长历史逐字符观察](../../../../docs/research/windterm-history-layout.md)只支持固定 ASCII 长逻辑行随视图宽度重新分段;历史重新进入视图后再覆盖写入的行为仍需调查。它不确认候选缓存策略,也不替代完整 #29 或 R2 验收。 diff --git a/experiments/core/results/2026-10-28-reflow-metadata/validation.json b/experiments/core/results/2026-10-28-reflow-metadata/validation.json new file mode 100644 index 00000000..3cb2c2b0 --- /dev/null +++ b/experiments/core/results/2026-10-28-reflow-metadata/validation.json @@ -0,0 +1,102 @@ +{ + "status": "in_progress_known_failing_core_regression", + "source_commit": "67ac9c12447884584098657f06742dcd3e0f3d59", + "source_tree": "476e25606ce93359c4d855726de93777e752139d", + "base_commit": "b59badb135b6ab53dcc55cd5b1a4c3493273ae6d", + "candidate_version": "0.26.1-dev+termforge.7", + "rust_toolchain": "1.92.0", + "configuration": "Windows x64 Release candidate core; no product/backend workflow exercised", + "source_sha256": { + "experiments/candidates/alacritty/src/ffi.rs": "3a906b85b4ee5afd71a0c689d037a0961595693db1890e5d80ad91bb4c34715a", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/resize.rs": "f24af4b2ed88f1e56d7909956978af1879c929f91b0f9d36d677a5efa92b6222", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/tests.rs": "2d3e07a5baebe021f597e2e3ee4489c7d8f4e96373d5e9d188af2aea57b4fdf3", + "experiments/core/candidate_terminal_test.cpp": "17064470dc959f764571f0b94d24f898121d6bdaf35a405fda3fa5b9d2ae557a", + "experiments/core/candidate_terminal.cpp": "e0d856ce47510df93baf950a8ba36d1ab8e3db313f501cfa7207e707099e3f11", + "experiments/candidates/alacritty/Cargo.lock": "35c15e1c3212b9dca758f89d5deaabcbcf750234083e6429e4c45f96eb11390d" + }, + "binary_sha256": { + "build/candidate-adapter/bin/Release/candidate_core_test.exe": "d7317c8063196aca89df577eb8db72f5a30c6c01a27c403ac74ab9e39c727fc4", + "experiments/candidates/alacritty/target/release/termforge_alacritty_core.lib": "cb27c96b5d0dec2a111d7fa39becb228fa4bebc155513fae19e472a074f00f4b", + "experiments/candidates/alacritty/target/release/termforge-alacritty-trace.exe": "d6f4ecb1fa40f113ddc7a1d18baad7e320246fc887d3060935f6ebed38444ebd" + }, + "private_artifacts_sha256": { + "metadata-red.txt": "9c5e2409e496328b457e9e01ba68caf6d5655c50affbe0d6f74b14f3a91545da", + "metadata-green.txt": "16ae9cbd2ffe3dab9bb701d838f906f4aff0cf13bc869ecf7ed89f9b8bd122c9", + "eviction-red.log": "5dcb95a077f288c267351f9b4047fbe885535056566ef31e7905ace88337f868", + "eviction-green.log": "19539a931d57fa957b383d4da0266528810fa0bd9e7d39e1ae8d34d1c60bc16d", + "ctest.log": "e6acc85db52bc307c1d4093887a54e11d7716fdc836f334789733245b081d230", + "rust-workspace-final.log": "2c6c1bec7007444b87a6975a80a823903fad15bf8487e0f278bcb52f68ec58b2", + "final-runner.log": "2e167ccae53d1fa4d63efd457d1dbfbe7acd512949d9c5a19385e8ff12f2fa36", + "final-core-build.log": "59cc7f6c601d0392a450b9e42a588d7948c5e4091f2946baf11454e09f871840", + "final-core.log": "b284440086395a8caa596ae63483917ca952f590a8e949a663bf66cc73ed2016", + "results.jsonl": "9caeaa0cfde848c51f751bf613328f29ed469d75d51277f3102397564170da1e", + "identity-before.json": "bd49905e949906095a23e8f8a170b592f66d491f20ed6f343ccc5c5a418a9e8e", + "identity-after.json": "180692eb0ff27b716a563469923c7f6a87f310380db94df4ace83505553f6b76" + }, + "native_metadata_red": { + "tests": 6, + "passed": 0 + }, + "native_metadata_green": { + "tests": 6, + "passed": 6 + }, + "capacity_eviction": { + "expected": 3, + "before": 0, + "after": 3, + "retained_ascii": "MNOPQRSTUVWX", + "explicit_clear_not_eviction": true + }, + "final_rust": { + "command": "cargo test --workspace --locked", + "exit": 0, + "vendor_unit": 163, + "ffi": 5, + "measure": 2, + "trace": 2, + "doc": 1 + }, + "final_runner": { + "command": "python -X utf8 -m unittest experiments.candidates.alacritty.test_candidate -v", + "exit": 0, + "passed": 6 + }, + "final_candidate_core": { + "command": "ctest --test-dir build/candidate-adapter -C Release -R '^candidate_core$' --output-on-failure", + "exit": 8, + "passed": 0, + "failed": 1, + "failure": "whole-feed resize replay duplicated or lost original history text", + "bytewise_branch_executed": false + }, + "public_api_probe": { + "cases": 12, + "phases_per_case": 3, + "records": 36, + "input_kind": "synthetic", + "same_source_tree_and_hashes_before_after": true, + "single_full_copy": "ABCDEFGH\r\nABCDB\r\nC\r\nD\r\n\r\n", + "soft_ascii_copy": "ABCDEFGHABCDIJKL\r\nC\r\nD\r\n\r\n", + "soft_ascii_ghij_hits": 0, + "blank_history_copy": "B\r\nC\r\nD\r\n\r\n" + }, + "review": { + "standards_required_remaining": 0, + "spec_new_findings": 0, + "optional_duplicate_normalization_retained": true + }, + "not_verified": [ + "source ownership", + "mixed-width viewport", + "complete resize/reflow", + "copy/Find parity", + "original post-resize overwrite behavior", + "full CMake suite", + "actual rendering", + "ConPTY child acknowledgement", + "experimental backend", + "performance", + "R2 acceptance" + ] +} From 66f1942324060e56974d85ebad3bff04749e7395 Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Wed, 28 Oct 2026 02:07:41 +0800 Subject: [PATCH 03/14] wip: track resize cell origins and correct scroll eviction accounting (#29) --- CMakeLists.txt | 18 +- docs/EVIDENCE.md | 2 +- docs/FEATURES.md | 2 +- docs/PROVENANCE.md | 2 +- experiments/candidates/alacritty/Cargo.lock | 2 +- .../candidates/alacritty/PROVENANCE.md | 9 +- experiments/candidates/alacritty/README.md | 2 +- .../alacritty/include/termforge_candidate.h | 42 +++ experiments/candidates/alacritty/run.py | 2 +- experiments/candidates/alacritty/src/ffi.rs | 318 +++++++++++++++++- .../candidates/alacritty/test_candidate.py | 2 +- .../vendor/alacritty_terminal/Cargo.toml | 2 +- .../vendor/alacritty_terminal/src/grid/mod.rs | 134 +++++++- .../alacritty_terminal/src/grid/origins.rs | 157 +++++++++ .../alacritty_terminal/src/grid/resize.rs | 159 ++++++++- .../vendor/alacritty_terminal/src/grid/row.rs | 27 +- .../alacritty_terminal/src/grid/storage.rs | 6 + .../vendor/alacritty_terminal/src/term/mod.rs | 25 +- experiments/core/CANDIDATE.md | 14 +- .../core/candidate_resize_map_test.cpp | 173 ++++++++++ 20 files changed, 1060 insertions(+), 38 deletions(-) create mode 100644 experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/origins.rs create mode 100644 experiments/core/candidate_resize_map_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index e5b3e38a..02cd623d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -62,13 +62,16 @@ if(TERMFORGE_BUILD_CANDIDATE_ADAPTER) endfunction() add_executable(candidate_core_test experiments/core/candidate_terminal_test.cpp experiments/core/candidate_terminal.cpp) - target_link_libraries(candidate_core_test PRIVATE termforge_core termforge_candidate_core) - termforge_warnings(candidate_core_test) - if(WIN32) - target_link_libraries(candidate_core_test PRIVATE ws2_32 userenv ntdll) - else() - target_link_libraries(candidate_core_test PRIVATE pthread dl m) - endif() + add_executable(candidate_resize_map_test experiments/core/candidate_resize_map_test.cpp) + foreach(candidate_test IN ITEMS candidate_core_test candidate_resize_map_test) + target_link_libraries(${candidate_test} PRIVATE termforge_core termforge_candidate_core) + termforge_warnings(${candidate_test}) + if(WIN32) + target_link_libraries(${candidate_test} PRIVATE ws2_32 userenv ntdll) + else() + target_link_libraries(${candidate_test} PRIVATE pthread dl m) + endif() + endforeach() endif() add_executable(resize_test experiments/core/resize_test.cpp) @@ -268,6 +271,7 @@ if(BUILD_TESTING) add_test(NAME scenario_tick_gate COMMAND scenario_tick_gate_test) if(TERMFORGE_BUILD_CANDIDATE_ADAPTER) add_test(NAME candidate_core COMMAND candidate_core_test) + add_test(NAME candidate_resize_map COMMAND candidate_resize_map_test) endif() add_test(NAME resize_core COMMAND resize_test) add_test(NAME history_core COMMAND history_test) diff --git a/docs/EVIDENCE.md b/docs/EVIDENCE.md index 303b27b6..3cf52da1 100644 --- a/docs/EVIDENCE.md +++ b/docs/EVIDENCE.md @@ -9,7 +9,7 @@ | 配置 | 证据定位 | |---|---| | main 默认:自研核心 + 系统 ConPTY | R1 限定总验收和有限 VT/Unicode 子集;默认构建不需要 Rust | -| 当前源码显式候选:`termforge.7` + 系统 ConPTY | [固定来源/补丁台账](../experiments/candidates/alacritty/PROVENANCE.md)、[适配边界](../experiments/core/CANDIDATE.md)、[#39 字素合同](research/streaming-graphemes.md);历史限定结果保留,长行复制回归仍红 | +| 当前源码显式候选:`termforge.8` + 系统 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、绘制提交和真实呈现是不同检查点;未知端点保持未知。 diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 58a374c9..1b819d25 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -13,7 +13,7 @@ | 配置 | 当前定位 | 可以引用的证据与限制 | |---|---|---| | 默认 main:自研核心 + 系统 ConPTY | 默认构建与已发布 Alpha 的基础路线,不需要 Rust | 已有 R1 限定验收;VT/Unicode 能力仍为有限子集,不能引用候选核心的字素或 TUI 结果 | -| 当前源码显式候选:`termforge.7` + 系统 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.8` + 系统 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/拼音验收属于固定自研核心配置,不外推到当前候选 或实验后端联合配置。 diff --git a/docs/PROVENANCE.md b/docs/PROVENANCE.md index c265f01d..56a65ce3 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.7`(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.8`(Apache-2.0),固定上游 commit `542147067dd016fc5f49120ef0ba605a98a4e3a7` 的 manifest/changelog/许可证与 `src/`,并保留 ED、稳定行身份/淘汰、安全 swap、每缓冲区滚动区域、流式字素、显式历史清空代次、原生历史前缀淘汰、重排断行/容量计账及一次 resize 来源映射补丁;精确传递依赖及校验值由 `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/experiments/candidates/alacritty/Cargo.lock b/experiments/candidates/alacritty/Cargo.lock index 9347966e..3ac4d722 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.7" +version = "0.26.1-dev+termforge.8" dependencies = [ "arrayvec", "base64", diff --git a/experiments/candidates/alacritty/PROVENANCE.md b/experiments/candidates/alacritty/PROVENANCE.md index 264979d8..63b0d696 100644 --- a/experiments/candidates/alacritty/PROVENANCE.md +++ b/experiments/candidates/alacritty/PROVENANCE.md @@ -1,6 +1,6 @@ # 固定候选来源与本地补丁 -当前适配版本为 `alacritty_terminal 0.26.1-dev+termforge.7`,不是 crates.io 原始 `0.26.0`。 +当前适配版本为 `alacritty_terminal 0.26.1-dev+termforge.8`,不是 crates.io 原始 `0.26.0`。 ## 上游身份 @@ -73,6 +73,13 @@ 3. Grid 回归覆盖硬行/软链拆分、buffered continuation、完整/部分合并和空硬边界;公开 C ABI 的真实 feed/resize 校验导出边界及容量淘汰。C++ 长行复制回归仍失败,旧整行缓存与原生重排片段的来源归属尚未修复;本版本不宣称混宽历史、复制/Find、完整 reflow 或 #29 验收通过。 4. 升为 `0.26.1-dev+termforge.7`,固定上游、注册表依赖和许可证未变。新增补丁为独立实现,没有引入 WindTerm 私有源码或资源。 +## termforge.8 补丁 + +1. `src/grid/origins.rs` 与 Row 的可选临时表记录一次 resize 的物理来源区间及独立硬边界;拆分、合并、宽字 spacer、容量删除、高度裁剪和光标重定位同步转移来源,不按文本去重。每行只新增可选表指针,未给每个 Cell 永久增加来源字段。 +2. `Term::resize_with_map` 捕获主/备用屏,返回后清理全部 storage 槽的追踪状态;普通 `Term::resize` 不捕获。C ABI 暴露拥有值、epoch 和明确的失效规则,不将后续 feed 误认为仍保留旧来源。 +3. 修复高度缩小一次滚动超过旧历史数量时的来源漏记:把同步移出容量的原可见行也记录为移除,保留其硬边界。普通顶部滚动按实际超容量行数计账,修复多行滚动少计;扩高为搬移合成空行而执行的滚动不计淘汰。公开 FFI 反例和 C++ 静态库调用覆盖来源、计数、生命周期及双屏命名。 +4. 升为 `0.26.1-dev+termforge.8`;固定上游、注册表依赖和许可证未变。映射补丁独立实现,没有引入 WindTerm 私有代码或资源。C++ 历史缓存的源文本重复与空硬行丢失仍未修复,性能成本未测,完整 #29/R2 保持未完成。 + ## 历史基线 #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 249394d6..74286176 100644 --- a/experiments/candidates/alacritty/README.md +++ b/experiments/candidates/alacritty/README.md @@ -6,7 +6,7 @@ | 项目 | 当前固定值 | 来源 | |---|---|---| -| `alacritty_terminal` | `0.26.1-dev+termforge.7`,Apache-2.0 | [上游 commit `5421470`](https://github.com/alacritty/alacritty/commit/542147067dd016fc5f49120ef0ba605a98a4e3a7)与[来源/补丁台账](PROVENANCE.md) | +| `alacritty_terminal` | `0.26.1-dev+termforge.8`,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 53ba25ba..d160c31b 100644 --- a/experiments/candidates/alacritty/include/termforge_candidate.h +++ b/experiments/candidates/alacritty/include/termforge_candidate.h @@ -34,11 +34,53 @@ typedef struct TfCellMeta { bool clipped, grapheme_fragment, leading_spacer; } TfCellMeta; +typedef struct TfResizeMapMeta { + uint64_t epoch; + size_t before_rows, before_columns, before_history; + size_t after_rows, after_columns, after_history; + uint64_t discarded_before, discarded_after; + size_t source_rows, target_rows, removed_rows; +} TfResizeMapMeta; + +typedef struct TfResizeSourceMeta { + uint64_t id; + int32_t line; + size_t columns; + bool hard_break, wrap; +} TfResizeSourceMeta; + +typedef struct TfResizeRowMeta { + uint64_t id, hard_boundary; + int32_t line; + uint32_t removal_reason; + size_t columns, spans; +} TfResizeRowMeta; + +typedef struct TfResizeSpan { + uint64_t source_row; + size_t source_column, column, len; +} TfResizeSpan; + void* tf_terminal_new(size_t rows, size_t columns, size_t history); void tf_terminal_free(void* terminal); int tf_terminal_feed(void* terminal, const uint8_t* data, size_t len, uint8_t* error, size_t error_capacity); 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); +// Owned-value queries for the latest resize only. alternate selects the named +// buffer, regardless of which buffer is active. Copy before any successful +// feed (including empty), finish, discard, or next resize; later writes are not +// described by this map. Row/span queries reject a different epoch. +// Reasons: retained=0, capacity=1, cursor reposition=2, height crop=3, column crop=4. +// Removed records describe fragments, not final grid rows. Spans map physical +// cells; absent padding/spacers are not proof of lost logical text. +// Invalid queries return 1 without writing the output; valid queries return 0. +int tf_terminal_resize_map_meta(void* terminal, bool alternate, TfResizeMapMeta* output); +int tf_terminal_resize_source(void* terminal, uint64_t epoch, bool alternate, + size_t index, TfResizeSourceMeta* output); +int tf_terminal_resize_row(void* terminal, uint64_t epoch, bool alternate, bool removed, + size_t index, TfResizeRowMeta* output); +int tf_terminal_resize_span(void* terminal, uint64_t epoch, bool alternate, bool removed, + size_t row, size_t index, TfResizeSpan* output); 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. diff --git a/experiments/candidates/alacritty/run.py b/experiments/candidates/alacritty/run.py index ef004e3d..9fc9ab3d 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.7" +VERSION = "0.26.1-dev+termforge.8" UPSTREAM_COMMIT = "542147067dd016fc5f49120ef0ba605a98a4e3a7" BASE_CRATE_SHA256 = "bda177466b9524d59f1b12f0dd30b68696788e9992a7e959021c4a0ed96fcf59" diff --git a/experiments/candidates/alacritty/src/ffi.rs b/experiments/candidates/alacritty/src/ffi.rs index c274a34b..cb8ebb67 100644 --- a/experiments/candidates/alacritty/src/ffi.rs +++ b/experiments/candidates/alacritty/src/ffi.rs @@ -7,9 +7,9 @@ use std::ffi::{c_int, c_uchar, c_void}; use std::panic::{AssertUnwindSafe, catch_unwind}; use alacritty_terminal::event::Event; -use alacritty_terminal::grid::{Dimensions, GridCell}; +use alacritty_terminal::grid::{Dimensions, GridCell, GridResizeMap, ResizeMappedRow, ResizeRemovalReason}; use alacritty_terminal::index::{Column, Line}; -use alacritty_terminal::term::{Config, Term, TermMode, cell::{Cell, Flags}}; +use alacritty_terminal::term::{Config, Term, TermMode, TerminalResizeMap, cell::{Cell, Flags}}; use alacritty_terminal::vte::ansi::{Color, NamedColor, Processor}; use trace::{Listener, Size}; @@ -18,6 +18,8 @@ struct Terminal { parser: Processor, listener: Listener, events: VecDeque, + resize_epoch: u64, + resize_map: Option, } #[repr(C)] @@ -59,6 +61,42 @@ pub struct CellMeta { leading_spacer: bool, } +#[repr(C)] +pub struct ResizeMapMeta { + epoch: u64, + before_rows: usize, before_columns: usize, before_history: usize, + after_rows: usize, after_columns: usize, after_history: usize, + discarded_before: u64, discarded_after: u64, + source_rows: usize, target_rows: usize, removed_rows: usize, +} + +#[repr(C)] +pub struct ResizeSourceMeta { + id: u64, + line: i32, + columns: usize, + hard_break: bool, + wrap: bool, +} + +#[repr(C)] +pub struct ResizeRowMeta { + id: u64, + hard_boundary: u64, + line: i32, + removal_reason: u32, + columns: usize, + spans: usize, +} + +#[repr(C)] +pub struct ResizeSpan { + source_row: u64, + source_column: usize, + column: usize, + len: usize, +} + fn color(value: Color) -> i32 { match value { Color::Named(NamedColor::Foreground | NamedColor::Background) => -1, @@ -120,7 +158,8 @@ pub extern "C" fn tf_terminal_new(rows: usize, columns: usize, history: usize) - let listener = Listener::default(); let config = Config { scrolling_history: history, ..Config::default() }; let term = Term::new(config, &Size { columns, rows }, listener.clone()); - let value = Terminal { term, parser: Processor::new(), listener, events: VecDeque::new() }; + let value = Terminal { term, parser: Processor::new(), listener, events: VecDeque::new(), + resize_epoch: 0, resize_map: None }; Box::into_raw(Box::new(value)).cast::() })) .unwrap_or(std::ptr::null_mut()) @@ -154,6 +193,7 @@ pub unsafe extern "C" fn tf_terminal_feed( } else { unsafe { std::slice::from_raw_parts(data, len) } }; + value.resize_map = None; value.parser.advance(&mut value.term, bytes); drain_events(value) }) @@ -166,7 +206,7 @@ pub unsafe extern "C" fn tf_terminal_finish( error_capacity: usize, ) -> c_int { guard(error, error_capacity, || { - unsafe { terminal(value)? }; + unsafe { terminal(value)? }.resize_map = None; Ok(()) }) } @@ -184,11 +224,102 @@ pub unsafe extern "C" fn tf_terminal_resize( return Err("invalid candidate dimensions".into()); } let value = unsafe { terminal(value)? }; - value.term.resize(Size { columns, rows }); - drain_events(value) + let epoch = value.resize_epoch.checked_add(1).ok_or("resize map epoch exhausted")?; + value.resize_map = None; + let map = value.term.resize_with_map(Size { columns, rows }); + drain_events(value)?; + value.resize_epoch = epoch; + value.resize_map = Some(map); + Ok(()) }) } +fn resize_buffer(value: &Terminal, alternate: bool) -> Option<&GridResizeMap> { + let map = value.resize_map.as_ref()?; + Some(if alternate { &map.alternate } else { &map.primary }) +} + +fn resize_row(map: &GridResizeMap, removed: bool, index: usize) -> Option<(&ResizeMappedRow, u32)> { + if removed { + let value = map.removed_rows.get(index)?; + let reason = match value.reason { + ResizeRemovalReason::Capacity => 1, + ResizeRemovalReason::CursorReposition => 2, + ResizeRemovalReason::HeightCrop => 3, + ResizeRemovalReason::ColumnCrop => 4, + }; + Some((&value.row, reason)) + } else { + Some((map.target_rows.get(index)?, 0)) + } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn tf_terminal_resize_map_meta( + value: *mut c_void, alternate: bool, output: *mut ResizeMapMeta, +) -> c_int { + if output.is_null() { return 1; } + let Ok(value) = (unsafe { terminal(value) }) else { return 1 }; + let Some(map) = resize_buffer(value, alternate) else { return 1 }; + unsafe { *output = ResizeMapMeta { + epoch: value.resize_epoch, + before_rows: map.before_rows, before_columns: map.before_columns, before_history: map.before_history, + after_rows: map.after_rows, after_columns: map.after_columns, after_history: map.after_history, + discarded_before: map.discarded_before, discarded_after: map.discarded_after, + source_rows: map.source_rows.len(), target_rows: map.target_rows.len(), removed_rows: map.removed_rows.len(), + } }; + 0 +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn tf_terminal_resize_source( + value: *mut c_void, epoch: u64, alternate: bool, index: usize, output: *mut ResizeSourceMeta, +) -> c_int { + if output.is_null() { return 1; } + let Ok(value) = (unsafe { terminal(value) }) else { return 1 }; + if epoch != value.resize_epoch { return 1; } + let Some(map) = resize_buffer(value, alternate) else { return 1 }; + let Some(row) = map.source_rows.get(index) else { return 1 }; + unsafe { *output = ResizeSourceMeta { + id: row.id, line: row.line, columns: row.columns, hard_break: row.hard_break, wrap: row.wrap, + } }; + 0 +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn tf_terminal_resize_row( + value: *mut c_void, epoch: u64, alternate: bool, removed: bool, + index: usize, output: *mut ResizeRowMeta, +) -> c_int { + if output.is_null() { return 1; } + let Ok(value) = (unsafe { terminal(value) }) else { return 1 }; + if epoch != value.resize_epoch { return 1; } + let Some(map) = resize_buffer(value, alternate) else { return 1 }; + let Some((row, reason)) = resize_row(map, removed, index) else { return 1 }; + unsafe { *output = ResizeRowMeta { + id: row.id, hard_boundary: row.origins.hard_boundary.unwrap_or(0), line: row.line, + removal_reason: reason, columns: row.columns, spans: row.origins.spans.len(), + } }; + 0 +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn tf_terminal_resize_span( + value: *mut c_void, epoch: u64, alternate: bool, removed: bool, + row: usize, index: usize, output: *mut ResizeSpan, +) -> c_int { + if output.is_null() { return 1; } + let Ok(value) = (unsafe { terminal(value) }) else { return 1 }; + if epoch != value.resize_epoch { return 1; } + let Some(map) = resize_buffer(value, alternate) else { return 1 }; + let Some((row, _)) = resize_row(map, removed, row) else { return 1 }; + let Some(span) = row.origins.spans.get(index) else { return 1 }; + unsafe { *output = ResizeSpan { + source_row: span.source_row, source_column: span.source_column, column: span.column, len: span.len, + } }; + 0 +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn tf_terminal_meta(value: *mut c_void, output: *mut TerminalMeta) -> c_int { if output.is_null() { @@ -244,6 +375,7 @@ pub unsafe extern "C" fn tf_terminal_discard_history( return Err("history eviction prefix identity differs".into()); } } + value.resize_map = None; value.term.grid_mut().discard_oldest_history(count); unsafe { *discarded = value.term.grid().discarded() }; Ok(()) @@ -445,6 +577,180 @@ mod tests { } } + #[test] + fn capacity_counter_counts_real_loss_but_not_height_growth() { + for (rows, history, input, next_rows, discarded, text) in [ + (3, 1, "A\r\nB\r\nC", 1, 1, "BC"), + // rows() leaves its zeroed byte unchanged for an unwritten cell + // whose public cell_text result has length zero. + (1, 0, "A", 2, 0, "A\0"), + (1, 1, "A\r\nB", 3, 0, "AB\0"), + (3, 4, "A\r\nB\r\nC", 1, 0, "ABC"), + ] { + let value = Handle(tf_terminal_new(rows, 1, history)); + value.feed(input.as_bytes()); + assert_eq!(value.meta().discarded, 0); + assert_eq!(unsafe { tf_terminal_resize(value.0, next_rows, 1, null_mut(), 0) }, 0); + assert_eq!(value.meta().discarded, discarded, + "{rows} rows, history {history}, resize to {next_rows}"); + assert_eq!(value.rows().iter().map(|row| row.1).collect::>(), text.as_bytes()); + } + + let value = Handle(tf_terminal_new(3, 1, 1)); + value.feed(b"A\r\nB\r\nC\x1b[2S"); + assert_eq!(value.meta().discarded, 1, "multi-line SU lost A past the capacity"); + assert_eq!(value.rows().iter().map(|row| row.1).collect::>(), b"BC\0\0"); + let no_history = Handle(tf_terminal_new(1, 1, 0)); + no_history.feed(b"A\r\nB"); + assert_eq!(no_history.meta().discarded, 1, "ordinary zero-capacity scroll still evicts A"); + } + + #[test] + fn resize_map_records_visible_rows_evicted_during_height_shrink() { + let value = Handle(tf_terminal_new(4, 3, 1)); + value.feed(b"ABCDEFGH\r\nIJKLMNOP\r\nQRSTUVWX"); + let sources = value.rows(); + assert_eq!(sources.iter().map(|row| row.1).collect::>(), b"LOQTW"); + assert_eq!(value.meta().discarded, 4); + assert_eq!(unsafe { tf_terminal_resize(value.0, 1, 1, null_mut(), 0) }, 0); + let mut map = MaybeUninit::::uninit(); + assert_eq!(unsafe { tf_terminal_resize_map_meta(value.0, false, map.as_mut_ptr()) }, 0); + let map = unsafe { map.assume_init() }; + assert_eq!((map.source_rows, map.target_rows, map.removed_rows), (5, 2, 6)); + assert_eq!((map.discarded_before, map.discarded_after), (4, 10)); + assert_eq!(value.rows().iter().map(|row| row.1).collect::>(), b"WX"); + + let mut removed = std::collections::BTreeSet::new(); + let mut hard_boundaries = Vec::new(); + for index in 0..map.removed_rows { + let mut row = MaybeUninit::::uninit(); + assert_eq!(unsafe { + tf_terminal_resize_row(value.0, map.epoch, false, true, index, row.as_mut_ptr()) + }, 0); + let row = unsafe { row.assume_init() }; + assert_eq!((row.removal_reason, row.spans), (1, 1)); + if row.hard_boundary != 0 { hard_boundaries.push(row.hard_boundary); } + let mut span = MaybeUninit::::uninit(); + assert_eq!(unsafe { + tf_terminal_resize_span(value.0, map.epoch, false, true, index, 0, span.as_mut_ptr()) + }, 0); + let span = unsafe { span.assume_init() }; + assert_eq!(span.column, 0); + assert!(removed.insert((span.source_row, span.source_column, span.len))); + } + assert_eq!(removed, std::collections::BTreeSet::from([ + (sources[0].0, 0, 3), (sources[1].0, 0, 3), (sources[2].0, 0, 3), + (sources[3].0, 0, 1), (sources[3].0, 1, 1), (sources[3].0, 2, 1), + ])); + assert_eq!(hard_boundaries, vec![sources[1].0]); + for index in 0..2 { + let mut span = MaybeUninit::::uninit(); + assert_eq!(unsafe { + tf_terminal_resize_span(value.0, map.epoch, false, false, index, 0, span.as_mut_ptr()) + }, 0); + let span = unsafe { span.assume_init() }; + assert_eq!((span.source_row, span.source_column, span.column, span.len), + (sources[4].0, index, 0, 1)); + } + } + + #[test] + fn resize_map_clipped_wide_cell_does_not_own_its_removed_spacer() { + let value = Handle(tf_terminal_new(2, 4, 8)); + value.feed("中X\r\nY".as_bytes()); + let sources = value.rows(); + assert_eq!(unsafe { tf_terminal_resize(value.0, 2, 1, null_mut(), 0) }, 0); + let mut map = MaybeUninit::::uninit(); + assert_eq!(unsafe { tf_terminal_resize_map_meta(value.0, false, map.as_mut_ptr()) }, 0); + let map = unsafe { map.assume_init() }; + assert_eq!(map.target_rows, 3); + for (index, &(source, column, hard)) in [(0, 0, false), (0, 2, true), (1, 0, false)].iter().enumerate() { + let mut row = MaybeUninit::::uninit(); + assert_eq!(unsafe { + tf_terminal_resize_row(value.0, map.epoch, false, false, index, row.as_mut_ptr()) + }, 0); + let row = unsafe { row.assume_init() }; + assert_eq!((row.spans, row.hard_boundary), (1, if hard { sources[source].0 } else { 0 })); + let mut span = MaybeUninit::::uninit(); + assert_eq!(unsafe { + tf_terminal_resize_span(value.0, map.epoch, false, false, index, 0, span.as_mut_ptr()) + }, 0); + let span = unsafe { span.assume_init() }; + assert_eq!((span.source_row, span.source_column, span.column, span.len), + (sources[source].0, column, 0, 1)); + } + } + + #[test] + fn resize_map_merges_equal_text_from_distinct_sources() { + let value = Handle(tf_terminal_new(2, 4, 8)); + value.feed(b"ABCDABCD\r\nABCD"); + let sources = value.rows(); + assert_eq!(sources.len(), 3); + assert_eq!(unsafe { tf_terminal_resize(value.0, 2, 8, null_mut(), 0) }, 0); + let mut map = MaybeUninit::::uninit(); + assert_eq!(unsafe { tf_terminal_resize_map_meta(value.0, false, map.as_mut_ptr()) }, 0); + let map = unsafe { map.assume_init() }; + assert_eq!((map.source_rows, map.target_rows, map.after_history, map.discarded_after), (3, 2, 0, 0)); + let mut row = MaybeUninit::::uninit(); + assert_eq!(unsafe { tf_terminal_resize_row(value.0, map.epoch, false, false, 0, row.as_mut_ptr()) }, 0); + let row = unsafe { row.assume_init() }; + assert_eq!((row.columns, row.spans, row.hard_boundary), (8, 2, sources[1].0)); + for index in 0..2 { + let mut span = MaybeUninit::::uninit(); + assert_eq!(unsafe { + tf_terminal_resize_span(value.0, map.epoch, false, false, 0, index, span.as_mut_ptr()) + }, 0); + let span = unsafe { span.assume_init() }; + assert_eq!((span.source_row, span.source_column, span.column, span.len), + (sources[index].0, 0, index * 4, 4)); + } + let mut separate = MaybeUninit::::uninit(); + assert_eq!(unsafe { + tf_terminal_resize_span(value.0, map.epoch, false, false, 1, 0, separate.as_mut_ptr()) + }, 0); + let separate = unsafe { separate.assume_init() }; + assert_eq!((separate.source_row, separate.source_column, separate.column, separate.len), + (sources[2].0, 0, 0, 4)); + } + + #[test] + fn resize_map_tracks_split_source_cells_and_hard_boundaries() { + let value = Handle(tf_terminal_new(2, 8, 8)); + value.feed(b"ABCDEFGH\r\nIJKLMNOP\r\nQRSTUVWX"); + let sources = value.rows(); + assert_eq!(sources.len(), 3); + assert_eq!(unsafe { tf_terminal_resize(value.0, 2, 4, null_mut(), 0) }, 0); + let mut map = MaybeUninit::::uninit(); + assert_eq!(unsafe { tf_terminal_resize_map_meta(value.0, false, map.as_mut_ptr()) }, 0); + let map = unsafe { map.assume_init() }; + assert_eq!((map.before_rows, map.before_columns, map.before_history), (2, 8, 1)); + assert_eq!((map.after_rows, map.after_columns, map.after_history), (2, 4, 4)); + assert_eq!((map.source_rows, map.target_rows, map.removed_rows), (3, 6, 0)); + let actual_ids = value.rows(); + for (index, &(source_index, source_column, hard)) in [ + (0, 0, false), (0, 4, true), + (1, 0, false), (1, 4, true), + (2, 0, false), (2, 4, false), + ].iter().enumerate() { + let mut row = MaybeUninit::::uninit(); + assert_eq!(unsafe { + tf_terminal_resize_row(value.0, map.epoch, false, false, index, row.as_mut_ptr()) + }, 0); + let row = unsafe { row.assume_init() }; + assert_eq!((row.id, row.line, row.columns, row.spans, row.removal_reason), + (actual_ids[index].0, index as i32 - 4, 4, 1, 0)); + assert_eq!(row.hard_boundary, if hard { sources[source_index].0 } else { 0 }); + let mut span = MaybeUninit::::uninit(); + assert_eq!(unsafe { + tf_terminal_resize_span(value.0, map.epoch, false, false, index, 0, span.as_mut_ptr()) + }, 0); + let span = unsafe { span.assume_init() }; + assert_eq!((span.source_row, span.source_column, span.column, span.len), + (sources[source_index].0, source_column, 0, 4)); + } + } + #[test] fn width_reflow_exports_logical_line_ends() { let value = Handle(tf_terminal_new(2, 8, 8)); diff --git a/experiments/candidates/alacritty/test_candidate.py b/experiments/candidates/alacritty/test_candidate.py index 8144d944..a38664c2 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.7") + self.assertEqual(self.summary["version"], "0.26.1-dev+termforge.8") 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 5e3008bc..ecf99db7 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.7" +version = "0.26.1-dev+termforge.8" 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 ca8ddd1e..111efbaa 100644 --- a/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/mod.rs +++ b/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/mod.rs @@ -11,14 +11,67 @@ use crate::term::cell::{Flags, ResetDiscriminant}; use crate::vte::ansi::{CharsetIndex, StandardCharset}; pub mod resize; +pub mod origins; mod row; mod storage; #[cfg(test)] mod tests; pub use self::row::Row; +use self::origins::RowOrigins; use self::storage::Storage; +/// A physical source row immediately before one resize transaction. +#[derive(Clone, Debug)] +pub struct ResizeSourceRow { + pub id: u64, + pub line: i32, + pub columns: usize, + pub hard_break: bool, + pub wrap: bool, +} + +/// The source cells carried by a retained or removed physical row. +#[derive(Clone, Debug)] +pub struct ResizeMappedRow { + pub id: u64, + /// Native grid line at capture; negative values denote history. + pub line: i32, + pub columns: usize, + pub origins: RowOrigins, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(u32)] +pub enum ResizeRemovalReason { + Capacity = 1, + CursorReposition = 2, + HeightCrop = 3, + ColumnCrop = 4, +} + +#[derive(Clone, Debug)] +pub struct ResizeRemoval { + pub row: ResizeMappedRow, + pub reason: ResizeRemovalReason, +} + +/// Cell movement during one resize, separate from later writes or text policy. +#[derive(Clone, Debug)] +pub struct GridResizeMap { + pub before_rows: usize, + pub before_columns: usize, + pub before_history: usize, + pub after_rows: usize, + pub after_columns: usize, + pub after_history: usize, + pub discarded_before: u64, + pub discarded_after: u64, + pub source_rows: Vec, + pub target_rows: Vec, + pub removed_rows: Vec, +} + pub trait GridCell: Sized { /// Check if the cell contains any content. fn is_empty(&self) -> bool; @@ -138,6 +191,9 @@ pub struct Grid { #[cfg_attr(feature = "serde", serde(skip))] discarded: u64, + + #[cfg_attr(feature = "serde", serde(skip))] + resize_removed: Option>, } impl Grid { @@ -146,6 +202,7 @@ impl Grid { raw: Storage::with_capacity(lines, columns), max_scroll_limit, discarded: 0, + resize_removed: None, display_offset: 0, saved_cursor: Cursor::default(), cursor: Cursor::default(), @@ -154,6 +211,50 @@ impl Grid { } } + pub(crate) fn begin_resize_map(&mut self) -> GridResizeMap { + self.raw.clear_resize_origins(); + self.resize_removed = Some(Vec::new()); + let history = self.history_size(); + let mut source_rows = Vec::with_capacity(history + self.lines); + for line in -(history as i32)..self.lines as i32 { + let row = &mut self.raw[Line(line)]; + source_rows.push(ResizeSourceRow { + id: row.id(), line, columns: row.len(), hard_break: row.hard_break(), + wrap: row.last().is_some_and(|cell| cell.flags().contains(Flags::WRAPLINE)), + }); + row.begin_origins(); + } + GridResizeMap { + before_rows: self.lines, before_columns: self.columns, before_history: history, + after_rows: self.lines, after_columns: self.columns, after_history: history, + discarded_before: self.discarded, discarded_after: self.discarded, + source_rows, target_rows: Vec::new(), removed_rows: Vec::new(), + } + } + + pub(crate) fn finish_resize_map(&mut self, mut map: GridResizeMap) -> GridResizeMap { + map.after_rows = self.lines; + map.after_columns = self.columns; + map.after_history = self.history_size(); + map.discarded_after = self.discarded; + for line in -(map.after_history as i32)..self.lines as i32 { + map.target_rows.push(Self::take_resize_row(&mut self.raw[Line(line)], line)); + } + map.removed_rows = self.resize_removed.take().unwrap_or_default(); + // Storage retains inactive allocation slots after height changes. They + // must not carry origins into a later resize or row reuse. + self.raw.clear_resize_origins(); + map + } + + fn take_resize_row(row: &mut Row, line: i32) -> ResizeMappedRow { + let columns = row.len(); + let origins = row.take_origins().map(|value| *value).unwrap_or_else(|| RowOrigins { + columns, ..RowOrigins::default() + }); + ResizeMappedRow { id: row.id(), line, columns, origins } + } + /// Update the size of the scrollback history. pub fn update_history(&mut self, history_size: usize) { let current_history_size = self.history_size(); @@ -257,6 +358,14 @@ impl Grid { /// /// This is the performance-sensitive part of scrolling. pub fn scroll_up(&mut self, region: &Range, positions: usize) + where + T: ResetDiscriminant, + D: PartialEq, + { + self.scroll_up_inner(region, positions, true); + } + + fn scroll_up_inner(&mut self, region: &Range, positions: usize, count_evictions: bool) where T: ResetDiscriminant, D: PartialEq, @@ -279,12 +388,25 @@ impl Grid { // Only rotate the entire history if the active region starts at the top. if region.start == 0 { let available = self.max_scroll_limit - self.history_size(); - let evicted = if self.max_scroll_limit == 0 { - positions - } else { - min(positions.saturating_sub(available), self.history_size()) - }; - self.discarded = self.discarded.checked_add(evicted as u64).expect("history discarded exhausted"); + let evicted = positions.saturating_sub(available); + let oldest = -(self.history_size() as i32); + if let Some(records) = &mut self.resize_removed { + // A large rotation can also discard visible rows when the + // history capacity is smaller than the scroll distance. + for line in oldest..oldest + evicted as i32 { + let row = &mut self.raw[Line(line)]; + if row.origins().is_some() { + records.push(ResizeRemoval { + row: Self::take_resize_row(row, line), + reason: ResizeRemovalReason::Capacity, + }); + } + } + } + if count_evictions { + self.discarded = self.discarded.checked_add(evicted as u64) + .expect("history discarded exhausted"); + } // Create scrollback for the new lines. self.increase_scroll_limit(positions); diff --git a/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/origins.rs b/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/origins.rs new file mode 100644 index 00000000..5427d0db --- /dev/null +++ b/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/origins.rs @@ -0,0 +1,157 @@ +//! Sparse physical-cell origins for one synchronous resize operation. + +/// A contiguous run of cells moved from one pre-resize row. +/// +/// Both coordinates count physical cells. A clipped wide glyph retains its +/// lead-cell origin; its logical width still belongs to the source cell. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct OriginSpan { + pub source_row: u64, + pub source_column: usize, + pub column: usize, + pub len: usize, +} + +/// Origins carried beside a row or a temporary row fragment during resize. +/// +/// `columns` includes gaps and padding without origins. The optional hard +/// boundary is independent of cells, so an empty hard line remains observable. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct RowOrigins { + pub columns: usize, + pub spans: Vec, + pub hard_boundary: Option, +} + +impl RowOrigins { + pub(crate) fn new(source_row: u64, columns: usize, hard: bool) -> Self { + let spans = if columns == 0 { + Vec::new() + } else { + vec![OriginSpan { source_row, source_column: 0, column: 0, len: columns }] + }; + Self { columns, spans, hard_boundary: hard.then_some(source_row) } + } + + /// Move the suffix and its end boundary into a fragment starting at zero. + pub(crate) fn split_off(&mut self, at: usize) -> Self { + let at = at.min(self.columns); + let mut suffix = Self { + columns: self.columns - at, + spans: Vec::new(), + hard_boundary: self.hard_boundary.take(), + }; + let spans = std::mem::take(&mut self.spans); + for span in spans { + let end = span.column + span.len; + if span.column < at { + self.push(OriginSpan { len: end.min(at) - span.column, ..span }); + } + if end > at { + let start = span.column.max(at); + suffix.push(OriginSpan { + source_column: span.source_column + start - span.column, + column: start - at, + len: end - start, + ..span + }); + } + } + self.columns = at; + suffix + } + + /// Move a prefix out and shift the remaining origins toward column zero. + pub(crate) fn take_prefix(&mut self, count: usize) -> Self { + if count >= self.columns { + return std::mem::take(self); + } + let suffix = self.split_off(count); + std::mem::replace(self, suffix) + } + + /// Append a fragment at `offset`, including any unowned intervening gap. + pub(crate) fn append(&mut self, other: Self, offset: usize) { + assert!(offset >= self.columns, "origin append overlaps existing extent"); + self.columns = offset.checked_add(other.columns).expect("origin extent exhausted"); + self.hard_boundary = other.hard_boundary; + for mut span in other.spans { + span.column = offset.checked_add(span.column).expect("origin column exhausted"); + self.push(span); + } + } + + /// Insert a preceding fragment; the original right-hand end remains ours. + pub(crate) fn prepend(&mut self, other: Self, width: usize) { + assert!(other.columns <= width, "origin prefix exceeds inserted width"); + let columns = self.columns.checked_add(width).expect("origin extent exhausted"); + let end = self.hard_boundary; + let previous = std::mem::take(&mut self.spans); + for span in other.spans { + self.push(span); + } + for mut span in previous { + span.column = span.column.checked_add(width).expect("origin column exhausted"); + self.push(span); + } + self.columns = columns; + self.hard_boundary = end; + } + + /// Drop a physical suffix while retaining the separately owned end boundary. + pub(crate) fn truncate(&mut self, columns: usize) { + if columns >= self.columns { + return; + } + self.spans.retain_mut(|span| { + if span.column >= columns { + return false; + } + span.len = span.len.min(columns - span.column); + true + }); + self.columns = columns; + } + + /// Insert synthetic cells without assigning them an old source. + pub(crate) fn insert_gap(&mut self, at: usize, len: usize) { + if len == 0 { + return; + } + let at = at.min(self.columns); + let suffix = self.split_off(at); + let offset = at.checked_add(len).expect("origin extent exhausted"); + self.append(suffix, offset); + } + + /// Remove physical cells and close the gap, preserving the logical end. + pub(crate) fn remove_range(&mut self, at: usize, len: usize) { + let at = at.min(self.columns); + let len = len.min(self.columns - at); + if len == 0 { + return; + } + let boundary = self.hard_boundary; + let mut suffix = self.split_off(at); + suffix.take_prefix(len); + // Removing the final physical cells does not erase their hard boundary. + self.append(suffix, at); + self.hard_boundary = boundary; + } + + fn push(&mut self, span: OriginSpan) { + if span.len == 0 { + return; + } + if let Some(previous) = self.spans.last_mut() { + if previous.source_row == span.source_row + && previous.column + previous.len == span.column + && previous.source_column + previous.len == span.source_column + { + previous.len += span.len; + return; + } + } + self.spans.push(span); + } +} diff --git a/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/resize.rs b/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/resize.rs index 2a6202fb..70ff490c 100644 --- a/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/resize.rs +++ b/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/resize.rs @@ -7,7 +7,18 @@ use crate::index::{Boundary, Column, Line}; use crate::term::cell::{Flags, ResetDiscriminant}; use crate::grid::row::Row; -use crate::grid::{Dimensions, Grid, GridCell}; +use crate::grid::origins::RowOrigins; +use crate::grid::{Dimensions, Grid, GridCell, ResizeMappedRow, ResizeRemoval, ResizeRemovalReason}; + +fn origin_extent(origins: &mut Option>, columns: usize) { + if let Some(origins) = origins { + if columns < origins.columns { + origins.truncate(columns); + } else if columns > origins.columns { + origins.insert_gap(origins.columns, columns - origins.columns); + } + } +} impl Grid { /// Resize the grid's width and/or height. @@ -57,7 +68,8 @@ impl Grid { // Move existing lines up for every line that couldn't be pulled from history. if from_history != lines_added { let delta = lines_added - from_history; - self.scroll_up(&(Line(0)..Line(target as i32)), delta); + // This rotation removes newly allocated rows, not retained history. + self.scroll_up_inner(&(Line(0)..Line(target as i32)), delta, false); } // Move cursor down for every line pulled from history. @@ -92,6 +104,17 @@ impl Grid { // Clamp saved cursor, since only primary cursor is scrolled into viewport. self.saved_cursor.point.line = min(self.saved_cursor.point.line, Line(target as i32 - 1)); + if let Some(records) = &mut self.resize_removed { + for line in target..self.lines { + let row = &mut self.raw[Line(line as i32)]; + if row.origins().is_some() { + records.push(ResizeRemoval { + row: Self::take_resize_row(row, line as i32), + reason: ResizeRemovalReason::HeightCrop, + }); + } + } + } self.raw.rotate((self.lines - target) as isize); self.raw.shrink_visible_lines(target); self.lines = target; @@ -134,21 +157,27 @@ impl Grid { } // Remove leading spacers when reflowing wide char to the previous line. + let mut last_origins = last_row.take_origins(); let mut last_len = last_row.len(); if last_len >= 1 && last_row[Column(last_len - 1)].flags().contains(Flags::LEADING_WIDE_CHAR_SPACER) { last_row.shrink(last_len - 1); last_len -= 1; + origin_extent(&mut last_origins, last_len); } // Don't try to pull more cells from the next line than available. let mut num_wrapped = columns - last_len; let len = min(row.len(), num_wrapped); + let mut origins = row.take_origins(); + let hard_origin = origins.as_ref().and_then(|origins| origins.hard_boundary); + let mut moved_columns = len; // Insert leading spacer when there's not enough room for reflowing wide char. let mut cells = if row[Column(len - 1)].flags().contains(Flags::WIDE_CHAR) { num_wrapped -= 1; + moved_columns -= 1; let mut cells = row.front_split_off(len - 1); @@ -161,6 +190,27 @@ impl Grid { row.front_split_off(len) }; + let mut moved_origins = origins.as_mut().map(|origins| { + Box::new(origins.take_prefix(moved_columns)) + }); + origin_extent(&mut moved_origins, cells.len()); + // The continuation's hard boundary moves only if that row is + // actually consumed, including the cursor-sensitive empty case. + if let Some(origins) = &mut origins { + origins.hard_boundary = hard_origin; + } + if let Some(mut moved) = moved_origins { + moved.hard_boundary = None; + let previous = last_origins.get_or_insert_with(|| Box::new(RowOrigins { + columns: last_len, ..RowOrigins::default() + })); + previous.append(*moved, last_len); + } else if let Some(previous) = &mut last_origins { + previous.insert_gap(last_len, cells.len()); + previous.hard_boundary = None; + } + row.set_origins(origins); + // Add removed cells to previous row and reflow content. last_row.append(&mut cells); @@ -185,6 +235,10 @@ impl Grid { // The consumed continuation can carry a hard boundary even // when its remaining cells are empty. last_row.set_hard_break(row.hard_break()); + if let Some(origins) = &mut last_origins { + origins.hard_boundary = hard_origin; + } + last_row.set_origins(last_origins); continue; } @@ -203,6 +257,10 @@ impl Grid { // A physical row may disappear; its logical end must survive // on the row which received its cells. last_row.set_hard_break(row.hard_break()); + if let Some(origins) = &mut last_origins { + origins.hard_boundary = hard_origin; + } + last_row.set_origins(last_origins); // Don't push line into the new buffer. continue; @@ -212,6 +270,7 @@ impl Grid { // Set wrap flag if next line still has cells. cell.flags_mut().insert(Flags::WRAPLINE); } + last_row.set_origins(last_origins); reversed.push(row); } @@ -229,7 +288,17 @@ impl Grid { let cursor_buffer_line = self.lines - self.cursor.point.line.0 as usize - 1; let available = min(cursor_buffer_line, reversed.len() - self.lines); let overflow = cursor_line_delta.saturating_sub(available); - reversed.truncate(reversed.len() + overflow - cursor_line_delta); + let retained = reversed.len() + overflow - cursor_line_delta; + let history = reversed.len() - self.lines; + if let Some(records) = &mut self.resize_removed { + for index in retained..reversed.len() { + records.push(ResizeRemoval { + row: Self::take_resize_row(&mut reversed[index], index as i32 - history as i32), + reason: ResizeRemovalReason::CursorReposition, + }); + } + } + reversed.truncate(retained); self.cursor.point.line = max(self.cursor.point.line - overflow, Line(0)); } @@ -238,6 +307,9 @@ impl Grid { for mut row in reversed.drain(..).rev() { if row.len() < columns { row.grow(columns); + let mut origins = row.take_origins(); + origin_extent(&mut origins, columns); + row.set_origins(origins); } if reflow { let wrapped = row.last().is_some_and(|cell| cell.flags().contains(Flags::WRAPLINE)); @@ -267,15 +339,27 @@ impl Grid { let mut new_raw = Vec::with_capacity(self.raw.len()); let mut buffered: Option> = None; + let mut buffered_origins: Option> = None; let mut rows = self.raw.take_all(); for (i, mut row) in rows.drain(..).enumerate().rev() { // The logical end belongs to the final fragment, not to the // physical container retained for the first fragment. let hard_break = row.hard_break(); + let hard_origin = row.origins().and_then(|origins| origins.hard_boundary); // Append lines left over from the previous row. if let Some(buffered) = buffered.take() { + let width = buffered.len(); + let mut origins = row.take_origins(); + if let Some(prefix) = buffered_origins.take() { + let origins = origins.get_or_insert_with(|| Box::new(RowOrigins { + columns: row.len(), ..RowOrigins::default() + })); + origins.prepend(*prefix, width); + } else if let Some(origins) = &mut origins { + origins.insert_gap(0, width); + } // Add a column for every cell added before the cursor, if it goes beyond the new // width it is then later reflown. let cursor_buffer_line = self.lines - self.cursor.point.line.0 as usize - 1; @@ -284,13 +368,37 @@ impl Grid { } row.append_front(buffered); + row.set_origins(origins); } loop { + let mut origins = row.take_origins(); + let mut wrapped_origins = origins.as_mut().map(|origins| { + Box::new(origins.split_off(columns)) + }); // Remove all cells which require reflowing. - let mut wrapped = match row.shrink(columns) { + let removed = row.shrink(columns); + let mut wrapped = match removed { Some(wrapped) if reflow => wrapped, - _ => { + removed => { + if !reflow { + if let Some(origins) = &mut wrapped_origins { + origins.hard_boundary = None; + origins.truncate(removed.as_ref().map_or(0, Vec::len)); + } + if let (Some(removed), Some(origins), Some(records)) = + (removed, wrapped_origins.take(), &mut self.resize_removed) + { + if !origins.spans.is_empty() { + records.push(ResizeRemoval { + row: ResizeMappedRow { id: row.id(), + line: self.lines as i32 - i as i32 - 1, + columns: removed.len(), origins: *origins }, + reason: ResizeRemovalReason::ColumnCrop, + }); + } + } + } // The adapter supports one-column windows. Preserve a // whole glyph in a clipped footprint in the non-reflow // alternate buffer as well. @@ -304,12 +412,18 @@ impl Grid { // whitespace and need to wrap it like normal content. Vec::new() } else { + if let Some(origins) = &mut origins { + origins.hard_boundary = hard_origin; + } + origin_extent(&mut origins, row.len()); + row.set_origins(origins); // Since it fits, just push the existing line without any reflow. new_raw.push(row); break; } }, }; + origin_extent(&mut wrapped_origins, wrapped.len()); // A two-cell glyph cannot be reflowed into a one-column // grid: repeatedly pushing it to the next row never converges. @@ -319,12 +433,19 @@ impl Grid { row[Column(0)].flags_mut().insert(Flags::CLIPPED_GRAPHEME); if wrapped.first().is_some_and(|cell| cell.flags().contains(Flags::WIDE_CHAR_SPACER)) { wrapped.remove(0); + if let Some(origins) = &mut wrapped_origins { + origins.remove_range(0, 1); + } let cursor_buffer_line = self.lines - self.cursor.point.line.0 as usize - 1; if i == cursor_buffer_line && self.cursor.point.column.0 > 0 { self.cursor.point.column -= 1; } } if wrapped.is_empty() { + if let Some(origins) = &mut origins { + origins.hard_boundary = hard_origin; + } + row.set_origins(origins); new_raw.push(row); break; } @@ -339,6 +460,19 @@ impl Grid { let wide_char = mem::replace(&mut row[Column(columns - 1)], spacer); wrapped.insert(0, wide_char); + let leading = origins.as_mut().map(|origins| { + let leading = origins.split_off(columns - 1); + origins.insert_gap(columns - 1, 1); + leading + }); + if let Some(leading) = leading { + let suffix = wrapped_origins.get_or_insert_with(|| Box::new(RowOrigins { + columns: wrapped.len() - 1, ..RowOrigins::default() + })); + suffix.prepend(leading, 1); + } else if let Some(origins) = &mut wrapped_origins { + origins.insert_gap(0, 1); + } } // Remove wide char spacer before shrinking. @@ -346,15 +480,18 @@ impl Grid { if len > 0 && wrapped[len - 1].flags().contains(Flags::LEADING_WIDE_CHAR_SPACER) { if len == 1 { row[Column(columns - 1)].flags_mut().insert(Flags::WRAPLINE); + row.set_origins(origins); new_raw.push(row); break; } else { // Remove the leading spacer from the end of the wrapped row. wrapped[len - 2].flags_mut().insert(Flags::WRAPLINE); wrapped.truncate(len - 1); + origin_extent(&mut wrapped_origins, wrapped.len()); } } + row.set_origins(origins); new_raw.push(row); // Set line as wrapped if cells got removed. @@ -375,6 +512,7 @@ impl Grid { // Add removed cells to start of next row. buffered = Some(wrapped); + buffered_origins = wrapped_origins; break; } else { // Reflow cursor if a line below it is deleted. @@ -397,8 +535,10 @@ impl Grid { if occ < columns { wrapped.resize_with(columns, T::default); } + origin_extent(&mut wrapped_origins, wrapped.len()); row = Row::from_vec(wrapped, occ); row.set_hard_break(hard_break); + row.set_origins(wrapped_origins); if i < self.display_offset { // Since we added a new line, rotate up the viewport. @@ -415,6 +555,15 @@ impl Grid { let evicted = reversed.len().saturating_sub(self.max_scroll_limit + self.lines); self.discarded = self.discarded.checked_add(evicted as u64) .expect("history discarded exhausted"); + if let Some(records) = &mut self.resize_removed { + let limit = self.max_scroll_limit + self.lines; + for index in (limit..reversed.len()).rev() { + records.push(ResizeRemoval { + row: Self::take_resize_row(&mut reversed[index], self.lines as i32 - index as i32 - 1), + reason: ResizeRemovalReason::Capacity, + }); + } + } reversed.truncate(self.max_scroll_limit + self.lines); if reflow { for row in &mut reversed { diff --git a/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/row.rs b/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/row.rs index d0e3fbdb..428604d3 100644 --- a/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/row.rs +++ b/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/row.rs @@ -9,6 +9,7 @@ use std::{iter, slice}; use serde::{Deserialize, Serialize}; use crate::grid::GridCell; +use crate::grid::origins::RowOrigins; use crate::index::Column; use crate::term::cell::ResetDiscriminant; @@ -37,6 +38,9 @@ pub struct Row { hard_break: bool, #[cfg_attr(feature = "serde", serde(skip))] wrap_columns: usize, + /// Present only while a synchronous resize is recording cell origins. + #[cfg_attr(feature = "serde", serde(skip))] + origins: Option>, } impl Default for Row { @@ -55,7 +59,7 @@ impl Row { /// Create a new terminal row. pub fn new(columns: usize) -> Row { let inner = iter::repeat_with(T::default).take(columns).collect(); - Row { inner, occ: 0, id: next_row_id(), hard_break: false, wrap_columns: 0 } + Row { inner, occ: 0, id: next_row_id(), hard_break: false, wrap_columns: 0, origins: None } } /// Increase the number of columns in the row. @@ -112,6 +116,7 @@ impl Row { self.occ = 0; self.hard_break = false; self.wrap_columns = 0; + self.origins = None; } } @@ -119,7 +124,7 @@ impl Row { impl Row { #[inline] pub fn from_vec(vec: Vec, occ: usize) -> Row { - Row { inner: vec, occ, id: next_row_id(), hard_break: false, wrap_columns: 0 } + Row { inner: vec, occ, id: next_row_id(), hard_break: false, wrap_columns: 0, origins: None } } #[inline] @@ -137,9 +142,27 @@ impl Row { self.wrap_columns } + /// Temporary cell origins for the current resize, if capture is enabled. + pub fn origins(&self) -> Option<&RowOrigins> { + self.origins.as_deref() + } + + pub(crate) fn begin_origins(&mut self) { + self.origins = Some(Box::new(RowOrigins::new(self.id, self.len(), self.hard_break))); + } + + pub(crate) fn take_origins(&mut self) -> Option> { + self.origins.take() + } + + pub(crate) fn set_origins(&mut self, origins: Option>) { + self.origins = origins; + } + #[inline] pub(crate) fn renew_identity(&mut self) { self.id = next_row_id(); + self.origins = None; } #[inline] diff --git a/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/storage.rs b/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/storage.rs index 426d0dc8..f0a334ed 100644 --- a/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/storage.rs +++ b/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/storage.rs @@ -146,6 +146,12 @@ impl Storage { self.len } + pub(super) fn clear_resize_origins(&mut self) { + for row in &mut self.inner { + row.take_origins(); + } + } + /// Swap complete rows, including their identity and linebreak metadata. pub fn swap(&mut self, a: Line, b: Line) { let a = self.compute_index(a); diff --git a/experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/mod.rs b/experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/mod.rs index 618fbb2c..cd0e57c2 100644 --- a/experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/mod.rs +++ b/experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/mod.rs @@ -14,7 +14,7 @@ use log::{debug, trace}; use unicode_width::UnicodeWidthChar; use crate::event::{Event, EventListener}; -use crate::grid::{Dimensions, Grid, GridIterator, Scroll}; +use crate::grid::{Dimensions, Grid, GridIterator, GridResizeMap, Scroll}; use crate::index::{self, Boundary, Column, Direction, Line, Point, Side}; use crate::selection::{Selection, SelectionRange, SelectionType}; use crate::term::cell::{Cell, Flags, LineLength}; @@ -39,6 +39,13 @@ pub const MIN_COLUMNS: usize = 1; /// Minimum number of visible lines. pub const MIN_SCREEN_LINES: usize = 1; +/// Owned movement maps for both buffers in one terminal resize transaction. +#[derive(Clone, Debug)] +pub struct TerminalResizeMap { + pub primary: GridResizeMap, + pub alternate: GridResizeMap, +} + /// Max size of the window title stack. const TITLE_STACK_MAX_DEPTH: usize = 4096; @@ -727,6 +734,22 @@ impl Term { self.damage.resize(num_cols, num_lines); } + /// Resize while recording physical cell origins, then remove all tracing + /// state from the grids. The returned map describes only this operation. + pub fn resize_with_map(&mut self, size: S) -> TerminalResizeMap { + self.seal_grapheme(); + let active_before = self.grid.begin_resize_map(); + let inactive_before = self.inactive_grid.begin_resize_map(); + self.resize(size); + let active = self.grid.finish_resize_map(active_before); + let inactive = self.inactive_grid.finish_resize_map(inactive_before); + if self.mode.contains(TermMode::ALT_SCREEN) { + TerminalResizeMap { primary: inactive, alternate: active } + } else { + TerminalResizeMap { primary: active, alternate: inactive } + } + } + /// Active terminal modes. #[inline] pub fn mode(&self) -> &TermMode { diff --git a/experiments/core/CANDIDATE.md b/experiments/core/CANDIDATE.md index b801e233..c520c9f4 100644 --- a/experiments/core/CANDIDATE.md +++ b/experiments/core/CANDIDATE.md @@ -2,9 +2,19 @@ 按 [ADR-0006](../../docs/adr/0006-confirmed-project-direction.md),用户确认保留本候选代码继续评估,尚未确认最终采用。下述实现、来源与回归结果继续保留;与 WindTerm 2.7.0 的重要差异、性能容差及正式选型分别提交用户确认。 -`CandidateTerminal` 是 #27 的拥有值适配层,不是自研核心的替代品声明。它把固定 `0.26.1-dev+termforge.7` 的 `Term/Processor` 接到现有 `Screen`/`Line`;默认 CMake 保留自研核心且不需要 Rust,候选产品由 `TERMFORGE_BUILD_CANDIDATE_ADAPTER=ON` 显式启用。 +`CandidateTerminal` 是 #27 的拥有值适配层,不是自研核心的替代品声明。它把固定 `0.26.1-dev+termforge.8` 的 `Term/Processor` 接到现有 `Screen`/`Line`;默认 CMake 保留自研核心且不需要 Rust,候选产品由 `TERMFORGE_BUILD_CANDIDATE_ADAPTER=ON` 显式启用。 -当前 `.7` 修复原生 resize 的硬/软边界传递及缩宽容量淘汰计数,来源见[补丁台账](../candidates/alacritty/PROVENANCE.md#termforge7-补丁),结果见[工作检查点](results/2026-10-28-reflow-metadata/README.md)。C++ 长行 resize/repaint 的整段复制回归仍失败:旧历史行与原生片段重复,来源归属和统一视图映射仍由 #29 继续处理。这些原生修复不构成候选完整正确性通过。 +`.7` 已修复原生 resize 的硬/软边界传递及缩宽容量淘汰计数,结果保留在[工作检查点](results/2026-10-28-reflow-metadata/README.md)。当前 `.8` 增加一次 resize 的来源映射,来源见[补丁台账](../candidates/alacritty/PROVENANCE.md#termforge8-补丁)。C++ 长行 resize/repaint 的整段复制回归仍失败:旧历史行与原生片段重复,来源归属和统一视图映射仍由 #29 继续处理。这些原生修复不构成候选完整正确性通过。 + +## 一次 resize 的来源映射 + +`tf_terminal_resize` 导出主屏和备用屏各自的旧行、目标行和被移除片段,不因当前激活哪个屏幕而改变两者名称。每个稀疏区间描述旧行 ID、旧物理列、目标物理列和长度;硬换行边界独立保存旧行 ID,因此空硬行也有身份。新补 padding/leading spacer 不假造旧来源;单列裁剪宽字保留 lead cell 来源,逻辑宽度仍属于原始 cell。移除原因区分容量、光标重定位、底部高度裁剪和非 reflow 列裁剪,缺失 padding/spacer 不等于逻辑文本丢失证明。 + +映射只描述同步 resize 本身。C ABI 返回拥有值并校验 epoch;下一次成功 feed(包括空输入)、finish、历史淘汰或 resize 使旧映射失效,非法尺寸和未通过前缀校验的淘汰请求保持原映射。后续写入可能复用原生行 ID,调用方不能用本映射推断写入后的来源。普通 Rust `Term::resize` 不启用追踪;`resize_with_map` 结束时清理所有存储槽中的临时来源,包括非活动缓存槽。 + +原生淘汰数累计实际超出历史容量的物理行,包括一次多行滚动中移出容量的原可见行。扩高时仅为补空行而执行的内部滚动不计淘汰;显式清空、物理行合并及非容量裁剪仍分别处理。删除片段数量不是通用的逻辑文本损失计数。 + +`candidate_resize_map_test` 从 C++ 调用真实静态库,验证字段布局、容量片段、映射生命周期、主/备用屏命名和空硬行。Rust FFI 测试覆盖拆分、相同文字的不同来源合并、宽字 spacer 和高度缩小的来源移除。当前 C++ 历史合并尚未消费此映射,后续覆盖写入、源文本归属、复制/Find 和视口坐标仍需实现与完整验证;该接口不裁决原版历史覆盖策略,也未测量新增来源表的资源成本。 ## 构建与验证 diff --git a/experiments/core/candidate_resize_map_test.cpp b/experiments/core/candidate_resize_map_test.cpp new file mode 100644 index 00000000..4edf4569 --- /dev/null +++ b/experiments/core/candidate_resize_map_test.cpp @@ -0,0 +1,173 @@ +#include "../candidates/alacritty/include/termforge_candidate.h" +#include +#include +#include +#include +#include +#include +#include + +namespace { +void require(bool value, const char* message) { + if (!value) throw std::runtime_error(message); +} + +void capacity_mapping_and_lifetime() { + std::unique_ptr terminal(tf_terminal_new(2, 8, 1), tf_terminal_free); + require(terminal != nullptr, "could not create candidate"); + const std::string_view input = "ABCDEFGH\r\nIJKLMNOP\r\nQRSTUVWX"; + require(tf_terminal_feed(terminal.get(), reinterpret_cast(input.data()), + input.size(), nullptr, 0) == 0, "source input failed"); + std::array source{}; + for (size_t row = 0; row < source.size(); ++row) + require(tf_terminal_row_meta(terminal.get(), row, &source[row]) == 0, "source row missing"); + + require(tf_terminal_resize(terminal.get(), 2, 4, nullptr, 0) == 0, "resize failed"); + TfResizeMapMeta map{}; + require(tf_terminal_resize_map_meta(terminal.get(), false, &map) == 0, "primary map missing"); + require(map.epoch != 0 && map.before_rows == 2 && map.before_columns == 8 && map.before_history == 1 && + map.after_rows == 2 && map.after_columns == 4 && map.after_history == 1 && + map.discarded_before == 0 && map.discarded_after == 3 && + map.source_rows == 3 && map.target_rows == 3 && map.removed_rows == 3, + "C ABI map metadata or capacity accounting differs"); + + TfResizeSourceMeta old{}; + require(tf_terminal_resize_source(terminal.get(), map.epoch, false, 0, &old) == 0 && + old.id == source[0].id && old.line == -1 && old.columns == 8 && old.hard_break && !old.wrap, + "source row identity or boundary differs"); + TfResizeRowMeta retained{}; + TfResizeSpan first{}; + require(tf_terminal_resize_row(terminal.get(), map.epoch, false, false, 0, &retained) == 0 && + retained.line == -1 && retained.columns == 4 && retained.removal_reason == 0 && + retained.hard_boundary == source[1].id && retained.spans == 1, + "retained MNOP boundary differs"); + require(tf_terminal_resize_span(terminal.get(), map.epoch, false, false, 0, 0, &first) == 0 && + first.source_row == source[1].id && first.source_column == 4 && first.column == 0 && first.len == 4, + "retained MNOP origin differs"); + + using Range = std::tuple; + std::set removed; + for (size_t index = 0; index < map.removed_rows; ++index) { + TfResizeRowMeta row{}; + TfResizeSpan span{}; + require(tf_terminal_resize_row(terminal.get(), map.epoch, false, true, index, &row) == 0 && + row.removal_reason == 1 && row.columns == 4 && row.spans == 1, + "capacity removal was misclassified"); + require(tf_terminal_resize_span(terminal.get(), map.epoch, false, true, index, 0, &span) == 0 && + span.column == 0, "removed source span missing"); + require(removed.emplace(span.source_row, span.source_column, span.len).second, + "capacity map repeats a source range"); + } + require(removed == std::set{{source[0].id, 0, 4}, {source[0].id, 4, 4}, {source[1].id, 0, 4}}, + "capacity map lost or invented source ranges"); + + TfResizeMapMeta alternate{}; + require(tf_terminal_resize_map_meta(terminal.get(), true, &alternate) == 0 && + alternate.epoch == map.epoch && alternate.before_history == 0 && alternate.after_history == 0 && + alternate.source_rows == 2 && alternate.target_rows == 2, + "inactive alternate map missing or mixed with primary"); + TfResizeSourceMeta alternate_source{}; + require(tf_terminal_resize_source(terminal.get(), map.epoch, true, 0, &alternate_source) == 0 && + alternate_source.id != source[0].id && alternate_source.id != source[1].id && + alternate_source.id != source[2].id, "buffer source identities overlap"); + + require(tf_terminal_resize(terminal.get(), 0, 4, nullptr, 0) == 1, + "invalid resize unexpectedly succeeded"); + TfResizeMapMeta unchanged{}; + require(tf_terminal_resize_map_meta(terminal.get(), false, &unchanged) == 0 && unchanged.epoch == map.epoch, + "rejected resize invalidated an unchanged map"); + uint64_t discarded = UINT64_MAX; + const uint64_t invalid_id = 0; + require(tf_terminal_discard_history(terminal.get(), &invalid_id, 1, &discarded, nullptr, 0) == 1 && + discarded == UINT64_MAX, "rejected history discard modified output"); + require(tf_terminal_resize_map_meta(terminal.get(), false, &unchanged) == 0 && unchanged.epoch == map.epoch, + "rejected history discard invalidated an unchanged map"); + const auto sentinel = retained.id; + require(tf_terminal_resize_row(terminal.get(), map.epoch + 1, false, false, 0, &retained) == 1 && + retained.id == sentinel, "wrong epoch wrote an output"); + require(tf_terminal_feed(terminal.get(), nullptr, 0, nullptr, 0) == 0, "empty feed failed"); + require(tf_terminal_resize_map_meta(terminal.get(), false, &unchanged) == 1 && + unchanged.epoch == map.epoch, "feed left a stale resize map available"); + require(tf_terminal_resize_row(terminal.get(), map.epoch, false, false, 0, &retained) == 1 && + retained.id == sentinel, "expired row map remained available"); + + require(tf_terminal_resize(terminal.get(), 2, 8, nullptr, 0) == 0, "second resize failed"); + TfResizeMapMeta next{}; + require(tf_terminal_resize_map_meta(terminal.get(), false, &next) == 0 && next.epoch > map.epoch, + "second resize reused the old epoch"); + require(tf_terminal_resize_source(terminal.get(), map.epoch, false, 0, &old) == 1, + "old source query read the new resize"); + require(tf_terminal_finish(terminal.get(), nullptr, 0) == 0 && + tf_terminal_resize_map_meta(terminal.get(), false, &next) == 1, + "finish left a stale map available"); + require(tf_terminal_resize(terminal.get(), 2, 8, nullptr, 0) == 0 && + tf_terminal_resize_map_meta(terminal.get(), false, &next) == 0, + "unchanged dimensions did not create a new identity map"); + require(tf_terminal_discard_history(terminal.get(), nullptr, 0, &discarded, nullptr, 0) == 0 && + tf_terminal_resize_map_meta(terminal.get(), false, &next) == 1, + "successful discard left a stale map available"); +} + +void active_alternate_keeps_primary_origins_and_empty_hard_line() { + std::unique_ptr terminal(tf_terminal_new(2, 4, 8), tf_terminal_free); + require(terminal != nullptr, "could not create candidate"); + const auto feed = [&](std::string_view input) { + require(tf_terminal_feed(terminal.get(), reinterpret_cast(input.data()), + input.size(), nullptr, 0) == 0, "buffer input failed"); + }; + feed("\r\nABCD"); + std::array primary{}; + for (size_t index = 0; index < primary.size(); ++index) + require(tf_terminal_row_meta(terminal.get(), index, &primary[index]) == 0, "primary source missing"); + require(primary[0].hard_break, "initial empty hard line missing"); + feed("\x1b[?1049h\x1b[H1234\r\nZ"); + std::array alternate{}; + for (size_t index = 0; index < alternate.size(); ++index) + require(tf_terminal_row_meta(terminal.get(), index, &alternate[index]) == 0, "alternate source missing"); + require(tf_terminal_resize(terminal.get(), 2, 2, nullptr, 0) == 0, "alternate-active resize failed"); + TfResizeMapMeta map{}; + require(tf_terminal_resize_map_meta(terminal.get(), false, &map) == 0 && + map.source_rows == 2 && map.target_rows == 3 && map.after_history == 1, + "inactive primary did not retain its reflow map"); + TfResizeRowMeta row{}; + require(tf_terminal_resize_row(terminal.get(), map.epoch, false, false, 0, &row) == 0 && + row.hard_boundary == primary[0].id && row.line == -1, + "empty primary hard boundary was lost or assigned to alternate"); + for (size_t index = 0; index < 2; ++index) { + TfResizeSpan span{}; + require(tf_terminal_resize_span(terminal.get(), map.epoch, false, false, index + 1, 0, &span) == 0 && + span.source_row == primary[1].id && span.source_column == index * 2 && + span.column == 0 && span.len == 2, "inactive primary split origin differs"); + } + TfResizeMapMeta alt_map{}; + require(tf_terminal_resize_map_meta(terminal.get(), true, &alt_map) == 0 && + alt_map.epoch == map.epoch && alt_map.target_rows == 2 && alt_map.after_history == 0, + "active alternate unexpectedly reflowed into history"); + TfResizeSpan span{}; + require(tf_terminal_resize_span(terminal.get(), map.epoch, true, false, 0, 0, &span) == 0 && + span.source_row == alternate[0].id && span.source_column == 0 && span.len == 2, + "alternate retained 12 origin differs"); + require(tf_terminal_resize_row(terminal.get(), map.epoch, true, true, 0, &row) == 0 && + row.removal_reason == 4 && row.hard_boundary == 0, + "alternate removed 34 was not a column crop"); + require(tf_terminal_resize_span(terminal.get(), map.epoch, true, true, 0, 0, &span) == 0 && + span.source_row == alternate[0].id && span.source_column == 2 && span.len == 2, + "alternate cropped 34 origin differs"); + feed("\x1b[?1049l"); + TfTerminalMeta restored{}; + require(tf_terminal_meta(terminal.get(), &restored) == 0 && restored.history == 1, + "primary history did not survive return from alternate"); +} +} // namespace + +int main() { + try { + capacity_mapping_and_lifetime(); + active_alternate_keeps_primary_origins_and_empty_hard_line(); + std::cout << "candidate resize map ABI checks passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From 2bccddb8e8796b3b244a7252bbf4e1390ee13dbb Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Wed, 28 Oct 2026 02:12:23 +0800 Subject: [PATCH 04/14] docs: preserve resize origin checks and first-grow observation limits (#29) --- docs/EVIDENCE.md | 2 + docs/FEATURES.md | 2 +- docs/research/windterm-resize-text.md | 23 + .../windterm-first-grow-input.json | 60 + .../windterm-first-grow-observations.json | 1049 +++++++++++++++++ .../2026-10-28-resize-origins/README.md | 32 + .../2026-10-28-resize-origins/validation.json | 123 ++ 7 files changed, 1290 insertions(+), 1 deletion(-) create mode 100644 experiments/core/results/2026-10-28-history-layout/windterm-first-grow-input.json create mode 100644 experiments/core/results/2026-10-28-history-layout/windterm-first-grow-observations.json create mode 100644 experiments/core/results/2026-10-28-resize-origins/README.md create mode 100644 experiments/core/results/2026-10-28-resize-origins/validation.json diff --git a/docs/EVIDENCE.md b/docs/EVIDENCE.md index 3cf52da1..07beaa35 100644 --- a/docs/EVIDENCE.md +++ b/docs/EVIDENCE.md @@ -40,6 +40,8 @@ R1 固定总出口的源码为 `ce9b24d`,早期 `2dc8d6d` / `9b16acc` 仍分 | 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、实际呈现或性能。 | | WindTerm 长历史行的视图折行(#29) | [逐字符观察与边界](research/windterm-history-layout.md)、[两次事实/哈希](../experiments/core/results/2026-10-28-history-layout/windterm-observations.json) | 独立的两次七阶段观察:185 字符软行由 80/80/25 变为 62/62/61,两条 77 字符硬行各变为 62/15;文档文本与位置不变,恢复宽度后投影恢复。仅 ASCII 文本坐标接口;覆盖写入、选择/Find、物理格及呈现仍未知。 | | 候选原生重排边界与未解决的源文本重复(#29) | [工作检查点与反例](../experiments/core/results/2026-10-28-reflow-metadata/README.md)、[来源与验证](../experiments/core/results/2026-10-28-reflow-metadata/validation.json) | `.7` 修复原生硬/软边界传递及缩宽容量计账;Rust 与 runner 通过,C++ 长行复制仍红。混宽历史、来源归属、选择/Find 及完整产品矩阵未完成。 | +| 候选一次 resize 来源映射与真实淘汰计数(#29) | [接口范围及检查点](../experiments/core/results/2026-10-28-resize-origins/README.md)、[源码/构建/日志身份](../experiments/core/results/2026-10-28-resize-origins/validation.json) | `.8` 导出双屏物理来源和硬边界,修复缩高来源漏记、多行滚动少计与扩高假计;Rust、C++ 映射 ABI 和 runner 通过,C++ 长行复制仍失败,历史合并尚未消费映射。未测性能。 | +| WindTerm 首次增高的前置条件停止(#29) | [独立观察与边界](research/windterm-resize-text.md#独立观察初始-18-行直接增高)、[合成输入](../experiments/core/results/2026-10-28-history-layout/windterm-first-grow-input.json)、[事实与哈希](../experiments/core/results/2026-10-28-history-layout/windterm-first-grow-observations.json) | 独立一次 18→26 行观察中未见编号历史进入非负 y;128 次有界查询不变,末尾空串为同一文末位置别名。按计划未发送 CUP/重绘,B 案例未运行,覆盖写入政策仍未知。 | | 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。 | | 进程生命周期和平台边界 | [ConPTY 探针](../experiments/conpty/README.md)、[跨环境记录](research/conpty-platform-check-2026-10-22.md)、[旧平台探针](../.github/workflows/conpty-compat.yml) | 受控后代、取消/超时和短批次回收;Windows 11/Server 2025 有固定记录,Server 2022 稳态句柄门槛仍失败。长期无泄漏未知。 | diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 1b819d25..f1942325 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -33,7 +33,7 @@ | 中文 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 和有界恢复。显式候选 `.7` 接入 `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。显式候选 `.7` 保留 [#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)仅证明固定输入保留及恢复尺寸后文本投影恢复;[逐字符历史观察](research/windterm-history-layout.md)另证明固定 ASCII 长行随宽度重新分段,未验证覆盖写入或交互。完整 reflow、child ack、实际呈现、系统/实验后端联合配置及性能分别验收;默认缩窄不可恢复 | R2 | +| 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)覆盖透明输入、主屏缓存归属及重复淘汰;`.8` 的[一次 resize 来源映射](../experiments/core/results/2026-10-28-resize-origins/README.md)及实际淘汰计数已有独立检查,尚未接入 C++ 历史合并 | [#29](https://github.com/JavaZeroo/termforge/issues/29) 的重绘后源历史重复、混宽历史视口及完整 resize/备用屏/滚动区交叉仍未完成;[原版两次文本观察](research/windterm-resize-text.md)仅证明固定输入保留及恢复尺寸后文本投影恢复;[逐字符历史观察](research/windterm-history-layout.md)另证明固定 ASCII 长行随宽度重新分段,未验证覆盖写入或交互。完整 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/research/windterm-resize-text.md b/docs/research/windterm-resize-text.md index 99310aae..c09fa9f1 100644 --- a/docs/research/windterm-resize-text.md +++ b/docs/research/windterm-resize-text.md @@ -31,3 +31,26 @@ 现有默认物理裁剪及候选缓存只描述本项目实现。原版上述文本保留事实要求继续调查当前跨宽度历史的表示和显示,不能用永久裁掉缓存文本来消除行宽不一致。修正候选 pending 快照与显式清空属于[独立不变量修复](../../experiments/core/results/2026-10-27-resize-snapshot/README.md),不等于本次原版观察已经验证这些候选行为。 后续独立的[长历史行逐字符观察](windterm-history-layout.md)补充了主动滚动和 80→62→80 列的视图分段证据。它保留本次原始结果,不扩大为覆盖写入、物理格或完整 reflow 的验证。 + +## 独立观察:初始 18 行直接增高 + +以下是另一输入与尺寸序列的一次 45 秒有界观察,不替代前述 42 条文本、五阶段、两次运行的事实。固定原版使用全新合成 profile 和 loopback Telnet,从 1230×679 外框开始;peer 收到 80×18 后发送 42 条短编号 ASCII 文本及无结尾换行的 `W29-TAIL`,共 43 条已知文本。正文 1070 字节,追加 DSR 查询后线上输入 1074 字节。[合成输入](../../experiments/core/results/2026-10-28-history-layout/windterm-first-grow-input.json)可重建正文及线上字节;[观察与原件哈希](../../experiments/core/results/2026-10-28-history-layout/windterm-first-grow-observations.json)记录完整有限查询范围。 + +| 项目 | 初始短视图 | 增高后 | +|---|---|---| +| 请求外框 | 1230×679 | 1230×887 | +| peer 实收 NAWS | 80×18 | 80×26 | +| visibleLineCount | 43 | 43 | +| screenLineCapacity | 18 | 26 | +| scrollTop | 25 | 25 | +| scrollbar value / max / page | 25 / 25 / 18 | 25 / 25 / 26 | +| selectionCaretP 文档位置 | 1016 | 1016 | +| 首个非负 y 的已知文本 | `W29-L0025\|KEEP-0025\|END` | 相同 | + +两阶段查询索引 0–127 的全部文本、文档边界和起止矩形完全一致。槽 0–42 与 43 条输入文本顺序相同;TAIL 位于槽 42,文档范围 1008–1016,起点矩形为 `[147,306,8,18]`,终点为 `[211,306,0,18]`。槽 43–127 的 85 次查询均返回空串,start=end=1016、visibleLine=42,起止矩形都为同一个 `[211,306,0,18]`。这些末尾位置别名不代表 85 条真实空逻辑行。128 槽的文档边界取值集合未增加;未暴露的空白文档位置和新增物理空白格仍不能据此判定,caret 文档位置也不解释为物理光标。 + +没有已知编号文本从负 y 进入增高后的非负 y 范围,`historical_entry` 为空。探针按冻结条件在 CUP 之前停止:只发送初始输入并收到一次 DSR 字节回复,CUP 未发,独立的 B 案例没有运行,ED2/重绘也未发。这是有效的前置条件停止记录,覆盖政策仍未知。 + +本次“初始 18 行直接增高”与前述不同输入、先缩高再恢复等动作下的历史回带具有不同条件;尚未分离输入和动作顺序的影响,不能合成为普遍的增高规则,也不能推断物理 grid。DSR 只作独立 peer 收到回复的字节处理栅栏,不表示呈现、ConPTY child 或物理光标状态;selection、Find、Unicode、性能及覆盖行为未验证。 + +本次原件、脚本和重建输入哈希已核对;Job 最终 active=0,peer 和 desktop/station 均已清理,profile 只有一个合成会话。公开材料仅含合成输入、白名单事实及哈希,没有私有路径、进程/站名、app/profile 或提取脚本。该研究输入不构成产品策略确认。 diff --git a/experiments/core/results/2026-10-28-history-layout/windterm-first-grow-input.json b/experiments/core/results/2026-10-28-history-layout/windterm-first-grow-input.json new file mode 100644 index 00000000..a8180048 --- /dev/null +++ b/experiments/core/results/2026-10-28-history-layout/windterm-first-grow-input.json @@ -0,0 +1,60 @@ +{ + "encoding": "ASCII", + "columns_at_payload_generation": 80, + "rows_at_payload_generation": 18, + "preamble_hex": "1b5b324a1b5b481b5b3f3768", + "line_separator_hex": "0d0a", + "final_text_newline": false, + "body_bytes": 1070, + "body_sha256": "c7efff3c2d919395cf3d499b0e5e005de514ae509a72bbcb46ff96c2d189758f", + "appended_dsr_query_hex": "1b5b366e", + "wire_bytes": 1074, + "wire_sha256": "1062aeca1d4be780c3a168844f05df98a6be447eb38384645d3ace85d0b82875", + "body_reconstruction": "bytes.fromhex(preamble_hex) + bytes.fromhex(line_separator_hex).join(line.encode(encoding) for line in logical_input_lines)", + "wire_reconstruction": "body + bytes.fromhex(appended_dsr_query_hex)", + "logical_input_lines": [ + "W29-L0000|KEEP-0000|END", + "W29-L0001|KEEP-0001|END", + "W29-L0002|KEEP-0002|END", + "W29-L0003|KEEP-0003|END", + "W29-L0004|KEEP-0004|END", + "W29-L0005|KEEP-0005|END", + "W29-L0006|KEEP-0006|END", + "W29-L0007|KEEP-0007|END", + "W29-L0008|KEEP-0008|END", + "W29-L0009|KEEP-0009|END", + "W29-L0010|KEEP-0010|END", + "W29-L0011|KEEP-0011|END", + "W29-L0012|KEEP-0012|END", + "W29-L0013|KEEP-0013|END", + "W29-L0014|KEEP-0014|END", + "W29-L0015|KEEP-0015|END", + "W29-L0016|KEEP-0016|END", + "W29-L0017|KEEP-0017|END", + "W29-L0018|KEEP-0018|END", + "W29-L0019|KEEP-0019|END", + "W29-L0020|KEEP-0020|END", + "W29-L0021|KEEP-0021|END", + "W29-L0022|KEEP-0022|END", + "W29-L0023|KEEP-0023|END", + "W29-L0024|KEEP-0024|END", + "W29-L0025|KEEP-0025|END", + "W29-L0026|KEEP-0026|END", + "W29-L0027|KEEP-0027|END", + "W29-L0028|KEEP-0028|END", + "W29-L0029|KEEP-0029|END", + "W29-L0030|KEEP-0030|END", + "W29-L0031|KEEP-0031|END", + "W29-L0032|KEEP-0032|END", + "W29-L0033|KEEP-0033|END", + "W29-L0034|KEEP-0034|END", + "W29-L0035|KEEP-0035|END", + "W29-L0036|KEEP-0036|END", + "W29-L0037|KEEP-0037|END", + "W29-L0038|KEEP-0038|END", + "W29-L0039|KEEP-0039|END", + "W29-L0040|KEEP-0040|END", + "W29-L0041|KEEP-0041|END", + "W29-TAIL" + ] +} diff --git a/experiments/core/results/2026-10-28-history-layout/windterm-first-grow-observations.json b/experiments/core/results/2026-10-28-history-layout/windterm-first-grow-observations.json new file mode 100644 index 00000000..1e5ddb50 --- /dev/null +++ b/experiments/core/results/2026-10-28-history-layout/windterm-first-grow-observations.json @@ -0,0 +1,1049 @@ +{ + "scope": "Independent first-grow research observation; valid planned precondition stop, not a product policy or acceptance result.", + "observed_version": "WindTerm 2.7.0 Windows x64", + "os": "Windows 11 Pro 10.0.26200 x64", + "transport": "Synthetic loopback Telnet; actual dimensions from the owned peer NAWS.", + "actual_target_runs": 1, + "case_a_runs": 1, + "case_b_runs": 0, + "instrumentation_limit_seconds": 45, + "observed_phases": 2, + "stop_reason": "no-new-historical-text-in-view", + "historical_entry_markers": [], + "cup_sent": false, + "ed2_sent": false, + "redraw_sent": false, + "target_sha256": "7b10531dccc7cd614ae559f36f1ef518e0c96335637431b4646354823b2804d0", + "distribution_files_previously_verified": 4047, + "distribution_manifest_sha256": "e459adf2f12adb6a1b7f55e66a9da1c22794185e66e848057d21e0c60d669203", + "probe_sha256": "06838cb7014bf27f4a95f7c0b355dc2f7cd1f3bcb74a6296f5035d4dfa44da52", + "probe_template_sha256": "0ee2e293b55bbbc8b94417881654870fdbba02c206cbde44d03df1a07c556b51", + "peer_script_sha256": "e094a504f450696a55a2bd3ce22ffb2878b14ed900af7be6d4da6165e102213c", + "frozen_plan_sha256": "3e59a253cdb0ee431de99e96c3ac9f724d95cfcd000ad5078428f1eca6d43076", + "evidence_id": "ev_77c41a98da715ac7a7d5f0d9e3432bc932c61bf5b177d3aa74288390f4d5d9dc", + "body_sha256": "c7efff3c2d919395cf3d499b0e5e005de514ae509a72bbcb46ff96c2d189758f", + "wire_sha256": "1062aeca1d4be780c3a168844f05df98a6be447eb38384645d3ace85d0b82875", + "input_reconstructed_exactly": true, + "only_initial_input_sent": true, + "dsr_fence": { + "query_hex": "1b5b366e", + "reply_hex": "1b5b31383b3952", + "replies": 1, + "meaning": "Peer-received reply bytes used only as an initial-input processing fence; no physical cursor or presentation interpretation." + }, + "phases": [ + { + "phase": "short-bottom", + "requested_outer": [ + 1230, + 679 + ], + "naws": [ + 80, + 18 + ], + "visibleLineCount": 43, + "screenLineCapacity": 18, + "scrollTop": 25, + "scrollbar": { + "value": 25, + "max": 25, + "page": 18 + }, + "selectionCaretP_document_position": 1016, + "queried_index_range_inclusive": [ + 0, + 127 + ], + "known_nonempty_texts": 43, + "query_slots_sha256": "0559e7ce193ee5163c7d410f980a657fd55b34ef4742709878216594a1e71e57" + }, + { + "phase": "grown-bottom", + "requested_outer": [ + 1230, + 887 + ], + "naws": [ + 80, + 26 + ], + "visibleLineCount": 43, + "screenLineCapacity": 26, + "scrollTop": 25, + "scrollbar": { + "value": 25, + "max": 25, + "page": 26 + }, + "selectionCaretP_document_position": 1016, + "queried_index_range_inclusive": [ + 0, + 127 + ], + "known_nonempty_texts": 43, + "query_slots_sha256": "0559e7ce193ee5163c7d410f980a657fd55b34ef4742709878216594a1e71e57" + } + ], + "all_128_query_slots_text_positions_and_coordinates_identical": true, + "common_known_slot_columns": [ + "queried_index", + "document_start", + "document_end", + "visibleLine", + "startRect_xywh", + "endRect_xywh" + ], + "common_known_slots": [ + [ + 0, + 0, + 23, + 0, + [ + 147, + -450, + 8, + 18 + ], + [ + 331, + -450, + 17, + 18 + ] + ], + [ + 1, + 24, + 47, + 1, + [ + 147, + -432, + 8, + 18 + ], + [ + 331, + -432, + 17, + 18 + ] + ], + [ + 2, + 48, + 71, + 2, + [ + 147, + -414, + 8, + 18 + ], + [ + 331, + -414, + 17, + 18 + ] + ], + [ + 3, + 72, + 95, + 3, + [ + 147, + -396, + 8, + 18 + ], + [ + 331, + -396, + 17, + 18 + ] + ], + [ + 4, + 96, + 119, + 4, + [ + 147, + -378, + 8, + 18 + ], + [ + 331, + -378, + 17, + 18 + ] + ], + [ + 5, + 120, + 143, + 5, + [ + 147, + -360, + 8, + 18 + ], + [ + 331, + -360, + 17, + 18 + ] + ], + [ + 6, + 144, + 167, + 6, + [ + 147, + -342, + 8, + 18 + ], + [ + 331, + -342, + 17, + 18 + ] + ], + [ + 7, + 168, + 191, + 7, + [ + 147, + -324, + 8, + 18 + ], + [ + 331, + -324, + 17, + 18 + ] + ], + [ + 8, + 192, + 215, + 8, + [ + 147, + -306, + 8, + 18 + ], + [ + 331, + -306, + 17, + 18 + ] + ], + [ + 9, + 216, + 239, + 9, + [ + 147, + -288, + 8, + 18 + ], + [ + 331, + -288, + 17, + 18 + ] + ], + [ + 10, + 240, + 263, + 10, + [ + 147, + -270, + 8, + 18 + ], + [ + 331, + -270, + 17, + 18 + ] + ], + [ + 11, + 264, + 287, + 11, + [ + 147, + -252, + 8, + 18 + ], + [ + 331, + -252, + 17, + 18 + ] + ], + [ + 12, + 288, + 311, + 12, + [ + 147, + -234, + 8, + 18 + ], + [ + 331, + -234, + 17, + 18 + ] + ], + [ + 13, + 312, + 335, + 13, + [ + 147, + -216, + 8, + 18 + ], + [ + 331, + -216, + 17, + 18 + ] + ], + [ + 14, + 336, + 359, + 14, + [ + 147, + -198, + 8, + 18 + ], + [ + 331, + -198, + 17, + 18 + ] + ], + [ + 15, + 360, + 383, + 15, + [ + 147, + -180, + 8, + 18 + ], + [ + 331, + -180, + 17, + 18 + ] + ], + [ + 16, + 384, + 407, + 16, + [ + 147, + -162, + 8, + 18 + ], + [ + 331, + -162, + 17, + 18 + ] + ], + [ + 17, + 408, + 431, + 17, + [ + 147, + -144, + 8, + 18 + ], + [ + 331, + -144, + 17, + 18 + ] + ], + [ + 18, + 432, + 455, + 18, + [ + 147, + -126, + 8, + 18 + ], + [ + 331, + -126, + 17, + 18 + ] + ], + [ + 19, + 456, + 479, + 19, + [ + 147, + -108, + 8, + 18 + ], + [ + 331, + -108, + 17, + 18 + ] + ], + [ + 20, + 480, + 503, + 20, + [ + 147, + -90, + 8, + 18 + ], + [ + 331, + -90, + 17, + 18 + ] + ], + [ + 21, + 504, + 527, + 21, + [ + 147, + -72, + 8, + 18 + ], + [ + 331, + -72, + 17, + 18 + ] + ], + [ + 22, + 528, + 551, + 22, + [ + 147, + -54, + 8, + 18 + ], + [ + 331, + -54, + 17, + 18 + ] + ], + [ + 23, + 552, + 575, + 23, + [ + 147, + -36, + 8, + 18 + ], + [ + 331, + -36, + 17, + 18 + ] + ], + [ + 24, + 576, + 599, + 24, + [ + 147, + -18, + 8, + 18 + ], + [ + 331, + -18, + 17, + 18 + ] + ], + [ + 25, + 600, + 623, + 25, + [ + 147, + 0, + 8, + 18 + ], + [ + 331, + 0, + 17, + 18 + ] + ], + [ + 26, + 624, + 647, + 26, + [ + 147, + 18, + 8, + 18 + ], + [ + 331, + 18, + 17, + 18 + ] + ], + [ + 27, + 648, + 671, + 27, + [ + 147, + 36, + 8, + 18 + ], + [ + 331, + 36, + 17, + 18 + ] + ], + [ + 28, + 672, + 695, + 28, + [ + 147, + 54, + 8, + 18 + ], + [ + 331, + 54, + 17, + 18 + ] + ], + [ + 29, + 696, + 719, + 29, + [ + 147, + 72, + 8, + 18 + ], + [ + 331, + 72, + 17, + 18 + ] + ], + [ + 30, + 720, + 743, + 30, + [ + 147, + 90, + 8, + 18 + ], + [ + 331, + 90, + 17, + 18 + ] + ], + [ + 31, + 744, + 767, + 31, + [ + 147, + 108, + 8, + 18 + ], + [ + 331, + 108, + 17, + 18 + ] + ], + [ + 32, + 768, + 791, + 32, + [ + 147, + 126, + 8, + 18 + ], + [ + 331, + 126, + 17, + 18 + ] + ], + [ + 33, + 792, + 815, + 33, + [ + 147, + 144, + 8, + 18 + ], + [ + 331, + 144, + 17, + 18 + ] + ], + [ + 34, + 816, + 839, + 34, + [ + 147, + 162, + 8, + 18 + ], + [ + 331, + 162, + 17, + 18 + ] + ], + [ + 35, + 840, + 863, + 35, + [ + 147, + 180, + 8, + 18 + ], + [ + 331, + 180, + 17, + 18 + ] + ], + [ + 36, + 864, + 887, + 36, + [ + 147, + 198, + 8, + 18 + ], + [ + 331, + 198, + 17, + 18 + ] + ], + [ + 37, + 888, + 911, + 37, + [ + 147, + 216, + 8, + 18 + ], + [ + 331, + 216, + 17, + 18 + ] + ], + [ + 38, + 912, + 935, + 38, + [ + 147, + 234, + 8, + 18 + ], + [ + 331, + 234, + 17, + 18 + ] + ], + [ + 39, + 936, + 959, + 39, + [ + 147, + 252, + 8, + 18 + ], + [ + 331, + 252, + 17, + 18 + ] + ], + [ + 40, + 960, + 983, + 40, + [ + 147, + 270, + 8, + 18 + ], + [ + 331, + 270, + 17, + 18 + ] + ], + [ + 41, + 984, + 1007, + 41, + [ + 147, + 288, + 8, + 18 + ], + [ + 331, + 288, + 17, + 18 + ] + ], + [ + 42, + 1008, + 1016, + 42, + [ + 147, + 306, + 8, + 18 + ], + [ + 211, + 306, + 0, + 18 + ] + ] + ], + "known_slot_texts": "For indices 0..42, exact text equals logical_input_lines[index] in windterm-first-grow-input.json.", + "tail_common": { + "index": 42, + "text": "W29-TAIL", + "start": 1008, + "end": 1016, + "visibleLine": 42, + "startRect": [ + 147, + 306, + 8, + 18 + ], + "endRect": [ + 211, + 306, + 0, + 18 + ] + }, + "empty_end_aliases_common": { + "queried_index_range_inclusive": [ + 43, + 127 + ], + "query_count": 85, + "text": "", + "start": 1016, + "end": 1016, + "visibleLine": 42, + "startRect": [ + 211, + 306, + 0, + 18 + ], + "endRect": [ + 211, + 306, + 0, + 18 + ], + "interpretation": "Identical empty end-position returns; not a count of actual empty logical lines." + }, + "queried_document_boundary_min": 0, + "queried_document_boundary_max": 1016, + "distinct_queried_document_boundaries": [ + 0, + 23, + 24, + 47, + 48, + 71, + 72, + 95, + 96, + 119, + 120, + 143, + 144, + 167, + 168, + 191, + 192, + 215, + 216, + 239, + 240, + 263, + 264, + 287, + 288, + 311, + 312, + 335, + 336, + 359, + 360, + 383, + 384, + 407, + 408, + 431, + 432, + 455, + 456, + 479, + 480, + 503, + 504, + 527, + 528, + 551, + 552, + 575, + 576, + 599, + 600, + 623, + 624, + 647, + 648, + 671, + 672, + 695, + 696, + 719, + 720, + 743, + 744, + 767, + 768, + 791, + 792, + 815, + 816, + 839, + 840, + 863, + 864, + 887, + 888, + 911, + 912, + 935, + 936, + 959, + 960, + 983, + 984, + 1007, + 1008, + 1016 + ], + "new_queried_document_boundaries_after_grow": [], + "blank_position_observation": "No new document boundary appeared in these finite queries. Physical blank cells or unexposed blank document positions remain unknown.", + "first_nonnegative_known_text_common": { + "index": 25, + "text": "W29-L0025|KEEP-0025|END", + "start": 600, + "end": 623, + "startRect": [ + 147, + 0, + 8, + 18 + ] + }, + "cleanup": { + "job_active": 0, + "job_total": 3, + "peer_closed": true, + "desktop_handle_closed": true, + "station_handle_closed": true, + "original_station_restored": true, + "only_synthetic_session": true + }, + "private_artifact_sha256": { + "evidence.json": "e2ba68900780556620da2579201eb5f9ff9a30d7afb9c54fed85f439405815fa", + "run.json": "98c2650cef41806f4c3f60aab287325db2ee24b2c6c21ea2a29f9c21e6d6177c", + "files.json": "a25c87cba935294d5bfd3bbc76c650d31a8473809564d798ad5a889e4aea00e8", + "probe.js": "06838cb7014bf27f4a95f7c0b355dc2f7cd1f3bcb74a6296f5035d4dfa44da52", + "input-00.bin": "1062aeca1d4be780c3a168844f05df98a6be447eb38384645d3ace85d0b82875", + "input-00.json": "8a383788bbd36b772ab2530ed283aa647592eefc0edafe37e8230e4c4c4268a2", + "peer.json": "ed2fb8f42deb6b632ae41feddf03a6b794e116b2f7d0952a32e306eef2f68ca2", + "cleanup.json": "8400f93b6a4a4a50cde35b5d474660590ed215732f55772a9d8afac138da4f1d", + "surface-cleanup.json": "2947878016d582f50a3241f5dfaf0c923671e6ddee62cd57e00a78a988ae7954", + "profile-check.json": "5e522a64e98197722ccd084639b1ce50258b238e03be2f016e81c8af023b5ee1" + }, + "limitations": [ + "Synthetic text labels are content markers, not native stable IDs.", + "Different input and resize history from the earlier shrink-and-restore observation; no general grow rule follows.", + "No CUP, ED2 or redraw was sent, so overwrite policy remains unknown.", + "No physical grid/cursor, presented frames, ConPTY child acknowledgement or performance conclusion.", + "No selection preservation, copy, Find, Unicode or complete VT reflow validation." + ] +} diff --git a/experiments/core/results/2026-10-28-resize-origins/README.md b/experiments/core/results/2026-10-28-resize-origins/README.md new file mode 100644 index 00000000..cc4b408e --- /dev/null +++ b/experiments/core/results/2026-10-28-resize-origins/README.md @@ -0,0 +1,32 @@ +# 一次 resize 来源映射与容量计账检查点 + +源码 `66f1942324060e56974d85ebad3bff04749e7395`,Git tree `7dd044c0cb8d2499a747f5abc70eb1b5d665d08a`;候选 `0.26.1-dev+termforge.8`。这是 #29 的实验基础,**C++ 长历史复制回归仍失败,完整 #29/R2 未完成**。[命令、日志哈希和构建产物](validation.json)分别记录通过与失败,未运行完整产品配置。 + +## 实际修复与检查 + +- 同步 resize 在主/备用屏分别导出旧行、目标行、来源区间及移除片段。来源使用旧行 ID 和物理列,不按相同文字去重;硬换行独立保存。宽字单列 footprint、合并和空硬行也保留各自来源边界。 +- 高度 4×3、历史容量 1,输入 `ABCDEFGH\r\nIJKLMNOP\r\nQRSTUVWX` 后缩至 1×1。来源检查最初只报告 4 条移除,遗漏原可见区的两行及硬边界;修复后记录 6 条,最终保留 W/X。后续计数修复使原生累计淘汰从 4 增至 10。 +- 高度 3×1、历史容量 1,输入 `A\r\nB\r\nC` 后缩至 1×1。实际 A 被容量淘汰,原计数却保持 0;现为 1。普通 `CSI 2S` 同样正确计数;扩高搬移合成空行不增加淘汰,有足够历史容量也不计损失,容量 0 的普通输出滚动仍计真实淘汰。 +- C++ 直接调用新 C ABI,检查容量片段、字段布局、非法请求不改输出、epoch 及 feed/finish/discard/resize 失效;备用屏激活时主屏仍独立重排,空硬行保留,备用屏列裁剪不混入主历史。 + +拆分边界与高度来源均有保存的先失败后通过记录。合并用例首次运行时实现已到位,因此保留初次通过,并以临时移除 grow 映射的负对照验证测试敏感性;不描述为严格的先红后绿。计数测试中曾把未写 cell 的空 `cell_text` 误写为 ASCII 空格,原失败单独保留,修正的是测试表示,不是降低淘汰判据。 + +## 本源码的验证结果 + +| 检查 | 结果 | 边界 | +|---|---|---| +| Rust workspace | 163 vendor、10 FFI、2 measure、2 trace、1 doctest 全通过 | 包含真实 feed/resize 与新增容量计数合同 | +| C++ `candidate_resize_map` | 通过 | Release 静态库真实跨语言调用 | +| C++ `candidate_core` | 失败 | 停在 whole-feed 长历史复制断言;后续 bytewise 分支未执行 | +| 重建 trace 后 Python runner | 6/6 通过 | 使用当前 `.8` trace,不代表 GUI 或原版对比 | +| serde feature check | 通过 | 临时追踪字段不参与序列化 | + +10,800 次有界来源诊断及另一次 960 次连续 resize 诊断为补充研究;它们在最终计数修正前执行,未与上述最终测试拼成一次验收。原始日志留在本地 `local/r29-origin-map`,公开文件只含合成案例、摘要及哈希。 + +固定 `66f1942` 的 Standards 与 Spec 两轴审查均为 0 项新增问题。Spec 另做 480 次有界诊断,覆盖光标覆盖、插删字符/行、滚动区域、SU/SD、擦除、样式空格和 Unicode;来源完整性/唯一性、硬边界与映射文字/颜色一致性通过,日志哈希另列。该审查不关闭已知的 C++ 历史归属缺口。 + +## 尚未闭合 + +映射只描述一次 resize,后续写入前失效;它不是持续写入来源追踪。现有 C++ 历史合并还没有使用此映射,长行重复、空硬行丢失、复制/Find 和视口坐标仍需一起修复。未运行本源码的完整 shell/TUI、系统剪贴板/IME、真实不同 DPI 或实验后端套件;未测新增分配和存储成本,不声明原版等价。 + +另一次[原版首次增高观察](../../../../docs/research/windterm-resize-text.md#独立观察初始-18-行直接增高)按前置条件停止,未发送 CUP 或重绘,覆盖写入政策仍未知。它与本次独立来源映射实现分开留证。 diff --git a/experiments/core/results/2026-10-28-resize-origins/validation.json b/experiments/core/results/2026-10-28-resize-origins/validation.json new file mode 100644 index 00000000..2a16f198 --- /dev/null +++ b/experiments/core/results/2026-10-28-resize-origins/validation.json @@ -0,0 +1,123 @@ +{ + "schema_version": 1, + "scope": "Experimental candidate resize origin map and real scroll eviction accounting; not complete #29/R2 acceptance", + "source_commit": "66f1942324060e56974d85ebad3bff04749e7395", + "source_tree": "7dd044c0cb8d2499a747f5abc70eb1b5d665d08a", + "candidate_version": "0.26.1-dev+termforge.8", + "configuration": "Windows x64, explicit candidate adapter, system backend; selected native core checks only", + "environment": { + "os": "Windows-11-10.0.26200-SP0", + "rust_toolchain": "1.92.0", + "cmake_configuration": "Release" + }, + "source_identity_note": "Tests ran on the equivalent pre-commit Git contents; resize.rs line endings were normalized before the final release rebuild. No code changed after those builds.", + "checks": [ + { + "command": "cargo test --workspace --locked", + "exit_code": 0, + "result": "163 vendor + 10 FFI + 2 measure + 2 trace + 1 doctest passed", + "local_log": "local/r29-origin-map/workspace-final.log", + "log_sha256": "902c5005a11eb58ca1794ac88cea4565bc2f0f22509627fb2b3f5e514822d674" + }, + { + "command": "ctest --test-dir build/candidate-adapter -C Release -R '^candidate_(core|resize_map)$' --output-on-failure", + "exit_code": 8, + "result": "candidate_resize_map passed; candidate_core failed at existing whole-feed history-copy regression", + "local_log": "local/r29-origin-map/ctest-final.log", + "log_sha256": "d2ea20fca33b393d32aacb7f04fbc55830477b289fea60525a03eed28a9730a5" + }, + { + "command": "python -m unittest experiments.candidates.alacritty.test_candidate -v", + "exit_code": 0, + "result": "6 passed against rebuilt trace", + "local_log": "local/r29-origin-map/runner-final.log", + "log_sha256": "c013c10660dccd5fae887773ca8c6e7d366e974f6abe4b9325419ba8350fff83" + }, + { + "command": "cargo check --locked -p alacritty_terminal --features serde", + "exit_code": 0, + "result": "passed", + "local_log": "local/r29-origin-map/serde-final.log", + "log_sha256": "caf1b09d20c09a89573134e3929e071fd25a7001ef4fdd533af832b224709389" + } + ], + "red_green": [ + { + "kind": "initial failing split boundary", + "local_log": "local/r29-origin-map/split-red.log", + "sha256": "69a7633e9a4cc0bc741ac7a6355541dfe063927029d383baa1e8292d6917f71c" + }, + { + "kind": "split corrected", + "local_log": "local/r29-origin-map/split-green.log", + "sha256": "a1f0a4bf61635d1a73c2e84ccd079012df9b26a1154fda39c4a590e876f9c59f" + }, + { + "kind": "initial merge check already green", + "local_log": "local/r29-origin-map/merge-initial-green.log", + "sha256": "43102717505ede280f98774f560bd3c99c70d0cc5ba6d242a7eb4dd85fce9cbf" + }, + { + "kind": "deliberate removal of mapped grow showed failing merge", + "local_log": "local/r29-origin-map/merge-negative-control.log", + "sha256": "248cdcf5483d46ac0a76027edcee9281a54dfc961c69ceea252e37b0baa96250" + }, + { + "kind": "restored implementation passed split and merge", + "local_log": "local/r29-origin-map/split-merge-green.log", + "sha256": "e5c920c340e234f671475b6395cddd27b4ad49f232fdddd99b9436d379275c28" + }, + { + "kind": "height capture missing two rows", + "local_log": "local/r29-origin-map/height-red.log", + "sha256": "7d7ad14538215e5fb0ace7ee9f3a0158dce2cbaf401de945587b3fd9f41c228e" + }, + { + "kind": "height capture corrected", + "local_log": "local/r29-origin-map/height-green.log", + "sha256": "feee2cf5314cd934cca3d545349bfef38494e35c23fb65a1514a1ad64b8934d6" + }, + { + "kind": "actual eviction counted as zero", + "local_log": "local/r29-origin-map/counter-red.log", + "sha256": "d85a403dca7aa310d9bf6d3e4b84556e85cb94aa461a1482f9325bb489c1e428" + }, + { + "kind": "test fixture correction: unwritten cell_text is empty, not an ASCII space", + "local_log": "local/r29-origin-map/counter-content-oracle-failure.log", + "sha256": "fb0e6e2494e11d4d34830af52c112e2e2013e99a9f71b050188251bd2dcb13cc" + }, + { + "kind": "counter behavior and corrected empty-text fixture passed", + "local_log": "local/r29-origin-map/counter-green.log", + "sha256": "22e323531320780380ee3a699ba17696997ae8c3a0e3e1995cfb849ef2674905" + } + ], + "artifacts": { + "build/candidate-adapter/bin/Release/candidate_resize_map_test.exe": "3ab0ba0e2355b6e3862a7a253dbfae50c71347b89c4a33a5de88f884f6137e51", + "build/candidate-adapter/bin/Release/candidate_core_test.exe": "ff5d02828cb5ad0a971aee23c9eac9cb9ff07e6c8a0fd6f50b82a62d80b6aa8a", + "experiments/candidates/alacritty/target/release/termforge_alacritty_core.lib": "09b97ccccefaf0e452d35ad55890b46108d922a22f123f2b238b83430fc7ac1e", + "experiments/candidates/alacritty/target/release/termforge-alacritty-trace.exe": "c98df74fac5a222a27a6fc71eeaf2ade8691c0fadb43f882a076065690b9e833" + }, + "limitations": [ + "C++ cached history does not consume the new origins yet; source duplication, blank hard-line loss, Find and viewport ownership remain unresolved.", + "candidate_core stops at its first whole-feed assertion; later bytewise cases did not execute in that run.", + "No full shell/TUI/clipboard/IME/DPI/experimental backend suite was run for this source.", + "No performance or WindTerm equality measurement; temporary origins allocate by physical row and fragment.", + "One-shot mapping expires before writes and cannot establish partial-overwrite provenance.", + "The 10800-transaction and 960-continuous-resize diagnostics preceded the final counter correction; they are supplemental observations, not a combined final acceptance." + ], + "reviews": { + "base": "a8fd664a27e208cecff7d4c24a3c3915cc4ff86b", + "target": "66f1942324060e56974d85ebad3bff04749e7395", + "standards_findings": 0, + "spec_findings": 0, + "scope": "Read-only two-axis review of the fixed implementation commit; known incomplete C++ history ownership remains open." + }, + "supplemental_final_source_check": { + "transactions": 480, + "scope": "Bounded origin completeness/uniqueness, hard boundaries and cell text/colors after cursor, erase, margin, insert/delete and scroll mutations; not product parity", + "local_log": "local/r29-origins-spec/completeness.log", + "sha256": "161585b3df11d7ae1ece4071a6f171ebc7ceeeddef9a8bc0915349fe7892cb9a" + } +} From 5cb61a84ec2a3a8e494c7970fc1c65985002b0f7 Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Wed, 28 Oct 2026 02:46:55 +0800 Subject: [PATCH 05/14] wip: trace surviving cell origins through parser feeds (#29) --- docs/EVIDENCE.md | 2 +- docs/FEATURES.md | 2 +- docs/PROVENANCE.md | 2 +- experiments/candidates/alacritty/Cargo.lock | 2 +- .../candidates/alacritty/PROVENANCE.md | 10 +- experiments/candidates/alacritty/README.md | 2 +- .../alacritty/include/termforge_candidate.h | 21 + experiments/candidates/alacritty/run.py | 2 +- experiments/candidates/alacritty/src/ffi.rs | 147 ++++-- .../candidates/alacritty/test_candidate.py | 2 +- .../alacritty/tests/feed_origins.rs | 485 ++++++++++++++++++ .../vendor/alacritty_terminal/Cargo.toml | 2 +- .../vendor/alacritty_terminal/src/grid/mod.rs | 14 +- .../alacritty_terminal/src/grid/origins.rs | 54 +- .../vendor/alacritty_terminal/src/grid/row.rs | 28 +- .../alacritty_terminal/src/term/grapheme.rs | 14 +- .../vendor/alacritty_terminal/src/term/mod.rs | 85 ++- experiments/core/CANDIDATE.md | 10 +- .../core/candidate_resize_map_test.cpp | 55 ++ 19 files changed, 873 insertions(+), 66 deletions(-) create mode 100644 experiments/candidates/alacritty/tests/feed_origins.rs diff --git a/docs/EVIDENCE.md b/docs/EVIDENCE.md index 07beaa35..035055f9 100644 --- a/docs/EVIDENCE.md +++ b/docs/EVIDENCE.md @@ -9,7 +9,7 @@ | 配置 | 证据定位 | |---|---| | main 默认:自研核心 + 系统 ConPTY | R1 限定总验收和有限 VT/Unicode 子集;默认构建不需要 Rust | -| 当前源码显式候选:`termforge.8` + 系统 ConPTY | [固定来源/补丁台账](../experiments/candidates/alacritty/PROVENANCE.md)、[适配边界](../experiments/core/CANDIDATE.md)、[#39 字素合同](research/streaming-graphemes.md);历史限定结果保留,长行复制回归仍红 | +| 当前源码显式候选:`termforge.9` + 系统 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、绘制提交和真实呈现是不同检查点;未知端点保持未知。 diff --git a/docs/FEATURES.md b/docs/FEATURES.md index f1942325..2950cde5 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -13,7 +13,7 @@ | 配置 | 当前定位 | 可以引用的证据与限制 | |---|---|---| | 默认 main:自研核心 + 系统 ConPTY | 默认构建与已发布 Alpha 的基础路线,不需要 Rust | 已有 R1 限定验收;VT/Unicode 能力仍为有限子集,不能引用候选核心的字素或 TUI 结果 | -| 当前源码显式候选:`termforge.8` + 系统 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.9` + 系统 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/拼音验收属于固定自研核心配置,不外推到当前候选 或实验后端联合配置。 diff --git a/docs/PROVENANCE.md b/docs/PROVENANCE.md index 56a65ce3..c6b048bb 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.8`(Apache-2.0),固定上游 commit `542147067dd016fc5f49120ef0ba605a98a4e3a7` 的 manifest/changelog/许可证与 `src/`,并保留 ED、稳定行身份/淘汰、安全 swap、每缓冲区滚动区域、流式字素、显式历史清空代次、原生历史前缀淘汰、重排断行/容量计账及一次 resize 来源映射补丁;精确传递依赖及校验值由 `Cargo.lock` 固定。未复制上游 46 MiB 行为录制,候选不进入当前产品包;默认 CMake 不依赖 Rust,显式适配器构建使用 staticlib。原始 0.26.0 身份、当前来源、许可证和补丁清单见[候选来源台账](../experiments/candidates/alacritty/PROVENANCE.md)。 +- 外部核心候选当前使用 `alacritty_terminal 0.26.1-dev+termforge.9`(Apache-2.0),固定上游 commit `542147067dd016fc5f49120ef0ba605a98a4e3a7` 的 manifest/changelog/许可证与 `src/`,并保留 ED、稳定行身份/淘汰、安全 swap、每缓冲区滚动区域、流式字素、显式历史清空代次、原生历史前缀淘汰、重排断行/容量计账及一次 resize 来源映射补丁;精确传递依赖及校验值由 `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/experiments/candidates/alacritty/Cargo.lock b/experiments/candidates/alacritty/Cargo.lock index 3ac4d722..095e75f4 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.8" +version = "0.26.1-dev+termforge.9" dependencies = [ "arrayvec", "base64", diff --git a/experiments/candidates/alacritty/PROVENANCE.md b/experiments/candidates/alacritty/PROVENANCE.md index 63b0d696..52d76748 100644 --- a/experiments/candidates/alacritty/PROVENANCE.md +++ b/experiments/candidates/alacritty/PROVENANCE.md @@ -1,6 +1,6 @@ # 固定候选来源与本地补丁 -当前适配版本为 `alacritty_terminal 0.26.1-dev+termforge.8`,不是 crates.io 原始 `0.26.0`。 +当前适配版本为 `alacritty_terminal 0.26.1-dev+termforge.9`,不是 crates.io 原始 `0.26.0`。 ## 上游身份 @@ -80,6 +80,14 @@ 3. 修复高度缩小一次滚动超过旧历史数量时的来源漏记:把同步移出容量的原可见行也记录为移除,保留其硬边界。普通顶部滚动按实际超容量行数计账,修复多行滚动少计;扩高为搬移合成空行而执行的滚动不计淘汰。公开 FFI 反例和 C++ 静态库调用覆盖来源、计数、生命周期及双屏命名。 4. 升为 `0.26.1-dev+termforge.8`;固定上游、注册表依赖和许可证未变。映射补丁独立实现,没有引入 WindTerm 私有代码或资源。C++ 历史缓存的源文本重复与空硬行丢失仍未修复,性能成本未测,完整 #29/R2 保持未完成。 +## termforge.9 补丁 + +1. `Term::feed_with_origin_map` 复用临时稀疏来源表,追踪一次真实 parser feed 后仍存活的旧物理格和硬边界。实际覆盖、擦除及字素续接会使相应旧来源失效,即使新旧文字相同;插入、删除和滚动按实际移动传递来源。普通 feed 不分配来源表,不将可变借用误当写入。 +2. 不封闭跨 feed 的待续字素;主/备用屏按事务前后真实切换正确命名。公开 Rust 集成测试核对同字重写、宽格关联清除、字素增长/缩减、插删擦除、硬边界、ALT/RIS 及后续 resize,不依赖内部 helper。 +3. 修正非底行自动软换行后残留的硬边界元数据;文字、格内容、光标和原有 WRAPLINE 行为不变。C ABI 新增显式 `tf_terminal_feed_mapped` 和 feed 查询,复用物理来源记录布局,与 resize 共用单调 epoch 并拒绝错误操作类型或过期查询。 +4. 移除记录是事务中的事件,不是完整擦除/先后顺序日志,也不授予外部历史保留许可。同一 feed 先淘汰、后 ED3/RIS、再输出时,最终清空代次不能区分清空前后的移除事件;调用方不能先清空缓存再无条件补回全部移除片段。 +5. 升为 `0.26.1-dev+termforge.9`;固定上游、依赖和许可证不变。新增分支及映射分配成本未测;C++ 历史合并尚未消费来源映射,已知长历史复制失败与完整 #29/R2 验收继续开放。 + ## 历史基线 #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 74286176..87719fa9 100644 --- a/experiments/candidates/alacritty/README.md +++ b/experiments/candidates/alacritty/README.md @@ -6,7 +6,7 @@ | 项目 | 当前固定值 | 来源 | |---|---|---| -| `alacritty_terminal` | `0.26.1-dev+termforge.8`,Apache-2.0 | [上游 commit `5421470`](https://github.com/alacritty/alacritty/commit/542147067dd016fc5f49120ef0ba605a98a4e3a7)与[来源/补丁台账](PROVENANCE.md) | +| `alacritty_terminal` | `0.26.1-dev+termforge.9`,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 d160c31b..61c9e8fb 100644 --- a/experiments/candidates/alacritty/include/termforge_candidate.h +++ b/experiments/candidates/alacritty/include/termforge_candidate.h @@ -64,6 +64,11 @@ typedef struct TfResizeSpan { void* tf_terminal_new(size_t rows, size_t columns, size_t history); void tf_terminal_free(void* terminal); int tf_terminal_feed(void* terminal, const uint8_t* data, size_t len, uint8_t* error, size_t error_capacity); +// Opt-in feed transaction tracking. New writes/erases have no old-cell origin, +// even when bytes are unchanged; surviving cells carry their old row/column. +// Uses the same physical-source record layout as resize, through feed queries. +// Does not seal a pending grapheme. Ordinary feed performs no origin capture. +int tf_terminal_feed_mapped(void* terminal, const uint8_t* data, size_t len, uint8_t* error, size_t error_capacity); 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); // Owned-value queries for the latest resize only. alternate selects the named @@ -81,6 +86,22 @@ int tf_terminal_resize_row(void* terminal, uint64_t epoch, bool alternate, bool size_t index, TfResizeRowMeta* output); int tf_terminal_resize_span(void* terminal, uint64_t epoch, bool alternate, bool removed, size_t row, size_t index, TfResizeSpan* output); +// A map is available for only its actual operation. A mapped feed expires the +// resize map, and resize expires the feed map. Both share the monotonic epoch +// and mutation/invalid-query rules above. Missing origins identify replaced or +// newly created cells; removed fragments are not a complete erase event log. +// Removal records are events within the transaction, not permission to retain +// external history: a later ED3/RIS can clear a source after its earlier removal. +// TerminalMeta.history_clear_epoch invalidates old external history, but its +// final value does not order removals around clears: do not clear then blindly +// append every removed fragment from the same feed. +int tf_terminal_feed_map_meta(void* terminal, bool alternate, TfResizeMapMeta* output); +int tf_terminal_feed_source(void* terminal, uint64_t epoch, bool alternate, + size_t index, TfResizeSourceMeta* output); +int tf_terminal_feed_row(void* terminal, uint64_t epoch, bool alternate, bool removed, + size_t index, TfResizeRowMeta* output); +int tf_terminal_feed_span(void* terminal, uint64_t epoch, bool alternate, bool removed, + size_t row, size_t index, TfResizeSpan* output); 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. diff --git a/experiments/candidates/alacritty/run.py b/experiments/candidates/alacritty/run.py index 9fc9ab3d..7bfb6ff7 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.8" +VERSION = "0.26.1-dev+termforge.9" UPSTREAM_COMMIT = "542147067dd016fc5f49120ef0ba605a98a4e3a7" BASE_CRATE_SHA256 = "bda177466b9524d59f1b12f0dd30b68696788e9992a7e959021c4a0ed96fcf59" diff --git a/experiments/candidates/alacritty/src/ffi.rs b/experiments/candidates/alacritty/src/ffi.rs index cb8ebb67..40e74d4c 100644 --- a/experiments/candidates/alacritty/src/ffi.rs +++ b/experiments/candidates/alacritty/src/ffi.rs @@ -13,13 +13,16 @@ use alacritty_terminal::term::{Config, Term, TermMode, TerminalResizeMap, cell:: use alacritty_terminal::vte::ansi::{Color, NamedColor, Processor}; use trace::{Listener, Size}; +#[derive(Clone, Copy, PartialEq, Eq)] +enum OriginOperation { Resize, Feed } + struct Terminal { term: Term, parser: Processor, listener: Listener, events: VecDeque, - resize_epoch: u64, - resize_map: Option, + origin_epoch: u64, + origin_map: Option<(OriginOperation, TerminalResizeMap)>, } #[repr(C)] @@ -159,7 +162,7 @@ pub extern "C" fn tf_terminal_new(rows: usize, columns: usize, history: usize) - let config = Config { scrolling_history: history, ..Config::default() }; let term = Term::new(config, &Size { columns, rows }, listener.clone()); let value = Terminal { term, parser: Processor::new(), listener, events: VecDeque::new(), - resize_epoch: 0, resize_map: None }; + origin_epoch: 0, origin_map: None }; Box::into_raw(Box::new(value)).cast::() })) .unwrap_or(std::ptr::null_mut()) @@ -172,13 +175,13 @@ pub unsafe extern "C" fn tf_terminal_free(value: *mut c_void) { } } -#[unsafe(no_mangle)] -pub unsafe extern "C" fn tf_terminal_feed( +unsafe fn feed( value: *mut c_void, data: *const c_uchar, len: usize, error: *mut c_uchar, error_capacity: usize, + capture: bool, ) -> c_int { guard(error, error_capacity, || { if len > 1024 * 1024 { @@ -193,12 +196,41 @@ pub unsafe extern "C" fn tf_terminal_feed( } else { unsafe { std::slice::from_raw_parts(data, len) } }; - value.resize_map = None; - value.parser.advance(&mut value.term, bytes); - drain_events(value) + let epoch = if capture { + Some(value.origin_epoch.checked_add(1).ok_or("origin map epoch exhausted")?) + } else { None }; + value.origin_map = None; + let map = if capture { + Some(value.term.feed_with_origin_map(&mut value.parser, bytes)) + } else { + value.parser.advance(&mut value.term, bytes); + None + }; + drain_events(value)?; + if let (Some(map), Some(epoch)) = (map, epoch) { + value.origin_epoch = epoch; + value.origin_map = Some((OriginOperation::Feed, map)); + } + Ok(()) }) } +#[unsafe(no_mangle)] +pub unsafe extern "C" fn tf_terminal_feed( + value: *mut c_void, data: *const c_uchar, len: usize, + error: *mut c_uchar, error_capacity: usize, +) -> c_int { + unsafe { feed(value, data, len, error, error_capacity, false) } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn tf_terminal_feed_mapped( + value: *mut c_void, data: *const c_uchar, len: usize, + error: *mut c_uchar, error_capacity: usize, +) -> c_int { + unsafe { feed(value, data, len, error, error_capacity, true) } +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn tf_terminal_finish( value: *mut c_void, @@ -206,7 +238,7 @@ pub unsafe extern "C" fn tf_terminal_finish( error_capacity: usize, ) -> c_int { guard(error, error_capacity, || { - unsafe { terminal(value)? }.resize_map = None; + unsafe { terminal(value)? }.origin_map = None; Ok(()) }) } @@ -224,18 +256,19 @@ pub unsafe extern "C" fn tf_terminal_resize( return Err("invalid candidate dimensions".into()); } let value = unsafe { terminal(value)? }; - let epoch = value.resize_epoch.checked_add(1).ok_or("resize map epoch exhausted")?; - value.resize_map = None; + let epoch = value.origin_epoch.checked_add(1).ok_or("origin map epoch exhausted")?; + value.origin_map = None; let map = value.term.resize_with_map(Size { columns, rows }); drain_events(value)?; - value.resize_epoch = epoch; - value.resize_map = Some(map); + value.origin_epoch = epoch; + value.origin_map = Some((OriginOperation::Resize, map)); Ok(()) }) } -fn resize_buffer(value: &Terminal, alternate: bool) -> Option<&GridResizeMap> { - let map = value.resize_map.as_ref()?; +fn origin_buffer(value: &Terminal, alternate: bool, operation: OriginOperation) -> Option<&GridResizeMap> { + let (actual, map) = value.origin_map.as_ref()?; + if *actual != operation { return None; } Some(if alternate { &map.alternate } else { &map.primary }) } @@ -254,15 +287,15 @@ fn resize_row(map: &GridResizeMap, removed: bool, index: usize) -> Option<(&Resi } } -#[unsafe(no_mangle)] -pub unsafe extern "C" fn tf_terminal_resize_map_meta( +unsafe fn origin_map_meta( value: *mut c_void, alternate: bool, output: *mut ResizeMapMeta, + operation: OriginOperation, ) -> c_int { if output.is_null() { return 1; } let Ok(value) = (unsafe { terminal(value) }) else { return 1 }; - let Some(map) = resize_buffer(value, alternate) else { return 1 }; + let Some(map) = origin_buffer(value, alternate, operation) else { return 1 }; unsafe { *output = ResizeMapMeta { - epoch: value.resize_epoch, + epoch: value.origin_epoch, before_rows: map.before_rows, before_columns: map.before_columns, before_history: map.before_history, after_rows: map.after_rows, after_columns: map.after_columns, after_history: map.after_history, discarded_before: map.discarded_before, discarded_after: map.discarded_after, @@ -272,13 +305,27 @@ pub unsafe extern "C" fn tf_terminal_resize_map_meta( } #[unsafe(no_mangle)] -pub unsafe extern "C" fn tf_terminal_resize_source( +pub unsafe extern "C" fn tf_terminal_resize_map_meta( + value: *mut c_void, alternate: bool, output: *mut ResizeMapMeta, +) -> c_int { + unsafe { origin_map_meta(value, alternate, output, OriginOperation::Resize) } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn tf_terminal_feed_map_meta( + value: *mut c_void, alternate: bool, output: *mut ResizeMapMeta, +) -> c_int { + unsafe { origin_map_meta(value, alternate, output, OriginOperation::Feed) } +} + +unsafe fn origin_source( value: *mut c_void, epoch: u64, alternate: bool, index: usize, output: *mut ResizeSourceMeta, + operation: OriginOperation, ) -> c_int { if output.is_null() { return 1; } let Ok(value) = (unsafe { terminal(value) }) else { return 1 }; - if epoch != value.resize_epoch { return 1; } - let Some(map) = resize_buffer(value, alternate) else { return 1 }; + if epoch != value.origin_epoch { return 1; } + let Some(map) = origin_buffer(value, alternate, operation) else { return 1 }; let Some(row) = map.source_rows.get(index) else { return 1 }; unsafe { *output = ResizeSourceMeta { id: row.id, line: row.line, columns: row.columns, hard_break: row.hard_break, wrap: row.wrap, @@ -287,14 +334,28 @@ pub unsafe extern "C" fn tf_terminal_resize_source( } #[unsafe(no_mangle)] -pub unsafe extern "C" fn tf_terminal_resize_row( +pub unsafe extern "C" fn tf_terminal_resize_source( + value: *mut c_void, epoch: u64, alternate: bool, index: usize, output: *mut ResizeSourceMeta, +) -> c_int { + unsafe { origin_source(value, epoch, alternate, index, output, OriginOperation::Resize) } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn tf_terminal_feed_source( + value: *mut c_void, epoch: u64, alternate: bool, index: usize, output: *mut ResizeSourceMeta, +) -> c_int { + unsafe { origin_source(value, epoch, alternate, index, output, OriginOperation::Feed) } +} + +unsafe fn origin_row( value: *mut c_void, epoch: u64, alternate: bool, removed: bool, index: usize, output: *mut ResizeRowMeta, + operation: OriginOperation, ) -> c_int { if output.is_null() { return 1; } let Ok(value) = (unsafe { terminal(value) }) else { return 1 }; - if epoch != value.resize_epoch { return 1; } - let Some(map) = resize_buffer(value, alternate) else { return 1 }; + if epoch != value.origin_epoch { return 1; } + let Some(map) = origin_buffer(value, alternate, operation) else { return 1 }; let Some((row, reason)) = resize_row(map, removed, index) else { return 1 }; unsafe { *output = ResizeRowMeta { id: row.id, hard_boundary: row.origins.hard_boundary.unwrap_or(0), line: row.line, @@ -304,14 +365,28 @@ pub unsafe extern "C" fn tf_terminal_resize_row( } #[unsafe(no_mangle)] -pub unsafe extern "C" fn tf_terminal_resize_span( +pub unsafe extern "C" fn tf_terminal_resize_row( + value: *mut c_void, epoch: u64, alternate: bool, removed: bool, index: usize, output: *mut ResizeRowMeta, +) -> c_int { + unsafe { origin_row(value, epoch, alternate, removed, index, output, OriginOperation::Resize) } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn tf_terminal_feed_row( + value: *mut c_void, epoch: u64, alternate: bool, removed: bool, index: usize, output: *mut ResizeRowMeta, +) -> c_int { + unsafe { origin_row(value, epoch, alternate, removed, index, output, OriginOperation::Feed) } +} + +unsafe fn origin_span( value: *mut c_void, epoch: u64, alternate: bool, removed: bool, row: usize, index: usize, output: *mut ResizeSpan, + operation: OriginOperation, ) -> c_int { if output.is_null() { return 1; } let Ok(value) = (unsafe { terminal(value) }) else { return 1 }; - if epoch != value.resize_epoch { return 1; } - let Some(map) = resize_buffer(value, alternate) else { return 1 }; + if epoch != value.origin_epoch { return 1; } + let Some(map) = origin_buffer(value, alternate, operation) else { return 1 }; let Some((row, _)) = resize_row(map, removed, row) else { return 1 }; let Some(span) = row.origins.spans.get(index) else { return 1 }; unsafe { *output = ResizeSpan { @@ -320,6 +395,20 @@ pub unsafe extern "C" fn tf_terminal_resize_span( 0 } +#[unsafe(no_mangle)] +pub unsafe extern "C" fn tf_terminal_resize_span( + value: *mut c_void, epoch: u64, alternate: bool, removed: bool, row: usize, index: usize, output: *mut ResizeSpan, +) -> c_int { + unsafe { origin_span(value, epoch, alternate, removed, row, index, output, OriginOperation::Resize) } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn tf_terminal_feed_span( + value: *mut c_void, epoch: u64, alternate: bool, removed: bool, row: usize, index: usize, output: *mut ResizeSpan, +) -> c_int { + unsafe { origin_span(value, epoch, alternate, removed, row, index, output, OriginOperation::Feed) } +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn tf_terminal_meta(value: *mut c_void, output: *mut TerminalMeta) -> c_int { if output.is_null() { @@ -375,7 +464,7 @@ pub unsafe extern "C" fn tf_terminal_discard_history( return Err("history eviction prefix identity differs".into()); } } - value.resize_map = None; + value.origin_map = None; value.term.grid_mut().discard_oldest_history(count); unsafe { *discarded = value.term.grid().discarded() }; Ok(()) diff --git a/experiments/candidates/alacritty/test_candidate.py b/experiments/candidates/alacritty/test_candidate.py index a38664c2..85b7ef93 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.8") + self.assertEqual(self.summary["version"], "0.26.1-dev+termforge.9") 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/tests/feed_origins.rs b/experiments/candidates/alacritty/tests/feed_origins.rs new file mode 100644 index 00000000..9bf53064 --- /dev/null +++ b/experiments/candidates/alacritty/tests/feed_origins.rs @@ -0,0 +1,485 @@ +use std::cell::RefCell; +use std::rc::Rc; + +use alacritty_terminal::event::{Event, EventListener, VoidListener}; +use alacritty_terminal::grid::Dimensions; +use alacritty_terminal::index::{Column, Line}; +use alacritty_terminal::term::{Config, Term, TerminalResizeMap}; +use alacritty_terminal::term::cell::Flags; +use alacritty_terminal::vte::ansi::Processor; + +struct Size { rows: usize, columns: usize } + +impl Dimensions for Size { + fn total_lines(&self) -> usize { self.rows } + fn screen_lines(&self) -> usize { self.rows } + fn columns(&self) -> usize { self.columns } +} + +fn terminal(rows: usize, columns: usize, bytes: &[u8]) -> (Term, Processor) { + let mut term = Term::new(Config::default(), &Size { rows, columns }, VoidListener); + let mut parser = Processor::new(); + parser.advance(&mut term, bytes); + (term, parser) +} + +fn spans(map: &TerminalResizeMap, line: i32) -> Vec<(u64, usize, usize, usize)> { + map.primary.target_rows.iter().find(|row| row.line == line).unwrap().origins.spans + .iter().map(|span| (span.source_row, span.source_column, span.column, span.len)).collect() +} + +fn row_text(term: &Term, line: i32) -> String { + (0..term.columns()).map(|col| term.grid()[Line(line)][Column(col)].c).collect() +} + +#[test] +fn writing_equal_text_replaces_only_the_written_source_cells() { + let mut term = Term::new(Config::default(), &Size { rows: 2, columns: 8 }, VoidListener); + let mut parser = Processor::new(); + parser.advance(&mut term, b"ABCDEFGH\r\nX"); + let source = term.grid()[Line(0)].id(); + + // The final bytes are unchanged. Content comparison must not mistake the + // explicit rewrite of C/D for survival of those two old source cells. + let map = term.feed_with_origin_map(&mut parser, b"\x1b[1;3HCD"); + let row = &map.primary.target_rows[0]; + let origins: Vec<_> = row.origins.spans.iter().map(|span| + (span.source_row, span.source_column, span.column, span.len)).collect(); + assert_eq!(origins, vec![(source, 0, 0, 2), (source, 4, 4, 4)]); + assert_eq!(row.origins.hard_boundary, Some(source)); + let text: String = (0..8).map(|col| term.grid()[Line(0)][Column(col)].c).collect(); + assert_eq!(text, "ABCDEFGH"); + assert!(term.grid()[Line(0)].origins().is_none()); +} + +#[test] +fn mapping_a_feed_preserves_the_open_grapheme_but_replaces_its_origins() { + let mut term = Term::new(Config::default(), &Size { rows: 2, columns: 8 }, VoidListener); + let mut parser = Processor::new(); + parser.advance(&mut term, b"1"); + let source = term.grid()[Line(0)].id(); + let map = term.feed_with_origin_map(&mut parser, "\u{fe0f}\u{20e3}".as_bytes()); + let cell = &term.grid()[Line(0)][Column(0)]; + assert_eq!(cell.c, '1'); + assert_eq!(cell.zerowidth().unwrap(), &['\u{fe0f}', '\u{20e3}']); + assert!(cell.flags.contains(Flags::WIDE_CHAR)); + assert_eq!(term.grid().cursor.point.column, Column(2)); + let origins: Vec<_> = map.primary.target_rows[0].origins.spans.iter().map(|span| + (span.source_row, span.source_column, span.column, span.len)).collect(); + assert_eq!(origins, vec![(source, 2, 2, 6)]); +} + +#[test] +fn inserted_cells_move_old_origins_without_owning_the_new_blanks() { + let mut term = Term::new(Config::default(), &Size { rows: 2, columns: 8 }, VoidListener); + let mut parser = Processor::new(); + parser.advance(&mut term, b"ABCDEFGH\r\nX"); + let source = term.grid()[Line(0)].id(); + let map = term.feed_with_origin_map(&mut parser, b"\x1b[1;3H\x1b[2@"); + let text: String = (0..8).map(|col| term.grid()[Line(0)][Column(col)].c).collect(); + assert_eq!(text, "AB CDEF"); + let origins: Vec<_> = map.primary.target_rows[0].origins.spans.iter().map(|span| + (span.source_row, span.source_column, span.column, span.len)).collect(); + assert_eq!(origins, vec![(source, 0, 0, 2), (source, 2, 4, 4)]); +} + +#[test] +fn deleted_cells_follow_the_existing_swaps_and_tail_clear_even_at_the_right_edge() { + for (bytes, text, expected) in [ + (&b"\x1b[1;3H\x1b[2P"[..], "ABEFGH ", vec![(0, 0, 2), (4, 2, 4)]), + (&b"\x1b[1;7H\x1b[4P"[..], "ABCD ", vec![(0, 0, 4)]), + ] { + let (mut term, mut parser) = terminal(2, 8, b"ABCDEFGH\r\nX"); + let source = term.grid()[Line(0)].id(); + let map = term.feed_with_origin_map(&mut parser, bytes); + assert_eq!(row_text(&term, 0), text); + assert_eq!(spans(&map, 0), expected.into_iter().map(|(src, dst, len)| + (source, src, dst, len)).collect::>()); + } +} + +#[test] +fn erased_cells_leave_both_neighboring_sources_in_place() { + let (mut term, mut parser) = terminal(2, 8, b"AB EFGH\r\nX"); + let source = term.grid()[Line(0)].id(); + let map = term.feed_with_origin_map(&mut parser, b"\x1b[1;3H\x1b[2X"); + assert_eq!(row_text(&term, 0), "AB EFGH"); + assert_eq!(spans(&map, 0), vec![(source, 0, 0, 2), (source, 4, 4, 4)]); +} + +#[test] +fn line_clear_tracks_its_actual_range_and_preserves_the_pending_wrap_noop() { + for (bytes, expected) in [ + (&b"\x1b[1;3H\x1b[K"[..], vec![(0, 0, 2)]), + (&b"\x1b[1;3H\x1b[1K"[..], vec![(3, 3, 5)]), + (&b"\x1b[1;3H\x1b[2K"[..], vec![]), + ] { + let (mut term, mut parser) = terminal(2, 8, b"ABCDEFGH\r\nX"); + let source = term.grid()[Line(0)].id(); + let map = term.feed_with_origin_map(&mut parser, bytes); + assert_eq!(spans(&map, 0), expected.into_iter().map(|(src, dst, len)| + (source, src, dst, len)).collect::>()); + assert_eq!(map.primary.target_rows[0].origins.hard_boundary, Some(source)); + } + let (mut term, mut parser) = terminal(2, 8, b"ABCDEFGH"); + let source = term.grid()[Line(0)].id(); + let map = term.feed_with_origin_map(&mut parser, b"\x1b[K"); + assert_eq!(row_text(&term, 0), "ABCDEFGH"); + assert_eq!(spans(&map, 0), vec![(source, 0, 0, 8)]); +} + +#[test] +fn screen_clear_discards_reset_rows_but_only_overwritten_cells_in_the_partial_row() { + for (bytes, remaining) in [ + (&b"\x1b[2;3H\x1b[J"[..], (0, 0, 2)), + (&b"\x1b[2;3H\x1b[1J"[..], (3, 3, 5)), + ] { + let (mut term, mut parser) = terminal(3, 8, b"ABCDEFGH\r\nIJKLMNOP\r\nQRSTUVWX"); + let source = term.grid()[Line(1)].id(); + let map = term.feed_with_origin_map(&mut parser, bytes); + assert_eq!(spans(&map, 1), vec![(source, remaining.0, remaining.1, remaining.2)]); + let reset_line = if remaining.0 == 0 { 2 } else { 0 }; + assert!(spans(&map, reset_line).is_empty()); + assert_eq!(map.primary.target_rows[reset_line as usize].origins.hard_boundary, None); + assert_eq!(map.primary.target_rows[1].origins.hard_boundary, Some(source)); + } + let (mut term, mut parser) = terminal(3, 8, b"A\r\nB\r\nC"); + let map = term.feed_with_origin_map(&mut parser, b"\x1b[2J"); + assert!(map.primary.target_rows.iter().all(|row| + row.origins.spans.is_empty() && row.origins.hard_boundary.is_none())); +} + +#[test] +fn tab_replaces_only_an_empty_cell_and_keeps_a_nonempty_cell_source() { + for (initial, expected, first) in [ + (&b"\r"[..], vec![(1, 1, 7)], '\t'), + (&b"ABCDEFGH\r"[..], vec![(0, 0, 8)], 'A'), + ] { + let (mut term, mut parser) = terminal(2, 8, initial); + let source = term.grid()[Line(0)].id(); + let map = term.feed_with_origin_map(&mut parser, b"\t"); + assert_eq!(term.grid()[Line(0)][Column(0)].c, first); + assert_eq!(spans(&map, 0), expected.into_iter().map(|(src, dst, len)| + (source, src, dst, len)).collect::>()); + } +} + +#[test] +fn alignment_test_replaces_every_visible_source_even_when_the_letters_are_equal() { + let (mut term, mut parser) = terminal(2, 8, b"EEEEEEEE\r\nEEEEEEEE"); + let map = term.feed_with_origin_map(&mut parser, b"\x1b#8"); + assert_eq!(row_text(&term, 0), "EEEEEEEE"); + assert_eq!(row_text(&term, 1), "EEEEEEEE"); + assert!(map.primary.target_rows.iter().all(|row| row.origins.spans.is_empty())); +} + +#[test] +fn a_combining_mark_attached_after_a_control_replaces_the_old_lead_source() { + let (mut term, mut parser) = terminal(2, 8, b"e\x1b[1;2H"); + let source = term.grid()[Line(0)].id(); + let map = term.feed_with_origin_map(&mut parser, "\u{301}".as_bytes()); + assert_eq!(term.grid()[Line(0)][Column(0)].zerowidth().unwrap(), &['\u{301}']); + assert_eq!(spans(&map, 0), vec![(source, 1, 1, 7)]); +} + +#[test] +fn insert_mode_moves_the_old_suffix_before_writing_the_new_character() { + let (mut term, mut parser) = terminal(2, 8, b"ABCDEFGH\r\x1b[4h"); + let source = term.grid()[Line(0)].id(); + let map = term.feed_with_origin_map(&mut parser, b"x"); + assert_eq!(row_text(&term, 0), "xABCDEFG"); + assert_eq!(spans(&map, 0), vec![(source, 0, 1, 7)]); +} + +#[test] +fn widening_an_open_grapheme_in_insert_mode_moves_the_old_tail() { + let (mut term, mut parser) = terminal(2, 8, b"ABCDEF\r\x1b[4h1"); + let source = term.grid()[Line(0)].id(); + let map = term.feed_with_origin_map(&mut parser, "\u{fe0f}\u{20e3}".as_bytes()); + assert_eq!(row_text(&term, 0), "1 ABCDEF"); + assert_eq!(term.grid().cursor.point.column, Column(2)); + assert_eq!(spans(&map, 0), vec![(source, 1, 2, 6)]); +} + +#[test] +fn narrowing_an_open_grapheme_in_insert_mode_moves_the_tail_and_replaces_padding() { + let (mut term, mut parser) = terminal(2, 8, "ABCDEF\r\x1b[4h⌚".as_bytes()); + let source = term.grid()[Line(0)].id(); + let map = term.feed_with_origin_map(&mut parser, "\u{fe0e}".as_bytes()); + assert_eq!(row_text(&term, 0), "⌚ABCDEF "); + assert_eq!(term.grid().cursor.point.column, Column(1)); + assert_eq!(spans(&map, 0), vec![(source, 2, 1, 6)]); +} + +#[test] +fn insert_mode_clipping_clears_the_displaced_wide_lead_source() { + let (mut term, mut parser) = terminal(2, 8, "ABCDEF界\r\x1b[4h".as_bytes()); + let source = term.grid()[Line(0)].id(); + let map = term.feed_with_origin_map(&mut parser, b"x"); + assert_eq!(row_text(&term, 0), "xABCDEF "); + assert_eq!(spans(&map, 0), vec![(source, 0, 1, 6)]); +} + +#[test] +fn repeated_hard_linefeed_keeps_its_boundary_and_a_new_boundary_has_no_old_source() { + let (mut term, mut parser) = terminal(3, 4, b"ABCD\r\nX"); + let source = term.grid()[Line(0)].id(); + let repeated = term.feed_with_origin_map(&mut parser, b"\x1b[1;1H\n"); + assert_eq!(repeated.primary.target_rows[0].origins.hard_boundary, Some(source)); + + let (mut term, mut parser) = terminal(3, 4, b"A"); + let source = term.grid()[Line(0)].id(); + let map = term.feed_with_origin_map(&mut parser, b"\n"); + assert!(term.grid()[Line(0)].hard_break()); + assert_eq!(map.primary.target_rows[0].origins.hard_boundary, None); + assert_eq!(spans(&map, 0), vec![(source, 0, 0, 4)]); +} + +#[test] +fn automatic_wrap_removes_the_old_hard_boundary_without_changing_cell_survivors() { + let (mut term, mut parser) = terminal(3, 4, b"ABCD\r\nX"); + let source = term.grid()[Line(0)].id(); + let map = term.feed_with_origin_map(&mut parser, b"\x1b[1;4HEF"); + assert_eq!(row_text(&term, 0), "ABCE"); + assert_eq!(row_text(&term, 1), "F "); + assert_eq!(term.grid().cursor.point.line, Line(1)); + assert_eq!(term.grid().cursor.point.column, Column(1)); + assert!(term.grid()[Line(0)][Column(3)].flags.contains(Flags::WRAPLINE)); + assert_eq!(spans(&map, 0), vec![(source, 0, 0, 3)]); + assert!(!term.grid()[Line(0)].hard_break()); + assert_eq!(map.primary.target_rows[0].origins.hard_boundary, None); +} + +#[test] +fn line_edits_and_region_scrolls_move_whole_row_sources_and_discard_reset_rows() { + for history in [0, 10] { + for (operation, expected) in [ + (&b"\x1b[L"[..], [Some(0), Some(1), None, Some(2), Some(4)]), + (&b"\x1b[M"[..], [Some(0), Some(1), Some(3), None, Some(4)]), + (&b"\x1b[S"[..], [Some(0), Some(2), Some(3), None, Some(4)]), + (&b"\x1b[T"[..], [Some(0), None, Some(1), Some(2), Some(4)]), + (&b"\x1b[99L"[..], [Some(0), Some(1), None, None, Some(4)]), + (&b"\x1b[99M"[..], [Some(0), Some(1), None, None, Some(4)]), + ] { + let mut config = Config::default(); + config.scrolling_history = history; + let mut term = Term::new(config, &Size { rows: 5, columns: 4 }, VoidListener); + let mut parser = Processor::new(); + if history != 0 { + parser.advance(&mut term, b"ZZZZ\r\n"); + } + parser.advance(&mut term, b"AAAA\r\nBBBB\r\nCCCC\r\nDDDD\r\nEEEE\x1b[2;4r\x1b[3;1H"); + let source: Vec<_> = (0..5).map(|line| term.grid()[Line(line)].id()).collect(); + let map = term.feed_with_origin_map(&mut parser, operation); + for (line, old) in expected.into_iter().enumerate() { + let expected = old.map(|old| vec![(source[old], 0, 0, 4)]).unwrap_or_default(); + assert_eq!(spans(&map, line as i32), expected, "history={history} op={operation:?} line={line}"); + let boundary = old.filter(|old| *old < 4).map(|old| source[old]); + let target = map.primary.target_rows.iter().find(|row| row.line == line as i32).unwrap(); + assert_eq!(target.origins.hard_boundary, boundary); + } + assert!(map.primary.removed_rows.is_empty()); + } + } +} + +#[test] +fn a_capacity_eviction_carries_surviving_origins_and_is_absent_from_target_rows() { + let mut config = Config::default(); + config.scrolling_history = 1; + let mut term = Term::new(config, &Size { rows: 2, columns: 4 }, VoidListener); + let mut parser = Processor::new(); + parser.advance(&mut term, b"AAAA\r\nBBBB\r\nCCCC"); + let source: Vec<_> = (-1..2).map(|line| term.grid()[Line(line)].id()).collect(); + let map = term.feed_with_origin_map(&mut parser, b"\r\nD"); + assert_eq!(map.primary.removed_rows.len(), 1); + assert_eq!(map.primary.removed_rows[0].row.origins.spans[0].source_row, source[0]); + assert_eq!(spans(&map, -1), vec![(source[1], 0, 0, 4)]); + assert_eq!(spans(&map, 0), vec![(source[2], 0, 0, 4)]); + assert!(spans(&map, 1).is_empty()); + assert_eq!(map.primary.discarded_after, map.primary.discarded_before + 1); + assert!(map.primary.target_rows.iter().all(|row| + row.origins.spans.iter().all(|span| span.source_row != source[0]))); +} + +#[test] +fn buffer_names_survive_odd_switches_roundtrips_reentry_and_reset() { + for (start_alt, bytes, clear_alt, reset) in [ + (false, &b"\x1b[?1049hA"[..], true, false), + (false, &b"\x1b[?1049hA\x1b[?1049l"[..], true, false), + (true, &b"\x1b[?1049l"[..], false, false), + (true, &b"\x1b[?1049h"[..], false, false), + (true, &b"\x1b[?1049l\x1b[?1049hB"[..], true, false), + (false, &b"\x1bc"[..], true, true), + (true, &b"\x1bc"[..], true, true), + ] { + let (mut term, mut parser) = terminal(2, 8, b"P\r\nQ"); + parser.advance(&mut term, b"\x1b[?1049hA\r\nB"); + if !start_alt { + parser.advance(&mut term, b"\x1b[?1049l"); + } + let before = term.feed_with_origin_map(&mut parser, b""); + let map = term.feed_with_origin_map(&mut parser, bytes); + for (old, new, cleared) in [ + (&before.primary, &map.primary, reset), + (&before.alternate, &map.alternate, clear_alt), + ] { + assert_eq!(old.source_rows.iter().map(|row| row.id).collect::>(), + new.source_rows.iter().map(|row| row.id).collect::>()); + for (index, row) in new.target_rows.iter().enumerate() { + if cleared { + assert!(row.origins.spans.is_empty(), "start_alt={start_alt} bytes={bytes:?}"); + assert_eq!(row.origins.hard_boundary, None); + } else { + assert_eq!(row.origins.spans, old.target_rows[index].origins.spans); + assert_eq!(row.origins.hard_boundary, old.target_rows[index].origins.hard_boundary); + } + } + } + } +} + +#[test] +fn overwriting_a_wide_pair_drops_both_cells_and_its_leading_wrap_spacer() { + for column in [2, 3] { + let (mut term, mut parser) = terminal(2, 8, "A界BCD".as_bytes()); + let source = term.grid()[Line(0)].id(); + let bytes = format!("\x1b[1;{column}Hx"); + let map = term.feed_with_origin_map(&mut parser, bytes.as_bytes()); + assert_eq!(spans(&map, 0), vec![(source, 0, 0, 1), (source, 3, 3, 5)]); + } + let (mut term, mut parser) = terminal(3, 4, "ABC界".as_bytes()); + let first = term.grid()[Line(0)].id(); + let second = term.grid()[Line(1)].id(); + let map = term.feed_with_origin_map(&mut parser, b"\x1b[2;1Hx"); + assert_eq!(spans(&map, 0), vec![(first, 0, 0, 3)]); + assert_eq!(spans(&map, 1), vec![(second, 2, 2, 2)]); +} + +#[test] +fn a_rejected_combining_append_keeps_the_full_old_fragment_source() { + // The candidate stores 64 scalars per fragment; the next mark is placed + // in a new fragment after this explicit control boundary. + let input = format!("e{}\x1b[1;2H", "\u{301}".repeat(63)); + let (mut term, mut parser) = terminal(2, 8, input.as_bytes()); + let source = term.grid()[Line(0)].id(); + let map = term.feed_with_origin_map(&mut parser, "\u{301}".as_bytes()); + assert_eq!(term.grid()[Line(0)][Column(0)].zerowidth().unwrap().len(), 63); + assert!(term.grid()[Line(0)][Column(1)].grapheme_fragment()); + assert_eq!(spans(&map, 0), vec![(source, 0, 0, 1), (source, 2, 2, 6)]); +} + +#[derive(Clone, Default)] +struct Events(Rc>>); + +impl EventListener for Events { + fn send_event(&self, event: Event) { + self.0.borrow_mut().push(format!("{event:?}")); + } +} + +#[test] +fn mapped_feeds_match_plain_parser_cells_styles_cursors_and_events_at_every_chunk() { + let plain_events = Events::default(); + let mapped_events = Events::default(); + let mut plain = Term::new(Config::default(), &Size { rows: 3, columns: 8 }, plain_events.clone()); + let mut mapped = Term::new(Config::default(), &Size { rows: 3, columns: 8 }, mapped_events.clone()); + let mut plain_parser: Processor = Processor::new(); + let mut mapped_parser: Processor = Processor::new(); + let chunks = [ + &b"\x1b[31;44mABC1"[..], + &b"\x1b[6n\x07\x1b]2;origins\x07"[..], + &b"\xef\xb8"[..], + &b"\x8f\xe2\x83\xa3"[..], + &b"\r\x1b[4hx\x1b[4l\x1b[2@\x1b[2P\x1b[2X"[..], + "\x1b[?1049h\x1b[1;32m界\r\n\t".as_bytes(), + &b"\x1b[?1049l\r\nA\r\nB\r\nC\x1b[1;2H\x1b[K"[..], + &b"\x1b[1J\x1b#8\x1b[2J\x1b[3J\x1bc"[..], + &b"1"[..], + "\u{fe0f}\u{20e3}".as_bytes(), + ]; + for bytes in chunks { + plain_parser.advance(&mut plain, bytes); + let map = mapped.feed_with_origin_map(&mut mapped_parser, bytes); + assert_eq!(plain.mode(), mapped.mode()); + assert_eq!(plain.grid().cursor, mapped.grid().cursor); + assert_eq!(plain.grid().saved_cursor, mapped.grid().saved_cursor); + assert_eq!(plain.history_size(), mapped.history_size()); + for line in -(plain.history_size() as i32)..plain.screen_lines() as i32 { + let left = &plain.grid()[Line(line)]; + let right = &mapped.grid()[Line(line)]; + assert_eq!(left, right, "chunk={bytes:?} line={line}"); + assert_eq!(left.hard_break(), right.hard_break()); + assert_eq!(left.wrap_columns(), right.wrap_columns()); + assert!(left.origins().is_none()); + assert!(right.origins().is_none()); + } + assert_eq!(*plain_events.0.borrow(), *mapped_events.0.borrow()); + for grid in [&map.primary, &map.alternate] { + for row in &grid.target_rows { + assert!(row.origins.spans.iter().all(|span| + grid.source_rows.iter().any(|source| source.id == span.source_row))); + } + } + } +} + +#[test] +fn removed_rows_are_events_and_do_not_override_a_later_history_clear() { + for clear in [&b"\x1b[3J"[..], &b"\x1bc"[..]] { + let mut config = Config::default(); + config.scrolling_history = 1; + let mut term = Term::new(config, &Size { rows: 2, columns: 4 }, VoidListener); + let mut parser = Processor::new(); + parser.advance(&mut term, b"AAAA\r\nBBBB\r\nCCCC"); + let old_history = term.grid()[Line(-1)].id(); + let newly_scrolled_history = term.grid()[Line(0)].id(); + let old_epoch = term.history_clear_epoch(); + let mut bytes = b"\r\nD".to_vec(); + bytes.extend_from_slice(clear); + let map = term.feed_with_origin_map(&mut parser, &bytes); + assert!(term.history_clear_epoch() > old_epoch); + assert_eq!(term.history_size(), 0); + assert!(map.primary.target_rows.iter().all(|row| row.origins.spans.iter().all(|span| + span.source_row != old_history && span.source_row != newly_scrolled_history))); + // This earlier capacity event cannot grant permission to restore the + // subsequently cleared source in an external history consumer. + assert_eq!(map.primary.removed_rows.len(), 1); + assert_eq!(map.primary.removed_rows[0].row.origins.spans[0].source_row, old_history); + } +} + +#[test] +fn a_feed_map_composes_with_the_next_resize_without_resurrecting_rewritten_cells() { + use std::collections::BTreeMap; + + let (mut term, mut parser) = terminal(2, 8, b"ABCDEFGH\r\nX"); + let source = term.grid()[Line(0)].id(); + let feed = term.feed_with_origin_map(&mut parser, b"\x1b[1;3HCD"); + let mut bindings = BTreeMap::new(); + for row in &feed.primary.target_rows { + for span in &row.origins.spans { + for offset in 0..span.len { + bindings.insert((row.id, span.column + offset), + (span.source_row, span.source_column + offset)); + } + } + } + let resize = term.resize_with_map(Size { rows: 2, columns: 4 }); + let mut survivors = Vec::new(); + for row in &resize.primary.target_rows { + for span in &row.origins.spans { + for offset in 0..span.len { + if let Some(&(old_row, old_column)) = bindings.get(&(span.source_row, span.source_column + offset)) { + if old_row == source { + survivors.push(old_column); + } + } + } + } + } + survivors.sort_unstable(); + assert_eq!(survivors, [0, 1, 4, 5, 6, 7]); + assert!((-(term.history_size() as i32)..term.screen_lines() as i32) + .all(|line| term.grid()[Line(line)].origins().is_none())); +} diff --git a/experiments/candidates/alacritty/vendor/alacritty_terminal/Cargo.toml b/experiments/candidates/alacritty/vendor/alacritty_terminal/Cargo.toml index ecf99db7..d753b7a8 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.8" +version = "0.26.1-dev+termforge.9" 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 111efbaa..65b16ed0 100644 --- a/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/mod.rs +++ b/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/mod.rs @@ -21,7 +21,7 @@ pub use self::row::Row; use self::origins::RowOrigins; use self::storage::Storage; -/// A physical source row immediately before one resize transaction. +/// A physical source row immediately before one resize or feed transaction. #[derive(Clone, Debug)] pub struct ResizeSourceRow { pub id: u64, @@ -56,7 +56,11 @@ pub struct ResizeRemoval { pub reason: ResizeRemovalReason, } -/// Cell movement during one resize, separate from later writes or text policy. +/// Old cell origins surviving one resize or feed, separate from text policy. +/// +/// The type name is retained from the resize record format. A feed records only +/// surviving old origins, not a complete log of writes, erasures, or their order. +/// Newly written cells and new hard boundaries have no pre-transaction origin. #[derive(Clone, Debug)] pub struct GridResizeMap { pub before_rows: usize, @@ -69,6 +73,10 @@ pub struct GridResizeMap { pub discarded_after: u64, pub source_rows: Vec, pub target_rows: Vec, + /// Removal events observed during the transaction, not final retention grants. + /// A later ED3 or reset in the same feed can clear an earlier removed source. + /// Consumers must honor history-clear state and cannot restore all these rows + /// merely because they occurred before the final map was returned. pub removed_rows: Vec, } @@ -242,7 +250,7 @@ impl Grid { } map.removed_rows = self.resize_removed.take().unwrap_or_default(); // Storage retains inactive allocation slots after height changes. They - // must not carry origins into a later resize or row reuse. + // must not carry origins into a later transaction or row reuse. self.raw.clear_resize_origins(); map } diff --git a/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/origins.rs b/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/origins.rs index 5427d0db..a49a40e9 100644 --- a/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/origins.rs +++ b/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/origins.rs @@ -1,6 +1,6 @@ -//! Sparse physical-cell origins for one synchronous resize operation. +//! Sparse physical-cell origins for one synchronous resize or feed transaction. -/// A contiguous run of cells moved from one pre-resize row. +/// A contiguous run of surviving cells from one pre-transaction row. /// /// Both coordinates count physical cells. A clipped wide glyph retains its /// lead-cell origin; its logical width still belongs to the source cell. @@ -12,10 +12,11 @@ pub struct OriginSpan { pub len: usize, } -/// Origins carried beside a row or a temporary row fragment during resize. +/// Origins carried beside a row or temporary fragment during a transaction. /// /// `columns` includes gaps and padding without origins. The optional hard /// boundary is independent of cells, so an empty hard line remains observable. +/// Feed writes remove old origins even when the replacement text is equal. #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct RowOrigins { pub columns: usize, @@ -139,6 +140,53 @@ impl RowOrigins { self.hard_boundary = boundary; } + /// Forget overwritten cells without moving their surviving neighbors. + pub(crate) fn invalidate_range(&mut self, at: usize, len: usize) { + let at = at.min(self.columns); + let end = at + len.min(self.columns - at); + if at == end { + return; + } + + let spans = std::mem::take(&mut self.spans); + for span in spans { + let span_end = span.column + span.len; + if span_end <= at || span.column >= end { + self.push(span); + continue; + } + if span.column < at { + self.push(OriginSpan { len: at - span.column, ..span }); + } + if span_end > end { + self.push(OriginSpan { + source_column: span.source_column + end - span.column, + column: end, + len: span_end - end, + ..span + }); + } + } + } + + /// Exchange two physical cells, carrying gaps and origins with them. + pub(crate) fn swap_cells(&mut self, a: usize, b: usize) { + if a == b { + return; + } + let (left, right) = if a < b { (a, b) } else { (b, a) }; + assert!(right < self.columns, "origin swap exceeds row extent"); + + let mut left_cell = self.split_off(left); + let mut between = left_cell.split_off(1); + let mut right_cell = between.split_off(right - left - 1); + let suffix = right_cell.split_off(1); + self.append(right_cell, left); + self.append(between, left + 1); + self.append(left_cell, right); + self.append(suffix, right + 1); + } + fn push(&mut self, span: OriginSpan) { if span.len == 0 { return; diff --git a/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/row.rs b/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/row.rs index 428604d3..b6db1549 100644 --- a/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/row.rs +++ b/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/row.rs @@ -38,7 +38,7 @@ pub struct Row { hard_break: bool, #[cfg_attr(feature = "serde", serde(skip))] wrap_columns: usize, - /// Present only while a synchronous resize is recording cell origins. + /// Present only while a synchronous resize or feed records cell origins. #[cfg_attr(feature = "serde", serde(skip))] origins: Option>, } @@ -142,7 +142,7 @@ impl Row { self.wrap_columns } - /// Temporary cell origins for the current resize, if capture is enabled. + /// Temporary origins for the current resize or feed, if capture is enabled. pub fn origins(&self) -> Option<&RowOrigins> { self.origins.as_deref() } @@ -159,6 +159,25 @@ impl Row { self.origins = origins; } + /// Record an actual cell replacement while leaving other origins in place. + #[inline] + pub(crate) fn invalidate_origins(&mut self, at: usize, len: usize) { + if let Some(origins) = self.origins.as_mut() { + origins.invalidate_range(at, len); + } + } + + /// Swap actual cells and their optional origins as one operation. + #[inline] + pub(crate) fn swap_cells(&mut self, a: usize, b: usize) { + // Match the dirty extent of the mutable full-row slice used for edits. + self.occ = self.inner.len(); + self.inner.swap(a, b); + if let Some(origins) = self.origins.as_mut() { + origins.swap_cells(a, b); + } + } + #[inline] pub(crate) fn renew_identity(&mut self) { self.id = next_row_id(); @@ -168,6 +187,11 @@ impl Row { #[inline] pub(crate) fn set_hard_break(&mut self, value: bool) { self.hard_break = value; + if !value { + if let Some(origins) = self.origins.as_mut() { + origins.hard_boundary = None; + } + } } #[inline] diff --git a/experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/grapheme.rs b/experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/grapheme.rs index 55e1abb9..a0f1e7f2 100644 --- a/experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/grapheme.rs +++ b/experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/grapheme.rs @@ -126,6 +126,7 @@ impl Term { point.column.0 = point.column.saturating_sub(1); } if self.grid[point].push_zerowidth(c) { + self.grid[point.line].invalidate_origins(point.column.0, 1); self.damage .damage_point(Point::new(point.line.0 as usize, point.column)); self.grapheme = pending; @@ -187,7 +188,7 @@ impl Term { let line = self.grid.cursor.point.line; let col = self.grid.cursor.point.column; for col in (col.0..(columns - width)).rev() { - self.grid[line][..].swap(col + width, col); + self.grid[line].swap_cells(col + width, col); } self.clip_inserted_right_edge(); } @@ -281,6 +282,7 @@ impl Term { if width < pending.width && self.mode.contains(TermMode::INSERT) { self.shift_grapheme_tail(point, pending.width, width); } + self.grid[point.line].invalidate_origins(point.column.0, 1); let cell = &mut self.grid[point]; *cell = pending.template.clone(); cell.c = pending.text.chars().next().unwrap(); @@ -301,16 +303,17 @@ impl Term { fn shift_grapheme_tail(&mut self, point: Point, old_width: usize, new_width: usize) { let columns = self.columns(); - let row = &mut self.grid[point.line][..]; + let row = &mut self.grid[point.line]; if new_width > old_width { for column in ((point.column.0 + old_width)..columns - 1).rev() { - row.swap(column + 1, column); + row.swap_cells(column + 1, column); } } else { for column in (point.column.0 + new_width)..columns - 1 { - row.swap(column, column + 1); + row.swap_cells(column, column + 1); } - row[columns - 1] = Cell::default(); + row.invalidate_origins(columns - 1, 1); + row[Column(columns - 1)] = Cell::default(); } self.clip_inserted_right_edge(); self.damage @@ -323,6 +326,7 @@ impl Term { let line = self.grid.cursor.point.line; let last = Column(self.columns() - 1); if self.grid[line][last].flags.contains(Flags::WIDE_CHAR) { + self.grid[line].invalidate_origins(last.0, 1); self.grid[line][last].clear_wide(); } } diff --git a/experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/mod.rs b/experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/mod.rs index cd0e57c2..75a725f3 100644 --- a/experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/mod.rs +++ b/experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/mod.rs @@ -39,7 +39,10 @@ pub const MIN_COLUMNS: usize = 1; /// Minimum number of visible lines. pub const MIN_SCREEN_LINES: usize = 1; -/// Owned movement maps for both buffers in one terminal resize transaction. +/// Owned origin maps for both named buffers in one resize or feed transaction. +/// +/// The existing resize record type is also used by explicit feed capture. It +/// identifies surviving old sources; it does not define external text retention. #[derive(Clone, Debug)] pub struct TerminalResizeMap { pub primary: GridResizeMap, @@ -750,6 +753,43 @@ impl Term { } } + /// Parse one input transaction while tracking its surviving old cell origins. + /// + /// Observation is transparent to the editable trailing grapheme. In + /// particular, neither boundary seals it or changes parser state. The named + /// buffers remain stable even when the input enters or leaves the alternate + /// screen during this transaction. + pub fn feed_with_origin_map( + &mut self, + processor: &mut ansi::Processor, + bytes: &[u8], + ) -> TerminalResizeMap + where + T: EventListener, + { + let active_before = self.grid.begin_resize_map(); + let inactive_before = self.inactive_grid.begin_resize_map(); + let (primary_before, alternate_before) = if self.mode.contains(TermMode::ALT_SCREEN) { + (inactive_before, active_before) + } else { + (active_before, inactive_before) + }; + + processor.advance(self, bytes); + + if self.mode.contains(TermMode::ALT_SCREEN) { + TerminalResizeMap { + primary: self.inactive_grid.finish_resize_map(primary_before), + alternate: self.grid.finish_resize_map(alternate_before), + } + } else { + TerminalResizeMap { + primary: self.grid.finish_resize_map(primary_before), + alternate: self.inactive_grid.finish_resize_map(alternate_before), + } + } + } + /// Active terminal modes. #[inline] pub fn mode(&self) -> &TermMode { @@ -1016,6 +1056,7 @@ impl Term { let point = self.grid.cursor.point; self.grid.cursor_cell().flags.insert(Flags::WRAPLINE); self.grid[point.line].set_wrap_columns(point.column.0 + 1); + self.grid[point.line].set_hard_break(false); if self.grid.cursor.point.line + 1 >= self.scroll_region.end { self.linefeed(); @@ -1038,28 +1079,35 @@ impl Term { let flags = self.grid.cursor.template.flags; let extra = self.grid.cursor.template.extra.clone(); - let mut cursor_cell = self.grid.cursor_cell(); + let point = self.grid.cursor.point; + let old_flags = self.grid[point.line][point.column].flags; // Clear all related cells when overwriting a fullwidth cell. - if cursor_cell.flags.intersects(Flags::WIDE_CHAR | Flags::WIDE_CHAR_SPACER) { + if old_flags.intersects(Flags::WIDE_CHAR | Flags::WIDE_CHAR_SPACER) { // Remove wide char and spacer. - let wide = cursor_cell.flags.contains(Flags::WIDE_CHAR); - let point = self.grid.cursor.point; + let wide = old_flags.contains(Flags::WIDE_CHAR); if wide && point.column < self.last_column() { + if self.grid[point.line][point.column + 1].flags.contains(Flags::WIDE_CHAR_SPACER) { + self.grid[point.line].invalidate_origins(point.column.0 + 1, 1); + } self.grid[point.line][point.column + 1].flags.remove(Flags::WIDE_CHAR_SPACER); } else if point.column > 0 { + self.grid[point.line].invalidate_origins(point.column.0 - 1, 1); self.grid[point.line][point.column - 1].clear_wide(); } // Remove leading spacers. if point.column <= 1 && point.line != self.topmost_line() { let column = self.last_column(); + if self.grid[point.line - 1i32][column].flags.contains(Flags::LEADING_WIDE_CHAR_SPACER) { + self.grid[point.line - 1i32].invalidate_origins(column.0, 1); + } self.grid[point.line - 1i32][column].flags.remove(Flags::LEADING_WIDE_CHAR_SPACER); } - - cursor_cell = self.grid.cursor_cell(); } + self.grid[point.line].invalidate_origins(point.column.0, 1); + let cursor_cell = self.grid.cursor_cell(); cursor_cell.c = c; cursor_cell.fg = fg; cursor_cell.bg = bg; @@ -1119,6 +1167,8 @@ impl Handler for Term { trace!("Decalnning"); for line in (0..self.screen_lines()).map(Line::from) { + let columns = self.columns(); + self.grid[line].invalidate_origins(0, columns); for column in 0..self.columns() { let cell = &mut self.grid[line][Column(column)]; *cell = Cell::default(); @@ -1179,15 +1229,16 @@ impl Handler for Term { let line = cursor.point.line; self.damage.damage_line(line.0 as usize, 0, self.columns() - 1); - let row = &mut self.grid[line][..]; + let row = &mut self.grid[line]; for offset in (0..num_cells).rev() { - row.swap(destination + offset, source.0 + offset); + row.swap_cells(destination + offset, source.0 + offset); } // Cells were just moved out toward the end of the line; // fill in between source and dest with blanks. - for cell in &mut row[source.0..destination] { + row.invalidate_origins(source.0, count); + for cell in &mut row[..][source.0..destination] { *cell = bg.into(); } } @@ -1368,6 +1419,8 @@ impl Handler for Term { let cell = self.grid.cursor_cell(); if cell.c == ' ' { cell.c = c; + let point = self.grid.cursor.point; + self.grid[point.line].invalidate_origins(point.column.0, 1); } loop { @@ -1534,6 +1587,7 @@ impl Handler for Term { let line = cursor.point.line; self.damage.damage_line(line.0 as usize, start.0, end.0); let row = &mut self.grid[line]; + row.invalidate_origins(start.0, end.0 - start.0); for cell in &mut row[start..end] { *cell = bg.into(); } @@ -1555,16 +1609,17 @@ impl Handler for Term { let line = cursor.point.line; self.damage.damage_line(line.0 as usize, 0, self.columns() - 1); - let row = &mut self.grid[line][..]; + let row = &mut self.grid[line]; for offset in 0..num_cells { - row.swap(start + offset, end + offset); + row.swap_cells(start + offset, end + offset); } // Clear last `count` cells in the row. If deleting 1 char, need to delete // 1 cell. let end = columns - count; - for cell in &mut row[end..] { + row.invalidate_origins(end, count); + for cell in &mut row[..][end..] { *cell = bg.into(); } } @@ -1660,6 +1715,7 @@ impl Handler for Term { self.damage.damage_line(point.line.0 as usize, left.0, right.0 - 1); let row = &mut self.grid[point.line]; + row.invalidate_origins(left.0, right.0 - left.0); for cell in &mut row[left..right] { *cell = bg.into(); } @@ -1784,6 +1840,7 @@ impl Handler for Term { // Clear up to the current column in the current line. let end = cmp::min(cursor.column + 1, Column(self.columns())); + self.grid[cursor.line].invalidate_origins(0, end.0); for cell in &mut self.grid[cursor.line][..end] { *cell = bg.into(); } @@ -1793,6 +1850,8 @@ impl Handler for Term { }, ansi::ClearMode::Below => { let cursor = self.grid.cursor.point; + let count = self.columns() - cursor.column.0; + self.grid[cursor.line].invalidate_origins(cursor.column.0, count); for cell in &mut self.grid[cursor.line][cursor.column..] { *cell = bg.into(); } diff --git a/experiments/core/CANDIDATE.md b/experiments/core/CANDIDATE.md index c520c9f4..700d8be9 100644 --- a/experiments/core/CANDIDATE.md +++ b/experiments/core/CANDIDATE.md @@ -2,9 +2,9 @@ 按 [ADR-0006](../../docs/adr/0006-confirmed-project-direction.md),用户确认保留本候选代码继续评估,尚未确认最终采用。下述实现、来源与回归结果继续保留;与 WindTerm 2.7.0 的重要差异、性能容差及正式选型分别提交用户确认。 -`CandidateTerminal` 是 #27 的拥有值适配层,不是自研核心的替代品声明。它把固定 `0.26.1-dev+termforge.8` 的 `Term/Processor` 接到现有 `Screen`/`Line`;默认 CMake 保留自研核心且不需要 Rust,候选产品由 `TERMFORGE_BUILD_CANDIDATE_ADAPTER=ON` 显式启用。 +`CandidateTerminal` 是 #27 的拥有值适配层,不是自研核心的替代品声明。它把固定 `0.26.1-dev+termforge.9` 的 `Term/Processor` 接到现有 `Screen`/`Line`;默认 CMake 保留自研核心且不需要 Rust,候选产品由 `TERMFORGE_BUILD_CANDIDATE_ADAPTER=ON` 显式启用。 -`.7` 已修复原生 resize 的硬/软边界传递及缩宽容量淘汰计数,结果保留在[工作检查点](results/2026-10-28-reflow-metadata/README.md)。当前 `.8` 增加一次 resize 的来源映射,来源见[补丁台账](../candidates/alacritty/PROVENANCE.md#termforge8-补丁)。C++ 长行 resize/repaint 的整段复制回归仍失败:旧历史行与原生片段重复,来源归属和统一视图映射仍由 #29 继续处理。这些原生修复不构成候选完整正确性通过。 +`.7` 已修复原生 resize 的硬/软边界传递及缩宽容量淘汰计数,结果保留在[工作检查点](results/2026-10-28-reflow-metadata/README.md)。`.8` 增加一次 resize 来源映射,当前 `.9` 扩展到一次真实 feed,来源见[补丁台账](../candidates/alacritty/PROVENANCE.md#termforge9-补丁)。C++ 长行 resize/repaint 的整段复制回归仍失败:旧历史行与原生片段重复,来源归属和统一视图映射仍由 #29 继续处理。这些原生修复不构成候选完整正确性通过。 ## 一次 resize 的来源映射 @@ -16,6 +16,12 @@ `candidate_resize_map_test` 从 C++ 调用真实静态库,验证字段布局、容量片段、映射生命周期、主/备用屏命名和空硬行。Rust FFI 测试覆盖拆分、相同文字的不同来源合并、宽字 spacer 和高度缩小的来源移除。当前 C++ 历史合并尚未消费此映射,后续覆盖写入、源文本归属、复制/Find 和视口坐标仍需实现与完整验证;该接口不裁决原版历史覆盖策略,也未测量新增来源表的资源成本。 +## 一次 feed 的来源映射 + +`tf_terminal_feed_mapped` 是显式来源捕获入口;普通 `tf_terminal_feed` 不捕获。记录结构沿用 `TfResize*` 命名与物理坐标,只通过 `tf_terminal_feed_*` 查询读取 feed 结果。实际写入或擦除使旧来源消失,文字相同也不保留旧身份;移动保留来源,待续字素不会因捕获而被封闭。一次操作仅有一种映射,resize/feed 查询不能互用,均受共享 epoch 和后续成功修改的失效规则约束。 + +feed 的 `removed_rows` 只记录过程中的移除事件。它不是完整擦除日志或最终保留政策;同一 feed 中 ED3/RIS 可能出现在移除之前或之后,最终 `history_clear_epoch` 不提供事件顺序。消费方必须避免复活清空前的来源,不能先清空再无条件追加整份 removed 列表。最终目标来源、清空与外部历史的保留规则仍须由上层一致处理。 + ## 构建与验证 ```powershell diff --git a/experiments/core/candidate_resize_map_test.cpp b/experiments/core/candidate_resize_map_test.cpp index 4edf4569..757d61f7 100644 --- a/experiments/core/candidate_resize_map_test.cpp +++ b/experiments/core/candidate_resize_map_test.cpp @@ -158,12 +158,67 @@ void active_alternate_keeps_primary_origins_and_empty_hard_line() { require(tf_terminal_meta(terminal.get(), &restored) == 0 && restored.history == 1, "primary history did not survive return from alternate"); } + +void feed_mapping_and_lifetime() { + std::unique_ptr terminal(tf_terminal_new(2, 8, 8), tf_terminal_free); + require(terminal != nullptr, "could not create feed candidate"); + const std::string_view initial = "ABCDEFGH\r\nX"; + require(tf_terminal_feed(terminal.get(), reinterpret_cast(initial.data()), + initial.size(), nullptr, 0) == 0, "initial feed failed"); + TfRowMeta source{}; + require(tf_terminal_row_meta(terminal.get(), 0, &source) == 0, "feed source missing"); + require(tf_terminal_resize(terminal.get(), 2, 8, nullptr, 0) == 0, "identity resize failed"); + TfResizeMapMeta resize{}; + require(tf_terminal_resize_map_meta(terminal.get(), false, &resize) == 0, "identity resize map missing"); + const std::string_view rewrite = "\x1b[1;3HCD"; + require(tf_terminal_feed_mapped(terminal.get(), reinterpret_cast(rewrite.data()), + rewrite.size(), nullptr, 0) == 0, "mapped feed failed"); + TfResizeMapMeta map{}; + require(tf_terminal_feed_map_meta(terminal.get(), false, &map) == 0 && + map.epoch > resize.epoch && map.source_rows == 2 && map.target_rows == 2, + "feed map metadata or shared epoch differs"); + require(tf_terminal_resize_map_meta(terminal.get(), false, &resize) == 1, + "feed map leaked through resize queries"); + TfResizeRowMeta row{}; + require(tf_terminal_feed_row(terminal.get(), map.epoch, false, false, 0, &row) == 0 && + row.spans == 2 && row.hard_boundary == source.id, "feed row origins differ"); + TfResizeSourceMeta old{}; + require(tf_terminal_feed_source(terminal.get(), map.epoch, false, 0, &old) == 0 && + old.id == source.id && old.columns == 8 && old.hard_break, + "feed source metadata differs"); + TfResizeSpan span{}; + require(tf_terminal_feed_span(terminal.get(), map.epoch, false, false, 0, 0, &span) == 0 && + span.source_row == source.id && span.source_column == 0 && span.column == 0 && span.len == 2, + "unchanged prefix source missing"); + require(tf_terminal_feed_span(terminal.get(), map.epoch, false, false, 0, 1, &span) == 0 && + span.source_row == source.id && span.source_column == 4 && span.column == 4 && span.len == 4, + "same-text rewrite kept stale origins or removed its unchanged suffix"); + require(tf_terminal_feed_span(terminal.get(), resize.epoch, false, false, 0, 1, &span) == 1 && + span.source_column == 4, "stale epoch changed feed span output"); + require(tf_terminal_feed_mapped(terminal.get(), nullptr, 1, nullptr, 0) == 1 && + tf_terminal_feed_map_meta(terminal.get(), false, &map) == 0, + "rejected mapped feed discarded valid metadata"); + require(tf_terminal_feed(terminal.get(), nullptr, 0, nullptr, 0) == 0 && + tf_terminal_feed_map_meta(terminal.get(), false, &map) == 1, + "ordinary feed retained stale origins"); + require(tf_terminal_feed_mapped(terminal.get(), nullptr, 0, nullptr, 0) == 0 && + tf_terminal_feed_map_meta(terminal.get(), false, &map) == 0, + "empty mapped feed did not return an identity map"); + require(tf_terminal_resize(terminal.get(), 2, 4, nullptr, 0) == 0 && + tf_terminal_feed_map_meta(terminal.get(), false, &map) == 1, + "resize retained the old feed map"); + require(tf_terminal_feed_mapped(terminal.get(), nullptr, 0, nullptr, 0) == 0 && + tf_terminal_finish(terminal.get(), nullptr, 0) == 0 && + tf_terminal_feed_map_meta(terminal.get(), false, &map) == 1, + "finish retained the old feed map"); +} } // namespace int main() { try { capacity_mapping_and_lifetime(); active_alternate_keeps_primary_origins_and_empty_hard_line(); + feed_mapping_and_lifetime(); std::cout << "candidate resize map ABI checks passed\n"; return 0; } catch (const std::exception& error) { From 33de4844218877d268791464c77c638acc417b9c Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Wed, 28 Oct 2026 02:54:48 +0800 Subject: [PATCH 06/14] docs: preserve feed origin checks and restore query observation (#29) --- docs/EVIDENCE.md | 1 + docs/FEATURES.md | 2 +- docs/PROVENANCE.md | 2 +- docs/research/windterm-resize-text.md | 28 + .../results/2026-10-28-feed-origins/README.md | 33 + .../2026-10-28-feed-origins/validation.json | 127 ++++ .../windterm-restore-fence-input.json | 50 ++ .../windterm-restore-fence-observations.json | 608 ++++++++++++++++++ 8 files changed, 849 insertions(+), 2 deletions(-) create mode 100644 experiments/core/results/2026-10-28-feed-origins/README.md create mode 100644 experiments/core/results/2026-10-28-feed-origins/validation.json create mode 100644 experiments/core/results/2026-10-28-history-layout/windterm-restore-fence-input.json create mode 100644 experiments/core/results/2026-10-28-history-layout/windterm-restore-fence-observations.json diff --git a/docs/EVIDENCE.md b/docs/EVIDENCE.md index 035055f9..5c99dda3 100644 --- a/docs/EVIDENCE.md +++ b/docs/EVIDENCE.md @@ -40,6 +40,7 @@ R1 固定总出口的源码为 `ce9b24d`,早期 `2dc8d6d` / `9b16acc` 仍分 | 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、实际呈现或性能。 | | WindTerm 长历史行的视图折行(#29) | [逐字符观察与边界](research/windterm-history-layout.md)、[两次事实/哈希](../experiments/core/results/2026-10-28-history-layout/windterm-observations.json) | 独立的两次七阶段观察:185 字符软行由 80/80/25 变为 62/62/61,两条 77 字符硬行各变为 62/15;文档文本与位置不变,恢复宽度后投影恢复。仅 ASCII 文本坐标接口;覆盖写入、选择/Find、物理格及呈现仍未知。 | | 候选原生重排边界与未解决的源文本重复(#29) | [工作检查点与反例](../experiments/core/results/2026-10-28-reflow-metadata/README.md)、[来源与验证](../experiments/core/results/2026-10-28-reflow-metadata/validation.json) | `.7` 修复原生硬/软边界传递及缩宽容量计账;Rust 与 runner 通过,C++ 长行复制仍红。混宽历史、来源归属、选择/Find 及完整产品矩阵未完成。 | +| 单次 feed 旧来源追踪(#29) | [固定实现与检查](../experiments/core/results/2026-10-28-feed-origins/README.md) | `.9` 原生 24 项公开集成、既有 workspace、C ABI 与 trace 检查通过;C++ 长历史复制仍红,来源映射不授予历史保留许可,新增成本未测。 | | 候选一次 resize 来源映射与真实淘汰计数(#29) | [接口范围及检查点](../experiments/core/results/2026-10-28-resize-origins/README.md)、[源码/构建/日志身份](../experiments/core/results/2026-10-28-resize-origins/validation.json) | `.8` 导出双屏物理来源和硬边界,修复缩高来源漏记、多行滚动少计与扩高假计;Rust、C++ 映射 ABI 和 runner 通过,C++ 长行复制仍失败,历史合并尚未消费映射。未测性能。 | | WindTerm 首次增高的前置条件停止(#29) | [独立观察与边界](research/windterm-resize-text.md#独立观察初始-18-行直接增高)、[合成输入](../experiments/core/results/2026-10-28-history-layout/windterm-first-grow-input.json)、[事实与哈希](../experiments/core/results/2026-10-28-history-layout/windterm-first-grow-observations.json) | 独立一次 18→26 行观察中未见编号历史进入非负 y;128 次有界查询不变,末尾空串为同一文末位置别名。按计划未发送 CUP/重绘,B 案例未运行,覆盖写入政策仍未知。 | | R2 复杂标量序列输入(#30 限定切片) | [真实键盘接收验证](../experiments/shell/results/2026-10-26-unicode-keyboard/validation.json)、[场景与复现](../experiments/shell/README.md) | 固定 ZWJ/keycap 通过 WM_CHAR/ConPTY/ReadConsoleW 精确接收;只证明输入保真,不证明绘制、字素宽度或完整应用键盘模式。 | diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 2950cde5..f6667122 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -33,7 +33,7 @@ | 中文 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 和有界恢复。显式候选 `.7` 接入 `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。显式候选 `.7` 保留 [#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)覆盖透明输入、主屏缓存归属及重复淘汰;`.8` 的[一次 resize 来源映射](../experiments/core/results/2026-10-28-resize-origins/README.md)及实际淘汰计数已有独立检查,尚未接入 C++ 历史合并 | [#29](https://github.com/JavaZeroo/termforge/issues/29) 的重绘后源历史重复、混宽历史视口及完整 resize/备用屏/滚动区交叉仍未完成;[原版两次文本观察](research/windterm-resize-text.md)仅证明固定输入保留及恢复尺寸后文本投影恢复;[逐字符历史观察](research/windterm-history-layout.md)另证明固定 ASCII 长行随宽度重新分段,未验证覆盖写入或交互。完整 reflow、child ack、实际呈现、系统/实验后端联合配置及性能分别验收;默认缩窄不可恢复 | R2 | +| 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)覆盖透明输入、主屏缓存归属及重复淘汰;`.8` 的[一次 resize 来源映射](../experiments/core/results/2026-10-28-resize-origins/README.md)及实际淘汰计数已有独立检查;`.9` 的[单次 feed 旧来源追踪](../experiments/core/results/2026-10-28-feed-origins/README.md)覆盖真实写入/移动与清空边界,尚未接入 C++ 历史合并 | [#29](https://github.com/JavaZeroo/termforge/issues/29) 的重绘后源历史重复、混宽历史视口及完整 resize/备用屏/滚动区交叉仍未完成;[原版两次文本观察](research/windterm-resize-text.md)仅证明固定输入保留及恢复尺寸后文本投影恢复;[逐字符历史观察](research/windterm-history-layout.md)另证明固定 ASCII 长行随宽度重新分段,未验证覆盖写入或交互。完整 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 c6b048bb..f31e6705 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.9`(Apache-2.0),固定上游 commit `542147067dd016fc5f49120ef0ba605a98a4e3a7` 的 manifest/changelog/许可证与 `src/`,并保留 ED、稳定行身份/淘汰、安全 swap、每缓冲区滚动区域、流式字素、显式历史清空代次、原生历史前缀淘汰、重排断行/容量计账及一次 resize 来源映射补丁;精确传递依赖及校验值由 `Cargo.lock` 固定。未复制上游 46 MiB 行为录制,候选不进入当前产品包;默认 CMake 不依赖 Rust,显式适配器构建使用 staticlib。原始 0.26.0 身份、当前来源、许可证和补丁清单见[候选来源台账](../experiments/candidates/alacritty/PROVENANCE.md)。 +- 外部核心候选当前使用 `alacritty_terminal 0.26.1-dev+termforge.9`(Apache-2.0),固定上游 commit `542147067dd016fc5f49120ef0ba605a98a4e3a7` 的 manifest/changelog/许可证与 `src/`,并保留 ED、稳定行身份/淘汰、安全 swap、每缓冲区滚动区域、流式字素、显式历史清空代次、原生历史前缀淘汰、重排断行/容量计账及一次 resize/feed 来源映射补丁;精确传递依赖及校验值由 `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/research/windterm-resize-text.md b/docs/research/windterm-resize-text.md index c09fa9f1..ae89fcb6 100644 --- a/docs/research/windterm-resize-text.md +++ b/docs/research/windterm-resize-text.md @@ -54,3 +54,31 @@ 本次“初始 18 行直接增高”与前述不同输入、先缩高再恢复等动作下的历史回带具有不同条件;尚未分离输入和动作顺序的影响,不能合成为普遍的增高规则,也不能推断物理 grid。DSR 只作独立 peer 收到回复的字节处理栅栏,不表示呈现、ConPTY child 或物理光标状态;selection、Find、Unicode、性能及覆盖行为未验证。 本次原件、脚本和重建输入哈希已核对;Job 最终 active=0,peer 和 desktop/station 均已清理,profile 只有一个合成会话。公开材料仅含合成输入、白名单事实及哈希,没有私有路径、进程/站名、app/profile 或提取脚本。该研究输入不构成产品策略确认。 + +## 独立观察:恢复高度后发送 DSR + +这是一次新的 45 秒有界观察,复用前述 1091 字节、42 条已知文本及完整五阶段尺寸顺序,不替代旧两次观察或首次 18→26 增高的停止记录。[本次输入](../../experiments/core/results/2026-10-28-history-layout/windterm-restore-fence-input.json)引用旧公开输入的固定 Git blob 与 SHA-256,并记录后加的字节;[本次事实与原件哈希](../../experiments/core/results/2026-10-28-history-layout/windterm-restore-fence-observations.json)保留精简阶段摘要、七个标记坐标及完整有限查询的本地原件哈希。 + +初始 NAWS 为 80×25,发送原 1091 字节后依次得到 80×26→62×26→80×26→80×18→80×26。五阶段的已知文本、文档边界、视图坐标和滚动状态均重现旧观察。缩高后恢复时,本次实际采样确认 H0019–H0025 七个内容标记从负 y 进入非负 y;H0019 的文档范围仍为 342–359,起点矩形从 `[147,-126,8,18]` 回到 `[147,0,8,18]`。 + +随后只发送一次 4 字节 DSR 查询 `ESC[6n`(hex `1b5b366e`),独立 peer 收到一次回复 `1b5b31383b3852`。回复后的稳定采样出现以下变化: + +| 项目 | 恢复高度后、DSR 前 | DSR 回复后 | +|---|---|---| +| peer 实收 NAWS | 80×26 | 80×26 | +| scrollTop | 19 | 26 | +| scrollbar value / max / page | 19 / 19 / 26 | 26 / 26 / 26 | +| visibleLineCount / screenLineCapacity | 44 / 26 | 44 / 26 | +| selectionCaretP / selectionAnchorP 文档位置 | 1038 / 1038 | 1038 / 1038 | +| H0019 起点 y | 0 | −126 | +| 首个非负 y 的已知文本 | H0019 | H0026 | + +六阶段全部 128 槽(索引 0–127)的文字、文档 start/end 和边界集合保持相同,42 条已知输入仍按原顺序存在。单独比较 DSR 前后,128 槽的 visibleLine 与矩形 x/width/height 也相同,全部 256 个起止矩形 y 统一减少 126。窗口和 editor 矩形未变。 + +文末 `R29-END` 位于槽 41、文档范围 1031–1038,起点从 `[147,432,8,18]` 变为 `[147,306,8,18]`。槽 42–127 的 86 次查询均为空串,start=end=1038、visibleLine=43,起止矩形是同一个末尾别名,由 `[203,432,0,18]` 变为 `[203,306,0,18]`;不能据此计为 86 条真实空行,或推断未暴露的空白文档位置、物理空白格。 + +因 DSR 后状态不再相同,探针按冻结条件在 CUP 前停止。实际线上输入只有原 1091 字节和独立 4 字节 DSR;17 字节 CUP@TOP+DSR 未发,后续滚动、额外 ED2、B 案例和第二次运行均未执行。因此覆盖政策仍未知,不能把这次观察解释为 CUP 覆盖结果或普遍增高规则。 + +DSR 回复只作 peer 收到字节回复的处理栅栏,不能假定该步骤对文本视图透明;本次只确定发送 DSR 后发生状态变化,尚未分离 DSR 特定作用、后续输入处理或延迟 resize 工作的原因。内容标记不是原生稳定 ID;文字接口不证明物理 grid、光标或实际呈现,也不构成 ConPTY、性能、selection/Find 或 Unicode 验收。 + +原件、脚本与重建输入哈希已复核;REA 无错误或消息截断,Job 最终 active=0,peer、desktop/station 均已清理,仅有一个合成会话。未操作个人剪贴板、DPI 或输入桌面。完整六阶段查询留在本地,公开文件只保留摘要、必要坐标和哈希,不包含私有路径、进程/站名、app/profile 或提取脚本。 diff --git a/experiments/core/results/2026-10-28-feed-origins/README.md b/experiments/core/results/2026-10-28-feed-origins/README.md new file mode 100644 index 00000000..7763332e --- /dev/null +++ b/experiments/core/results/2026-10-28-feed-origins/README.md @@ -0,0 +1,33 @@ +# 单次输入的旧来源追踪:限定检查点 + +实现提交 `5cb61a84ec2a3a8e494c7970fc1c65985002b0f7`,候选 `0.26.1-dev+termforge.9`。本记录固定该实现的原生与 C ABI 检查,不代表 C++ 历史合并已修复、完整 #29/R2 或 WindTerm 等价通过。[验证记录](validation.json)保存源码、二进制和本地日志哈希;后续实现不能覆盖本次仍失败的结果。 + +## 追踪合同 + +显式 feed 捕获记录仍存活的旧物理格来源;相同文字的实际重写也失去旧来源。插删和滚动按实际移动传递来源,硬边界独立于文本,待续字素不因捕获被封闭。普通解析不分配来源表。C ABI 通过操作类型与共享 epoch 拒绝错误或过期查询,并在后续成功修改时失效。 + +`removed_rows` 是过程事件,不是完整擦除/顺序日志。一次输入先淘汰、再 ED3/RIS、再输出时,最终清空代次不能区分清空前后的移除;它也不提供本次新写后又移除的完整内容。调用方不能将该列表当成外部历史保留许可或清空后直接全部补回。 + +自动软换行另有独立反例:文字、格内容、光标和 WRAPLINE 已正确,但旧硬边界元数据残留。清除残留元数据后该反例通过,未改变真实格内容或光标算法。 + +## 实际验证 + +环境为 Windows 11 x64、Rust 1.92.0、Release C++ 适配器。仅检查核心和跨语言接口,没有执行 transport。 + +| 检查 | 实际结果 | 边界 | +|---|---|---| +| Rust workspace | 163 vendor、10 FFI、2 measure、2 trace、24 feed 集成测试、1 doctest 全通过 | 包括真实 parser 的同字写入、字素续接、插删擦除、宽格清理、硬边界、ALT/RIS、容量与清空、feed→resize 和普通解析一致性 | +| C++ `candidate_resize_map` | 通过 | 真实静态库的 feed/resize 查询、旧来源区间、硬边界、错误请求及有效期 | +| C++ `candidate_core` | **失败** | 停在既有 whole-feed 长历史复制断言;同一函数后续 bytewise 分支未执行 | +| 重建 trace 后 Python runner | 6/6 通过 | 固定正确性与负例,不是性能验收 | +| serde feature check | 通过 | 构建检查 | + +同字重写、跨 feed keycap、ICH 及自动软换行均保留 red→green 日志。透明解析测试的类型错误、滚动测试用目标序号代替原生行号的错误分别归为夹具错误,不冒充产品反例。完整回归只在最终源码执行一次,早期局部运行另留本地记录。 + +C++ 历史合并在本提交尚未消费映射,旧缓存与原生片段重复、空硬行丢失及统一选择/Find/视口归属继续开放。未执行本源码的完整 shell/TUI/剪贴板/IME/实际 DPI/实验后端矩阵;新增分支和捕获分配成本未测。 + +Standards 审查硬规则违反 0、实质维护性异味 0;Spec 审查 0 项。两轴仅覆盖上述固定实现提交,不包含后续 C++ 集成。 + +## 原版观察边界 + +独立的[恢复高度后 DSR 观察](../../../../docs/research/windterm-resize-text.md#独立观察恢复高度后发送-dsr)重现旧五阶段和七个历史标记回带,但单独发送 4 字节查询后,scrollTop 从 19 变为 26,全部文本矩形 y 减少 126;文本和文档范围未变。按预设条件在 CUP 前停止,覆盖政策仍未知。原版记录与上述候选测试独立,不合并为一次验收。 diff --git a/experiments/core/results/2026-10-28-feed-origins/validation.json b/experiments/core/results/2026-10-28-feed-origins/validation.json new file mode 100644 index 00000000..8fd61e5d --- /dev/null +++ b/experiments/core/results/2026-10-28-feed-origins/validation.json @@ -0,0 +1,127 @@ +{ + "schema_version": 1, + "scope": "Experimental parser-feed origin tracking and C ABI; C++ history composition remains incomplete", + "source_commit": "5cb61a84ec2a3a8e494c7970fc1c65985002b0f7", + "source_tree": "5aa5eb9a21b1fd4a44b4f1f10c12e926d6a8c57a", + "candidate_version": "0.26.1-dev+termforge.9", + "configuration": "Windows x64 explicit candidate adapter; native core and ABI only, no transport exercised", + "environment": { + "os": "Windows-11-10.0.26200-SP0", + "rust_toolchain": "1.92.0", + "cmake_configuration": "Release" + }, + "source_identity_note": "Tests ran before commit on the same implementation contents; tested physical-file hashes are recorded separately from Git blob identity.", + "checks": [ + { + "command": "cargo test --manifest-path experiments/candidates/alacritty/Cargo.toml --workspace --locked", + "exit_code": 0, + "result": "163 vendor + 10 FFI + 2 measure + 2 trace + 24 public feed-origin integration + 1 doctest passed", + "log": "workspace-final.log" + }, + { + "command": "ctest --test-dir build/candidate-adapter -C Release -R '^candidate_(core|resize_map)$' --output-on-failure", + "exit_code": 8, + "result": "candidate_resize_map passed; candidate_core stopped at the existing whole-feed duplicate/lost-history assertion; later bytewise branch did not run", + "log": "ctest-final.log" + }, + { + "command": "python -m unittest experiments.candidates.alacritty.test_candidate -v", + "exit_code": 0, + "result": "6 passed against rebuilt trace", + "log": "runner-final.log" + }, + { + "command": "cargo check --manifest-path experiments/candidates/alacritty/Cargo.toml --locked -p alacritty_terminal --features serde", + "exit_code": 0, + "result": "passed", + "log": "serde-final.log" + } + ], + "local_logs": { + "local/r29-feed-origins/workspace-final.log": "802666440a864148aabf271537d29d312a03caaff1cf4388f7959f0535c02030", + "local/r29-feed-origins/build-final.log": "ffa19be302b1cdfb802f29c3770e66792f141cc8f226683cc951a6e1ca01f0b8", + "local/r29-feed-origins/ctest-final.log": "b84ec83f24695e26a9cbb1f8d88b1015a27ef6a5d1d3f3b78225dadf12b3a143", + "local/r29-feed-origins/trace-build-final.log": "5d0ca788d2e31ec1ab5c5b5ecc9d70820f2ed3ae740a00fee4bef72048658cc9", + "local/r29-feed-origins/runner-final.log": "9d2a62eff20dc792f57fefbb0c74b90014dd3e77ddb26eba101dedc47f9f854d", + "local/r29-feed-origins/serde-final.log": "0f73f9764a1efff532705be43050a12c318575cbaf77af456013f784e3437e8d", + "local/r29-feed-origins/equal-write-red.log": "1a56ba53b07b76ba8644cad272a110703d0394ebff7489c0642bb9338c8d4557", + "local/r29-feed-origins/keycap-red.log": "05df7aa4ebb0c5fc6fa4740e8ecf6f7d40c80bc5307fb2ea40f837045cc44ea5", + "local/r29-feed-origins/equal-keycap-green.log": "61760f7be2055f6b01ebdd6034f6bee006418db62e0529a9e4e2e52deb129bbe", + "local/r29-feed-origins/insert-red.log": "7b67566a2ed27a91fe58c7447428e4ad95bdf1eeb86e54bc779332ae5c614405", + "local/r29-feed-origins/insert-green.log": "5e099360a4f6d5cdf0c995e0608a6cd51880fc4e608be024c63d67f935cb83a9", + "local/r29-feed-origins/automatic-wrap-metadata-red.log": "bf3125f042866b2642f1b07c52cccf9da1ad35e326fc0cfdcc62172b60bfb39e", + "local/r29-feed-origins/automatic-wrap-metadata-green.log": "3d28fde15a7cf0222f2fed9dd122e813ef253a6b34ae6ceef7af1c5919ad2421", + "local/r29-feed-origins/parser-transparency-fixture-type-error.log": "9ba50746ee2014397c8b2ba8edcc7e1175f893d335f55727c2cb67aae42d02ed", + "local/r29-feed-origins/scroll-alt-fixture-index-error.log": "ea75ea19751ca7886a52dbc0ea668965ec4072fb6b99b5fbea81287ff76b54ad" + }, + "artifacts": { + "build/candidate-adapter/bin/Release/candidate_resize_map_test.exe": "e09d71f72b0ced6f99ba3ed80c3ab9c3317c042c7ab94af2eb0a2fb3b3a4a2c5", + "build/candidate-adapter/bin/Release/candidate_core_test.exe": "5f1af95f46e69f3b957aebe002dd6a35596b3a19db19c13d4f04099572c95de5", + "experiments/candidates/alacritty/target/release/termforge_alacritty_core.lib": "cc6283118d3b50fc0980e712ea17bf67a3c3d4beb7ed2c9f92bf5d104753d81c", + "experiments/candidates/alacritty/target/release/termforge-alacritty-trace.exe": "3bdd62d6adc0b050356e17bc6707e10abefde24d0617a1d93afaae582567b869" + }, + "physical_source_sha256": { + "docs/EVIDENCE.md": "7773ebadac211b20dc880e29d1586968d23ff71a780f47b699ba27960dd6d2c1", + "docs/FEATURES.md": "48d45744966ca33f23c86484ab2b6164aee53eec348d54bd97a306c840811dcb", + "docs/PROVENANCE.md": "ca0df4fc83eee25928682ae74361d10a3f14de2def935ee5335b8fec67295e46", + "experiments/candidates/alacritty/Cargo.lock": "1827b6b388c90865c56019760caa65ec7253441873cd21df2904b8270c6a8ca1", + "experiments/candidates/alacritty/PROVENANCE.md": "dc71e743871ce9a8fce3cc0fcb1ba2ed07029864b2d04d28bb88111424aa7e60", + "experiments/candidates/alacritty/README.md": "87e168ab6cfefb004ff76b798659bf155ea77de75996dd826a33ad42bdd9b458", + "experiments/candidates/alacritty/include/termforge_candidate.h": "d9217eba71f93f37347bc99f4f66972ab0c72e433f460edfb366042fe0edd038", + "experiments/candidates/alacritty/run.py": "bcd33dab56bacd92348c3a82a530373960cadb8330adb0ee70bd1ecf20dbf6fe", + "experiments/candidates/alacritty/src/ffi.rs": "92bcd8f0f2cdf719bb901885926e2c1586c1071a0b03e1cc456a64cd03152319", + "experiments/candidates/alacritty/test_candidate.py": "678655de0227eb1ace66221d5e0b754a9aa7aec0dbdf88a1001abe9aa7eac24e", + "experiments/candidates/alacritty/tests/feed_origins.rs": "54e386340ae7a093f422d461dec90b022d260d3f315957096fe0be99d7d7e050", + "experiments/candidates/alacritty/vendor/alacritty_terminal/Cargo.toml": "2dd3cdbbbbfe23855c15ccb44f97942718840c94af94c357edc5f5a4b1780983", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/mod.rs": "53547b09ac165e788a814b308499d79d686cbddd585b7a2ca37107a4d5f42fa7", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/origins.rs": "8f44ef735fbf48072654e07f3740b63d9e376547e51c5770dcfda14748a77eb0", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/row.rs": "1cb8956d77a9da2f59b3a66f0ce23b1dfa6773dc89ece087d9e860f0310e8326", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/grapheme.rs": "b325e9a91294c74267ee34564fdd7a92a3c4700c197c18b640f3e738121a506c", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/mod.rs": "515c4b787d1878194c3ea224d4ade64596e0ed9d64eee7f9d3ab640a6f523be3", + "experiments/core/CANDIDATE.md": "3c202ea08f500e81a4913b9068b2235dbb89ac7233d7778dd0bae1a584d0ddfe", + "experiments/core/candidate_resize_map_test.cpp": "bd2b6f64d986e76aa99f6c302dc51a40a5a83459ab558fc0dc8eb08ed6ef109d" + }, + "red_green": [ + { + "case": "equal text explicitly rewritten", + "red": "equal-write-red.log", + "green": "equal-keycap-green.log" + }, + { + "case": "cross-feed keycap extends pending grapheme without retaining old lead source", + "red": "keycap-red.log", + "green": "equal-keycap-green.log" + }, + { + "case": "ICH moves old source cells", + "red": "insert-red.log", + "green": "insert-green.log" + }, + { + "case": "automatic soft wrap clears stale hard-boundary metadata; text, cursor, cells and WRAPLINE already correct", + "red": "automatic-wrap-metadata-red.log", + "green": "automatic-wrap-metadata-green.log" + } + ], + "fixture_failures": [ + "parser-transparency-fixture-type-error.log is a test-fixture type error, not a product failure.", + "scroll-alt-fixture-index-error.log used a target ordinal instead of its native line; it is not a product red." + ], + "limitations": [ + "C++ cached history has not consumed the maps; repeated text, empty hard-line loss, Find and viewport ownership remain unresolved.", + "Maps identify surviving pre-feed physical sources; no origin is assigned to newly written or erased cells, including equal-text rewrites.", + "Removed fragments are events, not a complete write/erase/order log or permission to retain external history. Final history_clear_epoch cannot order evictions before/after a clear.", + "No full shell/TUI/clipboard/IME/DPI/experimental-backend or performance validation ran for this source.", + "Ordinary feeds allocate no origin tables; added branch costs and opt-in capture allocation have not been measured.", + "WindTerm overwrite policy remains unknown. The independent restore/DSR observation stopped before CUP and cannot establish it." + ], + "reviews": { + "base": "2bccddb8e8796b3b244a7252bbf4e1390ee13dbb", + "target": "5cb61a84ec2a3a8e494c7970fc1c65985002b0f7", + "standards_findings": 0, + "spec_findings": 0, + "standards_hard_violations": 0, + "standards_substantive_smells": 0, + "scope": "Independent fixed-commit Standards/Spec read-only review; no new diagnostic or original run; unfinished C++ ownership remains explicit." + } +} diff --git a/experiments/core/results/2026-10-28-history-layout/windterm-restore-fence-input.json b/experiments/core/results/2026-10-28-history-layout/windterm-restore-fence-input.json new file mode 100644 index 00000000..55a59d34 --- /dev/null +++ b/experiments/core/results/2026-10-28-history-layout/windterm-restore-fence-input.json @@ -0,0 +1,50 @@ +{ + "case": "original-five-phase-restore-then-standalone-DSR", + "initial_input": { + "logical_text_source": { + "repository_path": "experiments/core/results/2026-10-27-resize-snapshot/windterm-input.json", + "git_blob_id": "839362216853d4f3c0ad82b0741bf6fe2083cb9f", + "git_blob_sha256": "ea445181775da8f6604880dd5d74a0bc1d104950c988245f37371b1d679879e0" + }, + "logical_input_count": 42, + "encoding": "UTF-8", + "first_peer_naws": [ + 80, + 25 + ], + "preamble_hex": "1b5b324a1b5b481b5b3f3768", + "hard_separator_hex": "0d0a", + "final_newline": false, + "wire_bytes": 1091, + "wire_sha256": "9474531ee34ea08ddd5c3c6b9a19c446852c2aff784dea484c54a91138724b3e", + "appended_dsr": false + }, + "after_restore_input": { + "sequence": 1, + "action": "standalone DSR", + "peer_naws": [ + 80, + 26 + ], + "wire_hex": "1b5b366e", + "wire_bytes": 4, + "wire_sha256": "d907695b104f1c48f93ac2a6f8f6826688a7b359b4b101b26af46022778dbf72", + "observed_reply_hex": [ + "1b5b31383b3852" + ], + "reply_interpretation": "peer-received byte-processing fence only" + }, + "actual_wire_input_bytes": [ + 1091, + 4 + ], + "cup_sent": false, + "not_executed": [ + "17-byte CUP@TOP plus DSR", + "post-CUP scrolling", + "additional ED2", + "B case", + "second run" + ], + "publication": "Reviewed synthetic input, finite-query summaries and hashes only; app, profiles, full raw observations and extraction scripts remain private." +} diff --git a/experiments/core/results/2026-10-28-history-layout/windterm-restore-fence-observations.json b/experiments/core/results/2026-10-28-history-layout/windterm-restore-fence-observations.json new file mode 100644 index 00000000..7048d35f --- /dev/null +++ b/experiments/core/results/2026-10-28-history-layout/windterm-restore-fence-observations.json @@ -0,0 +1,608 @@ +{ + "case": "original-five-phase-restore-then-standalone-DSR", + "observed_version": "WindTerm 2.7.0 Windows x64", + "planned_runs": 1, + "actual_runs": 1, + "instrumentation_limit_seconds": 45, + "result": "valid-precondition-stop-with-post-DSR-state-change", + "target_sha256": "7b10531dccc7cd614ae559f36f1ef518e0c96335637431b4646354823b2804d0", + "distribution_manifest_sha256": "e459adf2f12adb6a1b7f55e66a9da1c22794185e66e848057d21e0c60d669203", + "distribution_files": 4047, + "source_hashes": { + "frozen_manifest_sha256": "4f80abea89731cc3427fd9758368734c17a9a7a646255b412d0b499d45a35449", + "probe_sha256": "c60f5dadf4c1c2d5da8534a4e012c146592a6d462c2f6e3eecefbcb9c82c0fa9", + "peer_sha256": "7f03b91378fdb56253d8d01dc842868bdd761924c8e3505f5f151033b0e1910d", + "template_sha256": "40aaa81a10d9fec334bcdc790d3e5e37d146ba60312ac970a93ddefbe5561e14" + }, + "initial_logical_text_source": { + "repository_path": "experiments/core/results/2026-10-27-resize-snapshot/windterm-input.json", + "git_blob_id": "839362216853d4f3c0ad82b0741bf6fe2083cb9f", + "git_blob_sha256": "ea445181775da8f6604880dd5d74a0bc1d104950c988245f37371b1d679879e0" + }, + "evidence_id": "ev_5cc64eb6655d9e6a92a9976d58312a818ee79d2d7b1d76b0873c8e704dce0893", + "raw_artifact_sha256": { + "evidence.json": "c076b774f91ba906fb8209001806b16daa1bf24f0f19ea58ab3ee2827e1c11ae", + "run.json": "27d89c8154a78636406f1a443a4aa19eef2f40ab792af49b8be2aaf668b31ab3", + "files.json": "a25c87cba935294d5bfd3bbc76c650d31a8473809564d798ad5a889e4aea00e8", + "probe.js": "c60f5dadf4c1c2d5da8534a4e012c146592a6d462c2f6e3eecefbcb9c82c0fa9", + "peer.json": "63433fe11f1f202a5c60111b1c83a66e944b1cfc9c1346a3a52d039569680103", + "input-00.bin": "9474531ee34ea08ddd5c3c6b9a19c446852c2aff784dea484c54a91138724b3e", + "input-00.json": "0b24907ee8defc134f9e57767019a9bdea984641814b23dd0a791ec27709d488", + "input-01.bin": "d907695b104f1c48f93ac2a6f8f6826688a7b359b4b101b26af46022778dbf72", + "input-01.json": "b075ed12d102461d7feb98d9bf350c10fa2f5eeaca839ab6eb7e597986b0a8f0", + "cleanup.json": "8400f93b6a4a4a50cde35b5d474660590ed215732f55772a9d8afac138da4f1d", + "surface-cleanup.json": "2947878016d582f50a3241f5dfaf0c923671e6ddee62cd57e00a78a988ae7954", + "profile-check.json": "5e522a64e98197722ccd084639b1ce50258b238e03be2f016e81c8af023b5ee1" + }, + "complete_private_finite_query_artifact": { + "filename": "text-observations.json", + "sha256": "ce1e7c0d8a4cf33f67b6dd423a7a6e838fe3c478dceffcf230e5e543ca4bc52c", + "phase_count": 6, + "queried_slot_range_inclusive": [ + 0, + 127 + ], + "slots_per_phase": 128 + }, + "finite_query_comparison": { + "all_six_phases_all_128_text_start_end_identical": true, + "all_six_phases_document_boundary_sets_identical": true, + "all_42_known_texts_match_referenced_input_in_exact_order_each_phase": true, + "nonempty_slot_range_inclusive": [ + 0, + 41 + ], + "common_document_boundary_count": 84, + "common_document_boundary_min_max": [ + 0, + 1038 + ], + "common_128_text_start_end_canonical_json_sha256": "1b9836186c1b6f830cb0ca9a071de3439fa36ee0844a34e94763e1dd3cb1b34a", + "end_marker": { + "text": "R29-END", + "queried_index": 41, + "document_range": [ + 1031, + 1038 + ] + }, + "empty_aliases_all_phases": { + "slot_range_inclusive": [ + 42, + 127 + ], + "count": 86, + "text": "", + "start": 1038, + "end": 1038, + "meaning": "end-position aliases, not 86 real empty logical lines" + } + }, + "phase_summaries": [ + { + "phase": "baseline", + "requested_outer": [ + 1230, + 887 + ], + "naws": [ + 80, + 26 + ], + "visibleLineCount": 44, + "capacity": 26, + "caret_document_position": 1038, + "anchor_document_position": 1038, + "scrollTop": 19, + "scrollbar": { + "value": 19, + "max": 19, + "page": 26 + }, + "h0019_start_rect": [ + 147, + 0, + 8, + 18 + ], + "end_marker_start_rect": [ + 147, + 432, + 8, + 18 + ], + "empty_end_alias_rect": [ + 203, + 432, + 0, + 18 + ], + "empty_end_alias_visibleLineFromLine": 43 + }, + { + "phase": "narrow-width", + "requested_outer": [ + 1010, + 887 + ], + "naws": [ + 62, + 26 + ], + "visibleLineCount": 46, + "capacity": 26, + "caret_document_position": 1038, + "anchor_document_position": 1038, + "scrollTop": 20, + "scrollbar": { + "value": 20, + "max": 20, + "page": 26 + }, + "h0019_start_rect": [ + 147, + -18, + 8, + 18 + ], + "end_marker_start_rect": [ + 147, + 450, + 8, + 18 + ], + "empty_end_alias_rect": [ + 203, + 450, + 0, + 18 + ], + "empty_end_alias_visibleLineFromLine": 45 + }, + { + "phase": "restore-width", + "requested_outer": [ + 1230, + 887 + ], + "naws": [ + 80, + 26 + ], + "visibleLineCount": 44, + "capacity": 26, + "caret_document_position": 1038, + "anchor_document_position": 1038, + "scrollTop": 19, + "scrollbar": { + "value": 19, + "max": 19, + "page": 26 + }, + "h0019_start_rect": [ + 147, + 0, + 8, + 18 + ], + "end_marker_start_rect": [ + 147, + 432, + 8, + 18 + ], + "empty_end_alias_rect": [ + 203, + 432, + 0, + 18 + ], + "empty_end_alias_visibleLineFromLine": 43 + }, + { + "phase": "short-height", + "requested_outer": [ + 1230, + 679 + ], + "naws": [ + 80, + 18 + ], + "visibleLineCount": 44, + "capacity": 18, + "caret_document_position": 1038, + "anchor_document_position": 1038, + "scrollTop": 26, + "scrollbar": { + "value": 26, + "max": 26, + "page": 18 + }, + "h0019_start_rect": [ + 147, + -126, + 8, + 18 + ], + "end_marker_start_rect": [ + 147, + 306, + 8, + 18 + ], + "empty_end_alias_rect": [ + 203, + 306, + 0, + 18 + ], + "empty_end_alias_visibleLineFromLine": 43 + }, + { + "phase": "restore-height", + "requested_outer": [ + 1230, + 887 + ], + "naws": [ + 80, + 26 + ], + "visibleLineCount": 44, + "capacity": 26, + "caret_document_position": 1038, + "anchor_document_position": 1038, + "scrollTop": 19, + "scrollbar": { + "value": 19, + "max": 19, + "page": 26 + }, + "h0019_start_rect": [ + 147, + 0, + 8, + 18 + ], + "end_marker_start_rect": [ + 147, + 432, + 8, + 18 + ], + "empty_end_alias_rect": [ + 203, + 432, + 0, + 18 + ], + "empty_end_alias_visibleLineFromLine": 43 + }, + { + "phase": "restore-fenced", + "requested_outer": [ + 1230, + 887 + ], + "naws": [ + 80, + 26 + ], + "visibleLineCount": 44, + "capacity": 26, + "caret_document_position": 1038, + "anchor_document_position": 1038, + "scrollTop": 26, + "scrollbar": { + "value": 26, + "max": 26, + "page": 26 + }, + "h0019_start_rect": [ + 147, + -126, + 8, + 18 + ], + "end_marker_start_rect": [ + 147, + 306, + 8, + 18 + ], + "empty_end_alias_rect": [ + 203, + 306, + 0, + 18 + ], + "empty_end_alias_visibleLineFromLine": 43 + } + ], + "seven_history_markers": [ + { + "text": "R29-H0019|history", + "queried_index": 19, + "document_range": [ + 342, + 359 + ], + "short_start_rect": [ + 147, + -126, + 8, + 18 + ], + "restored_start_rect": [ + 147, + 0, + 8, + 18 + ], + "after_DSR_start_rect": [ + 147, + -126, + 8, + 18 + ] + }, + { + "text": "R29-H0020|history", + "queried_index": 20, + "document_range": [ + 360, + 377 + ], + "short_start_rect": [ + 147, + -108, + 8, + 18 + ], + "restored_start_rect": [ + 147, + 18, + 8, + 18 + ], + "after_DSR_start_rect": [ + 147, + -108, + 8, + 18 + ] + }, + { + "text": "R29-H0021|history", + "queried_index": 21, + "document_range": [ + 378, + 395 + ], + "short_start_rect": [ + 147, + -90, + 8, + 18 + ], + "restored_start_rect": [ + 147, + 36, + 8, + 18 + ], + "after_DSR_start_rect": [ + 147, + -90, + 8, + 18 + ] + }, + { + "text": "R29-H0022|history", + "queried_index": 22, + "document_range": [ + 396, + 413 + ], + "short_start_rect": [ + 147, + -72, + 8, + 18 + ], + "restored_start_rect": [ + 147, + 54, + 8, + 18 + ], + "after_DSR_start_rect": [ + 147, + -72, + 8, + 18 + ] + }, + { + "text": "R29-H0023|history", + "queried_index": 23, + "document_range": [ + 414, + 431 + ], + "short_start_rect": [ + 147, + -54, + 8, + 18 + ], + "restored_start_rect": [ + 147, + 72, + 8, + 18 + ], + "after_DSR_start_rect": [ + 147, + -54, + 8, + 18 + ] + }, + { + "text": "R29-H0024|history", + "queried_index": 24, + "document_range": [ + 432, + 449 + ], + "short_start_rect": [ + 147, + -36, + 8, + 18 + ], + "restored_start_rect": [ + 147, + 90, + 8, + 18 + ], + "after_DSR_start_rect": [ + 147, + -36, + 8, + 18 + ] + }, + { + "text": "R29-H0025|history", + "queried_index": 25, + "document_range": [ + 450, + 467 + ], + "short_start_rect": [ + 147, + -18, + 8, + 18 + ], + "restored_start_rect": [ + 147, + 108, + 8, + 18 + ], + "after_DSR_start_rect": [ + 147, + -18, + 8, + 18 + ] + } + ], + "restore_to_after_DSR": { + "all_128_slot_fields_except_rectangle_y_unchanged": true, + "all_256_start_and_end_rectangle_y_deltas": [ + -126 + ], + "main_and_editor_rects_unchanged": true, + "editor_rect": [ + 0, + 0, + 817, + 470 + ], + "text_state_except_scrollTop_unchanged": true, + "scrollTop_before_after": [ + 19, + 26 + ], + "scrollbar_value_before_after": [ + 19, + 26 + ], + "scrollbar_max_before_after": [ + 19, + 26 + ], + "scrollbar_page_before_after": [ + 26, + 26 + ], + "caret_before_after": [ + 1038, + 1038 + ], + "anchor_before_after": [ + 1038, + 1038 + ], + "visibleLineCount_before_after": [ + 44, + 44 + ], + "capacity_before_after": [ + 26, + 26 + ], + "NAWS_before_after": [ + [ + 80, + 26 + ], + [ + 80, + 26 + ] + ], + "first_nonnegative_known_index_before_after": [ + 19, + 26 + ] + }, + "actual_wire_input_bytes": [ + 1091, + 4 + ], + "dsr_reply_hex": [ + "1b5b31383b3852" + ], + "stop_reason": "state-changed-after-standalone-DSR", + "cup_sent": false, + "cleanup": { + "active": 0, + "total": 3, + "terminated": 0 + }, + "surface_cleanup": { + "desktop_handle_closed": true, + "station_handle_closed": true, + "original_station_restored": true + }, + "profile_check": { + "only_synthetic_session": true, + "session_count": 1 + }, + "limits": [ + "Only one bounded case ran. No CUP was sent; overwrite policy remains unknown.", + "Change was observed after standalone DSR; DSR-specific cause, subsequent input processing and delayed resize work were not isolated.", + "Content markers are not native stable IDs; text slots/rectangles and DSR bytes do not establish physical grid, cursor or presentation.", + "No ConPTY, performance, selection, Find or Unicode acceptance; no general grow rule.", + "Empty end-position aliases do not establish hidden blank document positions or physical blank cells.", + "Earlier first-grow and five-phase observations retain their separate inputs and scope." + ], + "publication": "Reviewed synthetic input, finite-query summaries and hashes only; app, profiles, full raw observations and extraction scripts remain private.", + "peer_cleanup": { + "closed": true, + "error": null + }, + "rea_result": { + "ok": true, + "messages_truncated": false, + "probe_errors": 0 + } +} From b6764a763b118b86d79f62df7168a58badbad669 Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Wed, 28 Oct 2026 03:24:38 +0800 Subject: [PATCH 07/14] wip: compose cell origins for cached history (#29) --- CMakeLists.txt | 4 + experiments/core/candidate_terminal.cpp | 324 ++++++++++++++++--- experiments/core/candidate_terminal.hpp | 28 +- experiments/core/candidate_terminal_test.cpp | 168 ++++++++++ 4 files changed, 477 insertions(+), 47 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 02cd623d..950fbfcf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -272,6 +272,10 @@ if(BUILD_TESTING) if(TERMFORGE_BUILD_CANDIDATE_ADAPTER) add_test(NAME candidate_core COMMAND candidate_core_test) add_test(NAME candidate_resize_map COMMAND candidate_resize_map_test) + add_test(NAME candidate_source_visible_suffix COMMAND candidate_core_test --source-visible-suffix) + add_test(NAME candidate_source_new_background COMMAND candidate_core_test --source-new-background) + add_test(NAME candidate_source_partial_capacity COMMAND candidate_core_test --source-partial-capacity) + set_tests_properties(candidate_source_visible_suffix candidate_source_new_background candidate_source_partial_capacity PROPERTIES TIMEOUT 10) endif() add_test(NAME resize_core COMMAND resize_test) add_test(NAME history_core COMMAND history_test) diff --git a/experiments/core/candidate_terminal.cpp b/experiments/core/candidate_terminal.cpp index 7dc18eb7..04b6b9c7 100644 --- a/experiments/core/candidate_terminal.cpp +++ b/experiments/core/candidate_terminal.cpp @@ -99,11 +99,190 @@ std::vector replay_keys(const History& history, const Lines& lines) } // namespace void CandidateTerminal::begin_replay() { - replay_origin_ = std::make_unique(screen, identity_aliases_, native_discarded_, preserve_history_); + replay_origin_ = std::make_unique(screen, identity_aliases_, published_sources_, native_discarded_, preserve_history_); replay_bytes_ = 0; replay_pending_ = replay_hide_ = replay_show_ = false; } +uint64_t CandidateTerminal::allocate_source() { + if (next_source_ == (std::numeric_limits::max)()) + throw std::overflow_error("candidate cell source exhausted"); + return next_source_++; +} + +bool CandidateTerminal::covers(const RowSources& owner, const RowSources& value) { + if (value.hard_boundary && owner.hard_boundary != value.hard_boundary) return false; + for (const auto& span : value.cells) { + size_t cursor = span.source_column; + const auto end = cursor + span.length; + while (cursor < end) { + size_t next = cursor; + for (const auto& owned : owner.cells) + if (owned.source == span.source && owned.source_column <= cursor && + owned.source_column + owned.length > cursor) + next = (std::max)(next, (std::min)(end, owned.source_column + owned.length)); + if (next == cursor) return false; + cursor = next; + } + } + return !value.cells.empty() || value.hard_boundary != 0; +} + +bool CandidateTerminal::intersects(const RowSources& a, const RowSources& b) { + if (a.hard_boundary && a.hard_boundary == b.hard_boundary) return true; + for (const auto& first : a.cells) for (const auto& second : b.cells) + if (first.source == second.source && first.source_column < second.source_column + second.length && + second.source_column < first.source_column + first.length) return true; + return false; +} + +CandidateTerminal::RowSources CandidateTerminal::projected_sources(const Line& line, const RowSources& origins) { + RowSources result; + result.hard_boundary = origins.hard_boundary; + size_t end = line.size(); + while (end && !line[end - 1].written) --end; + // A blank physical history row still occupies one retained slot. Its old + // padding origins distinguish an unchanged row from an explicit erasure. + if (!end && !origins.hard_boundary) return origins; + for (const auto& span : origins.cells) { + if (span.column >= end) continue; + auto copied = span; + copied.length = (std::min)(copied.length, end - copied.column); + result.cells.push_back(copied); + } + return result; +} + +void CandidateTerminal::compose_sources(bool resize, bool seed_publication) { + const auto meta_query = resize ? tf_terminal_resize_map_meta : tf_terminal_feed_map_meta; + const auto source_query = resize ? tf_terminal_resize_source : tf_terminal_feed_source; + const auto row_query = resize ? tf_terminal_resize_row : tf_terminal_feed_row; + const auto span_query = resize ? tf_terminal_resize_span : tf_terminal_feed_span; + constexpr uint64_t alternate_mask = uint64_t{1} << 12; + const size_t published_buffer = (mode_bits_ & alternate_mask) ? 1 : 0; + for (size_t buffer = 0; buffer < native_sources_.size(); ++buffer) { + TfResizeMapMeta meta{}; + check(meta_query(impl_, buffer != 0, &meta), "candidate source map metadata failed"); + auto& before = native_sources_[buffer]; + for (size_t index = 0; index < meta.source_rows; ++index) { + TfResizeSourceMeta source{}; + check(source_query(impl_, meta.epoch, buffer != 0, index, &source), "candidate source row failed"); + auto found = before.find(source.id); + if (found == before.end()) { + RowSources origins; + if (source.columns) origins.cells.push_back({allocate_source(), 0, 0, source.columns}); + found = before.emplace(source.id, std::move(origins)).first; + } + if (source.hard_break && !found->second.hard_boundary) + found->second.hard_boundary = allocate_source(); + if (!source.hard_break) found->second.hard_boundary = 0; + if (seed_publication && buffer == published_buffer) { + const auto alias = identity_aliases_.find(source.id); + if (alias != identity_aliases_.end()) published_sources_[alias->second.id] = found->second; + } + } + const auto compose = [&](bool removed, size_t row_index, TfResizeRowMeta& row) { + check(row_query(impl_, meta.epoch, buffer != 0, removed, row_index, &row), "candidate mapped row failed"); + RowSources result; + if (row.hard_boundary) { + const auto source = before.find(row.hard_boundary); + if (source == before.end()) throw std::runtime_error("candidate hard boundary source missing"); + result.hard_boundary = source->second.hard_boundary; + } + for (size_t index = 0; index < row.spans; ++index) { + TfResizeSpan span{}; + check(span_query(impl_, meta.epoch, buffer != 0, removed, row_index, index, &span), + "candidate mapped span failed"); + const auto source = before.find(span.source_row); + if (source == before.end()) throw std::runtime_error("candidate cell source missing"); + for (const auto& old : source->second.cells) { + const auto first = (std::max)(span.source_column, old.column); + const auto last = (std::min)(span.source_column + span.len, old.column + old.length); + if (first < last) result.cells.push_back({old.source, old.source_column + first - old.column, + span.column + first - span.source_column, last - first}); + } + } + std::sort(result.cells.begin(), result.cells.end(), [](const SourceSpan& a, const SourceSpan& b) { + return a.column < b.column; + }); + if (!removed) { + // A gap has no old origin. Give the final new cells a fresh source + // for the next transaction, without guessing from their text. + std::vector complete; + size_t column = 0; + for (const auto& span : result.cells) { + if (span.column < column || span.column + span.length > row.columns) + throw std::runtime_error("candidate mapped source bounds differ"); + if (column < span.column) + complete.push_back({allocate_source(), 0, column, span.column - column}); + complete.push_back(span); + column = span.column + span.length; + } + if (column < row.columns) complete.push_back({allocate_source(), 0, column, row.columns - column}); + result.cells = std::move(complete); + } + return result; + }; + SourceBindings after; + for (size_t index = 0; index < meta.target_rows; ++index) { + TfResizeRowMeta row{}; + auto origins = compose(false, index, row); + after.emplace(row.id, std::move(origins)); + } + if (buffer == 0) for (size_t index = 0; index < meta.removed_rows; ++index) { + TfResizeRowMeta row{}; + auto origins = compose(true, index, row); + if (row.removal_reason == 1) capacity_sources_.push_back(std::move(origins)); + } + before = std::move(after); + } +} + +void CandidateTerminal::subtract_capacity_sources(SourceBindings& bindings) { + if (capacity_sources_.empty()) return; + for (auto& entry : bindings) for (const auto& removed : capacity_sources_) { + auto& owner = entry.second; + if (removed.hard_boundary && owner.hard_boundary == removed.hard_boundary) + owner.hard_boundary = 0; + for (const auto& lost : removed.cells) { + std::vector surviving; + for (const auto& span : owner.cells) { + const auto first = (std::max)(span.source_column, lost.source_column); + const auto last = (std::min)(span.source_column + span.length, lost.source_column + lost.length); + if (span.source != lost.source || first >= last) { + surviving.push_back(span); + continue; + } + if (span.source_column < first) + surviving.push_back({span.source, span.source_column, span.column, first - span.source_column}); + if (last < span.source_column + span.length) + surviving.push_back({span.source, last, span.column + last - span.source_column, + span.source_column + span.length - last}); + } + owner.cells = std::move(surviving); + } + } +} + +Line CandidateTerminal::remaining_source_cells(const Line& line, RowSources& origins) { + Line result = line; + result.clear(); + // Origins retain the old physical columns until publication. Copy complete + // Cell values, including styles and wide spacers, without rebuilding text. + for (auto& span : origins.cells) { + const auto column = span.column; + span.column = result.size(); + if (column > line.size() || span.length > line.size() - column) + throw std::runtime_error("candidate archived source bounds differ"); + result.insert(result.end(), line.begin() + column, line.begin() + column + span.length); + } + if (line.ending == LineEnding::hard_break && !origins.hard_boundary) + result.ending = LineEnding::none; + if (result.ending == LineEnding::soft_wrap) + result.wrap_columns = (std::min)(result.wrap_columns, result.size()); + return result; +} + void CandidateTerminal::scan_replay(std::string_view bytes) { const std::string data = replay_tail_ + std::string(bytes); for (size_t position = 0; (position = data.find("\x1b[?25", position)) != std::string::npos; position += 6) { @@ -173,82 +352,108 @@ std::unordered_set CandidateTerminal::reconcile_replay(std::vector CandidateTerminal::merge_history(std::vector& retained, - std::vector& original_ids, size_t& history, uint64_t& discarded, uint64_t& native_discarded) { + std::vector& original_ids, std::vector& sources, + 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 (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()); + const auto& old_sources = replay_origin_ ? replay_origin_->sources : published_sources_; + std::vector> native_history; + for (size_t index = 0; index < history; ++index) + native_history.emplace_back(original_ids[index], projected_sources(retained[index], sources[index])); + RowSources resident; + std::unordered_set resident_boundaries; + for (const auto& origins : sources) { + resident.cells.insert(resident.cells.end(), origins.cells.begin(), origins.cells.end()); + if (origins.hard_boundary) resident_boundaries.insert(origins.hard_boundary); + } + std::deque previous; + SourceBindings previous_sources; + for (const auto& line : source.history) { + const auto old = old_sources.find(line.id); + if (old == old_sources.end() || (old->second.cells.empty() && !old->second.hard_boundary)) continue; + auto origins = old->second; + auto remaining = remaining_source_cells(line, origins); + // A full old owner can span several final native rows across the + // history/viewport boundary. Do not restore its duplicate if those + // rows together still contain all its meaningful source intervals. + // Owners wholly in history retain their existing publication identity. + resident.hard_boundary = resident_boundaries.count(origins.hard_boundary) ? origins.hard_boundary : 0; + const auto meaningful = projected_sources(remaining, origins); + bool visible = false; + if (covers(resident, meaningful)) { + for (size_t index = history; index < retained.size(); ++index) + if (intersects(meaningful, projected_sources(retained[index], sources[index]))) { visible = true; break; } + } + if (visible) continue; + previous.push_back(std::move(remaining)); + previous_sources.emplace(line.id, std::move(origins)); } 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; - for (const auto& line : previous) - if (replay_origin_ || raw_ids.find(line.id) == raw_ids.end()) candidates.insert(line.id); - for (const auto& line : previous) { - if (candidates.find(line.id) == candidates.end()) continue; - const auto key = replay_key(line); - if (key.empty()) continue; - for (size_t index = 0; index < history; ++index) - if (retained[index].id == line.id && replay_key(retained[index]) == key) { - unchanged_history.insert(line.id); - break; - } - for (size_t index = history; index < retained.size(); ++index) - if (retained[index].id == line.id && replay_key(retained[index]) == key) { - unchanged_visible.insert(line.id); - break; - } + + for (size_t index = 0; index < history;) { + const bool owned = std::any_of(previous.begin(), previous.end(), [&](const Line& line) { + return covers(previous_sources.at(line.id), projected_sources(retained[index], sources[index])); + }); + if (!owned) { ++index; continue; } + retained.erase(retained.begin() + index); + original_ids.erase(original_ids.begin() + index); + sources.erase(sources.begin() + index); + --history; } - screen.normalize_identities(retained); + size_t insertion = 0; for (auto& line : previous) { - if (candidates.find(line.id) == candidates.end() || !line.id || replay_key(line).empty() || - unchanged_history.find(line.id) != unchanged_history.end() || - unchanged_visible.find(line.id) != unchanged_visible.end()) continue; - const auto position = std::lower_bound(retained.begin(), retained.begin() + history, line.id, - [](const Line& candidate, uint64_t id) { return candidate.id < id; }); - const auto index = static_cast(position - retained.begin()); + const auto origins = previous_sources.at(line.id); + const auto index = insertion++; + const auto position = retained.begin() + index; retained.insert(position, std::move(line)); original_ids.insert(original_ids.begin() + index, 0); + sources.insert(sources.begin() + index, origins); ++history; } std::vector evicted_native; + std::vector evicted_sources; 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()); + evicted_sources.push_back(projected_sources(retained.front(), sources.front())); retained.erase(retained.begin()); original_ids.erase(original_ids.begin()); + sources.erase(sources.begin()); --history; } + // A cached owner can cover native fragments omitted from the publication. + // Retire their exact native prefix too, so an empty feed cannot import them + // again and charge the same eviction twice. + for (const auto& native : native_history) { + if (!native.first || !std::any_of(evicted_sources.begin(), evicted_sources.end(), [&](const RowSources& old) { + return covers(old, native.second); + })) break; + evicted_native.push_back(native.first); + } 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{}; + const auto native_before = native_discarded; check(tf_terminal_discard_history(impl_, evicted_native.data(), evicted_native.size(), &native_discarded, error.data(), error.size()), "candidate history eviction failed"); + for (const auto id : evicted_native) native_sources_[0].erase(id); + const auto native_count = native_discarded - native_before; + if (native_count > evicted.size()) discarded += native_count - evicted.size(); } return evicted; } void CandidateTerminal::feed(std::string_view bytes) { std::array error{}; - check(tf_terminal_feed(impl_, reinterpret_cast(bytes.data()), bytes.size(), + const auto feed = tracking_sources_ ? tf_terminal_feed_mapped : tf_terminal_feed; + check(feed(impl_, reinterpret_cast(bytes.data()), bytes.size(), error.data(), error.size()), "candidate feed failed"); + // Native lineage advances even while a pending repaint freezes publication. + if (tracking_sources_) compose_sources(false); scan_replay(bytes); synchronize(); uint32_t kind = 0; @@ -283,6 +488,8 @@ void CandidateTerminal::resize(int rows, int columns) { std::array error{}; check(tf_terminal_resize(impl_, static_cast(rows), static_cast(columns), error.data(), error.size()), "candidate resize failed"); + compose_sources(true, !tracking_sources_); + tracking_sources_ = true; begin_replay(); screen.resize(rows, columns); synchronize(); @@ -306,9 +513,18 @@ void CandidateTerminal::synchronize() { suspended_primary_.reset(); preserve_history_ = false; identity_aliases_.clear(); + published_sources_.clear(); + capacity_sources_.clear(); replay_pending_ = replay_hide_ = replay_show_ = false; replay_bytes_ = 0; } + // Apply removal events to every snapshot that could later restore an old + // owner. Keep only surviving intervals, so pending replay cannot resurrect + // a resize eviction after the transaction's event list has been released. + subtract_capacity_sources(published_sources_); + if (replay_origin_) subtract_capacity_sources(replay_origin_->sources); + if (suspended_primary_) subtract_capacity_sources(suspended_primary_->sources); + capacity_sources_.clear(); // 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; @@ -318,7 +534,7 @@ void CandidateTerminal::synchronize() { // 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_); + std::make_unique(screen, identity_aliases_, published_sources_, native_discarded_, preserve_history_); if (replay_pending_) suspended_primary_->preserve_history = true; } replay_origin_.reset(); @@ -348,6 +564,8 @@ void CandidateTerminal::synchronize() { std::vector retained(meta.history + meta.rows); std::vector raw_ids; raw_ids.reserve(retained.size()); + std::vector row_sources; + row_sources.reserve(retained.size()); for (size_t r = 0; r < retained.size(); ++r) { TfRowMeta row{}; check(tf_terminal_row_meta(impl_, r, &row), "candidate row metadata failed"); @@ -356,6 +574,16 @@ void CandidateTerminal::synchronize() { raw_ids.push_back(line.id); line.ending = row.wrap ? LineEnding::soft_wrap : row.hard_break ? LineEnding::hard_break : LineEnding::none; line.wrap_columns = row.wrap_columns; + RowSources origins; + if (tracking_sources_) { + auto found = native_sources_[alternate ? 1 : 0].find(row.id); + if (found == native_sources_[alternate ? 1 : 0].end()) + throw std::runtime_error("candidate publication source missing"); + if (line.ending == LineEnding::hard_break && !found->second.hard_boundary) + found->second.hard_boundary = allocate_source(); + origins = found->second; + } + row_sources.push_back(std::move(origins)); line.resize(meta.columns); for (size_t c = 0; c < meta.columns; ++c) { TfCellMeta cell{}; @@ -378,7 +606,7 @@ void CandidateTerminal::synchronize() { 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); + evicted = merge_history(retained, raw_ids, row_sources, meta.history, meta.discarded, native_discarded); cached_history_discarded_ += (meta.discarded - before_merge) - (native_discarded - native_before_merge); } screen.normalize_identities(retained); @@ -415,8 +643,12 @@ void CandidateTerminal::synchronize() { screen.lines.assign(retained.begin() + meta.history, retained.end()); versions_ = std::move(versions); identity_aliases_.clear(); - for (size_t index = 0; index < retained.size(); ++index) + published_sources_.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 (tracking_sources_) published_sources_.emplace(retained[index].id, std::move(row_sources[index])); + } + capacity_sources_.clear(); 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. diff --git a/experiments/core/candidate_terminal.hpp b/experiments/core/candidate_terminal.hpp index 9b228a9d..68383352 100644 --- a/experiments/core/candidate_terminal.hpp +++ b/experiments/core/candidate_terminal.hpp @@ -1,5 +1,6 @@ #pragma once #include "screen.hpp" +#include #include #include #include @@ -23,15 +24,32 @@ class CandidateTerminal { std::vector events_; struct IdentityAlias { uint64_t id = 0, version = 0; }; std::unordered_map identity_aliases_; + struct SourceSpan { + uint64_t source = 0; + size_t source_column = 0, column = 0, length = 0; + }; + struct RowSources { + std::vector cells; + uint64_t hard_boundary = 0; + }; + using SourceBindings = std::unordered_map; + // These keys describe cell lineage, independently of publication row IDs. + std::array native_sources_; + SourceBindings published_sources_; + std::vector capacity_sources_; + uint64_t next_source_ = 1; + bool tracking_sources_ = false; uint64_t native_discarded_ = 0; struct Publication { Screen screen; std::unordered_map aliases; + SourceBindings sources; uint64_t native_discarded; bool preserve_history; Publication(const Screen& value, const std::unordered_map& ids, + const SourceBindings& origins, uint64_t discarded, bool preserve) - : screen(value), aliases(ids), native_discarded(discarded), preserve_history(preserve) {} + : screen(value), aliases(ids), sources(origins), native_discarded(discarded), preserve_history(preserve) {} }; std::unique_ptr replay_origin_; // The alternate grid has no primary scrollback; retain its owner separately. @@ -41,10 +59,18 @@ class CandidateTerminal { bool replay_pending_ = false, replay_hide_ = false, replay_show_ = false; bool preserve_history_ = false; + uint64_t allocate_source(); + void compose_sources(bool resize, bool seed_publication = false); + static bool covers(const RowSources& owner, const RowSources& value); + static bool intersects(const RowSources& a, const RowSources& b); + static RowSources projected_sources(const Line& line, const RowSources& origins); + void subtract_capacity_sources(SourceBindings& bindings); + static Line remaining_source_cells(const Line& line, RowSources& origins); void begin_replay(); void scan_replay(std::string_view bytes); std::unordered_set reconcile_replay(std::vector& retained); std::unordered_set merge_history(std::vector& retained, std::vector& original_ids, + std::vector& sources, 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 56e47595..735d5ffd 100644 --- a/experiments/core/candidate_terminal_test.cpp +++ b/experiments/core/candidate_terminal_test.cpp @@ -592,6 +592,154 @@ void cached_history_resize_replay_preserves_long_text() { } } +void cached_history_preserves_empty_hard_boundaries() { + const std::string expected = "\r\n\r\nABCDEFGH\r\nB\r\nC\r\nD"; + for (const bool fragmented : {false, true}) { + termforge::CandidateTerminal terminal(2, 8, 16); + 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(expected); + terminal.resize(4, 4); + feed("\x1b[?25l\x1b[2J\x1b[HC\r\nD\x1b[?25h"); + termforge::TextDocument document(terminal.screen); + const auto begin = document.position(0, 0); + const auto end = document.position(terminal.screen.history.size() + 1, 1); + termforge::TextSelection selection; + require(begin && end && selection.set(document, {*begin, *end}), "empty-boundary selection failed"); + const auto copy = selection.copy(document, 128); + require(copy.status == termforge::SelectionCopyStatus::ok && copy.text == expected, + "resize replay lost consecutive empty hard boundaries"); + } +} + +void cached_history_preserves_cross_soft_find() { + const std::string expected = "ABCDEFGHIJKL\r\nB\r\nC\r\nD"; + for (const bool fragmented : {false, true}) { + termforge::CandidateTerminal terminal(2, 8, 16); + 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(expected); + terminal.resize(4, 4); + feed("\x1b[?25l\x1b[2J\x1b[HC\r\nD\x1b[?25h"); + termforge::TextDocument document(terminal.screen); + termforge::TextSearch search; + const auto generation = search.start(document, "GHIJ"); + for (int step = 0; step < 100 && search.status() == termforge::SearchStatus::running; ++step) + search.step(document, generation, 64); + const auto match = search.match(document, 0); + termforge::TextSelection selection; + require(search.status() == termforge::SearchStatus::complete && search.matches().size() == 1 && match && + selection.set(document, match->range) && selection.copy(document, 128).text == "GHIJ", + "resize replay lost or duplicated a Find hit across the original soft boundary"); + const auto begin = document.position(0, 0); + const auto end = document.position(terminal.screen.history.size() + 1, 1); + require(begin && end && selection.set(document, {*begin, *end}) && + selection.copy(document, 128).text == expected, "cross-soft replay copy differs"); + } +} + +void cached_history_partial_visibility() { + termforge::CandidateTerminal terminal(2, 8, 16); + terminal.feed("ABCDEFGH\r\nB\r\nC\r\nD"); + terminal.resize(4, 4); + terminal.feed("\x1b[?25l\x1b[2;1H\x1b[2K\x1b[?25h"); + termforge::TextDocument document(terminal.screen); + std::cout << state(terminal) << '\n'; + termforge::TextSelection selection; + const auto begin = document.position(0, 0), end = document.position(document.size() - 1, 1); + require(begin && end && selection.set(document, {*begin, *end}), "partial history selection failed"); + std::cout << "copy="; + termforge::Screen::quoted(std::cout, selection.copy(document, 128).text); + std::cout << '\n'; + termforge::TextSearch search; + auto generation = search.start(document, "EFGH"); + for (int step = 0; step < 100 && search.status() == termforge::SearchStatus::running; ++step) + search.step(document, generation, 64); + std::cout << "EFGH matches=" << search.matches().size() << '\n'; +} + +void cached_history_visible_suffix_is_not_duplicated() { + const std::string expected = "ABCDEFGH\r\nB\r\nC\r\nD"; + termforge::CandidateTerminal terminal(2, 8, 16); + terminal.feed(expected); + terminal.resize(4, 4); + terminal.feed("\x1b[?25l\x1b[?25h"); + termforge::TextDocument document(terminal.screen); + termforge::TextSelection selection; + const auto begin = document.position(0, 0), end = document.position(document.size() - 1, 1); + require(begin && end && selection.set(document, {*begin, *end}), "visible suffix selection failed"); + const auto copied = selection.copy(document, 128); + require(copied.status == termforge::SelectionCopyStatus::ok && copied.text == expected, + "unchanged visible suffix was duplicated by its archived owner"); +} + +void cached_history_keeps_new_native_background() { + for (const bool replay : {false, true}) { + termforge::CandidateTerminal terminal(2, 12, 16); + terminal.feed("ABCDEFGHIJKL\r\nB\r\nC\r\nD"); + terminal.resize(5, 4); + std::string bytes = "\x1b[1;4H\x1b[41m\x1b[X\x1b[0m\x1b[5;1H\n"; + if (replay) bytes = "\x1b[?25l" + bytes + "\x1b[?25h"; + terminal.feed(bytes); + bool background = false; + for (const auto& line : terminal.screen.history) + for (const auto& cell : line) + background |= cell.style.bg == 1; + require(background, replay ? "cached owner swallowed new native background" : + "native background control fixture failed"); + } +} + +void cached_history_partial_capacity_keeps_unremoved_source() { + for (const bool fragmented : {false, true}) { + termforge::CandidateTerminal terminal(2, 12, 1); + 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("ABCDEFGHIJKL\r\nB\r\nC"); + termforge::TextDocument original(terminal.screen); + termforge::TextSelection old_prefix; + const auto old_begin = original.position(0, 0), old_end = original.position(0, 4); + require(old_begin && old_end && old_prefix.set(original, {*old_begin, *old_end}), + "partial capacity original selection failed"); + terminal.resize(3, 4); + require(terminal.screen.history.size() == 1 && text(terminal.screen.history[0]) == "EFGH" && + text(terminal.screen.lines[0]) == "IJKL" && terminal.screen.discarded == 1, + "partial capacity native fixture differs"); + feed("\x1b[?25l\x1b[2J\x1b[HB\r\nC\x1b[?25h"); + termforge::TextDocument document(terminal.screen); + require(!document.valid(*old_begin) && + old_prefix.copy(document, 128).status == termforge::SelectionCopyStatus::invalidated, + "partial capacity retained a selection of the evicted prefix"); + termforge::TextSelection selection; + const auto begin = document.position(0, 0), end = document.position(terminal.screen.history.size() + 1, 1); + require(begin && end && selection.set(document, {*begin, *end}), "partial capacity selection failed"); + const auto copied = selection.copy(document, 128); + std::ostringstream failure; + failure << "capacity removal lost surviving source or restored an evicted prefix: "; + termforge::Screen::quoted(failure, copied.text); + require(copied.status == termforge::SelectionCopyStatus::ok && copied.text == "EFGHIJKL\r\nB\r\nC", + failure.str().c_str()); + termforge::TextSearch search; + const auto generation = search.start(document, "IJKL"); + for (int step = 0; step < 100 && search.status() == termforge::SearchStatus::running; ++step) + search.step(document, generation, 64); + const auto match = search.match(document, 0); + require(search.status() == termforge::SearchStatus::complete && search.matches().size() == 1 && match && + selection.set(document, match->range) && selection.copy(document, 128).text == "IJKL", + "partial capacity survivor is not selectable through Find"); + const auto unchanged = state(terminal); + feed(""); + require(state(terminal) == unchanged && terminal.screen.discarded == 1, + "partial capacity empty feed repeated removal or changed its count"); + } +} + void cached_history_stays_with_primary(bool fragmented) { termforge::CandidateTerminal terminal(2, 8, 4); terminal.feed("A\r\nB\r\nC\r\nD"); @@ -795,6 +943,8 @@ void conpty_resize_replay(const std::filesystem::path& path) { pending_replay_keeps_resized_grid(); cached_history_survives_noop(); cached_history_resize_replay_preserves_long_text(); + cached_history_preserves_empty_hard_boundaries(); + cached_history_preserves_cross_soft_find(); cached_history_stays_with_primary(false); cached_history_stays_with_primary(true); for (const bool fragmented : {false, true}) { @@ -911,6 +1061,24 @@ void resize_and_errors() { int main(int argc, char** argv) { try { + // Named scenarios allow independent source-ownership regression checks. + // The partial-write observation reports state without choosing policy. + if (argc > 1 && std::string_view(argv[1]) == "--source-partial-visible") { + cached_history_partial_visibility(); + return 0; + } + if (argc > 1 && std::string_view(argv[1]) == "--source-visible-suffix") { + cached_history_visible_suffix_is_not_duplicated(); + return 0; + } + if (argc > 1 && std::string_view(argv[1]) == "--source-new-background") { + cached_history_keeps_new_native_background(); + return 0; + } + if (argc > 1 && std::string_view(argv[1]) == "--source-partial-capacity") { + cached_history_partial_capacity_keeps_unremoved_source(); + return 0; + } fragmentation(); streaming_graphemes_selection_and_history(); streaming_margin_padding_is_not_copied_as_an_inserted_space(); From eea3a2c7d79ceffba95065a2544a5fe6f67aed3c Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Wed, 28 Oct 2026 03:28:51 +0800 Subject: [PATCH 08/14] fix: retain streaming graphemes during history eviction (#29) --- experiments/candidates/alacritty/Cargo.lock | 2 +- .../candidates/alacritty/PROVENANCE.md | 10 +- experiments/candidates/alacritty/README.md | 2 +- experiments/candidates/alacritty/run.py | 2 +- experiments/candidates/alacritty/src/ffi.rs | 100 +++++++++++++++++- .../candidates/alacritty/test_candidate.py | 2 +- .../vendor/alacritty_terminal/Cargo.toml | 2 +- .../vendor/alacritty_terminal/src/term/mod.rs | 7 ++ 8 files changed, 119 insertions(+), 8 deletions(-) diff --git a/experiments/candidates/alacritty/Cargo.lock b/experiments/candidates/alacritty/Cargo.lock index 095e75f4..d62dcadf 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.9" +version = "0.26.1-dev+termforge.10" dependencies = [ "arrayvec", "base64", diff --git a/experiments/candidates/alacritty/PROVENANCE.md b/experiments/candidates/alacritty/PROVENANCE.md index 52d76748..336aa274 100644 --- a/experiments/candidates/alacritty/PROVENANCE.md +++ b/experiments/candidates/alacritty/PROVENANCE.md @@ -1,6 +1,6 @@ # 固定候选来源与本地补丁 -当前适配版本为 `alacritty_terminal 0.26.1-dev+termforge.9`,不是 crates.io 原始 `0.26.0`。 +当前适配版本为 `alacritty_terminal 0.26.1-dev+termforge.10`,不是 crates.io 原始 `0.26.0`。 ## 上游身份 @@ -86,7 +86,13 @@ 2. 不封闭跨 feed 的待续字素;主/备用屏按事务前后真实切换正确命名。公开 Rust 集成测试核对同字重写、宽格关联清除、字素增长/缩减、插删擦除、硬边界、ALT/RIS 及后续 resize,不依赖内部 helper。 3. 修正非底行自动软换行后残留的硬边界元数据;文字、格内容、光标和原有 WRAPLINE 行为不变。C ABI 新增显式 `tf_terminal_feed_mapped` 和 feed 查询,复用物理来源记录布局,与 resize 共用单调 epoch 并拒绝错误操作类型或过期查询。 4. 移除记录是事务中的事件,不是完整擦除/先后顺序日志,也不授予外部历史保留许可。同一 feed 先淘汰、后 ED3/RIS、再输出时,最终清空代次不能区分清空前后的移除事件;调用方不能先清空缓存再无条件补回全部移除片段。 -5. 升为 `0.26.1-dev+termforge.9`;固定上游、依赖和许可证不变。新增分支及映射分配成本未测;C++ 历史合并尚未消费来源映射,已知长历史复制失败与完整 #29/R2 验收继续开放。 +5. 升为 `0.26.1-dev+termforge.9`;固定上游、依赖和许可证不变。新增分支及映射分配成本未测;该检查点的 C++ 历史合并尚未消费来源映射,长历史复制仍失败。后续适配状态见 [CANDIDATE](../../core/CANDIDATE.md),完整 #29/R2 验收继续开放。 + +## termforge.10 补丁 + +1. `Term::discard_oldest_history` 只淘汰历史前缀,不经过会封闭待续字素的通用 `grid_mut()`;可见格与光标不因历史维护改变。FFI 保持完整前缀校验、计数及成功调用使临时来源映射失效的合同。 +2. 公开 C ABI 反例确认合法空/非空淘汰曾将跨 feed 的 `👩‍💻` 拆成两格字素加另一字素,光标列从预期 2 变为 4。修复后完整文本、宽度/spacer、光标、映射有效期通过;错误前缀控制不改状态。 +3. 升为 `0.26.1-dev+termforge.10`,上游 commit、注册表依赖及许可证不变。本补丁为独立维护修复,不含 WindTerm 私有源码;不裁决原版覆盖政策或证明性能等价。 ## 历史基线 diff --git a/experiments/candidates/alacritty/README.md b/experiments/candidates/alacritty/README.md index 87719fa9..b8554a62 100644 --- a/experiments/candidates/alacritty/README.md +++ b/experiments/candidates/alacritty/README.md @@ -6,7 +6,7 @@ | 项目 | 当前固定值 | 来源 | |---|---|---| -| `alacritty_terminal` | `0.26.1-dev+termforge.9`,Apache-2.0 | [上游 commit `5421470`](https://github.com/alacritty/alacritty/commit/542147067dd016fc5f49120ef0ba605a98a4e3a7)与[来源/补丁台账](PROVENANCE.md) | +| `alacritty_terminal` | `0.26.1-dev+termforge.10`,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/run.py b/experiments/candidates/alacritty/run.py index 7bfb6ff7..d343b173 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.9" +VERSION = "0.26.1-dev+termforge.10" UPSTREAM_COMMIT = "542147067dd016fc5f49120ef0ba605a98a4e3a7" BASE_CRATE_SHA256 = "bda177466b9524d59f1b12f0dd30b68696788e9992a7e959021c4a0ed96fcf59" diff --git a/experiments/candidates/alacritty/src/ffi.rs b/experiments/candidates/alacritty/src/ffi.rs index 40e74d4c..8684929c 100644 --- a/experiments/candidates/alacritty/src/ffi.rs +++ b/experiments/candidates/alacritty/src/ffi.rs @@ -465,7 +465,7 @@ pub unsafe extern "C" fn tf_terminal_discard_history( } } value.origin_map = None; - value.term.grid_mut().discard_oldest_history(count); + value.term.discard_oldest_history(count); unsafe { *discarded = value.term.grid().discarded() }; Ok(()) }) @@ -897,6 +897,104 @@ mod tests { assert_eq!(value.meta().discarded, 3, "explicit history clear is not capacity eviction"); } + + fn open_grapheme_with_history() -> (Handle, ResizeMapMeta) { + let value = Handle(tf_terminal_new(2, 8, 2)); + assert!(!value.0.is_null()); + let input = "A\r\nB\r\n\u{1f469}".as_bytes(); + assert_eq!(unsafe { + tf_terminal_feed_mapped(value.0, input.as_ptr(), input.len(), null_mut(), 0) + }, 0); + let meta = value.meta(); + assert_eq!((meta.history, meta.row, meta.column, meta.discarded), (1, 1, 2, 0)); + let mut map = MaybeUninit::::uninit(); + assert_eq!(unsafe { tf_terminal_feed_map_meta(value.0, false, map.as_mut_ptr()) }, 0); + (value, unsafe { map.assume_init() }) + } + + fn expect_joined_grapheme_after_history_eviction(value: &Handle, previous_epoch: u64) { + let suffix = "\u{200d}\u{1f4bb}".as_bytes(); + assert_eq!(unsafe { + tf_terminal_feed_mapped(value.0, suffix.as_ptr(), suffix.len(), null_mut(), 0) + }, 0); + let meta = value.meta(); + assert_eq!((meta.row, meta.column), (1, 2), "history maintenance sealed the visible grapheme"); + let row = meta.history + 1; + let mut lead = MaybeUninit::::uninit(); + assert_eq!(unsafe { tf_terminal_cell_meta(value.0, row, 0, lead.as_mut_ptr()) }, 0); + let lead = unsafe { lead.assume_init() }; + assert_eq!((lead.width, lead.marks, lead.written), (2, 2, true)); + assert!(!lead.clipped && !lead.grapheme_fragment && !lead.leading_spacer); + let mut text = [0; 32]; + let mut needed = 0; + assert_eq!(unsafe { + tf_terminal_cell_text(value.0, row, 0, text.as_mut_ptr(), text.len(), &mut needed) + }, 0); + assert_eq!(&text[..needed], "\u{1f469}\u{200d}\u{1f4bb}".as_bytes()); + let mut spacer = MaybeUninit::::uninit(); + assert_eq!(unsafe { tf_terminal_cell_meta(value.0, row, 1, spacer.as_mut_ptr()) }, 0); + assert_eq!(unsafe { spacer.assume_init() }.width, 0); + let mut following = MaybeUninit::::uninit(); + assert_eq!(unsafe { tf_terminal_cell_meta(value.0, row, 2, following.as_mut_ptr()) }, 0); + assert!(!unsafe { following.assume_init() }.written, "suffix created another lead"); + let mut next_map = MaybeUninit::::uninit(); + assert_eq!(unsafe { tf_terminal_feed_map_meta(value.0, false, next_map.as_mut_ptr()) }, 0); + assert!(unsafe { next_map.assume_init() }.epoch > previous_epoch); + } + + #[test] + fn history_eviction_empty_prefix_preserves_open_grapheme() { + let (value, mut map) = open_grapheme_with_history(); + let epoch = map.epoch; + let mut discarded = u64::MAX; + assert_eq!(unsafe { + tf_terminal_discard_history(value.0, std::ptr::null(), 0, &mut discarded, null_mut(), 0) + }, 0); + assert_eq!(discarded, 0); + let meta = value.meta(); + assert_eq!((meta.history, meta.row, meta.column), (1, 1, 2)); + assert_eq!(unsafe { tf_terminal_feed_map_meta(value.0, false, &mut map) }, 1); + assert_eq!(map.epoch, epoch, "expired-map query changed the output"); + expect_joined_grapheme_after_history_eviction(&value, epoch); + } + + #[test] + fn history_eviction_valid_prefix_preserves_open_grapheme() { + let (value, mut map) = open_grapheme_with_history(); + let epoch = map.epoch; + let prefix = value.rows()[0].0; + let mut discarded = u64::MAX; + assert_eq!(unsafe { + tf_terminal_discard_history(value.0, &prefix, 1, &mut discarded, null_mut(), 0) + }, 0); + assert_eq!(discarded, 1); + let meta = value.meta(); + assert_eq!((meta.history, meta.row, meta.column), (0, 1, 2)); + assert_eq!(value.rows()[0].1, b'B'); + assert_eq!(unsafe { tf_terminal_feed_map_meta(value.0, false, &mut map) }, 1); + assert_eq!(map.epoch, epoch, "expired-map query changed the output"); + expect_joined_grapheme_after_history_eviction(&value, epoch); + } + + #[test] + fn history_eviction_rejected_prefix_preserves_open_grapheme() { + let (value, mut map) = open_grapheme_with_history(); + let epoch = map.epoch; + let before = value.rows(); + let wrong_prefix = before[0].0 ^ 1; + let mut discarded = u64::MAX; + assert_eq!(unsafe { + tf_terminal_discard_history(value.0, &wrong_prefix, 1, &mut discarded, null_mut(), 0) + }, 1); + assert_eq!(discarded, u64::MAX); + assert_eq!(value.rows(), before); + let meta = value.meta(); + assert_eq!((meta.history, meta.row, meta.column, meta.discarded), (1, 1, 2, 0)); + assert_eq!(unsafe { tf_terminal_feed_map_meta(value.0, false, &mut map) }, 0); + assert_eq!(map.epoch, epoch, "rejected eviction expired the map"); + expect_joined_grapheme_after_history_eviction(&value, epoch); + } + #[test] fn history_eviction_checks_entire_prefix_before_mutating() { let value = Handle(tf_terminal_new(2, 8, 4)); diff --git a/experiments/candidates/alacritty/test_candidate.py b/experiments/candidates/alacritty/test_candidate.py index 85b7ef93..e0f795df 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.9") + self.assertEqual(self.summary["version"], "0.26.1-dev+termforge.10") 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 d753b7a8..892a53bb 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.9" +version = "0.26.1-dev+termforge.10" 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/term/mod.rs b/experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/mod.rs index 75a725f3..5117c55c 100644 --- a/experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/mod.rs +++ b/experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/mod.rs @@ -682,6 +682,13 @@ impl Term { &mut self.grid } + /// Evict an oldest history prefix without ending the visible trailing grapheme. + /// + /// This maintenance changes neither visible cells nor cursor coordinates. + pub fn discard_oldest_history(&mut self, count: usize) { + self.grid.discard_oldest_history(count); + } + /// Resize terminal to new dimensions. pub fn resize(&mut self, size: S) { self.seal_grapheme(); From caeb5bfd1accc93c3eea7ff1a9d25a256e3149f5 Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Wed, 28 Oct 2026 03:33:27 +0800 Subject: [PATCH 09/14] docs: record fixed-state original cursor write (#29) --- docs/research/windterm-resize-text.md | 22 + .../windterm-fenced-cup-input.json | 88 +++ .../windterm-fenced-cup-observations.json | 616 ++++++++++++++++++ 3 files changed, 726 insertions(+) create mode 100644 experiments/core/results/2026-10-28-history-layout/windterm-fenced-cup-input.json create mode 100644 experiments/core/results/2026-10-28-history-layout/windterm-fenced-cup-observations.json diff --git a/docs/research/windterm-resize-text.md b/docs/research/windterm-resize-text.md index ae89fcb6..6c84870b 100644 --- a/docs/research/windterm-resize-text.md +++ b/docs/research/windterm-resize-text.md @@ -82,3 +82,25 @@ DSR 回复只作 peer 收到字节回复的处理栅栏,不能假定该步骤对文本视图透明;本次只确定发送 DSR 后发生状态变化,尚未分离 DSR 特定作用、后续输入处理或延迟 resize 工作的原因。内容标记不是原生稳定 ID;文字接口不证明物理 grid、光标或实际呈现,也不构成 ConPTY、性能、selection/Find 或 Unicode 验收。 原件、脚本与重建输入哈希已复核;REA 无错误或消息截断,Job 最终 active=0,peer、desktop/station 均已清理,仅有一个合成会话。未操作个人剪贴板、DPI 或输入桌面。完整六阶段查询留在本地,公开文件只保留摘要、必要坐标和哈希,不包含私有路径、进程/站名、app/profile 或提取脚本。 + +## 独立观察:DSR 后固定状态下的 CUP 改写 + +本次是新的单次 45 秒有界观察;旧两次五阶段、first-grow 停止记录及上次 DSR 后前置条件停止记录均保持原结论。此次事先将上次实际观察到的 DSR 后完整状态冻结为 CUP 的前置条件。[三段合成输入](../../experiments/core/results/2026-10-28-history-layout/windterm-fenced-cup-input.json)记录精确字节与哈希;[本次观察和原件哈希](../../experiments/core/results/2026-10-28-history-layout/windterm-fenced-cup-observations.json)记录门控、双样本及有限查询差异。 + +再次复现同一 1091B、42 条文本和五阶段后,H0019–H0025 回带成立。随后独立发送 4B DSR,收到 `18;8R`;top/max26、容量26、visibleLineCount44、选择文档位置1038及全部128槽均匹配冻结状态。每个阶段都保存两个完整且相同的128槽样本,间隔分别为265、266、265、282、328、329、344ms,均至少250ms。完整前置门槛先落盘并回读,peer独立复核、写出回执后才允许第三段发送。回执与发送记录处于同一粗粒度时间戳;其先后来自受控执行顺序及持久化记录,不据此计算时间差。 + +第三段只有一次17B `CUP1;1 + CUP@TOP + DSR`,第二次收到 `1;8R`。三段共1112B,没有额外ED2、滚动操作、B案例或重跑。写入后128槽中**仅槽26文字**从 `R29-H0026|history` 变为 `CUP@TOP26|history`:文档范围仍468–485,visibleLine26,起点矩形仍为 `[147,0,8,18]`,后缀 `26|history` 保留。H0019未改;其余127槽全部字段、所有128槽的非文字字段及其余完整状态均未改变。有限查询中旧H0026整段不再出现,其他41条旧已知文本仍各出现一次,`CUP@TOP`只出现一次;未观察到新旧两段并存或额外副本。 + +| 项目 | CUP 前的 DSR 后状态 | 17B 序列后 | +|---|---|---| +| 协议回复 row / column | 18 / 8 | 1 / 8 | +| selectionCaretP / selectionAnchorP 文档位置 | 1038 / 1038 | 1038 / 1038 | +| scrollTop;scrollbar value / max / page | 26;26 / 26 / 26 | 相同 | +| visibleLineCount / screenLineCapacity | 44 / 26 | 相同 | +| 文末 `R29-END` 的终点矩形 | `[203,306,0,18]` | 相同 | + +选择字段保持1038,而协议回复从18;8变为1;8;这些selection字段不能充当本观察里的终端光标坐标或身份。槽42–127仍为空串、start=end1038、visibleLine43及同一末尾矩形的86次别名返回,不代表86条真实空逻辑行。 + +本案只确定**已查询且满足冻结状态时,整个17B CUP+DSR序列最终改写了H0026对应文字**。它不回答未发DSR时的CUP位置,不分离DSR特定作用、后续输入或延迟resize,不证明物理activegrid的历史存储方式,也不是普遍覆盖政策或用户确认的行为选择。末尾DSR仍可能影响视图,不能称为单独CUP的无扰动测量;内容标记不是原生稳定ID。两次相同有限采样不表示整个45秒持续稳定,本次亦不构成实际呈现、ConPTY、性能、selection/Find或Unicode验收。 + +REA及执行进程退出均为0,消息未截断、无探针错误;Job最终active0/total3,peer关闭且无错误,desktop/station已关闭并恢复原station,只有一个合成会话。未操作个人剪贴板、DPI或输入桌面。公开材料只包含合成输入、白名单事实和哈希;完整原件与提取脚本继续留在本地。 diff --git a/experiments/core/results/2026-10-28-history-layout/windterm-fenced-cup-input.json b/experiments/core/results/2026-10-28-history-layout/windterm-fenced-cup-input.json new file mode 100644 index 00000000..437e7ba3 --- /dev/null +++ b/experiments/core/results/2026-10-28-history-layout/windterm-fenced-cup-input.json @@ -0,0 +1,88 @@ +{ + "case": "original-post-DSR-fenced-state-CUP", + "actual_runs": 1, + "observation_limit_seconds": 45, + "sequences": [ + { + "sequence": 0, + "action": "initial", + "body_bytes": 1091, + "body_sha256": "9474531ee34ea08ddd5c3c6b9a19c446852c2aff784dea484c54a91138724b3e", + "wire_bytes": 1091, + "wire_sha256": "9474531ee34ea08ddd5c3c6b9a19c446852c2aff784dea484c54a91138724b3e", + "peer_naws": [ + 80, + 25 + ], + "logical_text_source": { + "repository_path": "experiments/core/results/2026-10-27-resize-snapshot/windterm-input.json", + "git_blob_id": "839362216853d4f3c0ad82b0741bf6fe2083cb9f", + "git_blob_sha256": "ea445181775da8f6604880dd5d74a0bc1d104950c988245f37371b1d679879e0" + }, + "encoding": "UTF-8", + "logical_input_count": 42, + "preamble_hex": "1b5b324a1b5b481b5b3f3768", + "hard_separator_hex": "0d0a", + "final_newline": false, + "appended_dsr": false + }, + { + "sequence": 1, + "action": "restore-fence", + "body_bytes": 0, + "body_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "wire_bytes": 4, + "wire_sha256": "d907695b104f1c48f93ac2a6f8f6826688a7b359b4b101b26af46022778dbf72", + "peer_naws": [ + 80, + 26 + ], + "body_hex": "", + "appended_query_hex": "1b5b366e", + "wire_hex": "1b5b366e" + }, + { + "sequence": 2, + "action": "cup", + "body_bytes": 13, + "body_sha256": "0c3712c29d7551ebff62d418cf41d72b1d8159842bf9475b2698dd4868f0e68f", + "wire_bytes": 17, + "wire_sha256": "49c5a60820f6774abddedce417cf750d22736c419cea9f42e124337d6321e2f6", + "peer_naws": [ + 80, + 26 + ], + "body_hex": "1b5b313b314843555040544f50", + "appended_query_hex": "1b5b366e", + "wire_hex": "1b5b313b314843555040544f501b5b366e" + } + ], + "actual_wire_input_bytes": [ + 1091, + 4, + 17 + ], + "total_wire_input_bytes": 1112, + "replies": [ + { + "after_input_sequence": 1, + "hex": "1b5b31383b3852", + "reported_row": 18, + "reported_column": 8 + }, + { + "after_input_sequence": 2, + "hex": "1b5b313b3852", + "reported_row": 1, + "reported_column": 8 + } + ], + "reply_interpretation": "Observed protocol response values; query processing can affect state. They are not proof of physical cells, presentation or ConPTY.", + "not_executed": [ + "extra ED2", + "post-CUP scroll mutation", + "B case", + "second run" + ], + "publication": "Synthetic bytes, sanitized finite observations and hashes only; no application, profile, private paths or extraction scripts." +} diff --git a/experiments/core/results/2026-10-28-history-layout/windterm-fenced-cup-observations.json b/experiments/core/results/2026-10-28-history-layout/windterm-fenced-cup-observations.json new file mode 100644 index 00000000..d240b1a5 --- /dev/null +++ b/experiments/core/results/2026-10-28-history-layout/windterm-fenced-cup-observations.json @@ -0,0 +1,616 @@ +{ + "case": "original-post-DSR-fenced-state-CUP", + "status": "one completed bounded observation; no product acceptance or policy selection", + "actual_runs": 1, + "limits": { + "rea_observation_seconds": 45, + "scenario_deadline_ms": 43000, + "outer_cli_timeout_seconds": 60 + }, + "original_identity": { + "product": "WindTerm", + "version": "2.7.0", + "platform": "Windows x64", + "exe_sha256": "7b10531dccc7cd614ae559f36f1ef518e0c96335637431b4646354823b2804d0", + "copied_files": 4047, + "canonical_distribution_manifest_sha256": "e459adf2f12adb6a1b7f55e66a9da1c22794185e66e848057d21e0c60d669203" + }, + "frozen_plan_sha256": "2181352f2b1df81a8a103e1ef70615d40b27c311c633af32e268d31fea945a65", + "frozen_source_manifest_sha256": "211d5a60c21fb3c6a306198ccce9ed0b90490177b01e46919768e48dee480793", + "frozen_source_file_sha256": { + "plan.json": "2181352f2b1df81a8a103e1ef70615d40b27c311c633af32e268d31fea945a65", + "prepare_probe.py": "c0a9c90b0422abfcdc217abc260b61002cc302c6e2af34d6fe96a2ee3efedcd6", + "validate_and_freeze.py": "a706b1c0a3ba2a3b363d74b17a9e09549dd27d89b2ef19675b3c37d4526f0e91", + "launch.py": "96f11ec10374b3bcf5fbd9c2377a21edbf0ba6deaca1a3400d0e4a829db7dffc", + "peer.py": "741755bcf9dd715ebb06bf26784947d26bf654795c1a9a2688123eaee9f41bc8", + "qt_r1_text.js": "267c40af273e671e75d51b44f757674375203831471382c3f3d7990fe931cb3c", + "r1_menu_wrap.js": "249337c6e48c14184e9f79c61d2c86c940a8f40637f7d2215d42482086552b55", + "input-original.bin": "9474531ee34ea08ddd5c3c6b9a19c446852c2aff784dea484c54a91138724b3e", + "input-original.json": "c37f68e63d8a9f7beb2743639b3e96771f4ca1ce5cc0a893228cdabd86a78342", + "expected-phases.json": "f20d1a208a1214b2cfc98e380a0ceaadd90f7f2fc42ec126e481ba8589c725cf", + "pre-cup-state.json": "e8660fa9cfe15d20f81bcce76b9bd0e81b30d36974a05909a6e3ef33486082ac", + "provider-source-manifest.json": "b2d0fe975a294a1e54cb12d7bdd0998b8628162259a3d0d7ba0ff866fc6d0417", + "preparation-verification.json": "73811868702a5177e20c8bf27f937fd012b751c2ab4f37bb8ff95f866744a3a1", + "distribution-verification.json": "82ea4ae069153b53d615d37acc5a9bacda61745f44a7f112304c7fd3bdd44129", + "offline-validation.json": "253fa43da0277439959d8a470b5c56cddcb9ad19d7855fe6011613e09b534b8a" + }, + "actual_probe_sha256": "e559af1182cbbb5c737fc033f02dafac7135e0e73ba85d5ba629709c142043b1", + "probe_template_sha256": "ab1819af4796ddba1bf47e5d9fb41b88f6474f94478adb33c2f0e6b5b04ab9ce", + "evidence_id": "ev_4afb2d0daf461e4e895628deceafc76fb325250738d6e6bb363faf565c492de4", + "private_artifact_sha256": { + "evidence.json": "cfe109d9d15687821546a31d3d620f67a269f13e932d1a5fc98a235226d339c7", + "probe.js": "e559af1182cbbb5c737fc033f02dafac7135e0e73ba85d5ba629709c142043b1", + "run.json": "466ccd6690c4608f92f6f825b4f31afa0c2226068d2f6429e1a09a49779ba587", + "peer.json": "900356beb2e0e12ea1c76dfd0ff3e78d212ab71ab1ec9006fbfc87861221d4be", + "input-00.bin": "9474531ee34ea08ddd5c3c6b9a19c446852c2aff784dea484c54a91138724b3e", + "input-01.bin": "d907695b104f1c48f93ac2a6f8f6826688a7b359b4b101b26af46022778dbf72", + "input-02.bin": "49c5a60820f6774abddedce417cf750d22736c419cea9f42e124337d6321e2f6", + "fenced-state-gate.json": "6ec20c1ab25d4379fd60089fc0ed7e1d529d88e687824439c1d5586c893be5c2", + "gate-verified-before-cup.json": "eed80c5e25bb275b59a9080c7b6cac32f7ab658339ac9a97cdd8015d13f605a7", + "cleanup.json": "8400f93b6a4a4a50cde35b5d474660590ed215732f55772a9d8afac138da4f1d", + "surface-cleanup.json": "2947878016d582f50a3241f5dfaf0c923671e6ddee62cd57e00a78a988ae7954", + "profile-check.json": "5e522a64e98197722ccd084639b1ce50258b238e03be2f016e81c8af023b5ee1", + "complete-finite-query-export": "4b74fdeb5cf51d7b6aabe658517b36face7248a230df65995f0a848c9fa8126a", + "private-analysis-summary": "8e36c1e59acaf1c7a4271226b5e7122eeae96a75dbfc4f2c8b1a32008545754b" + }, + "finite_query_scope": { + "indices": [ + 0, + 127 + ], + "slots_per_complete_sample": 128, + "known_nonempty_text_slots": 42, + "end_alias_indices": [ + 42, + 127 + ], + "end_alias_count": 86, + "end_alias_text": "", + "end_alias_start_end": 1038, + "end_alias_visible_line": 43, + "end_alias_start_end_rect": [ + 203, + 306, + 0, + 18 + ], + "alias_interpretation": "Repeated terminal document-end aliases, not86 real empty logical rows.", + "alias_values_apply_to": [ + "restore-fenced", + "after-cup" + ] + }, + "naws_sequence": [ + [ + 80, + 25 + ], + [ + 80, + 26 + ], + [ + 62, + 26 + ], + [ + 80, + 26 + ], + [ + 80, + 18 + ], + [ + 80, + 26 + ] + ], + "phases": [ + { + "phase": "baseline", + "elapsed_ms": 2640, + "requested_outer": [ + 1230, + 887 + ], + "peer_naws": [ + 80, + 26 + ], + "editor_rect": [ + 0, + 0, + 817, + 470 + ], + "scrollbar": { + "value": 19, + "max": 19, + "page": 26 + }, + "text_metadata": { + "scroll": 19, + "visibleLineCount": 44, + "capacity": 26, + "caret": 1038, + "anchor": 1038, + "selection": "R29-END\r\n", + "queried_slots": 128 + }, + "complete_equal_sample_elapsed_ms": [ + 2375, + 2640 + ], + "complete_equal_sample_interval_ms": 265, + "complete_sample_query_slots_each": 128 + }, + { + "phase": "narrow-width", + "elapsed_ms": 3422, + "requested_outer": [ + 1010, + 887 + ], + "peer_naws": [ + 62, + 26 + ], + "editor_rect": [ + 0, + 0, + 671, + 470 + ], + "scrollbar": { + "value": 20, + "max": 20, + "page": 26 + }, + "text_metadata": { + "scroll": 20, + "visibleLineCount": 46, + "capacity": 26, + "caret": 1038, + "anchor": 1038, + "selection": "R29-END\r\n", + "queried_slots": 128 + }, + "complete_equal_sample_elapsed_ms": [ + 3156, + 3422 + ], + "complete_equal_sample_interval_ms": 266, + "complete_sample_query_slots_each": 128 + }, + { + "phase": "restore-width", + "elapsed_ms": 4218, + "requested_outer": [ + 1230, + 887 + ], + "peer_naws": [ + 80, + 26 + ], + "editor_rect": [ + 0, + 0, + 817, + 470 + ], + "scrollbar": { + "value": 19, + "max": 19, + "page": 26 + }, + "text_metadata": { + "scroll": 19, + "visibleLineCount": 44, + "capacity": 26, + "caret": 1038, + "anchor": 1038, + "selection": "R29-END\r\n", + "queried_slots": 128 + }, + "complete_equal_sample_elapsed_ms": [ + 3953, + 4218 + ], + "complete_equal_sample_interval_ms": 265, + "complete_sample_query_slots_each": 128 + }, + { + "phase": "short-height", + "elapsed_ms": 5047, + "requested_outer": [ + 1230, + 679 + ], + "peer_naws": [ + 80, + 18 + ], + "editor_rect": [ + 0, + 0, + 817, + 331 + ], + "scrollbar": { + "value": 26, + "max": 26, + "page": 18 + }, + "text_metadata": { + "scroll": 26, + "visibleLineCount": 44, + "capacity": 18, + "caret": 1038, + "anchor": 1038, + "selection": "R29-END\r\n", + "queried_slots": 128 + }, + "complete_equal_sample_elapsed_ms": [ + 4765, + 5047 + ], + "complete_equal_sample_interval_ms": 282, + "complete_sample_query_slots_each": 128 + }, + { + "phase": "restore-height", + "elapsed_ms": 5937, + "requested_outer": [ + 1230, + 887 + ], + "peer_naws": [ + 80, + 26 + ], + "editor_rect": [ + 0, + 0, + 817, + 470 + ], + "scrollbar": { + "value": 19, + "max": 19, + "page": 26 + }, + "text_metadata": { + "scroll": 19, + "visibleLineCount": 44, + "capacity": 26, + "caret": 1038, + "anchor": 1038, + "selection": "R29-END\r\n", + "queried_slots": 128 + }, + "complete_equal_sample_elapsed_ms": [ + 5609, + 5937 + ], + "complete_equal_sample_interval_ms": 328, + "complete_sample_query_slots_each": 128 + }, + { + "phase": "restore-fenced", + "elapsed_ms": 6797, + "requested_outer": [ + 1230, + 887 + ], + "peer_naws": [ + 80, + 26 + ], + "editor_rect": [ + 0, + 0, + 817, + 470 + ], + "scrollbar": { + "value": 26, + "max": 26, + "page": 26 + }, + "text_metadata": { + "scroll": 26, + "visibleLineCount": 44, + "capacity": 26, + "caret": 1038, + "anchor": 1038, + "selection": "R29-END\r\n", + "queried_slots": 128 + }, + "complete_equal_sample_elapsed_ms": [ + 6468, + 6797 + ], + "complete_equal_sample_interval_ms": 329, + "complete_sample_query_slots_each": 128 + }, + { + "phase": "after-cup", + "elapsed_ms": 7687, + "requested_outer": [ + 1230, + 887 + ], + "peer_naws": [ + 80, + 26 + ], + "editor_rect": [ + 0, + 0, + 817, + 470 + ], + "scrollbar": { + "value": 26, + "max": 26, + "page": 26 + }, + "text_metadata": { + "scroll": 26, + "visibleLineCount": 44, + "capacity": 26, + "caret": 1038, + "anchor": 1038, + "selection": "R29-END\r\n", + "queried_slots": 128 + }, + "complete_equal_sample_elapsed_ms": [ + 7343, + 7687 + ], + "complete_equal_sample_interval_ms": 344, + "complete_sample_query_slots_each": 128 + } + ], + "old_path_gates": { + "five_phase_projections_matched": true, + "historical_entry_content_markers": [ + "R29-H0019|history", + "R29-H0020|history", + "R29-H0021|history", + "R29-H0022|history", + "R29-H0023|history", + "R29-H0024|history", + "R29-H0025|history" + ], + "h0019_document_range": [ + 342, + 359 + ], + "h0019_short_start_rect": [ + 147, + -126, + 8, + 18 + ], + "h0019_restored_start_rect": [ + 147, + 0, + 8, + 18 + ] + }, + "pre_cup_gate": { + "accepted": true, + "checks": { + "exact_state": true, + "exact_reply": true, + "two_complete_samples": true, + "separation": true + }, + "expected_state_sha256": "e8660fa9cfe15d20f81bcce76b9bd0e81b30d36974a05909a6e3ef33486082ac", + "elapsed_ms": 6797, + "complete_samples_equal_to_frozen_state": true, + "complete_sample_elapsed_ms": [ + 6468, + 6797 + ], + "gate_record_sha256": "6ec20c1ab25d4379fd60089fc0ed7e1d529d88e687824439c1d5586c893be5c2", + "peer_receipt_sha256": "eed80c5e25bb275b59a9080c7b6cac32f7ab658339ac9a97cdd8015d13f605a7", + "raw_message_ordinal_gate": 35, + "raw_message_ordinal_cup_request": 36, + "gate_written_read_back_then_peer_verified_before_send": true, + "peer_receipt_verified_before_input_sequence": 2, + "receipt_and_send_share_coarse_timestamp": true, + "ordering_basis": "Complete gate file was written/read back before command; peer independently validated it and persisted its receipt before the third send. Equal coarse timestamps do not measure a positive interval.", + "raw_message_ordinal_base": 0 + }, + "input_and_reply_order": { + "peer_clock_origin": "first input send-start; separate from driver elapsed clock", + "send_start_relative_ms": [ + 0, + 4140, + 5015 + ], + "reply_observed_relative_ms": [ + 4140, + 5015 + ] + }, + "post_cup_comparison": { + "changed_slots": [ + { + "index": 26, + "before": { + "index": 26, + "text": "R29-H0026|history", + "start": 468, + "end": 485, + "visibleLine": 26, + "startRect": [ + 147, + 0, + 8, + 18 + ], + "endRect": [ + 283, + 0, + 17, + 18 + ] + }, + "after": { + "index": 26, + "text": "CUP@TOP26|history", + "start": 468, + "end": 485, + "visibleLine": 26, + "startRect": [ + 147, + 0, + 8, + 18 + ], + "endRect": [ + 283, + 0, + 17, + 18 + ] + } + } + ], + "other127_slots_all_fields_equal": true, + "all128_nontext_slot_fields_equal": true, + "all_other_state_fields_equal": true, + "old_h0026_full_text_occurrences_after": 0, + "other41_old_known_texts_each_occur_once": true, + "cup_marker_occurrences_after": 1, + "h0019_before": { + "index": 19, + "text": "R29-H0019|history", + "start": 342, + "end": 359, + "visibleLine": 19, + "startRect": [ + 147, + -126, + 8, + 18 + ], + "endRect": [ + 283, + -126, + 17, + 18 + ] + }, + "h0019_after": { + "index": 19, + "text": "R29-H0019|history", + "start": 342, + "end": 359, + "visibleLine": 19, + "startRect": [ + 147, + -126, + 8, + 18 + ], + "endRect": [ + 283, + -126, + 17, + 18 + ] + }, + "tail_before": { + "index": 41, + "text": "R29-END", + "start": 1031, + "end": 1038, + "visibleLine": 43, + "startRect": [ + 147, + 306, + 8, + 18 + ], + "endRect": [ + 203, + 306, + 0, + 18 + ] + }, + "tail_after": { + "index": 41, + "text": "R29-END", + "start": 1031, + "end": 1038, + "visibleLine": 43, + "startRect": [ + 147, + 306, + 8, + 18 + ], + "endRect": [ + 203, + 306, + 0, + 18 + ] + } + }, + "protocol_reply_and_selection_fields": [ + { + "phase": "restore-fenced", + "reply_hex": "1b5b31383b3852", + "reported_row": 18, + "reported_column": 8, + "selectionCaretP": 1038, + "selectionAnchorP": 1038 + }, + { + "phase": "after-cup", + "reply_hex": "1b5b313b3852", + "reported_row": 1, + "reported_column": 8, + "selectionCaretP": 1038, + "selectionAnchorP": 1038 + } + ], + "completion": { + "scenario_complete_elapsed_ms": 7687, + "phase_count": 7, + "rea_exit": 0, + "exec_exit": 0, + "messages_truncated": false, + "probe_errors": 0, + "job": { + "active": 0, + "total": 3, + "terminated": 0 + }, + "peer_closed_without_error": true, + "surfaces": { + "desktop_handle_closed": true, + "station_handle_closed": true, + "original_station_restored": true + }, + "synthetic_session_count": 1 + }, + "limitations": [ + "Only this previously queried post-DSR state and complete17-byte CUP+DSR sequence.", + "No conclusion about unqueried grow, DSR causality, physical cells, active-buffer ownership or general overwrite policy.", + "selectionCaretP/AnchorP remained1038 while the DSR report changed18;8 to1;8; they must not be used as terminal cursor coordinates.", + "Stable sampling means two equal finite samples; not continuous stability throughout the45-second attach window.", + "Markers are content, not native stableIDs; empty out-of-range aliases are not additional logical rows.", + "No presentation, ConPTY, performance, selection/Find or Unicode acceptance.", + "No full raw query sets, application/profile content, private process/desktop identifiers or extraction scripts are published." + ] +} From 95cd2c38ad36b5c4bc756192b9a8780466936a0a Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Wed, 28 Oct 2026 03:36:00 +0800 Subject: [PATCH 10/14] fix: preserve styled padding origins in history (#29) --- experiments/core/candidate_terminal.cpp | 5 +++- experiments/core/candidate_terminal_test.cpp | 24 ++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/experiments/core/candidate_terminal.cpp b/experiments/core/candidate_terminal.cpp index 04b6b9c7..47ff9290 100644 --- a/experiments/core/candidate_terminal.cpp +++ b/experiments/core/candidate_terminal.cpp @@ -140,7 +140,10 @@ CandidateTerminal::RowSources CandidateTerminal::projected_sources(const Line& l RowSources result; result.hard_boundary = origins.hard_boundary; size_t end = line.size(); - while (end && !line[end - 1].written) --end; + // A cell without copyable text can still carry visible style or glyph + // metadata. Only complete default padding is inert for source ownership. + const Cell padding; + while (end && same_cell(line[end - 1], padding)) --end; // A blank physical history row still occupies one retained slot. Its old // padding origins distinguish an unchanged row from an explicit erasure. if (!end && !origins.hard_boundary) return origins; diff --git a/experiments/core/candidate_terminal_test.cpp b/experiments/core/candidate_terminal_test.cpp index 735d5ffd..abdd7742 100644 --- a/experiments/core/candidate_terminal_test.cpp +++ b/experiments/core/candidate_terminal_test.cpp @@ -694,6 +694,29 @@ void cached_history_keeps_new_native_background() { } } +void cached_history_keeps_new_native_bold_space() { + for (const bool replay : {false, true}) { + termforge::CandidateTerminal terminal(2, 12, 16); + terminal.feed("ABCDEFGHIJKL\r\nB\r\nC\r\nD"); + terminal.resize(5, 4); + std::string bytes = "\x1b[1;4H\x1b[1m \x1b[0m\x1b[5;1H\n"; + if (replay) bytes = "\x1b[?25l" + bytes + "\x1b[?25h"; + terminal.feed(bytes); + size_t bold_spaces = 0; + for (const auto& line : terminal.screen.history) { + if (line.size() <= 3) continue; + const auto& cell = line[3]; + if (!cell.style.bold) continue; + require(cell.text == " " && cell.width == 1 && cell.style.fg == -1 && cell.style.bg == -1 && + !cell.written && cell.marks == 0 && !cell.clipped && !cell.grapheme_fragment && + !cell.leading_spacer, "native bold-space Cell attributes differ"); + ++bold_spaces; + } + require(bold_spaces == 1, replay ? "cached owner swallowed new native bold space" : + "native bold-space control fixture failed"); + } +} + void cached_history_partial_capacity_keeps_unremoved_source() { for (const bool fragmented : {false, true}) { termforge::CandidateTerminal terminal(2, 12, 1); @@ -1073,6 +1096,7 @@ int main(int argc, char** argv) { } if (argc > 1 && std::string_view(argv[1]) == "--source-new-background") { cached_history_keeps_new_native_background(); + cached_history_keeps_new_native_bold_space(); return 0; } if (argc > 1 && std::string_view(argv[1]) == "--source-partial-capacity") { From 8521cd7f497f52ab169d76a5a99a976f6adfc8df Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Wed, 28 Oct 2026 03:54:18 +0800 Subject: [PATCH 11/14] docs: record history source checks and remaining UI failures (#29) --- docs/EVIDENCE.md | 4 +- docs/FEATURES.md | 4 +- experiments/core/CANDIDATE.md | 14 +- .../2026-10-28-source-ownership/README.md | 36 + .../validation.json | 980 ++++++++++++++++++ 5 files changed, 1032 insertions(+), 6 deletions(-) create mode 100644 experiments/core/results/2026-10-28-source-ownership/README.md create mode 100644 experiments/core/results/2026-10-28-source-ownership/validation.json diff --git a/docs/EVIDENCE.md b/docs/EVIDENCE.md index 5c99dda3..66764273 100644 --- a/docs/EVIDENCE.md +++ b/docs/EVIDENCE.md @@ -9,7 +9,7 @@ | 配置 | 证据定位 | |---|---| | main 默认:自研核心 + 系统 ConPTY | R1 限定总验收和有限 VT/Unicode 子集;默认构建不需要 Rust | -| 当前源码显式候选:`termforge.9` + 系统 ConPTY | [固定来源/补丁台账](../experiments/candidates/alacritty/PROVENANCE.md)、[适配边界](../experiments/core/CANDIDATE.md)、[#39 字素合同](research/streaming-graphemes.md);历史限定结果保留,长行复制回归仍红 | +| 当前源码显式候选:`termforge.10` + 系统 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、绘制提交和真实呈现是不同检查点;未知端点保持未知。 @@ -41,8 +41,10 @@ R1 固定总出口的源码为 `ce9b24d`,早期 `2dc8d6d` / `9b16acc` 仍分 | WindTerm 长历史行的视图折行(#29) | [逐字符观察与边界](research/windterm-history-layout.md)、[两次事实/哈希](../experiments/core/results/2026-10-28-history-layout/windterm-observations.json) | 独立的两次七阶段观察:185 字符软行由 80/80/25 变为 62/62/61,两条 77 字符硬行各变为 62/15;文档文本与位置不变,恢复宽度后投影恢复。仅 ASCII 文本坐标接口;覆盖写入、选择/Find、物理格及呈现仍未知。 | | 候选原生重排边界与未解决的源文本重复(#29) | [工作检查点与反例](../experiments/core/results/2026-10-28-reflow-metadata/README.md)、[来源与验证](../experiments/core/results/2026-10-28-reflow-metadata/validation.json) | `.7` 修复原生硬/软边界传递及缩宽容量计账;Rust 与 runner 通过,C++ 长行复制仍红。混宽历史、来源归属、选择/Find 及完整产品矩阵未完成。 | | 单次 feed 旧来源追踪(#29) | [固定实现与检查](../experiments/core/results/2026-10-28-feed-origins/README.md) | `.9` 原生 24 项公开集成、既有 workspace、C ABI 与 trace 检查通过;C++ 长历史复制仍红,来源映射不授予历史保留许可,新增成本未测。 | +| C++ 历史来源、部分淘汰与字素维护(#29) | [反例、修复与剩余范围](../experiments/core/results/2026-10-28-source-ownership/README.md)、[分次验证身份](../experiments/core/results/2026-10-28-source-ownership/validation.json) | 固定复制/空硬行/跨软行 Find、可见后缀、部分淘汰与样式空格反例修复;`.10` 历史维护不再封闭待续字素。完整矩阵有真实 shell 失败,部分重绘源顺序和混宽视图仍未闭合,不能宣布 #29 或产品通过。 | | 候选一次 resize 来源映射与真实淘汰计数(#29) | [接口范围及检查点](../experiments/core/results/2026-10-28-resize-origins/README.md)、[源码/构建/日志身份](../experiments/core/results/2026-10-28-resize-origins/validation.json) | `.8` 导出双屏物理来源和硬边界,修复缩高来源漏记、多行滚动少计与扩高假计;Rust、C++ 映射 ABI 和 runner 通过,C++ 长行复制仍失败,历史合并尚未消费映射。未测性能。 | | WindTerm 首次增高的前置条件停止(#29) | [独立观察与边界](research/windterm-resize-text.md#独立观察初始-18-行直接增高)、[合成输入](../experiments/core/results/2026-10-28-history-layout/windterm-first-grow-input.json)、[事实与哈希](../experiments/core/results/2026-10-28-history-layout/windterm-first-grow-observations.json) | 独立一次 18→26 行观察中未见编号历史进入非负 y;128 次有界查询不变,末尾空串为同一文末位置别名。按计划未发送 CUP/重绘,B 案例未运行,覆盖写入政策仍未知。 | +| WindTerm 查询后固定状态的 CUP 改写(#29) | [独立观察与限制](research/windterm-resize-text.md#独立观察dsr-后固定状态下的-cup-改写)、[精确输入](../experiments/core/results/2026-10-28-history-layout/windterm-fenced-cup-input.json)、[事实与哈希](../experiments/core/results/2026-10-28-history-layout/windterm-fenced-cup-observations.json) | 单次有界序列在完整前置状态核对后只改 H0026 文字;DSR 回复改变而选择文档位置不变。未裁决未查询状态、一般覆盖政策、物理 grid、ConPTY 或性能。 | | 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。 | | 进程生命周期和平台边界 | [ConPTY 探针](../experiments/conpty/README.md)、[跨环境记录](research/conpty-platform-check-2026-10-22.md)、[旧平台探针](../.github/workflows/conpty-compat.yml) | 受控后代、取消/超时和短批次回收;Windows 11/Server 2025 有固定记录,Server 2022 稳态句柄门槛仍失败。长期无泄漏未知。 | diff --git a/docs/FEATURES.md b/docs/FEATURES.md index f6667122..fa0cadfb 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -13,7 +13,7 @@ | 配置 | 当前定位 | 可以引用的证据与限制 | |---|---|---| | 默认 main:自研核心 + 系统 ConPTY | 默认构建与已发布 Alpha 的基础路线,不需要 Rust | 已有 R1 限定验收;VT/Unicode 能力仍为有限子集,不能引用候选核心的字素或 TUI 结果 | -| 当前源码显式候选:`termforge.9` + 系统 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.10` + 系统 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/拼音验收属于固定自研核心配置,不外推到当前候选 或实验后端联合配置。 @@ -33,7 +33,7 @@ | 中文 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 和有界恢复。显式候选 `.7` 接入 `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。显式候选 `.7` 保留 [#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)覆盖透明输入、主屏缓存归属及重复淘汰;`.8` 的[一次 resize 来源映射](../experiments/core/results/2026-10-28-resize-origins/README.md)及实际淘汰计数已有独立检查;`.9` 的[单次 feed 旧来源追踪](../experiments/core/results/2026-10-28-feed-origins/README.md)覆盖真实写入/移动与清空边界,尚未接入 C++ 历史合并 | [#29](https://github.com/JavaZeroo/termforge/issues/29) 的重绘后源历史重复、混宽历史视口及完整 resize/备用屏/滚动区交叉仍未完成;[原版两次文本观察](research/windterm-resize-text.md)仅证明固定输入保留及恢复尺寸后文本投影恢复;[逐字符历史观察](research/windterm-history-layout.md)另证明固定 ASCII 长行随宽度重新分段,未验证覆盖写入或交互。完整 reflow、child ack、实际呈现、系统/实验后端联合配置及性能分别验收;默认缩窄不可恢复 | R2 | +| 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)覆盖透明输入、主屏缓存归属及重复淘汰;`.8` 的[一次 resize 来源映射](../experiments/core/results/2026-10-28-resize-origins/README.md)及实际淘汰计数已有独立检查;`.9` 的[单次 feed 旧来源追踪](../experiments/core/results/2026-10-28-feed-origins/README.md)覆盖真实写入/移动与清空边界;C++ 已组合来源,固定长文本/空硬行/跨软行查找、无写入可见后缀与部分容量淘汰反例通过,`.10` 另修复历史删除打断待续字素 | [#29](https://github.com/JavaZeroo/termforge/issues/29) 的部分可见重绘顺序、混合来源合并、混宽历史视口及完整 resize/备用屏/滚动区交叉仍未完成;[原版两次文本观察](research/windterm-resize-text.md)仅证明固定输入保留及恢复尺寸后文本投影恢复;[逐字符历史观察](research/windterm-history-layout.md)另证明固定 ASCII 长行随宽度重新分段,[固定状态下的 CUP+DSR 观察](research/windterm-resize-text.md#独立观察dsr-后固定状态下的-cup-改写)仅确认一次序列改写 H0026,尚无普遍覆盖政策或交互验收。完整 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/experiments/core/CANDIDATE.md b/experiments/core/CANDIDATE.md index 700d8be9..fbc226c8 100644 --- a/experiments/core/CANDIDATE.md +++ b/experiments/core/CANDIDATE.md @@ -2,9 +2,9 @@ 按 [ADR-0006](../../docs/adr/0006-confirmed-project-direction.md),用户确认保留本候选代码继续评估,尚未确认最终采用。下述实现、来源与回归结果继续保留;与 WindTerm 2.7.0 的重要差异、性能容差及正式选型分别提交用户确认。 -`CandidateTerminal` 是 #27 的拥有值适配层,不是自研核心的替代品声明。它把固定 `0.26.1-dev+termforge.9` 的 `Term/Processor` 接到现有 `Screen`/`Line`;默认 CMake 保留自研核心且不需要 Rust,候选产品由 `TERMFORGE_BUILD_CANDIDATE_ADAPTER=ON` 显式启用。 +`CandidateTerminal` 是 #27 的拥有值适配层,不是自研核心的替代品声明。它把固定 `0.26.1-dev+termforge.10` 的 `Term/Processor` 接到现有 `Screen`/`Line`;默认 CMake 保留自研核心且不需要 Rust,候选产品由 `TERMFORGE_BUILD_CANDIDATE_ADAPTER=ON` 显式启用。 -`.7` 已修复原生 resize 的硬/软边界传递及缩宽容量淘汰计数,结果保留在[工作检查点](results/2026-10-28-reflow-metadata/README.md)。`.8` 增加一次 resize 来源映射,当前 `.9` 扩展到一次真实 feed,来源见[补丁台账](../candidates/alacritty/PROVENANCE.md#termforge9-补丁)。C++ 长行 resize/repaint 的整段复制回归仍失败:旧历史行与原生片段重复,来源归属和统一视图映射仍由 #29 继续处理。这些原生修复不构成候选完整正确性通过。 +`.7` 已修复原生 resize 的硬/软边界传递及缩宽容量淘汰计数,结果保留在[工作检查点](results/2026-10-28-reflow-metadata/README.md)。`.8` 增加一次 resize 来源映射,`.9` 扩展到一次真实 feed,`.10` 修复历史淘汰封闭待续字素的副作用,来源见[补丁台账](../candidates/alacritty/PROVENANCE.md)。C++ 已消费来源并修复固定整段/逐字节重绘中的长文本复制、空硬行与部分容量淘汰反例;部分可见重绘的顺序和混合来源合并仍未闭合,不构成候选完整正确性或 #29 验收通过。 ## 一次 resize 的来源映射 @@ -14,7 +14,7 @@ 原生淘汰数累计实际超出历史容量的物理行,包括一次多行滚动中移出容量的原可见行。扩高时仅为补空行而执行的内部滚动不计淘汰;显式清空、物理行合并及非容量裁剪仍分别处理。删除片段数量不是通用的逻辑文本损失计数。 -`candidate_resize_map_test` 从 C++ 调用真实静态库,验证字段布局、容量片段、映射生命周期、主/备用屏命名和空硬行。Rust FFI 测试覆盖拆分、相同文字的不同来源合并、宽字 spacer 和高度缩小的来源移除。当前 C++ 历史合并尚未消费此映射,后续覆盖写入、源文本归属、复制/Find 和视口坐标仍需实现与完整验证;该接口不裁决原版历史覆盖策略,也未测量新增来源表的资源成本。 +`candidate_resize_map_test` 从 C++ 调用真实静态库,验证字段布局、容量片段、映射生命周期、主/备用屏命名和空硬行。Rust FFI 测试覆盖拆分、相同文字的不同来源合并、宽字 spacer 和高度缩小的来源移除。C++ 从首次 resize 起组合每次映射;该接口不裁决原版历史覆盖策略,也未测量新增来源表的资源成本。 ## 一次 feed 的来源映射 @@ -22,6 +22,14 @@ feed 的 `removed_rows` 只记录过程中的移除事件。它不是完整擦除日志或最终保留政策;同一 feed 中 ED3/RIS 可能出现在移除之前或之后,最终 `history_clear_epoch` 不提供事件顺序。消费方必须避免复活清空前的来源,不能先清空再无条件追加整份 removed 列表。最终目标来源、清空与外部历史的保留规则仍须由上层一致处理。 +## C++ 历史来源与剩余边界 + +适配器为物理格来源和硬换行边界维护独立身份;写入后不按相同文本推断来源。待决重绘期间也持续组合 feed 映射。最终原生行的来源并集用于识别仍完整驻留的旧内容,容量移除按区间从已发布、重绘与挂起主屏快照扣除,复制剩余完整 `Cell`,不重建或按文字去重。原有文本键仅服务于固定重放的 UI 行身份,不用于来源合并。 + +固定反例已覆盖长历史整段/逐字节复制、连续空硬行、跨软行 `GHIJ` 查找、无写入可见后缀不重复及部分容量淘汰不复活 `ABCD`;淘汰前选择/坐标失效,剩余 `IJKL` 可以重新查找与选择。新背景和默认色粗体空格的对照通过;来源投影仅裁去完整默认 `Cell`,不再用 written 字段单独决定尾部来源。完整样式与宽格组合仍需验证,见[分次结果与真实失败](results/2026-10-28-source-ownership/README.md)。 + +`--source-partial-visible` 仍只输出观察:`B` 排在 `ABCDEFGH` 前,其返回 0 不表示匹配通过。混合原生行的部分来源拆分、多旧行联合覆盖以及统一文档/视图顺序仍由 #29 处理;不能用有限缓存规则裁决未知原版行为。混宽快照也仍需投影和交互映射,完整矩阵的历史重绘与选择失败尚未解决。来源组合与分配成本未测。 + ## 构建与验证 ```powershell diff --git a/experiments/core/results/2026-10-28-source-ownership/README.md b/experiments/core/results/2026-10-28-source-ownership/README.md new file mode 100644 index 00000000..0e009896 --- /dev/null +++ b/experiments/core/results/2026-10-28-source-ownership/README.md @@ -0,0 +1,36 @@ +# 历史来源组合、容量区间与待续字素 + +这是 #29 的实现检查点,尚未解决完整历史视图或原版覆盖政策。代码 `b6764a7` 将 resize/feed 来源接入 C++ 缓存;`eea3a2c` 修复历史删除封闭字素;`95cd2c3` 修复尾部粗体空格来源丢失。候选版本为 `0.26.1-dev+termforge.10`,固定上游、注册表依赖和许可证不变。实现为独立 TermForge 适配与候选补丁,没有导入 WindTerm 私有代码或资源。 + +## 修复及反例 + +- 旧缓存与原生片段现在按格来源和独立硬边界合并。固定整段/逐字节重绘保留 `ABCDEFGH\r\nB\r\nC\r\nD`、连续空硬行,以及跨软行 `GHIJ` 的唯一 Find 命中。来源不按相同文字去重;旧文本键仍只服务于限定重放的 UI 身份。 +- 无写入的 hide/show 不再重复保留跨历史/可见区的完整旧来源。容量移除按区间作用于已发布、重绘和挂起主屏快照:12 列长行缩至 4 列、容量只淘汰 `ABCD` 后,旧快照曾把它复活;现在复制为 `EFGHIJKL\r\nB\r\nC`,旧坐标/选择失效,`IJKL` 可重新查找和选择,空输入不重复计数。 +- 合法空/非空历史前缀删除曾经过 `grid_mut()`,使 `👩` 与后续 ZWJ+`💻` 分开、光标由预期列 2 变为 4。窄职责历史删除接口保留待续字素;完整前缀校验与映射失效规则不变。 +- 独立审查发现默认色粗体空格可以是 `written=false`。按 written 裁尾会吞掉其新来源;公开正常/重绘对照真实失败后,改为只裁去完整默认 `Cell`。文字、宽度、样式、标记和字素字段一起参与判断。 + +## 验证身份与实际失败 + +[validation.json](validation.json) 保存每次源码、工具命令、日志和二进制身份。Rust `.10` workspace 的 163 vendor、13 FFI、2 measure、2 trace、24 公开 feed 集成及 1 doctest 共 205 项通过。 + +`eea3a2c` 的 Windows x64 / Release / 显式候选 / 系统 ConPTY 完整 CTest 为 **52/53**。失败的 shell 套件有两项 error:历史 retained frame 与 full redraw 不一致;681 高度选择用例报告 `render API failed`。套件内部共 9 项 skip,其中 8 项实际系统剪贴板门槛未验、1 项为未启用的固定原版失败注入;CTest 级别没有跳过。不得写成完整产品回归通过。 + +相同旧 shell 二进制、原设置与期限下,两个场景各独立执行一次并保留现场:history 精确复现原错误,退出 2;selection 停在同一 `zoom-before` 前缀,但这次无错误文本、退出 `3221226525 / 0xC000041D`。后者是不同失败,原 API 错误仍未解释。两案未超时,Job active=0、测试 desktop 回收;13 个旧 EXE 与 3 个分发字体文件另保全于本地。未重跑取绿,未操作输入桌面、个人 DPI 或系统剪贴板。 + +粗体修复在 `95cd2c3` 的公开核心及三个已注册来源用例通过。该源码重新构建后的完整 CTest 仍为 **52/53、9 项内部 skip**:history 退出 2 并报告相同重绘差异;selection 681 这次 stderr 为空、退出 `0xC000041D`,两案均未超时且 Job/desktop 已回收。该结果与旧矩阵分别保存,不证明两个错误具有同一原因。重建后的 `.10` trace 报告回归另有 6/6 通过。 + +早期 `partial-capacity-frozen.json` 的测试文件哈希在新增旧坐标失效断言后过期,不能用它证明最终测试身份;末次源码和重新构建结果另列。 + +## 剩余边界 + +`--source-partial-visible` 仍复制出 `B\r\nABCDEFGH\r\n\r\nC\r\nD`,返回 0 仅表示观察完成,不能计为正确性通过。混合原生行的部分来源拆分、多旧行联合覆盖及完整源顺序仍未闭合。新背景与粗体空格对照不代表所有样式/宽字组合通过。 + +历史快照仍可能混宽。源码中的“首行宽度用于索引全部行”存在越界访问路径,但尚未证明它就是上述两项运行失败的原因。后续需要临时折行投影及双向源位置映射,同时核对鼠标、高亮、Find reveal 和绘制,不能靠永久裁剪历史或隐藏源文本掩盖。 + +本次[原版固定状态 CUP+DSR 观察](../../../../docs/research/windterm-resize-text.md#独立观察dsr-后固定状态下的-cup-改写)只确定一次序列改写 H0026;它不是候选缓存政策、未查询状态或普遍覆盖行为的验收。真实不同 DPI、完整系统输入、实验官方后端、实际呈现、性能与最终选型各自保持未完成。 + +## 审查 + +Standards:来源组合没有硬违规,保留 1 项非阻断维护建议——合并流程的 Line/native ID/来源三个并行数组应考虑合成一个内部行结构。字素与样式修复的独立 Standards 审查均无新增问题。 + +Spec:来源组合的粗体空格 P2 已由真实反例确认并在 `95cd2c3` 修复,独立复核无新增问题;字素生命周期修复为 0 项。已明确的顺序、混合来源、视图和原版缺口没有被计为通过。 diff --git a/experiments/core/results/2026-10-28-source-ownership/validation.json b/experiments/core/results/2026-10-28-source-ownership/validation.json new file mode 100644 index 00000000..b436977c --- /dev/null +++ b/experiments/core/results/2026-10-28-source-ownership/validation.json @@ -0,0 +1,980 @@ +{ + "schema_version": 1, + "status": "Implementation checkpoint with unresolved source-order, viewport and product failures", + "source_commit": "95cd2c38ad36b5c4bc756192b9a8780466936a0a", + "source_tree": "9a77b750f5621115fb042be693956149ee11e3c9", + "candidate_version": "0.26.1-dev+termforge.10", + "configuration": "Windows 11 x64; Release; explicit candidate adapter; system ConPTY", + "environment": { + "os_build": "10.0.26200", + "generator": "Visual Studio 17 2022 x64", + "rust_toolchain": "1.92.0", + "process_local_vim_path": "Git bundled usr/bin", + "PYTHONUTF8": "1" + }, + "checks": [ + { + "kind": "Rust workspace", + "command": "cargo test --manifest-path experiments/candidates/alacritty/Cargo.toml --workspace --locked", + "command_exit_source": "root-tool-observation", + "exit_code": 0, + "RUSTUP_TOOLCHAIN": "1.92.0", + "log": { + "path": "local/r29-source-integration/workspace-final.log", + "sha256": "f91253a7e095b7e6387077ccbca34856ad0e9655a685fd3641c682d77484c44e" + }, + "test_results": [ + { + "target_label_from_log": "unittests src/lib.rs", + "status": "ok", + "passed": 163, + "failed": 0, + "ignored": 0, + "measured": 0, + "filtered": 0 + }, + { + "target_label_from_log": "unittests src/ffi.rs", + "status": "ok", + "passed": 13, + "failed": 0, + "ignored": 0, + "measured": 0, + "filtered": 0 + }, + { + "target_label_from_log": "unittests src/measure.rs", + "status": "ok", + "passed": 2, + "failed": 0, + "ignored": 0, + "measured": 0, + "filtered": 0 + }, + { + "target_label_from_log": "unittests src/main.rs", + "status": "ok", + "passed": 2, + "failed": 0, + "ignored": 0, + "measured": 0, + "filtered": 0 + }, + { + "target_label_from_log": "tests/feed_origins.rs", + "status": "ok", + "passed": 24, + "failed": 0, + "ignored": 0, + "measured": 0, + "filtered": 0 + }, + { + "target_label_from_log": "Doc-tests alacritty_terminal", + "status": "ok", + "passed": 1, + "failed": 0, + "ignored": 0, + "measured": 0, + "filtered": 0 + } + ], + "source_commit": "eea3a2c7d79ceffba95065a2544a5fe6f67aed3c" + }, + { + "kind": "CMake build", + "command": "cmake --build build/candidate-adapter --config Release --parallel 2", + "command_exit_source": "root-tool-observation", + "exit_code": 0, + "RUSTUP_TOOLCHAIN": "1.92.0", + "log": { + "path": "local/r29-source-integration/build-final.log", + "sha256": "4bf6a9d1b3fd39b5a2f192767657fd467e81fa89e41a85c3c3afba6aee9ed68c" + }, + "source_commit": "eea3a2c7d79ceffba95065a2544a5fe6f67aed3c" + }, + { + "kind": "full CTest", + "command": "ctest --test-dir build/candidate-adapter -C Release --output-on-failure", + "command_exit_source": "root-tool-observation and before-style-frozen.json", + "exit_code": 8, + "configuration": "explicit candidate adapter, system backend, Windows Release", + "total": 53, + "passed": 52, + "failed": 1, + "failed_ctest": "shell", + "ctest_level_skipped": 0, + "internal_skipped": 9, + "clipboard_skipped": 8, + "reference_failure_injection_skipped": 1, + "shell_unittest": { + "ran": 39, + "errors": 2, + "skipped": 3 + }, + "errors": [ + { + "test": "test_real_history_scroll_anchor_eviction_and_hidden_sessions", + "message": "shell probe failed: history retained frame differs from full redraw" + }, + { + "test": "test_selection_autoscroll_with_short_and_tall_workspace", + "subtest": "client_height=681", + "message": "shell probe failed: render API failed" + } + ], + "logs": [ + { + "path": "local/r29-source-integration/ctest-before-style.log", + "sha256": "dec6bae5cb8ac3190628ad0211b17afa37d26d79649c5a37947976bc4d626c0b" + }, + { + "path": "local/r29-source-integration/last-test-before-style.log", + "sha256": "8e87f17660f166187e9a2cc61b92558bb6ae39fa64ab19c576ca5f801adc3446" + } + ], + "limitations": [ + "Passed parent CTest does not mean internally skipped clipboard/reference cases ran.", + "No performance claim; elapsed test duration intentionally omitted." + ], + "source_commit": "eea3a2c7d79ceffba95065a2544a5fe6f67aed3c" + }, + { + "kind": "full CTest after style fix", + "source_commit": "95cd2c38ad36b5c4bc756192b9a8780466936a0a", + "command": "ctest --test-dir build/candidate-adapter -C Release --output-on-failure", + "exit_code": 8, + "total": 53, + "passed": 52, + "failed": 1, + "internal_skipped": 9, + "failed_cases": [ + "shell" + ], + "log": "local/r29-source-integration/ctest-after-style.log", + "limitations": [ + "A later result never erases the prior failed matrix or diagnoses its cause." + ] + }, + { + "kind": "final product build", + "source_commit": "95cd2c38ad36b5c4bc756192b9a8780466936a0a", + "exit_code": 0, + "command": "cmake --build build/candidate-adapter --config Release --parallel 2", + "log": "local/r29-source-integration/build-after-style.log" + }, + { + "kind": "rebuilt candidate trace", + "source_commit": "95cd2c38ad36b5c4bc756192b9a8780466936a0a", + "exit_code": 0, + "command": "cargo build --manifest-path experiments/candidates/alacritty/Cargo.toml --release --locked --bin termforge-alacritty-trace", + "log": "local/r29-source-integration/trace-build-final.log" + }, + { + "kind": "trace reporting regression", + "source_commit": "95cd2c38ad36b5c4bc756192b9a8780466936a0a", + "exit_code": 0, + "passed": 6, + "command": "python -X utf8 -m unittest experiments.candidates.alacritty.test_candidate -v", + "log": "local/r29-source-integration/runner-final.log" + }, + { + "kind": "partial-visible observation only", + "source_commit": "95cd2c38ad36b5c4bc756192b9a8780466936a0a", + "exit_code": 0, + "command": "build/candidate-adapter/bin/Release/candidate_core_test.exe --source-partial-visible", + "result": "B before ABCDEFGH; EFGH matches=1; correctness remains failed", + "log": "local/r29-source-integration/partial-visible-final.log" + } + ], + "before_style_source_identity": [ + { + "path": "CMakeLists.txt", + "physical_sha256": "420231ef0c0a69504a6ebbaa0d545b1b1d103ad8353eeb94630a4b785202ae30", + "git_blob_sha256": "420231ef0c0a69504a6ebbaa0d545b1b1d103ad8353eeb94630a4b785202ae30", + "commit": "eea3a2c7d79ceffba95065a2544a5fe6f67aed3c", + "equivalence": "exact bytes or LF/CRLF only" + }, + { + "path": "experiments/core/candidate_terminal.cpp", + "physical_sha256": "6aeea757dbb5a89cf7c85340e6dc03ca1f4a9f8b6535df9ac4c5e30a42f4f47c", + "git_blob_sha256": "6aeea757dbb5a89cf7c85340e6dc03ca1f4a9f8b6535df9ac4c5e30a42f4f47c", + "commit": "eea3a2c7d79ceffba95065a2544a5fe6f67aed3c", + "equivalence": "exact bytes or LF/CRLF only" + }, + { + "path": "experiments/core/candidate_terminal.hpp", + "physical_sha256": "123fd595d6f993be29bac2c7af35c5b87c975af66275a0f05b94a4766a635a9b", + "git_blob_sha256": "123fd595d6f993be29bac2c7af35c5b87c975af66275a0f05b94a4766a635a9b", + "commit": "eea3a2c7d79ceffba95065a2544a5fe6f67aed3c", + "equivalence": "exact bytes or LF/CRLF only" + }, + { + "path": "experiments/core/candidate_terminal_test.cpp", + "physical_sha256": "596e236984856efe2773720250311c76424631ab329201217275176dd55abdc6", + "git_blob_sha256": "596e236984856efe2773720250311c76424631ab329201217275176dd55abdc6", + "commit": "eea3a2c7d79ceffba95065a2544a5fe6f67aed3c", + "equivalence": "exact bytes or LF/CRLF only" + }, + { + "path": "experiments/candidates/alacritty/src/ffi.rs", + "physical_sha256": "ac0e7a7f8bb5eff27363de4c525a8f8898b59e3e1f9c7c1e3d8b0b025f7ff243", + "git_blob_sha256": "ac0e7a7f8bb5eff27363de4c525a8f8898b59e3e1f9c7c1e3d8b0b025f7ff243", + "commit": "eea3a2c7d79ceffba95065a2544a5fe6f67aed3c", + "equivalence": "exact bytes or LF/CRLF only" + }, + { + "path": "experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/mod.rs", + "physical_sha256": "f79ac363f6fb046eec9b28d114bcea8390fb581084edf8643d07b64880bdf4d7", + "git_blob_sha256": "f79ac363f6fb046eec9b28d114bcea8390fb581084edf8643d07b64880bdf4d7", + "commit": "eea3a2c7d79ceffba95065a2544a5fe6f67aed3c", + "equivalence": "exact bytes or LF/CRLF only" + }, + { + "path": "experiments/candidates/alacritty/vendor/alacritty_terminal/Cargo.toml", + "physical_sha256": "5eb92aa34f551ab98424a99f5f20ec5ecbb773d6a56fb20931387ceb83398ccd", + "git_blob_sha256": "5eb92aa34f551ab98424a99f5f20ec5ecbb773d6a56fb20931387ceb83398ccd", + "commit": "eea3a2c7d79ceffba95065a2544a5fe6f67aed3c", + "equivalence": "exact bytes or LF/CRLF only" + }, + { + "path": "experiments/candidates/alacritty/Cargo.lock", + "physical_sha256": "df83cd8bec1fd2ac5c8f84c27aded75b7e726ee385b108bd99b94024f215d5cc", + "git_blob_sha256": "df83cd8bec1fd2ac5c8f84c27aded75b7e726ee385b108bd99b94024f215d5cc", + "commit": "eea3a2c7d79ceffba95065a2544a5fe6f67aed3c", + "equivalence": "exact bytes or LF/CRLF only" + } + ], + "current_cpp_source_identity": [ + { + "path": "experiments/core/candidate_terminal.cpp", + "physical_sha256": "ac6945bca9d803f05cbf81193a63f9fa364775f240135d4453c6cb0c66cfb70c", + "git_blob_sha256": "ac6945bca9d803f05cbf81193a63f9fa364775f240135d4453c6cb0c66cfb70c", + "commit": "95cd2c38ad36b5c4bc756192b9a8780466936a0a", + "equivalence": "exact bytes or LF/CRLF only" + }, + { + "path": "experiments/core/candidate_terminal.hpp", + "physical_sha256": "123fd595d6f993be29bac2c7af35c5b87c975af66275a0f05b94a4766a635a9b", + "git_blob_sha256": "123fd595d6f993be29bac2c7af35c5b87c975af66275a0f05b94a4766a635a9b", + "commit": "95cd2c38ad36b5c4bc756192b9a8780466936a0a", + "equivalence": "exact bytes or LF/CRLF only" + }, + { + "path": "experiments/core/candidate_terminal_test.cpp", + "physical_sha256": "35414ded20bcef4f1750924d04d71c6c8ba7e399e95096b3e14c432d86ad97de", + "git_blob_sha256": "35414ded20bcef4f1750924d04d71c6c8ba7e399e95096b3e14c432d86ad97de", + "commit": "95cd2c38ad36b5c4bc756192b9a8780466936a0a", + "equivalence": "exact bytes or LF/CRLF only" + } + ], + "before_style_artifacts": { + "build/candidate-adapter/bin/Release/alpha_normal_smoke.exe": "f14bf500afc054575c72a80cf22167bd64a0100ffdd43997cce8f56cbfd70812", + "build/candidate-adapter/bin/Release/candidate_core_test.exe": "ab4191af6be220d64eaac986dfe1221914d413485ca2ec857c53a402fcaa141f", + "build/candidate-adapter/bin/Release/candidate_resize_map_test.exe": "d1744502b116d812bef62d0da345a8467c8fc44e19bc3e0a6af57ea564ee6827", + "build/candidate-adapter/bin/Release/candidate_title_test.exe": "50f19cc23686b5b18fa64e1d9733ec89a195d1ff287e430de880df7cbbfb3763", + "build/candidate-adapter/bin/Release/clipboard_policy_test.exe": "36b98b15162aa95c3fb5ba634a3cc831022303d5966d8c43d71c2e36c8ac4e2d", + "build/candidate-adapter/bin/Release/conpty_ctrl_c_probe.exe": "2883537e3a90c27308a00c6a203161b472b231b456733b621fe8973c186c61d8", + "build/candidate-adapter/bin/Release/conpty_margin_probe.exe": "da640b5f4150fde8f6d1c4c838ba52c1318dfe834b85cc47e564096706156a34", + "build/candidate-adapter/bin/Release/core_measure.exe": "4fd604217e1874e7f3cb4d62ef77e85fe32ad73577383ed93bc97a58e55f75f7", + "build/candidate-adapter/bin/Release/find_bar_test.exe": "b1fa4befd1e70c1dd9d12a9cc413a9c0a6fe902f9c19fab78ef1f34e4b254f01", + "build/candidate-adapter/bin/Release/history_test.exe": "34694a7422e660de5a58e2e5d68b60423d923288a7e893f1c2a71ab36a8b1e0c", + "build/candidate-adapter/bin/Release/ime_context_test.exe": "e7bd99a75a0738e1e8d24e57ce39fac89752411ad11f11bcf1d590e85b23b932", + "build/candidate-adapter/bin/Release/ime_input_expectation_test.exe": "a3febd8e1f02c8ffa7c050f2542569ce520488b8c226ed6a7c057f483e7c5680", + "build/candidate-adapter/bin/Release/keyboard_test.exe": "22bfedc16265cf4063082e5aa0cc82e74b9d2b3af87bebafb87261e4a6331feb", + "build/candidate-adapter/bin/Release/layout_state_test.exe": "5a94a47ee125737fd1835cbb43585aaab4f91ddc95192a9f919178cbd90ce59d", + "build/candidate-adapter/bin/Release/paste_test.exe": "d698700223cf42208a2aa379c5f7dcb894519788f3f53fb3ca85ecde050128c0", + "build/candidate-adapter/bin/Release/presentation_probe.exe": "39c2e9463a35cc73df7aa0bdad381519592228efcfc6407fb16520f8dab1b58d", + "build/candidate-adapter/bin/Release/raw_console_receiver_test.exe": "2f3214c9ccb57f9bfaf6f228015ffd5838e61fbababd087b72a3baf14b9642b9", + "build/candidate-adapter/bin/Release/real_app_capture.exe": "72d26bda61af90b21b83bdb6cee3a048abec2d277494aaf86bfd99ded2a4368a", + "build/candidate-adapter/bin/Release/receive.exe": "a4d914f1c804c7cb240be00c987801bbfbbe1ed0006423e1a97b1d301d1f78df", + "build/candidate-adapter/bin/Release/recovery_probe.exe": "ef2e7c61ca875d0dbb02220f8c4df2646332f278863fd64673679c7102af6596", + "build/candidate-adapter/bin/Release/renderer_metrics_test.exe": "f9b99a24bc568a87aa21f546a932118b1802bb74198db15caf6fe1bbce08cf01", + "build/candidate-adapter/bin/Release/replay.exe": "37dfecd9adfdf89b2ee44b581863da0114d6fa486ee5d669c7f073ae0581505a", + "build/candidate-adapter/bin/Release/resize_test.exe": "c5ad023c3076574466b9284102751218436bd40333eedeb70f41e964f22f8dcf", + "build/candidate-adapter/bin/Release/runtime_observation_test.exe": "b1c01f58c01d237adc6e4d4d904dac1735b71e1b465e0a7c3377a47a7c6c2773", + "build/candidate-adapter/bin/Release/scenario_tick_gate_test.exe": "87ebac936c81caa8306b981307da218ac97d432c366a2b21cce93b7f4dc6c67b", + "build/candidate-adapter/bin/Release/search_test.exe": "77a88b31c54547cc64bb188abb201e203a6792a1492bb231449c8c41b605fbd2", + "build/candidate-adapter/bin/Release/selection_test.exe": "5a32e5219ca4d6ccf96be6616bdad92f24ee5ae49a10ecc6aee27d9eefadd0d1", + "build/candidate-adapter/bin/Release/sent_message_wait_test.exe": "02633453bb43ee175380b32d9d74c5e810e18373fc7ae0b9d281b480ececd70d", + "build/candidate-adapter/bin/Release/shell_clipboard_reader.exe": "7fcd3e07232000c276c85e5667b2525cff67926342cb870d0980b88a84273377", + "build/candidate-adapter/bin/Release/shell_ime_client.exe": "75c0b15ed5e79b9abf4815e6036c657fa2713531318d0ab256c98f420dcba97b", + "build/candidate-adapter/bin/Release/shell_input_client.exe": "4c70b7cd5fc8b31df2e33f9af8fc714e2ff3fff8b709499768e26943c146b97b", + "build/candidate-adapter/bin/Release/shell_launch_test.exe": "7fa6d0080ad05bdc93716f9c9ee0f4dc4e9aa181b4dac17dc0bdc1876bb86002", + "build/candidate-adapter/bin/Release/shell_paste_probe.exe": "59a978d4657e419b14a362ece8a6ca83c01283aa13c11293007b1d3386334040", + "build/candidate-adapter/bin/Release/shell_query_client.exe": "d9a74b89e702cb21bb3d2b79989bb488d764e33027df6e52f1dd296657754c5d", + "build/candidate-adapter/bin/Release/shell_queue_probe.exe": "1e05519f1bc802d81407b0836cbc8a25ce8a01f9354c12a03e91f3d5bde0b0d5", + "build/candidate-adapter/bin/Release/shell_selection_text_client.exe": "a87359d579e4d897c84e542a83b750b2993cf5cab7fb7fe9a525551134d9ead6", + "build/candidate-adapter/bin/Release/shell_size_client.exe": "67764272d9e53298b4f8eefe351b63f74dc36fe31fb9b8550a93d7c700da33da", + "build/candidate-adapter/bin/Release/shell_test_launcher.exe": "3923d6cd21b6b5ceffe0d91d4167d67ee4b8c2b946194ffcb079bd9ac041bad3", + "build/candidate-adapter/bin/Release/shell_text_client.exe": "9ccd38100fc77e2e39e1eee811e68aac9a656c2cfb8c3585121c38dc7a3b3bde", + "build/candidate-adapter/bin/Release/termforge_conpty_probe.exe": "b48137edad63d1e204e94431fbe950a59d7ca9785c690ba0ee60d7fb236df1f6", + "build/candidate-adapter/bin/Release/termforge_ime_probe.exe": "5bfded9b7605ddadd87908729ded01fd29af366a938c98ab0167248a70789cea", + "build/candidate-adapter/bin/Release/termforge_shell.exe": "77861d5a63d62725624d4879eb0206fced4bb05375257dc289940fd9520d424a", + "build/candidate-adapter/bin/Release/terminal_trace.exe": "446f45870fffbc121d3099ff31e6f6751429d3a1bdb0c4e6eb79e88b394277bf", + "build/candidate-adapter/bin/Release/text_action_policy_test.exe": "1945dcdd963dcff679334669df943dc8462bbca2096020120a355d0e4c344baa", + "build/candidate-adapter/bin/Release/transport_completion_test.exe": "82236eb238ccc1f8ef34d62da8d68bdbb0b5966b92375b0920e055ba376ea1bc", + "build/candidate-adapter/bin/Release/workflow_fixture_test.exe": "7fa7bea14776ca254e23ef14e070eb40656fe35badf017f31063d3d25abbf8ad", + "build/candidate-adapter/bin/Release/workspace_state_test.exe": "e6510653716daa1a7ca9128da937e53aa80652add940b1cfc09bb9e76b74a523" + }, + "final_artifacts": { + "build/candidate-adapter/bin/Release/alpha_normal_smoke.exe": "f14bf500afc054575c72a80cf22167bd64a0100ffdd43997cce8f56cbfd70812", + "build/candidate-adapter/bin/Release/candidate_core_test.exe": "f5f9ff93236f8f6e4a70b76adc682262245b525d5825b9edbb3fff47c6bb80e4", + "build/candidate-adapter/bin/Release/candidate_resize_map_test.exe": "d1744502b116d812bef62d0da345a8467c8fc44e19bc3e0a6af57ea564ee6827", + "build/candidate-adapter/bin/Release/candidate_title_test.exe": "a2980cf8beb093514177898bff68243845e670e8e53264fe0f644a94435bf23e", + "build/candidate-adapter/bin/Release/clipboard_policy_test.exe": "36b98b15162aa95c3fb5ba634a3cc831022303d5966d8c43d71c2e36c8ac4e2d", + "build/candidate-adapter/bin/Release/conpty_ctrl_c_probe.exe": "2883537e3a90c27308a00c6a203161b472b231b456733b621fe8973c186c61d8", + "build/candidate-adapter/bin/Release/conpty_margin_probe.exe": "da640b5f4150fde8f6d1c4c838ba52c1318dfe834b85cc47e564096706156a34", + "build/candidate-adapter/bin/Release/core_measure.exe": "4fd604217e1874e7f3cb4d62ef77e85fe32ad73577383ed93bc97a58e55f75f7", + "build/candidate-adapter/bin/Release/find_bar_test.exe": "b1fa4befd1e70c1dd9d12a9cc413a9c0a6fe902f9c19fab78ef1f34e4b254f01", + "build/candidate-adapter/bin/Release/history_test.exe": "34694a7422e660de5a58e2e5d68b60423d923288a7e893f1c2a71ab36a8b1e0c", + "build/candidate-adapter/bin/Release/ime_context_test.exe": "e7bd99a75a0738e1e8d24e57ce39fac89752411ad11f11bcf1d590e85b23b932", + "build/candidate-adapter/bin/Release/ime_input_expectation_test.exe": "a3febd8e1f02c8ffa7c050f2542569ce520488b8c226ed6a7c057f483e7c5680", + "build/candidate-adapter/bin/Release/keyboard_test.exe": "22bfedc16265cf4063082e5aa0cc82e74b9d2b3af87bebafb87261e4a6331feb", + "build/candidate-adapter/bin/Release/layout_state_test.exe": "5a94a47ee125737fd1835cbb43585aaab4f91ddc95192a9f919178cbd90ce59d", + "build/candidate-adapter/bin/Release/paste_test.exe": "d698700223cf42208a2aa379c5f7dcb894519788f3f53fb3ca85ecde050128c0", + "build/candidate-adapter/bin/Release/presentation_probe.exe": "39c2e9463a35cc73df7aa0bdad381519592228efcfc6407fb16520f8dab1b58d", + "build/candidate-adapter/bin/Release/raw_console_receiver_test.exe": "2f3214c9ccb57f9bfaf6f228015ffd5838e61fbababd087b72a3baf14b9642b9", + "build/candidate-adapter/bin/Release/real_app_capture.exe": "72d26bda61af90b21b83bdb6cee3a048abec2d277494aaf86bfd99ded2a4368a", + "build/candidate-adapter/bin/Release/receive.exe": "a4d914f1c804c7cb240be00c987801bbfbbe1ed0006423e1a97b1d301d1f78df", + "build/candidate-adapter/bin/Release/recovery_probe.exe": "ef2e7c61ca875d0dbb02220f8c4df2646332f278863fd64673679c7102af6596", + "build/candidate-adapter/bin/Release/renderer_metrics_test.exe": "f9b99a24bc568a87aa21f546a932118b1802bb74198db15caf6fe1bbce08cf01", + "build/candidate-adapter/bin/Release/replay.exe": "37dfecd9adfdf89b2ee44b581863da0114d6fa486ee5d669c7f073ae0581505a", + "build/candidate-adapter/bin/Release/resize_test.exe": "c5ad023c3076574466b9284102751218436bd40333eedeb70f41e964f22f8dcf", + "build/candidate-adapter/bin/Release/runtime_observation_test.exe": "b5b61f0a2b1dbf43e75e271880d4df992d65021e03638dcff6a43d2faaf9d7d4", + "build/candidate-adapter/bin/Release/scenario_tick_gate_test.exe": "87ebac936c81caa8306b981307da218ac97d432c366a2b21cce93b7f4dc6c67b", + "build/candidate-adapter/bin/Release/search_test.exe": "77a88b31c54547cc64bb188abb201e203a6792a1492bb231449c8c41b605fbd2", + "build/candidate-adapter/bin/Release/selection_test.exe": "5a32e5219ca4d6ccf96be6616bdad92f24ee5ae49a10ecc6aee27d9eefadd0d1", + "build/candidate-adapter/bin/Release/sent_message_wait_test.exe": "02633453bb43ee175380b32d9d74c5e810e18373fc7ae0b9d281b480ececd70d", + "build/candidate-adapter/bin/Release/shell_clipboard_reader.exe": "7fcd3e07232000c276c85e5667b2525cff67926342cb870d0980b88a84273377", + "build/candidate-adapter/bin/Release/shell_ime_client.exe": "75c0b15ed5e79b9abf4815e6036c657fa2713531318d0ab256c98f420dcba97b", + "build/candidate-adapter/bin/Release/shell_input_client.exe": "4c70b7cd5fc8b31df2e33f9af8fc714e2ff3fff8b709499768e26943c146b97b", + "build/candidate-adapter/bin/Release/shell_launch_test.exe": "7fa6d0080ad05bdc93716f9c9ee0f4dc4e9aa181b4dac17dc0bdc1876bb86002", + "build/candidate-adapter/bin/Release/shell_paste_probe.exe": "59a978d4657e419b14a362ece8a6ca83c01283aa13c11293007b1d3386334040", + "build/candidate-adapter/bin/Release/shell_query_client.exe": "d9a74b89e702cb21bb3d2b79989bb488d764e33027df6e52f1dd296657754c5d", + "build/candidate-adapter/bin/Release/shell_queue_probe.exe": "1e05519f1bc802d81407b0836cbc8a25ce8a01f9354c12a03e91f3d5bde0b0d5", + "build/candidate-adapter/bin/Release/shell_selection_text_client.exe": "a87359d579e4d897c84e542a83b750b2993cf5cab7fb7fe9a525551134d9ead6", + "build/candidate-adapter/bin/Release/shell_size_client.exe": "67764272d9e53298b4f8eefe351b63f74dc36fe31fb9b8550a93d7c700da33da", + "build/candidate-adapter/bin/Release/shell_test_launcher.exe": "3923d6cd21b6b5ceffe0d91d4167d67ee4b8c2b946194ffcb079bd9ac041bad3", + "build/candidate-adapter/bin/Release/shell_text_client.exe": "9ccd38100fc77e2e39e1eee811e68aac9a656c2cfb8c3585121c38dc7a3b3bde", + "build/candidate-adapter/bin/Release/termforge_conpty_probe.exe": "b48137edad63d1e204e94431fbe950a59d7ca9785c690ba0ee60d7fb236df1f6", + "build/candidate-adapter/bin/Release/termforge_ime_probe.exe": "5bfded9b7605ddadd87908729ded01fd29af366a938c98ab0167248a70789cea", + "build/candidate-adapter/bin/Release/termforge_shell.exe": "360d180011ab9a4e3bc6e9ea93353dacda18181207e4e690a7a2ddaa62fb261d", + "build/candidate-adapter/bin/Release/terminal_trace.exe": "446f45870fffbc121d3099ff31e6f6751429d3a1bdb0c4e6eb79e88b394277bf", + "build/candidate-adapter/bin/Release/text_action_policy_test.exe": "1945dcdd963dcff679334669df943dc8462bbca2096020120a355d0e4c344baa", + "build/candidate-adapter/bin/Release/transport_completion_test.exe": "82236eb238ccc1f8ef34d62da8d68bdbb0b5966b92375b0920e055ba376ea1bc", + "build/candidate-adapter/bin/Release/workflow_fixture_test.exe": "7fa7bea14776ca254e23ef14e070eb40656fe35badf017f31063d3d25abbf8ad", + "build/candidate-adapter/bin/Release/workspace_state_test.exe": "e6510653716daa1a7ca9128da937e53aa80652add940b1cfc09bb9e76b74a523" + }, + "final_native_artifacts": { + "experiments/candidates/alacritty/target/release/termforge_alacritty_core.lib": "bd9ce457ecd1d64822d23e877ea44daa01e4e63747c7a3ad0b07e64a870bf39b", + "experiments/candidates/alacritty/target/release/termforge-alacritty-trace.exe": "75e3964fa590c799ce8216449f35a8cb71234e2c0e3cb9671ad6b000542bd8e3" + }, + "final_shell_failures": [ + { + "case": "history-0", + "sample": { + "mode": "ui-history", + "status": "failed", + "timed_out": false, + "exit_code": 2, + "stdout": "", + "stderr": "history retained frame differs from full redraw\r\n", + "job_after": { + "active": 0, + "total": 13, + "terminated": 0 + }, + "job_after_cleanup": { + "active": 0, + "total": 13, + "terminated": 0 + }, + "desktop_gone": true, + "error": "shell probe failed: history retained frame differs from full redraw" + }, + "local_artifact_directory": "build/test-artifacts/shell/run-bq6w_jfc/history-0", + "raw_file_sha256": { + "native-scroll.jsonl": "d46a879fac54c911e01fa03e81d906e3ffb357270ecf263728e4e1404901adc0", + "sample.json": "f09fb21dbc6ca747f193868c6a36b1d11613c12e69b6795c6fd5399b36f1e795", + "workspace.layout": "bf48acf03f30b7b947c3f2a351dac62be6e81dbb75672b27b2c15a1203d3ed3c" + } + }, + { + "case": "selection-height-681", + "sample": { + "mode": "ui-selection", + "status": "failed", + "timed_out": false, + "exit_code": 3221226525, + "stdout": "", + "stderr": "", + "job_after": { + "active": 0, + "total": 9, + "terminated": 0 + }, + "job_after_cleanup": { + "active": 0, + "total": 9, + "terminated": 0 + }, + "desktop_gone": true, + "error": "shell probe failed: " + }, + "local_artifact_directory": "build/test-artifacts/shell/run-bq6w_jfc/selection-height-681", + "raw_file_sha256": { + "sample.json": "0124938b19f82782dcfb7b9c11655365cce14de982cef5439c05ecea26192b8c", + "selection-base.jsonl": "998a7d795747f10648a9c5c84391db26a5c1fe3951c0b2726c0b5662d4a24158", + "workspace.layout": "58a15cf3e3e4101a5239edcb0ad7d43c31d01337b0499885c9e4b31ff14735ea" + } + } + ], + "source_ownership_logs": [ + { + "path": "local/r29-source-ownership/build-frozen-probes.log", + "sha256": "3dc6a5da989532287d8bc4d3d45ef5f5ad084b953cfbb949a95223daeec20467", + "command": null, + "command_status": "not recorded in supplied log", + "exit_code": null, + "classification": "build output; process exit not recorded" + }, + { + "path": "local/r29-source-ownership/build-partial-capacity-identity.log", + "sha256": "3dc6a5da989532287d8bc4d3d45ef5f5ad084b953cfbb949a95223daeec20467", + "command": null, + "command_status": "not recorded in supplied log", + "exit_code": null, + "classification": "build output; process exit not recorded" + }, + { + "path": "local/r29-source-ownership/core-capacity-frozen.log", + "sha256": "3d7ca02ffd7486699a996c9037e88c4276157115eafe3d643ee1895c993b0a5a", + "command": null, + "command_status": "not recorded in supplied log", + "exit_code": 0, + "classification": "named checkpoint completed successfully" + }, + { + "path": "local/r29-source-ownership/core-frozen.log", + "sha256": "2130f9a0b2d172e571c94ba7dd1c6c5c8f562267976a1fa84ff8c5d8c565d20e", + "command": null, + "command_status": "not recorded in supplied log", + "exit_code": null, + "classification": "historical failure; not final green", + "failure_message": "ConPTY replay changed the logical row identity or text" + }, + { + "path": "local/r29-source-ownership/new-background-capacity-frozen.log", + "sha256": "a9f58776a09b5dac438049683f24bf85764e0ff8e7455952456165c68c158627", + "command": null, + "command_status": "not recorded in supplied log", + "exit_code": 0, + "classification": "named checkpoint completed successfully" + }, + { + "path": "local/r29-source-ownership/partial-capacity-detail-red.log", + "sha256": "aff720a8da785b28f46dbc0e9b83fcea7fcadf2e146b0473207aaf4110ea6649", + "command": null, + "command_status": "not recorded in supplied log", + "exit_code": null, + "classification": "historical assertion failure; exit code not recorded", + "failure_message": "capacity removal lost surviving source or restored an evicted prefix: \"ABCDEFGHIJKL\\u000d\\u000aB\\u000d\\u000aC\"" + }, + { + "path": "local/r29-source-ownership/partial-capacity-frozen.log", + "sha256": "a9f58776a09b5dac438049683f24bf85764e0ff8e7455952456165c68c158627", + "command": null, + "command_status": "not recorded in supplied log", + "exit_code": 0, + "classification": "named checkpoint completed successfully" + }, + { + "path": "local/r29-source-ownership/partial-capacity-identity.log", + "sha256": "a9f58776a09b5dac438049683f24bf85764e0ff8e7455952456165c68c158627", + "command": null, + "command_status": "not recorded in supplied log", + "exit_code": 0, + "classification": "named checkpoint completed successfully" + }, + { + "path": "local/r29-source-ownership/partial-capacity-red.log", + "sha256": "5fd97b1cb702bdedaee43c71d1cd9263ee5bbae58565f22af59b0553e8d218de", + "command": null, + "command_status": "not recorded in supplied log", + "exit_code": null, + "classification": "historical assertion failure; exit code not recorded", + "failure_message": "capacity removal lost surviving source or restored an evicted prefix" + }, + { + "path": "local/r29-source-ownership/partial-visible-capacity-frozen.log", + "sha256": "f8afae989a01a69b0a8f1572f3449dc89405629cb287c3c84053c3dfd5c44e52", + "command": null, + "command_status": "not recorded in supplied log", + "exit_code": 0, + "classification": "observation only; not an acceptance test", + "observation": { + "history": [ + "B ", + "ABCD" + ], + "visible": [ + "EFGH", + " ", + "C ", + "D " + ], + "copy": "B\r\nABCDEFGH\r\n\r\nC\r\nD", + "EFGH_matches": 1, + "unresolved": "B precedes ABCDEFGH; exit 0 only records completion of observation." + } + }, + { + "path": "local/r29-source-ownership/partial-visible-final.log", + "sha256": "8c85c093c56979ccf5ec1076b31a319119bdb9c860bbed2301e6984c821fc16d", + "command": null, + "command_status": "not recorded in supplied log", + "exit_code": null, + "classification": "observation only; not an acceptance test", + "observation": { + "history": [ + "B ", + "ABCD" + ], + "visible": [ + "EFGH", + " ", + "C ", + "D " + ], + "copy": "B\r\nABCDEFGH\r\n\r\nC\r\nD", + "EFGH_matches": 1, + "unresolved": "B precedes ABCDEFGH; exit 0 only records completion of observation." + } + }, + { + "path": "local/r29-source-ownership/partial-visible-frozen.log", + "sha256": "8c85c093c56979ccf5ec1076b31a319119bdb9c860bbed2301e6984c821fc16d", + "command": null, + "command_status": "not recorded in supplied log", + "exit_code": null, + "classification": "observation only; not an acceptance test", + "observation": { + "history": [ + "B ", + "ABCD" + ], + "visible": [ + "EFGH", + " ", + "C ", + "D " + ], + "copy": "B\r\nABCDEFGH\r\n\r\nC\r\nD", + "EFGH_matches": 1, + "unresolved": "B precedes ABCDEFGH; exit 0 only records completion of observation." + } + }, + { + "path": "local/r29-source-ownership/visible-suffix-capacity-frozen.log", + "sha256": "a9f58776a09b5dac438049683f24bf85764e0ff8e7455952456165c68c158627", + "command": null, + "command_status": "not recorded in supplied log", + "exit_code": 0, + "classification": "named checkpoint completed successfully" + }, + { + "path": "local/r29-source-ownership/visible-suffix-red.log", + "sha256": "f4167b9c916e503eda2558f0d7afeca03269dd090e8da4d102d3de5e87a140ae", + "command": null, + "command_status": "not recorded in supplied log", + "exit_code": null, + "classification": "historical assertion failure; exit code not recorded", + "failure_message": "unchanged visible suffix was duplicated by its archived owner", + "diagnostic_counts": { + "EFGH_matches": 2, + "B_matches": 2 + } + } + ], + "stale_capture_warning": { + "path": "experiments/core/candidate_terminal_test.cpp", + "captured_sha256": "8cbcbc09549ed4093c40872b03697cacbb1ffa20cc098f836c3530ea64655745", + "reason": "The test changed after this snapshot to add an identity assertion. This old test hash and associated binary are not the final b6764a7 test identity.", + "later_log": { + "path": "local/r29-source-ownership/partial-capacity-identity.log", + "sha256": "a9f58776a09b5dac438049683f24bf85764e0ff8e7455952456165c68c158627" + }, + "later_log_exit_code": 0, + "later_run_source_binary_hashes": null, + "final_identity_source": "Use before-style-frozen.json for the full rebuilt matrix; do not invent a map from the later identity-only log to the older binary." + }, + "discard_grapheme_red_green": [ + { + "path": "local/r29-discard-grapheme/red-validation.json", + "sha256": "b1a5aa8b248827e0ece7860cb06802125467e1c37907e196f806642d8332afef", + "stage": "red", + "exit_code": 101, + "passed": 2, + "failed": 2, + "ignored": 0, + "filtered": 9, + "command": "cargo test --manifest-path experiments/candidates/alacritty/Cargo.toml --locked -p termforge-alacritty-trace --lib history_eviction_ -- --nocapture --test-threads=1", + "command_source": "local/r29-discard-grapheme/red-validation.json", + "RUSTUP_TOOLCHAIN": "1.92.0", + "recorded_sha256": { + "experiments/candidates/alacritty/src/ffi.rs": "1d863d951d15d426843578f7ee8fb73c5cfab9f990ceca5637d9d44ae7690558", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/mod.rs": "515c4b787d1878194c3ea224d4ade64596e0ed9d64eee7f9d3ab640a6f523be3", + "experiments/candidates/alacritty/target/debug/deps/termforge_alacritty_core-205ebf5c426fc1e2.exe": "12b31dd43492a075c203ab8156c1b5160ded0d5f963d874042ba563b920d0c4f", + "local/r29-discard-grapheme/red.log": "50b64ce6c0affea3b69407132e5973928c2876efd94de8137312af21cba088fb", + "local/r29-discard-grapheme/red-exit.txt": "bdc830287616dae1818ced3e7c186e1387155900ad30d5983ec02da1519c4a95" + }, + "recorded_head": null, + "candidate_version_during_run": "0.26.1-dev+termforge.9", + "scope": "debug public C ABI tests before version bump" + }, + { + "path": "local/r29-discard-grapheme/green-validation.json", + "sha256": "04e51c96a03c43946c52ae10ecafeff0e33074b3a3377ac7006d15f28999d194", + "stage": "green-frozen", + "exit_code": 0, + "passed": 4, + "failed": 0, + "ignored": 0, + "filtered": 9, + "command": "cargo test --manifest-path experiments/candidates/alacritty/Cargo.toml --locked -p termforge-alacritty-trace --lib history_eviction_ -- --nocapture --test-threads=1", + "command_source": "local/r29-discard-grapheme/green-validation.json", + "RUSTUP_TOOLCHAIN": "1.92.0", + "recorded_sha256": { + "experiments/candidates/alacritty/src/ffi.rs": "ac0e7a7f8bb5eff27363de4c525a8f8898b59e3e1f9c7c1e3d8b0b025f7ff243", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/mod.rs": "f79ac363f6fb046eec9b28d114bcea8390fb581084edf8643d07b64880bdf4d7", + "experiments/candidates/alacritty/target/debug/deps/termforge_alacritty_core-205ebf5c426fc1e2.exe": "3e64655df0b6a6634b6146f6b24e53a97d7e1451f3f4bdb3705f0dcc8a8d597b", + "local/r29-discard-grapheme/green.log": "77eca7a31625f1516af42f3b78a6ec4006cdba2fe52e2e6d76fe1a9e7adf1f52", + "local/r29-discard-grapheme/green-exit.txt": "13bf7b3039c63bf5a50491fa3cfd8eb4e699d1ba1436315aef9cbe5711530354" + }, + "recorded_head": "b6764a763b118b86d79f62df7168a58badbad669", + "candidate_version_during_run": "0.26.1-dev+termforge.9", + "scope": "debug public C ABI tests before version bump" + } + ], + "style_red_green": [ + { + "path": "local/r29-source-style/red-validation.json", + "sha256": "a50229511d59bd216ee7ff6f69269b547482a02541c370177a78661dcce68b27", + "stage": "red", + "recorded_head": "caeb5bfd1accc93c3eea7ff1a9d25a256e3149f5", + "recorded_files": [ + { + "path": "experiments/core/candidate_terminal.cpp", + "sha256": "6aeea757dbb5a89cf7c85340e6dc03ca1f4a9f8b6535df9ac4c5e30a42f4f47c" + }, + { + "path": "experiments/core/candidate_terminal.hpp", + "sha256": "123fd595d6f993be29bac2c7af35c5b87c975af66275a0f05b94a4766a635a9b" + }, + { + "path": "experiments/core/candidate_terminal_test.cpp", + "sha256": "35414ded20bcef4f1750924d04d71c6c8ba7e399e95096b3e14c432d86ad97de" + }, + { + "path": "build/candidate-adapter/bin/Release/candidate_core_test.exe", + "sha256": "dd287292968893416664ba8375c02ba9e890d1cc5330f378ce78284734e14e4a" + }, + { + "path": "local/r29-source-style/build-red.log", + "sha256": "3dc6a5da989532287d8bc4d3d45ef5f5ad084b953cfbb949a95223daeec20467" + }, + { + "path": "local/r29-source-style/bold-space-red.log", + "sha256": "2b0c0cdd92d68793f071f3b13d203a1064c50c9f74f71aedb5a758a17fabdfea" + } + ], + "exit_code": 1, + "command": "build/candidate-adapter/bin/Release/candidate_core_test.exe --source-new-background", + "command_source": "author-agent report relayed by root; exit and log hashes in captured JSON", + "scope": "background and new bold-space case, each ordinary/replay path; not full CTest", + "logs": [ + { + "path": "local/r29-source-style/build-red.log", + "exit_code": null, + "command": null + }, + { + "path": "local/r29-source-style/bold-space-red.log", + "exit_code": 1, + "command": "build/candidate-adapter/bin/Release/candidate_core_test.exe --source-new-background" + } + ], + "source_identity": "eea3a2c implementation plus the new test from 95cd2c3" + }, + { + "path": "local/r29-source-style/green-validation.json", + "sha256": "6aeaf6b3102d43977ab62a0ebb253e15b20393362ce4e51138c674cba04859c6", + "stage": "green", + "recorded_head": "caeb5bfd1accc93c3eea7ff1a9d25a256e3149f5", + "recorded_files": [ + { + "path": "experiments/core/candidate_terminal.cpp", + "sha256": "ac6945bca9d803f05cbf81193a63f9fa364775f240135d4453c6cb0c66cfb70c" + }, + { + "path": "experiments/core/candidate_terminal.hpp", + "sha256": "123fd595d6f993be29bac2c7af35c5b87c975af66275a0f05b94a4766a635a9b" + }, + { + "path": "experiments/core/candidate_terminal_test.cpp", + "sha256": "35414ded20bcef4f1750924d04d71c6c8ba7e399e95096b3e14c432d86ad97de" + }, + { + "path": "build/candidate-adapter/bin/Release/candidate_core_test.exe", + "sha256": "f5f9ff93236f8f6e4a70b76adc682262245b525d5825b9edbb3fff47c6bb80e4" + }, + { + "path": "local/r29-source-style/build-green.log", + "sha256": "4722a25358f604c7062ebea67c3a54d19051c54278bffab77ef2fea913a63b12" + }, + { + "path": "local/r29-source-style/bold-space-green.log", + "sha256": "a9f58776a09b5dac438049683f24bf85764e0ff8e7455952456165c68c158627" + } + ], + "exit_code": 0, + "command": "build/candidate-adapter/bin/Release/candidate_core_test.exe --source-new-background", + "command_source": "author-agent report relayed by root; exit and log hashes in captured JSON", + "scope": "background and new bold-space case, each ordinary/replay path; not full CTest", + "logs": [ + { + "path": "local/r29-source-style/build-green.log", + "exit_code": null, + "command": null + }, + { + "path": "local/r29-source-style/bold-space-green.log", + "exit_code": 0, + "command": "build/candidate-adapter/bin/Release/candidate_core_test.exe --source-new-background" + } + ], + "source_identity": "95cd2c38ad36b5c4bc756192b9a8780466936a0a" + }, + { + "path": "local/r29-source-style/frozen-validation.json", + "sha256": "8ff5baab71bf6a1228a4f300c1bd12a034a668ec73956d102f35fc2f52f0f681", + "stage": "frozen", + "recorded_head": "caeb5bfd1accc93c3eea7ff1a9d25a256e3149f5", + "recorded_files": [ + { + "path": "experiments/core/candidate_terminal.cpp", + "sha256": "ac6945bca9d803f05cbf81193a63f9fa364775f240135d4453c6cb0c66cfb70c" + }, + { + "path": "experiments/core/candidate_terminal.hpp", + "sha256": "123fd595d6f993be29bac2c7af35c5b87c975af66275a0f05b94a4766a635a9b" + }, + { + "path": "experiments/core/candidate_terminal_test.cpp", + "sha256": "35414ded20bcef4f1750924d04d71c6c8ba7e399e95096b3e14c432d86ad97de" + }, + { + "path": "build/candidate-adapter/bin/Release/candidate_core_test.exe", + "sha256": "f5f9ff93236f8f6e4a70b76adc682262245b525d5825b9edbb3fff47c6bb80e4" + }, + { + "path": "local/r29-source-style/build-green.log", + "sha256": "4722a25358f604c7062ebea67c3a54d19051c54278bffab77ef2fea913a63b12" + }, + { + "path": "local/r29-source-style/bold-space-green.log", + "sha256": "a9f58776a09b5dac438049683f24bf85764e0ff8e7455952456165c68c158627" + }, + { + "path": "local/r29-source-style/core-regression.log", + "sha256": "3d7ca02ffd7486699a996c9037e88c4276157115eafe3d643ee1895c993b0a5a" + }, + { + "path": "local/r29-source-style/visible-suffix-regression.log", + "sha256": "a9f58776a09b5dac438049683f24bf85764e0ff8e7455952456165c68c158627" + }, + { + "path": "local/r29-source-style/partial-capacity-regression.log", + "sha256": "a9f58776a09b5dac438049683f24bf85764e0ff8e7455952456165c68c158627" + } + ], + "exit_code": null, + "scope": "core and source ownership only; shell not run", + "logs": [ + { + "path": "local/r29-source-style/build-green.log", + "exit_code": null, + "command": null + }, + { + "path": "local/r29-source-style/bold-space-green.log", + "exit_code": 0, + "command": null + }, + { + "path": "local/r29-source-style/core-regression.log", + "exit_code": 0, + "command": null + }, + { + "path": "local/r29-source-style/visible-suffix-regression.log", + "exit_code": 0, + "command": null + }, + { + "path": "local/r29-source-style/partial-capacity-regression.log", + "exit_code": 0, + "command": null + } + ], + "source_identity": "95cd2c38ad36b5c4bc756192b9a8780466936a0a" + } + ], + "local_log_sha256": { + "local/r29-source-integration/workspace-final.log": "f91253a7e095b7e6387077ccbca34856ad0e9655a685fd3641c682d77484c44e", + "local/r29-source-integration/build-final.log": "4bf6a9d1b3fd39b5a2f192767657fd467e81fa89e41a85c3c3afba6aee9ed68c", + "local/r29-source-integration/ctest-before-style.log": "dec6bae5cb8ac3190628ad0211b17afa37d26d79649c5a37947976bc4d626c0b", + "local/r29-source-integration/last-test-before-style.log": "8e87f17660f166187e9a2cc61b92558bb6ae39fa64ab19c576ca5f801adc3446", + "local/r29-source-ownership/build-partial-capacity-final.log": "51738d0a56d352c98f09d369a55638f66266af2f7fd40732e263b442e8d5e875", + "local/r29-source-ownership/partial-capacity-detail-red.log": "aff720a8da785b28f46dbc0e9b83fcea7fcadf2e146b0473207aaf4110ea6649", + "local/r29-source-ownership/partial-capacity-frozen.log": "a9f58776a09b5dac438049683f24bf85764e0ff8e7455952456165c68c158627", + "local/r29-source-ownership/core-capacity-frozen.log": "3d7ca02ffd7486699a996c9037e88c4276157115eafe3d643ee1895c993b0a5a", + "local/r29-source-ownership/visible-suffix-capacity-frozen.log": "a9f58776a09b5dac438049683f24bf85764e0ff8e7455952456165c68c158627", + "local/r29-source-ownership/new-background-capacity-frozen.log": "a9f58776a09b5dac438049683f24bf85764e0ff8e7455952456165c68c158627", + "local/r29-source-ownership/partial-visible-capacity-frozen.log": "f8afae989a01a69b0a8f1572f3449dc89405629cb287c3c84053c3dfd5c44e52", + "local/r29-discard-grapheme/red.log": "50b64ce6c0affea3b69407132e5973928c2876efd94de8137312af21cba088fb", + "local/r29-discard-grapheme/red-exit.txt": "bdc830287616dae1818ced3e7c186e1387155900ad30d5983ec02da1519c4a95", + "local/r29-discard-grapheme/green.log": "77eca7a31625f1516af42f3b78a6ec4006cdba2fe52e2e6d76fe1a9e7adf1f52", + "local/r29-discard-grapheme/green-exit.txt": "13bf7b3039c63bf5a50491fa3cfd8eb4e699d1ba1436315aef9cbe5711530354", + "local/r29-source-style/build-red.log": "3dc6a5da989532287d8bc4d3d45ef5f5ad084b953cfbb949a95223daeec20467", + "local/r29-source-style/bold-space-red.log": "2b0c0cdd92d68793f071f3b13d203a1064c50c9f74f71aedb5a758a17fabdfea", + "local/r29-source-style/build-green.log": "4722a25358f604c7062ebea67c3a54d19051c54278bffab77ef2fea913a63b12", + "local/r29-source-style/bold-space-green.log": "a9f58776a09b5dac438049683f24bf85764e0ff8e7455952456165c68c158627", + "local/r29-source-style/core-regression.log": "3d7ca02ffd7486699a996c9037e88c4276157115eafe3d643ee1895c993b0a5a", + "local/r29-source-style/visible-suffix-regression.log": "a9f58776a09b5dac438049683f24bf85764e0ff8e7455952456165c68c158627", + "local/r29-source-style/partial-capacity-regression.log": "a9f58776a09b5dac438049683f24bf85764e0ff8e7455952456165c68c158627", + "local/r29-source-integration/build-after-style.log": "6426e47aeb0618ff2a0b4a292ca602081d0c3c93d559f89a10e59b855a0e9bae", + "local/r29-source-integration/ctest-after-style.log": "65c5b0ea4b6a2e007bab55c82eebfe5b258185194edd69671db136504a792c37", + "local/r29-source-integration/last-test-after-style.log": "0a91231b47f8927d9f4b3174fc4852a76338026620717283d842afd5c75b978d", + "local/r29-source-integration/trace-build-final.log": "08fe3f075a16643d29a49ec8bfe75d629f54508155e7a797cde15412d8e45f14", + "local/r29-source-integration/runner-final.log": "ef004522aab5552fe83b1e32263326d5406b235e993f81fd304c7f70339b23ac", + "local/r29-source-integration/partial-visible-final.log": "8c85c093c56979ccf5ec1076b31a319119bdb9c860bbed2301e6984c821fc16d" + }, + "isolated_old_binary_reproductions": { + "source_commit": "eea3a2c7d79ceffba95065a2544a5fe6f67aed3c", + "runs_each": 1, + "cases": [ + { + "case": "ui-history", + "invocations": 1, + "target_exit_code": 2, + "target_exit_hex": "0x00000002", + "original_stderr": "history retained frame differs from full redraw\r\n", + "stderr": "history retained frame differs from full redraw\r\n", + "stdout": "", + "timed_out": false, + "job_after": { + "active": 0, + "total": 13, + "terminated": 0 + }, + "job_after_cleanup": { + "active": 0, + "total": 13, + "terminated": 0 + }, + "desktop_gone": true, + "cleanup_errors": [], + "verdict": "original_error_reproduced", + "native_scroll_count": 2, + "failed_phase": "not persisted by original runner" + }, + { + "case": "selection-height-681", + "invocations": 1, + "target_exit_code": 3221226525, + "target_exit_hex": "0xC000041D", + "original_stderr": "render API failed\r\n", + "stderr": "", + "stdout": "", + "timed_out": false, + "job_after": { + "active": 0, + "total": 10, + "terminated": 0 + }, + "job_after_cleanup": { + "active": 0, + "total": 10, + "terminated": 0 + }, + "desktop_gone": true, + "cleanup_errors": [], + "verdict": "different_failure_original_render_error_unresolved", + "selection_phases": [ + "empty", + "forward", + "reverse", + "word", + "line", + "zoom-before" + ], + "selection_base_equals_original": true + } + ], + "summary_sha256": "c7dd77cfa3954a2b2b7bffd5d225024452b0bd445e2ebc0c14e3d30bde15cd36", + "frozen_binaries_manifest_sha256": "3720864e36d98a479a975951046316c4ea6efd3ae6fe6461ee8b1e318fdbc203", + "preserved_executables": 13, + "distributed_font_files": 3, + "scope": "Original 40-second runner limit and cleanup; private non-input desktop in current station. No root-cause claim." + }, + "reviews": { + "source_composition": { + "standards_hard": 0, + "standards_smells": 1, + "smell": "Parallel Line/native ID/source arrays; nonblocking maintenance suggestion", + "spec_p2": "Styled blank source loss, independently reproduced and fixed in 95cd2c3" + }, + "native_lifecycle": { + "standards": 0, + "spec": 0 + }, + "style_fix": { + "standards": 0, + "spec": 0 + } + }, + "known_unfinished": [ + { + "item": "Partial visible repaint document order", + "status": "observed unresolved counterexample", + "evidence": "local/r29-source-ownership/partial-visible-capacity-frozen.log", + "detail": "Copy puts B before ABCDEFGH; one EFGH match removes the earlier duplicate but does not fix chronology." + }, + { + "item": "Mixed native row partial-source splitting and multi-owner union suppression", + "status": "implementation/validation gap; not a passed comparison", + "source": "parent task and prior source review; no new execution in this draft" + }, + { + "item": "Dedicated equal-text independent-source adapter counterexample", + "status": "not newly validated by this slice; existing equal-output regression is narrower" + }, + { + "item": "Complete candidate product matrix", + "status": "Final full CTest still has failures; original failures remain separately preserved" + }, + { + "item": "Original behavior, final architecture, performance, full R2 acceptance", + "status": "not established by these records" + } + ], + "limits": [ + "No experimental backend, actual different-DPI, full system input or presentation acceptance.", + "No performance measurement or approved tolerance; no final core/GUI selection.", + "Partial-visible diagnostic exit zero is observation only; B-before-A order remains wrong.", + "The mixed-width indexing path is a source finding, not the proven cause of either runtime failure.", + "Private raw artifacts and frozen executables stay local; no WindTerm binary/resources/code are published." + ] +} From 1fafb999d6e031ac7206e499062f7e1b2f1a53ff Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Wed, 28 Oct 2026 04:37:54 +0800 Subject: [PATCH 12/14] fix: project complete history through source coordinates (#29) --- CMakeLists.txt | 8 +- experiments/core/candidate_terminal_test.cpp | 174 +++++++++++++ experiments/core/history_view.hpp | 254 +++++++++++++++++-- experiments/core/view_mapping.hpp | 86 +++++++ experiments/presentation/render.hpp | 13 +- experiments/shell/search_interactions.hpp | 16 +- experiments/shell/text_interactions.hpp | 13 +- 7 files changed, 519 insertions(+), 45 deletions(-) create mode 100644 experiments/core/view_mapping.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 950fbfcf..954afbe0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -275,7 +275,13 @@ if(BUILD_TESTING) add_test(NAME candidate_source_visible_suffix COMMAND candidate_core_test --source-visible-suffix) add_test(NAME candidate_source_new_background COMMAND candidate_core_test --source-new-background) add_test(NAME candidate_source_partial_capacity COMMAND candidate_core_test --source-partial-capacity) - set_tests_properties(candidate_source_visible_suffix candidate_source_new_background candidate_source_partial_capacity PROPERTIES TIMEOUT 10) + add_test(NAME candidate_history_viewport_projection COMMAND candidate_core_test --history-viewport-projection) + add_test(NAME candidate_history_viewport_text COMMAND candidate_core_test --history-viewport-text) + add_test(NAME candidate_history_viewport_wide COMMAND candidate_core_test --history-viewport-wide) + add_test(NAME candidate_history_viewport_find COMMAND candidate_core_test --history-viewport-find) + set_tests_properties(candidate_source_visible_suffix candidate_source_new_background candidate_source_partial_capacity + candidate_history_viewport_projection candidate_history_viewport_text + candidate_history_viewport_wide candidate_history_viewport_find PROPERTIES TIMEOUT 10) endif() add_test(NAME resize_core COMMAND resize_test) add_test(NAME history_core COMMAND history_test) diff --git a/experiments/core/candidate_terminal_test.cpp b/experiments/core/candidate_terminal_test.cpp index abdd7742..25d50f0f 100644 --- a/experiments/core/candidate_terminal_test.cpp +++ b/experiments/core/candidate_terminal_test.cpp @@ -994,6 +994,164 @@ void conpty_resize_replay(const std::filesystem::path& path) { } } +void cached_history_viewport_projects_complete_source() { + const std::string expected = "ABCDEFGH\r\nB\r\nC\r\nD"; + termforge::CandidateTerminal terminal(2, 8, 16); + terminal.feed(expected); + terminal.resize(4, 4); + terminal.feed("\x1b[?25l\x1b[2J\x1b[HC\r\nD\x1b[?25h"); + + termforge::TextDocument document(terminal.screen); + termforge::TextSelection whole; + const auto begin = document.position(0, 0); + const auto end = document.position(terminal.screen.history.size() + 1, 1); + require(begin && end && whole.set(document, {*begin, *end}) && + whole.copy(document, 128).text == expected, "history viewport source fixture differs"); + + termforge::HistoryViewport viewport; + viewport.scroll_to(terminal.screen, 0); + const auto top = viewport.snapshot(terminal.screen); + require(top.lines.size() == 4 && top.cursor_row == -1, "historical page shape or caret differs"); + for (const auto& line : top.lines) + require(line.size() == 4, "history viewport supplied a nonrectangular renderer page"); + require(text(top.lines[0]) == "ABCD" && text(top.lines[1]) == "EFGH" && + text(top.lines[2]) == "B" && text(top.lines[3]) == "C", + "history viewport did not expose the complete long source at four columns"); + + viewport.scroll_by(terminal.screen, 1); + const auto next = viewport.snapshot(terminal.screen); + require(next.lines.size() == 4, "scrolled history page height differs"); + for (const auto& line : next.lines) + require(line.size() == 4, "scrolled history page is not rectangular"); + require(text(next.lines[0]) == "EFGH" && text(next.lines[1]) == "B" && + text(next.lines[2]) == "C" && text(next.lines[3]) == "D", + "history viewport cannot reach the next source line in display order"); + const auto copy = whole.copy(document, 128); + require(document.valid(*begin) && document.valid(*end) && + copy.status == termforge::SelectionCopyStatus::ok && copy.text == expected, + "view projection clipped or changed the complete source document"); +} + +void cached_history_viewport_selects_displayed_text() { + termforge::CandidateTerminal terminal(2, 8, 16); + terminal.feed("ABCDEFGH\r\nB\r\nC\r\nD"); + terminal.resize(4, 4); + terminal.feed("\x1b[?25l\x1b[2J\x1b[HC\r\nD\x1b[?25h"); + termforge::TextDocument document(terminal.screen); + termforge::HistoryViewport viewport; + viewport.scroll_to(terminal.screen, 0); + const auto page = viewport.snapshot(terminal.screen); + const auto begin = page.mapping.hit(document, 1, 0, termforge::ViewAffinity::leading); + const auto end = page.mapping.hit(document, 1, 4, termforge::ViewAffinity::trailing); + require(begin && end && *begin == *document.position(0, 4) && *end == *document.position(0, 8), + "displayed EFGH endpoints did not map to the original source columns"); + termforge::TextSelection selection; + require(selection.set(document, {*begin, *end}) && selection.copy(document, 64).text == "EFGH", + "dragging across displayed EFGH copied different text"); + const auto spans = page.mapping.project(document, *selection.range()); + require(spans.size() == 1 && spans[0].row == 1 && + spans[0].column_begin == 0 && spans[0].column_end == 4, + "EFGH selection did not highlight exactly its displayed row"); + const auto c = page.mapping.hit(document, 0, 2, termforge::ViewAffinity::leading); + const auto f = page.mapping.hit(document, 1, 2, termforge::ViewAffinity::trailing); + require(c && f, "cross-fragment drag endpoints are missing"); + for (const auto range : {termforge::TextRange{*c, *f}, termforge::TextRange{*f, *c}}) { + require(selection.set(document, range) && selection.copy(document, 64).text == "CDEF", + "cross-fragment drag inserted a break or changed text"); + const auto highlight = page.mapping.project(document, *selection.range()); + require(highlight.size() == 2 && highlight[0].row == 0 && highlight[0].column_begin == 2 && + highlight[0].column_end == 4 && highlight[1].row == 1 && + highlight[1].column_begin == 0 && highlight[1].column_end == 2, + "forward or reverse cross-fragment highlight differs"); + } + const auto g = page.mapping.hit(document, 1, 2, termforge::ViewAffinity::leading); + const auto b = page.mapping.hit(document, 2, 1, termforge::ViewAffinity::trailing); + require(g && b && selection.set(document, {*g, *b}) && selection.copy(document, 64).text == "GH\r\nB", + "display projection lost a source hard break during drag selection"); +} + +void cached_history_viewport_reveals_find_position() { + termforge::CandidateTerminal terminal(2, 8, 16); + terminal.feed("ABCDEFGH\r\nB\r\nC\r\nD"); + terminal.resize(4, 4); + terminal.feed("\x1b[?25l\x1b[2J\x1b[HC\r\nD\x1b[?25h"); + termforge::TextDocument document(terminal.screen); + termforge::TextSearch search; + const auto generation = search.start(document, "EFGH"); + for (int step = 0; step < 100 && search.status() == termforge::SearchStatus::running; ++step) + search.step(document, generation, 64); + const auto match = search.match(document, 0); + require(search.status() == termforge::SearchStatus::complete && search.matches().size() == 1 && match, + "viewport Find fixture did not locate one EFGH"); + termforge::HistoryViewport viewport; + viewport.bottom(terminal.screen); + require(viewport.reveal(terminal.screen, match->range.begin).has_value(), "live Find position was rejected"); + const auto page = viewport.snapshot(terminal.screen); + require(text(page.lines[0]) == "EFGH" && text(page.lines[1]) == "B" && + text(page.lines[2]) == "C" && text(page.lines[3]) == "D", + "Find reveal lost the source column and displayed the start of the old long row"); + const auto spans = page.mapping.project(document, match->range); + require(spans.size() == 1 && spans[0].row == 0 && spans[0].column_begin == 0 && spans[0].column_end == 4, + "Find match was not highlighted in the revealed fragment"); + termforge::TextSelection selection; + require(selection.set(document, match->range), "Find selection failed"); + terminal.resize(4, 8); + terminal.feed("\x1b[?25l\x1b[2J\x1b[HC\r\nD\x1b[?25h"); + require(text(viewport.snapshot(terminal.screen).lines[0]) == "ABCDEFGH" && + selection.copy(document, 64).text == "EFGH", "width growth lost the source selection or reading row"); + terminal.resize(4, 4); + terminal.feed("\x1b[?25l\x1b[2J\x1b[HC\r\nD\x1b[?25h"); + require(text(viewport.snapshot(terminal.screen).lines[0]) == "EFGH" && + selection.copy(document, 64).text == "EFGH", "restored width lost the second-fragment reading anchor"); + terminal.feed("\x1b[3J"); + const termforge::TextDocument cleared(terminal.screen); + require(!page.mapping.hit(cleared, 0, 0, termforge::ViewAffinity::leading) && + page.mapping.project(cleared, match->range).empty() && + !viewport.reveal(terminal.screen, match->range.begin) && !search.match(cleared, 0) && + selection.copy(cleared, 64).status == termforge::SelectionCopyStatus::invalidated, + "cleared history remained accessible through an old display map"); +} + +void cached_history_viewport_reflows_wide_wrap_gap() { + termforge::CandidateTerminal terminal(2, 4, 16); + terminal.feed("ABC\xe7\x95\x8cZ\r\nB\r\nC\r\nD"); + terminal.resize(4, 3); + terminal.feed("\x1b[?25l\x1b[2J\x1b[HC\r\nD\x1b[?25h"); + const auto& history = terminal.screen.history; + require(history.size() >= 2 && history[0].size() == 4 && history[0][3].leading_spacer && + termforge::continues_into(history[0], history[1]) && history[1][0].text == "\xe7\x95\x8c" && + history[1][0].width == 2 && history[1][1].width == 0 && history[1][2].text == "Z", + "wide-wrap viewport source fixture differs"); + termforge::HistoryViewport viewport; + viewport.scroll_to(terminal.screen, 0); + const auto page = viewport.snapshot(terminal.screen); + require(page.lines.size() == 4 && text(page.lines[0]) == "ABC" && + termforge::project_line(page.lines[1]).text == "\xe7\x95\x8cZ" && + text(page.lines[2]) == "B" && text(page.lines[3]) == "C", + "history viewport retained an obsolete wide-wrap gap in its display order"); + require(page.lines[1][0].text == "\xe7\x95\x8c" && page.lines[1][0].width == 2 && + page.lines[1][1].width == 0 && page.lines[1][2].text == "Z", + "history viewport misplaced the wide glyph after reflow"); + termforge::CandidateTerminal narrow(2, 4, 16); + narrow.feed("\xe7\x95\x8cZ\r\nB\r\nC\r\nD"); + narrow.resize(4, 1); + narrow.feed("\x1b[?25l\x1b[2J\x1b[HC\r\nD\x1b[?25h"); + termforge::HistoryViewport single; + single.scroll_to(narrow.screen, 0); + const auto clipped = single.snapshot(narrow.screen); + require(clipped.lines[0].size() == 1 && clipped.lines[0][0].text == "\xe7\x95\x8c" && + clipped.lines[0][0].width == 1 && clipped.lines[0][0].clipped, + "single-column history lost the complete wide glyph"); + const termforge::TextDocument narrow_document(narrow.screen); + const auto lead = clipped.mapping.hit(narrow_document, 0, 0, termforge::ViewAffinity::leading); + const auto tail = clipped.mapping.hit(narrow_document, 0, 1, termforge::ViewAffinity::trailing); + require(lead && tail && lead->column == 0 && tail->column == 2, "clipped glyph did not retain its full source span"); + termforge::TextSelection selected; + for (const auto range : {termforge::TextRange{*lead, *tail}, termforge::TextRange{*tail, *lead}}) + require(selected.set(narrow_document, range) && selected.copy(narrow_document, 64).text == "\xe7\x95\x8c", + "clipped wide glyph selection lost text in one drag direction"); +} + void selection_search_and_viewport() { termforge::CandidateTerminal terminal(2, 8, 4); terminal.feed("alpha\r\nbeta\r\ngamma"); @@ -1103,6 +1261,22 @@ int main(int argc, char** argv) { cached_history_partial_capacity_keeps_unremoved_source(); return 0; } + if (argc > 1 && std::string_view(argv[1]) == "--history-viewport-projection") { + cached_history_viewport_projects_complete_source(); + return 0; + } + if (argc > 1 && std::string_view(argv[1]) == "--history-viewport-text") { + cached_history_viewport_selects_displayed_text(); + return 0; + } + if (argc > 1 && std::string_view(argv[1]) == "--history-viewport-wide") { + cached_history_viewport_reflows_wide_wrap_gap(); + return 0; + } + if (argc > 1 && std::string_view(argv[1]) == "--history-viewport-find") { + cached_history_viewport_reveals_find_position(); + return 0; + } fragmentation(); streaming_graphemes_selection_and_history(); streaming_margin_padding_is_not_copied_as_an_inserted_space(); diff --git a/experiments/core/history_view.hpp b/experiments/core/history_view.hpp index 2d6fd413..55d416ba 100644 --- a/experiments/core/history_view.hpp +++ b/experiments/core/history_view.hpp @@ -1,6 +1,6 @@ -// Independent physical-row history viewport. No HWND, parser or transport state. +// Independent history display projection. No HWND, parser or transport state. #pragma once -#include "screen.hpp" +#include "view_mapping.hpp" namespace termforge { struct ViewportState { @@ -11,27 +11,210 @@ struct ViewportState { struct ViewSnapshot { std::vector lines; ViewportState viewport; + ViewMapping mapping; + int columns = 0; int cursor_row = -1, cursor_column = 0; }; class HistoryViewport { + struct Piece { + size_t source_row = 0, source_column = 0, length = 0; + size_t column = 0, display_length = 0; + bool clipped = false; + }; + struct DisplayRow { + size_t source_row = 0, source_column = 0, used = 0; + bool physical = true; + LineEnding ending = LineEnding::none; + std::vector pieces; + }; + struct Layout { + std::vector rows; + size_t history = 0; + }; uint64_t anchor_ = 0; + size_t anchor_column_ = 0; bool following_ = true; -public: - // A viewport is owned by exactly one session. Never share it across Screens. - ViewportState reconcile(const Screen& screen) { - const size_t maximum = screen.history.size(); - const size_t top = following_ ? maximum : (std::min)(screen.lower_bound_line(anchor_), maximum); - anchor_ = screen.retained_line(top).id; - return {top, screen.retained_size(), static_cast(screen.height()), maximum, anchor_, following_}; + + static bool default_padding(const Cell& cell) { + return cell.text == " " && cell.width == 1 && cell.style.plain() && cell.marks == 0 && + !cell.written && !cell.clipped && !cell.grapheme_fragment && !cell.leading_spacer; + } + + static void project_chain(const Screen& screen, size_t first, size_t last, Layout& layout) { + const auto width = static_cast(screen.width()); + const auto start = layout.rows.size(); + DisplayRow current; + current.physical = false; + current.source_row = first; + const auto flush = [&] { + current.ending = LineEnding::soft_wrap; + layout.rows.push_back(std::move(current)); + current = DisplayRow{}; + current.physical = false; + }; + for (size_t index = first; index <= last; ++index) { + const auto& line = screen.retained_line(index); + size_t end = line.size(); + while (end && default_padding(line[end - 1])) --end; + for (size_t column = 0; column < end;) { + // An old autowrap gap belongs to its former grid geometry, + // not to the logical text being laid out at the new width. + if (line[column].leading_spacer) { ++column; continue; } + const auto length = line[column].width == 2 && column + 1 < end && + line[column + 1].width == 0 ? size_t{2} : size_t{1}; + const auto display_length = (std::min)(length, width); + if (current.used && current.used + display_length > width) flush(); + if (!current.used) { + current.source_row = index; + current.source_column = column; + } + const bool clipped = display_length != length; + if (!current.pieces.empty() && !clipped && !current.pieces.back().clipped && + current.pieces.back().source_row == index && + current.pieces.back().source_column + current.pieces.back().length == column) { + current.pieces.back().length += length; + current.pieces.back().display_length += display_length; + } else { + current.pieces.push_back({index, column, length, current.used, display_length, clipped}); + } + current.used += display_length; + column += length; + if (current.used == width) flush(); + } + } + if (current.used || layout.rows.size() == start) { + if (!current.used) current.source_row = first; + layout.rows.push_back(std::move(current)); + } + layout.rows.back().ending = screen.retained_line(last).ending; + } + + static Layout layout(const Screen& screen) { + Layout result; + result.rows.reserve(screen.retained_size()); + for (size_t first = 0; first < screen.history.size();) { + size_t last = first; + bool mixed = screen.retained_line(first).size() != static_cast(screen.width()); + while (last + 1 < screen.history.size() && + continues_into(screen.retained_line(last), screen.retained_line(last + 1))) { + ++last; + mixed |= screen.retained_line(last).size() != static_cast(screen.width()); + } + if (mixed) project_chain(screen, first, last, result); + else for (size_t index = first; index <= last; ++index) { + DisplayRow row; + row.source_row = index; + result.rows.push_back(std::move(row)); + } + first = last + 1; + } + result.history = result.rows.size(); + // The active grid retains its physical layout and cursor coordinates. + for (size_t index = screen.history.size(); index < screen.retained_size(); ++index) { + DisplayRow row; + row.source_row = index; + result.rows.push_back(std::move(row)); + } + return result; + } + + static size_t locate(const Layout& display, size_t source_row, size_t column) { + size_t nearest = display.rows.size() - 1; + for (size_t index = 0; index < display.rows.size(); ++index) { + const auto& row = display.rows[index]; + if (row.physical) { + if (row.source_row >= source_row) return index; + continue; + } + if (row.source_row > source_row) return nearest == display.rows.size() - 1 ? index : nearest; + if (row.source_row == source_row) nearest = index; + for (const auto& piece : row.pieces) { + if (piece.source_row != source_row) continue; + nearest = index; + if (column < piece.source_column + piece.length) return index; + } + } + return nearest; + } + + void set_anchor(const Screen& screen, const DisplayRow& row) { + anchor_ = screen.retained_line(row.source_row).id; + anchor_column_ = row.source_column; + } + + ViewportState reconcile(const Screen& screen, const Layout& display) { + size_t top = display.history; + if (following_) set_anchor(screen, display.rows[top]); + else { + const auto source_row = screen.lower_bound_line(anchor_); + top = (std::min)(locate(display, source_row, anchor_column_), display.history); + if (source_row >= screen.retained_size() || screen.retained_line(source_row).id != anchor_) + set_anchor(screen, display.rows[top]); + else anchor_column_ = (std::min)(anchor_column_, screen.retained_line(source_row).size()); + } + return {top, display.rows.size(), static_cast(screen.height()), display.history, anchor_, following_}; + } + + ViewportState scroll_to(const Screen& screen, const Layout& display, size_t top) { + top = (std::min)(top, display.history); + following_ = top == display.history; + set_anchor(screen, display.rows[top]); + return reconcile(screen, display); + } + + static Line materialize(const Screen& screen, const DisplayRow& row) { + const auto& source = screen.retained_line(row.source_row); + if (row.physical) return source; + Line result(static_cast(screen.width())); + result.id = source.id; + result.version = source.version; + result.ending = row.ending; + result.wrap_columns = row.ending == LineEnding::soft_wrap ? row.used : 0; + for (const auto& piece : row.pieces) { + const auto& origin = screen.retained_line(piece.source_row); + if (piece.clipped) { + result[piece.column] = origin[piece.source_column]; + result[piece.column].width = static_cast(piece.display_length); + result[piece.column].clipped = true; + } else std::copy_n(origin.begin() + piece.source_column, piece.length, result.begin() + piece.column); + } + return result; } - ViewportState scroll_to(const Screen& screen, size_t top) { - top = (std::min)(top, screen.history.size()); - following_ = top == screen.history.size(); - anchor_ = screen.retained_line(top).id; - return reconcile(screen); + + static void map_piece(const Screen& screen, size_t row, const Piece& piece, + std::vector& mapping) { + const auto& source = screen.retained_line(piece.source_row); + size_t display_column = piece.column; + const auto end = piece.source_column + piece.length; + for (size_t column = piece.source_column; column < end;) { + const auto length = source[column].width == 2 && column + 1 < end && + source[column + 1].width == 0 ? size_t{2} : size_t{1}; + const auto width = piece.clipped ? piece.display_length : length; + mapping.push_back({row, display_column, display_column + width, + {{source.id, source.version, column}, {source.id, source.version, column + length}}}); + display_column += width; + column += length; + } } + + static void map_row(const Screen& screen, size_t row, const DisplayRow& display, + std::vector& mapping) { + const auto& source = screen.retained_line(display.source_row); + if (display.physical) { + map_piece(screen, row, {display.source_row, 0, source.size(), 0, source.size(), false}, mapping); + } else if (display.pieces.empty()) { + const TextPosition position{source.id, source.version, display.source_column}; + mapping.push_back({row, 0, 0, {position, position}}); + } else for (const auto& piece : display.pieces) map_piece(screen, row, piece, mapping); + } + +public: + // A viewport is owned by exactly one session. Never share it across Screens. + ViewportState reconcile(const Screen& screen) { return reconcile(screen, layout(screen)); } + ViewportState scroll_to(const Screen& screen, size_t top) { return scroll_to(screen, layout(screen), top); } ViewportState scroll_by(const Screen& screen, int64_t rows) { - const auto state = reconcile(screen); + const auto display = layout(screen); + const auto state = reconcile(screen, display); size_t top; if (rows < 0) { // Avoid signed overflow for INT64_MIN and never wrap size_t. @@ -41,21 +224,48 @@ class HistoryViewport { const auto distance = static_cast(rows); top = distance > state.maximum_top - state.top ? state.maximum_top : state.top + static_cast(distance); } - return scroll_to(screen, top); + return scroll_to(screen, display, top); + } + ViewportState bottom(const Screen& screen) { + const auto display = layout(screen); + return scroll_to(screen, display, display.history); } - ViewportState bottom(const Screen& screen) { return scroll_to(screen, screen.history.size()); } ViewportState reveal(const Screen& screen, uint64_t identity) { - const auto index = screen.lower_bound_line(identity); - return scroll_to(screen, index); + const auto display = layout(screen); + return scroll_to(screen, display, locate(display, screen.lower_bound_line(identity), 0)); + } + std::optional reveal(const Screen& screen, TextPosition position) { + if (!TextDocument(screen).valid(position)) return std::nullopt; + const auto display = layout(screen); + auto state = scroll_to(screen, display, + locate(display, screen.lower_bound_line(position.line_id), position.column)); + if (!following_) { + anchor_ = position.line_id; + anchor_column_ = position.column; + state.anchor = anchor_; + } + return state; } bool following() const { return following_; } uint64_t anchor() const { return anchor_; } ViewSnapshot snapshot(const Screen& screen) { + const auto display = layout(screen); ViewSnapshot result; - result.viewport = reconcile(screen); + result.columns = screen.width(); + result.viewport = reconcile(screen, display); result.lines.reserve(result.viewport.page); - for (size_t i = 0; i < result.viewport.page; ++i) - result.lines.push_back(screen.retained_line(result.viewport.top + i)); + std::vector mapping; + for (size_t i = 0; i < result.viewport.page; ++i) { + result.lines.push_back(materialize(screen, display.rows[result.viewport.top + i])); + map_row(screen, i, display.rows[result.viewport.top + i], mapping); + } + result.mapping = ViewMapping(static_cast(result.columns), std::move(mapping)); + for (size_t i = 0; i < result.lines.size(); ++i) { + const auto next = result.viewport.top + i + 1; + if (!display.rows[result.viewport.top + i].physical && + result.lines[i].ending == LineEnding::soft_wrap && next < display.rows.size()) + result.lines[i].next_id = screen.retained_line(display.rows[next].source_row).id; + } // A paused historical view must not draw a live caret over unrelated text. if (result.viewport.following) { result.cursor_row = screen.row; diff --git a/experiments/core/view_mapping.hpp b/experiments/core/view_mapping.hpp new file mode 100644 index 00000000..abefb95a --- /dev/null +++ b/experiments/core/view_mapping.hpp @@ -0,0 +1,86 @@ +// Value-only source coordinates published with one immutable display page. +#pragma once +#include "text_document.hpp" + +namespace termforge { +enum class ViewAffinity { leading, trailing }; +struct ViewCellSpan { + size_t row = 0, column_begin = 0, column_end = 0; +}; +struct ViewSourceSpan { + size_t row = 0, column_begin = 0, column_end = 0; + TextRange source; +}; + +class ViewMapping { + size_t columns_ = 0; + std::vector sources_; + + static bool before(TextPosition a, TextPosition b) { + return a.line_id < b.line_id || (a.line_id == b.line_id && a.column < b.column); + } + bool valid(const TextDocument& document, const ViewSourceSpan& span) const { + return span.column_begin <= span.column_end && span.column_end <= columns_ && + span.source.begin.line_id == span.source.end.line_id && + span.source.begin.version == span.source.end.version && + span.source.begin.column <= span.source.end.column && + document.valid(span.source.begin) && document.valid(span.source.end); + } +public: + ViewMapping() = default; + ViewMapping(size_t columns, std::vector sources) + : columns_(columns), sources_(std::move(sources)) {} + + std::optional hit(const TextDocument& document, size_t row, size_t column, + ViewAffinity affinity) const { + if (column > columns_) return std::nullopt; + const ViewSourceSpan* first = nullptr; + const ViewSourceSpan* previous = nullptr; + for (const auto& span : sources_) { + if (span.row != row) continue; + if (!valid(document, span)) return std::nullopt; + if (!first) first = &span; + } + if (!first) return std::nullopt; + for (const auto& span : sources_) { + if (span.row != row) continue; + if (column <= span.column_begin) { + if (previous && affinity == ViewAffinity::trailing) return previous->source.end; + return span.source.begin; + } + if (column < span.column_end) { + auto result = span.source.begin; + const auto source_width = span.source.end.column - result.column; + const auto display_width = span.column_end - span.column_begin; + // Ordinary wide-cell interiors keep their physical boundary. + // TextDocument normalizes a complete drag range afterward. + if (source_width == display_width) { + result.column += column - span.column_begin; + return result; + } + return affinity == ViewAffinity::leading ? span.source.begin : span.source.end; + } + if (column == span.column_end && affinity == ViewAffinity::trailing) return span.source.end; + previous = &span; + } + // Synthetic trailing padding has no independent source coordinates. + return previous ? std::optional{previous->source.end} : std::nullopt; + } + + std::vector project(const TextDocument& document, TextRange range) const { + const auto normalized = document.normalize(range); + if (!normalized || normalized->begin == normalized->end) return {}; + std::vector result; + for (const auto& span : sources_) { + if (!valid(document, span)) return {}; + if (!before(span.source.begin, normalized->end) || !before(normalized->begin, span.source.end)) + continue; + if (span.column_begin == span.column_end) continue; + if (!result.empty() && result.back().row == span.row && + result.back().column_end == span.column_begin) result.back().column_end = span.column_end; + else result.push_back({span.row, span.column_begin, span.column_end}); + } + return result; + } +}; +} // namespace termforge diff --git a/experiments/presentation/render.hpp b/experiments/presentation/render.hpp index 7728b020..6bc7c234 100644 --- a/experiments/presentation/render.hpp +++ b/experiments/presentation/render.hpp @@ -79,15 +79,20 @@ static unsigned color(int index, bool background) { return index < 0 ? (background ? 0x101010 : 0xe0e0e0) : palette[index]; } static COLORREF gdi_color(unsigned rgb) { return RGB((rgb>>16)&255, (rgb>>8)&255, rgb&255); } -// Copy once at the ownership boundary. No concurrent publication or reflow claim. +// Copy once at the ownership boundary. All rows share the explicit page width; +// source-document coordinates are independent of this presentation rectangle. struct Snapshot { const std::vector lines; + const termforge::ViewMapping mapping; + const int column_count; const int cursor_row, cursor_column, revision; - explicit Snapshot(const termforge::Screen& s, int rev) : lines(s.lines), cursor_row(s.row), cursor_column(s.column), revision(rev) {} + explicit Snapshot(const termforge::Screen& s, int rev) + : lines(s.lines), column_count(s.width()), cursor_row(s.row), cursor_column(s.column), revision(rev) {} explicit Snapshot(termforge::ViewSnapshot view, int rev) - : lines(std::move(view.lines)), cursor_row(view.cursor_row), cursor_column(view.cursor_column), revision(rev) {} + : lines(std::move(view.lines)), mapping(std::move(view.mapping)), column_count(view.columns), + cursor_row(view.cursor_row), cursor_column(view.cursor_column), revision(rev) {} int rows() const { return static_cast(lines.size()); } - int columns() const { return static_cast(lines.front().size()); } + int columns() const { return column_count; } }; static std::vector changed_rows(const Snapshot& a, const Snapshot& b) { std::set changed; diff --git a/experiments/shell/search_interactions.hpp b/experiments/shell/search_interactions.hpp index 2c028c5e..72250873 100644 --- a/experiments/shell/search_interactions.hpp +++ b/experiments/shell/search_interactions.hpp @@ -58,7 +58,7 @@ inline void ShellWindow::navigate_find(termforge::SearchDirection direction) { return; } find_current_=found; find_initial_=false; find_pending_directions_.clear(); - viewport_.reveal(parser_.screen,found->match.range.begin.line_id); + viewport_.reveal(parser_.screen,found->match.range.begin); if(find_) find_->set_status(partial?L"Searching… partial match (no wrap)": std::to_wstring(found->index+1)+L" / "+std::to_wstring(search_.matches().size())+ (search_.status()==termforge::SearchStatus::capped?L" — result limit reached":L"")+ @@ -136,17 +136,11 @@ inline void ShellWindow::search_overlay(termforge::ViewSnapshot& view) { if(view.lines.empty()) return; termforge::TextDocument document(parser_.screen); for(size_t index=0;indexview.lines.back().id) continue; const auto match=search_.match(document,index); if(!match) continue; - const auto& range=match->range; - for(auto& row:view.lines) { - if(row.idrange.end.line_id) continue; - const size_t first=row.id==range.begin.line_id?range.begin.column:0; - const size_t last=row.id==range.end.line_id?range.end.column:row.size(); - for(size_t column=first;columnindex==index?11:3; - row[column].style.fg=0; + for(const auto& span:view.mapping.project(document,match->range)) { + for(size_t column=span.column_begin;columnindex==index?11:3; + view.lines[span.row][column].style.fg=0; } } } diff --git a/experiments/shell/text_interactions.hpp b/experiments/shell/text_interactions.hpp index 3a372c7c..509875a8 100644 --- a/experiments/shell/text_interactions.hpp +++ b/experiments/shell/text_interactions.hpp @@ -7,8 +7,8 @@ inline std::optional ShellWindow::selection_position(in const int row=std::clamp(y/renderer_->cell_height(),0,snapshot_->rows()-1); const int column=std::clamp((x+(nearest?renderer_->cell_width()/2:0))/renderer_->cell_width(),0,snapshot_->columns()); termforge::TextDocument document(parser_.screen); - const auto index=document.index_of(snapshot_->lines[static_cast(row)].id); - return index?document.position(*index,static_cast(column)):std::nullopt; + return snapshot_->mapping.hit(document,static_cast(row),static_cast(column), + termforge::ViewAffinity::leading); } inline void ShellWindow::selection_end() { @@ -65,12 +65,11 @@ inline void ShellWindow::selection_autoscroll() { inline void ShellWindow::selection_overlay(termforge::ViewSnapshot& view) { termforge::TextDocument document(parser_.screen); - for(size_t row=0;rowbegin;columnend && column Date: Wed, 28 Oct 2026 04:46:44 +0800 Subject: [PATCH 13/14] fix: retain empty history anchors and validate display shape (#29) --- experiments/core/candidate_terminal_test.cpp | 22 +++++++++++++++++++ experiments/core/history_view.hpp | 11 ++++++---- experiments/presentation/render.hpp | 11 ++++++++-- .../presentation/renderer_metrics_test.cpp | 12 ++++++++++ 4 files changed, 50 insertions(+), 6 deletions(-) diff --git a/experiments/core/candidate_terminal_test.cpp b/experiments/core/candidate_terminal_test.cpp index 25d50f0f..c2fc8104 100644 --- a/experiments/core/candidate_terminal_test.cpp +++ b/experiments/core/candidate_terminal_test.cpp @@ -1032,6 +1032,27 @@ void cached_history_viewport_projects_complete_source() { "view projection clipped or changed the complete source document"); } +void cached_history_viewport_keeps_empty_anchor() { + termforge::CandidateTerminal terminal(2, 8, 16); + terminal.feed("A\r\n\r\nC\r\nD"); + terminal.resize(4, 4); + terminal.feed("\x1b[?25l\x1b[2J\x1b[HC\r\nD\x1b[?25h"); + const auto& history = terminal.screen.history; + require(history.size() == 2 && history[1].size() == 8 && + history[1].ending == termforge::LineEnding::hard_break && termforge::project_line(history[1]).text.empty(), + "empty history anchor source fixture differs"); + termforge::HistoryViewport viewport; + viewport.scroll_to(terminal.screen, 1); + const auto page = viewport.snapshot(terminal.screen); + require(page.viewport.top == 1 && text(page.lines[0]).empty() && text(page.lines[1]) == "C", + "scrolling to an empty cached hard line skipped to the active grid"); + const termforge::TextDocument document(terminal.screen); + viewport.bottom(terminal.screen); + require(viewport.reveal(terminal.screen, *document.position(1, 8)).has_value() && + viewport.snapshot(terminal.screen).viewport.top == 1, + "revealing an empty source end skipped its display row"); +} + void cached_history_viewport_selects_displayed_text() { termforge::CandidateTerminal terminal(2, 8, 16); terminal.feed("ABCDEFGH\r\nB\r\nC\r\nD"); @@ -1263,6 +1284,7 @@ int main(int argc, char** argv) { } if (argc > 1 && std::string_view(argv[1]) == "--history-viewport-projection") { cached_history_viewport_projects_complete_source(); + cached_history_viewport_keeps_empty_anchor(); return 0; } if (argc > 1 && std::string_view(argv[1]) == "--history-viewport-text") { diff --git a/experiments/core/history_view.hpp b/experiments/core/history_view.hpp index 55d416ba..149771c1 100644 --- a/experiments/core/history_view.hpp +++ b/experiments/core/history_view.hpp @@ -9,6 +9,8 @@ struct ViewportState { bool following = true; }; struct ViewSnapshot { + // Fragments may share their first source line's id/version. Text actions + // resolve original document coordinates through the mapping, not these ids. std::vector lines; ViewportState viewport; ViewMapping mapping; @@ -119,14 +121,15 @@ class HistoryViewport { } static size_t locate(const Layout& display, size_t source_row, size_t column) { - size_t nearest = display.rows.size() - 1; + std::optional nearest; for (size_t index = 0; index < display.rows.size(); ++index) { const auto& row = display.rows[index]; if (row.physical) { - if (row.source_row >= source_row) return index; + if (row.source_row == source_row) return index; + if (row.source_row > source_row) return nearest.value_or(index); continue; } - if (row.source_row > source_row) return nearest == display.rows.size() - 1 ? index : nearest; + if (row.source_row > source_row) return nearest.value_or(index); if (row.source_row == source_row) nearest = index; for (const auto& piece : row.pieces) { if (piece.source_row != source_row) continue; @@ -134,7 +137,7 @@ class HistoryViewport { if (column < piece.source_column + piece.length) return index; } } - return nearest; + return nearest.value_or(display.rows.size() - 1); } void set_anchor(const Screen& screen, const DisplayRow& row) { diff --git a/experiments/presentation/render.hpp b/experiments/presentation/render.hpp index 6bc7c234..5b5d1c3a 100644 --- a/experiments/presentation/render.hpp +++ b/experiments/presentation/render.hpp @@ -86,11 +86,18 @@ struct Snapshot { const termforge::ViewMapping mapping; const int column_count; const int cursor_row, cursor_column, revision; +private: + void validate_shape() const { + require(column_count > 0 && !lines.empty(), "empty presentation snapshot"); + for (const auto& line : lines) + require(line.size() == static_cast(column_count), "presentation snapshot row width mismatch"); + } +public: explicit Snapshot(const termforge::Screen& s, int rev) - : lines(s.lines), column_count(s.width()), cursor_row(s.row), cursor_column(s.column), revision(rev) {} + : lines(s.lines), column_count(s.width()), cursor_row(s.row), cursor_column(s.column), revision(rev) { validate_shape(); } explicit Snapshot(termforge::ViewSnapshot view, int rev) : lines(std::move(view.lines)), mapping(std::move(view.mapping)), column_count(view.columns), - cursor_row(view.cursor_row), cursor_column(view.cursor_column), revision(rev) {} + cursor_row(view.cursor_row), cursor_column(view.cursor_column), revision(rev) { validate_shape(); } int rows() const { return static_cast(lines.size()); } int columns() const { return column_count; } }; diff --git a/experiments/presentation/renderer_metrics_test.cpp b/experiments/presentation/renderer_metrics_test.cpp index 939c5034..7b27189b 100644 --- a/experiments/presentation/renderer_metrics_test.cpp +++ b/experiments/presentation/renderer_metrics_test.cpp @@ -13,6 +13,17 @@ static void verify_text_conversion() { } } +static void verify_snapshot_shape() { + termforge::Parser parser(2, 4); + termforge::HistoryViewport viewport; + auto page = viewport.snapshot(parser.screen); + page.lines[1].pop_back(); + bool rejected = false; + try { Snapshot invalid(std::move(page), 0); } + catch (const std::runtime_error&) { rejected = true; } + require(rejected, "nonrectangular page reached the fixed-width renderer snapshot"); +} + static void expect_rejected(const std::string& backend, int dpi, int zoom) { bool rejected = false; try { Renderer renderer(backend, dpi, zoom); } @@ -163,6 +174,7 @@ int main(int argc, char** argv) { return 1; } require(argc == 1, "unexpected renderer metrics arguments"); + verify_snapshot_shape(); verify_missing_font(); for (const int dpi : {-1,0,47,769}) expect_rejected("dwrite", dpi, 100); for (const int zoom : {-1,0,49,201}) expect_rejected("gdi", 96, zoom); From beedcedd66e53713ca05e86f43a6d09195b62772 Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Wed, 28 Oct 2026 15:11:33 +0800 Subject: [PATCH 14/14] docs: preserve viewport validation and current R2 gaps (#29) --- docs/EVIDENCE.md | 7 +- docs/FEATURES.md | 4 +- docs/PROVENANCE.md | 2 +- docs/TRACKING.md | 8 +- docs/milestones/R2-local-terminal.md | 8 +- experiments/core/CANDIDATE.md | 10 +- .../2026-10-28-history-viewport/README.md | 53 + .../validation.json | 1698 +++++++++++++++++ 8 files changed, 1775 insertions(+), 15 deletions(-) create mode 100644 experiments/core/results/2026-10-28-history-viewport/README.md create mode 100644 experiments/core/results/2026-10-28-history-viewport/validation.json diff --git a/docs/EVIDENCE.md b/docs/EVIDENCE.md index 66764273..ea5ac83b 100644 --- a/docs/EVIDENCE.md +++ b/docs/EVIDENCE.md @@ -10,7 +10,7 @@ |---|---| | main 默认:自研核心 + 系统 ConPTY | R1 限定总验收和有限 VT/Unicode 子集;默认构建不需要 Rust | | 当前源码显式候选:`termforge.10` + 系统 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 或完整官方矩阵验收 | +| PR #50 实验:`termforge.5` + 固定官方 ConPTY | [Draft PR](https://github.com/JavaZeroo/termforge/pull/50) / [#47](https://github.com/JavaZeroo/termforge/issues/47);早期 `.4` 的[联合基线](research/joint-conpty-baseline.md)保留。#51/#63 已在实验父分支限定闭合,#52 仍开放;#53 的[固定粘贴与八会话证据](https://github.com/JavaZeroo/termforge/blob/848cb0e621a291f732cc0e046d0b2fab6a77d962/docs/research/resize-replay-cost.md)不包含同次真实 CF/IME。完整后端矩阵仍未验收,不能引用当前候选/system 的成功代替 | R1 固定总出口的源码为 `ce9b24d`,早期 `2dc8d6d` / `9b16acc` 仍分别只代表原记录。源 VT、原生控制台 CHAR_INFO、transport、末端状态、自有 DIB、绘制提交和真实呈现是不同检查点;未知端点保持未知。 @@ -36,12 +36,13 @@ 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 清空不复活。`.6` 的[缓存与淘汰修复](../experiments/core/results/2026-10-28-history-cache/README.md)覆盖透明输入、主/备用屏归属、原生前缀和旧回放缓存失效。混宽历史视口、完整 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、实际呈现或性能。 | | WindTerm 长历史行的视图折行(#29) | [逐字符观察与边界](research/windterm-history-layout.md)、[两次事实/哈希](../experiments/core/results/2026-10-28-history-layout/windterm-observations.json) | 独立的两次七阶段观察:185 字符软行由 80/80/25 变为 62/62/61,两条 77 字符硬行各变为 62/15;文档文本与位置不变,恢复宽度后投影恢复。仅 ASCII 文本坐标接口;覆盖写入、选择/Find、物理格及呈现仍未知。 | | 候选原生重排边界与未解决的源文本重复(#29) | [工作检查点与反例](../experiments/core/results/2026-10-28-reflow-metadata/README.md)、[来源与验证](../experiments/core/results/2026-10-28-reflow-metadata/validation.json) | `.7` 修复原生硬/软边界传递及缩宽容量计账;Rust 与 runner 通过,C++ 长行复制仍红。混宽历史、来源归属、选择/Find 及完整产品矩阵未完成。 | | 单次 feed 旧来源追踪(#29) | [固定实现与检查](../experiments/core/results/2026-10-28-feed-origins/README.md) | `.9` 原生 24 项公开集成、既有 workspace、C ABI 与 trace 检查通过;C++ 长历史复制仍红,来源映射不授予历史保留许可,新增成本未测。 | -| C++ 历史来源、部分淘汰与字素维护(#29) | [反例、修复与剩余范围](../experiments/core/results/2026-10-28-source-ownership/README.md)、[分次验证身份](../experiments/core/results/2026-10-28-source-ownership/validation.json) | 固定复制/空硬行/跨软行 Find、可见后缀、部分淘汰与样式空格反例修复;`.10` 历史维护不再封闭待续字素。完整矩阵有真实 shell 失败,部分重绘源顺序和混宽视图仍未闭合,不能宣布 #29 或产品通过。 | +| C++ 历史来源、部分淘汰与字素维护(#29) | [反例、修复与剩余范围](../experiments/core/results/2026-10-28-source-ownership/README.md)、[分次验证身份](../experiments/core/results/2026-10-28-source-ownership/validation.json) | 固定复制/空硬行/跨软行 Find、可见后缀、部分淘汰与样式空格反例修复;`.10` 历史维护不再封闭待续字素。该检查点完整矩阵有真实 shell 失败、混宽视图未闭合,旧结果保留;后续视图修复及新矩阵见下列独立记录。部分重绘源顺序仍错,不能宣布 #29 或产品通过。 | +| 完整历史显示与源坐标(#29) | [实现、反例与边界](../experiments/core/results/2026-10-28-history-viewport/README.md)、[固定源码验证](../experiments/core/results/2026-10-28-history-viewport/validation.json) | `f41f747` 候选/system CTest 57/57,内部 9 项跳过;矩形页、宽字旧 gap、源列 Find、空历史锚点和形状门禁有独立红→绿。两个旧 UI 场景在 `1fafb99` 各通过一次,不能反填旧根因。部分可见源顺序、混合来源、系统 CF/真实 DPI/实验后端与性能仍未验收。 | | 候选一次 resize 来源映射与真实淘汰计数(#29) | [接口范围及检查点](../experiments/core/results/2026-10-28-resize-origins/README.md)、[源码/构建/日志身份](../experiments/core/results/2026-10-28-resize-origins/validation.json) | `.8` 导出双屏物理来源和硬边界,修复缩高来源漏记、多行滚动少计与扩高假计;Rust、C++ 映射 ABI 和 runner 通过,C++ 长行复制仍失败,历史合并尚未消费映射。未测性能。 | | WindTerm 首次增高的前置条件停止(#29) | [独立观察与边界](research/windterm-resize-text.md#独立观察初始-18-行直接增高)、[合成输入](../experiments/core/results/2026-10-28-history-layout/windterm-first-grow-input.json)、[事实与哈希](../experiments/core/results/2026-10-28-history-layout/windterm-first-grow-observations.json) | 独立一次 18→26 行观察中未见编号历史进入非负 y;128 次有界查询不变,末尾空串为同一文末位置别名。按计划未发送 CUP/重绘,B 案例未运行,覆盖写入政策仍未知。 | | WindTerm 查询后固定状态的 CUP 改写(#29) | [独立观察与限制](research/windterm-resize-text.md#独立观察dsr-后固定状态下的-cup-改写)、[精确输入](../experiments/core/results/2026-10-28-history-layout/windterm-fenced-cup-input.json)、[事实与哈希](../experiments/core/results/2026-10-28-history-layout/windterm-fenced-cup-observations.json) | 单次有界序列在完整前置状态核对后只改 H0026 文字;DSR 回复改变而选择文档位置不变。未裁决未查询状态、一般覆盖政策、物理 grid、ConPTY 或性能。 | diff --git a/docs/FEATURES.md b/docs/FEATURES.md index fa0cadfb..d9362f0d 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -14,7 +14,7 @@ |---|---|---| | 默认 main:自研核心 + 系统 ConPTY | 默认构建与已发布 Alpha 的基础路线,不需要 Rust | 已有 R1 限定验收;VT/Unicode 能力仍为有限子集,不能引用候选核心的字素或 TUI 结果 | | 当前源码显式候选:`termforge.10` + 系统 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 默认关闭,不由实验结果选择产品后端 | +| 实验分支:`termforge.5` + 固定官方 ConPTY | [PR #50(Draft)](https://github.com/JavaZeroo/termforge/pull/50),早期 `termforge.4` 联合源码 `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) 与[工作区 #63](https://github.com/JavaZeroo/termforge/issues/63) 已在实验父分支限定闭合,完整 resize/历史由 [#29](https://github.com/JavaZeroo/termforge/issues/29) 接续;[GUI Vim #52](https://github.com/JavaZeroo/termforge/issues/52) 仍开放。[#53](https://github.com/JavaZeroo/termforge/issues/53) 的原 3MB/768B/16KiB 未完成粘贴取消/关闭、负例及八会话 data-only 两轮已限定验证并整合到实验父分支;同次真实 CF/IME 与完整官方矩阵未通过。fence 默认关闭,不由实验结果选择产品后端 | 以下能力以所读源码版本及各自固定证据为准;某个配置的通过结果不能转用于另一个配置。R1 的真实系统 CF/拼音验收属于固定自研核心配置,不外推到当前候选 或实验后端联合配置。 @@ -33,7 +33,7 @@ | 中文 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 和有界恢复。显式候选 `.7` 接入 `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。显式候选 `.7` 保留 [#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)覆盖透明输入、主屏缓存归属及重复淘汰;`.8` 的[一次 resize 来源映射](../experiments/core/results/2026-10-28-resize-origins/README.md)及实际淘汰计数已有独立检查;`.9` 的[单次 feed 旧来源追踪](../experiments/core/results/2026-10-28-feed-origins/README.md)覆盖真实写入/移动与清空边界;C++ 已组合来源,固定长文本/空硬行/跨软行查找、无写入可见后缀与部分容量淘汰反例通过,`.10` 另修复历史删除打断待续字素 | [#29](https://github.com/JavaZeroo/termforge/issues/29) 的部分可见重绘顺序、混合来源合并、混宽历史视口及完整 resize/备用屏/滚动区交叉仍未完成;[原版两次文本观察](research/windterm-resize-text.md)仅证明固定输入保留及恢复尺寸后文本投影恢复;[逐字符历史观察](research/windterm-history-layout.md)另证明固定 ASCII 长行随宽度重新分段,[固定状态下的 CUP+DSR 观察](research/windterm-resize-text.md#独立观察dsr-后固定状态下的-cup-改写)仅确认一次序列改写 H0026,尚无普遍覆盖政策或交互验收。完整 reflow、child ack、实际呈现、系统/实验后端联合配置及性能分别验收;默认缩窄不可恢复 | R2 | +| resize 与 reflow | **实现中,未验收**:默认按 ADR-0005 物理行裁剪;候选保留重放、显式清空、缓存及来源追踪的[分层合同](../experiments/core/CANDIDATE.md)。当前实现保留完整历史源文本,混宽软链按显示宽度投影,鼠标、选择/Find 高亮和阅读锚点使用源坐标;固定长行、硬/空行、宽字、实际 resize 往返及失效场景有[独立证据](../experiments/core/results/2026-10-28-history-viewport/README.md)。最终候选/system CTest 57/57,内部 9 项跳过分列 | [#29](https://github.com/JavaZeroo/termforge/issues/29) 的部分可见重绘源顺序仍错,混合来源合并、历史/活动区交叉及完整 resize/备用屏/滚动区仍未闭合。[原版观察](research/windterm-resize-text.md)和[ASCII 折行观察](research/windterm-history-layout.md)各有固定范围,尚无通用覆盖或 Unicode/交互对照。真实 child、系统输入、不同 DPI、实验官方后端及性能分别验收;默认缩窄不可恢复,正式核心选型待用户确认 | 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 f31e6705..65e7720c 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.9`(Apache-2.0),固定上游 commit `542147067dd016fc5f49120ef0ba605a98a4e3a7` 的 manifest/changelog/许可证与 `src/`,并保留 ED、稳定行身份/淘汰、安全 swap、每缓冲区滚动区域、流式字素、显式历史清空代次、原生历史前缀淘汰、重排断行/容量计账及一次 resize/feed 来源映射补丁;精确传递依赖及校验值由 `Cargo.lock` 固定。未复制上游 46 MiB 行为录制,候选不进入当前产品包;默认 CMake 不依赖 Rust,显式适配器构建使用 staticlib。原始 0.26.0 身份、当前来源、许可证和补丁清单见[候选来源台账](../experiments/candidates/alacritty/PROVENANCE.md)。 +- 外部核心候选当前使用 `alacritty_terminal 0.26.1-dev+termforge.10`(Apache-2.0),固定上游 commit `542147067dd016fc5f49120ef0ba605a98a4e3a7` 的 manifest/changelog/许可证与 `src/`,并保留 ED、稳定行身份/淘汰、安全 swap、每缓冲区滚动区域、流式字素、显式历史清空代次、原生历史前缀淘汰、重排断行/容量计账、一次 resize/feed 来源映射及历史淘汰不封闭待续字素补丁;精确传递依赖及校验值由 `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/TRACKING.md b/docs/TRACKING.md index d122c9ce..587f3bf9 100644 --- a/docs/TRACKING.md +++ b/docs/TRACKING.md @@ -22,8 +22,8 @@ |---|---|---| | 主线候选与官方后端的联合基线 | [联合配置基线 #55](https://github.com/JavaZeroo/termforge/issues/55) | 从明确的 main 与 Draft 身份整合;可带已知 red 如实结项,不等待所有后端问题先修好 | | 原生 API / VT 末端 margin 的处理路径 | [平台语义与方案 #56](https://github.com/JavaZeroo/termforge/issues/56) | 可用固定探针独立推进;不能以策略说明豁免 #41 原合同 | -| 实验后端连续 resize | [残尾与 Unicode 重放 #51](https://github.com/JavaZeroo/termforge/issues/51) | 原冻结配置可诊断;最终修复验收使用 #55 联合基线 | -| 实验后端 Vim | [Console-width Vim #52](https://github.com/JavaZeroo/termforge/issues/52) | 同上;GUI stage59 与 native new-buffer 分列,早期 flags0 成功不替代最终配置 | +| 实验后端连续 resize | [残尾与 Unicode 重放 #51](https://github.com/JavaZeroo/termforge/issues/51) | 原生 viewport 与原 GUI/Unicode 合同已由 PR #89 在实验父分支限定闭合;完整 resize 由 #29、最终官方配置由 #47 接续 | +| 实验后端 Vim | [Console-width Vim #52](https://github.com/JavaZeroo/termforge/issues/52) | GUI 兼容方案仍未闭合;native 捕获修正、运行库隔离实验与原固定运行库结果分列,早期 flags0 成功不替代最终配置 | | 实验后端未完成粘贴取消 | [确定性 sender fence #53](https://github.com/JavaZeroo/termforge/issues/53) | 原 3MB/768B/16KiB 未完成期 cancel/close、负例与八会话 data-only 已限定闭合,PR #76 已合入实验父分支;完整官方矩阵与同次 CF/IME 仍由 #47 接续 | | 终端模式、备用屏与滚动区 | [终端行为 #27](https://github.com/JavaZeroo/termforge/issues/27) | 核心前置 #26 已验;整体验收由 #41 阻塞 | | resize、逻辑行与历史 | [resize 合同 #29](https://github.com/JavaZeroo/termforge/issues/29) | 核心前置 #26 已验;与 #27/#28 联合核对模式、区域、历史和实际 child 尺寸 | @@ -37,13 +37,13 @@ | 工作 | 责任任务 | 完成条件的归属 | |---|---|---| | 持续输出中的 Ctrl-C 历史失败 | [原始 ETX 回归 #36](https://github.com/JavaZeroo/termforge/issues/36) | 有界定位历史因果与原生交付;后续通过不回填旧因 | -| 新增实际回归 | [恢复 oracle 修正 #61](https://github.com/JavaZeroo/termforge/issues/61) · [InputPump gate #62](https://github.com/JavaZeroo/termforge/issues/62) · [工作区 stage52 #63](https://github.com/JavaZeroo/termforge/issues/63) · [hosted reverse #66](https://github.com/JavaZeroo/termforge/issues/66) | #61/#66 与实验 #62/#53 已按各自原合同限定闭合,#63 继续开放;不放宽期限、负载或几何断言 | +| 已闭合的回归切片 | [恢复 oracle 修正 #61](https://github.com/JavaZeroo/termforge/issues/61) · [InputPump gate #62](https://github.com/JavaZeroo/termforge/issues/62) · [工作区 stage52 #63](https://github.com/JavaZeroo/termforge/issues/63) · [hosted reverse #66](https://github.com/JavaZeroo/termforge/issues/66) | #61/#66 与实验 #62/#53/#63 已按各自原合同限定闭合;#63 的原生 viewport 修正由 PR #93 合入实验父分支,完整官方矩阵仍归 #47 | | 工作区保存判据与选择失败 | [保存 stage2 #72](https://github.com/JavaZeroo/termforge/issues/72) · [681 高度选择 #73](https://github.com/JavaZeroo/termforge/issues/73) | #72 的原字节因果修正已合入主线;#73 精确阶段与因果仍待定位,不重开不同触发的 #61/#66 | | 原 10 秒纯夹具超时 | [IME inventory 定位 #40](https://github.com/JavaZeroo/termforge/issues/40) | 阶段与清理已补齐;下一步是固定源码/宿主的有界原因定位 | | controller 自然退出 | [close 前退出 #44](https://github.com/JavaZeroo/termforge/issues/44) | 保留 send/wait/pre-close 与清理差异;不能以 close 后结果当自然退出 | | 旧 normal-1 未知异常 | [事件转换边界 #57](https://github.com/JavaZeroo/termforge/issues/57) | 独立于已修合法空标题 #45;原事件 kind/载荷未知,不能回填根因 | | Windows 工作流可靠性 | [回归汇总 #38](https://github.com/JavaZeroo/termforge/issues/38) | #40/#44/#57/#73/#80/#81/[#85](https://github.com/JavaZeroo/termforge/issues/85);已闭合切片和旧失败保留在该总项,不再排入当前队列 | -| 显式官方 ConPTY 整合 | [后端汇总 #47](https://github.com/JavaZeroo/termforge/issues/47) | #51/#52(GUI)/#63;#62/#53 与 #52 native 捕获已独立修正,#55 已固定实验联合基线,实验可选不等于产品默认或日常验收 | +| 显式官方 ConPTY 整合 | [后端汇总 #47](https://github.com/JavaZeroo/termforge/issues/47) | #52 GUI Vim、完整官方矩阵和同次系统输入仍未闭合;#51/#53/#62/#63 与 #52 native 捕获已有各自限定修正,实验可选不等于产品默认或日常验收 | | 每缓冲区 margin 端到端合同 | [margin 汇总 #41](https://github.com/JavaZeroo/termforge/issues/41) | #47/#56;原生 API、transport 和末端分别验证 | | 产品核心、GUI/平台与呈现边界 | [选型提案与用户确认 #34](https://github.com/JavaZeroo/termforge/issues/34) | #28–#31/#54,结合 #23/#26/#32/#33 和原版对标证据;重审 ADR-0003/0004,记录用户确认 | | 最终 Windows 开发版 | [产物与持续运行 #35](https://github.com/JavaZeroo/termforge/issues/35) | #27–#34/#36/#38;在最终产物上执行完整合同后核对 #7 出口 | diff --git a/docs/milestones/R2-local-terminal.md b/docs/milestones/R2-local-terminal.md index 23b00d7b..5ae95e56 100644 --- a/docs/milestones/R2-local-terminal.md +++ b/docs/milestones/R2-local-terminal.md @@ -18,8 +18,8 @@ R2 的前期平台调查/候选比较、有限会话与恢复、首批流式字 | 阶段 | 用户可审查的交付 | 责任与完成条件 | |---|---|---| -| 1. 冻结联合基线 | 主线候选和显式官方后端来自同一份可重建源码;系统对照与联合配置各有实际结果 | [联合基线 #55](https://github.com/JavaZeroo/termforge/issues/55):固定 source/依赖/二进制/包/flags,验证 API 生命期、原 175B margin、Unicode/glyph、真实尺寸 ack、退出与回收;重放 #51–#53 原场景,分列 red、green 和未复现。默认路线与 #34 产品决定保持不变 | -| 2. 闭合兼容性与输入 | 明确支持的 shell、分页器、编辑器、复杂文字与会话交互在联合配置中连续工作 | #47/#51–#53 处理后端;#41 处理原生 API/末端语义;#27/#29 核对模式与 resize,#28 验收固定 TUI/回复/输入字节,#30 验收完整 IME/选择/粘贴。实际不同 DPI 由 #31 并行提供 | +| 1. 冻结联合基线 | 主线候选和显式官方后端来自同一份可重建源码;系统对照与联合配置各有实际结果 | [联合基线 #55](https://github.com/JavaZeroo/termforge/issues/55):固定 source/依赖/二进制/包/flags,验证 API 生命期、原 175B margin、Unicode/glyph、真实尺寸 ack、退出与回收;重放 #51–#53 原场景,分列 red、green 和未复现。维持当前默认构建配置;#34 的正式产品选型仍待用户确认 | +| 2. 闭合兼容性与输入 | 明确支持的 shell、分页器、编辑器、复杂文字与会话交互在联合配置中连续工作 | #47/#52 接续后端联合验收,#51/#53/#62/#63 的限定成果纳入最终复验;#41 处理原生 API/末端语义;#27/#29 核对模式与 resize,#28 验收固定 TUI/回复/输入字节,#30 验收完整 IME/选择/粘贴。实际不同 DPI 由 #31 并行提供 | | 3. 提交并确认产品架构与支持配置 | 提供可比较的核心/后端/GUI 方案、证据和取舍,经用户确认后定案 | #34 使用 #23/#26、#28–#33 的真实结果与 #54 的受控成本,重审 ADR-0003/0004;明确 Windows 11 x64 优先、未来平台障碍、接口、并发/取消和维护成本;范围及重要差异依 ADR-0006 确认 | | 4. 验收最终开发包 | 用户从公开源码复建或解压下载包,在声明环境中完成固定工作流 | #35 在最终产物上复验本页全部门槛、配置升级/恢复、错误/回收及预先冻结的持续运行与资源合同;#36/#38 的可靠性出口必须闭合。来源、许可、说明与产物身份一致后,才结项 #7 | @@ -28,7 +28,7 @@ R2 的前期平台调查/候选比较、有限会话与恢复、首批流式字 ```mermaid flowchart LR baseline["55 联合基线"] --> backend["47 后端整合"] - fixes["51 / 52 / 63 后端缺口"] --> backend + fixes["52 GUI Vim"] --> backend baseline -. "最终修复复验" .-> fixes native["56 原生语义路径"] --> margin["41 margin 合同"] backend --> margin --> modes["27 终端模式"] --> tui["28 固定 TUI"] @@ -44,7 +44,7 @@ flowchart LR ## 下一批的任务边界 -1. **在已冻结基线上逐项闭合后端回归。** #55 已交付含红的真实基线;接续 #51/#52/#63 的独立失败。#62 的[真实 writer 持块、取消与停止夹具](../research/sender-fence.md)及 #53 原 UI/3MB/八会话 data-only 合同已限定验证并整合到实验父分支,期限与负载保持;同次真实 CF/IME 和完整官方矩阵仍未通过。未完成项可以用原冻结输入独立定位,最终修复验收须在联合配置重建;一次未复现不自动构成因果修复。 +1. **在已冻结基线上闭合剩余后端回归与联合验收。** #55 已交付含红的真实基线;#51 原生 viewport resize、#63 工作区原生 viewport,以及 #62 的[真实 writer 持块、取消与停止夹具](../research/sender-fence.md)和 #53 原 UI/3MB/八会话 data-only 合同均已在各自范围验证并整合到实验父分支。接续 #52 GUI Vim 与 #47 的完整官方矩阵、同次真实 CF/IME;限定切片关闭不等于联合验收通过。未完成项可以用原冻结输入独立定位,最终修复验收须在联合配置重建;期限、负载和原失败保留,一次未复现不自动构成因果修复。 2. **并行处理原版对照、平台语义与性能。** [原生语义处理路径 #56](https://github.com/JavaZeroo/termforge/issues/56)先明确 #41 原合同需要什么、哪些路径可支持;保留必要的上游或独立实现工作,不用“平台不同”豁免缺口。#3/#4 优先建立原版 Windows 本地终端基线;#54 现在冻结重复方法与完成端点并定位吞吐成本,数值预算作为提案交用户确认,不等到 #34 才补性能。 3. **在主线推进独立行为合同。** #29 核对极窄/极短、软硬行、历史/淘汰、选择/Find、连续 resize 和 child ack;#30 核对 IME 组合/提交/取消与输出/resize/切换、应用键盘/粘贴接收、实际复制及同次八会话。已验收的 #39 只提供首批字素基础,最终配置的交叉验收仍需执行。 4. **补终端模式和应用矩阵。** #27 明确 DECOM/DECAWM、pending-wrap、保存恢复、异常序列与 margin×resize;#28 先固定应用版本/入口/动作及协议字节预期,再逐应用完成进入、操作、resize、退出、主屏恢复。Vim 的一个修复不替代完整 TUI 验收。 diff --git a/experiments/core/CANDIDATE.md b/experiments/core/CANDIDATE.md index fbc226c8..1b367871 100644 --- a/experiments/core/CANDIDATE.md +++ b/experiments/core/CANDIDATE.md @@ -28,7 +28,15 @@ feed 的 `removed_rows` 只记录过程中的移除事件。它不是完整擦 固定反例已覆盖长历史整段/逐字节复制、连续空硬行、跨软行 `GHIJ` 查找、无写入可见后缀不重复及部分容量淘汰不复活 `ABCD`;淘汰前选择/坐标失效,剩余 `IJKL` 可以重新查找与选择。新背景和默认色粗体空格的对照通过;来源投影仅裁去完整默认 `Cell`,不再用 written 字段单独决定尾部来源。完整样式与宽格组合仍需验证,见[分次结果与真实失败](results/2026-10-28-source-ownership/README.md)。 -`--source-partial-visible` 仍只输出观察:`B` 排在 `ABCDEFGH` 前,其返回 0 不表示匹配通过。混合原生行的部分来源拆分、多旧行联合覆盖以及统一文档/视图顺序仍由 #29 处理;不能用有限缓存规则裁决未知原版行为。混宽快照也仍需投影和交互映射,完整矩阵的历史重绘与选择失败尚未解决。来源组合与分配成本未测。 +`--source-partial-visible` 仍只输出观察:`B` 排在 `ABCDEFGH` 前,其返回 0 不表示匹配通过。混合原生行的部分来源拆分、多旧行联合覆盖以及源文本顺序仍由 #29 处理;显示投影不能掩盖这些缺口,也不能用有限缓存规则裁决未知原版行为。来源组合与分配成本未测。 + +## 历史显示与源坐标 + +`HistoryViewport` 保留完整源行,只把混宽历史中的完整软链按当前列宽投影;硬边界和空行分别保留,旧自动折行 spacer 不占用新布局。等宽历史和活动网格维持物理布局,只为当前页复制完整 `Cell`。宽字不能拆成两个独立字符,单列显示保留完整文字并将显示端点映回原来的两格范围。`Snapshot` 在构造时拒绝非矩形页面。 + +滚动范围按显示行计数,阅读位置保存源行与源列。每页同时发布拥有值的 `ViewMapping`;显示行可能共享来源 ID,鼠标命中、选择/查找高亮和 Find reveal 均通过源坐标,不把显示行号或分片列当作文档坐标。映射检查当前源 ID/version,清空或淘汰后旧页不能重新命中失效来源。复制与搜索仍读取 `TextDocument`。 + +固定长行、跨折行正反向选择、硬换行、空行锚点、宽字/单列、Find、实际核心 resize 往返及 ED3 失效已有[分次证据](results/2026-10-28-history-viewport/README.md)。未执行独立的“只改视图宽度、不调用核心 resize”实验;当前宽度来自 `Screen`。完整历史/活动区交叉、原版 Unicode/交互对照及 #29 全合同仍未完成。每次操作会重建布局,高命中量 Find 会逐结果扫描页映射,两项成本尚未测量,不据正确性通过宣称性能达标。 ## 构建与验证 diff --git a/experiments/core/results/2026-10-28-history-viewport/README.md b/experiments/core/results/2026-10-28-history-viewport/README.md new file mode 100644 index 00000000..a9f495c2 --- /dev/null +++ b/experiments/core/results/2026-10-28-history-viewport/README.md @@ -0,0 +1,53 @@ +# 完整历史显示与源坐标检查(#29) + +混宽缓存历史曾把不同长度的行交给固定宽度呈现器;仅把页面裁成矩形会丢失源文字,并使选择和 Find 使用错误坐标。现在保留完整源文档,对混宽历史软链建立临时显示行和双向来源映射,鼠标、选区/查找高亮及阅读位置通过源坐标连接。硬边界、空行、宽字和活动网格分别遵守[候选合同](../../CANDIDATE.md#历史显示与源坐标)。 + +这是候选实验的局部修复,**#29 与 R2 仍未验收**。原版未知行为、重要差异、性能容差和最终选型仍按 ADR-0006 处理。 + +## 固定源码与结果 + +环境为 Windows 11 x64(10.0.26200)、Release、显式 Alacritty `termforge.10`、系统 ConPTY。不是默认核心或实验官方 ConPTY 后端的联合结果。 + +| 源码 | 检查与结果 | 证据边界 | +|---|---|---| +| `1fafb999d6e031ac7206e499062f7e1b2f1a53ff` | 完整历史投影、源映射与 Find;旧 history 和原 681 高度 selection 场景各运行一次,均退出 0、无超时,Job active=0、desktop 已回收 | 原场景/期限保持,selection 的 system_clipboard=false;两次单次成功不反填旧失败根因 | +| `f41f747be27ad7b1de965b2279948eca7473cc0e` | 补空历史行阅读锚点与 Snapshot 形状门禁;最终完整 CTest **57/57**,退出 0,308.33 秒 | 内部 **9 项跳过**:8 项实际系统剪贴板、1 项未启用的原版失败注入;套件耗时不是终端性能测量 | + +[validation.json](validation.json)记录源码/运行时哈希、分次断言、跳过项和本地日志索引。最终构建冻结的 208 项文件在终态后全部重哈希匹配。此前 Rust 205 项与 runner 6/6 属于[来源接入检查点](../2026-10-28-source-ownership/README.md),本轮没有重跑或修改 Rust,不拼成另一配置的整体验收。 + +## 反例与验证范围 + +| 问题 | 实际失败依据 | 修复后的检查 | +|---|---|---| +| 非矩形历史页面 | 公开快照行宽与屏幕列数不一致,运行退出 1 | 保留完整源行,输出固定宽度页;初次相关检查 6/6 | +| 选择来源映射 | 新接口不存在导致编译红;不是既有运行错误的反例 | 显示 EFGH 对应源列 4–8;跨片段正反向选择 CDEF 无额外换行,硬边界保留 GH/CRLF/B | +| 宽字旧自动折行空隙 | 修正判据后公开 `project_line` 运行红 | 跳过旧 leading spacer,保留宽字原子性;单列显示仍映回两格源范围 | +| Find 丢失源列 | 旧 line-ID reveal 显示长源行开头,运行红 | 按源行与列定位;实际核心 resize/repaint 4→8→4 保持阅读位置和选择 | +| 空历史行锚点 | 空硬行滚动时被跳到活动区,运行红 | 区分无候选与有效末行,空行页首及源行末端 reveal 保持 | +| 非矩形页进入呈现器 | 公共 Snapshot 构造接受畸形页,运行红 | 两个入口统一拒绝;受控 renderer 32 个 DPI/zoom/backend 场景通过 | + +旧宽字检查使用 ASCII helper,误把 wide trailer 当空格,失败受判据干扰;保留原日志但不作为因果依据。真正依据是 `wide-corrected-red` 与随后通过。部分中间 green 只有固定日志,没有逐次独立构建 manifest,不冒充最终 Git blob 的验证。 + +Standards:本轮 `8521cd7..f41f747` 最终新增硬问题 0、实质代码异味 0,保留布局重建与高命中量 Find 的未测成本。Spec:空锚点 P2 和形状缺口经实际红→绿及独立复核关闭,新增未解决问题 0。审查结果来自独立代理消息的根汇总;更早来源组合的维护建议及各次审查保留在原检查点。 + +## 仍未完成 + +- 最终源码的 `--source-partial-visible` 仍得到 `B\r\nABCDEFGH\r\n\r\nC\r\nD`,EFGH 只有一个命中,但 B 仍排在 A 前;该观察模式返回 0 不表示比较通过。显示投影没有修复源顺序。 +- 混合原生行的部分来源、多旧行合并及跨历史/活动区软链尚未完整覆盖;不得按文字去重或永久裁剪缓存掩盖。 +- 没有执行独立的“只改显示宽度、不调用核心 resize”实验;实际 resize 往返不能代替它。原版 Unicode、交互与普遍覆盖政策仍需取证,本轮没有新增原版运行。 +- 源映射、布局重建、逐字素页映射及高命中量 Find 的成本未测;没有原版性能等价、数值容差或最终核心选型结论。 +- 实际系统剪贴板、不同 DPI、完整 child/模式交叉和实验官方后端分别验收。受控 DIB/renderer 结果不等于实际呈现,`presented_done=null`。 + +旧 `eea3a2c` / `95cd2c3` 的 52/53、history retained/full 差异及 selection 的不同退出路径继续保留在[原证据](../2026-10-28-source-ownership/README.md)。新运行通过不证明这些历史事件只有同一个原因。 + +## 复现与公开边界 + +按[候选构建入口](../../CANDIDATE.md#构建与验证)生成 Windows Release candidate/system 配置,在测试进程 PATH 提供既有 Vim,设置 `PYTHONUTF8=1` 后运行: + +```powershell +ctest --test-dir build/candidate-adapter -C Release --output-on-failure +``` + +局部入口为 `candidate_history_viewport_projection`、`candidate_history_viewport_text`、`candidate_history_viewport_wide`、`candidate_history_viewport_find`;命令只说明如何复验,不表示在其他宿主已通过。未启用要求隔离 hosted VM 的实际系统剪贴板门槛。 + +公开内容只含独立代码、合成断言、汇总和哈希。本地 proof 路径是取证索引,不是公共附件;原始日志、GUI/frame/layout、进程/desktop 标识、私有路径及 WindTerm 发行物不随本记录发布。 diff --git a/experiments/core/results/2026-10-28-history-viewport/validation.json b/experiments/core/results/2026-10-28-history-viewport/validation.json new file mode 100644 index 00000000..81dacd0f --- /dev/null +++ b/experiments/core/results/2026-10-28-history-viewport/validation.json @@ -0,0 +1,1698 @@ +{ + "schema": "termforge-r29-viewport-validation-v1", + "status": "candidate_system_checks_passed_scope_incomplete", + "prepared_at_utc": "2026-10-27T20:52:49.334775+00:00", + "scope": "Candidate experiment only; projection/mapping/Find and two Spec P2 fixes. Not product architecture approval or complete #29 acceptance.", + "source_checkpoints": [ + { + "source_commit": "1fafb999d6e031ac7206e499062f7e1b2f1a53ff", + "role": "projection_mapping_find_and_two_one_shot_ui_checks", + "configuration": "Windows x64 Release; explicit candidate termforge.10; system ConPTY", + "host": "Windows-11-10.0.26200-SP0", + "manifest": { + "path": "local/r29-viewport-projection/shell-build.json", + "sha256": "e717e98f6548240935273d048be6fbe4532fbb4dc34441767f1babf9c16dacfa", + "bytes": 33891 + }, + "recorded_files": [ + { + "path": "CMakeLists.txt", + "sha256": "f41c9010d095050dd059139b09db7961eba69f35090549297366848a2a9c60be" + }, + { + "path": "build/candidate-adapter/bin/Release/candidate_core_test.exe", + "sha256": "e7c832f1de864906bd59c659847a40fe7f35e1ef64f503f2721b15d132d78efc" + }, + { + "path": "build/candidate-adapter/bin/Release/renderer_metrics_test.exe", + "sha256": "4e4e9f54ea27bea56961dd04461c8e84ae0765cb47d0093afe039c3434facee5" + }, + { + "path": "build/candidate-adapter/bin/Release/shell_test_launcher.exe", + "sha256": "3923d6cd21b6b5ceffe0d91d4167d67ee4b8c2b946194ffcb079bd9ac041bad3" + }, + { + "path": "build/candidate-adapter/bin/Release/termforge_shell.exe", + "sha256": "23107d83c139af6a46fff76f30a9d4196acecbe76f3468bd9438ce8b0c9a0264" + }, + { + "path": "experiments/core/candidate_terminal.cpp", + "sha256": "ac6945bca9d803f05cbf81193a63f9fa364775f240135d4453c6cb0c66cfb70c" + }, + { + "path": "experiments/core/candidate_terminal.hpp", + "sha256": "123fd595d6f993be29bac2c7af35c5b87c975af66275a0f05b94a4766a635a9b" + }, + { + "path": "experiments/core/candidate_terminal_test.cpp", + "sha256": "db2dd9f2241b473ba68a357f74990074e734c79f81b3badd580bbbb4c465402d" + }, + { + "path": "experiments/core/history_view.hpp", + "sha256": "f4d2132743109fea7b073ae79a615de3325aef5b020815fbb72831d44f6395ba" + }, + { + "path": "experiments/core/view_mapping.hpp", + "sha256": "fd9b4d8c8773436f5a598aea67a420497e5958ee323944c4e3c5b5817fd3ab95" + }, + { + "path": "experiments/presentation/render.hpp", + "sha256": "0dc017923629b0172056875944bdac28a76d94f3f84113291c4aaf46920402c4" + }, + { + "path": "experiments/presentation/renderer_metrics_test.cpp", + "sha256": "cfdf2bcc443b92b9cd0152fa4603695d5654b1a98488f21901baa24a51561514" + }, + { + "path": "experiments/shell/search_interactions.hpp", + "sha256": "13c5d9058d1dedbf40c2a8b186754c2db13827579d7046c956ebe135463333d1" + }, + { + "path": "experiments/shell/text_interactions.hpp", + "sha256": "780c560221e7a8505b388353eb5cb5f68ca5a3cb746d4d5cb1fa5f93472fe007" + } + ], + "hash_semantics": "Recorded filesystem SHA-256 from that build manifest; historical source/binary values were not compared with current files or Git blobs." + }, + { + "source_commit": "f41f747be27ad7b1de965b2279948eca7473cc0e", + "role": "spec_p2_empty_anchor_and_snapshot_shape_guard", + "configuration": "Windows x64 Release; explicit candidate termforge.10; system ConPTY", + "host": "Windows-11-10.0.26200-SP0", + "manifest": { + "path": "local/r29-viewport-projection/final-build.json", + "sha256": "dc380be718d13796e1e19cdfae4b1b0e1dbf852aed7bf81204e99eca46978d19", + "bytes": 33721 + }, + "recorded_files": [ + { + "path": "CMakeLists.txt", + "sha256": "f41c9010d095050dd059139b09db7961eba69f35090549297366848a2a9c60be" + }, + { + "path": "build/candidate-adapter/bin/Release/candidate_core_test.exe", + "sha256": "b22b3fedd78b002a320cdeb6b23a7727b87fac7bf105802013b3cfd161ff5c1a" + }, + { + "path": "build/candidate-adapter/bin/Release/renderer_metrics_test.exe", + "sha256": "244cb42f8db6d27d31f7612b563229cbbda97535769212e53233a86162f08025" + }, + { + "path": "build/candidate-adapter/bin/Release/shell_test_launcher.exe", + "sha256": "3923d6cd21b6b5ceffe0d91d4167d67ee4b8c2b946194ffcb079bd9ac041bad3" + }, + { + "path": "build/candidate-adapter/bin/Release/termforge_shell.exe", + "sha256": "d6642de28158e38f2a015e1cbf65179c043a3b4bc9ce3363070aa86c166bf78a" + }, + { + "path": "experiments/core/candidate_terminal.cpp", + "sha256": "ac6945bca9d803f05cbf81193a63f9fa364775f240135d4453c6cb0c66cfb70c" + }, + { + "path": "experiments/core/candidate_terminal.hpp", + "sha256": "123fd595d6f993be29bac2c7af35c5b87c975af66275a0f05b94a4766a635a9b" + }, + { + "path": "experiments/core/candidate_terminal_test.cpp", + "sha256": "c87803866a83b2b8b2e4f24859746f9cea9fe359170b766073e3f884a46805e6" + }, + { + "path": "experiments/core/history_view.hpp", + "sha256": "c4de51b54a1134a155bd5bf8ede88b668de4c89c01cd71787ecad1ce8e454971" + }, + { + "path": "experiments/core/view_mapping.hpp", + "sha256": "fd9b4d8c8773436f5a598aea67a420497e5958ee323944c4e3c5b5817fd3ab95" + }, + { + "path": "experiments/presentation/render.hpp", + "sha256": "cebb6588e4b7ec63c761bd8615e8c14582284040d12370755e06a6a582312e66" + }, + { + "path": "experiments/presentation/renderer_metrics_test.cpp", + "sha256": "d6413f633210d824054f5f38c8e7d34843b6764bdab0ac862e6f9c66977758a4" + }, + { + "path": "experiments/shell/search_interactions.hpp", + "sha256": "13c5d9058d1dedbf40c2a8b186754c2db13827579d7046c956ebe135463333d1" + }, + { + "path": "experiments/shell/text_interactions.hpp", + "sha256": "780c560221e7a8505b388353eb5cb5f68ca5a3cb746d4d5cb1fa5f93472fe007" + } + ], + "hash_semantics": "Recorded filesystem SHA-256; all 208 final-manifest files rechecked after the terminal CTest result. This does not retroactively identify intermediate green builds." + } + ], + "cases": [ + { + "id": "rectangle", + "red_kind": "runtime", + "red_exit_code": 1, + "red_message": "history viewport supplied a nonrectangular renderer page", + "red_proofs": [ + "red-validation.json", + "red.stderr" + ], + "green_proofs": [ + "rectangle-green.json", + "rectangle-core.log" + ], + "green_result": "6/6 core checks passed in this bounded subset; no shell or mapping acceptance at this stage" + }, + { + "id": "mapping-selection", + "red_kind": "compile_interface_missing", + "red_exit_code": null, + "red_message": "ViewSnapshot.mapping and ViewAffinity were missing; this is not a runtime behavior counterexample", + "red_proofs": [ + "build-selection-interface-red.log" + ], + "green_proofs": [ + "selection-core.log" + ], + "green_result": "2/2 projection and text checks passed; later extended-core log covers cross-fragment forward/reverse selection and hard breaks" + }, + { + "id": "wide-wrap-gap", + "red_kind": "corrected_runtime", + "red_exit_code": 1, + "red_message": "history viewport retained an obsolete wide-wrap gap in its display order", + "red_proofs": [ + "wide-corrected-red.json", + "wide-corrected-red.log" + ], + "green_proofs": [ + "wide-corrected-green.log" + ], + "green_result": "3/3 viewport subset passed, including full glyph placement and single-column source span selection", + "excluded_inference": "Earlier wide-red.log used an ASCII cell-concatenation helper that counted a wide trailer; that failure is confounded and is not the behavioral proof." + }, + { + "id": "find-reveal", + "red_kind": "runtime_old_line_id_only_behavior", + "red_exit_code": 1, + "red_message": "Find reveal lost the source column and displayed the start of the old long row", + "red_proofs": [ + "find-red.json", + "find-red.log" + ], + "green_proofs": [ + "find-green.log", + "extended-core.log" + ], + "green_result": "4/4 viewport subset passed, including actual CandidateTerminal resize/repaint 4→8→4, source selection preservation and stale-map rejection after ED3", + "pure_view_width_probe": "not executed; actual core resize/repaint roundtrip is the tested route" + }, + { + "id": "empty-anchor-spec-p2", + "red_kind": "runtime", + "red_exit_code": 1, + "red_message": "scrolling to an empty cached hard line skipped to the active grid", + "red_proofs": [ + "empty-red.json", + "empty-red.log" + ], + "green_proofs": [ + "empty-green.log" + ], + "green_result": "4/4 viewport subset passed after the empty-anchor fix" + }, + { + "id": "snapshot-shape-spec-p2", + "red_kind": "runtime_public_presentation_boundary", + "red_exit_code": 1, + "red_message": "nonrectangular page reached the fixed-width renderer snapshot", + "red_proofs": [ + "shape-red.json", + "shape-red.log" + ], + "green_proofs": [ + "shape-green.log" + ], + "green_result": "renderer metrics reports 32 DPI/zoom/backend scenarios passed; this does not validate real cross-monitor DPI" + } + ], + "ui_one_shot_checks": [ + { + "case": "history", + "source_commit": "1fafb999d6e031ac7206e499062f7e1b2f1a53ff", + "attempts": 1, + "timeout_seconds": 40, + "status": "passed", + "exit_code": 0, + "timed_out": false, + "job_active_after": 0, + "job_active_after_cleanup": 0, + "desktop_gone": true, + "result": { + "valid": true, + "mode": "ui-history", + "sessions_closed": 3, + "history_verified": true, + "active_children_after": 0, + "presented_done": null + }, + "sample": { + "path": "local/r29-viewport-projection/shell-history-once/sample.json", + "sha256": "4107eccb7b9070a1c02d91d492f89f2116ec5a9543f45ef98825135ec166acb6", + "bytes": 818 + }, + "attempt_record": { + "path": "local/r29-viewport-projection/shell-history-attempt.json", + "sha256": "706998091ce0fa4f3d123a876b49983750328cdc79807da7c12572d9102b4476", + "bytes": 181 + }, + "build_manifest_sha256_recorded": "e717e98f6548240935273d048be6fbe4532fbb4dc34441767f1babf9c16dacfa", + "build_manifest_sha256_matches": true, + "meaning": "A one-shot rerun of the previous UI case passed on this source/configuration; earlier failures remain preserved and are not retroactively erased." + }, + { + "case": "selection681", + "source_commit": "1fafb999d6e031ac7206e499062f7e1b2f1a53ff", + "attempts": 1, + "timeout_seconds": 40, + "status": "passed", + "exit_code": 0, + "timed_out": false, + "job_active_after": 0, + "job_active_after_cleanup": 0, + "desktop_gone": true, + "result": { + "valid": true, + "mode": "ui-selection", + "sessions_closed": 2, + "active_children_after": 0, + "presented_done": null + }, + "sample": { + "path": "local/r29-viewport-projection/shell-selection681-once/sample.json", + "sha256": "50eb73cd4c69800fc0da8c76f2c109683b14d0102f532debdf98959faa2c74aa", + "bytes": 2840 + }, + "attempt_record": { + "path": "local/r29-viewport-projection/shell-selection681-attempt.json", + "sha256": "f5e086c792e2cb4a4c1db8186ab84974efb84a282bc8684661b532e9126ffa85", + "bytes": 186 + }, + "build_manifest_sha256_recorded": "e717e98f6548240935273d048be6fbe4532fbb4dc34441767f1babf9c16dacfa", + "build_manifest_sha256_matches": true, + "meaning": "A one-shot rerun of the previous UI case passed on this source/configuration; earlier failures remain preserved and are not retroactively erased.", + "selection": { + "system_clipboard": false, + "phases": 12, + "sessions_closed": 2, + "cleanup_verified": true + } + } + ], + "final_ctest": { + "source_commit": "f41f747be27ad7b1de965b2279948eca7473cc0e", + "configuration": "Windows x64 Release; explicit candidate termforge.10; system ConPTY", + "host": "Windows-11-10.0.26200-SP0", + "status": "passed", + "terminal_exit_code": 0, + "passed": 57, + "failed": 0, + "skipped": 0, + "total": 57, + "elapsed_seconds": 308.33, + "command": "ctest --test-dir build/candidate-adapter -C Release --output-on-failure", + "environment": { + "PYTHONUTF8": "1", + "vim": "existing Git Vim added to this process PATH" + }, + "internal_skipped": [ + { + "test": "test_actual_system_copy_paste_and_shortcuts_twice", + "reason": "actual system Copy/Paste requires a disposable hosted VM" + }, + { + "test": "test_suspended_target_is_reclaimed_if_job_assignment_fails", + "reason": "opt-in pinned reference failure injection" + }, + { + "test": "test_mouse_selection_to_real_system_clipboard", + "reason": "system copy UI gate requires a disposable hosted VM" + }, + { + "test": "test_system_clipboard_product_read_errors_cancel_and_close", + "reason": "product clipboard read gate requires a disposable hosted VM" + }, + { + "test": "test_system_clipboard_shortcut_to_window_confirmation_and_receiver", + "reason": "real system clipboard UI gate requires a disposable hosted VM" + }, + { + "test": "test_owned_clipboard_contention_preserves_write_and_read_failures", + "reason": "system CF requires a disposable hosted VM" + }, + { + "test": "test_real_system_copy_shortcuts_for_styled_fixture_twice", + "reason": "system CF requires a disposable hosted VM" + }, + { + "test": "test_real_system_copy_and_edit_context_twice", + "reason": "real CF/EDIT Copy requires a disposable hosted VM" + }, + { + "test": "test_real_system_clipboard", + "reason": "Private window station OpenClipboard denied (5); system API gate remains unverified." + } + ], + "internal_skip_count": 9, + "internal_skip_summary": "8 actual system clipboard cases; 1 opt-in pinned-reference failure injection", + "log": { + "path": "local/r29-viewport-projection/ctest-final.log", + "sha256": "205b4561d52a9bc1a20a0d022e99a012683db048c1bbafec433d1877f4d01800", + "bytes": 6770 + }, + "detailed_log": { + "path": "local/r29-viewport-projection/last-test-final.log", + "sha256": "165bad934f23f308c3f31ad4778bbf920ee9108a908af0d7148bc5b6547b9e6c", + "bytes": 137003 + }, + "note": "One final complete candidate/system run. CTest case success does not turn internal skips into acceptance. Elapsed time is suite wall time, not a terminal performance measurement." + }, + "unmeasured_cost_risks": [ + { + "id": "layout-scan", + "risk": "reconcile/scroll/reveal/snapshot rebuild display-row metadata by scanning retained history and mixed-width chains; large-history CPU, allocation and interactive latency costs are unmeasured" + }, + { + "id": "page-map-validation", + "risk": "materializing page Cells and per-glyph source maps plus hit/project source-version validation scans incur allocation and repeated lookup costs; per-frame, selection and Find overhead is unmeasured" + } + ], + "limitations": [ + "Pure view-width-only probe was not executed; the tested anchor roundtrip uses actual CandidateTerminal.resize plus repaint.", + "The independent partial-visible B-before-A source-order counterexample remains unresolved; projection must not conceal it.", + "Soft chains crossing the history/active-grid boundary are not accepted by this slice.", + "Original WindTerm Unicode behavior and overwrite policy are not validated by these candidate tests.", + "The one-shot selection case has system_clipboard=false; no system CF acceptance is claimed.", + "Controlled renderer scenarios do not validate actual multi-monitor DPI.", + "No performance equivalence, numeric tolerance, complete baseline, daily-use release or final core selection is established.", + "The two earlier UI failures are retained separately; new one-shot successes do not rewrite their evidence." + ], + "historical_identity_records": [ + { + "record": "red-validation.json", + "recorded_head": "8521cd7f497f52ab169d76a5a99a976f6adfc8df", + "stage": "runtime rectangle red", + "recorded_source_and_binary_sha256": { + "experiments/core/history_view.hpp": "bc6218d05cc331ad3f39db545575faf0d100713dc8cb8ad09dbf3ee635c4696d", + "experiments/core/candidate_terminal.cpp": "ac6945bca9d803f05cbf81193a63f9fa364775f240135d4453c6cb0c66cfb70c", + "experiments/core/candidate_terminal.hpp": "123fd595d6f993be29bac2c7af35c5b87c975af66275a0f05b94a4766a635a9b", + "experiments/core/candidate_terminal_test.cpp": "4cdef7fe1267bf159322a51f46cf5f153bf0e11e368ccfa4c4b858b639974d72", + "build/candidate-adapter/bin/Release/candidate_core_test.exe": "df443c3d21a61940577c53aa8e21b7dcf4167cafa85c6ad5427a16d189aae931" + }, + "verification": "historical recorded values only; no Git blob equivalence claim" + }, + { + "record": "rectangle-green.json", + "recorded_head": "8521cd7f497f52ab169d76a5a99a976f6adfc8df", + "stage": "6/6 core checks passed; shell and source mapping not yet validated", + "recorded_source_and_binary_sha256": { + "CMakeLists.txt": "62fdcab4c93be78803b47349f0f05e2456533862f73848b09928790263fb53c6", + "experiments/core/history_view.hpp": "a20d88765473059f5ceecd9745f8ea35d5372f0ef24b9b20261fd3150dd10012", + "experiments/core/candidate_terminal_test.cpp": "4cdef7fe1267bf159322a51f46cf5f153bf0e11e368ccfa4c4b858b639974d72", + "experiments/presentation/render.hpp": "47eff67ee721a820b79641bb306a540c02d3fcad88cf1363c99b03b22fdbbed6", + "build/candidate-adapter/bin/Release/candidate_core_test.exe": "5946d756763bf914ab73d24c6a27ed39d5e1fafaf0770cdcedf4f065d1368a63", + "build/candidate-adapter/bin/Release/history_test.exe": "092b448c1f5851fd7d7ab8d40f77ac85f3a02ea1e4dd7688905a33b66f39b35c" + }, + "verification": "historical recorded values only; no Git blob equivalence claim" + }, + { + "record": "wide-corrected-red.json", + "recorded_head": null, + "stage": "corrected text oracle runtime red; source fixture guard passed", + "recorded_source_and_binary_sha256": { + "experiments/core/history_view.hpp": "40c379daa1da92613b94fca6c84521681b1a4d32752d6985929eb51166323c9f", + "experiments/core/view_mapping.hpp": "fd9b4d8c8773436f5a598aea67a420497e5958ee323944c4e3c5b5817fd3ab95", + "experiments/core/candidate_terminal_test.cpp": "2c2d1979e52976dba2652b2cbd9af60e7930b6aa57da9fd565f0dd63c1c8e746", + "build/candidate-adapter/bin/Release/candidate_core_test.exe": "5f3b4bc09b426034fe530a518873754c18f21513743b8071d4b491365180a978" + }, + "verification": "historical recorded values only; no Git blob equivalence claim" + }, + { + "record": "find-red.json", + "recorded_head": null, + "stage": "runtime red with the previous line-id-only reveal behavior", + "recorded_source_and_binary_sha256": { + "experiments/core/history_view.hpp": "ab13baeb169043adf80e0e15075dc9c0a543f043b657f4cc30e396b20f02e502", + "experiments/core/view_mapping.hpp": "fd9b4d8c8773436f5a598aea67a420497e5958ee323944c4e3c5b5817fd3ab95", + "experiments/core/candidate_terminal_test.cpp": "7e279f06d15ef18b45d19fe221ee4c7d219e625ad61aba288c746d96ffe2c035", + "experiments/shell/search_interactions.hpp": "13c5d9058d1dedbf40c2a8b186754c2db13827579d7046c956ebe135463333d1", + "build/candidate-adapter/bin/Release/candidate_core_test.exe": "bd1cb86c1cfe5345a2ed11035f183b1158f10b411950d3b115b7b39ca9afb9c4" + }, + "verification": "historical recorded values only; no Git blob equivalence claim" + }, + { + "record": "empty-red.json", + "recorded_head": null, + "stage": "runtime red reproducing Spec review empty-history-anchor finding", + "recorded_source_and_binary_sha256": { + "experiments/core/history_view.hpp": "f4d2132743109fea7b073ae79a615de3325aef5b020815fbb72831d44f6395ba", + "experiments/core/candidate_terminal_test.cpp": "c87803866a83b2b8b2e4f24859746f9cea9fe359170b766073e3f884a46805e6", + "build/candidate-adapter/bin/Release/candidate_core_test.exe": "58758fa1b3e96bf19e38223df1b2e0f82921eb78dcfdea8d04f9a90fd5569062" + }, + "verification": "historical recorded values only; no Git blob equivalence claim" + }, + { + "record": "shape-red.json", + "recorded_head": null, + "stage": "runtime red at public presentation snapshot boundary", + "recorded_source_and_binary_sha256": { + "experiments/core/history_view.hpp": "91933722d48881cecfc7f95a2e020f731039d803c5fbc3b7fd89e28704d3f44c", + "experiments/presentation/render.hpp": "0dc017923629b0172056875944bdac28a76d94f3f84113291c4aaf46920402c4", + "experiments/presentation/renderer_metrics_test.cpp": "d6413f633210d824054f5f38c8e7d34843b6764bdab0ac862e6f9c66977758a4", + "build/candidate-adapter/bin/Release/renderer_metrics_test.exe": "b4abe2a52ceda1a261b5f15607c40241d53602ca259c0994e2841161ead964d2" + }, + "verification": "historical recorded values only; no Git blob equivalence claim" + } + ], + "proofs": [ + { + "path": "local/r29-viewport-projection/red-validation.json", + "sha256": "f0121aec240b6d7b58e121f853064353d748b8c99865e8f98e1a70b4484b7c7a", + "bytes": 1410 + }, + { + "path": "local/r29-viewport-projection/rectangle-green.json", + "sha256": "6e60c9546c62ce1739b25b38d765dde16c448f448a63843a37f360208035122c", + "bytes": 1199 + }, + { + "path": "local/r29-viewport-projection/wide-corrected-red.json", + "sha256": "155140efdde5b9c2e22bb33f39eebef027c9c9466835e1b82691064a81e243e3", + "bytes": 1131 + }, + { + "path": "local/r29-viewport-projection/find-red.json", + "sha256": "beae2320ee618d42cca06a74310e826a8b4c6e9cc43af226c9216124a4acea6a", + "bytes": 1021 + }, + { + "path": "local/r29-viewport-projection/empty-red.json", + "sha256": "5f77c4d8a47028699afb2a50a3e80101642220cc0404b275f7c0b3945cea9f86", + "bytes": 807 + }, + { + "path": "local/r29-viewport-projection/shape-red.json", + "sha256": "0a4f1d652fff57e94d62fcdd8cdc5723e63f7e7aaa2b98b874805510e8b441d4", + "bytes": 887 + }, + { + "path": "local/r29-viewport-projection/red.stdout", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "bytes": 0 + }, + { + "path": "local/r29-viewport-projection/red.stderr", + "sha256": "4a3fe3d156617929a0d2e127fb21a2a9b3f2577ce053fbf2a52137515c860b3e", + "bytes": 58 + }, + { + "path": "local/r29-viewport-projection/rectangle-core.log", + "sha256": "9a46d7bf5c8b2564cd8b989e6ba036879c7234fbd9025ba6d6e4860cb47d3a43", + "bytes": 972 + }, + { + "path": "local/r29-viewport-projection/rectangle-green.patch", + "sha256": "9d7ebd045f85f92926c7b5cc61f1379fac93d6191dcaafc1747cd73ee491615b", + "bytes": 17662 + }, + { + "path": "local/r29-viewport-projection/snapshot-complete-source-test.patch", + "sha256": "f712cba2692f5c0adaf93f40c098c303796e01c5e01f1c3e2cfe36fef12f489e", + "bytes": 2845 + }, + { + "path": "local/r29-viewport-projection/build-selection-interface-red.log", + "sha256": "fce5f43292e52a92735727fdd0149a71e8d44e5670b0f70ba1bb7b010184d1d9", + "bytes": 9594 + }, + { + "path": "local/r29-viewport-projection/selection-core.log", + "sha256": "3125929b1b27a27f2e492b8e460209eb7be603cec8af075b46f4f09da76506d4", + "bytes": 506 + }, + { + "path": "local/r29-viewport-projection/wide-red.json", + "sha256": "67ebc6131250f3f40513f25af1668a633dcef23f63d33cc7f67d65d78e7ecebd", + "bytes": 885 + }, + { + "path": "local/r29-viewport-projection/wide-red.log", + "sha256": "7f5fa48246a317f841a9ac8d02f02743bd527c8c81226d65d0be4b1f2783856e", + "bytes": 74 + }, + { + "path": "local/r29-viewport-projection/wide-inspect.log", + "sha256": "18fa09dec9e54d02954d666684eebf80454b385c3142735f1d0bd98a03f370d3", + "bytes": 477 + }, + { + "path": "local/r29-viewport-projection/wide-corrected-red.log", + "sha256": "7f5fa48246a317f841a9ac8d02f02743bd527c8c81226d65d0be4b1f2783856e", + "bytes": 74 + }, + { + "path": "local/r29-viewport-projection/wide-corrected-green.log", + "sha256": "91a09efbec6b079117078af28b0e8ed1e88d2da16d4304a53773ddc22cef62d4", + "bytes": 631 + }, + { + "path": "local/r29-viewport-projection/find-red.log", + "sha256": "42e470082dd13dc663a802a3841694a6e4b57094c83b4d0a4269d3ed910a46f2", + "bytes": 80 + }, + { + "path": "local/r29-viewport-projection/find-green.log", + "sha256": "0df8259e6ebf65fcf669f061e45a9387243af4a8519ff5309cf90528636925c2", + "bytes": 756 + }, + { + "path": "local/r29-viewport-projection/extended-core.log", + "sha256": "b90a53a8aca8eb69674730bb7c4d6d5008002e8e34cf985292979aaae7ff0e95", + "bytes": 756 + }, + { + "path": "local/r29-viewport-projection/empty-red.log", + "sha256": "ce5193319d475c822aadaa5e59c684ea8613a62a65fca88ac222636298910ba8", + "bytes": 67 + }, + { + "path": "local/r29-viewport-projection/empty-green.log", + "sha256": "3edb655063c22fd3cf00d278acc34fffe4059543c1f3551e31340aed7bcd4080", + "bytes": 756 + }, + { + "path": "local/r29-viewport-projection/shape-red.log", + "sha256": "947f24fe50a58f76075e1e9606f6b55f3ae7c3121fa030815929dede84fda08e", + "bytes": 63 + }, + { + "path": "local/r29-viewport-projection/shape-green.log", + "sha256": "078feeeea87356256e0eb5b5499815c1fecffe14f3db329f9267313a1729e8a9", + "bytes": 56 + }, + { + "path": "local/r29-viewport-projection/shell-build.json", + "sha256": "e717e98f6548240935273d048be6fbe4532fbb4dc34441767f1babf9c16dacfa", + "bytes": 33891 + }, + { + "path": "local/r29-viewport-projection/final-build.json", + "sha256": "dc380be718d13796e1e19cdfae4b1b0e1dbf852aed7bf81204e99eca46978d19", + "bytes": 33721 + }, + { + "path": "local/r29-viewport-projection/build-integrated.log", + "sha256": "effc8f74c59bcfa06d7e0fdf212e14104a14564dd2319f66a0a1b47bbc5a8fe7", + "bytes": 5987 + }, + { + "path": "local/r29-viewport-projection/build-final.log", + "sha256": "b2442ceb08728c5d6aa8fe4f5e5086e83f812099046129422c7e88d0d81e1df6", + "bytes": 6018 + }, + { + "path": "local/r29-viewport-projection/shell-history-attempt.json", + "sha256": "706998091ce0fa4f3d123a876b49983750328cdc79807da7c12572d9102b4476", + "bytes": 181 + }, + { + "path": "local/r29-viewport-projection/shell-selection681-attempt.json", + "sha256": "f5e086c792e2cb4a4c1db8186ab84974efb84a282bc8684661b532e9126ffa85", + "bytes": 186 + }, + { + "path": "local/r29-viewport-projection/shell-history-once/sample.json", + "sha256": "4107eccb7b9070a1c02d91d492f89f2116ec5a9543f45ef98825135ec166acb6", + "bytes": 818 + }, + { + "path": "local/r29-viewport-projection/shell-selection681-once/sample.json", + "sha256": "50eb73cd4c69800fc0da8c76f2c109683b14d0102f532debdf98959faa2c74aa", + "bytes": 2840 + }, + { + "path": "local/r29-viewport-projection/build-red.log", + "sha256": "3dc6a5da989532287d8bc4d3d45ef5f5ad084b953cfbb949a95223daeec20467", + "bytes": 240 + }, + { + "path": "local/r29-viewport-projection/build-rectangle.log", + "sha256": "838c75dffaa41863556e135ad7122619e0b9bca2c7417a60d0f865a0f97d8fc7", + "bytes": 988 + }, + { + "path": "local/r29-viewport-projection/build-wide-corrected-red.log", + "sha256": "3dc6a5da989532287d8bc4d3d45ef5f5ad084b953cfbb949a95223daeec20467", + "bytes": 240 + }, + { + "path": "local/r29-viewport-projection/build-find-red.log", + "sha256": "3dc6a5da989532287d8bc4d3d45ef5f5ad084b953cfbb949a95223daeec20467", + "bytes": 240 + }, + { + "path": "local/r29-viewport-projection/build-empty-red.log", + "sha256": "3dc6a5da989532287d8bc4d3d45ef5f5ad084b953cfbb949a95223daeec20467", + "bytes": 240 + }, + { + "path": "local/r29-viewport-projection/build-shape-red.log", + "sha256": "abe3a52ca6e7cf6369688d29c9c14cce9b662e76e25d99faaaf27fd12c85897b", + "bytes": 242 + }, + { + "path": "local/r29-viewport-projection/run-shell-once.py", + "sha256": "d98e164a4f19c90a7e155e9f2b28cb060339361f8f91065b9ae1c75496d0b013", + "bytes": 3122 + }, + { + "path": "local/r29-viewport-projection/ctest-final.log", + "sha256": "205b4561d52a9bc1a20a0d022e99a012683db048c1bbafec433d1877f4d01800", + "bytes": 6770 + }, + { + "path": "local/r29-viewport-projection/last-test-final.log", + "sha256": "165bad934f23f308c3f31ad4778bbf920ee9108a908af0d7148bc5b6547b9e6c", + "bytes": 137003 + }, + { + "path": "local/r29-viewport-projection/partial-visible-final.log", + "sha256": "8c85c093c56979ccf5ec1076b31a319119bdb9c860bbed2301e6984c821fc16d", + "bytes": 223 + } + ], + "existing_record_proof_hash_checks": [ + { + "record": "red-validation.json", + "path": "local/r29-viewport-projection/snapshot-complete-source-test.patch", + "expected_sha256": "f712cba2692f5c0adaf93f40c098c303796e01c5e01f1c3e2cfe36fef12f489e", + "actual_sha256": "f712cba2692f5c0adaf93f40c098c303796e01c5e01f1c3e2cfe36fef12f489e", + "matches": true + }, + { + "record": "red-validation.json", + "path": "local/r29-viewport-projection/build-red.log", + "expected_sha256": "3dc6a5da989532287d8bc4d3d45ef5f5ad084b953cfbb949a95223daeec20467", + "actual_sha256": "3dc6a5da989532287d8bc4d3d45ef5f5ad084b953cfbb949a95223daeec20467", + "matches": true + }, + { + "record": "rectangle-green.json", + "path": "local/r29-viewport-projection/build-rectangle.log", + "expected_sha256": "838c75dffaa41863556e135ad7122619e0b9bca2c7417a60d0f865a0f97d8fc7", + "actual_sha256": "838c75dffaa41863556e135ad7122619e0b9bca2c7417a60d0f865a0f97d8fc7", + "matches": true + }, + { + "record": "rectangle-green.json", + "path": "local/r29-viewport-projection/rectangle-core.log", + "expected_sha256": "9a46d7bf5c8b2564cd8b989e6ba036879c7234fbd9025ba6d6e4860cb47d3a43", + "actual_sha256": "9a46d7bf5c8b2564cd8b989e6ba036879c7234fbd9025ba6d6e4860cb47d3a43", + "matches": true + }, + { + "record": "rectangle-green.json", + "path": "local/r29-viewport-projection/rectangle-green.patch", + "expected_sha256": "9d7ebd045f85f92926c7b5cc61f1379fac93d6191dcaafc1747cd73ee491615b", + "actual_sha256": "9d7ebd045f85f92926c7b5cc61f1379fac93d6191dcaafc1747cd73ee491615b", + "matches": true + }, + { + "record": "wide-corrected-red.json", + "path": "local/r29-viewport-projection/build-wide-corrected-red.log", + "expected_sha256": "3dc6a5da989532287d8bc4d3d45ef5f5ad084b953cfbb949a95223daeec20467", + "actual_sha256": "3dc6a5da989532287d8bc4d3d45ef5f5ad084b953cfbb949a95223daeec20467", + "matches": true + }, + { + "record": "wide-corrected-red.json", + "path": "local/r29-viewport-projection/wide-corrected-red.log", + "expected_sha256": "7f5fa48246a317f841a9ac8d02f02743bd527c8c81226d65d0be4b1f2783856e", + "actual_sha256": "7f5fa48246a317f841a9ac8d02f02743bd527c8c81226d65d0be4b1f2783856e", + "matches": true + }, + { + "record": "find-red.json", + "path": "local/r29-viewport-projection/build-find-red.log", + "expected_sha256": "3dc6a5da989532287d8bc4d3d45ef5f5ad084b953cfbb949a95223daeec20467", + "actual_sha256": "3dc6a5da989532287d8bc4d3d45ef5f5ad084b953cfbb949a95223daeec20467", + "matches": true + }, + { + "record": "find-red.json", + "path": "local/r29-viewport-projection/find-red.log", + "expected_sha256": "42e470082dd13dc663a802a3841694a6e4b57094c83b4d0a4269d3ed910a46f2", + "actual_sha256": "42e470082dd13dc663a802a3841694a6e4b57094c83b4d0a4269d3ed910a46f2", + "matches": true + }, + { + "record": "empty-red.json", + "path": "local/r29-viewport-projection/build-empty-red.log", + "expected_sha256": "3dc6a5da989532287d8bc4d3d45ef5f5ad084b953cfbb949a95223daeec20467", + "actual_sha256": "3dc6a5da989532287d8bc4d3d45ef5f5ad084b953cfbb949a95223daeec20467", + "matches": true + }, + { + "record": "empty-red.json", + "path": "local/r29-viewport-projection/empty-red.log", + "expected_sha256": "ce5193319d475c822aadaa5e59c684ea8613a62a65fca88ac222636298910ba8", + "actual_sha256": "ce5193319d475c822aadaa5e59c684ea8613a62a65fca88ac222636298910ba8", + "matches": true + }, + { + "record": "shape-red.json", + "path": "local/r29-viewport-projection/build-shape-red.log", + "expected_sha256": "abe3a52ca6e7cf6369688d29c9c14cce9b662e76e25d99faaaf27fd12c85897b", + "actual_sha256": "abe3a52ca6e7cf6369688d29c9c14cce9b662e76e25d99faaaf27fd12c85897b", + "matches": true + }, + { + "record": "shape-red.json", + "path": "local/r29-viewport-projection/shape-red.log", + "expected_sha256": "947f24fe50a58f76075e1e9606f6b55f3ae7c3121fa030815929dede84fda08e", + "actual_sha256": "947f24fe50a58f76075e1e9606f6b55f3ae7c3121fa030815929dede84fda08e", + "matches": true + }, + { + "record": "shell-build.json", + "path": "local/r29-viewport-projection/build-integrated.log", + "expected_sha256": "effc8f74c59bcfa06d7e0fdf212e14104a14564dd2319f66a0a1b47bbc5a8fe7", + "actual_sha256": "effc8f74c59bcfa06d7e0fdf212e14104a14564dd2319f66a0a1b47bbc5a8fe7", + "matches": true + }, + { + "record": "shell-build.json", + "path": "local/r29-viewport-projection/run-shell-once.py", + "expected_sha256": "d98e164a4f19c90a7e155e9f2b28cb060339361f8f91065b9ae1c75496d0b013", + "actual_sha256": "d98e164a4f19c90a7e155e9f2b28cb060339361f8f91065b9ae1c75496d0b013", + "matches": true + }, + { + "record": "final-build.json", + "path": "local/r29-viewport-projection/build-final.log", + "expected_sha256": "b2442ceb08728c5d6aa8fe4f5e5086e83f812099046129422c7e88d0d81e1df6", + "actual_sha256": "b2442ceb08728c5d6aa8fe4f5e5086e83f812099046129422c7e88d0d81e1df6", + "matches": true + } + ], + "privacy": { + "included": "whitelisted synthetic assertions, relative repository proof references, hashes, source/configuration identity and aggregate exit/cleanup fields", + "excluded": "PID values, desktop names, absolute private paths, raw GUI/frame/layout data, full stdout/stderr from UI samples, proprietary/original WindTerm materials" + }, + "final_build_manifest": { + "source_commit": "f41f747be27ad7b1de965b2279948eca7473cc0e", + "configuration": "Windows x64 Release; explicit candidate termforge.10; system ConPTY", + "host": "Windows-11-10.0.26200-SP0", + "files": [ + { + "path": "CMakeLists.txt", + "sha256": "f41c9010d095050dd059139b09db7961eba69f35090549297366848a2a9c60be" + }, + { + "path": "build/candidate-adapter/bin/Release/alpha_normal_smoke.exe", + "sha256": "f14bf500afc054575c72a80cf22167bd64a0100ffdd43997cce8f56cbfd70812" + }, + { + "path": "build/candidate-adapter/bin/Release/assets/fonts/RobotoMono-Regular.ttf", + "sha256": "af0bff7599c3df3831755c16e39b3c496df74b8c8d8a1161b14dc8461be17cb4" + }, + { + "path": "build/candidate-adapter/bin/Release/candidate_core_test.exe", + "sha256": "b22b3fedd78b002a320cdeb6b23a7727b87fac7bf105802013b3cfd161ff5c1a" + }, + { + "path": "build/candidate-adapter/bin/Release/candidate_resize_map_test.exe", + "sha256": "d1744502b116d812bef62d0da345a8467c8fc44e19bc3e0a6af57ea564ee6827" + }, + { + "path": "build/candidate-adapter/bin/Release/candidate_title_test.exe", + "sha256": "e584187c5270b1a63c733e68ec8be51d2fca157816723a0848eb6f7d5052e4c1" + }, + { + "path": "build/candidate-adapter/bin/Release/clipboard_policy_test.exe", + "sha256": "36b98b15162aa95c3fb5ba634a3cc831022303d5966d8c43d71c2e36c8ac4e2d" + }, + { + "path": "build/candidate-adapter/bin/Release/conpty_ctrl_c_probe.exe", + "sha256": "2883537e3a90c27308a00c6a203161b472b231b456733b621fe8973c186c61d8" + }, + { + "path": "build/candidate-adapter/bin/Release/conpty_margin_probe.exe", + "sha256": "da640b5f4150fde8f6d1c4c838ba52c1318dfe834b85cc47e564096706156a34" + }, + { + "path": "build/candidate-adapter/bin/Release/core_measure.exe", + "sha256": "4fd604217e1874e7f3cb4d62ef77e85fe32ad73577383ed93bc97a58e55f75f7" + }, + { + "path": "build/candidate-adapter/bin/Release/find_bar_test.exe", + "sha256": "b1fa4befd1e70c1dd9d12a9cc413a9c0a6fe902f9c19fab78ef1f34e4b254f01" + }, + { + "path": "build/candidate-adapter/bin/Release/history_test.exe", + "sha256": "3ab3283aa2415d58fb194dd840c1851a262cca61d14e5cb6bbee90759c801ce5" + }, + { + "path": "build/candidate-adapter/bin/Release/ime_context_test.exe", + "sha256": "e7bd99a75a0738e1e8d24e57ce39fac89752411ad11f11bcf1d590e85b23b932" + }, + { + "path": "build/candidate-adapter/bin/Release/ime_input_expectation_test.exe", + "sha256": "a3febd8e1f02c8ffa7c050f2542569ce520488b8c226ed6a7c057f483e7c5680" + }, + { + "path": "build/candidate-adapter/bin/Release/keyboard_test.exe", + "sha256": "22bfedc16265cf4063082e5aa0cc82e74b9d2b3af87bebafb87261e4a6331feb" + }, + { + "path": "build/candidate-adapter/bin/Release/layout_state_test.exe", + "sha256": "5a94a47ee125737fd1835cbb43585aaab4f91ddc95192a9f919178cbd90ce59d" + }, + { + "path": "build/candidate-adapter/bin/Release/paste_test.exe", + "sha256": "d698700223cf42208a2aa379c5f7dcb894519788f3f53fb3ca85ecde050128c0" + }, + { + "path": "build/candidate-adapter/bin/Release/presentation_probe.exe", + "sha256": "5194adb45f0a573dfcb9f08bd442b60e03a35f0811f4720f1e06c5f2b43eb0b8" + }, + { + "path": "build/candidate-adapter/bin/Release/raw_console_receiver_test.exe", + "sha256": "2f3214c9ccb57f9bfaf6f228015ffd5838e61fbababd087b72a3baf14b9642b9" + }, + { + "path": "build/candidate-adapter/bin/Release/real_app_capture.exe", + "sha256": "72d26bda61af90b21b83bdb6cee3a048abec2d277494aaf86bfd99ded2a4368a" + }, + { + "path": "build/candidate-adapter/bin/Release/receive.exe", + "sha256": "a4d914f1c804c7cb240be00c987801bbfbbe1ed0006423e1a97b1d301d1f78df" + }, + { + "path": "build/candidate-adapter/bin/Release/recovery_probe.exe", + "sha256": "ef2e7c61ca875d0dbb02220f8c4df2646332f278863fd64673679c7102af6596" + }, + { + "path": "build/candidate-adapter/bin/Release/renderer_metrics_test.exe", + "sha256": "244cb42f8db6d27d31f7612b563229cbbda97535769212e53233a86162f08025" + }, + { + "path": "build/candidate-adapter/bin/Release/replay.exe", + "sha256": "37dfecd9adfdf89b2ee44b581863da0114d6fa486ee5d669c7f073ae0581505a" + }, + { + "path": "build/candidate-adapter/bin/Release/resize_test.exe", + "sha256": "c5ad023c3076574466b9284102751218436bd40333eedeb70f41e964f22f8dcf" + }, + { + "path": "build/candidate-adapter/bin/Release/runtime_observation_test.exe", + "sha256": "c2cfd3ec70cb468eb071cc05336ecc22fa601d38475fb06389116e107e670235" + }, + { + "path": "build/candidate-adapter/bin/Release/scenario_tick_gate_test.exe", + "sha256": "87ebac936c81caa8306b981307da218ac97d432c366a2b21cce93b7f4dc6c67b" + }, + { + "path": "build/candidate-adapter/bin/Release/search_test.exe", + "sha256": "77a88b31c54547cc64bb188abb201e203a6792a1492bb231449c8c41b605fbd2" + }, + { + "path": "build/candidate-adapter/bin/Release/selection_test.exe", + "sha256": "5a32e5219ca4d6ccf96be6616bdad92f24ee5ae49a10ecc6aee27d9eefadd0d1" + }, + { + "path": "build/candidate-adapter/bin/Release/sent_message_wait_test.exe", + "sha256": "02633453bb43ee175380b32d9d74c5e810e18373fc7ae0b9d281b480ececd70d" + }, + { + "path": "build/candidate-adapter/bin/Release/shell_clipboard_reader.exe", + "sha256": "7fcd3e07232000c276c85e5667b2525cff67926342cb870d0980b88a84273377" + }, + { + "path": "build/candidate-adapter/bin/Release/shell_ime_client.exe", + "sha256": "75c0b15ed5e79b9abf4815e6036c657fa2713531318d0ab256c98f420dcba97b" + }, + { + "path": "build/candidate-adapter/bin/Release/shell_input_client.exe", + "sha256": "4c70b7cd5fc8b31df2e33f9af8fc714e2ff3fff8b709499768e26943c146b97b" + }, + { + "path": "build/candidate-adapter/bin/Release/shell_launch_test.exe", + "sha256": "7fa6d0080ad05bdc93716f9c9ee0f4dc4e9aa181b4dac17dc0bdc1876bb86002" + }, + { + "path": "build/candidate-adapter/bin/Release/shell_paste_probe.exe", + "sha256": "59a978d4657e419b14a362ece8a6ca83c01283aa13c11293007b1d3386334040" + }, + { + "path": "build/candidate-adapter/bin/Release/shell_query_client.exe", + "sha256": "d9a74b89e702cb21bb3d2b79989bb488d764e33027df6e52f1dd296657754c5d" + }, + { + "path": "build/candidate-adapter/bin/Release/shell_queue_probe.exe", + "sha256": "1e05519f1bc802d81407b0836cbc8a25ce8a01f9354c12a03e91f3d5bde0b0d5" + }, + { + "path": "build/candidate-adapter/bin/Release/shell_selection_text_client.exe", + "sha256": "a87359d579e4d897c84e542a83b750b2993cf5cab7fb7fe9a525551134d9ead6" + }, + { + "path": "build/candidate-adapter/bin/Release/shell_size_client.exe", + "sha256": "67764272d9e53298b4f8eefe351b63f74dc36fe31fb9b8550a93d7c700da33da" + }, + { + "path": "build/candidate-adapter/bin/Release/shell_test_launcher.exe", + "sha256": "3923d6cd21b6b5ceffe0d91d4167d67ee4b8c2b946194ffcb079bd9ac041bad3" + }, + { + "path": "build/candidate-adapter/bin/Release/shell_text_client.exe", + "sha256": "9ccd38100fc77e2e39e1eee811e68aac9a656c2cfb8c3585121c38dc7a3b3bde" + }, + { + "path": "build/candidate-adapter/bin/Release/termforge_conpty_probe.exe", + "sha256": "b48137edad63d1e204e94431fbe950a59d7ca9785c690ba0ee60d7fb236df1f6" + }, + { + "path": "build/candidate-adapter/bin/Release/termforge_ime_probe.exe", + "sha256": "5bfded9b7605ddadd87908729ded01fd29af366a938c98ab0167248a70789cea" + }, + { + "path": "build/candidate-adapter/bin/Release/termforge_shell.exe", + "sha256": "d6642de28158e38f2a015e1cbf65179c043a3b4bc9ce3363070aa86c166bf78a" + }, + { + "path": "build/candidate-adapter/bin/Release/terminal_trace.exe", + "sha256": "446f45870fffbc121d3099ff31e6f6751429d3a1bdb0c4e6eb79e88b394277bf" + }, + { + "path": "build/candidate-adapter/bin/Release/text_action_policy_test.exe", + "sha256": "1945dcdd963dcff679334669df943dc8462bbca2096020120a355d0e4c344baa" + }, + { + "path": "build/candidate-adapter/bin/Release/transport_completion_test.exe", + "sha256": "82236eb238ccc1f8ef34d62da8d68bdbb0b5966b92375b0920e055ba376ea1bc" + }, + { + "path": "build/candidate-adapter/bin/Release/workflow_fixture_test.exe", + "sha256": "7fa7bea14776ca254e23ef14e070eb40656fe35badf017f31063d3d25abbf8ad" + }, + { + "path": "build/candidate-adapter/bin/Release/workspace_state_test.exe", + "sha256": "e6510653716daa1a7ca9128da937e53aa80652add940b1cfc09bb9e76b74a523" + }, + { + "path": "experiments/candidates/alacritty/Cargo.lock", + "sha256": "df83cd8bec1fd2ac5c8f84c27aded75b7e726ee385b108bd99b94024f215d5cc" + }, + { + "path": "experiments/candidates/alacritty/vendor/alacritty_terminal/Cargo.toml", + "sha256": "5eb92aa34f551ab98424a99f5f20ec5ecbb773d6a56fb20931387ceb83398ccd" + }, + { + "path": "experiments/conpty/ctrl_c_probe.cpp", + "sha256": "5ccc2fc3405f666b1bd9225f00eca1f6c527246ff8665097351450428ea6d969" + }, + { + "path": "experiments/conpty/ctrl_c_run.py", + "sha256": "698e76ac95b7de74b0479b5dbc79596137ad770c28b9185140ca790389752c9b" + }, + { + "path": "experiments/conpty/margin_contract.py", + "sha256": "f6ff82099b4675d7f64c9bbebcd226b9c3d5da1998a20c8477945956eadd3561" + }, + { + "path": "experiments/conpty/margin_probe.cpp", + "sha256": "4dc43c132f994280367d810e1066e8dbfcc2bb273897f7e6ceebae1e6780f713" + }, + { + "path": "experiments/conpty/margin_run.py", + "sha256": "bbfa6b4b114cf0e95cefd1b5d87f28cb1d1abfd03236b3cac556b9a2c63fa1c9" + }, + { + "path": "experiments/conpty/probe.cpp", + "sha256": "1d199896c9cffbb04b3deca2002644dcdfb9e37851e84b7c21f92ed748a62032" + }, + { + "path": "experiments/conpty/real_app_capture.cpp", + "sha256": "95b16e22e0c8fc9cf763093900f1982a01ad6808a679f00e52c3761195099fe6" + }, + { + "path": "experiments/conpty/run.py", + "sha256": "c8eef1e36b6d200b2d508cef91eaa5ca79a91fa87c5daf6265b2475c31dce329" + }, + { + "path": "experiments/conpty/session.hpp", + "sha256": "6c05bafd481c70e167d2a340aa92231e713f9e8904cf6e57ac8d8dcdb3ab3456" + }, + { + "path": "experiments/conpty/test_conpty.py", + "sha256": "ad7bf27c5909ba7534b5d3c7b3c9b553aa5803dfb5f73ce2ac5e40cd231040d7" + }, + { + "path": "experiments/conpty/test_ctrl_c.py", + "sha256": "c8e1d00ed3188856f392baca1cb59742daebc58af5fb3076e381fbf87a9e6db5" + }, + { + "path": "experiments/conpty/test_margin_contract.py", + "sha256": "9fd095900ee58c1dc5ca81d4efc05cf2741502eff388b20d4597100f1ff88d1d" + }, + { + "path": "experiments/conpty/test_margin_run.py", + "sha256": "53d0ae3183797d98dded54a9db8c68b9ecf6b08e6407b58b4b123dc75c90ec4b" + }, + { + "path": "experiments/core/candidate_resize_map_test.cpp", + "sha256": "bd2b6f64d986e76aa99f6c302dc51a40a5a83459ab558fc0dc8eb08ed6ef109d" + }, + { + "path": "experiments/core/candidate_terminal.cpp", + "sha256": "ac6945bca9d803f05cbf81193a63f9fa364775f240135d4453c6cb0c66cfb70c" + }, + { + "path": "experiments/core/candidate_terminal.hpp", + "sha256": "123fd595d6f993be29bac2c7af35c5b87c975af66275a0f05b94a4766a635a9b" + }, + { + "path": "experiments/core/candidate_terminal_test.cpp", + "sha256": "c87803866a83b2b8b2e4f24859746f9cea9fe359170b766073e3f884a46805e6" + }, + { + "path": "experiments/core/history_test.cpp", + "sha256": "bc8d6afffe6b70cee6d0252bd5ef924af17ec49cc638b3265fc7d7a2cfa849f8" + }, + { + "path": "experiments/core/history_view.hpp", + "sha256": "c4de51b54a1134a155bd5bf8ede88b668de4c89c01cd71787ecad1ce8e454971" + }, + { + "path": "experiments/core/recovery_probe.cpp", + "sha256": "bab1898226ff775113475867d206e7cba02458026f2f9356dac229b2f5eb15de" + }, + { + "path": "experiments/core/replay.cpp", + "sha256": "4d34504f11659f859db6f253c04240abf5e1b8f4c1d57367e86c0ffa9cafd778" + }, + { + "path": "experiments/core/resize_test.cpp", + "sha256": "46c8a6a45c18c5994b9aa58673957f05226eba026287f8f3ea1e1354fc1e092b" + }, + { + "path": "experiments/core/screen.hpp", + "sha256": "fa80d3e67375b0cfacfe4ad640ecef0a47c36ca493499c0054ab5b3e7abf46c1" + }, + { + "path": "experiments/core/search_test.cpp", + "sha256": "42dc6381a827f0621007987cc0c3bc8f0bf35d8b5242e4ee6b2a1928e88bffc5" + }, + { + "path": "experiments/core/selection_test.cpp", + "sha256": "0e74f19801a0e2e7273d2312fb36259487fbfefb180fcc64048ad415cce753a6" + }, + { + "path": "experiments/core/terminal.hpp", + "sha256": "e5ca6b065b38b98ba4bb32f579e4f0e8e126a7872de147ff37c2c68aac414358" + }, + { + "path": "experiments/core/text_document.hpp", + "sha256": "87efae9d6a9fccb88dd353cdd1be6a570480b6942fabbdf3c9dac2c6370e026a" + }, + { + "path": "experiments/core/text_projection.hpp", + "sha256": "3853bcf053964e54ea00bb7acfd0d302abd536ae0ed4eddfe791a6bcffdb6914" + }, + { + "path": "experiments/core/text_search.hpp", + "sha256": "fcc64b96c1f8d39b3a09dc45ffe537e7dc0fd70727b4d46768706ab55467caac" + }, + { + "path": "experiments/core/text_selection.hpp", + "sha256": "7d41d58d434ad3c32ce2f67c043e337277abbbc69e4a7a2b6bdf7e519f9f5462" + }, + { + "path": "experiments/core/trace.cpp", + "sha256": "91e6f4594ae47a9e0c60a435c71687666ec114373899d033fa59e1e46b9945d2" + }, + { + "path": "experiments/core/unicode_width.hpp", + "sha256": "d48bf60090a9c6a4263bb46ac587b13814b52c0dbe9d2649cee20e832a1e1926" + }, + { + "path": "experiments/core/view_mapping.hpp", + "sha256": "fd9b4d8c8773436f5a598aea67a420497e5958ee323944c4e3c5b5817fd3ab95" + }, + { + "path": "experiments/presentation/desktop.hpp", + "sha256": "2efc54b1a23acbbc4877fd1b381d5a87e98395ddeed01b5159cbe78fa92a0ccf" + }, + { + "path": "experiments/presentation/presentation.cpp", + "sha256": "6d274ac311231f288a4c5bbbffd52153dafa54d2873fe0e318d55c8c2004bd49" + }, + { + "path": "experiments/presentation/render.hpp", + "sha256": "cebb6588e4b7ec63c761bd8615e8c14582284040d12370755e06a6a582312e66" + }, + { + "path": "experiments/presentation/renderer_metrics_test.cpp", + "sha256": "d6413f633210d824054f5f38c8e7d34843b6764bdab0ac862e6f9c66977758a4" + }, + { + "path": "experiments/shell/action_checks.py", + "sha256": "eb1d2b08c3f1384b57a45eb6154f462363bc52d4f900cf887c1518b17bb2a1bb" + }, + { + "path": "experiments/shell/alternate_checks.py", + "sha256": "8a2a7b789d60c6b64d14cfad21b02f00cc964814a0a17ea06d187ceba4182de0" + }, + { + "path": "experiments/shell/candidate_title_test.cpp", + "sha256": "60066ba29b6f153c78ac96fb2dbc61b68f025623d2b84c9cb83afb3604cd5362" + }, + { + "path": "experiments/shell/clipboard.hpp", + "sha256": "d90f103661f8a4a4fe45d678027a7c070232cdc8f4da80c59d218c3f29d93a2a" + }, + { + "path": "experiments/shell/clipboard_checks.py", + "sha256": "cca669e9a08b32c4285260443165364ac75b1d7a057d8f371e06bc4e9f54c696" + }, + { + "path": "experiments/shell/clipboard_failure.hpp", + "sha256": "f37a9867e83b95eaa084211c9ab47bb4bf400682c2c02c59b941bbafbfac0bca" + }, + { + "path": "experiments/shell/clipboard_owner_policy.hpp", + "sha256": "5bfc713790d302b48266a583614db3e9b43be8c59bb74b2e531595c8898e5f0c" + }, + { + "path": "experiments/shell/clipboard_policy_test.cpp", + "sha256": "780567c70cb5028c9ace2ed50944177808f62fc42365572f9d48ff7fda5f26a3" + }, + { + "path": "experiments/shell/clipboard_scenario_state.hpp", + "sha256": "0824d44ff250ff9f4d62468aa710f07833ac4b48e5c496b5c750cc39a15aa408" + }, + { + "path": "experiments/shell/clipboard_text.hpp", + "sha256": "b050e6da2fb92c080b054e3b1357e9a87484818fb8efdd43b388641f6ff256cd" + }, + { + "path": "experiments/shell/clipboard_worker.cpp", + "sha256": "d1f45e20ea8b63e747c2cbd43d0b0dd3fda2f22502a90af6e1867ac9fc4075c7" + }, + { + "path": "experiments/shell/clipboard_worker.hpp", + "sha256": "7d883bd28f115f986ce07837b619e0985b0ddc8faa7395a8d3919415c889f4a2" + }, + { + "path": "experiments/shell/edit_actions.hpp", + "sha256": "83c0129a463fc376a4184cfef1da1be617546c60a86678daf5c6af79be94668a" + }, + { + "path": "experiments/shell/find_bar.hpp", + "sha256": "809b458c3d534f750e590a8e3bfc2355aeb02413f1ed5cb4f2ed8021fd2711f2" + }, + { + "path": "experiments/shell/find_bar_test.cpp", + "sha256": "f150adc9d0b83dc17355e3e5759a6a09328805864371e5afba21806056bed6b2" + }, + { + "path": "experiments/shell/find_checks.py", + "sha256": "cf1463a67c33ce367b72cb79aaadda9197ea66fb75fea7163a527f22fd363301" + }, + { + "path": "experiments/shell/history_checks.py", + "sha256": "27366c9fe6c40e16c63f9afc2ae377cff4df2682c3b334d3894454b3f7be453c" + }, + { + "path": "experiments/shell/hosted_ime_input.hpp", + "sha256": "b556a1d9cfd3bf7fcd0de34b5aa11ee5743a92f8d3239af8f1828c0726050aab" + }, + { + "path": "experiments/shell/ime.hpp", + "sha256": "470d3e40a0962506ec58237af0593cf0510d6aa34cebb62bae383012d4652ee0" + }, + { + "path": "experiments/shell/ime_client.cpp", + "sha256": "817355892bfa539f66f5913045bd72184670eac53522a1c3f02a3284ef2619f2" + }, + { + "path": "experiments/shell/ime_context_test.cpp", + "sha256": "5aef11634c3dac10a22e2b28f3e9313aabdb707d8b51ba5e0dda854ebbb26872" + }, + { + "path": "experiments/shell/ime_desktop_process.hpp", + "sha256": "ed5a6248cb74c190c04165fe864ac0ca41e0a96ba9945f7797b3f9b10c281161" + }, + { + "path": "experiments/shell/ime_input_expectation.hpp", + "sha256": "084f39af300e7b347850339a1864d16001e35a993f9537e58b314802817cff96" + }, + { + "path": "experiments/shell/ime_input_expectation_test.cpp", + "sha256": "73a4b4400ccbc63e8c301838d8350b1f8d3a847d25ee8411cede913d5fc4a39d" + }, + { + "path": "experiments/shell/ime_probe.cpp", + "sha256": "6060b91314fb93fc5d417c2c22c7eb42dc12a3c2829562370488f38a6a53471a" + }, + { + "path": "experiments/shell/input_client.cpp", + "sha256": "67ebb343978ac6822f8feaafa7510f9ee325737fce91b3d2aed8385deea6a969" + }, + { + "path": "experiments/shell/keyboard.hpp", + "sha256": "0790ddd0a3b755de5fc22a7bff5c3421b91c19b78dcd7bc0af9de22fa471c3b3" + }, + { + "path": "experiments/shell/keyboard_test.cpp", + "sha256": "7a0f312d8ba63a7f403b4350acaa92b19041cfd8fb16fb6818bf98eb329154e8" + }, + { + "path": "experiments/shell/layout_state.hpp", + "sha256": "ac1b56bb2809ff2b28cf81e7806280cda0c547d5b1417228b1b8b264f1d1d5c5" + }, + { + "path": "experiments/shell/layout_state_test.cpp", + "sha256": "ae3ad7d272e9bdb839fafc2ff38a8859e9443e855816ba6cc6eb32a33123f37f" + }, + { + "path": "experiments/shell/local_shell_diagnostics.hpp", + "sha256": "3e7bb71f7b7bae0072795316c81cdde87902dd0485b1ae555944b91f3208e396" + }, + { + "path": "experiments/shell/margin_checks.py", + "sha256": "d571ea1cd1d99c41a0ae1d2939549becfca3c6d95dc431a4dcbd0255e4259f1c" + }, + { + "path": "experiments/shell/menu_gesture.hpp", + "sha256": "bf9b9c98c3f4b93699d2a06fc4f8cb580b721e3fa438fa20ed26eac1c60b3f53" + }, + { + "path": "experiments/shell/native_scroll_gesture.hpp", + "sha256": "603c16f370882899b6954596b1a55dacca524a5a3be9d8e5b3be0b4a0374c241" + }, + { + "path": "experiments/shell/operation_observation.hpp", + "sha256": "b53d054e23fcb2fb9429a38ca8ed2fd5471c1ca21744898e2068d990e4c386f0" + }, + { + "path": "experiments/shell/paste.hpp", + "sha256": "f905a693d840256ca7c6a8ffed2601bff41f74e549413f38822db7aceb1c7c82" + }, + { + "path": "experiments/shell/paste_checks.py", + "sha256": "f37db2ada88816e6936563d5155578a31276161cae11c99c27bc4f0253b053d6" + }, + { + "path": "experiments/shell/paste_probe.cpp", + "sha256": "eb06a1a2ba41013c740d183226f297ddbdc8fd4369a625f3e1d3f5356cdbee7e" + }, + { + "path": "experiments/shell/paste_prompt.hpp", + "sha256": "9746f97a1f9f252a3cffd12a4cf727273bffcf38a53080fd11ae9c9f11710c55" + }, + { + "path": "experiments/shell/paste_scenario_state.hpp", + "sha256": "fd433a7273f15f67980a1a87d517e299f351eb3874f2fc2894cf914a00130382" + }, + { + "path": "experiments/shell/paste_test.cpp", + "sha256": "f4e056681ffd175928060191bc20cbfdc889c8d6ffa76843eda2365243dff8c4" + }, + { + "path": "experiments/shell/query_client.cpp", + "sha256": "c8b871ba3cb6ca5c40bddf23e5006c52803eba52ccb5bee56d0b7c52cad68869" + }, + { + "path": "experiments/shell/queue_probe.cpp", + "sha256": "d93802e53eb01eed5330fde547443f7d4dd8e83221a067a91484915dacef3afb" + }, + { + "path": "experiments/shell/queues.hpp", + "sha256": "9087944a7749176789a489bce0522c55764461342b59c138611743b6947845b6" + }, + { + "path": "experiments/shell/raw_console_receiver.hpp", + "sha256": "9ea35252c33a938a7dc76d4ea7c6db72d04a3fd010c1ce6b0289c9747e50afcc" + }, + { + "path": "experiments/shell/raw_console_receiver_test.cpp", + "sha256": "a2492a2a57462af6348c7d0b659b557d4fa1cc5c3838286d5bbae6958b904bc4" + }, + { + "path": "experiments/shell/resize.hpp", + "sha256": "14e44837d8fc2d0b88356db824e46616ef6c210dcf9b668445478bac06284415" + }, + { + "path": "experiments/shell/run.py", + "sha256": "d77454ce626be025e088e6adea9d5f12150c039296397161b2bb8e00e08c1d66" + }, + { + "path": "experiments/shell/runtime_observation.hpp", + "sha256": "887e76c02c581a049fe05001e7cc7d4065dacd3a783ae5803177acfe7e775c8b" + }, + { + "path": "experiments/shell/runtime_observation_test.cpp", + "sha256": "577b5e9368d945c65a8628f55eb882cf93bb4e0da9d06d234d217ec646beb90d" + }, + { + "path": "experiments/shell/scenario_tick_gate.hpp", + "sha256": "29a045b5559d7d00c9e0ec48793d889afcd1aea0d30e70d49f95a8394ea0c404" + }, + { + "path": "experiments/shell/scenario_tick_gate_test.cpp", + "sha256": "c105a6f56c079ecd6efe2154976b7b867a74b8238c20975adaeefe10209acf1a" + }, + { + "path": "experiments/shell/scroll_checks.py", + "sha256": "88830c09bd201a50ff6028f95766fc8c2c8a229192da6f0ae347fee0f69dee03" + }, + { + "path": "experiments/shell/search_interactions.hpp", + "sha256": "13c5d9058d1dedbf40c2a8b186754c2db13827579d7046c956ebe135463333d1" + }, + { + "path": "experiments/shell/selection_bytes_checks.py", + "sha256": "7e008035ee827ac1e16416c98536c9b1e7ee523bbf1c3acaf68e5c62ea9a9874" + }, + { + "path": "experiments/shell/selection_checks.py", + "sha256": "24eeb34ad94e4d71bed49e42eba3992ec7d7dbfd1f3d4958d8b3110546ffddd3" + }, + { + "path": "experiments/shell/selection_context_checks.py", + "sha256": "18e51c400b93660bd5c88c220e9049f27af314c2e2de145ed9d218afc5bb9551" + }, + { + "path": "experiments/shell/sent_message_wait.hpp", + "sha256": "b40558ecb8652695ce8c81b2bda85b7956a5a19a060839e27a406f9ee955293d" + }, + { + "path": "experiments/shell/sent_message_wait_test.cpp", + "sha256": "10d9b64f7e91c765c4b7e8cf5a3306dc5002f82f0bd7074b1e4149be24a15dde" + }, + { + "path": "experiments/shell/shell.cpp", + "sha256": "cc3830a52aebfad69871d974fed6c3f295f58e1e75feb451f7cfc11e0e56a3ef" + }, + { + "path": "experiments/shell/shell_launch.cpp", + "sha256": "03fac4122d8151539adf9e88c273e9fbaf632f2e80ba10ea77c3873f588db052" + }, + { + "path": "experiments/shell/shell_launch.hpp", + "sha256": "921724b7101c5146637a49b942c834d2860d0d426105c117354ddd8680ef50eb" + }, + { + "path": "experiments/shell/shell_launch_test.cpp", + "sha256": "8e786e3ef7d3fe127cb3e2d26f3bf5a392d0dc14d2792827527421a9caa0ff05" + }, + { + "path": "experiments/shell/shell_selection_text_client.cpp", + "sha256": "e4efe9d634839c491fca51219b6d249193d0dc770082d63149ebbe472a642b9a" + }, + { + "path": "experiments/shell/size_client.cpp", + "sha256": "28d41fd5bbe01f60653de1ea8473707ac36969bb8d6b17c93e87294ff8cf7487" + }, + { + "path": "experiments/shell/test_actions.py", + "sha256": "29eed3bd65e590ba24d18049f9493215665eb44c93efbcdb740b3c3b3bdfe1d1" + }, + { + "path": "experiments/shell/test_candidate_tui.py", + "sha256": "ccbab94cf0b9587a9c914c84cd8bec5ae9caabe516767b84457a6be7ff294ed8" + }, + { + "path": "experiments/shell/test_copy_error_frame.py", + "sha256": "d8bbe74d9ad92f6f79046ebc9dc4c80e49f7fda9445ca85221d37d10637a74d5" + }, + { + "path": "experiments/shell/test_launcher.cpp", + "sha256": "19cab4a960f47f252c02f92a36316d43eed2481da1f72b5a7e97003c4fa1c2ed" + }, + { + "path": "experiments/shell/test_paste.py", + "sha256": "c0815385d0825f6d33985e0060da9ecb8b2281ae1d10c446706d3b3b9abc567a" + }, + { + "path": "experiments/shell/test_raw_console_receiver.py", + "sha256": "b6d424f09e61edf8879b5336f66f8f1bfbbe3dee7f8b77ebb2b4bf30eaa907a5" + }, + { + "path": "experiments/shell/test_selection_bytes.py", + "sha256": "03705399c8802475b83fc3ef22fce78133a7f87f01754b4d70338e0c135f8d14" + }, + { + "path": "experiments/shell/test_selection_context.py", + "sha256": "19d141ffd7038bebe44c7f9431cd077149374b37ba82bcd9a7a01ba82eab848a" + }, + { + "path": "experiments/shell/test_shell.py", + "sha256": "db1baad242e0be1ec6d7eaa156d94f894f5db98398bb13d194c5bbe46ea03ddd" + }, + { + "path": "experiments/shell/test_workflow.py", + "sha256": "f935800810cb498d73fc5ec601b541da6d45562341b5b83a4d7027aa46b00dbc" + }, + { + "path": "experiments/shell/test_workflow_resource_phases.py", + "sha256": "b95aba0fdf9287ea7d3e04270c4297b2f6795eba751fe378b805ee9f7c3261e0" + }, + { + "path": "experiments/shell/test_workflow_resources.py", + "sha256": "da0bf33ae4aaa42d5ea345fc830113fe02ec1ba08df01ee38471b1bf34116600" + }, + { + "path": "experiments/shell/test_workflow_sessions.py", + "sha256": "1c32f36cba9bdaa1a0594e2865085b311aa1e6d919d8895e7ef4a60f76fc7c6c" + }, + { + "path": "experiments/shell/text_action_policy_test.cpp", + "sha256": "95ae32d32c5a727ff1903edb68eec3977f45babf159bd1bde302e008fcbeaeea" + }, + { + "path": "experiments/shell/text_actions.hpp", + "sha256": "95b91c52cf4c6a507e92089795d38a2475c2d35ae6065c8b227b4cf5b0eb2f0b" + }, + { + "path": "experiments/shell/text_client.cpp", + "sha256": "aea8ed527e6e79fbe61446afadbb25d4fa0b7ddef4966930583ac8fa04cadd15" + }, + { + "path": "experiments/shell/text_interactions.hpp", + "sha256": "780c560221e7a8505b388353eb5cb5f68ca5a3cb746d4d5cb1fa5f93472fe007" + }, + { + "path": "experiments/shell/transport_completion.hpp", + "sha256": "535f9c1cf238061781a56ca597dd7222308a0f57aa96e3e5959fbca9a1d34602" + }, + { + "path": "experiments/shell/transport_completion_test.cpp", + "sha256": "c9bb02bd739f916e78530590ee0f3b29bc952c58e445d31bc047ee69d301dcee" + }, + { + "path": "experiments/shell/ui_font.hpp", + "sha256": "2f5f7a5a447a765f316955086554b4a79371062d3604eb858fd6c62094beea58" + }, + { + "path": "experiments/shell/unicode_checks.py", + "sha256": "95fffb54274b695be2b857d942555dd8001801eeec5e3f020a9d448cbe1a82e5" + }, + { + "path": "experiments/shell/workflow_checks.py", + "sha256": "f186ff0f5335c26fc5c4f0286961981dfd6af8fbaafc789e2bc50058486efa44" + }, + { + "path": "experiments/shell/workflow_controller.hpp", + "sha256": "61005e442aa1729759dd1c1b8f0919826cb2e71f225fae53f0ea0c9cc28d38eb" + }, + { + "path": "experiments/shell/workflow_fixture.hpp", + "sha256": "332d73699320a6f503fc212417a4d3820b82cc2d3feb3d1fffe3930660d57453" + }, + { + "path": "experiments/shell/workflow_fixture_test.cpp", + "sha256": "892155cc5f6b42a6fff500607a9523e69318c53034d0e2d2f2ccaacf97420aeb" + }, + { + "path": "experiments/shell/workflow_resource_phases.py", + "sha256": "187f95d71a44e363b54269613e962d8ff6de1b758d78b7053da3cbedebd54b00" + }, + { + "path": "experiments/shell/workflow_resources.py", + "sha256": "70bfc20d8950d1796b091c3f178fe5e3aa5de2906393efa5df4d1ab15a4a4595" + }, + { + "path": "experiments/shell/workflow_scenario_state.hpp", + "sha256": "0a17b04c391370417be258801d2a8bfcce55fcb4f88174795b42eed89ca62760" + }, + { + "path": "experiments/shell/workflow_sessions_checks.py", + "sha256": "33f92a6c0a963e32de46f749b1e88d3a57a6efec4f93e9c4412bc4ac61d57278" + }, + { + "path": "experiments/shell/workflow_system_state.hpp", + "sha256": "5babb8576a963aa405597b614a4b9a5ee7fcf1f97ad4f44067d09e901ef28f78" + }, + { + "path": "experiments/shell/workspace.hpp", + "sha256": "75063983f4047efb88b254406cf25aaab6de113a44fde30fcdbe2eb7809bfc16" + }, + { + "path": "experiments/shell/workspace_actions_scenario.hpp", + "sha256": "0da18068dffcf702e1c5a3bd7e524660d24a59ca3416dc3ce7167940a85527a0" + }, + { + "path": "experiments/shell/workspace_clipboard_scenario.hpp", + "sha256": "a017e206283afdfee4965bfd793049de4bb9d026d9a6afabfafa92a105e307ed" + }, + { + "path": "experiments/shell/workspace_dpi_scenario.hpp", + "sha256": "ec076e8dd1bf456231b9858acf5e884fae0e9980a94102dddae5eef4a1116b2b" + }, + { + "path": "experiments/shell/workspace_find_scenario.hpp", + "sha256": "66ac75407793ab01323f1922f1476b482452113d275dd241d6f2fdc279d6f606" + }, + { + "path": "experiments/shell/workspace_history_scenario.hpp", + "sha256": "368cd1e9f9e0276f5ac83827ebbf02a301d877caa41399c64936e610f6d1f1e8" + }, + { + "path": "experiments/shell/workspace_ime_scenario.hpp", + "sha256": "2fade75db98dbd4355e41027a60a2a21fa57bacd9a716587be8ca4f04fd0febc" + }, + { + "path": "experiments/shell/workspace_interactions.hpp", + "sha256": "1d65c22bee7570b0cfe576e2105821f174ed50569a6146d1bd513f57806347a0" + }, + { + "path": "experiments/shell/workspace_keyboard_scenario.hpp", + "sha256": "03b75bde8c6dcd640d9acbee5e417529081d8e70940ff0aa0b0ab3adeb0b1bcc" + }, + { + "path": "experiments/shell/workspace_launch_failure_scenario.hpp", + "sha256": "124b4fc337466b260c847f94f2e76e8c65a641b16fdcacfce5a60804c2949797" + }, + { + "path": "experiments/shell/workspace_main_scenario.hpp", + "sha256": "c3994cc60041cd1e7a8599e5c9037473634412bcb6cfd349850ff8cd2808a47e" + }, + { + "path": "experiments/shell/workspace_monitor_scenario.hpp", + "sha256": "b2409405e10e49f4a0b9df8c4822e6cf96747f12388915780e2e52fe4771df22" + }, + { + "path": "experiments/shell/workspace_paste_scenario.hpp", + "sha256": "406cc1162560fa7eb1b607c6895280a5353315ffc01099984685f25bab20656b" + }, + { + "path": "experiments/shell/workspace_persistence_scenario.hpp", + "sha256": "7f70d755549718d0fddb94b43cce12f3955727fcc51599a8bb124bf3e59ec263" + }, + { + "path": "experiments/shell/workspace_scenarios.hpp", + "sha256": "c9a40dc370bf538c24b62b6177795975b4fac84ff78b0055117f7203b58a7b7d" + }, + { + "path": "experiments/shell/workspace_selection_bytes_scenario.hpp", + "sha256": "ed2c2131fc6ef7634dec8d81fc7d4d22287f989cc75b7d5658f3bfa4f8057d35" + }, + { + "path": "experiments/shell/workspace_selection_context_scenario.hpp", + "sha256": "e134f28afe99b199ab141be3f2eabfc4e84f0c2a26c41f5accfe16dd0d36d9a0" + }, + { + "path": "experiments/shell/workspace_selection_scenario.hpp", + "sha256": "3d7c2fd82fc9d06cedd517cdb15c9e07f3a38640931be6e3638d692356ef4269" + }, + { + "path": "experiments/shell/workspace_shells_scenario.hpp", + "sha256": "aaf5624cf5ab8883e3dca5197a9637fc95ee005d3056acbefdce781fd8af2720" + }, + { + "path": "experiments/shell/workspace_split_scenario.hpp", + "sha256": "fc04870698b99659e52363ba4dd82aabd10132c79350fd4c46abda8071f166f7" + }, + { + "path": "experiments/shell/workspace_state.hpp", + "sha256": "f81aec1d9ff1816f0cb34cde5621e39f859dabcaff5e02a29eced164f544fc09" + }, + { + "path": "experiments/shell/workspace_state_test.cpp", + "sha256": "ccf27ac3f538ea30bb5bbcf6fc4edace86c043d482fc816551907d5883ba7c87" + }, + { + "path": "experiments/shell/workspace_workflow_scenario.hpp", + "sha256": "94746441b9fe4186dba0bd480dc32bae93a87b10b9e8effe02f4aea8e89c4dfd" + }, + { + "path": "experiments/shell/workspace_workflow_system.hpp", + "sha256": "de7dd67dd7cef36dfaf4187faf1927ceff8ea141dc8a1ab654e84529de7bd78e" + }, + { + "path": "local/r29-viewport-projection/build-final.log", + "sha256": "b2442ceb08728c5d6aa8fe4f5e5086e83f812099046129422c7e88d0d81e1df6" + } + ] + }, + "final_build_identity_check": { + "files": 208, + "matching_sha256": 208, + "missing": 0, + "method": "All frozen source/runtime/log files rehashed after final CTest; documentation-only wrap-up does not change compiled inputs." + }, + "partial_visible_observation": { + "source_commit": "f41f747be27ad7b1de965b2279948eca7473cc0e", + "command": "candidate_core_test.exe --source-partial-visible", + "status": "unresolved_source_order", + "history": [ + "B ", + "ABCD" + ], + "visible": [ + "EFGH", + " ", + "C ", + "D " + ], + "copy": "B\r\nABCDEFGH\r\n\r\nC\r\nD", + "EFGH_matches": 1, + "proof": { + "path": "local/r29-viewport-projection/partial-visible-final.log", + "sha256": "8c85c093c56979ccf5ec1076b31a319119bdb9c860bbed2301e6984c821fc16d", + "bytes": 223 + }, + "note": "Observation mode returning zero is not a comparison pass; B still precedes A." + }, + "review": { + "base": "8521cd7f497f52ab169d76a5a99a976f6adfc8df", + "head": "f41f747be27ad7b1de965b2279948eca7473cc0e", + "standards": { + "new_hard_findings": 0, + "new_substantive_smells": 0 + }, + "spec": { + "open_new_findings": 0, + "empty_anchor_P2": "runtime red, fixed and independently reviewed", + "snapshot_shape_gap": "runtime red, fixed and independently reviewed" + }, + "record_source": "Root summary of completed independent review-agent messages; no standalone review-log artifact is claimed.", + "scope": "This viewport slice only. Earlier native/source-composition reviews remain in their own checkpoint records; full #29 acceptance is incomplete." + }, + "publication_notes": [ + "Intermediate green logs are preserved and hashed, but do not each have a separately supplied green source/binary manifest; do not relabel them as exact Git-blob validations.", + "Mapping red is compile-only and the first wide oracle failure is confounded; retain these labels in any public summary.", + "Final CTest and its internal skipped cases are now terminal and recorded separately from the two earlier one-shot UI checks. Raw logs remain local; public references are hashes and sanitized summaries." + ] +}