diff --git a/docs/EVIDENCE.md b/docs/EVIDENCE.md index 015f836d..423ac7d9 100644 --- a/docs/EVIDENCE.md +++ b/docs/EVIDENCE.md @@ -36,7 +36,7 @@ R1 固定总出口的源码为 `ce9b24d`,早期 `2dc8d6d` / `9b16acc` 仍分 | R2 流式字素与固定 emoji(#39) | [合同](research/streaming-graphemes.md)、[红/绿及实际场景验证](../experiments/shell/results/2026-10-26-streaming-graphemes/validation.json) | 已合并 #39:.4 的固定 ZWJ/keycap 原子格/选择/绘制和单列 resize。766 个规范用例只验证分段 helper;64 标量超限按明确无损分片,不宣称完整字素布局或完整 Unicode/IME。 | | 真实备用屏切换(#27 限定切片) | [五阶段原生快照与候选验证](../experiments/shell/results/2026-10-26-alternate-screen/validation.json)、[候选接口](../experiments/core/CANDIDATE.md) | 真实 1049h/1049l 五阶段的源 VT、CHAR_INFO、样式/光标恢复及候选历史隔离;origin/wrap、margin/resize 交叉与 #27 整体验收另做。 | | 局部滚动区域(#27 限定切片) | [原生阶段快照、独立 transport 重放与候选验证](../experiments/shell/results/2026-10-26-scroll-region/validation.json)、[候选接口](../experiments/core/CANDIDATE.md) | 真实 DECSTBM 局部滚动的源、原生快照与 transport 独立重放;区域外保持/历史隔离通过,不要求控制字节原样回显。 | -| ConPTY resize 重放、历史与 Find/选区(#29) | [限定合同](adr/0005-physical-row-resize.md)、[重放与历史验证](../experiments/core/results/2026-10-26-resize-replay/validation.json)、[候选接口](../experiments/core/CANDIDATE.md) | 固定 861B resize 重放的行身份、Find/选区、连续同步、历史合并/清空与上限;不等于所有 resize/reflow、真实 DPI 或 WindTerm 运行时。 | +| 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/CANDIDATE.md) | 固定 861B 重放的行身份、连续同步、历史合并及回放后清空;独立列位置回归检查移位后的选区失效和 Find 精确列。冻结快照尺寸、回放期间清空、完整 resize/reflow 及 WindTerm 运行时仍未验收。 | | 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 aa28f292..ab3e1648 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 和有界恢复。显式候选 `.4` 接入 `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。显式候选 `.4` 的 [#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 物理行裁剪;候选在[固定 ConPTY 重放](../experiments/core/results/2026-10-26-resize-replay/validation.json)与连续同步中保持行身份、Find/选区和有界历史,淘汰/清空不复活;候选 `.4` 另覆盖主/备用屏缩到单列 | [#29](https://github.com/JavaZeroo/termforge/issues/29) 的完整 resize 交叉未完成;固定事务不等于完整 reflow 或 WindTerm 等价。系统/实验 ConPTY、备用屏/滚动区、真实 DPI、呈现、性能及 #36/#38 分别验收;默认缩窄不可恢复 | R2 | +| resize 与 reflow | **限定验收**:默认按 ADR-0005 物理行裁剪;候选在[固定 ConPTY 重放](../experiments/core/results/2026-10-26-resize-replay/validation.json)与连续同步中保持行身份、Find/选区和有界历史,覆盖限定淘汰及回放完成后的清空;[列位置回归](../experiments/core/results/2026-10-27-replay-coordinates/README.md)核对文字移位后选择失效和 Find 重定位;候选 `.4` 另覆盖主/备用屏缩到单列 | [#29](https://github.com/JavaZeroo/termforge/issues/29) 的冻结快照尺寸、回放期间清空和完整 resize 交叉未完成;固定事务不等于完整 reflow 或 WindTerm 等价。系统/实验 ConPTY、备用屏/滚动区、真实 DPI、呈现、性能及 #36/#38 分别验收;默认缩窄不可恢复 | 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/adr/0005-physical-row-resize.md b/docs/adr/0005-physical-row-resize.md index bdc2b748..1867d5ff 100644 --- a/docs/adr/0005-physical-row-resize.md +++ b/docs/adr/0005-physical-row-resize.md @@ -40,7 +40,7 @@ API 依据是 [Microsoft ResizePseudoConsole](https://learn.microsoft.com/en-us/ 上述物理行合同继续适用于默认自研回退,包括缩窄不可恢复及 resize 改变行版本的规则。显式 `TERMFORGE_BUILD_CANDIDATE_ADAPTER=ON` 的候选采用以下独立合同,不把上游 `Term::resize` 的结果直接称为默认物理行策略或完整 reflow。 - 候选在 resize 时保存有界屏幕快照,识别可跨 feed 的 `CSI ?25l` / `CSI ?25h` 重放。等待期间提供一致快照;等待超过 8192 字节或 `finish` 时解除冻结并同步实际状态。只有 resize 后的该事务才进入此路径,普通应用隐藏光标不等于已验证的 resize 重放。 -- 行键包含换行、wrap、裁剪状态、文字、宽度、样式、组合标记和 written 元数据,仅去除真正未写入的首尾 padding。只有旧、新集合中都唯一且顺序一致的键才能保留 ID/version;重复文本不盲配。最终所有保留行必须具有非零、唯一、严格递增的 ID,冲突行取得新身份和版本,不能让选区或 Find 错配另一段文字。 +- 行键包含换行、wrap、裁剪状态、文字、宽度、样式、组合标记、written 元数据及首个非 padding 单元格的列号。首部未写 padding 的数量决定文字位置,不能因文字字节相同而忽略;只有末尾真正未写 padding 不影响匹配。缓存身份映射也执行相同列位置检查。只有旧、新集合中都唯一且顺序一致的键才能保留 ID/version;重复文本不盲配。最终所有保留行必须具有非零、唯一、严格递增的 ID,冲突行取得新身份和版本,不能让选区或 Find 错配另一段文字。 - 上游增高可先把历史拉入可见区,随后 ConPTY 重放覆盖这些行。候选在完整重放后保留 TermForge 的有界历史,后续同步按 ID 和完整行键合并历史及可见行;同 ID 不同内容不视为同一行。历史容量和淘汰计数仍有界,真实淘汰不恢复,显式清空历史不复活旧行。无完整重放、无法确定语义的 reflow 和任意空白行身份均不在此保证内。 - Find 和选区同时检查 ID、位置、版本和实际投影文本。非软换行的物理列宽或后继 ID 变化不单独使命中失效;从文末新增后继会引入分隔符,仍需重扫。真正的软换行继续要求完整元数据和后继关系。半开选区排除的第 0 列末端空行可以在仍为空白时替换身份;其它行变化、非空端点及实际淘汰仍使选区原子失效。 - 候选合并不表示通用逻辑行 reflow、备用屏/滚动区全语义、所有应用、真实 DPI、呈现完成或性能验收通过。测试辅助读取使用共享文本投影,宽字符 spacer 不算额外空格,显式样式尾空格仍逐字校验。 @@ -48,3 +48,5 @@ API 依据是 [Microsoft ResizePseudoConsole](https://learn.microsoft.com/en-us/ 固定 WindTerm 2.7.0 的限定静态复核中,`0x140238f90` 仅在缩短时进入 `0x140233a30` 的保存路径;增高走 `0x1402ee110`,分配并初始化新屏幕行,不消耗 scrollback 来回填视口。该路径支持独立保留旧历史,与上述保守合同一致,但不要求照抄上游或宣称所有 WindTerm 配置均如此。本地 `local/analysis/windterm-resize-history-20261026/` 的 44 个定向函数均完成反编译并核对关键汇编,目标 SHA-256 为 `7b10531dccc7cd614ae559f36f1ef518e0c96335637431b4646354823b2804d0`。原始输出只留本地;这是静态、限定路径证据,不是运行时全量行为比较,也不覆盖 Document/Scintilla 的所有动态连接。 命令、哈希、默认/候选完整回归及未满足项见[重放与历史验证记录](../../experiments/core/results/2026-10-26-resize-replay/validation.json)。 + +相同文字左右移位后的选区失效和 Find 重新定位另见[列位置回归](../../experiments/core/results/2026-10-27-replay-coordinates/README.md)。这是现有身份/版本合同的修复;它不裁决 WindTerm 的重排、历史回填或选择保留策略,也不补足冻结快照尺寸和回放期间清空历史的验证。 diff --git a/experiments/core/CANDIDATE.md b/experiments/core/CANDIDATE.md index a7f81954..750a82f1 100644 --- a/experiments/core/CANDIDATE.md +++ b/experiments/core/CANDIDATE.md @@ -26,6 +26,8 @@ Linux 使用相应 CMake 配置。候选 workflow 在 Windows/Ubuntu 构建 `can - 不走 JSON。Rust/C++ 文本由调用方拥有,未做增量 FFI 或性能验收;候选 damage 的产品化仍属后续。 - `screen.resize` 与候选 `Term::resize` 同步尺寸;默认自研回退继续采用 ADR-0005 的物理行裁剪,候选不把它称为完整 reflow。真实 ConPTY 重放可能先由上游把历史拉入可见区再覆盖,候选在完整重放后合并有界历史,按 ID 和完整行键去重,保留 Find/选区所需的稳定身份,且不让冲突产生重复 ID。连续同步缓存原始 FFI ID 与最终身份的映射,仅在需要重放的未知旧 ID 时回退到完整键匹配,不重复扫描全部历史。真实淘汰不恢复,显式清空不复活;普通空白行、未决 reflow、备用屏/滚动区全语义不在此保证内。合同与验证见 [resize/历史记录](results/2026-10-26-resize-replay/validation.json)。 +回放行键与后续缓存匹配同时保留文字的起始列,只忽略末尾未写 padding。相同 `ABC` 从第 0 列移到第 2 列(或反向)不能沿用旧版本供选择/Find 使用;原选区失效,Find 重扫后命中实际列。独立[列位置回归](results/2026-10-27-replay-coordinates/README.md)覆盖回放中/回放后、整段/逐字节和原位对照。这不补足冻结快照尺寸、回放期间清空或原版行为的验证;已有历史清空证据只覆盖回放完成后的场景。 + ## 真实产品场景 `candidate_shell_tui` 在同一 `ShellWindow` 和 ConPTY 路径上连续执行两轮真实 Vim:进入全屏编辑、插入文字、resize 到 60×12、退出写盘、确认主屏 shell 返回。本机 Vim 输出未发送 `1049/1047/47`,所以此场景不计作备用屏验收;1049 的固定核心用例与真实 Vim 证据分开。固定自动命令写入 `candidate-vim.vim` 并以短 `-S` 命令加载,避免交互式 cmd 拆分长启动命令。启动和插入由 Vim 写入的真实 `VimEnter`/`InsertEnter` 事件文件确认,避免 `echon` 消息被重绘覆盖;随后通过插入模式的 `Ctrl-O` 调用 Vim 命令,实际保存 `&columns/&lines` 并检查 60×12,再以 `Ctrl-O:wq` 写盘退出。不把 ConPTY 重绘或 `ResizePseudoConsole` 返回当作 Vim 已就绪。本路径不声称验证 Vim 的 Escape 行为,独立控制台 ESC 接收另测。`-W` 日志核对插入文本与 `:wq` 顺序,其中可能含 Vim 内部特殊键,不能冒充纯键盘字节。随后用独立 `shell_query_client.exe` 发出真实 `CSI 6n`,保存终端经 `InputQueue` 返回的光标报告,并用 `shell_size_client.exe` 核对子进程尺寸。另有 resize 后独立 ESC 接收检查。 diff --git a/experiments/core/candidate_terminal.cpp b/experiments/core/candidate_terminal.cpp index cc12246e..9904c6d2 100644 --- a/experiments/core/candidate_terminal.cpp +++ b/experiments/core/candidate_terminal.cpp @@ -63,7 +63,7 @@ bool same_replay_line(const Line& a, const Line& b) { if (a.ending != b.ending || a.wrap_columns != b.wrap_columns || a.cropped != b.cropped) return false; size_t a_first, a_last, b_first, b_last; replay_bounds(a, a_first, a_last); replay_bounds(b, b_first, b_last); - if (a_first == a_last || b_first == b_last || a_last - a_first != b_last - b_first) return false; + if (a_first == a_last || b_first == b_last || a_first != b_first || a_last != b_last) return false; return std::equal(a.begin() + a_first, a.begin() + a_last, b.begin() + b_first, same_cell); } @@ -73,7 +73,9 @@ std::string replay_key(const Line& line) { if (first == last) return {}; std::ostringstream out; out.imbue(std::locale::classic()); - out << static_cast(line.ending) << ':' << line.wrap_columns << ':' << line.cropped << ':'; + // Leading padding locates the text: equal bytes at different columns cannot + // retain a version used by selection and Find. Only trailing padding is inert. + out << static_cast(line.ending) << ':' << line.wrap_columns << ':' << line.cropped << ':' << first << ':'; for (size_t index = first; index < last; ++index) { const auto& cell = line[index]; out << cell.text.size() << ':' << cell.text << cell.width << ',' << cell.style.fg << ',' diff --git a/experiments/core/candidate_terminal_test.cpp b/experiments/core/candidate_terminal_test.cpp index 8f1082c0..63efa4dc 100644 --- a/experiments/core/candidate_terminal_test.cpp +++ b/experiments/core/candidate_terminal_test.cpp @@ -417,11 +417,64 @@ void history_clear_does_not_resurrect() { } } +void replay_text_coordinates(int from, int to, bool during_replay, bool fragmented) { + termforge::CandidateTerminal terminal(2, 8, 4); + terminal.feed(from == 0 ? "H\r\nABC\r\nTAIL" : "H\r\n\x1b[3GABC\r\nTAIL"); + termforge::TextDocument initial(terminal.screen); + termforge::TextSelection selection; + require(selection.set(initial, {*initial.position(1, from), *initial.position(1, from + 3)}) && + selection.copy(initial, 64).text == "ABC", "shift fixture selection differs"); + termforge::TextSearch search; + const auto generation = search.start(initial, "ABC"); + for (int step = 0; step < 200 && search.status() == termforge::SearchStatus::running; ++step) + search.step(initial, generation, 64); + const auto before = search.match(initial, 0); + require(search.status() == termforge::SearchStatus::complete && search.matches().size() == 1 && + before && before->range.begin.column == static_cast(from) && + before->range.end.column == static_cast(from + 3), + "shift fixture Find coordinates differ"); + + terminal.resize(2, 10); + if (!during_replay) terminal.feed("\x1b[?25l\x1b[?25h"); + // Exercise both resize replay and subsequent output, in both directions. + // Keeping column 2 unchanged is the control for harmless trailing padding. + std::string bytes = "\x1b[1;1H\x1b[2K"; + bytes += to == 0 ? "ABC\r\n" : "\x1b[1;3HABC\r\n"; + if (during_replay) bytes = "\x1b[?25l" + bytes + "\x1b[?25h"; + if (fragmented) for (const char byte : bytes) terminal.feed(std::string_view(&byte, 1)); + else terminal.feed(bytes); + termforge::TextDocument shifted(terminal.screen); + require(shifted.line(1).projection.text == (to == 0 ? "ABC" : " ABC"), "shifted live text differs"); + if (from != to) { + require(selection.copy(shifted, 64).status == termforge::SelectionCopyStatus::invalidated, + "replay kept a selection at the old columns after moving its text"); + require(!search.match(shifted, 0), "replay kept a Find hit at the old columns after moving its text"); + } else { + const auto copy = selection.copy(shifted, 64); + require(copy.status == termforge::SelectionCopyStatus::ok && copy.text == "ABC", + "unchanged text coordinates invalidated a selection"); + } + + const auto changed = search.notify_change(shifted); + for (int step = 0; step < 200 && search.status() == termforge::SearchStatus::running; ++step) + search.step(shifted, changed, 64); + const auto after = search.match(shifted, 0); + require(search.status() == termforge::SearchStatus::complete && search.matches().size() == 1 && + after && after->range.begin.column == static_cast(to) && + after->range.end.column == static_cast(to + 3), + "Find did not converge to the moved text's actual columns"); +} + void conpty_resize_replay(const std::filesystem::path& path) { replay_case(path, false); replay_case(path, true); incomplete_replay_is_bounded(); history_clear_does_not_resurrect(); + for (const bool during_replay : {true, false}) for (const bool fragmented : {false, true}) { + replay_text_coordinates(0, 2, during_replay, fragmented); + replay_text_coordinates(2, 0, during_replay, fragmented); + replay_text_coordinates(2, 2, during_replay, fragmented); + } } void selection_search_and_viewport() { diff --git a/experiments/core/results/2026-10-27-replay-coordinates/README.md b/experiments/core/results/2026-10-27-replay-coordinates/README.md new file mode 100644 index 00000000..16f1b4cd --- /dev/null +++ b/experiments/core/results/2026-10-27-replay-coordinates/README.md @@ -0,0 +1,41 @@ +# resize 回放中的文字列位置 + +本记录修复 [#29](https://github.com/JavaZeroo/termforge/issues/29) 的一个候选适配层错误:相同文字移到其他列后,旧选区和 Find 命中仍可能被当作有效。发布核心源码为 `fbcbe3844b49d3151dea60c71d0aee066ead6283`,与已测本地提交 `f8692d4203bdcae9a362fc1dc6ace88567bf9511` 的 Git tree 完全相同;基点为 `8a13782aaac03780617e177b56ec55d3cb9c79be`。固定 Rust 候选仍为 `0.26.1-dev+termforge.4`,未修改上游、FFI 或默认核心。 + +## 原因与修复 + +回放完整行键及后续身份缓存比较均去掉首尾未写 padding,因而把第 0 列的 `ABC` 与第 2 列的 `ABC` 视为相同,保留了依赖坐标的旧版本。选择和 Find 可以继续返回旧坐标,而当前投影已是 ` ABC`。 + +两条匹配路径现在都保留首个内容列号;末尾未写 padding 仍不影响匹配。文字移位后原选区失效,旧 Find 命中失效,增量重扫取得正确列范围。这遵循已有选择/版本合同,没有采用新的 reflow 或原版差异策略。 + +## 独立反例与覆盖 + +用公开 `CandidateTerminal::feed/resize`、`TextSelection::copy` 和 `TextSearch::match/notify_change` 验证。2×8、history=4 输入 `H\r\nABC\r\nTAIL`,resize 到 2×10,再输入: + +```text +ESC[?25l ESC[1;1H ESC[2K ESC[1;3H ABC CR LF ESC[?25h +``` + +上行空格仅分隔控制序列;实际字节及独立预期写在 `candidate_terminal_test.cpp` 的 `replay_text_coordinates` 中。旧代码返回失败 `replay kept a selection at the old columns after moving its text`;修正后通过。另将缓存比较单独恢复旧逻辑,保留新行键,完整测试仍失败,证实缓存入口也需要修正。原始失败消息保留在本目录;首轮负例的完整构建产物未归档,不以它声明二进制身份。 + +矩阵有 12 个组合: + +- 列 `0→2`、`2→0`,以及 `2→2` 的不变对照。 +- 在 hide/show 包围的 resize 回放中,或完成回放后的普通输出中重画。 +- 整段 feed 或逐字节 feed。 + +移动用例要求旧选区和旧命中失效,Find 重扫精确命中 `[2,5)` 或 `[0,3)`;不变对照要求选择仍复制 `ABC`。原有固定 861B 回放、历史和其他候选检查保持执行,没有改预期、删除用例或放宽期限。 + +## 复验与证据边界 + +```powershell +cmake -S . -B build/candidate-adapter -A x64 -DTERMFORGE_BUILD_CANDIDATE_ADAPTER=ON +cmake --build build/candidate-adapter --config Release --target candidate_core_test +ctest --test-dir build/candidate-adapter -C Release -R '^candidate_core$' --output-on-failure +``` + +[validation.json](validation.json) 保存代码文件及二进制哈希、环境和该候选构建的完整 CTest 结果;运行时间仅为测试诊断。源改动经过独立 Standards/Spec 审查。 + +Windows 11 x64、MSVC 19.42、Rust 1.92.0 的首次完整候选 CTest 为 **48/49 通过、1 失败**。失败套件 `candidate_shell_tui` 的 6 个用例中,Vim 场景在 stage50 报 `'vim' is not recognized as an internal or external command`,其余 5 个用例通过。保持代码、二进制和期限,仅为测试进程的 PATH 加入已有 MSYS2 Vim 9.1(patches 1–532),单独复验该套件 **1/1 通过**,包含 6 个用例、每个场景两轮。首次失败与补齐依赖后的独立复验分别保留,不拼成一次 49/49 结果。 + +本次没有运行 WindTerm 对照或性能测量。冻结期间网格尺寸、补回历史行宽和回放期间显式清空的源码疑点由 #29 接续;未复现的疑点不写成已证实故障。真实子进程尺寸、备用屏/滚动区交叉、实验官方后端、真实不同 DPI、完整 R1 系统输入组合及完整 R2 仍需各自验收。这里的成功不等于完整 resize/reflow、WindTerm 等价或最终核心选型批准。 diff --git a/experiments/core/results/2026-10-27-replay-coordinates/green-matrix.txt b/experiments/core/results/2026-10-27-replay-coordinates/green-matrix.txt new file mode 100644 index 00000000..10c2db47 --- /dev/null +++ b/experiments/core/results/2026-10-27-replay-coordinates/green-matrix.txt @@ -0,0 +1 @@ +candidate terminal adapter checks passed diff --git a/experiments/core/results/2026-10-27-replay-coordinates/red-alias-control.txt b/experiments/core/results/2026-10-27-replay-coordinates/red-alias-control.txt new file mode 100644 index 00000000..7d2cc29e --- /dev/null +++ b/experiments/core/results/2026-10-27-replay-coordinates/red-alias-control.txt @@ -0,0 +1 @@ +replay kept a selection at the old columns after moving its text diff --git a/experiments/core/results/2026-10-27-replay-coordinates/red-selection.txt b/experiments/core/results/2026-10-27-replay-coordinates/red-selection.txt new file mode 100644 index 00000000..7d2cc29e --- /dev/null +++ b/experiments/core/results/2026-10-27-replay-coordinates/red-selection.txt @@ -0,0 +1 @@ +replay kept a selection at the old columns after moving its text diff --git a/experiments/core/results/2026-10-27-replay-coordinates/validation.json b/experiments/core/results/2026-10-27-replay-coordinates/validation.json new file mode 100644 index 00000000..c6da022c --- /dev/null +++ b/experiments/core/results/2026-10-27-replay-coordinates/validation.json @@ -0,0 +1,450 @@ +{ + "scope": "R2 #29 candidate adapter text coordinate regression; not complete resize acceptance", + "source_commit": "fbcbe3844b49d3151dea60c71d0aee066ead6283", + "reviewed_local_commit": "f8692d4203bdcae9a362fc1dc6ace88567bf9511", + "identical_source_tree": "258b3a6424cf834c37f0fcc3a6aa6f30c7085e96", + "base_commit": "8a13782aaac03780617e177b56ec55d3cb9c79be", + "source_files": [ + { + "path": "experiments/core/candidate_terminal.cpp", + "git_blob": "9904c6d23c0c7eebbe5f35d9311ed270cbbd7cfb", + "worktree_sha256": "da248c7751f51fa2fea7881c73383318b87c210381a34735a5137b4b2fcb1f29" + }, + { + "path": "experiments/core/candidate_terminal_test.cpp", + "git_blob": "63efa4dc10daee1e1a630b564b8e544c69d88438", + "worktree_sha256": "fb17722042567a25b81f46c9530d973deba235379d8f78f3d433f4ffc3e5cf55" + }, + { + "path": "experiments/core/candidate_terminal.hpp", + "git_blob": "55de3176ec220b2e404cc7e108f6d0063af3c004", + "worktree_sha256": "328411c1af6ddb509623ded8fe9c814cb6ac56cd502a2952e8cedd0a3bb2d997" + }, + { + "path": "experiments/core/screen.hpp", + "git_blob": "c05d37f131a170dfd47aa2d65c73795f2488669f", + "worktree_sha256": "fa80d3e67375b0cfacfe4ad640ecef0a47c36ca493499c0054ab5b3e7abf46c1" + }, + { + "path": "experiments/core/text_projection.hpp", + "git_blob": "fcef22e60e8ad61aec9b055088bca08dd3bb30d7", + "worktree_sha256": "3853bcf053964e54ea00bb7acfd0d302abd536ae0ed4eddfe791a6bcffdb6914" + }, + { + "path": "experiments/core/text_document.hpp", + "git_blob": "96465ee7eb94652378d5c4db8848bf879eff569a", + "worktree_sha256": "87efae9d6a9fccb88dd353cdd1be6a570480b6942fabbdf3c9dac2c6370e026a" + }, + { + "path": "experiments/core/text_selection.hpp", + "git_blob": "8af73a58512d79b67cd38eb2d5421c396677c42d", + "worktree_sha256": "7d41d58d434ad3c32ce2f67c043e337277abbbc69e4a7a2b6bdf7e519f9f5462" + }, + { + "path": "experiments/core/text_search.hpp", + "git_blob": "5e964dcc8bdfba2fb23671fa70401a6db9aeac14", + "worktree_sha256": "fcc64b96c1f8d39b3a09dc45ffe537e7dc0fd70727b4d46768706ab55467caac" + }, + { + "path": "experiments/candidates/alacritty/src/ffi.rs", + "git_blob": "4f56d33ffdca519f249bae95aa5d943c9b926427", + "worktree_sha256": "8c6fffa0559f0ab5f6e776bab03a35a1375b2b9f977c047da9bc62b8e5c30bfa" + }, + { + "path": "experiments/candidates/alacritty/include/termforge_candidate.h", + "git_blob": "9ab501e9cac70b18691fc5bfc6feacdf491f5133", + "worktree_sha256": "999c635ecd6d4482303d39d6548312afb3179edaeb17d14d4683688d83334ecc" + }, + { + "path": "experiments/candidates/alacritty/Cargo.lock", + "git_blob": "6501189a98bfe0610ebcb5e3c31d3abc2ae3f132", + "worktree_sha256": "896c6e60909d55d61fb26d277b1a98daf9245e8aee811caff37fc3e9d06ca627" + }, + { + "path": "CMakeLists.txt", + "git_blob": "e5b3e38afdb2edca99bba8b3fafc91e90cb19e28", + "worktree_sha256": "0763ec046daa5e932583012021a0058351844b28aecf314657ede1f4a2c64603" + } + ], + "configuration": { + "candidate": "0.26.1-dev+termforge.4", + "adapter": true, + "backend": "system ConPTY", + "build_type": "Release", + "os": "Windows 11 Pro 10.0.26200 x64", + "msvc": "19.42.34433.0", + "windows_sdk": "10.0.26100.0", + "rust": "1.92.0", + "python": "3.12.10" + }, + "binaries": [ + { + "path": "build/candidate-adapter/bin/Release/candidate_core_test.exe", + "sha256": "07c3a716d65ace8f5922ac9b91b3881a152edf83290cb26f94a7b6989564a9c1" + }, + { + "path": "build/candidate-adapter/bin/Release/termforge_shell.exe", + "sha256": "23daa6fb173ce5d483c07c52437fb7ef10b89d4f4c8875d3862e89a764cf2d7c" + }, + { + "path": "experiments/candidates/alacritty/target/release/termforge_alacritty_core.lib", + "sha256": "2278140a6f189fbdcb606242d83d103e227bd1ea129bd5b0d92c27e708c155b8" + } + ], + "negative_controls": [ + { + "name": "original implementation with initial single public-interface regression", + "exit_code": 1, + "output": "red-selection.txt", + "complete_binary_identity_retained": false + }, + { + "name": "new full replay key but old cached alias comparison", + "exit_code": 1, + "test_source_git_blob": "63efa4dc10daee1e1a630b564b8e544c69d88438", + "mutation": "same_replay_line guard restored to a_last - a_first != b_last - b_first", + "output": "red-alias-control.txt", + "complete_binary_identity_retained": false + } + ], + "public_interface_cases": { + "count": 12, + "columns": [ + [ + 0, + 2 + ], + [ + 2, + 0 + ], + [ + 2, + 2 + ] + ], + "stage": [ + "during resize replay", + "subsequent ordinary output" + ], + "feed": [ + "whole", + "one byte" + ], + "new_find_columns": [ + [ + 2, + 5 + ], + [ + 0, + 3 + ] + ], + "unchanged_control": "selection still copies ABC" + }, + "ctest": { + "command": "ctest --test-dir build/candidate-adapter -C Release --output-on-failure", + "total": 49, + "passed": 48, + "failed": 1, + "skipped": 0, + "tests": [ + { + "name": "shell_launch", + "status": "passed", + "elapsed_seconds_diagnostic_only": 0.0186761 + }, + { + "name": "candidate_title", + "status": "passed", + "elapsed_seconds_diagnostic_only": 0.977466 + }, + { + "name": "runtime_observation", + "status": "passed", + "elapsed_seconds_diagnostic_only": 2.22933 + }, + { + "name": "ime_input_expectation", + "status": "passed", + "elapsed_seconds_diagnostic_only": 0.0239317 + }, + { + "name": "sent_message_wait", + "status": "passed", + "elapsed_seconds_diagnostic_only": 0.302373 + }, + { + "name": "find_bar", + "status": "passed", + "elapsed_seconds_diagnostic_only": 0.392337 + }, + { + "name": "renderer_metrics", + "status": "passed", + "elapsed_seconds_diagnostic_only": 0.902099 + }, + { + "name": "ime_context", + "status": "passed", + "elapsed_seconds_diagnostic_only": 0.687326 + }, + { + "name": "layout_state", + "status": "passed", + "elapsed_seconds_diagnostic_only": 0.117694 + }, + { + "name": "workspace_state", + "status": "passed", + "elapsed_seconds_diagnostic_only": 0.28399 + }, + { + "name": "transport_completion", + "status": "passed", + "elapsed_seconds_diagnostic_only": 0.0129886 + }, + { + "name": "scenario_tick_gate", + "status": "passed", + "elapsed_seconds_diagnostic_only": 0.0127553 + }, + { + "name": "candidate_core", + "status": "passed", + "elapsed_seconds_diagnostic_only": 1.47503 + }, + { + "name": "resize_core", + "status": "passed", + "elapsed_seconds_diagnostic_only": 0.0135294 + }, + { + "name": "history_core", + "status": "passed", + "elapsed_seconds_diagnostic_only": 0.0161851 + }, + { + "name": "selection_core", + "status": "passed", + "elapsed_seconds_diagnostic_only": 0.0744326 + }, + { + "name": "search_core", + "status": "passed", + "elapsed_seconds_diagnostic_only": 0.111531 + }, + { + "name": "paste", + "status": "passed", + "elapsed_seconds_diagnostic_only": 0.0164571 + }, + { + "name": "clipboard_policy", + "status": "passed", + "elapsed_seconds_diagnostic_only": 0.0135761 + }, + { + "name": "keyboard", + "status": "passed", + "elapsed_seconds_diagnostic_only": 0.0154536 + }, + { + "name": "text_action_policy", + "status": "passed", + "elapsed_seconds_diagnostic_only": 0.0134497 + }, + { + "name": "candidate_shell_tui", + "status": "failed", + "elapsed_seconds_diagnostic_only": 31.5453 + }, + { + "name": "core", + "status": "passed", + "elapsed_seconds_diagnostic_only": 4.66261 + }, + { + "name": "recovery", + "status": "passed", + "elapsed_seconds_diagnostic_only": 8.69672 + }, + { + "name": "measure", + "status": "passed", + "elapsed_seconds_diagnostic_only": 4.07593 + }, + { + "name": "binary_paths", + "status": "passed", + "elapsed_seconds_diagnostic_only": 0.164067 + }, + { + "name": "conpty_margin_contract", + "status": "passed", + "elapsed_seconds_diagnostic_only": 0.31854 + }, + { + "name": "benchmark_tools", + "status": "passed", + "elapsed_seconds_diagnostic_only": 5.40389 + }, + { + "name": "differential", + "status": "passed", + "elapsed_seconds_diagnostic_only": 2.53997 + }, + { + "name": "ime_inventory", + "status": "passed", + "elapsed_seconds_diagnostic_only": 50.6506 + }, + { + "name": "text_actions", + "status": "passed", + "elapsed_seconds_diagnostic_only": 7.25059 + }, + { + "name": "workflow_fixture", + "status": "passed", + "elapsed_seconds_diagnostic_only": 22.5549 + }, + { + "name": "workflow_sessions", + "status": "passed", + "elapsed_seconds_diagnostic_only": 40.8189 + }, + { + "name": "workflow_resources", + "status": "passed", + "elapsed_seconds_diagnostic_only": 0.456499 + }, + { + "name": "workflow_resource_phases", + "status": "passed", + "elapsed_seconds_diagnostic_only": 4.23949 + }, + { + "name": "workflow_system_content", + "status": "passed", + "elapsed_seconds_diagnostic_only": 0.154572 + }, + { + "name": "workflow_system_runner", + "status": "passed", + "elapsed_seconds_diagnostic_only": 0.631644 + }, + { + "name": "copy_error_frame", + "status": "passed", + "elapsed_seconds_diagnostic_only": 0.120771 + }, + { + "name": "research_guards", + "status": "passed", + "elapsed_seconds_diagnostic_only": 4.92514 + }, + { + "name": "receive", + "status": "passed", + "elapsed_seconds_diagnostic_only": 0.180502 + }, + { + "name": "conpty", + "status": "passed", + "elapsed_seconds_diagnostic_only": 16.5922 + }, + { + "name": "conpty_ctrl_c", + "status": "passed", + "elapsed_seconds_diagnostic_only": 4.37408 + }, + { + "name": "conpty_margin", + "status": "passed", + "elapsed_seconds_diagnostic_only": 0.359594 + }, + { + "name": "presentation", + "status": "passed", + "elapsed_seconds_diagnostic_only": 3.95394 + }, + { + "name": "shell", + "status": "passed", + "elapsed_seconds_diagnostic_only": 84.8579 + }, + { + "name": "selection_bytes", + "status": "passed", + "elapsed_seconds_diagnostic_only": 3.64536 + }, + { + "name": "selection_context", + "status": "passed", + "elapsed_seconds_diagnostic_only": 3.60959 + }, + { + "name": "raw_console_receiver", + "status": "passed", + "elapsed_seconds_diagnostic_only": 0.370588 + }, + { + "name": "paste_system", + "status": "passed", + "elapsed_seconds_diagnostic_only": 5.12561 + } + ], + "failure": { + "name": "candidate_shell_tui", + "python_tests": 6, + "passed": 5, + "errors": 1, + "scenario": "candidate-tui", + "stage": 50, + "observed_message": "'vim' is not recognized as an internal or external command", + "classification": "missing Vim in test process PATH; editor did not start" + } + }, + "public_logs": [ + { + "path": "red-selection.txt", + "sha256": "ca8c825916bc1b45dc3084dea80c4c5ca7f65448c799896548dc72824fd02928", + "raw_local_sha256": "79d536375b53df703428075ef00a0fb162d18ea26c0ea8ab995bce85d13192d2", + "normalization": "UTF-8 LF" + }, + { + "path": "red-alias-control.txt", + "sha256": "ca8c825916bc1b45dc3084dea80c4c5ca7f65448c799896548dc72824fd02928", + "raw_local_sha256": "79d536375b53df703428075ef00a0fb162d18ea26c0ea8ab995bce85d13192d2", + "normalization": "UTF-8 LF" + }, + { + "path": "green-matrix.txt", + "sha256": "caa3a5ad900c2d7cc0db5bcef2dfc69f45c6b326703df025f09701c4ebb04c05", + "raw_local_sha256": "f249f16080e7539f36f1dc3103a9b03e5366fbdc5ef7166c21bea21f1a334a71", + "normalization": "UTF-8 LF" + } + ], + "limitations": [ + "No WindTerm runtime comparison or performance measurement", + "No complete resize, history-clear-during-replay or pending-grid-dimension acceptance", + "No experimental official ConPTY backend or actual distinct-DPI acceptance", + "No full R1 system clipboard/IME combination or R2 acceptance", + "CTest times are diagnostics, not performance results" + ], + "focused_retry": { + "command": "ctest --test-dir build/candidate-adapter -C Release -R '^candidate_shell_tui$' --output-on-failure", + "change": "prepend existing Git for Windows usr/bin to test process PATH only", + "vim_version": "MSYS2 Vim 9.1 patches 1-532, compiled 2024-07-09", + "vim_exe_sha256": "2e0fda63a89171adfc46c9417d5105cc39ed778cc69a890b83a4eabc34e7646a", + "same_source_and_binaries": true, + "ctest_total": 1, + "ctest_passed": 1, + "python_tests": 6, + "repeats_per_scenario": 2, + "elapsed_seconds_diagnostic_only": 7.84349, + "does_not_replace_original_full_run": true + } +}