Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions docs/EVIDENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 验收 |
Expand Down
218 changes: 218 additions & 0 deletions experiments/core/decom_origin_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
#include "terminal.hpp"
#include <iostream>
#include <stdexcept>
#include <string>
#include <string_view>
#include <vector>

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<Phase> 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<std::string> expected_replies() {
std::vector<std::string> 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<std::string>{"\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<std::string>{"\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;
}
39 changes: 39 additions & 0 deletions experiments/core/results/2026-11-01-decom-origin/README.md
Original file line number Diff line number Diff line change
@@ -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=<Python>
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 继续保持各自出口。
Loading