diff --git a/CMakeLists.txt b/CMakeLists.txt index a4d0930c..b7a9f880 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -96,6 +96,10 @@ add_executable(alternate_mode_test experiments/core/alternate_mode_test.cpp) target_link_libraries(alternate_mode_test PRIVATE termforge_core) termforge_warnings(alternate_mode_test) +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(paste_test experiments/shell/paste_test.cpp) termforge_warnings(paste_test) add_executable(clipboard_policy_test experiments/shell/clipboard_policy_test.cpp) @@ -296,6 +300,8 @@ if(BUILD_TESTING) add_test(NAME search_core COMMAND search_test) add_test(NAME buffer_state_core COMMAND buffer_state_test) add_test(NAME alternate_mode_core COMMAND alternate_mode_test) + add_test(NAME decawm_mode_core COMMAND decawm_mode_test) + set_tests_properties(decawm_mode_core PROPERTIES TIMEOUT 10) add_test(NAME paste COMMAND paste_test) add_test(NAME clipboard_policy COMMAND clipboard_policy_test) add_test(NAME keyboard COMMAND keyboard_test) diff --git a/docs/EVIDENCE.md b/docs/EVIDENCE.md index 0d768596..ff7379d2 100644 --- a/docs/EVIDENCE.md +++ b/docs/EVIDENCE.md @@ -8,6 +8,7 @@ | 配置 | 证据定位 | |---|---| +| 自主 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++ 限定 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 验收 | | Qt 基础 Unicode 文本字节 | [红绿与联合验证](../experiments/shell/results/2026-10-29-qt-unicode-input/README.md)、[固定联合身份](../experiments/shell/results/2026-10-29-qt-unicode-input/validation.json):公开Qt事件到独立ReadConsoleW接收端,整/分事件代理项、焦点/隐藏/关闭、1024B整包与NUL、两目标隔离。2b6d源码一次61项分列Qt3/其余58,13内部skip、旧红例与27原件保留;不是实际IME、字素/字形、原生Unicode窗口、DPI或性能验收 | diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 9351990b..de895b74 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -25,7 +25,7 @@ |---|---|---|---| | 自主 C++ / Qt / 固定微软 ConPTY | **实现中,未验收**:[显式 Qt 入口](../src/qt/README.md)提供默认 80×24、随可用区域变化的 cmd 标签、基本 ASCII 与[限定 Unicode 文本字节](../experiments/shell/results/2026-10-29-qt-unicode-input/README.md)、16 色及各会话独立有界队列;[基础动态尺寸验证](../experiments/shell/results/2026-10-29-qt-resize/README.md)分离窗口/核心/transport 与真实 child,覆盖极小、重复、隐藏页和关闭交叉,并复验原标签算术及事件循环回收。678 原生极窄失败保留;0ff 仅修订字面子区域/顶层实际区域夹具,另有[冻结包一次隔离原生成功](../experiments/shell/results/2026-10-29-qt-resize/native-validation.json):实际顶层区域、core/transport/child 一致,窗口仅 DPI 96。旧尺寸结果保留各自源码;[Unicode 联合源码的一次完整61项](../experiments/shell/results/2026-10-29-qt-unicode-input/validation.json)分开 Qt3项与其余58项既有/独立诊断,13条内部skip保留 | 分屏、其他 shell、工作区恢复、完整 resize/reflow、历史/选择/Find、完整 Unicode 字素/绘制与 IME、真实 DPI、应用兼容与性能仍需实现或迁移;物理裁剪不恢复丢弃正文,完整 WindTerm 标签/尺寸行为对照未完成;当前不是可发布开发包 | R2 | -自主 C++ 核心已有 [#130 双缓冲存储基础](../experiments/core/results/2026-10-29-buffer-state-r2/README.md),并接入 [#135 限定 1049 协议](../experiments/core/results/2026-10-29-cpp-1049/README.md):strict/recovery 识别 `?1049h/l`,重复进入归首格、清旧备用文本和复位写入颜色,返回恢复主屏文字/光标/颜色及 pending-wrap。共享行 ID、双帧物理 resize 与逻辑 epoch 防止旧位置、选择、搜索及 viewport 复活,包括清屏后重写相同文字。原版完整网格/背景、非颜色属性及其他模式的完整行为仍未知;[origin/wrap/margin有限直接观察](research/windterm-origin-wrap-reference-2026-10-29.md)只提供后续实现依据,尚未接入产品。本地55项中 Qt1项与其余54项诊断分列,13条内部跳过保留,不表示完整 [#27](https://github.com/JavaZeroo/termforge/issues/27)、真实 TUI/Qt 交互或系统剪贴板已验收。 +自主 C++ 核心已有 [#130 双缓冲存储基础](../experiments/core/results/2026-10-29-buffer-state-r2/README.md),并接入 [#135 限定 1049 协议](../experiments/core/results/2026-10-29-cpp-1049/README.md):strict/recovery 识别 `?1049h/l`,重复进入归首格、清旧备用文本和复位写入颜色,返回恢复主屏文字/光标/颜色及 pending-wrap。共享行 ID、双帧物理 resize 与逻辑 epoch 防止旧位置、选择、搜索及 viewport 复活,包括清屏后重写相同文字。[主屏 ASCII DECAWM 切片 #161](../experiments/core/results/2026-10-29-cpp-decawm/README.md)另按[有限直接观察](research/windterm-origin-wrap-reference-2026-10-29.md)实现 `?7h/l` 与末列开关,验证五阶段79B的字面文字/位置及分块;原版 origin/margin、wrap跨屏/宽字/resize和完整网格/样式仍未知或未实现。#135本地55项中Qt1项与其余54项诊断分列,13条内部跳过保留;#161仅有独立core CLI定向回归,不表示完整 [#27](https://github.com/JavaZeroo/termforge/issues/27)、真实 TUI/Qt 交互或系统剪贴板已验收。 Qt 构建显式准备并部署[固定微软 CRT 14.51.36247.0](../third_party/msvc_runtime/README.md),不安装到 System32。隔离 Windows 的[限定加载与 cmd 验证](../experiments/shell/results/2026-10-29-qt-crt-deploy/README.md)保留原包失败、原 52 文件不变且仅追加 CRT/原许可后的成功,以及新源码包的独立验证;它不解除完整产品打包、Qt/第三方分发审计、实际 DPI/IME 或物理呈现的验收缺口。 diff --git a/experiments/core/README.md b/experiments/core/README.md index b2288b29..9a2f5cfb 100644 --- a/experiments/core/README.md +++ b/experiments/core/README.md @@ -12,7 +12,7 @@ | 输入 | 当前行为 | |---|---| -| ASCII `0x20..0x7e` | 单列写入和覆盖;写到最右列时置待换行标志,下一个可打印字符才换行 | +| ASCII `0x20..0x7e` | 单列写入和覆盖;默认自动换行开启,写到最右列时记录末列状态,下一个可打印字符才换行 | | CR / LF | CR 回到本行第 0 列;LF 下移并保持列号,底部时全屏向上滚动;清除待换行 | | BS / TAB | BS 左移并限制到第 0 列;TAB 前进到下个 8 列制表位,限制到最右列;清除待换行 | | `CSI A/B/C/D` | 相对移动,省略或 0 表示 1,限制到屏幕边界 | @@ -22,12 +22,13 @@ | UTF-8 | 跨块解码 1–4 字节;严格拒绝过长编码、代理区、越界、孤立续字节及截断输入 | | `CSI m` | 0 重置、1/22 粗体开关;30–37/40–47 和 90–97/100–107 前景/背景色;39/49 恢复默认色 | | `CSI ?1049h/l` | [限定文字/光标/颜色合同](results/2026-10-29-cpp-1049/README.md):进入/重复进入归首格、清旧备用文本及复位前景/背景,退出恢复主屏与 pending-wrap;重复进入的逻辑世代使旧读者失效。原版完整网格背景、非颜色属性及其他模式未知 | +| `CSI ?7h/l` | [主屏 ASCII DECAWM 切片](results/2026-10-29-cpp-decawm/README.md):精确单参数开启/关闭;关闭后末列覆盖,关闭时写满末列后重新开启仍可换行。跨1049、宽/结合字及resize组合未完成原版对照 | 状态 JSON 使用 0 起始 `row/column`,每行保留尾部空格,记录 `wrap_pending`、完整可见行、保留的历史行和已淘汰行数。滚动按物理行保存历史,默认 24×80、最多 100 行历史;没有逻辑行重排。搜索由独立只读投影模块执行,解析器不持有查询。清除显示不是清除历史。 解析器在 `feed` 调用之间保留 ESC/CSI 和 UTF-8 状态。SGR 参数最多 16 个,其他命令按语义限制为 1 或 2 个;数值最多 99,999、CSI 参数到终止字符最多 32 字节。尺寸限制为 1–200 行、1–500 列,历史容量 0–10,000;容量为零时只累计淘汰数。 -**明确未支持:** 完整字素簇和文字整形、ZWJ/变体选择符/旗帜/肤色修饰序列、256 色/真彩色及其他 SGR 属性、OSC/DCS 的功能、完整备用屏关联模式、滚动区域、插入删除、逻辑行 reflow、1049 以外的模式切换、回复查询及其他控制字节。严格模式下,未支持输入、非法参数或 EOF 时截断的序列使运行失败;不静默忽略,也不输出成功快照。严格模式不支持控制序列内部的 C0 中断/取消;错误后不得继续复用该解析器对象。Recovery 可消费这些未实现功能,但不会执行它们。 +**明确未支持:** 完整字素簇和文字整形、ZWJ/变体选择符/旗帜/肤色修饰序列、256 色/真彩色及其他 SGR 属性、OSC/DCS 的功能、完整备用屏关联模式、滚动区域、插入删除、逻辑行 reflow、7/1049 以外的模式切换、回复查询及其他控制字节。严格模式下,未支持输入、非法参数或 EOF 时截断的序列使运行失败;不静默忽略,也不输出成功快照。严格模式不支持控制序列内部的 C0 中断/取消;错误后不得继续复用该解析器对象。Recovery 可消费这些未实现功能,但不会执行它们。 ## 字符宽度与单元格契约 diff --git a/experiments/core/decawm_mode_test.cpp b/experiments/core/decawm_mode_test.cpp new file mode 100644 index 00000000..5ebed733 --- /dev/null +++ b/experiments/core/decawm_mode_test.cpp @@ -0,0 +1,175 @@ +#include "terminal.hpp" +#include "text_projection.hpp" +#include +#include +#include +#include + +using namespace termforge; + +static void check(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +// WindTerm phase 15: the two source letters have no intervening DSR. +static void disabled_edge_overwrites(ParseMode mode) { + Parser parser(25, 91, 2, mode); + parser.feed("\x1b[?7l\x1b[10;91HCD"); + parser.finish(); + const auto& screen = parser.screen; + check(screen.row == 9 && screen.column == 90, + "disabled edge CD must end at (10,91)"); + check(project_line(screen.lines[9]).text == std::string(90, ' ') + "D" && + project_line(screen.lines[10]).text.empty() && screen.history.empty(), + "disabled edge CD must retain only D without a wrapped successor"); +} + +struct ObservedPhase { + const char* source; + int cursor_row, cursor_column, first_row; + const char* first_text; + const char* second_text; + bool wrapped; +}; + +// Extracted phases 14..18, excluding DSR queries and the unsupported phase-13 +// margin reset. Coordinates and logical text are independent WindTerm facts; +// physical rows/continuation links below are this Screen's engineering model. +static const std::vector phases{ + {"\x1b[?7h\x1b[8;91HAB", 9, 2, 7, "A", "B", true}, + {"\x1b[?7l\x1b[10;91HCD", 10, 91, 9, "D", "", false}, + {"\x1b[?7h\x1b[12;91HEF", 13, 2, 11, "E", "F", true}, + {"\x1b[14;91HG\x1b[?7lH", 14, 91, 13, "H", "", false}, + {"\x1b[?7l\x1b[16;91HI\x1b[?7hJ", 17, 2, 15, "I", "J", true}, +}; + +static void observed_prefix(ParseMode mode, size_t count, size_t split, bool bytewise) { + std::string source; + for (size_t index = 0; index < count; ++index) source += phases[index].source; + Parser parser(25, 91, 2, mode); + if (bytewise) for (const auto byte : source) parser.feed(std::string_view(&byte, 1)); + else { + parser.feed(std::string_view(source).substr(0, split)); + parser.feed(std::string_view(source).substr(split)); + } + parser.finish(); + const auto& screen = parser.screen; + const auto& last = phases[count - 1]; + check(screen.row + 1 == last.cursor_row && screen.column + 1 == last.cursor_column, + "observed prefix cursor differs from its literal DSR coordinates"); + check(screen.active_buffer() == ScreenBuffer::primary && screen.history.empty() && + screen.discarded == 0 && parser.diagnostics().ignored_sequences == 0, + "a supported main-screen source changed buffers/history or was ignored"); + for (size_t index = 0; index < count; ++index) { + const auto& phase = phases[index]; + const auto& first = screen.lines[phase.first_row]; + const auto& second = screen.lines[phase.first_row + 1]; + const auto first_projection = project_line(first).text; + const auto second_projection = project_line(second).text; + check(first_projection == std::string(90, ' ') + phase.first_text && + second_projection == phase.second_text, + "observed prefix text differs from independent literal text"); + check(continues_into(first, second) == phase.wrapped && + first.ending == (phase.wrapped ? LineEnding::soft_wrap : LineEnding::none) && + first.wrap_columns == (phase.wrapped ? 91u : 0u), + "the model recorded a wrap without an actual continuation, or lost a real wrap"); + } +} + +static void observed_sources_across_chunks(ParseMode mode) { + size_t prefix_bytes = 0, cases = 0; + for (size_t count = 1; count <= phases.size(); ++count) { + prefix_bytes += std::string_view(phases[count - 1].source).size(); + for (size_t split = 0; split <= prefix_bytes; ++split) { + observed_prefix(mode, count, split, false); ++cases; + } + observed_prefix(mode, count, 0, true); ++cases; + } + std::cout << "observed extracted source: " << prefix_bytes << " bytes, " << cases + << " literal-prefix chunk cases\n"; +} + +static void private_commands_are_atomic(ParseMode mode) { + const std::vector unsupported{ + "\x1b[?7;1049h", "\x1b[?1049;7h", "\x1b[?7;7l", "\x1b[?7;l", + "\x1b[?7;9999l", "\x1b[?9999h", "\x1b[?7m", "\x1b[?7 h", + "\x1b[?7:1h", "\x1b[??7l", "\x1b[0?7l", + std::string("\x1b[?") + std::string(33, '0') + "7l"}; + for (const auto& sequence : unsupported) { + Parser parser(25, 91, 2, mode); + parser.feed("\x1b[10;91H"); + bool rejected = false; + try { parser.feed(sequence); } + catch (const std::exception&) { rejected = true; } + if (mode == ParseMode::strict) { + check(rejected && parser.screen.active_buffer() == ScreenBuffer::primary && + parser.screen.row == 9 && parser.screen.column == 90 && + project_line(parser.screen.lines[9]).text.empty(), + "strict private input was accepted or partially changed visible state"); + } else { + check(!rejected, "recovery failed instead of consuming unsupported private input"); + parser.feed("CD"); parser.finish(); + check(parser.screen.active_buffer() == ScreenBuffer::primary && + parser.screen.row == 10 && parser.screen.column == 1 && + project_line(parser.screen.lines[9]).text == std::string(90, ' ') + "C" && + project_line(parser.screen.lines[10]).text == "D" && + parser.diagnostics().ignored_sequences == 1, + "recovery partially applied private modes or contaminated the next printable bytes"); + } + } +} + +static void recovery_drops_incomplete_mode_commands() { + for (const auto cancellation : {char(0x18), char(0x1a), char(0x1b)}) { + Parser parser(25, 91, 2, ParseMode::recovery); + parser.feed(std::string("\x1b[?7") + cancellation + "\x1b[10;91HCD"); + parser.finish(); + check(parser.screen.row == 10 && parser.screen.column == 1 && + project_line(parser.screen.lines[10]).text == "D", + "cancellation or ESC resynchronization applied an incomplete private mode"); + } + Parser parser(25, 91, 2, ParseMode::recovery); + parser.feed("\x1b[?7"); parser.finish(); + parser.feed("\x1b[10;91HCD"); parser.finish(); + check(parser.screen.row == 10 && parser.screen.column == 1 && + parser.diagnostics().truncated_sequences == 1 && + project_line(parser.screen.lines[10]).text == "D", + "EOF truncation applied a private mode or contaminated the next CSI"); +} + +// Safety/compatibility only: off x wide text was not observed in WindTerm. +// Preserve the pre-existing insufficient-space wrap without replacing content. +static void wide_fit_policy_is_preserved(ParseMode mode) { + Parser parser(3, 3, 2, mode); + parser.feed("\x1b[?7l\x1b[1;3H\xe4\xb8\xad"); + parser.finish(); + check(parser.screen.row == 1 && parser.screen.column == 2 && + parser.screen.lines[1].size() == 3 && + parser.screen.lines[1][0].text == "\xe4\xb8\xad" && + parser.screen.lines[1][0].width == 2 && parser.screen.lines[1][1].width == 0 && + parser.screen.lines[1][2].text == " " && + project_line(parser.screen.lines[1]).text == "\xe4\xb8\xad" && + parser.diagnostics().replaced_scalars == 0, + "wide scalar fit fallback replaced text, overflowed a row, or lost its trailing cell"); +} + +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 + " disabled edge", [&] { disabled_edge_overwrites(mode); }); + run(name + " observed chunks", [&] { observed_sources_across_chunks(mode); }); + run(name + " private guards", [&] { private_commands_are_atomic(mode); }); + run(name + " wide fit safety", [&] { wide_fit_policy_is_preserved(mode); }); + } + run("recovery cancellation/truncation", recovery_drops_incomplete_mode_commands); + std::cout << "DECAWM checks: " << (checks - failures) << '/' << checks << " passed\n"; + return failures ? 1 : 0; +} diff --git a/experiments/core/results/2026-10-29-cpp-decawm/README.md b/experiments/core/results/2026-10-29-cpp-decawm/README.md new file mode 100644 index 00000000..a1e04684 --- /dev/null +++ b/experiments/core/results/2026-10-29-cpp-decawm/README.md @@ -0,0 +1,38 @@ +# 主屏 ASCII DECAWM 与末列开关(#161) + +自主 C++ strict/recovery 识别精确单参数 `CSI ?7h/l`,在本项主屏 ASCII 样例中关闭后覆盖末列,重新开启后可继续换行。依据是 [#151 原版报告](../../../../docs/research/windterm-origin-wrap-reference-2026-10-29.md)阶段14–18;没有新增原版运行,也没有实现查询回复、origin或margin。 + +## 固定输入与范围 + +新建25×91主屏,抽取五段合成源共79B,不插入DSR。原阶段13的margin reset不在当前核心支持范围,因此这不是原25阶段完整回放。文字投影与光标一基坐标直接沿用原版有限观察: + +| 阶段 | 输入(每个模式操作带CSI前缀) | 字面结果 | +|---|---|---| +| 14 | `?7h`、CUP(8,91)、`AB` | 保留AB,(9,2) | +| 15 | `?7l`、CUP(10,91)、`CD` | 仅D,(10,91) | +| 16 | `?7h`、CUP(12,91)、`EF` | 保留EF,(13,2) | +| 17 | CUP(14,91)、`G`、`?7l`、`H` | 仅H,(14,91) | +| 18 | `?7l`、CUP(16,91)、`I`、`?7h`、`J` | 保留IJ,(17,2) | + +[测试](../../decawm_mode_test.cpp)通过公开 `Parser::feed/finish` 检查独立字面文字及位置;对每个阶段前缀检查所有二段分割点和逐字节输入,strict/recovery各235例。物理行、软连接和版本属于自研模型不变量;原版文档不是完整active grid。混合、未知、畸形及超长private输入保留原拒绝/消费策略,不部分应用模式;取消、截断和ESC重同步有单独回归。 + +wrap许可位由Screen持有、默认开启,独立于FrameState;开关不抹掉末列已写信息。这是内部工程选择,**不证明原版跨1049继承、保存或恢复规则**。宽2字符空间不足仍沿既有有界预换行路径,安全回归验证正文及续格保全,不新增替换/丢字策略,也不计为原版off×宽字匹配。结合字、wrap×resize、每屏origin、返回margin下界及完整样式仍未知。 + +## 红绿与实际验证 + +[验证记录](validation.json)保留源码、执行前工具身份、最终二进制SHA、命令期限及本地原始stdout/stderr的SHA;直接执行的RED/首个GREEN另有执行前测试二进制SHA。最终二进制在补建与测试结束后取哈希,不声称全程连续监控;日志留在`local/decawm-r2/`。 + +- RED `85c867f1b5307651ba4b444840d0901d2fe4a04d`:MSVC构建成功,直接检查0/2、exit1。strict报`unsupported private CSI command`;recovery忽略开关,末列CD不在(10,91)。 +- 首个局部GREEN `f638f7e`:同两例2/2。该中间源曾加入未执行验证的off×宽字拒绝分支,随后在 `a64376ae16f609db97af2cd05367b42a659c4bb4` 移除,保留既有宽字fit路径;不把旧分支写为原版结论。 +- 最终运行源/测试合同 `2ed20754696ed8ed530c1fbddb5da262f1e5b2cb`:Visual Studio 2022、MSVC19.42.34433.0、Release,Qt和candidate关闭。定向构建成功;首次9项CTest为8/9、exit8,唯一core错误是构建清单遗漏其所需的`receive.exe`,其中31条Python测试有1个依赖错误。其余6项既有C++测试、新DECAWM的9组及recovery的34条Python测试均通过。 +- 同源补建`receive`,仅重验core:1/1、31条Python测试通过、0跳过。初次失败和补充结果分列,**没有合称一次9/9**。定向范围含既有1049 pending恢复、双帧/历史、ID/版本、选择/搜索、Unicode和物理resize;未运行完整项目或GUI矩阵。 + +复验只需普通C++构建,无新增依赖: + +```powershell +cmake -S . -B build/decawm -G 'Visual Studio 17 2022' -A x64 -DTERMFORGE_BUILD_QT_SHELL=OFF -DTERMFORGE_BUILD_CANDIDATE_ADAPTER=OFF -DPython3_EXECUTABLE= +cmake --build build/decawm --config Release --target decawm_mode_test alternate_mode_test buffer_state_test history_test resize_test selection_test search_test replay terminal_trace recovery_probe receive --parallel 2 +ctest --test-dir build/decawm -C Release --verbose -R '^(decawm_mode_core|alternate_mode_core|buffer_state_core|history_core|resize_core|selection_core|search_core|core|recovery)$' +``` + +这里只声明有限core feed能力;Qt、原生GUI、固定ConPTY路径、真实TUI、呈现和性能均未由本项运行或验收。#27/#28/#29/#30及R2继续保持各自出口。 diff --git a/experiments/core/results/2026-10-29-cpp-decawm/validation.json b/experiments/core/results/2026-10-29-cpp-decawm/validation.json new file mode 100644 index 00000000..9b4cd6ba --- /dev/null +++ b/experiments/core/results/2026-10-29-cpp-decawm/validation.json @@ -0,0 +1,328 @@ +{ + "schema": "termforge-cpp-decawm-local-validation-v1", + "issue": 161, + "base_commit": "6a10e5322a1bddeb038670e50f32ff6deac145cf", + "runtime_implementation_commit": "a64376ae16f609db97af2cd05367b42a659c4bb4", + "validated_source_commit": "2ed20754696ed8ed530c1fbddb5da262f1e5b2cb", + "final_binary_hash_observation": "After directed tests and the corrected receive dependency build; direct RED/initial GREEN records separately hash the test executable before invocation. No continuous file monitoring is claimed.", + "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", + "phases": [ + 14, + 15, + 16, + 17, + 18 + ], + "extracted_source_bytes": 79, + "extracted_source_sha256": "e83abe6fb1f3f8aff313fa8ab031deaa212f0ad00aac3341d062e09bb1017f11", + "whole_original_trace_replayed": false, + "dsr_reply_interface_added": false, + "original_runs_this_implementation": 0, + "per_mode_literal_prefix_chunk_cases": 235, + "parser_modes": [ + "strict", + "recovery" + ], + "cpp_test_groups": 9, + "sources": [ + { + "phase": 14, + "source_hex": "1b5b3f37681b5b383b3931484142", + "expected_model_cursor_1based": [ + 9, + 2 + ] + }, + { + "phase": 15, + "source_hex": "1b5b3f376c1b5b31303b3931484344", + "expected_model_cursor_1based": [ + 10, + 91 + ] + }, + { + "phase": 16, + "source_hex": "1b5b3f37681b5b31323b3931484546", + "expected_model_cursor_1based": [ + 13, + 2 + ] + }, + { + "phase": 17, + "source_hex": "1b5b31343b393148471b5b3f376c48", + "expected_model_cursor_1based": [ + 14, + 91 + ] + }, + { + "phase": 18, + "source_hex": "1b5b3f376c1b5b31363b393148491b5b3f37684a", + "expected_model_cursor_1based": [ + 17, + 2 + ] + } + ] + }, + "final_runtime_sha256": { + "experiments/core/terminal.hpp": "b57f13d06a219a9992d2b9b99bb98721be2bc13495b7fe1a99350f0da85304f2", + "experiments/core/screen.hpp": "aa3b79f232b183a9ba425b086a6a58b00894e468c73052217fd6d8beefde2040", + "experiments/core/unicode_width.hpp": "d48bf60090a9c6a4263bb46ac587b13814b52c0dbe9d2649cee20e832a1e1926", + "experiments/core/text_projection.hpp": "3853bcf053964e54ea00bb7acfd0d302abd536ae0ed4eddfe791a6bcffdb6914", + "experiments/core/decawm_mode_test.cpp": "2862ca6d44c035231119287c9a5d67297b9641b41606d8c70802b2e6e4824437", + "CMakeLists.txt": "0b6b1af99605f551ef50c998d3a46b166bed3cc678741e43b775dd179d55ed6b" + }, + "final_binary_sha256": { + "decawm_mode_test.exe": "036092ea2fba109ed94ce15312959fcc89380e9d122743318c2ae2e5b138baf4", + "alternate_mode_test.exe": "275423077b2d5bd4432dcd960f32e25d4961904e9693d7a8e2e6b29f87b0236e", + "buffer_state_test.exe": "d94411b70166034bb2371ac22d3bb43172571de51caeb1c15f0df2ffeb846846", + "history_test.exe": "7d432a98ba856959ed1bceeb9b96f4a609c77e38f1fe6939baf54a61a6e5d10e", + "resize_test.exe": "f3b901068c598f14bf1148eaeeb792d9535e437e5dbf24dd7c6eb1b1d1fa3c92", + "selection_test.exe": "34a6dbb31a5aa59a8e49e70d893140cae81883ac47f5b7778c54ba4dbcb8df8f", + "search_test.exe": "cc935e7f0b710a8f99535d39f2086fe81b573fea531876e204f6dc8776d24b4c", + "replay.exe": "ac7636a6f7c2fd0ee7823cd2a13a3044c6218292549ede3fdb702c3e0611075a", + "terminal_trace.exe": "f2ad493cbb264259c424111f18af8a752fa100c9ae581cab3a6457d623ce7c3b", + "recovery_probe.exe": "a3f948d50b7e92c5211f138754cc71d4f33832833502a78ea85a2798bdc1210c", + "receive.exe": "b5c9b857b30e97d50e7d439ba4540009c8c9a4e79a6f60c311e24e74109b01bb" + }, + "executions": [ + { + "label": "configure", + "source_commit": "85c867f1b5307651ba4b444840d0901d2fe4a04d", + "exit_code": 0, + "elapsed_seconds": 7.2189999999973224, + "process_timeout_seconds": 60, + "timed_out": false, + "stdout_sha256": "8473db434246551197a16617d86d9df3fd94196cad22d08c322bf1505cecccef", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "executable_sha256": "c52712b2e10098baa2bd9b2fb22b6bb276c8ec46d840f0e8487eda7f76a1727b", + "source_sha256": { + "CMakeLists.txt": "0b6b1af99605f551ef50c998d3a46b166bed3cc678741e43b775dd179d55ed6b", + "experiments/core/terminal.hpp": "ed3c030611a6df38df26057a8c2834f2808cd05306e73aead79927b1f76fe5ca", + "experiments/core/screen.hpp": "04c19f09ebc0c468efbef8dd4c9da8f4bcd44e94000a87c56239756c0df1b474", + "experiments/core/decawm_mode_test.cpp": "b9f3fb6dea72d37c2292c00353dd5f1c08439fdcd377f383e2579d2e423961fb" + } + }, + { + "label": "red-build", + "source_commit": "85c867f1b5307651ba4b444840d0901d2fe4a04d", + "exit_code": 0, + "elapsed_seconds": 3.437000000005355, + "process_timeout_seconds": 60, + "timed_out": false, + "stdout_sha256": "f878d02d92a60350f8375bccccd45e07f2e039b10b3659e6b2d477e416b437ec", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "executable_sha256": "c52712b2e10098baa2bd9b2fb22b6bb276c8ec46d840f0e8487eda7f76a1727b", + "source_sha256": { + "CMakeLists.txt": "0b6b1af99605f551ef50c998d3a46b166bed3cc678741e43b775dd179d55ed6b", + "experiments/core/terminal.hpp": "ed3c030611a6df38df26057a8c2834f2808cd05306e73aead79927b1f76fe5ca", + "experiments/core/screen.hpp": "04c19f09ebc0c468efbef8dd4c9da8f4bcd44e94000a87c56239756c0df1b474", + "experiments/core/decawm_mode_test.cpp": "b9f3fb6dea72d37c2292c00353dd5f1c08439fdcd377f383e2579d2e423961fb" + } + }, + { + "label": "red-test", + "source_commit": "85c867f1b5307651ba4b444840d0901d2fe4a04d", + "exit_code": 1, + "elapsed_seconds": 0.014999999999417923, + "process_timeout_seconds": 10, + "timed_out": false, + "stdout_sha256": "563e4c96208bf1575cedcc7ac9762a9b6242874442206c93015469433e21e57c", + "stderr_sha256": "ef1f8a51fae7552ee52be5d6b7424417fab392c5c1dc7c733c7ac7ee42b85277", + "executable_sha256": "75c5e980d168ed10893c8f84781fe7d56500e9ae103a0b3bf7ebfa8f02c501c9", + "source_sha256": { + "CMakeLists.txt": "0b6b1af99605f551ef50c998d3a46b166bed3cc678741e43b775dd179d55ed6b", + "experiments/core/terminal.hpp": "ed3c030611a6df38df26057a8c2834f2808cd05306e73aead79927b1f76fe5ca", + "experiments/core/screen.hpp": "04c19f09ebc0c468efbef8dd4c9da8f4bcd44e94000a87c56239756c0df1b474", + "experiments/core/decawm_mode_test.cpp": "b9f3fb6dea72d37c2292c00353dd5f1c08439fdcd377f383e2579d2e423961fb" + } + }, + { + "label": "green-build", + "source_commit": "f638f7ea66d8852a49f9721c4f2293dafa22b08e", + "exit_code": 0, + "elapsed_seconds": 2.9059999999881256, + "process_timeout_seconds": 60, + "timed_out": false, + "stdout_sha256": "243bfd53915ff8f542606f5be9f752418215c31afcc7a26428b0620fb58e34d6", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "executable_sha256": "c52712b2e10098baa2bd9b2fb22b6bb276c8ec46d840f0e8487eda7f76a1727b", + "source_sha256": { + "CMakeLists.txt": "0b6b1af99605f551ef50c998d3a46b166bed3cc678741e43b775dd179d55ed6b", + "experiments/core/terminal.hpp": "b57f13d06a219a9992d2b9b99bb98721be2bc13495b7fe1a99350f0da85304f2", + "experiments/core/screen.hpp": "fe4a159d757f770959526ffa41dc2d588ed6dccb9d8e265b4fc62b06441b8284", + "experiments/core/decawm_mode_test.cpp": "b9f3fb6dea72d37c2292c00353dd5f1c08439fdcd377f383e2579d2e423961fb" + } + }, + { + "label": "green-test", + "source_commit": "f638f7ea66d8852a49f9721c4f2293dafa22b08e", + "exit_code": 0, + "elapsed_seconds": 0.01600000000325963, + "process_timeout_seconds": 10, + "timed_out": false, + "stdout_sha256": "e96a47232c058f706398134819c594e83d842290ad409d40f5d169ae590ccae9", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "executable_sha256": "5123e3a88bce7690d4fad6d0ff2adc892c778f9fc22aefb920b46f60123bae85", + "source_sha256": { + "CMakeLists.txt": "0b6b1af99605f551ef50c998d3a46b166bed3cc678741e43b775dd179d55ed6b", + "experiments/core/terminal.hpp": "b57f13d06a219a9992d2b9b99bb98721be2bc13495b7fe1a99350f0da85304f2", + "experiments/core/screen.hpp": "fe4a159d757f770959526ffa41dc2d588ed6dccb9d8e265b4fc62b06441b8284", + "experiments/core/decawm_mode_test.cpp": "b9f3fb6dea72d37c2292c00353dd5f1c08439fdcd377f383e2579d2e423961fb" + } + }, + { + "label": "directed-build", + "source_commit": "2ed20754696ed8ed530c1fbddb5da262f1e5b2cb", + "exit_code": 0, + "elapsed_seconds": 36.0, + "process_timeout_seconds": 60, + "timed_out": false, + "stdout_sha256": "00af4acda79fed6d0410b33489a99adc3ae21be79d74eab0f50f42d6ff09d407", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "executable_sha256": "c52712b2e10098baa2bd9b2fb22b6bb276c8ec46d840f0e8487eda7f76a1727b", + "source_sha256": { + "CMakeLists.txt": "0b6b1af99605f551ef50c998d3a46b166bed3cc678741e43b775dd179d55ed6b", + "experiments/core/terminal.hpp": "b57f13d06a219a9992d2b9b99bb98721be2bc13495b7fe1a99350f0da85304f2", + "experiments/core/screen.hpp": "aa3b79f232b183a9ba425b086a6a58b00894e468c73052217fd6d8beefde2040", + "experiments/core/decawm_mode_test.cpp": "2862ca6d44c035231119287c9a5d67297b9641b41606d8c70802b2e6e4824437" + } + }, + { + "label": "directed-core-tests", + "source_commit": "2ed20754696ed8ed530c1fbddb5da262f1e5b2cb", + "exit_code": 8, + "elapsed_seconds": 15.23399999999674, + "process_timeout_seconds": 180, + "timed_out": false, + "stdout_sha256": "fcaafa2069e66db06077a11c486c5956330dc12b23ebf85eaaf225a605da21a9", + "stderr_sha256": "2658a2a2843e2e5798a6d65e55f35a5a0cb51fd08c5fec32961b306b36759c5c", + "executable_sha256": "0b764791178c52227d1e16a013a9c5abd09e0e07bfecb7189af432127fdfbbd5", + "source_sha256": { + "CMakeLists.txt": "0b6b1af99605f551ef50c998d3a46b166bed3cc678741e43b775dd179d55ed6b", + "experiments/core/terminal.hpp": "b57f13d06a219a9992d2b9b99bb98721be2bc13495b7fe1a99350f0da85304f2", + "experiments/core/screen.hpp": "aa3b79f232b183a9ba425b086a6a58b00894e468c73052217fd6d8beefde2040", + "experiments/core/decawm_mode_test.cpp": "2862ca6d44c035231119287c9a5d67297b9641b41606d8c70802b2e6e4824437" + } + }, + { + "label": "receive-build", + "source_commit": "2ed20754696ed8ed530c1fbddb5da262f1e5b2cb", + "exit_code": 0, + "elapsed_seconds": 3.7189999999973224, + "process_timeout_seconds": 60, + "timed_out": false, + "stdout_sha256": "28531346e90eb7193b1da0932c79820ef3e8bdb7a25b86c36f3cb86142530acc", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "executable_sha256": "c52712b2e10098baa2bd9b2fb22b6bb276c8ec46d840f0e8487eda7f76a1727b", + "source_sha256": { + "CMakeLists.txt": "0b6b1af99605f551ef50c998d3a46b166bed3cc678741e43b775dd179d55ed6b", + "experiments/core/terminal.hpp": "b57f13d06a219a9992d2b9b99bb98721be2bc13495b7fe1a99350f0da85304f2", + "experiments/core/screen.hpp": "aa3b79f232b183a9ba425b086a6a58b00894e468c73052217fd6d8beefde2040", + "experiments/core/decawm_mode_test.cpp": "2862ca6d44c035231119287c9a5d67297b9641b41606d8c70802b2e6e4824437" + } + }, + { + "label": "core-dependency-corrected", + "source_commit": "2ed20754696ed8ed530c1fbddb5da262f1e5b2cb", + "exit_code": 0, + "elapsed_seconds": 4.937999999994645, + "process_timeout_seconds": 60, + "timed_out": false, + "stdout_sha256": "3300177a5b8a69f172235b59fc47a09f9f3a8f84a5e06ec2d06264f7cfb4a3c1", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "executable_sha256": "0b764791178c52227d1e16a013a9c5abd09e0e07bfecb7189af432127fdfbbd5", + "source_sha256": { + "CMakeLists.txt": "0b6b1af99605f551ef50c998d3a46b166bed3cc678741e43b775dd179d55ed6b", + "experiments/core/terminal.hpp": "b57f13d06a219a9992d2b9b99bb98721be2bc13495b7fe1a99350f0da85304f2", + "experiments/core/screen.hpp": "aa3b79f232b183a9ba425b086a6a58b00894e468c73052217fd6d8beefde2040", + "experiments/core/decawm_mode_test.cpp": "2862ca6d44c035231119287c9a5d67297b9641b41606d8c70802b2e6e4824437" + } + } + ], + "results": { + "red": { + "source_commit": "85c867f1b5307651ba4b444840d0901d2fe4a04d", + "passed": 0, + "total": 2, + "exit_code": 1, + "failures": [ + "strict: unsupported private CSI command", + "recovery: disabled edge CD must end at (10,91)" + ] + }, + "initial_green": { + "source_commit": "f638f7ea66d8852a49f9721c4f2293dafa22b08e", + "passed": 2, + "total": 2, + "superseded_wide_guard": "An untested off x wide InputError guard in this intermediate source was removed before the final contract; no original comparison claimed." + }, + "directed_initial": { + "passed": 8, + "total": 9, + "exit_code": 8, + "failed": [ + "core" + ], + "cause": "The selected core test requires receive.exe, omitted from the first targeted build.", + "core_python_tests": 31, + "core_python_errors": 1, + "recovery_python_tests": 34, + "recovery_python_errors": 0, + "skipped_entries": 0, + "skipped_python_tests": 0, + "junit_sha256": "a196e0fd79a31a673feee683a92f8c65fb3a5e769c603bb3faa34759e4980c0e" + }, + "corrected_dependency": { + "built": "receive.exe", + "runtime_source_changed": false, + "passed_ctest_entries": 1, + "total_ctest_entries": 1, + "python_tests": 31, + "python_errors": 0, + "skipped_python_tests": 0, + "junit_sha256": "2c08d64de579312f07acef6e82774e3b2c4ef25bd081bf89f298040b1b3b530c" + }, + "one_complete_nine_entry_green_run": false + }, + "implementation_choices": [ + "Terminal-wide wrap permission is outside FrameState; only the main ASCII observed contract is accepted here.", + "Disabling does not erase delayed edge state; enabling can consume it on the next one-column write.", + "Insufficient room for a width-2 scalar retains the existing pre-wrap path; no new content replacement policy." + ], + "unknown": [ + "wrap x 1049 ownership/save/restore", + "per-buffer origin state", + "restored margin lower bound", + "off x wide/combining text original behavior", + "wrap x resize original behavior", + "full grid and styles", + "fixed ConPTY transport parity", + "product TUI acceptance", + "presentation", + "performance" + ], + "full_mode_parity": false, + "presented_done": null, + "performance_evaluated": false, + "gui_runs": 0, + "qt_runs": 0, + "original_runs": 0, + "new_dependencies": false +} diff --git a/experiments/core/screen.hpp b/experiments/core/screen.hpp index cedebb2f..9057fbcb 100644 --- a/experiments/core/screen.hpp +++ b/experiments/core/screen.hpp @@ -69,6 +69,8 @@ class Screen { lines.swap(other.lines); history.swap(other.history); } }; + // Permission belongs to the terminal; each frame retains its own edge state. + bool autowrap_enabled_ = true; int columns_; uint64_t next_id_ = 1; uint64_t buffer_epoch_ = 0; @@ -169,6 +171,9 @@ class Screen { int width() const { return columns_; } ScreenBuffer active_buffer() const { return active_buffer_; } uint64_t buffer_epoch() const { return buffer_epoch_; } + // 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; } // 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() { @@ -282,7 +287,9 @@ class Screen { base.text += utf8; ++base.marks; changed(lines[last_row]); return; } if (cells > width()) throw InputError("wide scalar cannot fit screen"); - if (wrap_pending || column + cells > width()) { + // Insufficient room for a wide scalar keeps the existing pre-wrap + // policy. The permission bit only gates consuming the delayed edge. + if ((autowrap_enabled_ && wrap_pending) || column + cells > width()) { const auto used = static_cast(wrap_pending ? width() : column); column = 0; newline(true, used); } diff --git a/experiments/core/terminal.hpp b/experiments/core/terminal.hpp index 228ca86b..bdf16c70 100644 --- a/experiments/core/terminal.hpp +++ b/experiments/core/terminal.hpp @@ -26,8 +26,12 @@ class Parser { std::string encoded; void dispatch(unsigned char final) { if (private_csi) { - if (params.size() != 1 || params[0] != 1049 || (final != 'h' && final != 'l')) + if (params.size() != 1 || (params[0] != 1049 && params[0] != 7) || (final != 'h' && final != 'l')) throw InputError("unsupported private CSI command"); + if (params[0] == 7) { + screen.set_autowrap(final == 'h'); + return; + } if (final == 'h') { // Re-entry clears only the active alternate frame; the saved // primary writing state must survive until a real return.