From 24907e2dbbc3f72f0dfd8fd97ad18d667fa8e6ac Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Wed, 28 Oct 2026 23:57:49 +0800 Subject: [PATCH 1/3] feat(core): own bounded screen buffer states and invalidate stale readers Add frame activation storage and shared row identities without private CSI wiring. Resize both frames transactionally and bind document handles, selections, searches, and history viewports to a switch epoch. Refs #130. --- CMakeLists.txt | 5 + experiments/core/buffer_state_test.cpp | 319 +++++++++++++++++++++++++ experiments/core/history_view.hpp | 10 +- experiments/core/screen.hpp | 125 +++++++--- experiments/core/text_document.hpp | 7 +- experiments/core/text_search.hpp | 30 ++- experiments/core/text_selection.hpp | 2 + 7 files changed, 456 insertions(+), 42 deletions(-) create mode 100644 experiments/core/buffer_state_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 3a959cf8..7c0cf628 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -88,6 +88,10 @@ add_executable(search_test experiments/core/search_test.cpp) target_link_libraries(search_test PRIVATE termforge_core) termforge_warnings(search_test) +add_executable(buffer_state_test experiments/core/buffer_state_test.cpp) +target_link_libraries(buffer_state_test PRIVATE termforge_core) +termforge_warnings(buffer_state_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) @@ -274,6 +278,7 @@ if(BUILD_TESTING) add_test(NAME history_core COMMAND history_test) add_test(NAME selection_core COMMAND selection_test) add_test(NAME search_core COMMAND search_test) + add_test(NAME buffer_state_core COMMAND buffer_state_test) 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/experiments/core/buffer_state_test.cpp b/experiments/core/buffer_state_test.cpp new file mode 100644 index 00000000..e5ae7fbe --- /dev/null +++ b/experiments/core/buffer_state_test.cpp @@ -0,0 +1,319 @@ +#include "terminal.hpp" +#include "text_document.hpp" +#include "text_selection.hpp" +#include "text_search.hpp" +#include "history_view.hpp" +#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 row_text(const Line& line) { + std::string value; + for (const auto& cell : line) value += cell.text; + return value; +} +static std::string snapshot(const Screen& screen) { + std::ostringstream out; + screen.json(out); + return out.str(); +} +static void independent_text_and_cursor() { + Parser parser(2, 8, 2); + parser.feed("MAIN\r\nBASE\r\nEND"); + auto& screen = parser.screen; + const auto main_top = screen.lines.front().id; + const auto main_bottom = screen.lines.back().id; + screen.activate_buffer(ScreenBuffer::alternate); + parser.feed("ALT"); + screen.move(1, 1); + const auto alternate_top = screen.lines.front().id; + screen.activate_buffer(ScreenBuffer::primary); + check(screen.active_buffer() == ScreenBuffer::primary && screen.row == 1 && screen.column == 3, + "primary cursor was not restored"); + check(screen.history.size() == 1 && row_text(screen.history.front()) == "MAIN " && + row_text(screen.lines[0]) == "BASE " && row_text(screen.lines[1]) == "END ", + "primary retained text was changed by alternate output"); + check(screen.lines.front().id == main_top && screen.lines.back().id == main_bottom, + "activation replaced saved primary row identities"); + check(alternate_top > main_bottom, "alternate rows reused primary row identities"); + parser.feed("!"); + screen.activate_buffer(ScreenBuffer::alternate); + check(screen.row == 1 && screen.column == 1 && row_text(screen.lines.front()) == "ALT ", + "alternate text or cursor was not preserved"); + screen.activate_buffer(ScreenBuffer::primary); + check(row_text(screen.lines.back()) == "END! ", "alternate activation overwrote later primary output"); +} +static void writing_context_roundtrip() { + Parser parser(2, 4, 2); + parser.feed("\x1b[31;44mABCD"); + auto& screen = parser.screen; + screen.activate_buffer(ScreenBuffer::alternate); + screen.move(0, 0); + parser.feed("\x1b[32;45mZ"); + screen.move(1, 1); + screen.activate_buffer(ScreenBuffer::primary); + check(screen.row == 0 && screen.column == 3 && screen.wrap_pending && + screen.style.fg == 1 && screen.style.bg == 4, + "activation lost the saved writing context"); + parser.feed("\xcc\x81"); + check(screen.lines[0][3].text == "D\xcc\x81" && screen.lines[0][3].marks == 1, + "restored combining attachment did not target its saved base"); + parser.feed("E"); + check(row_text(screen.lines[0]) == "ABCD\xcc\x81" && row_text(screen.lines[1]) == "E " && + screen.lines[0].ending == LineEnding::soft_wrap && screen.lines[1][0].style.fg == 1 && + screen.lines[1][0].style.bg == 4, + "restored pending wrap did not resume with saved text and style"); + screen.activate_buffer(ScreenBuffer::alternate); + check(screen.row == 1 && screen.column == 1 && screen.style.fg == 2 && screen.style.bg == 5, + "primary output changed the alternate cursor or style"); +} +static void bounded_independent_history() { + Parser parser(2, 8, 2); + parser.feed("ONE\r\nTWO\r\nTHREE\r\nFOUR\r\nFIVE"); + auto& screen = parser.screen; + const auto main_last = screen.lines.back().id; + screen.activate_buffer(ScreenBuffer::alternate); + parser.feed("A\r\nB\r\nC\r\nD\r\nE"); + const auto alternate_last = screen.lines.back().id; + check(screen.history.empty() && screen.discarded == 3, + "alternate retention was not bounded independently of primary history"); + check(alternate_last > main_last, "alternate scrolling recycled line identities"); + screen.activate_buffer(ScreenBuffer::primary); + check(screen.history.size() == 2 && screen.discarded == 1 && + row_text(screen.history[0]) == "TWO " && row_text(screen.history[1]) == "THREE " && + row_text(screen.lines[0]) == "FOUR " && row_text(screen.lines[1]) == "FIVE ", + "alternate scrolling changed saved primary history or eviction state"); + parser.feed("\r\nSIX"); + check(screen.history.size() == 2 && screen.discarded == 2 && + row_text(screen.history.front()) == "THREE " && screen.lines.back().id > alternate_last, + "primary scrolling lost its budget or global identity monotonicity"); +} +static void resize_both_frames_and_rollback() { + Parser parser(3, 6, 2); + parser.feed("A\xe4\xb8\xad" "B\r\nCDEF\r\nGHIJ"); + auto& screen = parser.screen; + const auto main_first = screen.lines.front().id; + const auto main_second = screen.lines[1].id; + const auto main_third = screen.lines[2].id; + screen.activate_buffer(ScreenBuffer::alternate); + parser.feed("ALT"); + screen.move(2, 5); + screen.resize(2, 3); + check(screen.height() == 2 && screen.width() == 3 && screen.history.empty() && screen.discarded == 1, + "alternate resize used primary retention"); + screen.activate_buffer(ScreenBuffer::primary); + check(screen.height() == 2 && screen.width() == 3 && screen.row == 1 && screen.column == 2 && + screen.history.size() == 1 && screen.history[0].id == main_first && + screen.lines[0].id == main_second && screen.lines[1].id == main_third && + row_text(screen.history[0]) == "A\xe4\xb8\xad" && + row_text(screen.lines[0]) == "CDE" && row_text(screen.lines[1]) == "GHI", + "resize did not preserve the hidden primary frame's physical-row policy"); + const auto primary = snapshot(screen); + screen.activate_buffer(ScreenBuffer::alternate); + const auto alternate = snapshot(screen); + bool rejected = false; + try { screen.resize(2, 0); } catch (const std::invalid_argument&) { rejected = true; } + check(rejected && snapshot(screen) == alternate && screen.width() == 3, + "invalid resize partially changed the active frame"); + screen.activate_buffer(ScreenBuffer::primary); + check(snapshot(screen) == primary, "invalid resize partially changed the hidden frame"); + screen.activate_buffer(ScreenBuffer::alternate); + screen.resize(2, 2); + screen.activate_buffer(ScreenBuffer::primary); + check(screen.history[0].size() == 2 && screen.history[0][1].width == 1 && + screen.history[0][1].text == " " && screen.history[0].cropped, + "hidden-frame width crop left half of a wide glyph"); +} +static void growth_preserves_metadata_and_unique_ids() { + Parser parser(3, 4, 2); + parser.feed("ABCDxy\r\nZ"); + auto& screen = parser.screen; + const auto soft = screen.lines[0], hard = screen.lines[1]; + screen.activate_buffer(ScreenBuffer::alternate); + parser.feed("ALT"); + const auto alternate_last = screen.lines.back().id; + screen.resize(5, 6); + const auto alternate_new_first = screen.lines[3].id, alternate_new_last = screen.lines[4].id; + check(alternate_new_first > alternate_last && alternate_new_last > alternate_new_first && + row_text(screen.lines[0]) == "ALT " && row_text(screen.lines.back()) == " ", + "alternate growth did not append fresh default rows"); + screen.activate_buffer(ScreenBuffer::primary); + TextDocument document(screen); + check(screen.lines[0].id == soft.id && screen.lines[0].version == soft.version + 1 && + screen.lines[0].ending == LineEnding::soft_wrap && screen.lines[0].next_id == soft.next_id && + screen.lines[0].wrap_columns == soft.wrap_columns && document.meta(0).joins_next && + screen.lines[1].id == hard.id && screen.lines[1].ending == LineEnding::hard_break && + !document.meta(1).joins_next, + "hidden growth changed soft-wrap links or hard-break metadata"); + check(screen.lines[3].id > alternate_new_last && screen.lines[4].id > screen.lines[3].id && + row_text(screen.lines[0]) == "ABCD " && row_text(screen.lines[1]) == "xy " && + row_text(screen.lines.back()) == " ", + "two-frame growth reused ids or reflowed physical rows"); +} +static void hidden_resize_failure_rolls_back_both_frames() { + Parser parser(2, 4, 2); + parser.feed("MAIN"); + auto& screen = parser.screen; + screen.activate_buffer(ScreenBuffer::alternate); + parser.feed("ALT"); + const auto alternate = snapshot(screen); + screen.activate_buffer(ScreenBuffer::primary); + const auto primary = snapshot(screen); + // Reserve imported identities near exhaustion. Active growth can prepare + // one row, but hidden growth cannot allocate its own row afterwards. + auto imported = screen.lines; + imported.resize(1); + imported[0].id = (std::numeric_limits::max)() - 2; + screen.normalize_identities(imported); + bool rejected = false; + try { screen.resize(3, 4); } catch (const std::overflow_error&) { rejected = true; } + check(rejected && screen.height() == 2 && snapshot(screen) == primary, + "hidden resize failure committed active geometry or content"); + screen.activate_buffer(ScreenBuffer::alternate); + check(screen.height() == 2 && snapshot(screen) == alternate, + "hidden resize failure changed the alternate frame"); + screen.activate_buffer(ScreenBuffer::primary); + parser.feed("\r\nA\r\nB"); + check(screen.lines.back().id == (std::numeric_limits::max)() - 1, + "failed hidden resize consumed the shared identity allocator"); +} +static void positions_do_not_revive() { + Parser parser(2, 8, 2); + parser.feed("MAIN"); + auto& screen = parser.screen; + TextDocument document(screen); + const auto original = *document.position(0, 0); + check(document.valid(TextPosition{original.line_id, original.version, original.column}), + "initial document no longer accepts the legacy position shape"); + screen.activate_buffer(ScreenBuffer::alternate); + parser.feed("ALT"); + screen.activate_buffer(ScreenBuffer::primary); + check(screen.lines[0].id == original.line_id && screen.lines[0].version == original.version, + "activation changed primary content identity to invalidate positions"); + check(!document.valid(original), "old primary position revived after a buffer roundtrip"); + const auto current = *document.position(0, 0); + check(document.valid(current) && current != original, "new document position lost its buffer context"); + screen.activate_buffer(ScreenBuffer::primary); + check(document.valid(current), "idempotent storage activation invalidated current positions"); +} +static void selection_does_not_revive() { + Parser parser(2, 8, 2); + parser.feed("MAIN"); + auto& screen = parser.screen; + TextDocument document(screen); + TextSelection selection; + check(selection.set(document, {*document.position(0, 0), *document.position(0, 4)}) && + selection.copy(document, 1024).text == "MAIN", "primary selection setup failed"); + screen.activate_buffer(ScreenBuffer::alternate); + screen.activate_buffer(ScreenBuffer::primary); + check(selection.copy(document, 1024).status == SelectionCopyStatus::invalidated && selection.empty(), + "selection reconciled an old range into the returned primary frame"); + check(selection.set(document, {*document.position(0, 0), *document.position(0, 4)}) && + selection.copy(document, 1024).text == "MAIN", "fresh selection failed after activation"); + screen.activate_buffer(ScreenBuffer::alternate); + parser.feed("ALT"); + check(selection.copy(document, 1024).status == SelectionCopyStatus::invalidated && selection.empty(), + "a primary selection stayed usable in the alternate frame"); + check(selection.set(document, {*document.position(0, 0), *document.position(0, 3)}) && + selection.copy(document, 1024).text == "ALT", "fresh alternate selection failed"); + screen.activate_buffer(ScreenBuffer::primary); + check(!selection.reconcile(document), "an alternate selection stayed usable in the primary frame"); +} +static void finish_search(TextSearch& search, const TextDocument& document, uint64_t generation) { + for (int step = 0; search.status() == SearchStatus::running && step < 100; ++step) + search.step(document, generation, 64); + check(search.status() == SearchStatus::complete, "bounded search did not finish"); +} +static void search_does_not_revive() { + Parser parser(2, 8, 2); + parser.feed("MAIN"); + auto& screen = parser.screen; + TextDocument document(screen); + TextSearch search; + auto generation = search.start(document, "MAIN"); + finish_search(search, document, generation); + check(search.matches().size() == 1 && search.match(document, 0).has_value(), "primary search setup failed"); + screen.activate_buffer(ScreenBuffer::alternate); + parser.feed("ALT"); + check(!search.match(document, 0), "a primary search match stayed usable in the alternate frame"); + generation = search.start(document, "ALT"); + finish_search(search, document, generation); + check(search.matches().size() == 1 && search.match(document, 0).has_value(), + "fresh alternate search positions did not carry the active document context"); + screen.activate_buffer(ScreenBuffer::primary); + search.notify_change(document); + check(search.status() == SearchStatus::invalidated && search.matches().empty(), + "search kept an old query scan after a buffer roundtrip"); + generation = search.start(document, "MAIN"); + finish_search(search, document, generation); + check(search.matches().size() == 1 && search.match(document, 0).has_value(), + "fresh search positions did not carry the active document context"); + screen.activate_buffer(ScreenBuffer::alternate); + screen.activate_buffer(ScreenBuffer::primary); + check(!search.match(document, 0), "completed search match revived before notification"); + const auto complete_step = search.step(document, generation, 0); + check(!complete_step.accepted && complete_step.status == SearchStatus::invalidated && search.matches().empty(), + "zero-budget step did not invalidate a completed search after activation"); + generation = search.start(document, "MAIN"); + search.step(document, generation, 2); + screen.activate_buffer(ScreenBuffer::alternate); + screen.activate_buffer(ScreenBuffer::primary); + const auto running_step = search.step(document, generation, 64); + check(!running_step.accepted && running_step.status == SearchStatus::invalidated && search.matches().empty(), + "a partial scan continued across activation without notification"); +} +static void viewport_does_not_revive() { + Parser parser(2, 8, 3); + parser.feed("ONE\r\nTWO\r\nTHREE\r\nFOUR"); + auto& screen = parser.screen; + HistoryViewport viewport; + const auto paused = viewport.scroll_to(screen, 0); + check(!paused.following && paused.top == 0, "primary viewport setup failed"); + screen.activate_buffer(ScreenBuffer::alternate); + HistoryViewport new_view; + const auto new_frame = new_view.snapshot(screen); + check(new_frame.viewport.following && new_frame.viewport.top == 0 && new_frame.cursor_row == screen.row, + "a new viewport did not initialize in a nonzero epoch"); + check(new_view.scroll_to(screen, 0).following, "a new alternate viewport could not scroll"); + screen.activate_buffer(ScreenBuffer::primary); + const auto resumed = viewport.snapshot(screen); + check(resumed.viewport.following && resumed.viewport.top == 2 && resumed.cursor_row == screen.row && + row_text(resumed.lines.front()) == "THREE ", + "a paused history viewport revived after a buffer roundtrip"); + screen.activate_buffer(ScreenBuffer::alternate); + screen.activate_buffer(ScreenBuffer::primary); + const auto requested = viewport.scroll_to(screen, 0); + check(!requested.following && requested.top == 0 && requested.anchor == paused.anchor, + "epoch reset overrode an explicit scroll in the current frame"); + HistoryViewport new_primary_view; + check(new_primary_view.snapshot(screen).viewport.following && + !new_primary_view.scroll_to(screen, 0).following && new_primary_view.snapshot(screen).viewport.top == 0, + "a new primary viewport could not follow and scroll in a nonzero epoch"); +} + +int main() { + try { + independent_text_and_cursor(); + writing_context_roundtrip(); + bounded_independent_history(); + resize_both_frames_and_rollback(); + growth_preserves_metadata_and_unique_ids(); + hidden_resize_failure_rolls_back_both_frames(); + positions_do_not_revive(); + selection_does_not_revive(); + search_does_not_revive(); + viewport_does_not_revive(); + std::cout << "owned buffer state checks passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/experiments/core/history_view.hpp b/experiments/core/history_view.hpp index 2d6fd413..b948f5dd 100644 --- a/experiments/core/history_view.hpp +++ b/experiments/core/history_view.hpp @@ -14,17 +14,25 @@ struct ViewSnapshot { int cursor_row = -1, cursor_column = 0; }; class HistoryViewport { - uint64_t anchor_ = 0; + uint64_t anchor_ = 0, observed_epoch_ = 0; bool following_ = true; + void sync_epoch(const Screen& screen) { + if (observed_epoch_ == screen.buffer_epoch()) return; + observed_epoch_ = screen.buffer_epoch(); + anchor_ = 0; + following_ = true; + } public: // A viewport is owned by exactly one session. Never share it across Screens. ViewportState reconcile(const Screen& screen) { + sync_epoch(screen); const size_t maximum = screen.history.size(); const size_t top = following_ ? maximum : (std::min)(screen.lower_bound_line(anchor_), maximum); anchor_ = screen.retained_line(top).id; return {top, screen.retained_size(), static_cast(screen.height()), maximum, anchor_, following_}; } ViewportState scroll_to(const Screen& screen, size_t top) { + sync_epoch(screen); top = (std::min)(top, screen.history.size()); following_ = top == screen.history.size(); anchor_ = screen.retained_line(top).id; diff --git a/experiments/core/screen.hpp b/experiments/core/screen.hpp index c05d37f1..a7808666 100644 --- a/experiments/core/screen.hpp +++ b/experiments/core/screen.hpp @@ -5,8 +5,10 @@ #include #include #include +#include #include #include +#include #include namespace termforge { @@ -35,6 +37,7 @@ struct Cell { bool leading_spacer = false; // Autowrap padding; paint it but never copy it as text. }; enum class LineEnding { none, hard_break, soft_wrap }; +enum class ScreenBuffer { primary, alternate }; // A value container: moving/copying a row preserves its identity and metadata. // Screen alone creates identities; callers must mutate through Screen. struct Line : std::vector { @@ -50,9 +53,38 @@ struct Line : std::vector { }; class Screen { + struct FrameState { + int row = 0, column = 0; + int last_row = -1, last_column = -1; + bool wrap_pending = false; + size_t discarded = 0; + Style style; + std::vector lines; + std::deque history; + void swap(FrameState& other) noexcept { + std::swap(row, other.row); std::swap(column, other.column); + std::swap(last_row, other.last_row); std::swap(last_column, other.last_column); + std::swap(wrap_pending, other.wrap_pending); std::swap(discarded, other.discarded); + std::swap(style, other.style); + lines.swap(other.lines); history.swap(other.history); + } + }; int columns_; uint64_t next_id_ = 1; + uint64_t buffer_epoch_ = 0; int last_row = -1, last_column = -1; + ScreenBuffer active_buffer_ = ScreenBuffer::primary; + std::optional inactive_; + FrameState active_state_copy() const { + return {row, column, last_row, last_column, wrap_pending, discarded, style, lines, history}; + } + void swap_active(FrameState& state) noexcept { + std::swap(row, state.row); std::swap(column, state.column); + std::swap(last_row, state.last_row); std::swap(last_column, state.last_column); + std::swap(wrap_pending, state.wrap_pending); std::swap(discarded, state.discarded); + std::swap(style, state.style); + lines.swap(state.lines); history.swap(state.history); + } static void changed(Line& line) { if (line.version == (std::numeric_limits::max)()) throw std::overflow_error("line version exhausted"); @@ -64,6 +96,32 @@ class Screen { Line line(columns, cell); line.id = identity++; line.version = 1; return line; } + static void resize_frame(FrameState& state, int rows, int columns, size_t limit, uint64_t& identity) { + const int shift = (std::max)(0, state.row - rows + 1); + for (int index = 0; index < shift; ++index) { + if (limit) { + state.history.push_back(std::move(state.lines[index])); + if (state.history.size() > limit) { state.history.pop_front(); ++state.discarded; } + } else ++state.discarded; + } + state.lines.erase(state.lines.begin(), state.lines.begin() + shift); + // Bottom rows outside the new viewport are dropped; their ids stay spent. + if (state.lines.size() > static_cast(rows)) state.lines.resize(rows); + while (state.lines.size() < static_cast(rows)) + state.lines.push_back(make_line(columns, Cell{}, identity)); + const auto fit = [columns](Line& line) { + if (line.size() == static_cast(columns)) return; + changed(line); + if (line.size() > static_cast(columns)) line.cropped = true; + line.resize(columns); + if (line.back().width == 2) line.back() = Cell{}; + }; + for (auto& line : state.lines) fit(line); + for (auto& line : state.history) fit(line); + state.row -= shift; state.column = (std::min)(state.column, columns - 1); + state.wrap_pending = false; + state.last_row = state.last_column = -1; + } Cell blank() const { return {" ", 1, style, 0}; } void break_glyph(Line& line, int col) { if (line[col].width == 0 && col > 0) line[col - 1] = blank(); @@ -113,6 +171,30 @@ class Screen { } int height() const { return static_cast(lines.size()); } int width() const { return columns_; } + ScreenBuffer active_buffer() const { return active_buffer_; } + uint64_t buffer_epoch() const { return buffer_epoch_; } + // The configured limit belongs to primary history; the independent + // alternate storage budget is zero. This does not define a document view. + size_t active_history_limit() const { return active_buffer_ == ScreenBuffer::primary ? history_limit : 0; } + // Activation exchanges owned state without applying a terminal mode's + // clearing, inheritance or cursor-reset policy. + void activate_buffer(ScreenBuffer buffer) { + if (buffer == active_buffer_) return; + if (buffer_epoch_ == (std::numeric_limits::max)()) + throw std::overflow_error("buffer epoch exhausted"); + auto identity = next_id_; + if (!inactive_) { + FrameState target; + target.lines.reserve(lines.size()); + for (int index = 0; index < height(); ++index) + target.lines.push_back(make_line(width(), Cell{}, identity)); + inactive_.emplace(std::move(target)); + } + swap_active(*inactive_); + active_buffer_ = buffer; + next_id_ = identity; + ++buffer_epoch_; + } size_t retained_size() const { return history.size() + lines.size(); } const Line& retained_line(size_t index) const { if (index >= retained_size()) throw std::out_of_range("retained line index"); @@ -150,39 +232,21 @@ class Screen { next_id_ = maximum == (std::numeric_limits::max)() ? maximum : maximum + 1; } // Physical-row policy: no reflow, no pulling old history into the viewport. - // Prepare copies first so invalid sizes/allocation failure leave state intact. + // Both frames share geometry. Prepare both before committing so invalid + // dimensions or allocation failure cannot leave one frame half-resized. void resize(int rows, int columns) { if (rows < 1 || rows > 200 || columns < 1 || columns > 500) throw std::invalid_argument("invalid screen dimensions"); if (rows == height() && columns == width()) return; - auto next = lines; - auto past = history; - auto lost = discarded; + auto next = active_state_copy(); + auto hidden = inactive_; auto identity = next_id_; - const int shift = (std::max)(0, row - rows + 1); - for (int i = 0; i < shift; ++i) { - if (history_limit) { - past.push_back(std::move(next[i])); - if (past.size() > history_limit) { past.pop_front(); ++lost; } - } else ++lost; - } - next.erase(next.begin(), next.begin() + shift); - // Bottom rows outside the new viewport are dropped. Never reuse their ids. - if (next.size() > static_cast(rows)) next.resize(rows); - while (next.size() < static_cast(rows)) next.push_back(make_line(columns, Cell{}, identity)); - const auto fit = [columns](Line& line) { - if (line.size() == static_cast(columns)) return; - changed(line); - if (line.size() > static_cast(columns)) line.cropped = true; - line.resize(columns); - // A truncated wide glyph must never leave a dangling leading cell. - if (line.back().width == 2) line.back() = Cell{}; - }; - for (auto& line : next) fit(line); - for (auto& line : past) fit(line); - lines.swap(next); history.swap(past); discarded = lost; columns_ = columns; next_id_ = identity; - row -= shift; column = (std::min)(column, columns - 1); - wrap_pending = false; invalidate_attachment(); + resize_frame(next, rows, columns, active_history_limit(), identity); + if (hidden) + resize_frame(*hidden, rows, columns, active_buffer_ == ScreenBuffer::primary ? 0 : history_limit, identity); + swap_active(next); + if (hidden) inactive_->swap(*hidden); + columns_ = columns; next_id_ = identity; } void invalidate_attachment() { last_row = last_column = -1; } void move(int r, int c) { @@ -198,9 +262,10 @@ class Screen { previous.next_id = automatic ? (row + 1 < height() ? lines[row + 1].id : next_id_) : 0; wrap_pending = false; invalidate_attachment(); if (row + 1 < height()) { ++row; return; } - if (history_limit) { + const auto limit = active_history_limit(); + if (limit) { history.push_back(std::move(lines.front())); - if (history.size() > history_limit) { history.pop_front(); ++discarded; } + if (history.size() > limit) { history.pop_front(); ++discarded; } } else ++discarded; lines.erase(lines.begin()); lines.push_back(make_line(width(), Cell{}, next_id_)); } diff --git a/experiments/core/text_document.hpp b/experiments/core/text_document.hpp index 96465ee7..ac104aed 100644 --- a/experiments/core/text_document.hpp +++ b/experiments/core/text_document.hpp @@ -7,9 +7,10 @@ namespace termforge { struct TextPosition { uint64_t line_id = 0, version = 0; size_t column = 0; + uint64_t epoch = 0; }; inline bool operator==(TextPosition a, TextPosition b) { - return a.line_id == b.line_id && a.version == b.version && a.column == b.column; + return a.line_id == b.line_id && a.version == b.version && a.column == b.column && a.epoch == b.epoch; } inline bool operator!=(TextPosition a, TextPosition b) { return !(a == b); } struct TextRange { TextPosition begin, end; }; // Half-open physical-cell positions. @@ -31,6 +32,7 @@ class TextDocument { const Screen& screen_; public: explicit TextDocument(const Screen& screen) : screen_(screen) {} + uint64_t epoch() const { return screen_.buffer_epoch(); } size_t size() const { return screen_.retained_size(); } DocumentLineMetadata meta(size_t index) const { const auto& value = screen_.retained_line(index); @@ -47,6 +49,7 @@ class TextDocument { return index; } bool valid(TextPosition value) const { + if (value.epoch != epoch()) return false; const auto index = index_of(value.line_id); if (!index) return false; const auto current = meta(*index); @@ -56,7 +59,7 @@ class TextDocument { if (index >= size()) return std::nullopt; const auto value = meta(index); if (column > value.columns) return std::nullopt; - return TextPosition{value.id, value.version, column}; + return TextPosition{value.id, value.version, column, epoch()}; } // A range is physical-cell based and half-open. Endpoints inside a width-2 // glyph expand outward, so no consumer can copy or paint half of a glyph. diff --git a/experiments/core/text_search.hpp b/experiments/core/text_search.hpp index 5e964dcc..5f285d77 100644 --- a/experiments/core/text_search.hpp +++ b/experiments/core/text_search.hpp @@ -35,7 +35,7 @@ struct SearchNavigation { SearchMatch match; size_t index = 0; bool wrapped = fa class TextSearch { struct ScannedRow { DocumentLineMetadata meta; size_t bytes = 0, prefix_before = 0; uint64_t successor_id = 0; }; struct Origin { TextPosition begin, end; }; - uint64_t generation_ = 0, change_serial_ = 0, audit_serial_ = 0; + uint64_t generation_ = 0, change_serial_ = 0, audit_serial_ = 0, document_epoch_ = 0; SearchStatus status_ = SearchStatus::idle; SearchOptions options_; std::string query_; @@ -62,7 +62,7 @@ class TextSearch { return a.line_id < b.line_id || (a.line_id == b.line_id && a.column < b.column); } static bool equal(const TextPosition& a, const TextPosition& b) { - return a.line_id == b.line_id && a.version == b.version && a.column == b.column; + return a == b; } static bool valid_utf8(const std::string& value) { uint32_t scalar = 0, minimum = 0; unsigned remaining = 0; @@ -97,6 +97,15 @@ class TextSearch { audit_cursor_ = 0; audit_requested_ = loaded_ = boundary_done_ = at_end_ = cap_reached_ = false; next_id_ = emit_from_id_ = 0; } + bool sync_epoch(const TextDocument& doc) { + if (document_epoch_ == doc.epoch()) return false; + document_epoch_ = doc.epoch(); + if (status_ == SearchStatus::running || status_ == SearchStatus::complete || status_ == SearchStatus::capped) { + invalidate(); + return true; + } + return false; + } bool row_valid(const TextDocument& doc, const DocumentLineMetadata& expected, uint64_t successor_id) const { const auto index = doc.index_of(expected.id); if (!index) return false; @@ -195,8 +204,8 @@ class TextSearch { if (span_ == spans.size()) throw std::logic_error("projected search byte has no cell span"); const auto& span = spans[span_]; consume(static_cast(text[byte_++]), - {{current_meta_.id, current_meta_.version, span.column_begin}, - {current_meta_.id, current_meta_.version, span.column_end}}); + {{current_meta_.id, current_meta_.version, span.column_begin, document_epoch_}, + {current_meta_.id, current_meta_.version, span.column_end, document_epoch_}}); ++progress.bytes_scanned; return true; } const auto index = doc.index_of(current_meta_.id); @@ -205,7 +214,8 @@ class TextSearch { if (!boundary_done_ && has_next && !current_.joins_next) { const auto next = doc.meta(*index + 1); const auto end = current_.projection.spans.empty() ? 0 : current_.projection.spans.back().column_end; - consume('\n', {{current_meta_.id, current_meta_.version, end}, {next.id, next.version, 0}}); + consume('\n', {{current_meta_.id, current_meta_.version, end, document_epoch_}, + {next.id, next.version, 0, document_epoch_}}); boundary_done_ = true; ++progress.bytes_scanned; return true; } rows_.push_back({current_meta_, text.size() + (has_next && !current_.joins_next ? 1u : 0u), current_prefix_before_, current_successor_id_}); @@ -214,12 +224,12 @@ class TextSearch { } public: static constexpr size_t query_byte_limit = 4096, result_hard_limit = 4096; - uint64_t start(const TextDocument&, std::string query, SearchOptions options = {}) { + uint64_t start(const TextDocument& doc, std::string query, SearchOptions options = {}) { if (query.size() > query_byte_limit || !valid_utf8(query)) throw std::invalid_argument("search query must be valid UTF-8 within 4096 bytes"); if (!options.result_limit || options.result_limit > result_hard_limit) throw std::invalid_argument("search result limit must be between 1 and 4096"); - advance_generation(); reset_scan(); query_ = std::move(query); options_ = options; + advance_generation(); reset_scan(); query_ = std::move(query); options_ = options; document_epoch_ = doc.epoch(); prefix_.assign(query_.size(), 0); for (size_t i = 1, length = 0; i < query_.size(); ++i) { const auto c = folded(static_cast(query_[i])); @@ -230,7 +240,8 @@ class TextSearch { status_ = query_.empty() ? SearchStatus::complete : SearchStatus::running; return generation_; } - uint64_t notify_change(const TextDocument&) { + uint64_t notify_change(const TextDocument& doc) { + if (sync_epoch(doc)) return generation_; advance_generation(); if (query_.empty() || status_ == SearchStatus::cancelled || status_ == SearchStatus::closed || status_ == SearchStatus::invalidated || status_ == SearchStatus::idle) return generation_; @@ -241,6 +252,7 @@ class TextSearch { at_end_ = false; status_ = SearchStatus::running; return generation_; } SearchStep step(const TextDocument& doc, uint64_t generation, size_t budget) { + sync_epoch(doc); SearchStep progress{status_, 0, 0, 0, generation == generation_}; if (!progress.accepted || status_ != SearchStatus::running || !budget) return progress; if (loaded_ && !row_valid(doc, current_meta_, current_successor_id_)) { @@ -270,7 +282,7 @@ class TextSearch { const std::vector& matches() const { return matches_; } size_t retained_rows() const { return rows_.size(); } std::optional match(const TextDocument& doc, size_t index) const { - if (index >= matches_.size()) return {}; + if (document_epoch_ != doc.epoch() || index >= matches_.size()) return {}; const auto hit = matches_[index]; if (!doc.valid(hit.range.begin) || !doc.valid(hit.range.end)) return {}; const auto first = std::lower_bound(rows_.begin(), rows_.end(), hit.range.begin.line_id, diff --git a/experiments/core/text_selection.hpp b/experiments/core/text_selection.hpp index 8af73a58..824d02ec 100644 --- a/experiments/core/text_selection.hpp +++ b/experiments/core/text_selection.hpp @@ -65,6 +65,8 @@ class TextSelection { // both the saved and live rows have no text or spans. bool reconcile(const TextDocument& document) { if (!range_) return !invalidated_; + if (range_->begin.epoch != document.epoch() || range_->end.epoch != document.epoch()) + return invalidate(); const auto begin = document.index_of(range_->begin.line_id); if (!begin || saved_.size() > document.size() - *begin) return invalidate(); for (size_t i = 0; i < saved_.size(); ++i) { From e02b02b34507514973085beb6e0a3f2a0ea8959a Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Thu, 29 Oct 2026 00:24:01 +0800 Subject: [PATCH 2/3] refactor(core): share buffer frame swap field list --- experiments/core/screen.hpp | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/experiments/core/screen.hpp b/experiments/core/screen.hpp index a7808666..26dd2d83 100644 --- a/experiments/core/screen.hpp +++ b/experiments/core/screen.hpp @@ -61,7 +61,7 @@ class Screen { Style style; std::vector lines; std::deque history; - void swap(FrameState& other) noexcept { + template void swap(State& other) noexcept { std::swap(row, other.row); std::swap(column, other.column); std::swap(last_row, other.last_row); std::swap(last_column, other.last_column); std::swap(wrap_pending, other.wrap_pending); std::swap(discarded, other.discarded); @@ -79,11 +79,7 @@ class Screen { return {row, column, last_row, last_column, wrap_pending, discarded, style, lines, history}; } void swap_active(FrameState& state) noexcept { - std::swap(row, state.row); std::swap(column, state.column); - std::swap(last_row, state.last_row); std::swap(last_column, state.last_column); - std::swap(wrap_pending, state.wrap_pending); std::swap(discarded, state.discarded); - std::swap(style, state.style); - lines.swap(state.lines); history.swap(state.history); + state.swap(*this); } static void changed(Line& line) { if (line.version == (std::numeric_limits::max)()) From 8350945f007d507069975c5f3342dd369c9184de Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Thu, 29 Oct 2026 00:37:04 +0800 Subject: [PATCH 3/3] docs(core): record bounded buffer foundation evidence --- docs/EVIDENCE.md | 1 + docs/FEATURES.md | 2 + .../2026-10-29-buffer-state-r2/README.md | 40 ++ .../validation.json | 570 ++++++++++++++++++ 4 files changed, 613 insertions(+) create mode 100644 experiments/core/results/2026-10-29-buffer-state-r2/README.md create mode 100644 experiments/core/results/2026-10-29-buffer-state-r2/validation.json diff --git a/docs/EVIDENCE.md b/docs/EVIDENCE.md index 0988c1db..84f74250 100644 --- a/docs/EVIDENCE.md +++ b/docs/EVIDENCE.md @@ -8,6 +8,7 @@ | 配置 | 证据定位 | |---|---| +| 自主 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 cmd 标签与退出前回收 | [固定结果](../experiments/shell/results/2026-10-28-qt-tabs/README.md)、[验证身份](../experiments/shell/results/2026-10-28-qt-tabs/validation.json):双 cmd 隔离、失败页、关闭与四个实际事件循环边界;Qt 1 项和其他 49 项诊断及内部跳过分列,旧中断轮不拼接 | | Qt 正式入口的独立 hosted 构建 | [固定结果](../experiments/shell/results/2026-10-28-qt-cpp-ci/README.md)、[验证身份](../experiments/shell/results/2026-10-28-qt-cpp-ci/validation.json):固定 SDK、VS 2026 hosted 与真实 cmd 的 offscreen / OwnedJob 检查;原依赖换行和 generator 失败保留。没有原生上屏、完整部署哈希或产品包验收 | | 自主 C++ + Qt Widgets + 固定微软 ConPTY 正式目标 | [ADR-0007](adr/0007-bundled-microsoft-conpty.md) / [ADR-0008](adr/0008-cpp-core-qt-widgets.md) 记录用户确认;新组合的实现与验证另行留证,不引用旧默认或 Alacritty 通过结果代替 | diff --git a/docs/FEATURES.md b/docs/FEATURES.md index cfe67a5f..ce6fa6b1 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -25,6 +25,8 @@ |---|---|---|---| | 自主 C++ / Qt / 固定微软 ConPTY | **实现中,未验收**:[显式 Qt 入口](../src/qt/README.md)提供固定 80×24 的 cmd 标签、基本 ASCII/16 色及各会话独立有界队列;双 cmd 输入、关闭和错误隔离及真实事件循环退出前回收有[限定验证](../experiments/shell/results/2026-10-28-qt-tabs/README.md)。Qt 1 项与其余 49 项既有/独立诊断分列 | 分屏、其他 shell、工作区恢复、动态 resize、历史/选择/Find、Unicode/IME、真实 DPI、应用兼容与性能仍需实现或迁移;应用/标签退出策略沿原型合同,完整 WindTerm 标签行为对照未完成;当前不是可发布开发包 | R2 | +自主 C++ 核心的 [#130 双缓冲存储基础](../experiments/core/results/2026-10-29-buffer-state-r2/README.md)已在独立模块边界实现拥有状态、交换激活、共享行 ID、双帧物理 resize 与文档 epoch 隔离,旧位置、选择、搜索及 viewport 不会在切换返回后复活。该基础尚未接入 private CSI;1049 的样式、重复进入清空及历史文档策略与完整 [#27](https://github.com/JavaZeroo/termforge/issues/27)另行验收,不表示产品已支持备用屏或 Qt 交互已迁移。 + 官方参考的基础文字行为已按 [R1-01 合同](research/windterm-r1-text-contract.md)完成两组各两轮的自动取证;这为独立实现提供行为依据,产品能力仍按下表分别验收。 | 能力 | 当前状态与边界 | 目标差距 | 路标 | diff --git a/experiments/core/results/2026-10-29-buffer-state-r2/README.md b/experiments/core/results/2026-10-29-buffer-state-r2/README.md new file mode 100644 index 00000000..568087ec --- /dev/null +++ b/experiments/core/results/2026-10-29-buffer-state-r2/README.md @@ -0,0 +1,40 @@ +# 自主 C++ 双缓冲存储与文档世代基础(#130) + +本记录只验证 [#130](https://github.com/JavaZeroo/termforge/issues/130) 的独立工程基础:两个缓冲拥有各自状态,交换激活并隔离旧读者。**未接入 private CSI,也不验收 1049、完整 #27 或 R2。** + +运行源码固定为 `63589fa21b68c3029501772b7bb415e66aa99e97`,已合入 main 基点 `337812bc27b10a8f4dee5e4e35ded21c90f4a151`。后续能力矩阵和本证据整理是文档提交,不改变该运行身份。字段、命令、源码/EXE 及原始本地记录哈希见 [validation.json](validation.json)。 + +## 实现与断言边界 + +- `Parser::screen` 保留原对象和主屏接口;另一帧私有拥有文本、光标、样式、pending-wrap、combining attachment、历史与丢弃计数。激活只交换状态,不复制保留历史;共享行 ID 分配器不随帧恢复。 +- 主屏保持原历史容量,备用存储历史上限为零;两帧共享尺寸。物理行 resize 先准备两帧,再提交;隐藏帧分配 ID 耗尽时,两帧内容、几何和分配器均不半更新。零历史是工程界限,不代表原版文档浏览合同。 +- `TextPosition` 带单调切换 epoch;selection、search 和 history viewport 检查世代。旧句柄不会因主屏返回且行 ID/version 保留而复活;非零 epoch 下新建的 alt/main 选择、查询及 viewport 仍可使用。未切换的默认 epoch 为零,现有三字段位置初始化及 JSON schema 保持兼容。 +- [独立公开边界测试](../../buffer_state_test.cpp)使用自写字面内容验证两帧往返、光标/样式/wrap/附件、隔离历史、宽字裁剪、软硬行元数据、增长 ID、resize 回滚与旧句柄失效;没有通过测试内部调用次数代替可见内容断言。 + +原版样式继承、重复进入清空、保存/恢复协议策略及历史文档呈现均不在本项选择。存储激活的幂等性不能用来宣称原版 1049 重复进入也幂等。 + +## 固定配置与完整回归 + +Windows 11 `10.0.26200`、x64 Release,Visual Studio 2022 / MSVC `19.42.34433.0`,Python `3.12.10`。`TERMFORGE_BUILD_QT_SHELL=OFF`,`TERMFORGE_BUILD_CANDIDATE_ADAPTER=OFF`;既有 Windows 诊断使用系统 ConPTY。**本轮不构建 Qt/固定 ConPTY 产品组合,也不构建 Alacritty 参考。** + +完整 configure 和 build 均退出 **0**;完整 CTest **50/50、退出 0**,耗时 **355.09 秒**。这是测试耗时,不是终端性能指标。运行前已观察固定提交及干净源码,完成后采集精选源码、45 个 EXE 和完整日志身份;没有单独的运行前冻结 manifest,也不声称可复现构建证明。 + +主体命令为: + +```powershell +cmake -S . -B build/core-buffer-state -G 'Visual Studio 17 2022' -A x64 -DTERMFORGE_BUILD_QT_SHELL=OFF -DTERMFORGE_BUILD_CANDIDATE_ADAPTER=OFF -DPython3_EXECUTABLE= +cmake --build build/core-buffer-state --config Release +ctest --test-dir build/core-buffer-state -C Release --output-on-failure --output-junit <工作树>/local/buffer-state-r2/13-main-all-ctest.xml +``` + +仅测试进程的 PATH 前置 Git `usr/bin`,并设置 `PYTHONUTF8=1`、`TERMFORGE_REQUIRE_SYSTEM_CLIPBOARD=0`。路径占位符替代私人绝对路径。原始 configure/build/CTest/JUnit、退出码文件及完整 `LastTest.log` 保存在本地;JSON 公开其实际字节数与哈希,不发布原件。 + +CTest 注册级没有 skip,但 Python 框架内部有 **12 条 skip**,逐条名称及原因在 JSON:4 个可选参考整类、7 个需要 disposable hosted VM 的实际系统剪贴板 UI 场景,以及1个 opt-in 原版失败注入。成功 JUnit 输出被 CTest 截短,内部 skip 以完整 `LastTest.log` 为准。`paste_system` 注册及其中的系统 API 检查实际通过,不能替代上述跳过的 UI 场景或完整剪贴板验收,也不转为 Qt 能力。 + +## 分次失败与审查 + +7 条实际运行红分别覆盖 writing context、备用历史界限、隐藏 resize、旧 position、selection、search 和 viewport;每条的失败日志及当时源码/测试/EXE 身份记录均保留,脱敏哈希见 JSON。接口编译失败、选择测试夹具参数错误、早期只构建部分 target 导致缺 `receive.exe` 的回归准备失败另列,不计为算法运行红;最终整轮已完整构建该 target。 + +双轴审查无未解决阻断;原字段交换清单的重复已用私有同一 helper 消除,复审确认没有引入 copy 或改变 `noexcept` 边界。没有独立审查报告附件,因此不补造报告哈希。远端 hosted 检查属于 PR 的独立运行,不并入本地 50 项计数。 + +本公开记录仅含合成断言、脱敏配置、计数及身份哈希;不包含 WindTerm 私有材料、资源、会话、原始抓取或私人路径。它不证明原版一致性、完整备用屏、GUI/TUI 组合、IME/真实 DPI、全平台完整产品、可分发包或性能达标。 diff --git a/experiments/core/results/2026-10-29-buffer-state-r2/validation.json b/experiments/core/results/2026-10-29-buffer-state-r2/validation.json new file mode 100644 index 00000000..ddf04712 --- /dev/null +++ b/experiments/core/results/2026-10-29-buffer-state-r2/validation.json @@ -0,0 +1,570 @@ +{ + "schema": "termforge-cpp-buffer-state-validation-v1", + "date": "2026-10-29", + "issue": "https://github.com/JavaZeroo/termforge/issues/130", + "runtime_source_commit": "63589fa21b68c3029501772b7bb415e66aa99e97", + "merged_main_base": "337812bc27b10a8f4dee5e4e35ded21c90f4a151", + "scope": "Owned bounded screen frames and document epoch foundation only; no private CSI dispatch or WindTerm mode acceptance", + "provenance": "Independently authored synthetic literals and existing synthetic regression suites; no WindTerm artifacts or private profiles published", + "configuration": { + "platform": "Windows-11-10.0.26200-SP0", + "architecture": "x64", + "generator": "Visual Studio 17 2022", + "compiler_version": "19.42.34433.0", + "configuration": "Release", + "python_version": "3.12.10", + "qt_shell": false, + "candidate_adapter": false, + "existing_windows_diagnostics_backend": "system ConPTY; not bundled product-route validation" + }, + "environment": { + "PATH_prepend": "/usr/bin", + "PYTHONUTF8": "1", + "TERMFORGE_REQUIRE_SYSTEM_CLIPBOARD": "0" + }, + "commands": [ + "cmake -S . -B build/core-buffer-state -G 'Visual Studio 17 2022' -A x64 -DTERMFORGE_BUILD_QT_SHELL=OFF -DTERMFORGE_BUILD_CANDIDATE_ADAPTER=OFF -DPython3_EXECUTABLE=", + "cmake --build build/core-buffer-state --config Release", + "ctest --test-dir build/core-buffer-state -C Release --output-on-failure --output-junit /local/buffer-state-r2/13-main-all-ctest.xml" + ], + "local_result": { + "configure_exit_code": 0, + "build_exit_code": 0, + "ctest_exit_code": 0, + "ctest_registered": 50, + "ctest_passed": 50, + "ctest_skipped": 0, + "internal_skip_source": "Complete LastTest.log; successful JUnit system-out sections are truncated and do not contain all skips", + "internal_skip_entries": [ + { + "ctest": "differential", + "detail": "setUpClass (test_candidate_compare.CandidateComparisonTests) ... skipped 'optional Alacritty binary is not built; core-candidate workflow runs these checks'" + }, + { + "ctest": "differential", + "detail": "setUpClass (test_capability_matrix.CapabilityMatrixTests) ... skipped 'optional Alacritty binary is not built; core-candidate workflow runs these checks'" + }, + { + "ctest": "differential", + "detail": "setUpClass (test_fuzz_matrix.FuzzMatrixTests) ... skipped 'optional Alacritty binary is not built; core-candidate workflow runs these checks'" + }, + { + "ctest": "differential", + "detail": "setUpClass (test_real_app_matrix.RealApplicationMatrixTests) ... skipped 'optional real application capture dependencies are not built'" + }, + { + "ctest": "text_actions", + "detail": "test_actual_system_copy_paste_and_shortcuts_twice (experiments.shell.test_actions.ActionWindowTest.test_actual_system_copy_paste_and_shortcuts_twice) ... skipped 'actual system Copy/Paste requires a disposable hosted VM'" + }, + { + "ctest": "research_guards", + "detail": "test_suspended_target_is_reclaimed_if_job_assignment_fails (test_ui_r1.EvidenceBoundaryTests.test_suspended_target_is_reclaimed_if_job_assignment_fails) ... skipped 'opt-in pinned reference failure injection'" + }, + { + "ctest": "shell", + "detail": "test_mouse_selection_to_real_system_clipboard (experiments.shell.test_shell.ShellTests.test_mouse_selection_to_real_system_clipboard) ... skipped 'system copy UI gate requires a disposable hosted VM'" + }, + { + "ctest": "shell", + "detail": "test_system_clipboard_product_read_errors_cancel_and_close (experiments.shell.test_shell.ShellTests.test_system_clipboard_product_read_errors_cancel_and_close) ... skipped 'product clipboard read gate requires a disposable hosted VM'" + }, + { + "ctest": "shell", + "detail": "test_system_clipboard_shortcut_to_window_confirmation_and_receiver (experiments.shell.test_shell.ShellTests.test_system_clipboard_shortcut_to_window_confirmation_and_receiver) ... skipped 'real system clipboard UI gate requires a disposable hosted VM'" + }, + { + "ctest": "selection_bytes", + "detail": "test_owned_clipboard_contention_preserves_write_and_read_failures (experiments.shell.test_selection_bytes.SelectionBytesWindowTest.test_owned_clipboard_contention_preserves_write_and_read_failures) ... skipped 'system CF requires a disposable hosted VM'" + }, + { + "ctest": "selection_bytes", + "detail": "test_real_system_copy_shortcuts_for_styled_fixture_twice (experiments.shell.test_selection_bytes.SelectionBytesWindowTest.test_real_system_copy_shortcuts_for_styled_fixture_twice) ... skipped 'system CF requires a disposable hosted VM'" + }, + { + "ctest": "selection_context", + "detail": "test_real_system_copy_and_edit_context_twice (experiments.shell.test_selection_context.SelectionContextWindowTest.test_real_system_copy_and_edit_context_twice) ... skipped 'real CF/EDIT Copy requires a disposable hosted VM'" + } + ], + "ctest_elapsed_seconds": 355.09, + "time_is_test_duration_not_terminal_performance": true + }, + "review": { + "standards_unresolved_blockers": 0, + "spec_unresolved_findings": 0, + "swap_helper_followup": "Parent reviewer confirmed shared private field-list swap is equivalent and introduces no new blocker; no separate report attachment" + }, + "runtime_reds": [ + { + "stage": "02-context", + "identity": { + "screen_sha256": "2015EB6E0B3A4BF70151AF0F566341FDDB0F7CCE51E47E08032A448313A2285E", + "test_sha256": "FDA5955B6702AF3A79E4A81DFC75CCE9B5536F945C6A8104F35CD6AA8A03C1D2", + "exe_sha256": "4B92EA7BBBA2C52BC548C167BAAA5222E541DD270C7B38B6A82B24A1DB40C889", + "exit_code": 1 + }, + "identity_record": { + "path": "local/buffer-state-r2/02-context-red-identity.json", + "bytes": 283, + "sha256": "5629acc21d04f3f3280c5c3e0b09810d0f987981f41107781e8d0fccfc4ff99f" + }, + "runtime_log": { + "path": "local/buffer-state-r2/02-context-red.log", + "bytes": 43, + "sha256": "cf79521f5b48970f8a73488c1c49d7acd7f882e5beace91ba15450f61464fb85" + } + }, + { + "stage": "03-history", + "identity": { + "screen_sha256": "06489F0F855FC41848EBABB74E9E284221963299902BBD7BE3F10FD30EEBEFDF", + "test_sha256": "54E965C24E7E00A5656E6020CA29FF408E29FE46FA6CFE0E3F9195B9FF5167EA", + "exe_sha256": "5BF7B736BFE636CE4CA704A190F0819D007F5DEEE1BB1C975DE068C5B10F23AC", + "exit_code": 1 + }, + "identity_record": { + "path": "local/buffer-state-r2/03-history-red-identity.json", + "bytes": 283, + "sha256": "9028dfa690a67a8cd9bff793ed4e7ba1fb8c59aa9c9b8004c64eac52a1dc6a62" + }, + "runtime_log": { + "path": "local/buffer-state-r2/03-history-red.log", + "bytes": 70, + "sha256": "cea8e2327b71792e137bcc96f1acfdfda97a4bf621370d331f59a45779ba26ba" + } + }, + { + "stage": "04-resize", + "identity": { + "screen_sha256": "4DE1F23A2BA0165AE9B2AB5D7AF0585EB5D662E81BF571CAD187551DA8423C88", + "test_sha256": "76F408D3EB7D23E76E268E5D3D8818CCC70BC16A77E4694A4A9C71D909EC1B57", + "exe_sha256": "FEC89BA8B516F8A7B5F4F6CDC8BC99BE0879CE54227C3B94E4F71968DD8D3925", + "exit_code": 1 + }, + "identity_record": { + "path": "local/buffer-state-r2/04-resize-red-identity.json", + "bytes": 283, + "sha256": "a050de7f729d17a2d3d2742427e755e21f1490041c23f15da70a1a071d6c41e7" + }, + "runtime_log": { + "path": "local/buffer-state-r2/04-resize-red.log", + "bytes": 41, + "sha256": "20ecb09b2ffc21ce93ce4b83cde5c1cbb7f6df5922f24fd424baab9cd53ef1a1" + } + }, + { + "stage": "05-position", + "identity": { + "screen_sha256": "58CCBE99894094BDA250A0C1A65BA4E7A7DBB99CCF03E48C3C820AAB8CFED8CC", + "document_sha256": "87EFAE9D6A9FCCB88DD353CDD1BE6A570480B6942FABBDF3C9DAC2C6370E026A", + "test_sha256": "324D846C7041C9646728874B4B8AD66AE430601FCC2F72FA9C1078575B90A6E8", + "exe_sha256": "4DACDEF02CC1D55E616DD6C82979EF330BAD5EFBF96DEA7EE019D8218176BD5D", + "exit_code": 1 + }, + "identity_record": { + "path": "local/buffer-state-r2/05-position-red-identity.json", + "bytes": 373, + "sha256": "715c4e07624dbdd5a72db3c63da9766b8685c3d336af8f1245ab9a9073f91e48" + }, + "runtime_log": { + "path": "local/buffer-state-r2/05-position-red.log", + "bytes": 55, + "sha256": "a7756a0c28cbe7770e9525a0a561dc189457bbd0cf5794bf0391de293b2406a4" + } + }, + { + "stage": "06-selection", + "identity": { + "selection_sha256": "7D41D58D434AD3C32CE2F67C043E337277ABBBC69E4A7A2B6BDF7E519F9F5462", + "test_sha256": "9F84B71EF85E56085851A8C11A8F423868FDBA8AEB72E0B1B70F100CE3E1B082", + "exe_sha256": "BDE0CEBEFDAB410741A5A6BC00F3BF246FD25A9E3249D6AA9C8ACE8D8D1BCDBC", + "exit_code": 1 + }, + "identity_record": { + "path": "local/buffer-state-r2/06-selection-red-identity.json", + "bytes": 286, + "sha256": "2f682c18496f1392d9fcf9e67504a3f8abd147c3b1f8a87dad8a04f20782ec85" + }, + "runtime_log": { + "path": "local/buffer-state-r2/06-selection-red.log", + "bytes": 67, + "sha256": "b40793bab48cb3a322585ac19f27a79ea64c02400e4742c8ee00aac15f64ab6f" + } + }, + { + "stage": "07-search", + "identity": { + "search_sha256": "FCC64B96C1F8D39B3A09DC45FFE537E7DC0FD70727B4D46768706AB55467CAAC", + "test_sha256": "6AFED61869F5EC83608949DE3B4AD1E6C93ECB2DFC921711A8AC016DA63E9F02", + "exe_sha256": "598DB3EFFD3E094C23ED1462635C14C9133EB68CA778DF41860F36F1804FBA4A", + "exit_code": 1 + }, + "identity_record": { + "path": "local/buffer-state-r2/07-search-red-identity.json", + "bytes": 283, + "sha256": "18ec93a7fe301f4132897fecdbb296d14800869b7741cb82bfe8f42a9af97fb6" + }, + "runtime_log": { + "path": "local/buffer-state-r2/07-search-red.log", + "bytes": 56, + "sha256": "88e72fcaf25e0390ae58e3c90174067428957396df80792df7bcc92c4d89ddc6" + } + }, + { + "stage": "08-viewport", + "identity": { + "exit_code": 1, + "viewport_sha256": "BC6218D05CC331AD3F39DB545575FAF0D100713DC8CB8AD09DBF3EE635C4696D", + "exe_sha256": "384192AB8699123E42003F882A600183F20974A03BA8AD641CC0EFCC9BFB18C7", + "test_sha256": "80A744959414BD94467BC0DF6C234C89F80D13F8AD4E21075D67AAB03AD83208" + }, + "identity_record": { + "path": "local/buffer-state-r2/08-viewport-red-identity.json", + "bytes": 285, + "sha256": "487fa9de1055bd52372a15bb7ded27b634b6492c9be6be053aa209c2ed9dcda7" + }, + "runtime_log": { + "path": "local/buffer-state-r2/08-viewport-red.log", + "bytes": 60, + "sha256": "be49afc085679fc3c7050e9be1a778b7f6f86389c2a87237263ea70a1315cc3a" + } + } + ], + "preparation_failures": [ + { + "kind": "interface compile failure", + "record": { + "path": "local/buffer-state-r2/01-interface-red.log", + "bytes": 4377, + "sha256": "c5553816dee0cd70011cdcfb96644681dc82c4eee53c778dc3520d9e1a6d45ab" + }, + "counts_as_runtime_red": false + }, + { + "kind": "selection fixture missing required copy limit", + "record": { + "path": "local/buffer-state-r2/06-selection-fixture-compile-error.log", + "bytes": 1826, + "sha256": "fb3da1789af0cb2c05f699d8a9bcc38f52f899d7b26d90c28e4320af12987feb" + }, + "counts_as_runtime_red": false + }, + { + "kind": "earlier related CTest core group missing unbuilt receive.exe", + "record": { + "path": "local/buffer-state-r2/04-existing-ctest.log", + "bytes": 5312, + "sha256": "640f1d27bf749d73fce97a7d2b56bd09ff61e4df78fba21178066de207f187a8" + }, + "counts_as_runtime_red": false + } + ], + "post_run_source_identity": [ + { + "path": "CMakeLists.txt", + "bytes": 23846, + "sha256": "2d5a395ee1890c12f4e4f560122bc6ae682386beed5629f5206fc7ac4121d78f" + }, + { + "path": "experiments/core/screen.hpp", + "bytes": 16884, + "sha256": "534068ce7fc2d9b5436c87b84626d9cf5b9e9bf2811a9a789c03c77925786ccc" + }, + { + "path": "experiments/core/terminal.hpp", + "bytes": 11155, + "sha256": "e5ca6b065b38b98ba4bb32f579e4f0e8e126a7872de147ff37c2c68aac414358" + }, + { + "path": "experiments/core/text_document.hpp", + "bytes": 3850, + "sha256": "a51903b9091471b0d52c47f34b966f56b23045042c007fe8cd6f6e1b1302934c" + }, + { + "path": "experiments/core/text_selection.hpp", + "bytes": 9883, + "sha256": "5c3848d06bcb34f036aabec7d3e8a953796caed1ba90a8c8d40b62d3e2242bb0" + }, + { + "path": "experiments/core/text_search.hpp", + "bytes": 18533, + "sha256": "77b2aa3b0989fca32ec953ca5376085bf141011be74d014cac646eb0bde15d2f" + }, + { + "path": "experiments/core/history_view.hpp", + "bytes": 3121, + "sha256": "74370e3d3a45920d495cf7edce1cc57916ec8e6d0c221e1144a5a3bcdd8ecda6" + }, + { + "path": "experiments/core/buffer_state_test.cpp", + "bytes": 17118, + "sha256": "9b7a6705b7c43774bb35c44adcf3c10f9490c4a31993c08c4480024ac4ebe4a4" + } + ], + "post_run_executable_identity": [ + { + "path": "build/core-buffer-state/bin/Release/alpha_normal_smoke.exe", + "bytes": 68608, + "sha256": "e9d509ed0b708922f9893f73e01fb91792c99a1a7ba29200831c3229ce57deb0" + }, + { + "path": "build/core-buffer-state/bin/Release/buffer_state_test.exe", + "bytes": 132096, + "sha256": "0926180f356e24ac0c201d1bf65bb5f2728d12283b1f4e85f3c0f46bd7c3e5a5" + }, + { + "path": "build/core-buffer-state/bin/Release/clipboard_policy_test.exe", + "bytes": 31232, + "sha256": "8fc64bbde65da8e9ba0fa4c0e76c2233f79b20aa294fd1747d34c67d8d9e4230" + }, + { + "path": "build/core-buffer-state/bin/Release/conpty_ctrl_c_probe.exe", + "bytes": 60416, + "sha256": "83666324bb263d6ad1003f37d85cd0596f149f86e241e6d478aabdfe85eac798" + }, + { + "path": "build/core-buffer-state/bin/Release/conpty_margin_probe.exe", + "bytes": 109056, + "sha256": "787761091e18016959e2352b3969132edb793d9cc729672d41212265b39c4f43" + }, + { + "path": "build/core-buffer-state/bin/Release/core_measure.exe", + "bytes": 83968, + "sha256": "ad761e8b5df12b4bec75cce4ac07ec33dc6f50afa2b09de9108c7219cb676929" + }, + { + "path": "build/core-buffer-state/bin/Release/find_bar_test.exe", + "bytes": 53760, + "sha256": "609ba5bf24e935c4163a3aee8edede2b649d421c781f55bc03400fe36991eece" + }, + { + "path": "build/core-buffer-state/bin/Release/history_test.exe", + "bytes": 115200, + "sha256": "fa0d6b2139885c573dc3bf72907f7276973c67edd18e1d6e6d380d0d7bfdf119" + }, + { + "path": "build/core-buffer-state/bin/Release/ime_context_test.exe", + "bytes": 47104, + "sha256": "776a4762ffdb27f7de1584cab0e1c5cd6d3fdf34f2d92cbdee86162b6b8617f4" + }, + { + "path": "build/core-buffer-state/bin/Release/ime_input_expectation_test.exe", + "bytes": 24576, + "sha256": "647829e99cbe771ba72555ac0703ec13e17f4c976b7f0c1e1aaf688264f2d1e1" + }, + { + "path": "build/core-buffer-state/bin/Release/keyboard_test.exe", + "bytes": 33792, + "sha256": "c205b6dc869928b1efa5da7dd56c7e98ba5bec9796ff463d8b733a0603ab0cea" + }, + { + "path": "build/core-buffer-state/bin/Release/layout_state_test.exe", + "bytes": 99328, + "sha256": "26aa719ed7f7aeb107f2f644bdda1c1f50efe34d6c889e3a8334ad20392ce703" + }, + { + "path": "build/core-buffer-state/bin/Release/paste_test.exe", + "bytes": 47104, + "sha256": "e4077916d42ee9d8b5388af8985364104d5293916e3794b06424f74a2bbefe1d" + }, + { + "path": "build/core-buffer-state/bin/Release/presentation_probe.exe", + "bytes": 124928, + "sha256": "a4e16aa3a52a438a217702bcf749deeb293115f7fbda796faa1014c590accacb" + }, + { + "path": "build/core-buffer-state/bin/Release/raw_console_receiver_test.exe", + "bytes": 94720, + "sha256": "053bcd7e2122648d6136dcfcd7fac6ad4c4939782bda0a31ce1f9583a2e0f06b" + }, + { + "path": "build/core-buffer-state/bin/Release/real_app_capture.exe", + "bytes": 90624, + "sha256": "6e6962d5c28bae9e31a5786f7d1423da8f63d7ab4425468edc2d00d79ad2b633" + }, + { + "path": "build/core-buffer-state/bin/Release/receive.exe", + "bytes": 91648, + "sha256": "8f27a353e696c5f8e1bd4f744f4e9322b31bf058ba42c26ef4617ed3ca0ef019" + }, + { + "path": "build/core-buffer-state/bin/Release/recovery_probe.exe", + "bytes": 84992, + "sha256": "d3dad682985d88ba2a85d9961c3aed0e9540138c4132831877fba7e8e5d111e6" + }, + { + "path": "build/core-buffer-state/bin/Release/renderer_metrics_test.exe", + "bytes": 129024, + "sha256": "e0f7b6b1d6c86a92c5fd1b51a7156855c8ec9875fc4eb87717f8ea273f5d036c" + }, + { + "path": "build/core-buffer-state/bin/Release/replay.exe", + "bytes": 75776, + "sha256": "6c5ffc3c7e538f3a07df1a15090e70e231d345bea8727897f1879d28c35f40e2" + }, + { + "path": "build/core-buffer-state/bin/Release/resize_test.exe", + "bytes": 102912, + "sha256": "661ef6340995b6bd289d0fb8c392d76642c15fc1497059f532722df53afb9919" + }, + { + "path": "build/core-buffer-state/bin/Release/runtime_observation_test.exe", + "bytes": 361984, + "sha256": "5a4922858c0ab736d4e5d330571fe08f9268997733ea462849229e312f6c24cd" + }, + { + "path": "build/core-buffer-state/bin/Release/scenario_tick_gate_test.exe", + "bytes": 17408, + "sha256": "cf45cba8fa48d16ed94fdf4c36ab09ec7f1a5d74c57219e2b491135048c4bdf6" + }, + { + "path": "build/core-buffer-state/bin/Release/search_test.exe", + "bytes": 146944, + "sha256": "7147508550102adac00e9eb07399d6ae5e0e2b4986c1d3d6b2b22ae331693366" + }, + { + "path": "build/core-buffer-state/bin/Release/selection_test.exe", + "bytes": 119808, + "sha256": "81d9ef66f38a600aae0cad8be12c116d9fbaff05e371ce25019019301250b023" + }, + { + "path": "build/core-buffer-state/bin/Release/sent_message_wait_test.exe", + "bytes": 34304, + "sha256": "ccd3046f50b3663159bf072d80ec41ea05c776df56f0a6f5bf1ea7fc349f9d47" + }, + { + "path": "build/core-buffer-state/bin/Release/shell_clipboard_reader.exe", + "bytes": 22528, + "sha256": "33747c661ed4c5ef7707c1ce7477774cd695482c982692ed313c89d94b9ca14f" + }, + { + "path": "build/core-buffer-state/bin/Release/shell_ime_client.exe", + "bytes": 126976, + "sha256": "b66d3ba9c77b4eee3c1093c805f6616a662519b7be2458f8877296923ef9d9e5" + }, + { + "path": "build/core-buffer-state/bin/Release/shell_input_client.exe", + "bytes": 41984, + "sha256": "bade38e969d86392175ba6a1d143fb195e4e9a06d79549756ea42590c0fee7de" + }, + { + "path": "build/core-buffer-state/bin/Release/shell_launch_test.exe", + "bytes": 50176, + "sha256": "1824b32a18c4c5c4ed7199344da06cb0720f2c619878f380e0d5ee7059cbd886" + }, + { + "path": "build/core-buffer-state/bin/Release/shell_paste_probe.exe", + "bytes": 195072, + "sha256": "a7e2d81759cd7b44fdd9481865d85125a00edf1ab314500d20571478f27e4fdf" + }, + { + "path": "build/core-buffer-state/bin/Release/shell_query_client.exe", + "bytes": 32768, + "sha256": "3b6c8cfbc2be8ef53f82880046c7473439db205292fd47c33e36465f65baf41e" + }, + { + "path": "build/core-buffer-state/bin/Release/shell_queue_probe.exe", + "bytes": 121344, + "sha256": "a994327ee92c127b350928e84ba89dd00e1329743781a8c8b90a098c42ce0bc9" + }, + { + "path": "build/core-buffer-state/bin/Release/shell_selection_text_client.exe", + "bytes": 56832, + "sha256": "29e675d87225bd28a2c78cdf7f3035449202017eccdc4ae98e32579b0b1c259f" + }, + { + "path": "build/core-buffer-state/bin/Release/shell_size_client.exe", + "bytes": 18432, + "sha256": "b2f39f8c47ee9eb354461fadefe535fc8c7559b2a2294139cba194500fd01296" + }, + { + "path": "build/core-buffer-state/bin/Release/shell_test_launcher.exe", + "bytes": 47616, + "sha256": "5885bf37f40a18e2f310a5a9a233b46e1b6df6fc7b6edcdf334a758baaa7266e" + }, + { + "path": "build/core-buffer-state/bin/Release/shell_text_client.exe", + "bytes": 147968, + "sha256": "496504291e46831915150a983efec60079f7473d00797892944558e102be06f4" + }, + { + "path": "build/core-buffer-state/bin/Release/termforge_conpty_probe.exe", + "bytes": 75776, + "sha256": "0d98ab38b6d05859e002db9a60902621a09a6503021f8cbbdb63972f9217f30d" + }, + { + "path": "build/core-buffer-state/bin/Release/termforge_ime_probe.exe", + "bytes": 65024, + "sha256": "e3a1cf0e37b70a1636f1c0a35adcd1024cccb6a2df1746d4a7c69336564b3892" + }, + { + "path": "build/core-buffer-state/bin/Release/termforge_shell.exe", + "bytes": 1234432, + "sha256": "49ccfaf3d1261a3665f9d7acff99eca4cdd167abc32a780627163dabe7dd144e" + }, + { + "path": "build/core-buffer-state/bin/Release/terminal_trace.exe", + "bytes": 81920, + "sha256": "2467e31fe631162702d8512cbb442df62a7102a1d6745bc0c7c0562d4be6cc90" + }, + { + "path": "build/core-buffer-state/bin/Release/text_action_policy_test.exe", + "bytes": 19968, + "sha256": "c0b19e83213b61b7c94164262d3189bf61443ca21293349071991f6a10ae9f49" + }, + { + "path": "build/core-buffer-state/bin/Release/transport_completion_test.exe", + "bytes": 44032, + "sha256": "dcde924f99c1d6ed1848aeb08b8c0c942b52c47f374772465442fb1b21dcadc8" + }, + { + "path": "build/core-buffer-state/bin/Release/workflow_fixture_test.exe", + "bytes": 134144, + "sha256": "3a5e97f189796bb66035a2dc28703057c88cc3ca84470e65c1ed7a82a955e5de" + }, + { + "path": "build/core-buffer-state/bin/Release/workspace_state_test.exe", + "bytes": 140288, + "sha256": "2ee17b20f94a341415f953107a8325926c716b70c71833f09ae42c27fc2c4d26" + } + ], + "raw_local_records": [ + { + "path": "local/buffer-state-r2/13-main-configure.log", + "bytes": 232, + "sha256": "12fe34e68441bfa729e3d50f163eaedbf5f74112a45d7614c7c80aca684cf5fd" + }, + { + "path": "local/buffer-state-r2/13-main-all-build.log", + "bytes": 10582, + "sha256": "e8b4c49735c565790c8c5261c5e4089423c1d19d2dc136ad1f4f46a73ce89f0c" + }, + { + "path": "local/buffer-state-r2/13-main-all-ctest.log", + "bytes": 5752, + "sha256": "0ad6c321a56f37d6f99605808948b17da372cabf5da453d3030b9f0e5c82f592" + }, + { + "path": "local/buffer-state-r2/13-main-all-ctest.xml", + "bytes": 40706, + "sha256": "aa03971dc335d48d994e0a0542e06cd991ee8b737ba9fd7d84b36f00f372ee55" + }, + { + "path": "local/buffer-state-r2/13-main-all-ctest.exit", + "bytes": 3, + "sha256": "13bf7b3039c63bf5a50491fa3cfd8eb4e699d1ba1436315aef9cbe5711530354" + }, + { + "path": "build/core-buffer-state/Testing/Temporary/LastTest.log", + "bytes": 133444, + "sha256": "023edcdfdcd45bf2059d3d5fab0454a91b8db73184459e6d84cef4034630ffba" + } + ], + "identity_limit": "Commit and clean source were observed before the supervised build/test. Listed hashes were collected after completion, not a separately frozen pre-run input manifest or reproducible-build proof.", + "hosted_checks": "Independent PR checks pending at local evidence publication; results are not folded into local counts", + "limits": [ + "No private CSI/1049 handler wiring", + "No choice of original style inheritance, repeat-entry clearing or history document presentation", + "Alternate storage history cap zero is an engineering bound, not a WindTerm comparison", + "No product Qt/fixed-ConPTY, original mode parity, complete #27, R2, clipboard/IME or performance acceptance claim" + ] +}