From 91e0c55ca4b61b8ddc2cff6f10339c18e5bf6ad3 Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Sun, 1 Nov 2026 16:22:16 +0800 Subject: [PATCH 1/2] feat(core): DECOM origin mode with margin and 1049 interaction refs #27 --- CMakeLists.txt | 5 + experiments/core/decom_origin_test.cpp | 218 +++++++++++++++++++++++++ experiments/core/screen.hpp | 21 ++- experiments/core/terminal.hpp | 19 ++- 4 files changed, 257 insertions(+), 6 deletions(-) create mode 100644 experiments/core/decom_origin_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index c6be02bb..0ac6219d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -116,6 +116,10 @@ add_executable(decawm_mode_test experiments/core/decawm_mode_test.cpp) target_link_libraries(decawm_mode_test PRIVATE termforge_core) termforge_warnings(decawm_mode_test) +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(view_mapping_test experiments/core/view_mapping_test.cpp) target_link_libraries(view_mapping_test PRIVATE termforge_core) termforge_warnings(view_mapping_test) @@ -336,6 +340,7 @@ if(BUILD_TESTING) add_test(NAME view_mapping_core COMMAND view_mapping_test) add_test(NAME alternate_mode_core COMMAND alternate_mode_test) add_test(NAME decawm_mode_core COMMAND decawm_mode_test) +add_test(NAME decom_origin_core COMMAND decom_origin_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/experiments/core/decom_origin_test.cpp b/experiments/core/decom_origin_test.cpp new file mode 100644 index 00000000..4b4dc9fe --- /dev/null +++ b/experiments/core/decom_origin_test.cpp @@ -0,0 +1,218 @@ +#include "terminal.hpp" +#include +#include +#include +#include +#include + +using namespace termforge; + +static void check(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +// The 25 phases of the fixed 91x25 WindTerm origin/wrap observation (#151), +// literal source bytes and the matching bare DSR reply for each phase. Every +// phase's reply depends on all preceding phases, so this is a cumulative trace. +struct Phase { + const char* label; + const char* source; + const char* reply; // "ESC[row;colR" +}; + +static const std::vector phases{ + {"fresh-session-cup", "\x1b[2J\x1b[HFT", "\x1b[1;3R"}, + {"explicit-origin-wrap-reset", "\x1b[?6l\x1b[?7h\x1b[r\x1b[2J\x1b[H", "\x1b[1;1R"}, + {"main-margin-set", "\x1b[2;5r", "\x1b[1;1R"}, + {"origin-reset-cup", "\x1b[1;3HD", "\x1b[1;4R"}, + {"origin-set", "\x1b[?6h", "\x1b[1;1R"}, + {"origin-set-cup", "\x1b[1;3HO", "\x1b[1;4R"}, + {"origin-set-upper-relative-bound", "\x1b[99A\x1b[99D", "\x1b[1;1R"}, + {"origin-set-lower-relative-bound", "\x1b[99B", "\x1b[4;1R"}, + {"origin-reset-after-margin", "\x1b[?6l", "\x1b[1;1R"}, + {"region-sentinels", "\x1b[r\x1b[2J\x1b[1;1HTP\x1b[25;1HBT\x1b[2;1HR0\x1b[3;1HR1\x1b[4;1HR2\x1b[5;1HR3\x1b[2;5r", "\x1b[1;1R"}, + {"region-bottom-newline", "\x1b[5;1H\r\nNW", "\x1b[5;3R"}, + {"above-region-newline", "\x1b[1;1H\nUP", "\x1b[2;3R"}, + {"below-region-newline", "\x1b[25;1H\nDN", "\x1b[25;3R"}, + {"main-margin-reset", "\x1b[r", "\x1b[1;1R"}, + {"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"}, + {"main-margin-origin-before-alt", "\x1b[?7h\x1b[2;5r\x1b[?6h\x1b[2;4HM", "\x1b[2;5R"}, + {"alternate-enter", "\x1b[?1049h", "\x1b[1;1R"}, + {"alternate-margin-origin", "\x1b[3;6r\x1b[?6h\x1b[2;4HA", "\x1b[2;5R"}, + {"alternate-exit", "\x1b[?1049l", "\x1b[2;5R"}, + {"main-cup-after-alternate", "\x1b[1;6HR", "\x1b[1;7R"}, + {"final-explicit-reset", "\x1b[?6l\x1b[?7h\x1b[r\x1b[1;1HDONE", "\x1b[1;5R"}, +}; + +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's source a DSR request, its reply must be +// the literal reply WindTerm 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.empty()) + throw std::runtime_error(std::string("phase ") + phase.label + " produced no reply"); + 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 == 0 && + parser.screen.column == 4 && !parser.screen.origin_mode() && + parser.screen.region_bottom() == 24 && parser.screen.region_top() == 0, + "trace ended outside the final explicit-reset state"); +} + +// The same 25 replies must not depend on how the byte stream is chunked. +static void chunk_invariance(ParseMode mode, size_t* cases) { + const std::string source = full_source(); + const auto expected = expected_replies(); + { + Parser parser(25, 91, 100, mode); + parser.feed(source); + check(parser.take_replies() == expected, "single-shot replies differ from the literal trace"); + parser.finish(); + } + { + 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(); + } + 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(); + ++*cases; + } +} + +// DECOM and DECAWM are independent terminal-wide bits: toggling one must not +// disturb the other, and both survive the 1049 buffer switch. +static void origin_autowrap_independence(ParseMode mode) { + Parser parser(3, 5, 2, mode); + parser.feed("\x1b[2;3r\x1b[?6h"); + check(parser.screen.origin_mode(), "origin did not turn on"); + parser.feed("\x1b[?7l"); + check(parser.screen.origin_mode(), "?7l cleared origin mode"); + parser.feed("\x1b[6n"); + check(parser.take_replies() == std::vector{"\x1b[1;1R"}, + "origin-relative reply lost after an autowrap toggle"); + parser.feed("\x1b[?6l"); + check(!parser.screen.origin_mode(), "?6l did not clear origin mode"); + // Autowrap is still off: a last-column write keeps the cursor on the cell. + parser.feed("\x1b[3;5HZ"); + check(parser.screen.row == 2 && parser.screen.column == 4, + "origin toggle re-enabled autowrap and moved the last-column cursor"); + // Origin and autowrap both persist across a 1049 roundtrip. + Parser buffers(3, 5, 2, mode); + buffers.feed("\x1b[2;3r\x1b[?6h\x1b[?7l"); + buffers.feed("\x1b[?1049h\x1b[?1049l"); + check(buffers.screen.active_buffer() == ScreenBuffer::primary && buffers.screen.origin_mode() && + buffers.screen.region_top() == 1 && buffers.screen.region_bottom() == 2, + "origin/region state was not preserved across 1049"); +} + +// Composed model check, not a literal-oracle phase: the autowrap newline stays +// region-bounded and the DSR stays origin-relative when both modes are on. +static void origin_autowrap_newline_is_region_bounded(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, + "wrap under origin did not land on the next region row"); + parser.feed("\x1b[6n"); + check(parser.take_replies() == std::vector{"\x1b[2;2R"}, + "wrap under origin reported an absolute rather than relative reply"); + parser.finish(); +} + +// Recovery consumes malformed/incomplete DECOM without changing state or +// contaminating the next CSI; strict rejects unsupported forms atomically. +static void abnormal_origin_sequences() { + for (const auto cancellation : {char(0x18), char(0x1a), char(0x1b)}) { + Parser parser(3, 10, 2, ParseMode::recovery); + parser.feed(std::string("\x1b[?6") + cancellation + "\x1b[2;2HA"); + parser.finish(); + check(!parser.screen.origin_mode() && parser.screen.row == 1 && parser.screen.column == 2, + "cancellation applied an incomplete DECOM or contaminated the next CSI"); + } + Parser truncated(3, 10, 2, ParseMode::recovery); + truncated.feed("\x1b[?6"); + truncated.finish(); + truncated.feed("\x1b[2;2HB"); + truncated.finish(); + check(!truncated.screen.origin_mode() && truncated.screen.row == 1 && truncated.screen.column == 2 && + truncated.diagnostics().truncated_sequences == 1, + "EOF truncation applied DECOM or contaminated the next CSI"); + Parser multi(3, 10, 2, ParseMode::recovery); + multi.feed("\x1b[?6;6h\x1b[2;2HC"); + multi.finish(); + check(!multi.screen.origin_mode() && multi.diagnostics().ignored_sequences == 1 && + multi.screen.row == 1 && multi.screen.column == 2, + "recovery partially applied an unsupported DECOM form"); + // Strict rejects the same unsupported private forms atomically, leaving the + // cursor and origin state untouched. + for (const auto* input : {"\x1b[?6;6h", "\x1b[?6m", "\x1b[?6 h"}) { + Parser parser(3, 10, 2); + parser.feed("\x1b[2;2HD"); + bool rejected = false; + try { parser.feed(input); } + catch (const InputError&) { rejected = true; } + check(rejected && !parser.screen.origin_mode() && parser.screen.row == 1 && parser.screen.column == 2, + "strict accepted an unsupported DECOM form or partially changed state"); + } +} + +static void ris_resets_origin(ParseMode mode) { + Parser parser(3, 10, 2, mode); + parser.feed("\x1b[2;3r\x1b[?6h"); + check(parser.screen.origin_mode(), "test setup did not enable origin"); + parser.feed("\x1b" "c"); + check(!parser.screen.origin_mode() && parser.screen.row == 0 && parser.screen.column == 0, + "RIS did not reset origin mode and home the cursor"); + parser.finish(); +} + +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); }); + size_t cases = 0; + run(name + " chunk invariance", [&] { chunk_invariance(mode, &cases); }); + std::cout << " " << name << " chunk cases: " << cases << "\n"; + run(name + " origin/autowrap independence", [&] { origin_autowrap_independence(mode); }); + run(name + " origin/autowrap region wrap", [&] { origin_autowrap_newline_is_region_bounded(mode); }); + run(name + " ris origin reset", [&] { ris_resets_origin(mode); }); + } + run("recovery abnormal sequences", abnormal_origin_sequences); + std::cout << "DECOM origin checks: " << (checks - failures) << '/' << checks << " passed\n"; + return failures ? 1 : 0; +} diff --git a/experiments/core/screen.hpp b/experiments/core/screen.hpp index 8a65d9b2..a55e322a 100644 --- a/experiments/core/screen.hpp +++ b/experiments/core/screen.hpp @@ -98,6 +98,10 @@ class Screen { }; // Permission belongs to the terminal; each frame retains its own edge state. bool autowrap_enabled_ = true; + // DECOM origin mode is terminal-wide like autowrap: it changes cursor + // addressing and the home position, not the stored rows. Frames keep only + // their own scroll region. + bool origin_mode_ = false; // Retained storage. Only the owner changes its membership or order: // resize, newline, activate_buffer, publish() and restore_frame(). std::vector lines; @@ -219,6 +223,13 @@ class Screen { // Keep the edge state: enabling after a disabled last-column write must // still let the next printable character start the continuation. void set_autowrap(bool enabled) { autowrap_enabled_ = enabled; } + // DECOM (?6): enabling homes the cursor to the region top-left, disabling to + // the screen origin, matching the observed replies for the fixed margin sample. + bool origin_mode() const { return origin_mode_; } + void set_origin_mode(bool enabled) { + origin_mode_ = enabled; + move(enabled ? region_top() : 0, 0); + } // An in-place logical reset must invalidate old readers even if the next // writes reproduce identical text using the same physical row storage. void invalidate_buffer_readers() { @@ -413,11 +424,18 @@ class Screen { } void invalidate_attachment() { last_row = last_column = -1; } void move(int r, int c) { - row = std::clamp(r, 0, height() - 1); column = std::clamp(c, 0, width() - 1); + // Origin mode confines the row to the scroll region; without it the whole + // viewport is addressable. Columns are never origin-relative. + const int top = origin_mode_ ? region_top() : 0; + const int bottom = origin_mode_ ? region_bottom() : height() - 1; + row = std::clamp(r, top, bottom); column = std::clamp(c, 0, width() - 1); wrap_pending = false; invalidate_attachment(); } int region_bottom() const { return scroll_bottom < 0 ? height() - 1 : scroll_bottom; } int region_top() const { return scroll_top; } + // 1-based row argument of CUP/HVP/VPA. Origin mode makes it relative to the + // scroll-region top; without it the row is absolute. + int position_row(int one_based) const { return (origin_mode_ ? region_top() : 0) + one_based - 1; } // DECSTBM: rows inside [top, bottom] scroll together. The list itself keeps // whole rows only, so a partial region scroll shifts entries in place; rows // that leave a full-screen region still reach history. @@ -555,6 +573,7 @@ class Screen { row = column = 0; wrap_pending = false; autowrap_enabled_ = true; + origin_mode_ = false; activate_buffer(ScreenBuffer::primary); invalidate_attachment(); } diff --git a/experiments/core/terminal.hpp b/experiments/core/terminal.hpp index 98bbd5ec..4048c7f6 100644 --- a/experiments/core/terminal.hpp +++ b/experiments/core/terminal.hpp @@ -40,6 +40,12 @@ class Parser { if (final == 'h') mode_bits_ |= bit; else mode_bits_ &= ~bit; return; } + // DECOM (?6) is owned by Screen alongside autowrap; it shifts cursor + // addressing and the home position onto the scrolling region. + if (params.size() == 1 && params[0] == 6 && (final == 'h' || final == 'l')) { + screen.set_origin_mode(final == 'h'); + return; + } if (params.size() != 1 || (params[0] != 1049 && params[0] != 7) || (final != 'h' && final != 'l')) throw InputError("unsupported private CSI command"); if (params[0] == 7) { @@ -68,8 +74,8 @@ class Parser { case 'C': screen.move(screen.row, screen.column + n); break; case 'D': screen.move(screen.row, screen.column - n); break; case 'G': screen.move(screen.row, n - 1); break; - case 'd': screen.move(n - 1, screen.column); break; - case 'H': case 'f': screen.move(n - 1, params.size() < 2 || params[1] == 0 ? 0 : params[1] - 1); break; + case 'd': screen.move(screen.position_row(n), screen.column); break; + case 'H': case 'f': screen.move(screen.position_row(n), params.size() < 2 || params[1] == 0 ? 0 : params[1] - 1); break; case 'J': screen.erase_display(params[0]); break; case 'K': screen.erase_line(params[0]); break; case 'L': screen.insert_lines(n); break; @@ -82,10 +88,13 @@ class Parser { case '@': screen.insert_characters(n); break; case 'P': screen.delete_characters(n); break; case 'n': - // Device status report 6 reports the 1-based cursor position. Other - // reports are not implemented and stay unsupported rather than faked. + // Device status report 6 reports the 1-based cursor position, which + // origin mode makes relative to the scroll-region top. Other reports + // are not implemented and stay unsupported rather than faked. if (params.size() == 1 && params[0] == 6) { - pending_replies.push_back("\x1b[" + std::to_string(screen.row + 1) + ";" + + const int report_row = + screen.origin_mode() ? screen.row - screen.region_top() + 1 : screen.row + 1; + pending_replies.push_back("\x1b[" + std::to_string(report_row) + ";" + std::to_string(screen.column + 1) + "R"); return; } From 8a50d40347300912d8d2a124fb5dd8ecef0c9c40 Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Sun, 1 Nov 2026 16:26:26 +0800 Subject: [PATCH 2/2] docs(core): archive DECOM origin validation and evidence refs #27 --- docs/EVIDENCE.md | 1 + .../results/2026-11-01-decom-origin/README.md | 39 +++ .../2026-11-01-decom-origin/validation.json | 327 ++++++++++++++++++ 3 files changed, 367 insertions(+) create mode 100644 experiments/core/results/2026-11-01-decom-origin/README.md create mode 100644 experiments/core/results/2026-11-01-decom-origin/validation.json diff --git a/docs/EVIDENCE.md b/docs/EVIDENCE.md index 25264f26..2f9879f9 100644 --- a/docs/EVIDENCE.md +++ b/docs/EVIDENCE.md @@ -11,6 +11,7 @@ | Qt 终端固定拼音 IME 组合/提交/取消 | [固定结果](../research/ui/results/2026-10-30-qt-ime-fixed-pinyin/README.md)、[验证身份](../research/ui/results/2026-10-30-qt-ime-fixed-pinyin/validation.json):隔离 VM、固定微软拼音,我们自己的 Qt 应用收到 preedit `ni'hao` → cancel → commit `e4bda0e5a5bd`(「你好」);光标全局矩形 (295,157) 与同轮屏幕变化框起点 (295,163) 相互印证。未覆盖候选窗身份/矩形、其他 DPI、完整字形与呈现 | | 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++ 行结构发布与精确身份查询 | [首片合同与固定夹具](../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/results/2026-11-01-decom-origin/README.md b/experiments/core/results/2026-11-01-decom-origin/README.md new file mode 100644 index 00000000..182544de --- /dev/null +++ b/experiments/core/results/2026-11-01-decom-origin/README.md @@ -0,0 +1,39 @@ +# 自主 C++ DECOM origin 模式与 margin/1049 交叉(#27 限定切片) + +自主 C++ strict/recovery 识别精确单参数 `CSI ?6h/l`(DECOM):origin 相对寻址、与 DECSTBM 上下界交互、与 DECAWM 独立共存、1049 进出时 origin 状态保持。依据是 [#151 原版报告](../../../../docs/research/windterm-origin-wrap-reference-2026-10-29.md) 全部 25 阶段的字面合成源与逐阶段 bare DSR 回复;没有新增原版运行。 + +## 固定输入与范围 + +新建 25×91 主屏,逐阶段累计喂入 25 段合成源共 334B,每阶段后追加 `ESC[6n` 并核对回复。这是原 25 阶段完整回放(含 origin、margin 区域 LF 与 origin 关闭的 DECAWM 阶段),DSR 一基坐标直接沿用原版有限观察: + +| 阶段 | 关键输入 | 字面回复 | +|---|---|---| +| 0–3 | 清屏/复位/`2;5r`/CUP(1,3)`D` | (1,3)→(1,1)→(1,4) | +| 4–8 | `?6h`、CUP(1,3)`O`、CUU99/CUB99、CUD99、`?6l` | (1,1)→(1,4)→(1,1)→(4,1)→(1,1) | +| 9–13 | 哨兵、区域底 CRLF、区域上/下 LF、`r` | (5,3)/(2,3)/(25,3)/(1,1) | +| 14–18 | 末列 wrap 开关五序列(origin 关闭) | (9,2)/(10,91)/(13,2)/(14,91)/(17,2) | +| 19–24 | 主屏 `?6h`、1049 进出、备用 `3;6r?6h`、返回、最终复位 | (2,5)/(1,1)/(2,5)/(2,5)/(1,7)/(1,5) | + +[测试](../../decom_origin_test.cpp)通过公开 `Parser::feed/finish/take_replies` 逐阶段核对字面回复,并对完整源(434B 含 DSR)在 strict/recovery 各做一次整体、一次逐字节、435 个二段分割点,回复序列一致。异常序列(CAN/SUB/ESC 重同步、EOF 截断、双参数/非法终字符)在 recovery 被消费不污染下一条 CSI、在 strict 原子拒绝;RIS 复位 origin;origin 与 DECAWM 互不串扰且都跨 1049 保持。 + +## 设计边界(区分实测/推断/未验证) + +- 实测:`?6h` 归位到区域左上、`?6l` 归位到屏幕原点;origin 下 CUP/VPA 行相对区域顶、CUU/CUD 行夹在 [区域顶,区域底];DSR 6n 行相对区域顶。均由阶段 4–8、19–24 的字面回复锁定。 +- 推断(标准行为):origin 位终端级共享(与 DECAWM 同层),1049 进出不重置;DECSTBM 在 origin 开启时经同一 clamp 归位区域顶。原版把“每屏 origin 位”与“返回后 margin 下界”列为未观测,故此二处是保守工程选择,不计为原版匹配。 +- 未验证:origin × 末列 wrap 是模型组合检查(wrap 阶段 14–18 均在 origin 关闭下观察),不是字面 oracle 阶段;完整 active grid、逐格样式、本地 ConPTY 运输仍未知。 + +## 红绿与实际验证 + +[验证记录](validation.json)保留源码、执行前工具身份、最终二进制 SHA、命令期限及通过/失败/跳过;最终二进制在定向构建与测试结束后取哈希,不声称全程连续监控。 + +- 实现源/测试合同 `91e0c55`:MSVC 19.42.34433.0、Release,Qt/candidate 关闭。定向构建成功;13 项核心 C++ CTest 全绿(12 项既有 + 新 `decom_origin_core` 11/11),0 跳过。 +- Python `experiments.core.test_core/test_unicode/test_recovery` 66 项通过(含补建 `recovery_probe` 与 `receive` 依赖)。 +- 唯一中途红是测试自身对“CUP(2,2)+单字符后光标列”的断言差一,修正后 11/11;实现代码无回退改动。 + +复验只需普通 C++ 构建,无新增依赖: + + cmake -S . -B build/decom -G 'Visual Studio 17 2022' -A x64 -DTERMFORGE_BUILD_QT_SHELL=OFF -DTERMFORGE_BUILD_CANDIDATE_ADAPTER=OFF -DPython3_EXECUTABLE= + cmake --build build/decom --config Release --target decom_origin_test decawm_mode_test alternate_mode_test buffer_state_test history_test resize_test selection_test search_test search_reorder_test reply_test edit_csi_test order_query_test view_mapping_test replay terminal_trace recovery_probe receive --parallel 2 + ctest --test-dir build/decom -C Release -R '^(resize_core|history_core|selection_core|search_core|search_reorder_core|buffer_state_core|view_mapping_core|alternate_mode_core|decawm_mode_core|reply_core|edit_csi_core|order_query_core|decom_origin_core)$' + +这里只声明有限 core feed 能力;Qt、原生 GUI、固定 ConPTY 路径、真实 TUI、呈现和性能均未由本项运行或验收。#27/#41 及 R2 继续保持各自出口。 diff --git a/experiments/core/results/2026-11-01-decom-origin/validation.json b/experiments/core/results/2026-11-01-decom-origin/validation.json new file mode 100644 index 00000000..8652954e --- /dev/null +++ b/experiments/core/results/2026-11-01-decom-origin/validation.json @@ -0,0 +1,327 @@ +{ + "schema": "termforge-cpp-decom-origin-local-validation-v1", + "issue": 27, + "base_commit": "33c719c6af054b47f336b9bbf622a8cdefdeba74", + "runtime_implementation_commit": "91e0c55ca4b61b8ddc2cff6f10339c18e5bf6ad3", + "validated_source_commit": "91e0c55ca4b61b8ddc2cff6f10339c18e5bf6ad3", + "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_issue": 151, + "reference_sha256": "9774e25bab9f5f751368e19f915e5d1ec8f4332be606070f15b5ffb33ce5b675", + "whole_original_trace_replayed": true, + "phases_replayed": 25, + "extracted_source_bytes": 334, + "dsr_request_bytes": 100, + "dsr_reply_interface_added": false, + "dsr_reply_origin_relative_added": true, + "original_runs_this_implementation": 0, + "parser_modes": [ + "strict", + "recovery" + ], + "per_mode_two_way_split_cases": 435, + "per_mode_bytewise_cases": 1, + "cpp_checks": 11, + "cpp_test_groups": 11, + "phase_replies": [ + { + "phase": 0, + "label": "fresh-session-cup", + "source_hex": "1b5b324a1b5b484654", + "cursor_reply_1based": [ + 1, + 3 + ] + }, + { + "phase": 1, + "label": "explicit-origin-wrap-reset", + "source_hex": "1b5b3f366c1b5b3f37681b5b721b5b324a1b5b48", + "cursor_reply_1based": [ + 1, + 1 + ] + }, + { + "phase": 2, + "label": "main-margin-set", + "source_hex": "1b5b323b3572", + "cursor_reply_1based": [ + 1, + 1 + ] + }, + { + "phase": 3, + "label": "origin-reset-cup", + "source_hex": "1b5b313b334844", + "cursor_reply_1based": [ + 1, + 4 + ] + }, + { + "phase": 4, + "label": "origin-set", + "source_hex": "1b5b3f3668", + "cursor_reply_1based": [ + 1, + 1 + ] + }, + { + "phase": 5, + "label": "origin-set-cup", + "source_hex": "1b5b313b33484f", + "cursor_reply_1based": [ + 1, + 4 + ] + }, + { + "phase": 6, + "label": "origin-set-upper-relative-bound", + "source_hex": "1b5b3939411b5b393944", + "cursor_reply_1based": [ + 1, + 1 + ] + }, + { + "phase": 7, + "label": "origin-set-lower-relative-bound", + "source_hex": "1b5b393942", + "cursor_reply_1based": [ + 4, + 1 + ] + }, + { + "phase": 8, + "label": "origin-reset-after-margin", + "source_hex": "1b5b3f366c", + "cursor_reply_1based": [ + 1, + 1 + ] + }, + { + "phase": 9, + "label": "region-sentinels", + "source_hex": "1b5b721b5b324a1b5b313b314854501b5b32353b314842541b5b323b314852301b5b333b314852311b5b343b314852321b5b353b314852331b5b323b3572", + "cursor_reply_1based": [ + 1, + 1 + ] + }, + { + "phase": 10, + "label": "region-bottom-newline", + "source_hex": "1b5b353b31480d0a4e57", + "cursor_reply_1based": [ + 5, + 3 + ] + }, + { + "phase": 11, + "label": "above-region-newline", + "source_hex": "1b5b313b31480a5550", + "cursor_reply_1based": [ + 2, + 3 + ] + }, + { + "phase": 12, + "label": "below-region-newline", + "source_hex": "1b5b32353b31480a444e", + "cursor_reply_1based": [ + 25, + 3 + ] + }, + { + "phase": 13, + "label": "main-margin-reset", + "source_hex": "1b5b72", + "cursor_reply_1based": [ + 1, + 1 + ] + }, + { + "phase": 14, + "label": "wrap-enabled-edge", + "source_hex": "1b5b3f37681b5b383b3931484142", + "cursor_reply_1based": [ + 9, + 2 + ] + }, + { + "phase": 15, + "label": "wrap-disabled-edge", + "source_hex": "1b5b3f376c1b5b31303b3931484344", + "cursor_reply_1based": [ + 10, + 91 + ] + }, + { + "phase": 16, + "label": "wrap-enabled-again-edge", + "source_hex": "1b5b3f37681b5b31323b3931484546", + "cursor_reply_1based": [ + 13, + 2 + ] + }, + { + "phase": 17, + "label": "pending-edge-disable-before-next", + "source_hex": "1b5b31343b393148471b5b3f376c48", + "cursor_reply_1based": [ + 14, + 91 + ] + }, + { + "phase": 18, + "label": "disabled-edge-enable-before-next", + "source_hex": "1b5b3f376c1b5b31363b393148491b5b3f37684a", + "cursor_reply_1based": [ + 17, + 2 + ] + }, + { + "phase": 19, + "label": "main-margin-origin-before-alt", + "source_hex": "1b5b3f37681b5b323b35721b5b3f36681b5b323b34484d", + "cursor_reply_1based": [ + 2, + 5 + ] + }, + { + "phase": 20, + "label": "alternate-enter", + "source_hex": "1b5b3f3130343968", + "cursor_reply_1based": [ + 1, + 1 + ] + }, + { + "phase": 21, + "label": "alternate-margin-origin", + "source_hex": "1b5b333b36721b5b3f36681b5b323b344841", + "cursor_reply_1based": [ + 2, + 5 + ] + }, + { + "phase": 22, + "label": "alternate-exit", + "source_hex": "1b5b3f313034396c", + "cursor_reply_1based": [ + 2, + 5 + ] + }, + { + "phase": 23, + "label": "main-cup-after-alternate", + "source_hex": "1b5b313b364852", + "cursor_reply_1based": [ + 1, + 7 + ] + }, + { + "phase": 24, + "label": "final-explicit-reset", + "source_hex": "1b5b3f366c1b5b3f37681b5b721b5b313b3148444f4e45", + "cursor_reply_1based": [ + 1, + 5 + ] + } + ] + }, + "final_runtime_sha256": { + "experiments/core/decom_origin_test.cpp": "8d75b078a31de0ebefdce62d1ef13dad31023b0a09b749e99474646e99a9bc01", + "experiments/core/screen.hpp": "e2b82a211ccc2cecb902c48dc7b345229acbcbd48c6cd6eefc9768e6c05fd09a", + "experiments/core/terminal.hpp": "582c5cdb4ac8ffb548d7cbb40336e0667705531779c6c67b66d87ecc196f26e5", + "CMakeLists.txt": "faaa7e3b081f52afbd2c1ddd4fa472a8dd14fb33f5d3a30e68e09dc106dd5b84" + }, + "final_binary_sha256": { + "decom_origin_test.exe": "9f1042acd6b697bb8a1bc9b9b3be3f9d58f0563c28c9a4772a05787419bb1c7e", + "decawm_mode_test.exe": "6babe61db7c3cb7e48f113aaba8cc1317d2798997ac0d7257ab2c3a8a2e78a5f", + "alternate_mode_test.exe": "c8f214cf043f50468567dfad2e1a7520f0cf43ba40508b931668c668fd97e98a", + "buffer_state_test.exe": "b1c43650527a15a1378ea321277593e41bcee0a3903b9118292de1b179e72a1e", + "history_test.exe": "caf12321f09c2f28e77a7e4027db6e7d028534c6df73c353fec7876291f7f4e5", + "resize_test.exe": "8b91537d83108aa91e5e61fa3465848633746a23d23d53170291a6fe0c792043", + "selection_test.exe": "d6c6e1bbd6e811d4d5ad3885b6cbf19e53e761a2689b7424964e2955bcc0f025", + "search_test.exe": "dd01d477e3bccedbac1e48c301259b9df4cf4dc8e82fd28b770297a4fc377f95", + "search_reorder_test.exe": "5a3e7e0985129b5f84cc1e18de590a759b536d87b60643f362e6174600c448af", + "reply_test.exe": "bbfc942043b677e9fb661705008eacf5a7f14d01533f563585930291349ea100", + "edit_csi_test.exe": "1916c0f0c0ec5c473abf292f9ca4830e1aa256d312f878f55b48ea0a5f134a5a", + "order_query_test.exe": "57ca9f7bf09cf19ef560404b26d24d24dabbd25f1cbd19a6709fbaa16f7b80ce", + "view_mapping_test.exe": "e39554762eac881ade4155358b7400a90ea243d624a00b146573ca0edfeff899", + "replay.exe": "e2c7ac70284bf2db1d25aff93d8ee09974a6188d2e0ec7602d5053326102045a", + "terminal_trace.exe": "0f702b61d22716fb0362320a2edc744ee6feb083d3b795098359fcd169caab8d", + "recovery_probe.exe": "efdfa0847129aafa80ed3cd450eac9a933e9794141b121254f8bb0dab64e6e1f", + "receive.exe": "1659b8a8f2c01173c9751693261eec1bea94ddbbc17df69a42a5bcc94bd5dffd" + }, + "executions": [ + { + "label": "configure", + "exit_code": 0, + "timed_out": false, + "note": "build/decom, Qt and candidate disabled" + }, + { + "label": "build-core-tests", + "exit_code": 0, + "timed_out": false, + "note": "13 core C++ test executables + replay/terminal_trace/recovery_probe/receive" + }, + { + "label": "decom_origin_test", + "exit_code": 0, + "note": "11/11 checks passed; strict+recovery literal 25-phase replay and 435 two-way splits each" + }, + { + "label": "ctest-core", + "exit_code": 0, + "note": "13/13 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": [ + "per-screen origin bit across 1049 is implemented as terminal-wide (like DECAWM); the reference marks per-screen origin unknown, so this is a conservative choice and not an oracle match.", + "post-return margin bottom (returning to primary after alternate sets a different bottom margin) is not oracle-covered.", + "origin x wrap at the last column is a composed model check, not a literal-oracle phase (wrap phases 14-18 are origin-off).", + "DECSTBM home while origin is set homes to the region top via the same clamp; not directly observed (no intermediate DSR).", + "full active grid, per-cell style, and local ConPTY transport are outside this slice." + ] +}