From 72715d5f38740a8ed36bf2e5f34d08695fa0f921 Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Sun, 1 Nov 2026 18:15:11 +0800 Subject: [PATCH 1/2] test(core): add resize-edges literal oracle for #29 extreme resize x wrap Strict CTest locks ADR-0005 baseline for 1-col/1-row resize combined with DECAWM soft wrap and CR/LF hard wrap: content retain/crop, stable line identities never reused after eviction, cursor clamp, 80x24->1x1->80x24 and 60x12<->100x30 alternation, pending-wrap/origin/margin crossing, invalid-size atomicity, and byte-wise/chunk invariance. --- CMakeLists.txt | 6 + experiments/core/resize_edges_test.cpp | 283 +++++++++++++++++++++++++ 2 files changed, 289 insertions(+) create mode 100644 experiments/core/resize_edges_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b610e477..02c2f82d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -80,6 +80,10 @@ add_executable(resize_test experiments/core/resize_test.cpp) target_link_libraries(resize_test PRIVATE termforge_core) termforge_warnings(resize_test) +add_executable(resize_edges_test experiments/core/resize_edges_test.cpp) +target_link_libraries(resize_edges_test PRIVATE termforge_core) +termforge_warnings(resize_edges_test) + add_executable(history_test experiments/core/history_test.cpp) target_link_libraries(history_test PRIVATE termforge_core) termforge_warnings(history_test) @@ -335,6 +339,8 @@ if(BUILD_TESTING) add_test(NAME candidate_core COMMAND candidate_core_test) endif() add_test(NAME resize_core COMMAND resize_test) + add_test(NAME resize_edges_core COMMAND resize_edges_test) + set_tests_properties(resize_edges_core PROPERTIES TIMEOUT 10) add_test(NAME history_core COMMAND history_test) add_test(NAME selection_core COMMAND selection_test) add_test(NAME search_core COMMAND search_test) diff --git a/experiments/core/resize_edges_test.cpp b/experiments/core/resize_edges_test.cpp new file mode 100644 index 00000000..04372ba8 --- /dev/null +++ b/experiments/core/resize_edges_test.cpp @@ -0,0 +1,283 @@ +// Bounded-slice literal oracle for #29 resize edges: extreme 1xN / Nx1 resize +// combined with DECAWM soft wrap and CR/LF hard wrap. Contract baseline is the +// existing core + ADR-0005 (docs/adr/0005-physical-row-resize.md): physical-row +// crop (no reflow), leftmost cells retained, growth blanks and never pulls +// history, wrap_pending/combining attachment cleared on a real size change and +// preserved on a no-op, scrolling margins reset on resize, and stable, unique, +// monotonically allocated line identities that are never reused after eviction. +// Expectations are authored literally from ADR-0005 wording; none are generated +// by this parser. Only strict mode is spoken here; recovery edges stay out of +// this slice's oracle. +#include "screen.hpp" +#include "terminal.hpp" +#include "text_document.hpp" +#include "text_projection.hpp" +#include +#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; +} + +// Shared structural invariants: cursor bounds, per-row width, wide-cell +// pairing, history bound, and (while the order is self-reported known) the +// retained sequence holds unique, strictly increasing identities. +static void invariant(const Screen& s) { + check(s.row >= 0 && s.row < s.height() && s.column >= 0 && s.column < s.width(), "cursor bounds"); + uint64_t previous = 0; + bool first = true; + size_t count = 0; + const auto rows = [&](const auto& collection) { + for (const auto& line : collection) { + check(int(line.size()) == s.width(), "row width"); + for (int c = 0; c < s.width(); ++c) { + if (line[c].width == 0) check(c > 0 && line[c - 1].width == 2, "orphan trail"); + if (line[c].width == 2) check(c + 1 < s.width() && line[c + 1].width == 0, "orphan lead"); + } + check(line.id != 0, "identity zero leaked"); + if (s.order_known()) { + if (!first) check(line.id > previous, "retained ids not strictly increasing"); + previous = line.id; + first = false; + } + ++count; + } + }; + rows(s.history_lines()); + rows(s.visible_lines()); + check(s.history_lines().size() <= s.history_limit, "history bound"); + check(count == s.retained_size(), "retained size mismatch"); +} + +// Canonical state signature for cross-chunk equality (not a contract oracle). +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 << ':' << line.version << ':' << static_cast(line.ending) << ':' << line.next_id + << ':' << line.wrap_columns << ':' << (line.cropped ? 1 : 0) << '|' << 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(); +} + +// ADR-0005 L13: an unchanged size is a strict no-op that keeps wrap_pending and +// the pending combining attachment alive. +static void no_op_size_preserves_pending_state() { + { + Parser p(2, 4, 2); + p.feed("a\xcc\x81"); // base 'a' + combining acute -> marks 1, pending base (0,0) + check(p.screen.visible_lines()[0][0].marks == 1, "fixture did not attach the first mark"); + p.screen.resize(2, 4); // same size: strict no-op + p.feed("\xcc\x81"); // attachment survived: second mark appends to the base + check(p.screen.visible_lines()[0][0].marks == 2, "no-op resize broke the pending combining base"); + } + { + Parser w(2, 2); + w.feed("ab"); // both columns filled, delayed wrap armed + check(w.screen.wrap_pending, "fixture did not arm delayed wrap"); + w.screen.resize(2, 2); // same size: strict no-op + check(w.screen.wrap_pending, "no-op resize cleared delayed wrap"); + w.feed("X"); // delayed wrap still armed: X must wrap onto the next row + check(w.screen.row == 1 && w.screen.column == 1 && w.screen.visible_lines()[1][0].text == "X", + "no-op resize changed the delayed-wrap behaviour"); + } +} + +static std::string one_column_crop_signature(const std::string& source, int style) { + Parser p(3, 4, 2); + if (style == 0) p.feed(source); + else if (style == 1) for (const auto byte : source) p.feed(std::string_view(&byte, 1)); + else { p.feed(std::string_view(source).substr(0, 3)); p.feed(std::string_view(source).substr(3)); } + p.screen.resize(3, 1); + return signature(p.screen); +} + +// ADR-0005 L14: shrinking width keeps the leftmost cell, drops the rest, and a +// later widening never restores it. Soft-wrap metadata survives the crop but the +// continuation link is severed by the crop flag (no reflow). +static void one_column_soft_wrap_crop() { + const std::string source = "abcdef"; + const auto single = one_column_crop_signature(source, 0); + check(one_column_crop_signature(source, 1) == single, "byte-wise differs from single-shot"); + check(one_column_crop_signature(source, 2) == single, "split differs from single-shot"); + Parser p(3, 4, 2); + p.feed(source); + const auto id0 = p.screen.visible_lines()[0].id, id1 = p.screen.visible_lines()[1].id; + p.screen.resize(3, 1); + invariant(p.screen); + check(line_text(p.screen.visible_lines()[0]) == "a" && line_text(p.screen.visible_lines()[1]) == "e" && + line_text(p.screen.visible_lines()[2]) == " ", "one-column crop kept leftmost cells"); + check(p.screen.visible_lines()[0].id == id0 && p.screen.visible_lines()[1].id == id1 && + p.screen.visible_lines()[2].id > id1, "one-column crop unstable ids"); + const auto& a = p.screen.visible_lines()[0]; + check(a.ending == LineEnding::soft_wrap && a.next_id == id1 && a.wrap_columns == 4 && a.cropped, + "soft-wrap provenance shrunk with the row"); + check(!continues_into(a, p.screen.visible_lines()[1]), "cropped soft wrap must not rejoin"); + check(p.screen.row == 1 && p.screen.column == 0, "one-column cursor clamp"); + check(!p.screen.wrap_pending && p.screen.history_lines().empty() && p.screen.discarded == 0, + "one-column crop of history/shifts"); + bool rejected = false; + try { p.feed("\xe4\xb8\xad"); } catch (const InputError&) { rejected = true; } + check(rejected, "one-column strict wide scalar must be rejected"); +} + +// ADR-0005 L15: shortening to one row drops discardable rows below the cursor +// without counting them as history eviction. The dropped soft-successor identity +// stays spent; growth after the crop mints fresh ids and never resurrects it. +static void one_row_hard_wrap_and_spent_identities() { + Parser p(3, 4, 2); + p.feed("abcdefgh"); // row0 "abcd" soft-wraps to row1 "efgh" + p.feed("\x1b[1;1H"); // park the cursor on row0 before the height crop + const auto id0 = p.screen.visible_lines()[0].id; + const auto id1 = p.screen.visible_lines()[1].id; + check(p.screen.visible_lines()[0].ending == LineEnding::soft_wrap && + p.screen.visible_lines()[0].next_id == id1, "fixture soft successor missing"); + p.screen.resize(1, 4); + invariant(p.screen); + check(line_text(p.screen.visible_lines()[0]) == "abcd" && p.screen.visible_lines()[0].id == id0, + "one-row height crop dropped the cursor row"); + check(p.screen.visible_lines()[0].ending == LineEnding::soft_wrap && + p.screen.visible_lines()[0].next_id == id1 && p.screen.visible_lines()[0].cropped == false, + "height-only crop must keep the full-width soft-wrap row uncropped"); + check(!p.screen.locate(id1), "dropped soft successor identity was resurrected"); + check(p.screen.history_lines().empty() && p.screen.discarded == 0, + "bottom-cropped rows must not count as history eviction"); + p.screen.resize(3, 4); + invariant(p.screen); + check(p.screen.visible_lines()[0].id == id0 && line_text(p.screen.visible_lines()[0]) == "abcd", + "grow-back replaced a surviving identity"); + check(p.screen.visible_lines()[1].id > id1 && p.screen.visible_lines()[2].id > p.screen.visible_lines()[1].id, + "growth reused a spent bottom identity"); + check(!p.screen.locate(id1), "spent identity returned after growth"); +} + +// ADR-0005 L14/L15/L16: 80x24 -> 1x1 -> 80x24 keeps only the single leftmost +// cursor-row cell, retains the top rows as 1-column history, and growth adds +// fresh blank rows without pulling history back or restoring cropped text. +static void eighty_by_twenty_four_roundtrip() { + Parser p(24, 80, 100); + std::string fill; + for (int i = 0; i < 24; ++i) { + fill.push_back(char('A' + i)); + fill.push_back(char('a' + i)); + if (i + 1 < 24) { fill.push_back('\r'); fill.push_back('\n'); } + } + p.feed(fill); + const auto epoch = p.screen.buffer_epoch(); + p.screen.resize(1, 1); + check(p.screen.height() == 1 && p.screen.width() == 1, "1x1 geometry"); + check(line_text(p.screen.visible_lines()[0]) == "X" && p.screen.row == 0 && p.screen.column == 0, + "1x1 keeps the cursor-row leftmost cell"); + check(p.screen.history_lines().size() == 23 && p.screen.discarded == 0, "1x1 top-row history"); + check(line_text(p.screen.history_lines()[0]) == "A" && line_text(p.screen.history_lines()[22]) == "W", + "1x1 history kept each row's leftmost cell"); + p.screen.resize(24, 80); + invariant(p.screen); + check(p.screen.visible_lines()[0][0].text == "X" && p.screen.visible_lines()[0][1].text == " ", + "roundtrip restored the cropped second cell or lost the cursor row"); + check(p.screen.visible_lines()[1][0].text == " " && p.screen.visible_lines()[1].cropped == false, + "growth pulled history back into the viewport or marked fresh rows cropped"); + check(line_text(p.screen.history_lines()[0]) == std::string("A") + std::string(79, ' '), + "history widening restored cropped columns"); + check(p.screen.history_lines().size() == 23, "roundtrip changed history membership"); + check(p.screen.buffer_epoch() == epoch, "physical resize changed the buffer epoch"); + invariant(p.screen); +} + +// Continuous alternating resize (60x12 <-> 100x30) with the cursor anchored at +// the top row: the top identity survives every transition, fresh rows mint fresh +// ids, and no structural invariant ever breaks. +static void alternating_resize_many_times() { + Parser p(12, 60, 50); + p.feed("TOP"); // top row carries a stable literal marker + const auto anchor = p.screen.visible_lines()[0].id; + for (int i = 0; i < 20; ++i) { + p.screen.resize(30, 100); + invariant(p.screen); + check(p.screen.visible_lines()[0].id == anchor && p.screen.visible_lines()[0][0].text == "T" && + p.screen.visible_lines()[0][1].text == "O" && p.screen.visible_lines()[0][2].text == "P", + "grow transition displaced the top identity"); + p.screen.resize(12, 60); + invariant(p.screen); + check(p.screen.visible_lines()[0].id == anchor && p.screen.visible_lines()[0][0].text == "T" && + p.screen.visible_lines()[0][1].text == "O" && p.screen.visible_lines()[0][2].text == "P", + "shrink transition displaced the top identity"); + } + check(p.screen.visible_lines()[0].id == anchor, "final anchor identity drifted"); +} + +// Resize clears a delayed wrap and resets DECSTBM margins to the full screen +// while the terminal-wide origin bit stays set. The DSR reply changes with the +// reset region, which is the literal crossing of pending-wrap x origin/margin. +static void resize_crosses_origin_margin_and_delayed_wrap() { + Parser p(5, 5, 2); + p.feed("\x1b[2;4r"); // DECSTBM rows 1..3 (0-based) + p.feed("\x1b[?6h"); // DECOM origin on + p.feed("\x1b[1;5H"); // home to region top (row1), last column + p.feed("Z"); // delayed wrap armed at the margin edge + check(p.screen.origin_mode() && p.screen.region_top() == 1 && p.screen.region_bottom() == 3 && + p.screen.wrap_pending && p.screen.row == 1 && p.screen.column == 4, + "margin/origin/wrap fixture mis-set"); + p.feed("\x1b[6n"); + check(p.take_replies() == std::vector{"\x1b[1;5R"}, "pre-resize origin reply wrong"); + p.screen.resize(3, 8); + check(p.screen.origin_mode(), "resize cleared the terminal-wide origin bit"); + check(p.screen.region_top() == 0 && p.screen.region_bottom() == 2, "resize did not reset scrolling margins"); + check(!p.screen.wrap_pending, "resize kept a stale delayed wrap"); + check(p.screen.row == 1 && p.screen.column == 4, "resize mis-clamped the cursor"); + p.feed("\x1b[6n"); + check(p.take_replies() == std::vector{"\x1b[2;5R"}, "post-resize origin reply wrong"); + p.feed("Q"); // delayed wrap cleared: Q overwrites the last-column cell in place + check(p.screen.row == 1 && p.screen.column == 5 && p.screen.visible_lines()[1][4].text == "Q", + "cleared delayed wrap still wrapped the next printable"); + invariant(p.screen); +} + +// ADR-0005 L18: invalid sizes leave the extreme state untouched. +static void invalid_size_is_atomic_at_the_edge() { + Parser p(24, 80, 100); + p.screen.resize(1, 1); + const auto before = signature(p.screen); + for (const auto size : {std::pair{0, 1}, {201, 1}, {1, 0}, {1, 501}}) { + bool invalid = false; + try { p.screen.resize(size.first, size.second); } catch (const std::invalid_argument&) { invalid = true; } + check(invalid && signature(p.screen) == before, "invalid resize changed the extreme state"); + } +} + +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'; + } + }; + run("no-op size keeps pending state", no_op_size_preserves_pending_state); + run("one-column soft wrap crop", one_column_soft_wrap_crop); + run("one-row hard wrap and spent identities", one_row_hard_wrap_and_spent_identities); + run("80x24 to 1x1 and back", eighty_by_twenty_four_roundtrip); + run("alternating 60x12 to 100x30", alternating_resize_many_times); + run("resize crosses origin/margin/delayed wrap", resize_crosses_origin_margin_and_delayed_wrap); + run("invalid size at the edge", invalid_size_is_atomic_at_the_edge); + std::cout << "resize edges checks: " << (checks - failures) << '/' << checks << " passed\n"; + return failures ? 1 : 0; +} From c69ba66bb15090b782495d4fc5f9b3141396b182 Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Mon, 2 Nov 2026 00:29:06 +0800 Subject: [PATCH 2/2] docs(core): archive #29 resize-edges literal oracle evidence --- docs/EVIDENCE.md | 1 + .../results/2026-11-01-resize-edges/README.md | 42 ++++++ .../2026-11-01-resize-edges/validation.json | 134 ++++++++++++++++++ 3 files changed, 177 insertions(+) create mode 100644 experiments/core/results/2026-11-01-resize-edges/README.md create mode 100644 experiments/core/results/2026-11-01-resize-edges/validation.json diff --git a/docs/EVIDENCE.md b/docs/EVIDENCE.md index 89b189d7..7ef8fa6f 100644 --- a/docs/EVIDENCE.md +++ b/docs/EVIDENCE.md @@ -9,6 +9,7 @@ | 配置 | 证据定位 | 覆盖边界 | |---|---|---| | 自主 C++ 行序/显示映射与 Qt 基础输入(#27/#29/#30 切片) | [固定实现与验证记录](../experiments/shell/results/2026-11-01-r2-core-input/README.md):区域 LF、IL/DL 后的精确行序,历史快照到文档的命中/局部选区投影,Qt 正反拖选高亮与无修饰导航键的独立接收字节 | 核心模型涵盖历史、反向、宽字/组合标记、resize 裁剪、淘汰与 epoch;Qt 只接基础可见网格拖选,不含系统复制/粘贴、选词/选行、历史浏览/自动滚动或 Find。不作完整 #27/#29/#30、真实 DPI/IME、TUI 或性能验收;实际源码与红绿分列在记录中 | +| 自主 C++ 极窄/极短 resize 与软硬换行(#29 切片) | [固定验证记录](../experiments/core/results/2026-11-01-resize-edges/README.md)、[验证身份](../experiments/core/results/2026-11-01-resize-edges/validation.json):以 ADR-0005 为字面 oracle,严格锁 1 列/1 行 resize 与 DECAWM 软换行、CR/LF 硬换行组合的内容裁剪/保留、已用行身份不复用、光标夹取、80×24 到 1×1 再回 80×24 与 60×12 到 100×30 交替、pending-wrap/origin/margin 交叉、非法尺寸原子性与逐字节/分块一致 | 参照只限现有实现 + ADR-0005,无新 WindTerm 原版运行;recovery 宽字替换、软续行淘汰后的真实 reflow、备用屏/滚动区全语义、Qt/固定 ConPTY、真实 DPI 与性能均不由此项验收;#29 整体不关闭 | | 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均不由本项验收 | diff --git a/experiments/core/results/2026-11-01-resize-edges/README.md b/experiments/core/results/2026-11-01-resize-edges/README.md new file mode 100644 index 00000000..216512fa --- /dev/null +++ b/experiments/core/results/2026-11-01-resize-edges/README.md @@ -0,0 +1,42 @@ +# 自主 C++ 极窄/极短 resize 与软硬行合同(#29 有界切片) + +自主 C++ strict 核心把「极窄(1 列)/极短(1 行)」resize 与「DECAWM 软换行 / CR-LF 硬换行」的组合,按 [ADR-0005](../../../../docs/adr/0005-physical-row-resize.md) 现有合同基线逐字节锁定为字面 oracle。这里没有新增 WindTerm 原版运行:本切片的参照是「现有实现 + ADR-0005」,不是新的原版观察。 + +## 固定输入与范围 + +新建对应尺寸主屏,喂入手写字面源,在 resize 前后按 ADR-0005 的字面期望逐项核对内容、历史、行 ID、光标与结构不变量: + +| 用例 | 关键组合 | 锁定的字面事实 | +|---|---|---| +| no-op 尺寸 | 2×2 / 2×4 同尺寸 | 保留 wrap_pending 与组合字附着(L13);同尺寸 resize 后仍按延迟换行把下一个字符卷到下一行 | +| 1 列裁剪 | 3×4 软换行六字符 → 3×1 | 只留最左格,右侧不可恢复;软换行元数据保留但被 cropped 断链;光标夹到列 0;strict 拒绝宽字;单次/逐字节/二分块签名一致 | +| 1 行硬换行与身份 | 软续行 → 高度裁剪 | 光标行保留、被裁续行身份「已用不复用」,底部裁剪不计历史淘汰,回增铸严格递增新身份 | +| 80×24 → 1×1 → 80×24 | 24 行硬换行标记 | 只留光标行最左格、顶部 23 行按 1 列入历史、回增补默认空行且不回拉历史、物理 resize 不改 buffer epoch | +| 60×12 ↔ 100×30 多次 | 20 轮交替 | 顶行身份稳定、每轮结构不变量(光标界/行宽/宽字配对/历史界/身份唯一且递增)不破坏 | +| origin/margin × pending-wrap | 区域+origin+末列写后 resize | resize 清延迟换行、复位 DECSTBM 到全屏、origin 位保持;DSR 回复随复位区域从 (1;5R) 变 (2;5R);下一字符原地覆盖 | +| 非法尺寸 | 1×1 下试 0/201 行、0/501 列 | 非法尺寸不改变 1×1 极值状态(L18) | + +[测试](../../resize_edges_test.cpp)只讲 strict;recovery 宽字替换不在本 oracle 内(既有 resize_test 已单独覆盖)。signature() 只是跨分块一致性工具,不是合同 oracle 本身。 + +## 设计边界(区分实测/推断/未验证) + +- 实测(对本实现,按 ADR-0005 基线):上述全部字面事实在 MSVC 19.42.34433.0 / Release 下复现,7/7 通过。 +- 推断(标准行为):resize 复位 DECSTBM、但 origin 位终端级保持,与 [decom-origin 切片](../2026-11-01-decom-origin/README.md) 及 screen.hpp resize 注释一致,属于保守工程选择,不计为原版匹配。 +- 未验证:WindTerm 2.7.0 在这些极值尺寸 × 软硬换行的真实行为未知;软续行被淘汰后的重排不是活跃终端 reflow;Qt shell、固定 ConPTY 运输、真实 GUI 与性能均不由此项验收。 + +## 红绿与实际验证 + +[验证记录](validation.json)保留源码/二进制 SHA、执行前工具身份、命令出口与通过/失败/跳过;最终二进制在定向构建与测试结束后取哈希,不声称全程连续监控。 + +- 实现源/测试合同 72715d5(rebase 到 origin/main f72c5c4 后复验):MSVC 19.42.34433.0、Release,Qt/candidate 关闭。定向构建成功;新增 resize_edges_core 7/7。 +- 15 项核心 C++ CTest 全绿(14 项既有 + 新 resize_edges_core),0 失败 0 跳过。 +- Python experiments.core.test_core / test_unicode / test_recovery 66 项通过(含补建 receive 依赖)。 +- 唯一中途红是测试自身把硬换行误写成裸 LF(LF 保列导致字母在递增列漂移),改为 CRLF 后全绿;实现代码零回退改动——现有实现已符合 ADR-0005 基线,无需最小修正。 + +复验只需普通 C++ 构建,无新增依赖: + + cmake -S . -B build -G "Visual Studio 17 2022" -A x64 -DTERMFORGE_BUILD_QT_SHELL=OFF -DTERMFORGE_BUILD_CANDIDATE_ADAPTER=OFF + cmake --build build --config Release --target resize_edges_test resize_test history_test selection_test search_test search_reorder_test buffer_state_test view_mapping_test alternate_mode_test decawm_mode_test private_mode_test decom_origin_test reply_test edit_csi_test order_query_test replay terminal_trace recovery_probe receive --parallel 2 + ctest --test-dir build -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)$" --output-on-failure + +这里只声明有限 core feed 能力;逻辑行连续性、完整 reflow、备用屏/滚动区全语义、真实 DPI 与性能继续由 #29 及后续切片验收。 diff --git a/experiments/core/results/2026-11-01-resize-edges/validation.json b/experiments/core/results/2026-11-01-resize-edges/validation.json new file mode 100644 index 00000000..dd9ea46b --- /dev/null +++ b/experiments/core/results/2026-11-01-resize-edges/validation.json @@ -0,0 +1,134 @@ +{ + "schema": "termforge-cpp-resize-edges-local-validation-v1", + "issue": 29, + "base_commit": "f72c5c4f68dd7eb579ad1e523762333b135aa074", + "runtime_implementation_commit": "72715d5f38740a8ed36bf2e5f34d08695fa0f921", + "validated_source_commit": "72715d5f38740a8ed36bf2e5f34d08695fa0f921", + "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", + "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": "docs/adr/0005-physical-row-resize.md existing contract baseline (no new WindTerm original run)", + "reference_sha256": null, + "original_runs_this_implementation": 0, + "parser_modes": [ + "strict" + ], + "note": "Literal expectations are hand-authored from ADR-0005 wording, not generated by this parser. Recovery mode is out of this slice's oracle.", + "checks": 7, + "checks_passed": 7, + "checks_failed": 0, + "cases": [ + { + "id": "no-op-size-keeps-pending-state", + "adr": "L13", + "locks": "unchanged size keeps wrap_pending and the pending combining attachment alive" + }, + { + "id": "one-column-soft-wrap-crop", + "adr": "L14/L17", + "locks": "1-col shrink keeps leftmost cell, drops the rest with no later recovery; soft-wrap provenance survives but the join is severed by the crop flag; cursor clamps to col 0; wide scalar rejected in strict; byte-wise and split feeds agree with single-shot" + }, + { + "id": "one-row-hard-wrap-and-spent-identities", + "adr": "L15", + "locks": "height crop drops the soft successor below the cursor without counting it as history eviction; the spent identity is never resurrected and growth mints fresh strictly-increasing ids" + }, + { + "id": "eighty-by-twenty-four-roundtrip", + "adr": "L14/L15/L16", + "locks": "80x24 -> 1x1 -> 80x24 keeps only the cursor-row leftmost cell, retains top rows as 1-column history, growth adds fresh blank rows without pulling history back, and the physical resize never changes the buffer epoch" + }, + { + "id": "alternating-resize-many-times", + "adr": "L15/L16", + "locks": "20x (60x12 <-> 100x30) keeps the top-row identity stable and preserves all structural invariants" + }, + { + "id": "resize-crosses-origin-margin-delayed-wrap", + "adr": "screen.hpp resize comment", + "locks": "resize clears a delayed wrap and resets DECSTBM margins to the full screen while the terminal-wide origin bit stays set; the DSR reply changes from (1;5R) to (2;5R) through the reset region" + }, + { + "id": "invalid-size-at-the-edge", + "adr": "L18", + "locks": "invalid sizes (0/201 rows, 0/501 columns) leave the 1x1 extreme state untouched" + } + ] + }, + "final_runtime_sha256": { + "experiments/core/resize_edges_test.cpp": "ca16b2d3bbaa154dbfc06e1a71848ea25145b91f86cd44c17c1a9da86157e49e", + "experiments/core/screen.hpp": "142e786bd202a63f948d5443bcd203985d57c30f7ca39bd1cb9133646780a9ed", + "experiments/core/terminal.hpp": "486527af086973b21a672699ad5a70cb5b2ed7b709203e420212f0ce0b5bc3c9", + "experiments/core/text_document.hpp": "d4862e269120fb7d9eb5e14f16d86ceadc905b0998fa45dbe8ea21665df5bb32", + "experiments/core/text_projection.hpp": "3853bcf053964e54ea00bb7acfd0d302abd536ae0ed4eddfe791a6bcffdb6914", + "CMakeLists.txt": "8400c7a6a1f49b4965b70fa87c359dae8459e6e2c6c44060833003f6056cbb31" + }, + "final_binary_sha256": { + "resize_edges_test.exe": "3e7dba8c22e21dd69d9a25ca5a5c81e5c4256ce5652283e37fc55a09487ea2c2", + "resize_test.exe": "bc315b89655d8e8f414882df89e784dabbd931c1e52162ea329306122002b224", + "history_test.exe": "afee4dfd1d5a44fcdb6a4411c2e9017288755b8bdc77381eb0bf186b1c4caeb2", + "selection_test.exe": "e2b8ae1bf95f5ddac07677a59e9bb529bfffd07dc81c6bf31c2d0602cd32955e", + "search_test.exe": "2e80237d6b1b60e92052eb7419ce03d36a132f5809e861befeaadf74040253b7", + "search_reorder_test.exe": "157a16fc604b51e92915a334da2229f1a1310d3d09e6cc9eb8ce3152b4af2551", + "buffer_state_test.exe": "e9b56449f756bd921aa6c1d328198b12ef5c5914987c42e0335167f98f6615f7", + "view_mapping_test.exe": "b2de513860a53fceeb81b91e029646dea13095c45be8f874ae2495a745447f56", + "alternate_mode_test.exe": "2cf69242438d5ad551f40901c0e950ea8c8c62d921606ae12499165ecc3c090d", + "decawm_mode_test.exe": "4cd3d40e9f81dcc21eea24381aba1efbd42dfb689b7aebdadc53332ce3c42eda", + "private_mode_test.exe": "c5d7860dc2b4daabad7ce631f1c86e228977ea54f2fdcf725672cfe77e33660a", + "decom_origin_test.exe": "b964cf8035ec06f8045400789dcd25f854710976a7d12a4c101c96f2807fc08c", + "reply_test.exe": "73f0b14755886a1821612b6e75da4a4d5653b213475a29954b305076e9b5ccbc", + "edit_csi_test.exe": "5c1bf5eb73056075060f3ca4a3e9bb981fc11550a55d09b867165562cd82f323", + "order_query_test.exe": "8eea190b256acf14d81521bfee6a4f9bd4ebe9aad8d2e1fa569e3e5afc163868", + "replay.exe": "705a0828d1b35e95e7b6dc7a4573adecd608d6908c88534f0df38eab0f0f37a6", + "terminal_trace.exe": "dbce26f2e3b2238fe633448a306519c33f37327af82c3f4646428742ce53fb60", + "recovery_probe.exe": "ec9b8258da550a0139b0ecd7472bfe5c4d6b2d6fc1713283b98ee6bc9b13df91", + "receive.exe": "ddcda1524fa342e895c8e95e086a58fcfb86addd684b8bbb6e85f63072b3c400" + }, + "executions": [ + { + "label": "configure", + "exit_code": 0, + "timed_out": false, + "note": "build/ Qt and candidate disabled" + }, + { + "label": "build-core-tests", + "exit_code": 0, + "timed_out": false, + "note": "15 core C++ test executables + replay/terminal_trace/recovery_probe/receive" + }, + { + "label": "resize_edges_test", + "exit_code": 0, + "note": "7/7 checks passed" + }, + { + "label": "ctest-core", + "exit_code": 0, + "note": "15/15 core C++ tests passed, 0 failed, 0 skipped" + }, + { + "label": "python-core-unicode-recovery", + "exit_code": 0, + "note": "66 tests passed" + } + ], + "skip_count": 0, + "residual_gaps": [ + "no new WindTerm original run: these edges are locked to the existing ADR-0005 contract baseline only, so original 2.7.0 behavior at 1-col/1-row x wrap remains unknown.", + "recovery-mode wide-scalar replacement is intentionally out of this strict slice (existing resize_test already covers it separately).", + "soft-wrap successor eviction after a full-width height crop is verified by identity spent-ness, not by a live terminal reflow.", + "cursor, style, wide-cell and combining-mark edges combine with the existing resize_test; this slice only re-asserts the no-op and crop intersections.", + "Qt shell, native ConPTY transport, real GUI and performance are outside this slice." + ] +}