From 33b58fa4452c4d4f23dcfc463f65c5c521e59065 Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Thu, 29 Oct 2026 16:15:45 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat(core):=20=E5=8F=91=E5=B8=83=E7=B2=BE?= =?UTF-8?q?=E7=A1=AE=E8=A1=8C=E5=BA=8F=E5=B9=B6=E8=A7=A3=E8=80=A6=E8=BA=AB?= =?UTF-8?q?=E4=BB=BD=E4=B8=8E=E6=96=87=E6=A1=A3=E4=BD=8D=E7=BD=AE=20refs?= =?UTF-8?q?=20#169?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 6 + experiments/core/candidate_terminal.cpp | 7 +- experiments/core/history_view.hpp | 12 +- experiments/core/order_query_test.cpp | 151 ++++++++++++++++++++++++ experiments/core/screen.hpp | 94 ++++++++++++++- experiments/core/text_document.hpp | 17 ++- experiments/core/text_search.hpp | 11 +- 7 files changed, 285 insertions(+), 13 deletions(-) create mode 100644 experiments/core/order_query_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 89b8803a..7948f3b9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -105,6 +105,10 @@ add_executable(decawm_mode_test experiments/core/decawm_mode_test.cpp) target_link_libraries(decawm_mode_test PRIVATE termforge_core) termforge_warnings(decawm_mode_test) +add_executable(order_query_test experiments/core/order_query_test.cpp) +target_link_libraries(order_query_test PRIVATE termforge_core) +termforge_warnings(order_query_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) @@ -307,6 +311,8 @@ if(BUILD_TESTING) add_test(NAME alternate_mode_core COMMAND alternate_mode_test) add_test(NAME decawm_mode_core COMMAND decawm_mode_test) set_tests_properties(decawm_mode_core PROPERTIES TIMEOUT 10) + add_test(NAME order_query_core COMMAND order_query_test) + set_tests_properties(order_query_core PROPERTIES TIMEOUT 10) add_test(NAME paste COMMAND paste_test) add_test(NAME clipboard_policy COMMAND clipboard_policy_test) add_test(NAME keyboard COMMAND keyboard_test) diff --git a/experiments/core/candidate_terminal.cpp b/experiments/core/candidate_terminal.cpp index cc12246e..41478ce9 100644 --- a/experiments/core/candidate_terminal.cpp +++ b/experiments/core/candidate_terminal.cpp @@ -281,6 +281,9 @@ void CandidateTerminal::synchronize() { screen.style = replay_origin_->style; screen.history = replay_origin_->history; screen.lines = replay_origin_->lines; + // This direct restore keeps the frozen frame image; the restored + // order is no longer provably the active screen's own sequence. + screen.mark_order_unknown(); } return; } @@ -348,8 +351,8 @@ void CandidateTerminal::synchronize() { screen.wrap_pending = meta.wrap_pending; screen.discarded = meta.discarded; screen.style = {meta.fg, meta.bg, meta.bold}; - screen.history.assign(retained.begin(), retained.begin() + meta.history); - screen.lines.assign(retained.begin() + meta.history, retained.end()); + screen.publish(std::vector(retained.begin(), retained.begin() + meta.history), + std::vector(retained.begin() + meta.history, retained.end())); versions_ = std::move(versions); identity_aliases_.clear(); for (size_t index = 0; index < retained.size(); ++index) diff --git a/experiments/core/history_view.hpp b/experiments/core/history_view.hpp index b948f5dd..e3c91ac2 100644 --- a/experiments/core/history_view.hpp +++ b/experiments/core/history_view.hpp @@ -24,10 +24,16 @@ class HistoryViewport { } public: // A viewport is owned by exactly one session. Never share it across Screens. + // A live anchor uses its exact rank; an evicted anchor keeps the existing + // ordered-history fallback (first retained row at or after the id). + size_t anchor_top(const Screen& screen) const { + if (const auto index = screen.locate(anchor_)) return (std::min)(*index, screen.history.size()); + return (std::min)(screen.lower_bound_line(anchor_), screen.history.size()); + } 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); + const size_t top = following_ ? maximum : anchor_top(screen); anchor_ = screen.retained_line(top).id; return {top, screen.retained_size(), static_cast(screen.height()), maximum, anchor_, following_}; } @@ -53,8 +59,8 @@ class HistoryViewport { } ViewportState bottom(const Screen& screen) { return scroll_to(screen, screen.history.size()); } ViewportState reveal(const Screen& screen, uint64_t identity) { - const auto index = screen.lower_bound_line(identity); - return scroll_to(screen, index); + if (const auto index = screen.locate(identity)) return scroll_to(screen, *index); + return scroll_to(screen, screen.lower_bound_line(identity)); } bool following() const { return following_; } uint64_t anchor() const { return anchor_; } diff --git a/experiments/core/order_query_test.cpp b/experiments/core/order_query_test.cpp new file mode 100644 index 00000000..2bf62f21 --- /dev/null +++ b/experiments/core/order_query_test.cpp @@ -0,0 +1,151 @@ +// Hand-authored rank/order expectations. A row identity is not a document +// position: a larger id may sit earlier in the retained sequence. +#include "history_view.hpp" +#include "screen.hpp" +#include "text_document.hpp" +#include "text_search.hpp" +#include +#include +#include +#include +#include +#include +#include +using namespace termforge; + +static void check(bool ok, const char* why) { if (!ok) throw std::runtime_error(why); } + +static Line row(int columns, uint64_t identity, char text) { + Line line(static_cast(columns), Cell{}); + line.id = identity; + line.version = 1; + line[0] = {std::string(1, text), 1, Style{}, 0, true}; + return line; +} + +static std::vector rows(int columns, std::initializer_list> spec) { + std::vector result; + for (const auto& item : spec) result.push_back(row(columns, item.first, item.second)); + return result; +} + +// The [1,3,4,5,7,6] shape: two live rows whose ids are not in document order. +static Screen reordered_screen() { + Screen screen(2, 4, 4); + screen.publish(rows(4, {{1, 'a'}, {3, 'b'}, {4, 'c'}, {5, 'd'}}), + rows(4, {{7, 'x'}, {6, 'x'}})); + return screen; +} + +static void published_order_is_exact() { + auto screen = reordered_screen(); + check(screen.retained_size() == 6, "published frame keeps every retained row"); + check(!screen.order_known(), "a reordered publication records that a bisect proof does not hold"); + check(screen.locate(7) == std::optional(4), "live identity 7 holds rank 4"); + check(screen.locate(6) == std::optional(5), "live identity 6 holds rank 5"); + check(screen.locate(5) == std::optional(3), "history identity 5 holds rank 3"); + check(!screen.locate(2), "a missing identity is never replaced by a neighbour"); + check(!screen.locate(0), "zero is not an identity"); + // The evicted-anchor fallback stays explicit and separate from exact lookup. + check(screen.lower_bound_line(2) == 1, "lower bound still resolves an insertion point"); + check(screen.retained_line(*screen.locate(6)).id == 6, "rank resolves back to the identity"); +} + +static void document_rank_and_compare() { + auto screen = reordered_screen(); + TextDocument doc(screen); + check(doc.index_of(7) == std::optional(4), "document rank follows the retained order"); + check(doc.index_of(6) == std::optional(5), "second reordered row keeps its own rank"); + check(!doc.index_of(2), "document reports a removed identity as missing"); + const auto earlier = doc.position(4, 0), later = doc.position(5, 0); + check(earlier && later, "positions exist for both live rows"); + check(doc.valid(*earlier) && doc.valid(*later), "published rows validate"); + check(doc.compare(*earlier, *later) == std::optional(-1), "7 precedes 6 in document order"); + check(doc.compare(*later, *earlier) == std::optional(1), "comparison is antisymmetric"); + check(doc.compare(*earlier, *earlier) == std::optional(0), "identical positions compare equal"); +} + +static void order_revision_tracks_structure_only() { + Screen screen(2, 4, 4); + const auto initial = screen.order_revision(); + screen.put('a', "a"); + check(screen.order_revision() == initial, "content writes are not structure changes"); + screen.newline(); + check(screen.order_revision() == initial, "a newline that only moves the cursor is not structural"); + screen.newline(); + check(screen.order_revision() > initial, "scrolling the bottom row changes structure"); + const auto scrolled = screen.order_revision(); + screen.resize(3, 4); + check(screen.order_revision() > scrolled, "physical resize changes structure"); + const auto resized = screen.order_revision(); + screen.resize(3, 4); + check(screen.order_revision() == resized, "an identical resize publishes nothing"); + screen.activate_buffer(ScreenBuffer::alternate); + check(screen.order_revision() > resized, "activating a frame changes the document owner"); + const auto published = screen.order_revision(); + screen.publish({}, {row(4, 900, 'z'), row(4, 901, 'w')}); + check(screen.order_revision() > published, "publishing different rows changes structure"); + const auto republished = screen.order_revision(); + screen.publish({}, {row(4, 900, 'z'), row(4, 901, 'w')}); + check(screen.order_revision() == republished, "republishing the same structure publishes nothing"); +} + +static void rejected_publication_commits_nothing() { + Screen screen(2, 4, 4); + const auto revision = screen.order_revision(); + const auto retained = screen.retained_size(); + const auto rejected = [&](auto&& call) { + try { call(); } catch (const std::invalid_argument&) { return true; } + return false; + }; + check(rejected([&] { screen.publish({}, rows(4, {{0, 'a'}, {1, 'b'}})); }), "zero identity rejected"); + check(rejected([&] { screen.publish(rows(4, {{3, 'a'}}), rows(4, {{3, 'b'}, {4, 'c'}})); }), + "duplicate identity rejected across history and live rows"); + check(rejected([&] { screen.publish({}, rows(3, {{9, 'a'}, {10, 'b'}})); }), "width mismatch rejected"); + check(rejected([&] { screen.publish({}, {}); }), "a frame without a live row is rejected"); + check(screen.order_revision() == revision && screen.retained_size() == retained && + screen.locate(screen.retained_line(0).id) == std::optional(0), + "a rejected publication leaves the published structure untouched"); +} + +static void search_navigation_follows_document_order() { + auto screen = reordered_screen(); + // Row 4 (id 7) holds "x"; row 5 (id 6) also holds "x". Document order says 7 + // comes first even though 6 < 7. + TextDocument doc(screen); + TextSearch search; + search.start(doc, "x"); + for (int guard = 0; guard < 256 && search.status() == SearchStatus::running; ++guard) + search.step(doc, search.generation(), 64); + check(search.matches().size() == 2, "both reordered rows produce a hit"); + const auto anchor = doc.position(5, 0); + check(anchor.has_value(), "anchor position exists"); + const auto previous = search.navigate(doc, SearchDirection::previous, anchor); + check(previous && previous->match.range.begin.line_id == 7, + "previous navigation uses document order, not identity order"); + const auto earlier = doc.position(4, 0); + const auto next = search.navigate(doc, SearchDirection::next, earlier); + check(next && next->match.range.begin.line_id == 6, "next navigation reaches the later live row"); +} + +static void viewport_uses_exact_rank() { + auto screen = reordered_screen(); + HistoryViewport view; + view.scroll_to(screen, 1); + check(view.reconcile(screen).anchor == 3, "paused viewport anchors on the row it shows"); + const auto state = view.reveal(screen, 5); + check(state.top == 3 && state.anchor == 5, "reveal scrolls to the exact published rank"); + check(view.reconcile(screen).top == 3, "the anchor survives reconciliation"); +} + +int main() { + try { + published_order_is_exact(); + document_rank_and_compare(); + order_revision_tracks_structure_only(); + rejected_publication_commits_nothing(); + search_navigation_follows_document_order(); + viewport_uses_exact_rank(); + std::cout << "exact identity rank, publication and document order passed\n"; + } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; } +} diff --git a/experiments/core/screen.hpp b/experiments/core/screen.hpp index 9057fbcb..0f1df5ee 100644 --- a/experiments/core/screen.hpp +++ b/experiments/core/screen.hpp @@ -61,11 +61,15 @@ class Screen { Style style; std::vector lines; std::deque history; + // True only while the retained sequence is known to be ordered by id. + // False is the safe fallback: lookups scan instead of bisecting. + bool ordered_ = true; 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); std::swap(style, other.style); + std::swap(ordered_, other.ordered_); lines.swap(other.lines); history.swap(other.history); } }; @@ -74,11 +78,13 @@ class Screen { int columns_; uint64_t next_id_ = 1; uint64_t buffer_epoch_ = 0; + uint64_t order_revision_ = 0; + bool ordered_ = true; 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}; + return {row, column, last_row, last_column, wrap_pending, discarded, style, lines, history, ordered_}; } void swap_active(FrameState& state) noexcept { state.swap(*this); @@ -88,6 +94,14 @@ class Screen { throw std::overflow_error("line version exhausted"); ++line.version; } + // Publishing a structure change is the single place that advances the + // document-order revision. Overflow fails before the caller mutates. + void advance_order(bool ordered) { + if (order_revision_ == (std::numeric_limits::max)()) + throw std::overflow_error("screen order revision exhausted"); + ++order_revision_; + ordered_ = ordered; + } static Line make_line(int columns, const Cell& cell, uint64_t& identity) { if (identity == (std::numeric_limits::max)()) throw std::overflow_error("line identities exhausted"); @@ -202,6 +216,7 @@ class Screen { active_buffer_ = buffer; next_id_ = identity; ++buffer_epoch_; + advance_order(ordered_); } size_t retained_size() const { return history.size() + lines.size(); } const Line& retained_line(size_t index) const { @@ -223,6 +238,81 @@ class Screen { const auto index = lower_bound_line(identity); return index < retained_size() && retained_line(index).id == identity ? &retained_line(index) : nullptr; } + // Owner-published structure revision: advances whenever retained membership, + // document order or the history/live split changes. It never stands in for a + // row content version or for the buffer epoch. + uint64_t order_revision() const { return order_revision_; } + // Exact retained rank for a live identity. A missing id is never replaced by + // a neighbour: callers that need the old evicted-anchor fallback must ask for + // it explicitly (lower_bound_line). + std::optional locate(uint64_t identity) const { + if (!identity) return std::nullopt; + if (ordered_) { + const auto index = lower_bound_line(identity); + if (index < retained_size() && retained_line(index).id == identity) return index; + return std::nullopt; + } + for (size_t index = 0; index < retained_size(); ++index) + if (retained_line(index).id == identity) return index; + return std::nullopt; + } + // A direct structural write that bypasses publish() must call this so later + // lookups scan instead of trusting a bisect that no longer holds. + void mark_order_unknown() { ordered_ = false; } + bool order_known() const { return ordered_; } + // Explicit publication of an owned row image (import/replay). Identities are + // validated, never renumbered: a duplicate or zero identity is rejected + // before anything is committed. Order is recorded so lookups stay exact. + // Cursor, style, discarded counters and the inactive frame are untouched. + void publish(std::vector history_rows, std::vector live_rows) { + const auto width = static_cast(columns_); + const auto validate = [width](const std::vector& rows, const char* what) { + for (const auto& line : rows) { + if (line.size() != width) throw std::invalid_argument(what); + if (!line.id) throw std::invalid_argument("published row identity is zero"); + } + }; + validate(history_rows, "published row width differs from the screen"); + validate(live_rows, "published row width differs from the screen"); + if (live_rows.empty() || live_rows.size() > 200) + throw std::invalid_argument("published live rows must be between 1 and 200"); + if (history_rows.size() > history_limit) + throw std::invalid_argument("published history exceeds the retained limit"); + std::vector identities; + identities.reserve(history_rows.size() + live_rows.size()); + for (const auto& line : history_rows) identities.push_back(line.id); + for (const auto& line : live_rows) identities.push_back(line.id); + std::sort(identities.begin(), identities.end()); + if (std::adjacent_find(identities.begin(), identities.end()) != identities.end()) + throw std::invalid_argument("published row identity is duplicated"); + if (order_revision_ == (std::numeric_limits::max)()) + throw std::overflow_error("screen order revision exhausted"); + bool ordered = true; + uint64_t maximum = next_id_ ? next_id_ - 1 : 0, previous = 0; + bool first = true; + const auto track = [&](const std::vector& rows) { + for (const auto& line : rows) { + if (!first && line.id < previous) ordered = false; + if (line.id > maximum) maximum = line.id; + previous = line.id; first = false; + } + }; + track(history_rows); track(live_rows); + if (maximum == (std::numeric_limits::max)()) + throw std::overflow_error("screen line identities exhausted"); + const auto differs = [](const auto& current, const std::vector& next) { + if (current.size() != next.size()) return true; + for (size_t index = 0; index < next.size(); ++index) + if (current[index].id != next[index].id) return true; + return false; + }; + const bool changed = differs(history, history_rows) || differs(lines, live_rows); + history.assign(history_rows.begin(), history_rows.end()); + lines.assign(live_rows.begin(), live_rows.end()); + if (changed) ++order_revision_; + ordered_ = ordered; + next_id_ = maximum + 1; + } // Imported rows retain ordered unique identities; conflicts get a fresh identity and version. void normalize_identities(std::vector& retained) { uint64_t maximum = next_id_ - 1; @@ -255,6 +345,7 @@ class Screen { swap_active(next); if (hidden) inactive_->swap(*hidden); columns_ = columns; next_id_ = identity; + advance_order(ordered_); } void invalidate_attachment() { last_row = last_column = -1; } void move(int r, int c) { @@ -276,6 +367,7 @@ class Screen { if (history.size() > limit) { history.pop_front(); ++discarded; } } else ++discarded; lines.erase(lines.begin()); lines.push_back(make_line(width(), Cell{}, next_id_)); + advance_order(ordered_); } void put(uint32_t cp, const std::string& utf8) { const int cells = scalar_width(cp); diff --git a/experiments/core/text_document.hpp b/experiments/core/text_document.hpp index ac104aed..2db99eb2 100644 --- a/experiments/core/text_document.hpp +++ b/experiments/core/text_document.hpp @@ -43,10 +43,19 @@ class TextDocument { const auto value = meta(index); return {project_line(screen_.retained_line(index)), value.columns, value.joins_next, value.ending}; } - std::optional index_of(uint64_t identity) const { - const auto index = screen_.lower_bound_line(identity); - if (index == size() || screen_.retained_line(index).id != identity) return std::nullopt; - return index; + // Exact current rank: identity is not document order, so a missing row is + // reported as missing instead of snapping to a neighbour. + std::optional index_of(uint64_t identity) const { return screen_.locate(identity); } + // Owner structure revision, for callers that cache rank-derived state. + uint64_t order_revision() const { return screen_.order_revision(); } + // Document-order comparison of two validated positions. Ids order nothing; + // the retained sequence does, so this stays correct for reordered rows. + std::optional compare(TextPosition a, TextPosition b) const { + if (!valid(a) || !valid(b)) return std::nullopt; + const auto first = *index_of(a.line_id), second = *index_of(b.line_id); + if (first != second) return first < second ? -1 : 1; + if (a.column != b.column) return a.column < b.column ? -1 : 1; + return 0; } bool valid(TextPosition value) const { if (value.epoch != epoch()) return false; diff --git a/experiments/core/text_search.hpp b/experiments/core/text_search.hpp index 5f285d77..772c71d0 100644 --- a/experiments/core/text_search.hpp +++ b/experiments/core/text_search.hpp @@ -58,8 +58,13 @@ class TextSearch { return a.id == b.id && a.version == b.version && a.columns == b.columns && a.joins_next == b.joins_next && a.ending == b.ending; } - static bool before(const TextPosition& a, const TextPosition& b) { - return a.line_id < b.line_id || (a.line_id == b.line_id && a.column < b.column); + // Document order, never identity order: a retained row may hold a larger id + // but still sit earlier in the document. + static bool before(const TextDocument& doc, const TextPosition& a, const TextPosition& b) { + const auto first = doc.index_of(a.line_id), second = doc.index_of(b.line_id); + if (!first || !second) return false; + if (*first != *second) return *first < *second; + return a.column < b.column; } static bool equal(const TextPosition& a, const TextPosition& b) { return a == b; @@ -300,7 +305,7 @@ class TextSearch { for (int pass = 0; pass < (allow_wrap ? 2 : 1); ++pass) for (size_t offset = 0; offset < matches_.size(); ++offset) { const auto index = next ? offset : matches_.size() - 1 - offset; const auto hit = match(doc, index); if (!hit) continue; - const bool beyond = !anchor || (next ? before(*anchor, hit->range.begin) : before(hit->range.begin, *anchor)); + const bool beyond = !anchor || (next ? before(doc, *anchor, hit->range.begin) : before(doc, hit->range.begin, *anchor)); if ((pass == 0 && beyond) || (pass == 1 && !beyond)) return SearchNavigation{*hit, index, pass == 1}; } return {}; From ca3c073cfa9c2abf79e9f5bf9d8330af5933724c Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Thu, 29 Oct 2026 16:17:12 +0800 Subject: [PATCH 2/4] =?UTF-8?q?docs(core):=20=E8=AE=B0=E5=BD=95=E8=A1=8C?= =?UTF-8?q?=E5=BA=8F=E6=9F=A5=E8=AF=A2=E9=A6=96=E7=89=87=E8=AF=81=E6=8D=AE?= =?UTF-8?q?=E4=B8=8E=E8=BE=B9=E7=95=8C=20refs=20#169?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/EVIDENCE.md | 1 + docs/FEATURES.md | 2 +- .../results/2026-10-29-order-query/README.md | 59 ++++++++++ .../2026-10-29-order-query/validation.json | 102 ++++++++++++++++++ 4 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 experiments/core/results/2026-10-29-order-query/README.md create mode 100644 experiments/core/results/2026-10-29-order-query/validation.json diff --git a/docs/EVIDENCE.md b/docs/EVIDENCE.md index 484daba3..760a239d 100644 --- a/docs/EVIDENCE.md +++ b/docs/EVIDENCE.md @@ -9,6 +9,7 @@ | 配置 | 证据定位 | |---|---| | 自主 C++ 主屏 ASCII DECAWM | [有限合同与红绿](../experiments/core/results/2026-10-29-cpp-decawm/README.md)、[固定验证身份](../experiments/core/results/2026-10-29-cpp-decawm/validation.json):抽取原版14–18阶段79B,strict/recovery各235个字面前缀分块用例;初次定向8/9的缺构建依赖失败、补建后的core单项31测试通过分列。跨1049/宽字/resize、origin/margin、GUI/固定ConPTY运输和完整TUI均不由本项验收 | +| 自主 C++ 行结构发布与精确身份查询 | [首片合同与固定夹具](../experiments/core/results/2026-10-29-order-query/README.md)、[验证身份](../experiments/core/results/2026-10-29-order-query/validation.json):`[1,3,4,5,7,6]` 的精确 rank(7→4、6→5、2 缺失)、文档序比较与强制 Find 导航、结构 revision 边界,核心 CTest 8/8、完整默认 Release 构建 exit0;候选路径仅 /W4 独立编译。未封住公开 lines/history,运行中跨重排的增量 Find、区域 LF、Qt/固定 ConPTY 与性能均未验收 | | 自主 C++ 限定 1049 协议 | [固定结果](../experiments/core/results/2026-10-29-cpp-1049/README.md)、[验证身份](../experiments/core/results/2026-10-29-cpp-1049/validation.json):文字/光标/颜色及 pending-wrap、重复进入与读者失效;55项中Qt1项分列,13条内部skip及旧负例判据失败保留。未验收非颜色属性/全网格背景、其他模式、真实TUI/IME/DPI、呈现或性能 | | 自主 C++ 双缓冲存储与文档 epoch 基础 | [固定结果](../experiments/core/results/2026-10-29-buffer-state-r2/README.md)、[验证身份](../experiments/core/results/2026-10-29-buffer-state-r2/validation.json):拥有状态、共享 ID、隐藏帧 resize 回滚与跨缓冲句柄失效;Qt/candidate 关闭配置的完整 50 项及 12 条内部 skip 分列。未接 private CSI,不是原版模式、Qt 产品路线或完整 #27 验收 | | Qt 基础 Unicode 文本字节 | [红绿与联合验证](../experiments/shell/results/2026-10-29-qt-unicode-input/README.md)、[固定联合身份](../experiments/shell/results/2026-10-29-qt-unicode-input/validation.json):公开Qt事件到独立ReadConsoleW接收端,整/分事件代理项、焦点/隐藏/关闭、1024B整包与NUL、两目标隔离。2b6d源码一次61项分列Qt3/其余58,13内部skip、旧红例与27原件保留;不是实际IME、字素/字形、原生Unicode窗口、DPI或性能验收 | diff --git a/docs/FEATURES.md b/docs/FEATURES.md index ef49b77d..ed99f182 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -25,7 +25,7 @@ |---|---|---|---| | 自主 C++ / Qt / 固定微软 ConPTY | **实现中,未验收**:[显式 Qt 入口](../src/qt/README.md)提供默认 80×24、随可用区域变化的 cmd 标签、基本 ASCII 与[限定 Unicode 文本字节](../experiments/shell/results/2026-10-29-qt-unicode-input/README.md)、16 色及各会话独立有界队列;[基础动态尺寸验证](../experiments/shell/results/2026-10-29-qt-resize/README.md)分离窗口/核心/transport 与真实 child,覆盖极小、重复、隐藏页和关闭交叉,并复验原标签算术及事件循环回收。678 原生极窄失败保留;0ff 仅修订字面子区域/顶层实际区域夹具,另有[冻结包一次隔离原生成功](../experiments/shell/results/2026-10-29-qt-resize/native-validation.json):实际顶层区域、core/transport/child 一致,窗口仅 DPI 96。旧尺寸结果保留各自源码;[Unicode 联合源码的一次完整61项](../experiments/shell/results/2026-10-29-qt-unicode-input/validation.json)分开 Qt3项与其余58项既有/独立诊断,13条内部skip保留 | 分屏、其他 shell、工作区恢复、完整 resize/reflow、历史/选择/Find、完整 Unicode 字素/绘制与 IME、真实 DPI、应用兼容与性能仍需实现或迁移;物理裁剪不恢复丢弃正文,完整 WindTerm 标签/尺寸行为对照未完成;当前不是可发布开发包 | R2 | -自主 C++ 核心已有 [#130 双缓冲存储基础](../experiments/core/results/2026-10-29-buffer-state-r2/README.md),并接入 [#135 限定 1049 协议](../experiments/core/results/2026-10-29-cpp-1049/README.md):strict/recovery 识别 `?1049h/l`,重复进入归首格、清旧备用文本和复位写入颜色,返回恢复主屏文字/光标/颜色及 pending-wrap。共享行 ID、双帧物理 resize 与逻辑 epoch 防止旧位置、选择、搜索及 viewport 复活,包括清屏后重写相同文字。[主屏 ASCII DECAWM 切片 #161](../experiments/core/results/2026-10-29-cpp-decawm/README.md)另按[有限直接观察](research/windterm-origin-wrap-reference-2026-10-29.md)实现 `?7h/l` 与末列开关,验证五阶段79B的字面文字/位置及分块;原版 origin/margin、wrap跨屏/宽字/resize和完整网格/样式仍未知或未实现。#135本地55项中Qt1项与其余54项诊断分列,13条内部跳过保留;#161仅有独立core CLI定向回归,不表示完整 [#27](https://github.com/JavaZeroo/termforge/issues/27)、真实 TUI/Qt 交互或系统剪贴板已验收。 +自主 C++ 核心已有 [#130 双缓冲存储基础](../experiments/core/results/2026-10-29-buffer-state-r2/README.md),并接入 [#135 限定 1049 协议](../experiments/core/results/2026-10-29-cpp-1049/README.md):strict/recovery 识别 `?1049h/l`,重复进入归首格、清旧备用文本和复位写入颜色,返回恢复主屏文字/光标/颜色及 pending-wrap。共享行 ID、双帧物理 resize 与逻辑 epoch 防止旧位置、选择、搜索及 viewport 复活,包括清屏后重写相同文字。[主屏 ASCII DECAWM 切片 #161](../experiments/core/results/2026-10-29-cpp-decawm/README.md)另按[有限直接观察](research/windterm-origin-wrap-reference-2026-10-29.md)实现 `?7h/l` 与末列开关,验证五阶段79B的字面文字/位置及分块;原版 origin/margin、wrap跨屏/宽字/resize和完整网格/样式仍未知或未实现。#135本地55项中Qt1项与其余54项诊断分列,13条内部跳过保留;#161仅有独立core CLI定向回归,不表示完整 [#27](https://github.com/JavaZeroo/termforge/issues/27)、真实 TUI/Qt 交互或系统剪贴板已验收。另有[行结构发布与精确身份查询首片 #169](../experiments/core/results/2026-10-29-order-query/README.md):`Screen` 经 `publish` 提交拥有值的行镜像并推进结构 revision,`locate` 精确回答 ID→rank(缺失不落到邻行),`TextDocument/HistoryViewport/TextSearch` 改用文档序;固定 `[1,3,4,5,7,6]` 夹具与核心 8/8 通过。该片**未**封住公开 `lines/history`、未迁移有序增量 Find 的 ID 区间假设,也没有区域 LF、新 VT 或 Qt/固定 ConPTY 证据;#169 与 #27/#29 保持开放。 Qt 构建显式准备并部署[固定微软 CRT 14.51.36247.0](../third_party/msvc_runtime/README.md),不安装到 System32。隔离 Windows 的[限定加载与 cmd 验证](../experiments/shell/results/2026-10-29-qt-crt-deploy/README.md)保留原包失败、原 52 文件不变且仅追加 CRT/原许可后的成功,以及新源码包的独立验证;它不解除完整产品打包、Qt/第三方分发审计、实际 DPI/IME 或物理呈现的验收缺口。 diff --git a/experiments/core/results/2026-10-29-order-query/README.md b/experiments/core/results/2026-10-29-order-query/README.md new file mode 100644 index 00000000..08df0f3c --- /dev/null +++ b/experiments/core/results/2026-10-29-order-query/README.md @@ -0,0 +1,59 @@ +# 行结构发布、精确身份查询与文档序(#169 首片) + +本片把“稳定行 ID 被当作文档顺序”的隐含假设收敛到 `Screen` 的 owner 发布口与精确查询:行身份只表示身份,位置由当前保留序列决定。没有新增 VT 模式、区域 LF 或任何用户可触发行为;`Screen::lines/history` 仍是公开成员,封住直接写入属于下一片。 + +## 交付内容 + +| 入口 | 变更 | +|---|---| +| `Screen::publish(history, live)` | 导入/回放显式提交拥有值的行镜像:校验宽度、非零且唯一 ID、保留上限与尺寸;成功才替换存储并推进结构 revision;失败不改任何已发布状态 | +| `Screen::locate(id)` | 精确 ID→rank;缺失 ID 返回空,**不再**退化为相邻行。只在可证有序时二分,其余逐行精确扫描 | +| `Screen::order_revision()` | 结构 revision:保留成员、文档顺序或 history/live 分界改变时前进;内容写入、仅移动光标的新行、同尺寸 resize 与同结构重复发布都不前进 | +| `Screen::mark_order_unknown()` | 绕过 `publish` 的直接结构写入必须调用;之后查询改走扫描,宁可慢也不再信任已经失效的二分 | +| `TextDocument::index_of` | 改用 `locate`,因此 `valid/normalize/meta` 及所有依赖都拿到精确 rank | +| `TextDocument::compare` | 按当前文档序比较两个有效位置,而不是比较 ID | +| `HistoryViewport` | 活 anchor 用精确 rank;已淘汰 anchor 保留原有 `lower_bound` 回退,两条路径分开 | +| `TextSearch::navigate` | 前后导航改用文档序;有序时与旧 ID 比较等价 | +| `CandidateTerminal` | 导入改经 `publish`;回放帧直接恢复后显式声明 order 未知 | + +## 固定夹具与期望 + +``` +4 列、history_limit=4 +history ids [1,3,4,5] live ids [7,6] +``` + +| 断言 | 期望 | +|---|---| +| `locate(7)` / `locate(6)` / `locate(5)` | rank 4 / 5 / 3 | +| `locate(2)` / `locate(0)` | 缺失 / 缺失(不落到邻行) | +| `lower_bound_line(2)` | 1(已淘汰 anchor 的旧回退保持独立) | +| 文档序比较 | 7 在 6 之前,尽管 6 < 7 | +| 新增强制 Find 的 previous/next | previous(6)→7、next(7)→6 | +| 结构 revision | 只在下滚、物理 resize、缓冲切换、结构发布时前进 | + +## 实际验证 + +环境:Windows 11 10.0.26200、Visual Studio 17 2022 / MSVC 19.42.34433.0、CMake 3.31.0、Release、Qt 与 candidate 关闭;源码 `33b58fa4452c4d4f23dcfc463f65c5c521e59065`。 + +- 完整默认 Release 构建 exit 0。 +- 核心 CTest `8/8` 通过:`order_query_core` 新增,`resize_core`/`history_core`/`selection_core`/`search_core`/`buffer_state_core`/`alternate_mode_core`/`decawm_mode_core` 维持通过。 +- 候选路径本机未启用适配器:`candidate_terminal.cpp` 仅做 `/W4` 独立编译(exit 0、无诊断),未链接、未运行 `candidate_core_test`,因此不构成候选行为验证。 +- 保留的首轮失败:新测试第一次运行 1/8,原因是夹具第二可见行写成 `y` 却期望两个 `x` 命中;另一次是把“已发布为乱序”错误期望成 `order_known()==true`。两处都只改测试夹具/期望,**不作为产品红→绿证据**。 + +身份与原始命令见 [validation.json](validation.json)。 + +## 未完成与本片不主张的结论 + +- `Screen::lines/history` 仍公开可写,外部仍可绕过 owner 发布;封口与全部 caller 迁移属下一片。 +- 有序增量 Find 内部的 `emit_from_id_`/`prune`/`rewind` 与 `match()` 的 ID 区间筛选仍假设有序:运行中跨重排的搜索未验收。 +- 未实现区域 LF、新的 VT 模式、reflow 或用户可达重排;默认物理裁剪策略不变。 +- 未运行 Qt、固定 ConPTY、真实 GUI、真实 TUI 或任何性能负载;没有性能数值或容差主张,#169 与 #27/#29 均未关闭。 + +## 复验 + +```powershell +cmake -S . -B build -G "Visual Studio 17 2022" -A x64 +cmake --build build --config Release --target order_query_test history_test selection_test search_test resize_test buffer_state_test alternate_mode_test decawm_mode_test --parallel 2 +ctest --test-dir build -C Release -R "^(order_query_core|history_core|selection_core|search_core|resize_core|buffer_state_core|alternate_mode_core|decawm_mode_core)$" --output-on-failure +``` diff --git a/experiments/core/results/2026-10-29-order-query/validation.json b/experiments/core/results/2026-10-29-order-query/validation.json new file mode 100644 index 00000000..61138995 --- /dev/null +++ b/experiments/core/results/2026-10-29-order-query/validation.json @@ -0,0 +1,102 @@ +{ + "schema": "termforge-core-order-query-local-validation-v1", + "issue": 169, + "base_commit": "05f04d0222c06bed958f6894a27d2a1b0708d270", + "validated_source_commit": "33b58fa4452c4d4f23dcfc463f65c5c521e59065", + "branch": "feat/core-order-query-r169", + "scope": "Screen 发布行结构、精确 ID→rank 查询、结构 revision;TextDocument/HistoryViewport/TextSearch 文档序;CandidateTerminal 经发布口导入。未封住公开 lines/history。", + "configuration": { + "platform": "Windows-11-10.0.26200", + "generator": "Visual Studio 17 2022", + "compiler": "MSVC 19.42.34433.0", + "build_type": "Release", + "qt_shell": false, + "candidate_adapter": false, + "candidate_source_standalone_compile": true, + "candidate_runtime_exercised": false + }, + "observed_contract": { + "fixture": { + "columns": 4, + "history_limit": 4, + "history_ids": [ + 1, + 3, + 4, + 5 + ], + "live_ids": [ + 7, + 6 + ] + }, + "expected": { + "locate_rank": { + "0": null, + "2": null, + "5": 3, + "6": 5, + "7": 4 + }, + "ordered_bisect_proof": false, + "lower_bound_id_2": 1, + "document_order_compare_7_before_6": true, + "previous_navigation_from_id_6": 7, + "next_navigation_from_id_7": 6 + }, + "order_revision_advances_on": [ + "bottom-row scroll", + "physical resize", + "buffer activation", + "structural publication" + ], + "order_revision_unchanged_on": [ + "cell write", + "cursor-only newline", + "identical resize", + "identical republication", + "rejected publication" + ] + }, + "commands": [ + "cmake --build build --config Release --parallel 2", + "ctest --test-dir build -C Release -R '^(order_query_core|resize_core|history_core|selection_core|search_core|buffer_state_core|alternate_mode_core|decawm_mode_core)$' --output-on-failure", + "cl /nologo /std:c++17 /EHsc /W4 /c /I experiments/core /I experiments/candidates/alacritty/include experiments/core/candidate_terminal.cpp" + ], + "results": { + "full_release_build": "exit 0", + "core_ctest": "8/8 passed", + "order_query_core": "passed", + "candidate_terminal_standalone_compile": "exit 0 with /W4, no diagnostics" + }, + "preserved_failures": [ + "order_query_core 首次运行 1/8,因测试夹具把第二可见行写成 y 却期望两个 x 命中;仅修正夹具后通过,不作为产品红绿。", + "同次首轮另一失败是把「已发布为乱序」期望成 order_known()==true;语义应为 false(bisect 证明不成立),仅修正测试期望。", + "候选适配器本机未启用,candidate_core_test 未构建或运行;只有 /W4 独立编译通过,不构成候选行为验证。" + ], + "source_sha256": { + "experiments/core/screen.hpp": "af146b42feae97cdc4a4856ba5364ac51d801709decc597923f83eb507d6b2d2", + "experiments/core/text_document.hpp": "659d1226c408cae46c72a21c7e74f85c004e654043451bee42a42953915e977a", + "experiments/core/text_search.hpp": "4cec4c85a17e671a4a2e5fbdf39fc653b7ca2969f52fa936e58fb099f4aef0b4", + "experiments/core/history_view.hpp": "50dcb2f7e801082f7f21bc23f43e6944af711fde0af6e4c100152ca46ed9bd13", + "experiments/core/candidate_terminal.cpp": "6c398d52628d9f8ee336814a4084ed8eff435cc73bd92449d5976b50a7cd0409", + "experiments/core/order_query_test.cpp": "bccd24f4d88b089b236bb3499c830e92c1d29f9af2d9b681e313c5f1732e381e", + "CMakeLists.txt": "cd5546218b758cff70471870d9b6b8b59479e330cfb432274253a4de8266a092" + }, + "binary_sha256": { + "order_query_test.exe": "9e1d0e5f057ea402132ca77e7e61a10577fc26e6dadbcf67dd8755aa2cfae551", + "history_test.exe": "3bcd89ea1d43e39317ad9953fbeb2a426eadba40451a86c7e6a395d9e3757927", + "selection_test.exe": "da448b9ec840fe29bb72de70b9ee38bca3e31846efc4eb0826b11fd31612421e", + "search_test.exe": "c480b3cca2906b5adfe688c2887f9e649dafde514a068b2d38c37abfb409c2f7", + "resize_test.exe": "d94cc45f32d3c3080be498e6967d7808f3812be3bcd41ad6a7b8cd5ef81b4a44", + "buffer_state_test.exe": "11ffe2b26e24998212620a879ec8f2c8c8f1ef29b72ea66594e41e2142337097", + "alternate_mode_test.exe": "adf1a8ba17e00a8a79dd38ade47d549e2e0ef696f2367b0cd5a4d9107c1533de", + "decawm_mode_test.exe": "dc66fc49c25ad0b1f55156eaaf40b2be3fdc511faa6c5d7fd328582552baded2" + }, + "limitations": [ + "Screen::lines/history 仍为公开成员:本片只提供 owner 发布口,未封住直接写入。", + "有序增量 Find 内部的 emit_from_id_/prune/rewind 与 match() 的 id 区间筛选仍假设有序;运行中任意重排未验收。", + "无 region LF、无新 VT 模式、无用户可达重排命令,也未改默认物理裁剪策略。", + "无窗口、无固定 ConPTY、无 Qt,未运行 GUI/原生/性能负载。" + ] +} From d5116e2cefc4755efc79a4bd8de26eb2629ca71a Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Thu, 29 Oct 2026 16:29:02 +0800 Subject: [PATCH 3/4] =?UTF-8?q?fix(core):=20=E6=A0=A1=E9=AA=8C=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E5=AE=BD=E5=BA=A6=E5=B9=B6=E4=BF=AE=E6=AD=A3=E5=80=99?= =?UTF-8?q?=E9=80=89=E5=9B=9E=E6=94=BE=E5=B8=A7=E6=81=A2=E5=A4=8D=20refs?= =?UTF-8?q?=20#169?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- experiments/core/candidate_terminal.cpp | 21 ++++++---- experiments/core/order_query_test.cpp | 52 +++++++++++++++++++++++- experiments/core/screen.hpp | 54 +++++++++++++++++++------ experiments/core/text_document.hpp | 3 ++ experiments/core/text_search.hpp | 19 ++++++--- 5 files changed, 123 insertions(+), 26 deletions(-) diff --git a/experiments/core/candidate_terminal.cpp b/experiments/core/candidate_terminal.cpp index 41478ce9..6633dbba 100644 --- a/experiments/core/candidate_terminal.cpp +++ b/experiments/core/candidate_terminal.cpp @@ -279,11 +279,7 @@ void CandidateTerminal::synchronize() { screen.wrap_pending = replay_origin_->wrap_pending; screen.discarded = replay_origin_->discarded; screen.style = replay_origin_->style; - screen.history = replay_origin_->history; - screen.lines = replay_origin_->lines; - // This direct restore keeps the frozen frame image; the restored - // order is no longer provably the active screen's own sequence. - screen.mark_order_unknown(); + screen.restore_frame(*replay_origin_); } return; } @@ -346,13 +342,24 @@ void CandidateTerminal::synchronize() { versions.emplace(line.id, version); } + // Preserved history arrives from an older frame, so it can still hold the + // previous width. Fit it with the same physical-row policy Screen::resize + // applies instead of publishing rows whose width contradicts the frame. + for (auto& line : retained) { + if (line.size() == static_cast(meta.columns)) continue; + if (line.size() > static_cast(meta.columns)) line.cropped = true; + line.resize(meta.columns); + if (!line.empty() && line.back().width == 2) line.back() = Cell{}; + } + screen.publish(std::vector(retained.begin(), retained.begin() + meta.history), + std::vector(retained.begin() + meta.history, retained.end())); + // Cursor and style follow the accepted publication: a rejected frame must + // not leave the screen half-updated. screen.row = meta.row; screen.column = static_cast(meta.column); screen.wrap_pending = meta.wrap_pending; screen.discarded = meta.discarded; screen.style = {meta.fg, meta.bg, meta.bold}; - screen.publish(std::vector(retained.begin(), retained.begin() + meta.history), - std::vector(retained.begin() + meta.history, retained.end())); versions_ = std::move(versions); identity_aliases_.clear(); for (size_t index = 0; index < retained.size(); ++index) diff --git a/experiments/core/order_query_test.cpp b/experiments/core/order_query_test.cpp index 2bf62f21..80826388 100644 --- a/experiments/core/order_query_test.cpp +++ b/experiments/core/order_query_test.cpp @@ -40,7 +40,6 @@ static Screen reordered_screen() { static void published_order_is_exact() { auto screen = reordered_screen(); check(screen.retained_size() == 6, "published frame keeps every retained row"); - check(!screen.order_known(), "a reordered publication records that a bisect proof does not hold"); check(screen.locate(7) == std::optional(4), "live identity 7 holds rank 4"); check(screen.locate(6) == std::optional(5), "live identity 6 holds rank 5"); check(screen.locate(5) == std::optional(3), "history identity 5 holds rank 3"); @@ -90,6 +89,50 @@ static void order_revision_tracks_structure_only() { check(screen.order_revision() == republished, "republishing the same structure publishes nothing"); } +static void publication_keeps_the_cursor_inside_the_frame() { + Screen screen(3, 4, 4); + screen.move(2, 3); + screen.publish({}, {row(4, 40, 'a')}); + check(screen.row == 0 && screen.column == 3, "a shorter published frame clamps only the lost row"); + screen.put('b', "b"); + check(screen.lines[0][3].text == "b", "writes after a shrinking publication stay in range"); +} + +static void publication_respects_the_active_history_budget() { + Screen screen(2, 4, 1); + bool rejected = false; + try { screen.publish(rows(4, {{1, 'a'}, {2, 'b'}}), rows(4, {{3, 'c'}, {4, 'd'}})); } + catch (const std::invalid_argument&) { rejected = true; } + check(rejected, "a publication above the active history budget is rejected"); + screen.activate_buffer(ScreenBuffer::alternate); + bool alternate = false; + try { screen.publish(rows(4, {{5, 'e'}}), rows(4, {{6, 'f'}, {7, 'g'}})); } + catch (const std::invalid_argument&) { alternate = true; } + check(alternate, "the alternate frame accepts no imported history"); +} + +static void eviction_never_revives_rank() { + Screen screen(2, 4, 2); + screen.publish(rows(4, {{10, 'a'}, {11, 'b'}}), rows(4, {{12, 'c'}, {13, 'd'}})); + check(screen.locate(10) == std::optional(0), "evictable history is located before eviction"); + screen.move(1, 0); + screen.newline(); + screen.newline(); + check(!screen.locate(10), "an evicted identity is not located any more"); + check(screen.locate(13).has_value(), "a surviving identity keeps an exact rank"); + check(screen.lower_bound_line(10) == 0, "the evicted-anchor fallback stays an explicit insertion point"); +} + +static void copies_keep_their_own_order() { + auto original = reordered_screen(); + Screen copy = original; + check(copy.locate(7) == std::optional(4) && copy.locate(6) == std::optional(5), + "a copy keeps the published order"); + copy.publish({}, {row(4, 1, 'q'), row(4, 2, 'r')}); + check(original.locate(7) == std::optional(4), "changing a copy leaves the original order intact"); + check(copy.locate(7) == std::nullopt, "a republished copy reports its own identities only"); +} + static void rejected_publication_commits_nothing() { Screen screen(2, 4, 4); const auto revision = screen.order_revision(); @@ -126,6 +169,9 @@ static void search_navigation_follows_document_order() { const auto earlier = doc.position(4, 0); const auto next = search.navigate(doc, SearchDirection::next, earlier); check(next && next->match.range.begin.line_id == 6, "next navigation reaches the later live row"); + const auto no_wrap = search.navigate_no_wrap(doc, SearchDirection::previous, anchor); + check(no_wrap && no_wrap->match.range.begin.line_id == 7, + "no-wrap previous navigation also follows document order"); } static void viewport_uses_exact_rank() { @@ -143,6 +189,10 @@ int main() { published_order_is_exact(); document_rank_and_compare(); order_revision_tracks_structure_only(); + publication_keeps_the_cursor_inside_the_frame(); + publication_respects_the_active_history_budget(); + eviction_never_revives_rank(); + copies_keep_their_own_order(); rejected_publication_commits_nothing(); search_navigation_follows_document_order(); viewport_uses_exact_rank(); diff --git a/experiments/core/screen.hpp b/experiments/core/screen.hpp index 0f1df5ee..cee00fe4 100644 --- a/experiments/core/screen.hpp +++ b/experiments/core/screen.hpp @@ -94,8 +94,9 @@ class Screen { throw std::overflow_error("line version exhausted"); ++line.version; } - // Publishing a structure change is the single place that advances the - // document-order revision. Overflow fails before the caller mutates. + // Internal structure mutations advance the document-order revision here. + // Overflow is reported rather than wrapped; reaching it requires 2^64 + // structural changes in one session, so it is not a caller-visible path. void advance_order(bool ordered) { if (order_revision_ == (std::numeric_limits::max)()) throw std::overflow_error("screen order revision exhausted"); @@ -235,8 +236,8 @@ class Screen { return first; } const Line* find_line(uint64_t identity) const { - const auto index = lower_bound_line(identity); - return index < retained_size() && retained_line(index).id == identity ? &retained_line(index) : nullptr; + const auto index = locate(identity); + return index ? &retained_line(*index) : nullptr; } // Owner-published structure revision: advances whenever retained membership, // document order or the history/live split changes. It never stands in for a @@ -259,16 +260,32 @@ class Screen { // A direct structural write that bypasses publish() must call this so later // lookups scan instead of trusting a bisect that no longer holds. void mark_order_unknown() { ordered_ = false; } + // Restore a complete frame image captured from this same screen family + // (replay). The restored order is not provably the active sequence any more, + // so lookups fall back to scanning and readers lose their rank-derived state. + // Columns and the identity allocator stay with this screen. + void restore_frame(const Screen& origin) { + if (order_revision_ == (std::numeric_limits::max)()) + throw std::overflow_error("screen order revision exhausted"); + std::vector next_lines = origin.lines; + std::deque next_history = origin.history; + row = origin.row; column = origin.column; + wrap_pending = origin.wrap_pending; discarded = origin.discarded; style = origin.style; + lines.swap(next_lines); history.swap(next_history); + ordered_ = origin.ordered_; + ++order_revision_; + invalidate_attachment(); + } bool order_known() const { return ordered_; } // Explicit publication of an owned row image (import/replay). Identities are // validated, never renumbered: a duplicate or zero identity is rejected // before anything is committed. Order is recorded so lookups stay exact. // Cursor, style, discarded counters and the inactive frame are untouched. void publish(std::vector history_rows, std::vector live_rows) { - const auto width = static_cast(columns_); - const auto validate = [width](const std::vector& rows, const char* what) { + const auto row_width = static_cast(columns_); + const auto validate = [row_width](const std::vector& rows, const char* what) { for (const auto& line : rows) { - if (line.size() != width) throw std::invalid_argument(what); + if (line.size() != row_width) throw std::invalid_argument(what); if (!line.id) throw std::invalid_argument("published row identity is zero"); } }; @@ -276,8 +293,8 @@ class Screen { validate(live_rows, "published row width differs from the screen"); if (live_rows.empty() || live_rows.size() > 200) throw std::invalid_argument("published live rows must be between 1 and 200"); - if (history_rows.size() > history_limit) - throw std::invalid_argument("published history exceeds the retained limit"); + if (history_rows.size() > active_history_limit()) + throw std::invalid_argument("published history exceeds the active retained limit"); std::vector identities; identities.reserve(history_rows.size() + live_rows.size()); for (const auto& line : history_rows) identities.push_back(line.id); @@ -285,8 +302,6 @@ class Screen { std::sort(identities.begin(), identities.end()); if (std::adjacent_find(identities.begin(), identities.end()) != identities.end()) throw std::invalid_argument("published row identity is duplicated"); - if (order_revision_ == (std::numeric_limits::max)()) - throw std::overflow_error("screen order revision exhausted"); bool ordered = true; uint64_t maximum = next_id_ ? next_id_ - 1 : 0, previous = 0; bool first = true; @@ -307,11 +322,24 @@ class Screen { return false; }; const bool changed = differs(history, history_rows) || differs(lines, live_rows); - history.assign(history_rows.begin(), history_rows.end()); - lines.assign(live_rows.begin(), live_rows.end()); + if (changed && order_revision_ == (std::numeric_limits::max)()) + throw std::overflow_error("screen order revision exhausted"); + // Prepare the replacement storage before touching the published frame so + // an allocation failure cannot leave history and live rows disagreeing. + std::deque next_history(std::make_move_iterator(history_rows.begin()), + std::make_move_iterator(history_rows.end())); + std::vector next_live(std::make_move_iterator(live_rows.begin()), + std::make_move_iterator(live_rows.end())); + history.swap(next_history); + lines.swap(next_live); if (changed) ++order_revision_; ordered_ = ordered; next_id_ = maximum + 1; + // The replaced frame may be shorter or narrower, and the old attachment + // points at a row that no longer exists. + row = std::clamp(row, 0, height() - 1); + column = std::clamp(column, 0, width() - 1); + invalidate_attachment(); } // Imported rows retain ordered unique identities; conflicts get a fresh identity and version. void normalize_identities(std::vector& retained) { diff --git a/experiments/core/text_document.hpp b/experiments/core/text_document.hpp index 2db99eb2..af6d74e1 100644 --- a/experiments/core/text_document.hpp +++ b/experiments/core/text_document.hpp @@ -48,6 +48,9 @@ class TextDocument { std::optional index_of(uint64_t identity) const { return screen_.locate(identity); } // Owner structure revision, for callers that cache rank-derived state. uint64_t order_revision() const { return screen_.order_revision(); } + // True while identities happen to be increasing in document order. Callers + // must not read this as "ids order the document"; it only permits bisects. + bool ordered() const { return screen_.order_known(); } // Document-order comparison of two validated positions. Ids order nothing; // the retained sequence does, so this stays correct for reordered rows. std::optional compare(TextPosition a, TextPosition b) const { diff --git a/experiments/core/text_search.hpp b/experiments/core/text_search.hpp index 772c71d0..2a3b8237 100644 --- a/experiments/core/text_search.hpp +++ b/experiments/core/text_search.hpp @@ -59,7 +59,8 @@ class TextSearch { a.joins_next == b.joins_next && a.ending == b.ending; } // Document order, never identity order: a retained row may hold a larger id - // but still sit earlier in the document. + // but still sit earlier in the document. Callers pass positions they already + // validated, so a missing row means "cannot place it", not "not before". static bool before(const TextDocument& doc, const TextPosition& a, const TextPosition& b) { const auto first = doc.index_of(a.line_id), second = doc.index_of(b.line_id); if (!first || !second) return false; @@ -290,10 +291,18 @@ class TextSearch { 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, - [](const ScannedRow& row, uint64_t id) { return row.meta.id < id; }); - for (auto row = first; row != rows_.end() && row->meta.id <= hit.range.end.line_id; ++row) - if (!row_valid(doc, row->meta, row->successor_id)) return {}; + if (doc.ordered()) { + const auto first = std::lower_bound(rows_.begin(), rows_.end(), hit.range.begin.line_id, + [](const ScannedRow& row, uint64_t id) { return row.meta.id < id; }); + for (auto row = first; row != rows_.end() && row->meta.id <= hit.range.end.line_id; ++row) + if (!row_valid(doc, row->meta, row->successor_id)) return {}; + } else { + // Scanned rows follow document order while identities need not; a + // bisect on id would silently skip rows instead of failing. Validate + // the whole bounded scan record, which is at most the retained rows. + for (const auto& row : rows_) + if (!row_valid(doc, row.meta, row.successor_id)) return {}; + } if (loaded_ && current_meta_.id >= hit.range.begin.line_id && current_meta_.id <= hit.range.end.line_id && !row_valid(doc, current_meta_, current_successor_id_)) return {}; return hit; From 9ceb3abb04d6870c6d794f5e698aea93fa51cb51 Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Thu, 29 Oct 2026 16:29:08 +0800 Subject: [PATCH 4/4] =?UTF-8?q?docs(core):=20=E8=AE=B0=E5=BD=95=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E5=AE=BD=E5=BA=A6=E6=A0=A1=E9=AA=8C=E4=B8=8E=E5=80=99?= =?UTF-8?q?=E9=80=89=E5=B8=A7=E6=81=A2=E5=A4=8D=E8=AF=81=E6=8D=AE=20refs?= =?UTF-8?q?=20#169?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/EVIDENCE.md | 2 +- docs/FEATURES.md | 2 +- .../results/2026-10-29-order-query/README.md | 35 ++++--- .../2026-10-29-order-query/validation.json | 91 ++++++++++++------- 4 files changed, 78 insertions(+), 52 deletions(-) diff --git a/docs/EVIDENCE.md b/docs/EVIDENCE.md index 760a239d..e3e15839 100644 --- a/docs/EVIDENCE.md +++ b/docs/EVIDENCE.md @@ -9,7 +9,7 @@ | 配置 | 证据定位 | |---|---| | 自主 C++ 主屏 ASCII DECAWM | [有限合同与红绿](../experiments/core/results/2026-10-29-cpp-decawm/README.md)、[固定验证身份](../experiments/core/results/2026-10-29-cpp-decawm/validation.json):抽取原版14–18阶段79B,strict/recovery各235个字面前缀分块用例;初次定向8/9的缺构建依赖失败、补建后的core单项31测试通过分列。跨1049/宽字/resize、origin/margin、GUI/固定ConPTY运输和完整TUI均不由本项验收 | -| 自主 C++ 行结构发布与精确身份查询 | [首片合同与固定夹具](../experiments/core/results/2026-10-29-order-query/README.md)、[验证身份](../experiments/core/results/2026-10-29-order-query/validation.json):`[1,3,4,5,7,6]` 的精确 rank(7→4、6→5、2 缺失)、文档序比较与强制 Find 导航、结构 revision 边界,核心 CTest 8/8、完整默认 Release 构建 exit0;候选路径仅 /W4 独立编译。未封住公开 lines/history,运行中跨重排的增量 Find、区域 LF、Qt/固定 ConPTY 与性能均未验收 | +| 自主 C++ 行结构发布与精确身份查询 | [首片合同与固定夹具](../experiments/core/results/2026-10-29-order-query/README.md)、[验证身份](../experiments/core/results/2026-10-29-order-query/validation.json):`[1,3,4,5,7,6]` 的精确 rank(7→4、6→5、2 缺失)、文档序比较与强制 Find 导航、结构 revision 边界、发布拒绝与光标夹取;筛选的 8 项核心 CTest 与本地候选 `candidate_core` 通过,首个推送版本的 CI 红(回放保留历史仍为上一帧宽度)与按物理行策略适配后的绿分列,完整默认 Release 构建 exit0。未封住公开 lines/history,运行中跨重排的增量 Find、区域 LF、Qt/固定 ConPTY 与性能均未验收 | | 自主 C++ 限定 1049 协议 | [固定结果](../experiments/core/results/2026-10-29-cpp-1049/README.md)、[验证身份](../experiments/core/results/2026-10-29-cpp-1049/validation.json):文字/光标/颜色及 pending-wrap、重复进入与读者失效;55项中Qt1项分列,13条内部skip及旧负例判据失败保留。未验收非颜色属性/全网格背景、其他模式、真实TUI/IME/DPI、呈现或性能 | | 自主 C++ 双缓冲存储与文档 epoch 基础 | [固定结果](../experiments/core/results/2026-10-29-buffer-state-r2/README.md)、[验证身份](../experiments/core/results/2026-10-29-buffer-state-r2/validation.json):拥有状态、共享 ID、隐藏帧 resize 回滚与跨缓冲句柄失效;Qt/candidate 关闭配置的完整 50 项及 12 条内部 skip 分列。未接 private CSI,不是原版模式、Qt 产品路线或完整 #27 验收 | | Qt 基础 Unicode 文本字节 | [红绿与联合验证](../experiments/shell/results/2026-10-29-qt-unicode-input/README.md)、[固定联合身份](../experiments/shell/results/2026-10-29-qt-unicode-input/validation.json):公开Qt事件到独立ReadConsoleW接收端,整/分事件代理项、焦点/隐藏/关闭、1024B整包与NUL、两目标隔离。2b6d源码一次61项分列Qt3/其余58,13内部skip、旧红例与27原件保留;不是实际IME、字素/字形、原生Unicode窗口、DPI或性能验收 | diff --git a/docs/FEATURES.md b/docs/FEATURES.md index ed99f182..1c42323b 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -25,7 +25,7 @@ |---|---|---|---| | 自主 C++ / Qt / 固定微软 ConPTY | **实现中,未验收**:[显式 Qt 入口](../src/qt/README.md)提供默认 80×24、随可用区域变化的 cmd 标签、基本 ASCII 与[限定 Unicode 文本字节](../experiments/shell/results/2026-10-29-qt-unicode-input/README.md)、16 色及各会话独立有界队列;[基础动态尺寸验证](../experiments/shell/results/2026-10-29-qt-resize/README.md)分离窗口/核心/transport 与真实 child,覆盖极小、重复、隐藏页和关闭交叉,并复验原标签算术及事件循环回收。678 原生极窄失败保留;0ff 仅修订字面子区域/顶层实际区域夹具,另有[冻结包一次隔离原生成功](../experiments/shell/results/2026-10-29-qt-resize/native-validation.json):实际顶层区域、core/transport/child 一致,窗口仅 DPI 96。旧尺寸结果保留各自源码;[Unicode 联合源码的一次完整61项](../experiments/shell/results/2026-10-29-qt-unicode-input/validation.json)分开 Qt3项与其余58项既有/独立诊断,13条内部skip保留 | 分屏、其他 shell、工作区恢复、完整 resize/reflow、历史/选择/Find、完整 Unicode 字素/绘制与 IME、真实 DPI、应用兼容与性能仍需实现或迁移;物理裁剪不恢复丢弃正文,完整 WindTerm 标签/尺寸行为对照未完成;当前不是可发布开发包 | R2 | -自主 C++ 核心已有 [#130 双缓冲存储基础](../experiments/core/results/2026-10-29-buffer-state-r2/README.md),并接入 [#135 限定 1049 协议](../experiments/core/results/2026-10-29-cpp-1049/README.md):strict/recovery 识别 `?1049h/l`,重复进入归首格、清旧备用文本和复位写入颜色,返回恢复主屏文字/光标/颜色及 pending-wrap。共享行 ID、双帧物理 resize 与逻辑 epoch 防止旧位置、选择、搜索及 viewport 复活,包括清屏后重写相同文字。[主屏 ASCII DECAWM 切片 #161](../experiments/core/results/2026-10-29-cpp-decawm/README.md)另按[有限直接观察](research/windterm-origin-wrap-reference-2026-10-29.md)实现 `?7h/l` 与末列开关,验证五阶段79B的字面文字/位置及分块;原版 origin/margin、wrap跨屏/宽字/resize和完整网格/样式仍未知或未实现。#135本地55项中Qt1项与其余54项诊断分列,13条内部跳过保留;#161仅有独立core CLI定向回归,不表示完整 [#27](https://github.com/JavaZeroo/termforge/issues/27)、真实 TUI/Qt 交互或系统剪贴板已验收。另有[行结构发布与精确身份查询首片 #169](../experiments/core/results/2026-10-29-order-query/README.md):`Screen` 经 `publish` 提交拥有值的行镜像并推进结构 revision,`locate` 精确回答 ID→rank(缺失不落到邻行),`TextDocument/HistoryViewport/TextSearch` 改用文档序;固定 `[1,3,4,5,7,6]` 夹具与核心 8/8 通过。该片**未**封住公开 `lines/history`、未迁移有序增量 Find 的 ID 区间假设,也没有区域 LF、新 VT 或 Qt/固定 ConPTY 证据;#169 与 #27/#29 保持开放。 +自主 C++ 核心已有 [#130 双缓冲存储基础](../experiments/core/results/2026-10-29-buffer-state-r2/README.md),并接入 [#135 限定 1049 协议](../experiments/core/results/2026-10-29-cpp-1049/README.md):strict/recovery 识别 `?1049h/l`,重复进入归首格、清旧备用文本和复位写入颜色,返回恢复主屏文字/光标/颜色及 pending-wrap。共享行 ID、双帧物理 resize 与逻辑 epoch 防止旧位置、选择、搜索及 viewport 复活,包括清屏后重写相同文字。[主屏 ASCII DECAWM 切片 #161](../experiments/core/results/2026-10-29-cpp-decawm/README.md)另按[有限直接观察](research/windterm-origin-wrap-reference-2026-10-29.md)实现 `?7h/l` 与末列开关,验证五阶段79B的字面文字/位置及分块;原版 origin/margin、wrap跨屏/宽字/resize和完整网格/样式仍未知或未实现。#135本地55项中Qt1项与其余54项诊断分列,13条内部跳过保留;#161仅有独立core CLI定向回归,不表示完整 [#27](https://github.com/JavaZeroo/termforge/issues/27)、真实 TUI/Qt 交互或系统剪贴板已验收。另有[行结构发布与精确身份查询首片 #169](../experiments/core/results/2026-10-29-order-query/README.md):`Screen` 经 `publish` 提交拥有值的行镜像并推进结构 revision,`locate` 精确回答 ID→rank(缺失不落到邻行),`TextDocument` 的定位/比较与 `HistoryViewport` 的活 anchor 改用精确 rank,`TextSearch` 只把**导航比较**改成文档序;固定 `[1,3,4,5,7,6]` 夹具、筛选的 8 项核心 CTest 与候选 `candidate_core` 通过(首个推送版本在该项为红,混合宽度帧修复后复跑为绿,过程保留)。该片**未**封住公开 `lines/history`、未迁移有序增量 Find 的 `emit/prune/rewind` ID 假设,也没有区域 LF、新 VT 或 Qt/固定 ConPTY 证据;#169 与 #27/#29 保持开放。 Qt 构建显式准备并部署[固定微软 CRT 14.51.36247.0](../third_party/msvc_runtime/README.md),不安装到 System32。隔离 Windows 的[限定加载与 cmd 验证](../experiments/shell/results/2026-10-29-qt-crt-deploy/README.md)保留原包失败、原 52 文件不变且仅追加 CRT/原许可后的成功,以及新源码包的独立验证;它不解除完整产品打包、Qt/第三方分发审计、实际 DPI/IME 或物理呈现的验收缺口。 diff --git a/experiments/core/results/2026-10-29-order-query/README.md b/experiments/core/results/2026-10-29-order-query/README.md index 08df0f3c..4dfb5749 100644 --- a/experiments/core/results/2026-10-29-order-query/README.md +++ b/experiments/core/results/2026-10-29-order-query/README.md @@ -6,15 +6,14 @@ | 入口 | 变更 | |---|---| -| `Screen::publish(history, live)` | 导入/回放显式提交拥有值的行镜像:校验宽度、非零且唯一 ID、保留上限与尺寸;成功才替换存储并推进结构 revision;失败不改任何已发布状态 | +| `Screen::publish(history, live)` | 导入的显式提交口:先校验行宽、非零且唯一 ID、活跃历史预算与尺寸,再把新存储准备好并交换,校验或分配失败都不留下半提交;成功才推进结构 revision,并把光标夹回新帧内 | | `Screen::locate(id)` | 精确 ID→rank;缺失 ID 返回空,**不再**退化为相邻行。只在可证有序时二分,其余逐行精确扫描 | -| `Screen::order_revision()` | 结构 revision:保留成员、文档顺序或 history/live 分界改变时前进;内容写入、仅移动光标的新行、同尺寸 resize 与同结构重复发布都不前进 | -| `Screen::mark_order_unknown()` | 绕过 `publish` 的直接结构写入必须调用;之后查询改走扫描,宁可慢也不再信任已经失效的二分 | -| `TextDocument::index_of` | 改用 `locate`,因此 `valid/normalize/meta` 及所有依赖都拿到精确 rank | -| `TextDocument::compare` | 按当前文档序比较两个有效位置,而不是比较 ID | +| `Screen::order_revision()` | 结构 revision:保留成员、文档顺序或 history/live 分界改变时前进;内容写入、仅移动光标的新行、同尺寸 resize 与同结构重复发布都不前进。内部结构变更走同一推进点,缓冲切换与回放恢复也计入 | +| `Screen::mark_order_unknown()` / `restore_frame(origin)` | 回放恢复改为显式入口:保留原帧镜像、推进 revision、声明顺序不再可证;绕过发布口的直接写入仍须调用 `mark_order_unknown()` | +| `TextDocument` | `index_of` 改用 `locate`(因此依赖它的位置比较与校验拿到精确 rank);新增按当前文档序的 `compare` 与 `ordered()` | | `HistoryViewport` | 活 anchor 用精确 rank;已淘汰 anchor 保留原有 `lower_bound` 回退,两条路径分开 | -| `TextSearch::navigate` | 前后导航改用文档序;有序时与旧 ID 比较等价 | -| `CandidateTerminal` | 导入改经 `publish`;回放帧直接恢复后显式声明 order 未知 | +| `TextSearch::navigate` | 前后导航改用文档序;`match()` 在顺序不可证时不再按 ID 二分校验扫描记录,改为整体校验 | +| `CandidateTerminal` | 导入改经 `publish`;回放恢复改经 `restore_frame`;保留历史先按物理行策略适配到当前帧宽,不再把上一帧宽度的行混进新帧 | ## 固定夹具与期望 @@ -29,24 +28,25 @@ history ids [1,3,4,5] live ids [7,6] | `locate(2)` / `locate(0)` | 缺失 / 缺失(不落到邻行) | | `lower_bound_line(2)` | 1(已淘汰 anchor 的旧回退保持独立) | | 文档序比较 | 7 在 6 之前,尽管 6 < 7 | -| 新增强制 Find 的 previous/next | previous(6)→7、next(7)→6 | -| 结构 revision | 只在下滚、物理 resize、缓冲切换、结构发布时前进 | +| 新增强制 Find 的 previous/next/no-wrap | previous(6)→7、next(7)→6、no-wrap previous(6)→7 | +| 结构 revision | 只在下滚、物理 resize、缓冲切换、结构发布、回放恢复时前进 | ## 实际验证 -环境:Windows 11 10.0.26200、Visual Studio 17 2022 / MSVC 19.42.34433.0、CMake 3.31.0、Release、Qt 与 candidate 关闭;源码 `33b58fa4452c4d4f23dcfc463f65c5c521e59065`。 +环境:Windows 11 10.0.26200、Visual Studio 17 2022 / MSVC 19.42.34433.0、CMake 3.31.0、Release;源码状态见 `validation.json`。 -- 完整默认 Release 构建 exit 0。 -- 核心 CTest `8/8` 通过:`order_query_core` 新增,`resize_core`/`history_core`/`selection_core`/`search_core`/`buffer_state_core`/`alternate_mode_core`/`decawm_mode_core` 维持通过。 -- 候选路径本机未启用适配器:`candidate_terminal.cpp` 仅做 `/W4` 独立编译(exit 0、无诊断),未链接、未运行 `candidate_core_test`,因此不构成候选行为验证。 -- 保留的首轮失败:新测试第一次运行 1/8,原因是夹具第二可见行写成 `y` 却期望两个 `x` 命中;另一次是把“已发布为乱序”错误期望成 `order_known()==true`。两处都只改测试夹具/期望,**不作为产品红→绿证据**。 +- 完整默认 Release 构建(Qt 与 candidate 关闭)exit 0。 +- 核心 CTest 筛选 8 项 `8/8` 通过:新增 `order_query_core`,`resize_core`/`history_core`/`selection_core`/`search_core`/`buffer_state_core`/`alternate_mode_core`/`decawm_mode_core` 维持通过。 +- 候选适配器另在本机以 `TERMFORGE_BUILD_CANDIDATE_ADAPTER=ON` 构建:`candidate_core` `1/1` 通过。CI 上同一项在首个推送版本为红(见下),修复后本地复跑为绿。 +- **首个推送版本的真实失败(保留)**:CI 的 `alacritty-headless` 两个 Ubuntu 作业在 `candidate_core` 报 `published row width differs from the screen`。原因是回放保留历史来自上一帧,宽度仍是旧值(111 列)而被发布进 149 列的新帧;旧代码直接 `assign` 掩盖了这个混合宽度的帧。修复方式是按 `Screen::resize` 的物理行策略把保留行适配到当前帧宽(缩窄置 `cropped`、宽带尾格清空),不是放宽发布校验。该红→绿只针对候选适配器,不转记为其他配置。 +- 新测试自身的首轮失败(`y`/`x` 夹具、`order_known` 期望、光标夹取期望)只改测试,**不作为产品红→绿证据**。 身份与原始命令见 [validation.json](validation.json)。 ## 未完成与本片不主张的结论 - `Screen::lines/history` 仍公开可写,外部仍可绕过 owner 发布;封口与全部 caller 迁移属下一片。 -- 有序增量 Find 内部的 `emit_from_id_`/`prune`/`rewind` 与 `match()` 的 ID 区间筛选仍假设有序:运行中跨重排的搜索未验收。 +- 有序增量 Find 内部的 `emit_from_id_`/`prune`/`rewind` 仍假设有序:运行中跨重排的搜索未验收;本片只把导航比较与 `match()` 的校验范围改成与顺序无关。 - 未实现区域 LF、新的 VT 模式、reflow 或用户可达重排;默认物理裁剪策略不变。 - 未运行 Qt、固定 ConPTY、真实 GUI、真实 TUI 或任何性能负载;没有性能数值或容差主张,#169 与 #27/#29 均未关闭。 @@ -56,4 +56,9 @@ history ids [1,3,4,5] live ids [7,6] cmake -S . -B build -G "Visual Studio 17 2022" -A x64 cmake --build build --config Release --target order_query_test history_test selection_test search_test resize_test buffer_state_test alternate_mode_test decawm_mode_test --parallel 2 ctest --test-dir build -C Release -R "^(order_query_core|history_core|selection_core|search_core|resize_core|buffer_state_core|alternate_mode_core|decawm_mode_core)$" --output-on-failure + +# 候选适配器(需要 Rust 工具链) +cmake -S . -B build/cand -G "Visual Studio 17 2022" -A x64 -DTERMFORGE_BUILD_CANDIDATE_ADAPTER=ON +cmake --build build/cand --config Release --target candidate_core_test --parallel 2 +ctest --test-dir build/cand -C Release -R "^candidate_core$" --output-on-failure ``` diff --git a/experiments/core/results/2026-10-29-order-query/validation.json b/experiments/core/results/2026-10-29-order-query/validation.json index 61138995..5cb3a993 100644 --- a/experiments/core/results/2026-10-29-order-query/validation.json +++ b/experiments/core/results/2026-10-29-order-query/validation.json @@ -2,18 +2,25 @@ "schema": "termforge-core-order-query-local-validation-v1", "issue": 169, "base_commit": "05f04d0222c06bed958f6894a27d2a1b0708d270", - "validated_source_commit": "33b58fa4452c4d4f23dcfc463f65c5c521e59065", + "validated_source_commit": "d5116e2cefc4755efc79a4bd8de26eb2629ca71a", + "first_pushed_source_commit": "33b58fa4452c4d4f23dcfc463f65c5c521e59065", + "docs_and_evidence_commit": "the commit that adds this file's current revision", "branch": "feat/core-order-query-r169", - "scope": "Screen 发布行结构、精确 ID→rank 查询、结构 revision;TextDocument/HistoryViewport/TextSearch 文档序;CandidateTerminal 经发布口导入。未封住公开 lines/history。", + "scope": "Screen 发布行结构、精确 ID→rank 查询、结构 revision、回放恢复入口;TextDocument 定位/比较、HistoryViewport 活 anchor、TextSearch 导航比较改用文档序;CandidateTerminal 经发布口导入。未封住公开 lines/history。", "configuration": { "platform": "Windows-11-10.0.26200", "generator": "Visual Studio 17 2022", "compiler": "MSVC 19.42.34433.0", "build_type": "Release", - "qt_shell": false, - "candidate_adapter": false, - "candidate_source_standalone_compile": true, - "candidate_runtime_exercised": false + "default_build": { + "qt_shell": false, + "candidate_adapter": false + }, + "candidate_build": { + "path": "build/cand", + "candidate_adapter": true, + "rust": "cargo 1.92.0" + } }, "observed_contract": { "fixture": { @@ -38,17 +45,18 @@ "6": 5, "7": 4 }, - "ordered_bisect_proof": false, "lower_bound_id_2": 1, "document_order_compare_7_before_6": true, "previous_navigation_from_id_6": 7, - "next_navigation_from_id_7": 6 + "next_navigation_from_id_7": 6, + "no_wrap_previous_from_id_6": 7 }, "order_revision_advances_on": [ "bottom-row scroll", "physical resize", "buffer activation", - "structural publication" + "structural publication", + "replay frame restore" ], "order_revision_unchanged_on": [ "cell write", @@ -56,47 +64,60 @@ "identical resize", "identical republication", "rejected publication" - ] + ], + "publication_rejections_covered": [ + "zero identity", + "duplicate identity", + "row width mismatch", + "no live row", + "history above active budget" + ], + "publication_cursor_policy": "clamp into the published frame, then invalidate the old attachment", + "candidate_history_fit": "preserved history rows from an older frame are fitted to the current width (shrink sets cropped, wide tail cell cleared) before publication" }, "commands": [ "cmake --build build --config Release --parallel 2", - "ctest --test-dir build -C Release -R '^(order_query_core|resize_core|history_core|selection_core|search_core|buffer_state_core|alternate_mode_core|decawm_mode_core)$' --output-on-failure", - "cl /nologo /std:c++17 /EHsc /W4 /c /I experiments/core /I experiments/candidates/alacritty/include experiments/core/candidate_terminal.cpp" + "ctest --test-dir build -C Release -R \"^(order_query_core|resize_core|history_core|selection_core|search_core|buffer_state_core|alternate_mode_core|decawm_mode_core)$\" --output-on-failure", + "cmake -S . -B build/cand -G \"Visual Studio 17 2022\" -A x64 -DTERMFORGE_BUILD_CANDIDATE_ADAPTER=ON", + "cmake --build build/cand --config Release --target candidate_core_test --parallel 2", + "ctest --test-dir build/cand -C Release -R \"^candidate_core$\" --output-on-failure" ], "results": { - "full_release_build": "exit 0", - "core_ctest": "8/8 passed", - "order_query_core": "passed", - "candidate_terminal_standalone_compile": "exit 0 with /W4, no diagnostics" + "full_default_release_build": "exit 0", + "core_ctest_filtered_8": "8/8 passed", + "candidate_core_local": "1/1 passed", + "first_pushed_ci": "alacritty-headless (ubuntu-24.04) failed: candidate_core -> published row width differs from the screen" }, "preserved_failures": [ - "order_query_core 首次运行 1/8,因测试夹具把第二可见行写成 y 却期望两个 x 命中;仅修正夹具后通过,不作为产品红绿。", - "同次首轮另一失败是把「已发布为乱序」期望成 order_known()==true;语义应为 false(bisect 证明不成立),仅修正测试期望。", - "候选适配器本机未启用,candidate_core_test 未构建或运行;只有 /W4 独立编译通过,不构成候选行为验证。" + "首个推送版本 33b58fa 的 CI:两个 Ubuntu alacritty-headless 作业在 candidate_core 失败,报 published row width differs from the screen。已定位为回放保留历史仍是上一帧宽度(111 列)被发布进 149 列新帧;旧代码的直接 assign 掩盖了混合宽度。修复按物理行策略适配保留行后,本地同配置复跑 1/1。该红→绿只属于候选适配器配置。", + "新测试首轮失败两次均来自测试自身:夹具第二可见行写成 y 却期望两个 x 命中;把已发布乱序期望成 order_known()==true;后续光标断言误期望列也归零。仅改测试,不作为产品红→绿。", + "Screen::lines/history 仍公开,封口与 caller 迁移未完成;有序增量 Find 的 emit_from_id_/prune/rewind 仍按 ID 假设,运行中重排未验收。", + "Qt、固定 ConPTY、真实 GUI/TUI 与任何性能负载均未由本项运行。" ], "source_sha256": { - "experiments/core/screen.hpp": "af146b42feae97cdc4a4856ba5364ac51d801709decc597923f83eb507d6b2d2", - "experiments/core/text_document.hpp": "659d1226c408cae46c72a21c7e74f85c004e654043451bee42a42953915e977a", - "experiments/core/text_search.hpp": "4cec4c85a17e671a4a2e5fbdf39fc653b7ca2969f52fa936e58fb099f4aef0b4", + "experiments/core/screen.hpp": "18d62af8d190c798218f7bce6d390ff8af5478aa6a1a0f1f1256059790866843", + "experiments/core/text_document.hpp": "d4862e269120fb7d9eb5e14f16d86ceadc905b0998fa45dbe8ea21665df5bb32", + "experiments/core/text_search.hpp": "ddd3b2fad0f39795f74003951a885f57bc7f2a5eb0678ab15111ebd523dad010", "experiments/core/history_view.hpp": "50dcb2f7e801082f7f21bc23f43e6944af711fde0af6e4c100152ca46ed9bd13", - "experiments/core/candidate_terminal.cpp": "6c398d52628d9f8ee336814a4084ed8eff435cc73bd92449d5976b50a7cd0409", - "experiments/core/order_query_test.cpp": "bccd24f4d88b089b236bb3499c830e92c1d29f9af2d9b681e313c5f1732e381e", + "experiments/core/candidate_terminal.cpp": "88457784ad947b55a3aa956bedde35a810fa2b77fc68de0d54515fed1dbe7812", + "experiments/core/order_query_test.cpp": "c546abd517b5a56ad39280d02c020a0bbfb172ccebfdbd5dacc30bddb531e409", "CMakeLists.txt": "cd5546218b758cff70471870d9b6b8b59479e330cfb432274253a4de8266a092" }, "binary_sha256": { - "order_query_test.exe": "9e1d0e5f057ea402132ca77e7e61a10577fc26e6dadbcf67dd8755aa2cfae551", - "history_test.exe": "3bcd89ea1d43e39317ad9953fbeb2a426eadba40451a86c7e6a395d9e3757927", - "selection_test.exe": "da448b9ec840fe29bb72de70b9ee38bca3e31846efc4eb0826b11fd31612421e", - "search_test.exe": "c480b3cca2906b5adfe688c2887f9e649dafde514a068b2d38c37abfb409c2f7", - "resize_test.exe": "d94cc45f32d3c3080be498e6967d7808f3812be3bcd41ad6a7b8cd5ef81b4a44", - "buffer_state_test.exe": "11ffe2b26e24998212620a879ec8f2c8c8f1ef29b72ea66594e41e2142337097", - "alternate_mode_test.exe": "adf1a8ba17e00a8a79dd38ade47d549e2e0ef696f2367b0cd5a4d9107c1533de", - "decawm_mode_test.exe": "dc66fc49c25ad0b1f55156eaaf40b2be3fdc511faa6c5d7fd328582552baded2" + "order_query_test.exe": "632dcc9f4a3b8a06dde355e269f52db48a3ba4b63c41aa1a3be39b1b6c69e885", + "history_test.exe": "cfb5fd1f9a9c0729186ec0a404ee7f30d87472d08629122b45419b1a9954bd12", + "selection_test.exe": "dc06e54ab192270d0bfdd6897e3de7f934cbe60c8bd1d67edbf855b0b895ba6c", + "search_test.exe": "e28165dd170cbb5e8bf724c4d545c10c1efd724bf454b4c4aa49670614b15daf", + "resize_test.exe": "de693ad51c7212c84ddad34b1faf6bd615cca71137b26ef57e328be1e22e5f3c", + "buffer_state_test.exe": "fa23349623be72f42ff207be2fb2d640ea5e76060197b78c3f9a99b211700221", + "alternate_mode_test.exe": "b5aad988e1fcc63e9731f0140124fd2f7af9c8699219f84607712f124768cf6b", + "decawm_mode_test.exe": "3795a7fa5f4507b96076ee16d6c629e89f92f5062b4e59e56dad275bb54fb635", + "candidate_core_test.exe": "f34a08f9f13ecc5e7a9d3e64ed19bf06d5e2a010fdf6c874c007133c317ba881" }, "limitations": [ - "Screen::lines/history 仍为公开成员:本片只提供 owner 发布口,未封住直接写入。", - "有序增量 Find 内部的 emit_from_id_/prune/rewind 与 match() 的 id 区间筛选仍假设有序;运行中任意重排未验收。", + "Screen::lines/history 仍为公开成员:本片只提供 owner 发布口与显式回放恢复,未封住直接写入。", + "有序增量 Find 内部的 emit_from_id_/prune/rewind 仍假设有序;运行中任意重排未验收。", "无 region LF、无新 VT 模式、无用户可达重排命令,也未改默认物理裁剪策略。", - "无窗口、无固定 ConPTY、无 Qt,未运行 GUI/原生/性能负载。" + "候选适配器只在本机以 Rust 依赖构建并运行 candidate_core;不覆盖真实 ConPTY 运输、GUI 或性能。" ] }