diff --git a/CMakeLists.txt b/CMakeLists.txt index 02c2f82d..f504d187 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -128,6 +128,10 @@ add_executable(decom_origin_test experiments/core/decom_origin_test.cpp) target_link_libraries(decom_origin_test PRIVATE termforge_core) termforge_warnings(decom_origin_test) +add_executable(pending_wrap_test experiments/core/pending_wrap_test.cpp) +target_link_libraries(pending_wrap_test PRIVATE termforge_core) +termforge_warnings(pending_wrap_test) + add_executable(view_mapping_test experiments/core/view_mapping_test.cpp) target_link_libraries(view_mapping_test PRIVATE termforge_core) termforge_warnings(view_mapping_test) @@ -352,6 +356,7 @@ add_test(NAME view_mapping_core COMMAND view_mapping_test) add_test(NAME decawm_mode_core COMMAND decawm_mode_test) add_test(NAME private_mode_core COMMAND private_mode_test) add_test(NAME decom_origin_core COMMAND decom_origin_test) +add_test(NAME pending_wrap_core COMMAND pending_wrap_test) add_test(NAME reply_core COMMAND reply_test) add_test(NAME edit_csi_core COMMAND edit_csi_test) set_tests_properties(decawm_mode_core PROPERTIES TIMEOUT 10) diff --git a/docs/EVIDENCE.md b/docs/EVIDENCE.md index 7ef8fa6f..bfb75239 100644 --- a/docs/EVIDENCE.md +++ b/docs/EVIDENCE.md @@ -14,6 +14,7 @@ | hosted Windows 子进程启动耗时 | [固定结果](../research/ui/results/2026-10-30-hosted-startup-latency/README.md)、[验证身份](../research/ui/results/2026-10-30-hosted-startup-latency/validation.json):windows-2025 上 100 次 PowerShell 5.1 启动 p50 110 ms、max 1.83 s,50 次 node p50 16 ms;8 路 CPU 负载下 p50 172 ms。用于排除 #81/#144/#40 的 10–12 秒超时由启动成本解释,不等于性能预算 | | 自主 C++ 主屏 ASCII DECAWM | [有限合同与红绿](../experiments/core/results/2026-10-29-cpp-decawm/README.md)、[固定验证身份](../experiments/core/results/2026-10-29-cpp-decawm/validation.json):抽取原版14–18阶段79B,strict/recovery各235个字面前缀分块用例;初次定向8/9的缺构建依赖失败、补建后的core单项31测试通过分列。跨1049/宽字/resize、origin/margin、GUI/固定ConPTY运输和完整TUI均不由本项验收 | | 自主 C++ DECOM origin 与 margin/1049(#27 切片) | [有限合同与验证](../experiments/core/results/2026-11-01-decom-origin/README.md)、[固定验证身份](../experiments/core/results/2026-11-01-decom-origin/validation.json):原版25阶段334B完整回放、逐阶段DSR核对;strict/recovery各整体/逐字节/435二段分割一致;13项核心CTest与66项Python核心测试通过、0跳过。每屏origin位、返回margin下界、origin×末列wrap与完整grid均不由此项验收 | +| 自主 C++ pending-wrap:延迟换行边缘跨 1049/DECAWM/DECOM(#27 切片) | [有限合同与验证](../experiments/core/results/2026-11-02-pending-wrap/README.md)、[固定验证身份](../experiments/core/results/2026-11-02-pending-wrap/validation.json):以 #123 备用屏阶段 8–9 与 #151 阶段 14–18 为字面 oracle,锁末列延迟换行在 1049 进出/重复切换下的保存恢复、DECAWM开/关 × DECOM开/关的 wrap 决策/光标落点/下一可打印字符起始;strict/recovery 各 7 阶段累计回放、逐字节与 151 个二段分割一致,异常序列恢复不串扰。origin×末列 wrap 为组合模型检查(原版未观测);16 项核心 CTest 与 66 项 Python 核心测试通过、0 跳过;实现与基线无冲突(仅测试自断行号一处修正)。宽字/结合字跨 1049、完整 grid/样式与固定 ConPTY 运输均不由此项验收 | | 自主 C++ 行结构发布与精确身份查询 | [首片合同与固定夹具](../experiments/core/results/2026-10-29-order-query/README.md)、[验证身份](../experiments/core/results/2026-10-29-order-query/validation.json):`[1,3,4,5,7,6]` 的精确 rank(7→4、6→5、2 缺失)、文档序比较与强制 Find 导航、结构 revision 边界、发布拒绝与光标夹取;筛选的 8 项核心 CTest 与本地候选 `candidate_core` 通过,首个推送版本的 CI 红(回放保留历史仍为上一帧宽度)与按物理行策略适配后的绿分列,完整默认 Release 构建 exit0。未封住公开 lines/history,运行中跨重排的增量 Find、区域 LF、Qt/固定 ConPTY 与性能均未验收 | | 自主 C++ 限定 1049 协议 | [固定结果](../experiments/core/results/2026-10-29-cpp-1049/README.md)、[验证身份](../experiments/core/results/2026-10-29-cpp-1049/validation.json):文字/光标/颜色及 pending-wrap、重复进入与读者失效;55项中Qt1项分列,13条内部skip及旧负例判据失败保留。未验收非颜色属性/全网格背景、其他模式、真实TUI/IME/DPI、呈现或性能 | | 自主 C++ 双缓冲存储与文档 epoch 基础 | [固定结果](../experiments/core/results/2026-10-29-buffer-state-r2/README.md)、[验证身份](../experiments/core/results/2026-10-29-buffer-state-r2/validation.json):拥有状态、共享 ID、隐藏帧 resize 回滚与跨缓冲句柄失效;Qt/candidate 关闭配置的完整 50 项及 12 条内部 skip 分列。未接 private CSI,不是原版模式、Qt 产品路线或完整 #27 验收 | diff --git a/experiments/core/pending_wrap_test.cpp b/experiments/core/pending_wrap_test.cpp new file mode 100644 index 00000000..634e4a46 --- /dev/null +++ b/experiments/core/pending_wrap_test.cpp @@ -0,0 +1,335 @@ + +// Bounded-slice literal oracle for issue #27 pending-wrap: the delayed edge +// left after a last-column printable, crossed with DECAWM, DECOM and the 1049 +// buffer switch. Contract baseline is the existing core (screen.hpp wrap_pending +// / FrameState save-restore, terminal.hpp DECAWM/DECOM/1049 dispatch). Literal +// expectations come from two fixed original observations, none authored by this +// parser: +// * #123 alternate-style phases 8-9: (3,91) 'W' -> 1049h -> alt -> 1049l -> +// 'X' replies (4,2); independent control (6,91) 'VZ' replies (7,2). +// * #151 phases 14-18: the five last-column DECAWM on/off edge sequences and +// their bare DSR replies. +// DECOM-on x last-column-wrap cells are composed model checks: #151 observed the +// wrap phases with origin off, so the original marks that crossing unobserved. +// Both strict and recovery modes replay the literal trace; recovery additionally +// consumes abnormal sequences without contaminating the pending edge, while +// strict rejects unsupported private forms atomically. +#include "terminal.hpp" +#include +#include +#include +#include +#include +#include + +using namespace termforge; + +static void check(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +static std::string line_text(const Line& line) { + std::string value; + for (const auto& cell : line) value += cell.text; + return value; +} + +// Canonical state signature for cross-chunk equality only, never a contract +// oracle. Captures cursor, pending edge, discarded count and per-row text plus +// wrap metadata so a chunking difference cannot hide behind equal replies. +static std::string signature(const Screen& s) { + std::ostringstream out; + out << s.row << ',' << s.column << ',' << (s.wrap_pending ? 1 : 0) << ',' << s.discarded << ';'; + const auto emit = [&](const Line& line) { + out << line.id << ':' << static_cast(line.ending) << ':' << line.next_id << ':' + << line.wrap_columns << '|' << line_text(line) << ';'; + }; + for (const auto& line : s.history_lines()) emit(line); + out << '#'; + for (const auto& line : s.visible_lines()) emit(line); + return out.str(); +} + +// Literal cumulative trace: the five #151 DECAWM edge phases plus the #123 +// pending-wrap 1049 roundtrip and its control. Replies are the original bare +// DSR 1-based coordinates; each phase depends on all earlier phases. +struct Phase { + const char* label; + const char* source; + const char* reply; // "ESC[row;colR" +}; + +static const std::vector phases{ + {"wrap-enabled-edge", "\x1b[?7h\x1b[8;91HAB", "\x1b[9;2R"}, + {"wrap-disabled-edge", "\x1b[?7l\x1b[10;91HCD", "\x1b[10;91R"}, + {"wrap-enabled-again-edge", "\x1b[?7h\x1b[12;91HEF", "\x1b[13;2R"}, + {"pending-edge-disable-before-next", "\x1b[14;91HG\x1b[?7lH", "\x1b[14;91R"}, + {"disabled-edge-enable-before-next", "\x1b[?7l\x1b[16;91HI\x1b[?7hJ", "\x1b[17;2R"}, + {"alternate-pending-wrap-roundtrip", "\x1b[?7h\x1b[3;91HW\x1b[?1049hTFA4\x1b[?1049lX", "\x1b[4;2R"}, + {"pending-wrap-control", "\x1b[6;91HVZ", "\x1b[7;2R"}, +}; + +static std::string full_source() { + std::string source; + for (const auto& phase : phases) { source += phase.source; source += "\x1b[6n"; } + return source; +} + +static std::vector expected_replies() { + std::vector replies; + replies.reserve(phases.size()); + for (const auto& phase : phases) replies.emplace_back(phase.reply); + return replies; +} + +// Cumulative feed; after each phase a DSR request whose reply must equal the +// literal reply the original reported at that phase. +static void literal_replay(ParseMode mode) { + Parser parser(25, 91, 100, mode); + for (const auto& phase : phases) { + parser.feed(phase.source); + parser.feed("\x1b[6n"); + const auto replies = parser.take_replies(); + if (replies.size() != 1 || replies[0] != phase.reply) + throw std::runtime_error(std::string("phase ") + phase.label + " reply mismatch"); + } + parser.finish(); + check(parser.screen.active_buffer() == ScreenBuffer::primary && parser.screen.row == 6 && + parser.screen.column == 1 && !parser.screen.wrap_pending && parser.screen.history_lines().empty() && + parser.screen.discarded == 0, + "literal trace ended outside the final pending-wrap control state"); + // The 1049 roundtrip must have preserved the real continuation: 'W' keeps a + // soft-wrap link to the row that 'X' started. + check(line_text(parser.screen.visible_lines()[2]) == std::string(90, ' ') + "W" && + line_text(parser.screen.visible_lines()[3]).substr(0, 1) == "X" && + parser.screen.visible_lines()[2].ending == LineEnding::soft_wrap && + parser.screen.visible_lines()[2].next_id == parser.screen.visible_lines()[3].id && + parser.screen.visible_lines()[2].wrap_columns == 91u, + "1049 roundtrip lost the literal pending-wrap continuation"); +} + +// The same seven replies must not depend on how the byte stream is chunked, +// including the last-column character landing in a packet by itself. +static void chunk_invariance(ParseMode mode, size_t* cases) { + const std::string source = full_source(); + const auto expected = expected_replies(); + std::string single; + { + Parser parser(25, 91, 100, mode); + parser.feed(source); + check(parser.take_replies() == expected, "single-shot replies differ from the literal trace"); + parser.finish(); + single = signature(parser.screen); + } + { + Parser parser(25, 91, 100, mode); + for (const auto byte : source) parser.feed(std::string_view(&byte, 1)); + check(parser.take_replies() == expected, "byte-wise replies differ from the literal trace"); + parser.finish(); + check(signature(parser.screen) == single, "byte-wise state differs from single-shot"); + } + for (size_t split = 0; split <= source.size(); ++split) { + Parser parser(25, 91, 100, mode); + parser.feed(std::string_view(source).substr(0, split)); + parser.feed(std::string_view(source).substr(split)); + check(parser.take_replies() == expected, "two-way split replies differ from the literal trace"); + parser.finish(); + check(signature(parser.screen) == single, "two-way split state differs from single-shot"); + ++*cases; + } + // A last-column byte split in isolation: 'W' at (3,91) then 'X' after the + // 1049 roundtrip must still wrap regardless of which feed carries 'W'. + for (size_t cut = 0; cut <= 1; ++cut) { + Parser parser(25, 91, 100, mode); + const std::string arm = "\x1b[3;91HW"; + parser.feed(std::string_view(arm).substr(0, cut)); + parser.feed(std::string_view(arm).substr(cut)); + parser.feed("\x1b[?1049hTFA4\x1b[?1049lX\x1b[6n"); + check(parser.take_replies() == std::vector{"\x1b[4;2R"}, + "split last-column arm changed the pending-wrap reply"); + parser.finish(); + } +} + +// #123 phase 8-9: pending wrap armed in primary survives a 1049 roundtrip; the +// next primary printable continues onto the successor row. Repeated switching +// is a composed model check, not a literal original phase. +static void pending_wrap_1049_roundtrip(ParseMode mode) { + Parser parser(25, 91, 2, mode); + parser.feed("\x1b[3;91HW"); + check(parser.screen.wrap_pending && parser.screen.row == 2 && parser.screen.column == 90, + "fixture did not arm the primary pending wrap"); + parser.feed("\x1b[?1049h"); + check(parser.screen.active_buffer() == ScreenBuffer::alternate && !parser.screen.wrap_pending, + "alternate entry retained the primary pending edge"); + parser.feed("TFA4\x1b[?1049l"); + check(parser.screen.active_buffer() == ScreenBuffer::primary && parser.screen.wrap_pending && + parser.screen.row == 2 && parser.screen.column == 90, + "first 1049 exit lost the saved primary pending wrap"); + // Re-enter and re-exit without consuming the edge; it must still be armed. + parser.feed("\x1b[?1049hALT2\x1b[?1049l"); + check(parser.screen.wrap_pending && parser.screen.row == 2 && parser.screen.column == 90, + "repeated 1049 switching dropped the primary pending wrap"); + parser.feed("X\x1b[6n"); + check(parser.take_replies() == std::vector{"\x1b[4;2R"}, + "restored pending wrap did not place the next printable on the successor row"); + check(parser.screen.row == 3 && parser.screen.column == 1 && !parser.screen.wrap_pending && + line_text(parser.screen.visible_lines()[2]) == std::string(90, ' ') + "W" && + line_text(parser.screen.visible_lines()[3]).substr(0, 1) == "X" && + parser.screen.visible_lines()[2].ending == LineEnding::soft_wrap && + parser.screen.visible_lines()[2].next_id == parser.screen.visible_lines()[3].id, + "repeated-switch pending wrap lost the real continuation"); + parser.finish(); +} + +// DECAWM on x origin off: literal #151 phases 14/18. A last-column write arms +// the delayed edge; the next printable wraps to the next row and starts there. +static void decawm_on_edge_wraps(ParseMode mode) { + Parser parser(3, 4, 2, mode); + parser.feed("\x1b[?7h\x1b[1;4HAB"); + check(parser.screen.row == 1 && parser.screen.column == 1 && !parser.screen.wrap_pending && + line_text(parser.screen.visible_lines()[0]) == " A" && + line_text(parser.screen.visible_lines()[1]).substr(0, 1) == "B" && + parser.screen.visible_lines()[0].ending == LineEnding::soft_wrap && + parser.screen.visible_lines()[0].next_id == parser.screen.visible_lines()[1].id, + "autowrap on did not wrap the delayed edge to the next row"); +} + +// DECAWM off x origin off: literal #151 phases 15-17. The last-column write +// stays put (cursor at the last column) and the next printable overwrites it. +static void decawm_off_edge_stays(ParseMode mode) { + Parser parser(3, 4, 2, mode); + parser.feed("\x1b[?7l\x1b[1;4HCD"); + check(parser.screen.row == 0 && parser.screen.column == 3 && parser.screen.wrap_pending && + line_text(parser.screen.visible_lines()[0]) == " D", + "autowrap off did not keep the last-column overwrite"); + // Disable-after-arm (#151 phase 17): armed edge stays but autowrap off + // gates consuming it, so the next printable overwrites in place. + Parser armed(3, 4, 2, mode); + armed.feed("\x1b[1;4HG\x1b[?7lH"); + check(armed.screen.row == 0 && armed.screen.column == 3 && + line_text(armed.screen.visible_lines()[0]) == " H", + "armed edge consumed despite autowrap being off"); + // Enable-after-arm (#151 phase 18): enabling autowrap lets the next + // printable start the continuation. + Parser rearm(3, 4, 2, mode); + rearm.feed("\x1b[?7l\x1b[1;4HI\x1b[?7hJ"); + check(rearm.screen.row == 1 && rearm.screen.column == 1 && + line_text(rearm.screen.visible_lines()[0]) == " I" && + line_text(rearm.screen.visible_lines()[1]).substr(0, 1) == "J" && + rearm.screen.visible_lines()[0].ending == LineEnding::soft_wrap, + "enabling autowrap did not start the delayed continuation"); +} + +// DECAWM on x DECOM on: composed model check. #151 observed the wrap phases +// with origin off, so this crossing is not a literal original phase. The wrap +// must stay region-bounded and the DSR stays origin-relative. +static void origin_on_edge_wraps_in_region(ParseMode mode) { + Parser parser(5, 5, 2, mode); + parser.feed("\x1b[2;4r\x1b[?6h\x1b[1;5HXY"); + check(parser.screen.origin_mode() && parser.screen.row == 2 && parser.screen.column == 1 && + !parser.screen.wrap_pending, + "origin wrap did not land on the next region row"); + parser.feed("\x1b[6n"); + check(parser.take_replies() == std::vector{"\x1b[2;2R"}, + "origin wrap reported an absolute rather than relative reply"); + parser.feed("Q"); + check(parser.screen.row == 2 && parser.screen.column == 2 && + line_text(parser.screen.visible_lines()[2]).substr(0, 3) == "YQ ", + "origin wrap misplaced the next printable"); + parser.finish(); +} + +// DECAWM off x DECOM on: composed model check, unobserved in the original. The +// last-column write stays on the region edge and the next printable overwrites. +static void origin_on_edge_stays(ParseMode mode) { + Parser parser(5, 5, 2, mode); + parser.feed("\x1b[2;4r\x1b[?6h\x1b[?7l\x1b[1;5HAB"); + check(parser.screen.row == 1 && parser.screen.column == 4 && parser.screen.wrap_pending && + line_text(parser.screen.visible_lines()[1]).substr(4) == "B", + "origin x autowrap-off did not keep the last-column overwrite"); + parser.feed("\x1b[6n"); + check(parser.take_replies() == std::vector{"\x1b[1;5R"}, + "origin x autowrap-off reported the wrong relative reply"); + parser.finish(); +} + +// Recovery consumes abnormal sequences without touching the armed edge or +// contaminating the next printable; strict rejects unsupported forms atomically. +static void abnormal_sequences_keep_pending_edge() { + // CAN/SUB/ESC resynchronization mid-private-mode: the armed edge and cursor + // survive, then 'X' still wraps onto the successor row. + for (const auto cancellation : {char(0x18), char(0x1a), char(0x1b)}) { + Parser parser(25, 91, 2, ParseMode::recovery); + parser.feed("\x1b[3;91HW"); + parser.feed(std::string("\x1b[?1049") + cancellation); + parser.feed("\x1b[6n"); + check(parser.take_replies() == std::vector{"\x1b[3;91R"} && + parser.screen.wrap_pending && parser.screen.row == 2 && parser.screen.column == 90, + "abnormal private prefix moved the cursor or cleared the pending edge"); + parser.feed("X"); parser.finish(); + check(parser.screen.row == 3 && parser.screen.column == 1 && + line_text(parser.screen.visible_lines()[3]).substr(0, 1) == "X", + "abnormal private prefix contaminated the next printable"); + } + // EOF truncation resets the recovery sequence without clearing the edge. + Parser truncated(25, 91, 2, ParseMode::recovery); + truncated.feed("\x1b[3;91HW"); + truncated.feed("\x1b[?1049"); + truncated.finish(); + check(truncated.diagnostics().truncated_sequences == 1 && truncated.screen.wrap_pending && + truncated.screen.row == 2 && truncated.screen.column == 90, + "EOF truncation cleared the pending edge"); + truncated.feed("X"); truncated.finish(); + check(truncated.screen.row == 3 && truncated.screen.column == 1 && + line_text(truncated.screen.visible_lines()[3]).substr(0, 1) == "X", + "truncated private input contaminated the next printable"); + // Unsupported private form is ignored without clearing the edge. + Parser ignored(25, 91, 2, ParseMode::recovery); + ignored.feed("\x1b[3;91HW"); + ignored.feed("\x1b[?1049;25h"); + check(ignored.diagnostics().ignored_sequences == 1 && ignored.screen.wrap_pending && + ignored.screen.row == 2 && ignored.screen.column == 90 && + ignored.screen.active_buffer() == ScreenBuffer::primary, + "unsupported private form mutated a buffer or cleared the pending edge"); + ignored.feed("X"); ignored.finish(); + check(ignored.screen.row == 3 && ignored.screen.column == 1 && + line_text(ignored.screen.visible_lines()[3]).substr(0, 1) == "X", + "unsupported private form contaminated the next printable"); + // Strict rejects unsupported private forms atomically, leaving the armed + // edge and cursor untouched (the parser is left mid-CSI, so no continuation). + Parser strict(25, 91, 2); + strict.feed("\x1b[3;91HW"); + bool rejected = false; + try { strict.feed("\x1b[?1049;25h"); } + catch (const InputError&) { rejected = true; } + check(rejected && strict.screen.wrap_pending && strict.screen.row == 2 && + strict.screen.column == 90 && strict.screen.active_buffer() == ScreenBuffer::primary, + "strict accepted an unsupported private form or partially applied its modes"); +} + +int main() { + int failures = 0, checks = 0; + const auto run = [&](const std::string& name, const auto& action) { + ++checks; + try { action(); std::cout << name << " passed\n"; } + catch (const std::exception& error) { + ++failures; std::cerr << name << " failed: " << error.what() << '\n'; + } + }; + for (const auto mode : {ParseMode::strict, ParseMode::recovery}) { + const std::string name = mode == ParseMode::strict ? "strict" : "recovery"; + run(name + " literal replay", [&] { literal_replay(mode); }); + run(name + " 1049 pending-wrap roundtrip", [&] { pending_wrap_1049_roundtrip(mode); }); + run(name + " decawm on edge", [&] { decawm_on_edge_wraps(mode); }); + run(name + " decawm off edge", [&] { decawm_off_edge_stays(mode); }); + run(name + " origin on edge", [&] { origin_on_edge_wraps_in_region(mode); }); + run(name + " origin off edge", [&] { origin_on_edge_stays(mode); }); + size_t cases = 0; + run(name + " chunk invariance", [&] { chunk_invariance(mode, &cases); }); + std::cout << " " << name << " chunk cases: " << cases << "\n"; + } + run("recovery abnormal sequences", abnormal_sequences_keep_pending_edge); + std::cout << "pending-wrap checks: " << (checks - failures) << '/' << checks << " passed\n"; + return failures ? 1 : 0; +} diff --git a/experiments/core/results/2026-11-02-pending-wrap/README.md b/experiments/core/results/2026-11-02-pending-wrap/README.md new file mode 100644 index 00000000..ffdced77 --- /dev/null +++ b/experiments/core/results/2026-11-02-pending-wrap/README.md @@ -0,0 +1,39 @@ +# 自主 C++ pending-wrap:延迟换行边缘跨 1049/DECAWM/DECOM(#27 限定切片) + +自主 C++ strict/recovery 锁定末列写入后的延迟换行(pending-wrap)边缘态:1049 进出时的保存恢复、与 DECAWM 开/关和 DECOM 开/关的组合、跨包/逐字节喂入一致、异常序列恢复不串扰。依据是两份固定的原版观察,没有新增原版运行: + +- #123 备用屏样式观察的阶段 8–9:(3,91) 写 W 后 1049h 进备用屏、1049l 退出后写 X 得到 DSR (4,2);独立对照 (6,91) 写 VZ 得到 (7,2)。见 [备用屏样式参考](../../../../docs/research/windterm-alternate-style-reference-2026-10-28.md)。 +- #151 origin/wrap 观察的阶段 14–18:末列 DECAWM 开/关五条序列与逐条 bare DSR。见 [origin/wrap 参考](../../../../docs/research/windterm-origin-wrap-reference-2026-10-29.md)。 + +## 固定输入与范围 + +[测试](../../pending_wrap_test.cpp) 通过公开 Parser 的 feed/finish/take_replies 接口锁定: + +- 七阶段累计字面回放:阶段 14–18 的 DECAWM 边缘 + 1049 pending-wrap 往返 + VZ 对照,每阶段后追加 ESC[6n 并核对字面 DSR。 +- pending-wrap × 1049:进入清掉备用屏可见 pending,退出恢复主屏 pending;(3,91)W 跨 1049 往返后 X 落到 (4,2),重复切换不丢;W 行保留软续行与下一行身份链接。 +- pending-wrap × DECAWM 开:末列写 AB 后 A 武装延迟边、B 换到下一行;关:末列写 CD 只留 D、关后再 H 原地覆盖、开后再 J 换行(阶段 14–18)。 +- pending-wrap × DECOM:开时末列写 XY 换到区域内下一行、DSR 行相对区域顶;关时末列写 CD 原地覆盖。DECOM 开通的两格是组合模型检查,不是字面 oracle 阶段(#151 的 wrap 阶段均在 origin 关闭下观察)。 +- 分块/逐字节一致:对完整含 DSR 的累计源做整体、逐字节、151 个二段分割,回复与状态一致;另把末列 W 单独拆到不同包验证仍得到 (4,2)。 +- 异常序列:recovery 下 CAN/SUB/ESC 重同步、EOF 截断、不支持私有形式均不移动光标也不清 pending;strict 对不支持私有形式原子拒绝。恢复后继续写 X 仍换行。 + +## 设计边界(区分实测/推断/未验证) + +- 实测:pending-wrap 在 1049 退出后保留(#123 阶段 8–9 的 (4,2) 与 VZ 对照 (7,2));DECAWM 开/关的五条末列定式(#151 阶段 14–18)。 +- 推断(组合模型):DECOM 开 × 末列 wrap 的区域界内换行与相对 DSR;DECOM 开 × DECAWM 关的原地覆盖。原版对此两格未观测,不计为原版匹配。 +- 未验证:宽字/结合字跨 1049、完整 active grid 与逐格样式、每屏 origin 位、固定 ConPTY 运输与真实 TUI。 + +## 红绿与实际验证 + +[验证记录](validation.json) 保留源码、二进制 SHA、配置与通过/失败/跳过;最终二进制在定向构建与测试结束后取哈希,不声称全程连续监控。 + +- 实现源/测试合同 3a8fc1a:MSVC 19.42.34433.0、Release,Qt/candidate 关闭。定向构建成功;16 项核心 C++ CTest 全绿(15 项既有 + 新 pending_wrap_core 15/15 checks),0 跳过。 +- Python experiments.core.test_core/test_unicode/test_recovery 66 项通过(补建 receive 依赖)。 +- 唯一中途红是测试自身对累计回放末态光标的断言行差一(row 5 与 row 6),修正后 15/15;生产实现代码无改动,与合同基线无冲突。 + +复验只需普通 C++ 构建,无新增依赖: + + cmake -S . -B build/pending-wrap -G 'Visual Studio 17 2022' -A x64 -DTERMFORGE_BUILD_QT_SHELL=OFF -DTERMFORGE_BUILD_CANDIDATE_ADAPTER=OFF + cmake --build build/pending-wrap --config Release --target pending_wrap_test decawm_mode_test alternate_mode_test decom_origin_test buffer_state_test history_test resize_test resize_edges_test selection_test search_test search_reorder_test reply_test edit_csi_test order_query_test view_mapping_test private_mode_test replay terminal_trace recovery_probe receive --parallel 2 + ctest --test-dir build/pending-wrap -C Release -R '^(resize_core|resize_edges_core|history_core|selection_core|search_core|search_reorder_core|buffer_state_core|view_mapping_core|alternate_mode_core|decawm_mode_core|private_mode_core|decom_origin_core|reply_core|edit_csi_core|order_query_core|pending_wrap_core)$' + +这里只声明有限 core feed 能力;Qt、原生 GUI、固定 ConPTY 路径、真实 TUI、呈现与性能均未由本项运行或验收。#27 保持开放。 diff --git a/experiments/core/results/2026-11-02-pending-wrap/validation.json b/experiments/core/results/2026-11-02-pending-wrap/validation.json new file mode 100644 index 00000000..78d6769e --- /dev/null +++ b/experiments/core/results/2026-11-02-pending-wrap/validation.json @@ -0,0 +1,172 @@ +{ + "schema": "termforge-cpp-pending-wrap-local-validation-v1", + "issue": 27, + "base_commit": "3ec11c429abb4ff3075737fdcce94afdd95cc73f", + "runtime_implementation_commit": "3a8fc1acdad84730ac7419a2e3e5173978fe5e2a", + "validated_source_commit": "3a8fc1acdad84730ac7419a2e3e5173978fe5e2a", + "final_binary_hash_observation": "After all directed tests completed; hashes taken once at the end and do not claim continuous monitoring.", + "configuration": { + "platform": "Windows-11-10.0.26200-SP0", + "python": "3.12.10", + "generator": "Visual Studio 17 2022", + "compiler": "MSVC 19.42.34433.0", + "compiler_sha256": "42fd88579adc0e2418a0a71ec9adcfcf677d39ff2724c9d6ac9d901264812bf2", + "configuration": "Release", + "qt_shell": false, + "candidate_adapter": false, + "conpty_runtime_exercised": false + }, + "observed_contract": { + "reference_issues": [ + 123, + 151 + ], + "reference_docs": { + "docs/research/windterm-alternate-style-reference-2026-10-28.md": "6a847aa16872d98c79367a407a2cb47c89458460573c850a9b237c6b2fafee74", + "docs/research/windterm-origin-wrap-reference-2026-10-29.md": "f21a29f4eff3c34d9df83a74b955d3b0cdca1517f0772a280ff5138d43802e8a" + }, + "whole_original_trace_replayed": true, + "phases_replayed": 7, + "extracted_source_bytes": 122, + "dsr_request_bytes": 28, + "dsr_reply_interface_added": false, + "original_runs_this_implementation": 0, + "parser_modes": [ + "strict", + "recovery" + ], + "per_mode_two_way_split_cases": 151, + "per_mode_bytewise_cases": 1, + "per_mode_isolated_arm_split_cases": 2, + "cpp_checks": 15, + "cpp_test_groups": 15, + "phase_replies": [ + { + "phase": 0, + "label": "wrap-enabled-edge", + "source_hex": "1b5b3f37681b5b383b3931484142", + "cursor_reply_1based": [ + 9, + 2 + ] + }, + { + "phase": 1, + "label": "wrap-disabled-edge", + "source_hex": "1b5b3f376c1b5b31303b3931484344", + "cursor_reply_1based": [ + 10, + 91 + ] + }, + { + "phase": 2, + "label": "wrap-enabled-again-edge", + "source_hex": "1b5b3f37681b5b31323b3931484546", + "cursor_reply_1based": [ + 13, + 2 + ] + }, + { + "phase": 3, + "label": "pending-edge-disable-before-next", + "source_hex": "1b5b31343b393148471b5b3f376c48", + "cursor_reply_1based": [ + 14, + 91 + ] + }, + { + "phase": 4, + "label": "disabled-edge-enable-before-next", + "source_hex": "1b5b3f376c1b5b31363b393148491b5b3f37684a", + "cursor_reply_1based": [ + 17, + 2 + ] + }, + { + "phase": 5, + "label": "alternate-pending-wrap-roundtrip", + "source_hex": "1b5b3f37681b5b333b393148571b5b3f3130343968544641341b5b3f313034396c58", + "cursor_reply_1based": [ + 4, + 2 + ] + }, + { + "phase": 6, + "label": "pending-wrap-control", + "source_hex": "1b5b363b393148565a", + "cursor_reply_1based": [ + 7, + 2 + ] + } + ] + }, + "final_runtime_sha256": { + "experiments/core/pending_wrap_test.cpp": "8e61049cce8dc21e3a99a71091b4b90051c9d77e6d3998119b2e9e477d718ed6", + "CMakeLists.txt": "16b96dbaf86ca58db8cc010e1b4b903d2c6623bc79038171d9cb8e69b1f513f0", + "experiments/core/screen.hpp": "142e786bd202a63f948d5443bcd203985d57c30f7ca39bd1cb9133646780a9ed", + "experiments/core/terminal.hpp": "486527af086973b21a672699ad5a70cb5b2ed7b709203e420212f0ce0b5bc3c9" + }, + "final_binary_sha256": { + "pending_wrap_test.exe": "0d55fd1b368c0c84f5fef3da3529e120210f8aa07c79b67494f49dcb81923100", + "decawm_mode_test.exe": "b164ba5236b34423e6116bf461f858884a364060dd3ac67218b4959fbf8369a7", + "alternate_mode_test.exe": "c908f885ae3447c1942d8fb0e0230626632f8b2f5dec697ddebdfc18929f7ef0", + "decom_origin_test.exe": "20e272d14ffdbaca32b67b61596638985540ed531f091b63d88cf2439797f10a", + "buffer_state_test.exe": "83a87096fb8368f4f1fb6bbbea3db72aa79d4e36bf60e1a65129162190f55250", + "history_test.exe": "fba45bf6b06fe41976f07eb4f7514cb8bc88571c3fa526fe3ad92bb65b165b21", + "resize_test.exe": "435fec501dd6caf98de15b63c080eb1ae38a761592f64469d77c1f5b7d345fdf", + "resize_edges_test.exe": "c03c9518086f5ebaf4bf6c2f0353add4eca58cc9f4636fb1ffc5f992b40e734a", + "selection_test.exe": "163fb71a9634d68f2c0118c8deb9c02068f26ab215bec95e402f7894a54c983d", + "search_test.exe": "263bc8d9ea5afa5294467c5479eb96565739983bb5eec945f176ee21b73f62f2", + "search_reorder_test.exe": "23410ec54fa1469991f741d1cf71e39ef5571867459beb7e86da0950a4642f6f", + "reply_test.exe": "d1dcba1ebcd5de91d05249bdf43c701e591c6f3fb0858d82518fb3e20af727be", + "edit_csi_test.exe": "7f8221af5979d526804dee54ea35e06f5496884ab4f2e7bdaa03bad8efe76221", + "order_query_test.exe": "25a36a3b4fd95bb8b06c2d3888af512e3369ed7b2f2067abcbdf41f66c5b4ea7", + "view_mapping_test.exe": "de3060254dd79612b9d0c05080d87c9154247ad118ad8299215e8c7611c501cf", + "private_mode_test.exe": "c47f4855852ad0bd2e75bd853f3271fe53c9b5c4573e25f8064f74e634a20158", + "replay.exe": "cf9941304f5d9235dfd070372357dcb06f61a7829dc61d7c12dcd0243883b61b", + "terminal_trace.exe": "38ed959979421df65d445ecdec2c9568c56a7f947c8fc0b5f070cf49899ade22", + "recovery_probe.exe": "593ac2fdd3f4505169c393f029e57d9e0d2af2818e346e028f8fba22bd048e9d", + "receive.exe": "39d4a74101effa88c3aaf772190e2778a55637fa4e4f1e6e3fff6cbee47871a3" + }, + "executions": [ + { + "label": "configure", + "exit_code": 0, + "timed_out": false, + "note": "build/pending-wrap, Qt and candidate disabled" + }, + { + "label": "build-core-tests", + "exit_code": 0, + "timed_out": false, + "note": "pending_wrap_test + 15 existing core C++ test executables + replay/terminal_trace/recovery_probe/receive" + }, + { + "label": "pending_wrap_test", + "exit_code": 0, + "note": "15/15 checks passed; strict+recovery literal 7-phase replay, 151 two-way splits and bytewise each" + }, + { + "label": "ctest-core", + "exit_code": 0, + "note": "16/16 core C++ tests passed, 0 failed, 0 skipped" + }, + { + "label": "python-core-unicode-recovery", + "exit_code": 0, + "note": "66 tests passed (experiments.core.test_core/test_unicode/test_recovery)" + } + ], + "skip_count": 0, + "residual_gaps": [ + "DECOM-on x last-column-wrap cells (origin wrap and origin x autowrap-off) are composed model checks; the original observed the wrap phases with origin off, so those crossings stay unobserved.", + "Wide scalars and combining marks across 1049 are outside this slice; the issue splits them from pending-wrap save/restore.", + "Per-screen origin bit, full active grid, per-cell style, and fixed-ConPTY transport are outside this slice." + ] +}