diff --git a/CMakeLists.txt b/CMakeLists.txt index c74d8be8..dc997fef 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -161,6 +161,10 @@ add_executable(resize_combined_test experiments/core/resize_combined_test.cpp) target_link_libraries(resize_combined_test PRIVATE termforge_core) termforge_warnings(resize_combined_test) +add_executable(resize_dual_axis_test experiments/core/resize_dual_axis_test.cpp) +target_link_libraries(resize_dual_axis_test PRIVATE termforge_core) +termforge_warnings(resize_dual_axis_test) + add_executable(resize_preserving_transaction_test experiments/core/resize_preserving_transaction_test.cpp) target_link_libraries(resize_preserving_transaction_test PRIVATE termforge_core) termforge_warnings(resize_preserving_transaction_test) @@ -451,6 +455,8 @@ add_test(NAME resize_height_measured_core COMMAND resize_height_measured_test) set_tests_properties(resize_height_measured_core PROPERTIES TIMEOUT 10) add_test(NAME resize_combined_core COMMAND resize_combined_test) set_tests_properties(resize_combined_core PROPERTIES TIMEOUT 10) +add_test(NAME resize_dual_axis_core COMMAND resize_dual_axis_test) +set_tests_properties(resize_dual_axis_core PROPERTIES TIMEOUT 10) add_test(NAME resize_preserving_transaction_core COMMAND resize_preserving_transaction_test) set_tests_properties(resize_preserving_transaction_core PROPERTIES TIMEOUT 60) add_test(NAME retained_resize_unicode_core COMMAND resize_unicode_test) @@ -540,6 +546,8 @@ add_test(NAME edit_csi_core COMMAND edit_csi_test) termforge_add_python_test(windterm_resize_height_measured_optimized -O -m unittest research.ui.test_resize_height_measured_reference research.ui.test_export_resize_height_measured_reference -v) termforge_add_python_test(windterm_resize_combined_reference -m unittest research.ui.test_resize_combined_reference research.ui.test_export_resize_combined_reference -v) termforge_add_python_test(windterm_resize_combined_optimized -O -m unittest research.ui.test_resize_combined_reference research.ui.test_export_resize_combined_reference -v) + termforge_add_python_test(windterm_resize_dual_axis_reference -m unittest research.ui.test_resize_dual_axis_reference research.ui.test_export_resize_dual_axis_reference -v) + termforge_add_python_test(windterm_resize_dual_axis_optimized -O -m unittest research.ui.test_resize_dual_axis_reference research.ui.test_export_resize_dual_axis_reference -v) termforge_add_python_test(windterm_resize_hidden_height_reference -m unittest research.ui.test_resize_hidden_height_reference research.ui.test_export_resize_hidden_height_reference research.ui.test_audit_resize_hidden_height_tail -v) termforge_add_python_test(windterm_resize_hidden_height_optimized -O -m unittest research.ui.test_resize_hidden_height_reference research.ui.test_export_resize_hidden_height_reference research.ui.test_audit_resize_hidden_height_tail -v) find_program(TERMFORGE_NODE_EXECUTABLE NAMES node) @@ -592,6 +600,10 @@ add_test(NAME edit_csi_core COMMAND edit_csi_test) ${CMAKE_SOURCE_DIR}/research/ui/test_resize_combined_stages.js ${CMAKE_SOURCE_DIR}/research/ui/resize_combined_reference.js) set_tests_properties(windterm_resize_combined_controller PROPERTIES TIMEOUT 15) + add_test(NAME windterm_resize_dual_axis_controller COMMAND ${TERMFORGE_NODE_EXECUTABLE} + ${CMAKE_SOURCE_DIR}/research/ui/test_resize_dual_axis_stages.js + ${CMAKE_SOURCE_DIR}/research/ui/resize_dual_axis_reference.js) + set_tests_properties(windterm_resize_dual_axis_controller PROPERTIES TIMEOUT 15) add_test(NAME windterm_resize_hidden_height_controller COMMAND ${TERMFORGE_NODE_EXECUTABLE} ${CMAKE_SOURCE_DIR}/research/ui/test_resize_hidden_height_stages.js ${CMAKE_SOURCE_DIR}/research/ui/resize_hidden_height_reference.js) diff --git a/docs/EVIDENCE.md b/docs/EVIDENCE.md index 4ad3f045..724aaf67 100644 --- a/docs/EVIDENCE.md +++ b/docs/EVIDENCE.md @@ -12,6 +12,7 @@ | 增量显示投影与跨步骤源身份 | [旧停滞诊断、13工程用例及身份](../experiments/core/results/2026-11-02-projection-job/README.md):500单列片段按200/200/100步完成一行,跨200边界反向选择/复制/Find一致,取消/同身份导入/旧owner等拒绝 | 新API无先行RED;旧诊断exit0表示复现停滞,归档于运行后。每步仍限200,不发布半页;源锚点非全局视觉行号。持续输出可能反复取消,任意定位/Qt调度未交付 | | 原版窗口高度分配及纯高度行为 | [独立两状态几何](../research/ui/results/2026-11-02-resize-geometry-reference/README.md)、[三阶段原版正文/DSR](../research/ui/results/2026-11-02-resize-height-measured-reference/README.md)和[核心12阶段回放](../experiments/core/results/2026-11-02-height-measured/README.md):window749→569实际改变180px,终端区域少120px;20→14→20行时正文不变,DSR19→14→14、scroll0→5→5 | 原10行预测失败保留,不回填缺失窗口测量;不把两状态比例推广。原top=null且非完整grid,scroll不证明内部物理历史。核心首轮夹具source_end越界保留,修夹具后1/1;没有生产修复或宽高组合/Qt验收 | | 自主 C++ 共 owner 宽高组合保留事务(#27/#29 显式入口) | [原版五阶段记录](../research/ui/results/2026-11-02-resize-combined-reference/README.md)、[字面回放、分配失败 sweep 与身份](../experiments/core/results/2026-11-02-retained-combined/README.md):`13186bf` 的旧"宽度保留 + 旧高度"路径 8/20 阶段、80/560 断言失败;`resize_preserving` 后 20/20、0/560,映射/选择 20/20、744 断言零失败;活动主屏/备用屏缩增各 151/42 次注入失败后重试一致 | 固定 75 B ASCII 主屏五阶段、后续仅 DSR;只走显式入口。默认 `Screen::resize` 与 Qt 仍物理裁剪,隐藏主屏、Unicode、resize 后写入、历史淘汰、Qt/固定 ConPTY 与性能未验收;工程不变量不是原版 oracle | +| 原版主屏双轴缩放+后续 CUP 写入后的视口/行/DSR(#27/#29) | [直接 Telnet 三阶段观察、严格出口与核心回放](../research/ui/results/2026-11-04-resize-dual-axis-reference/README.md):一次窗口动作 `91×20→61×14`(本次 NAWS 1→2),随后固定 `CUP(7;2)+DQ` 的**写后端点**为 `scroll=5`、DSR `7;4`、文档行 11 ` DQ`+58 空格+`w`(62);恢复 `91×20` 后写后端点仍 DSR `7;4`/scroll `5`/62 字符行;`resize_dual_axis_core` 11 项工程回放通过(pre-write 保留仅产品不变量) | 单次固定 75 B ASCII 主屏样本;**写后端点不能把裁剪/物化归因于 resize;没有 pre-write 原版正文观测;#308 无同样后续写入,不能作为裁剪差异对照**;不推断内部 history 条数/完整活动网格,不证明单窗口请求原子;不解释或放宽旧六阶段 B/CD native-sync 失败,ConPTY 原生网格与其它尺寸/顺序仍未观察;#27/#29 不关闭 | | 自主 C++ 区域续接、历史结构通知与每屏 origin | [模式交叉与红绿](../experiments/core/results/2026-11-02-r27-mode-cross/README.md)、[每屏 origin 修正](../experiments/core/results/2026-11-02-origin-frame/README.md)、[原版固定控制对照](../research/ui/results/2026-11-02-mode-state-reference/README.md) | 区域自动换行的跨行复制/Find、RIS/ED3 结构通知、Unicode/分块/重复/异常输入与独立主屏 origin 恢复;当前物理 resize 策略测试不冒充原版匹配,完整 #27 继续开放 | | Qt / 固定 ConPTY 模式与正文 resize 差异 | [独立字面预期及失败运输证据](../experiments/shell/results/2026-11-02-qt-terminal-modes/README.md) | 模式阶段经公开 Qt 按键和原生网格核对;缩窄后原生 reflow 与核心物理裁剪不同步,独立运输捕获未见重排字节。失败诊断与模式回归分列,#29 的正文重排和完整 #27 不由基础尺寸通过代替 | | 固定 ConPTY resize 与输出机制 | [1.25.260930003 包身份及 pinned 源码核查](research/conpty-resize-output-2026-11-02.md) | 当前 0x18 是字宽,旧 resize quirk 已删除;signal 成功非完成屏障,内部 reflow 不保证重发整屏,DSR 光标同步不证明正文一致。静态解释不替代同次 Qt 动态证据或原版 oracle;本次未新增运行或改 flags | diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 3fa9d120..1c998e33 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -23,7 +23,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 色及各会话独立有界队列、[输入法组合/提交/取消(事件级 + 固定拼音验证)](../research/ui/results/2026-10-30-qt-ime-fixed-pinyin/README.md),并接入[无修饰导航键与基础鼠标正反拖选高亮](../experiments/shell/results/2026-11-01-r2-core-input/README.md);[基础动态尺寸验证](../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保留。正式尺寸事务已改走显式保留入口并绘制独立显示投影/显示光标:[只缩宽的保留行契约](../experiments/shell/results/2026-11-03-qt-retained-display/README.md)在真实 child/固定 ConPTY 核对 12/12 检查,Qt 正式路径 CTest 5/5;旧六阶段 native-sync 失败保留并归类为原生重排网格与保留行合同的坐标差 | 分屏、其他 shell、工作区恢复、同时缩宽+缩高的原版视口/滚动与后续 CUP 证据、完整 reflow、历史浏览/自动滚动、选词/选行与系统复制/粘贴、Find、完整 Unicode 字素/绘制、候选窗与真实 IME 交叉(已有固定拼音;[固定时间点对照](research/qt-ime-candidate-panel-reference-2026-11-01.md)中输出后两侧都保持旧位置、resize 后延后点都更新;字体阶段原版未触发,面板身份未证明,且对照 Qt 源为历史 5fab30b)、真实 DPI、应用兼容与性能仍需实现或迁移;完整 WindTerm 标签/尺寸行为对照未完成;当前不是可发布开发包 | R2 | +| 自主 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 色及各会话独立有界队列、[输入法组合/提交/取消(事件级 + 固定拼音验证)](../research/ui/results/2026-10-30-qt-ime-fixed-pinyin/README.md),并接入[无修饰导航键与基础鼠标正反拖选高亮](../experiments/shell/results/2026-11-01-r2-core-input/README.md);[基础动态尺寸验证](../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保留。正式尺寸事务已改走显式保留入口并绘制独立显示投影/显示光标:[只缩宽的保留行契约](../experiments/shell/results/2026-11-03-qt-retained-display/README.md)在真实 child/固定 ConPTY 核对 12/12 检查,Qt 正式路径 CTest 5/5;旧六阶段 native-sync 失败保留并归类为原生重排网格与保留行合同的坐标差 | 分屏、其他 shell、工作区恢复、同时缩宽+缩高的原版视口/滚动与后续 CUP 证据(已有固定**写后端点**样本:一次动作 91×20→61×14 后执行 CUP 7;2+DQ,观测 scroll 5、DSR 7;4、62 字符行;无 pre-write 原版正文,裁剪归因未知;见[双轴结果](../research/ui/results/2026-11-04-resize-dual-axis-reference/README.md))、完整 reflow、历史浏览/自动滚动、选词/选行与系统复制/粘贴、Find、完整 Unicode 字素/绘制、候选窗与真实 IME 交叉(已有固定拼音;[固定时间点对照](research/qt-ime-candidate-panel-reference-2026-11-01.md)中输出后两侧都保持旧位置、resize 后延后点都更新;字体阶段原版未触发,面板身份未证明,且对照 Qt 源为历史 5fab30b)、真实 DPI、应用兼容与性能仍需实现或迁移;完整 WindTerm 标签/尺寸行为对照未完成;当前不是可发布开发包 | R2 | 自主 C++ 核心已具备双缓冲与限定 `?1049`、DECAWM、DECOM、DECCKM/DECTCEM、每屏 DECSTBM 边距、字符/行插删、ED3/RIS、DSR 6n 和部分 SGR 状态。依据分别见 [1049](../experiments/core/results/2026-10-29-cpp-1049/README.md)、[DECAWM](../experiments/core/results/2026-10-29-cpp-decawm/README.md)及[支持表](../experiments/core/README.md)。[原版每屏 origin 对照](../research/ui/results/2026-11-02-mode-state-reference/README.md)确认备用屏进入时关闭 origin、主屏返回恢复自身状态及固定样本的 margin 下界;核心已改为[每屏保存 origin](../experiments/core/results/2026-11-02-origin-frame/README.md)。区域 LF、IL/DL 后的精确行序,以及[区域自动换行续接、RIS/ED3 历史结构发布](../experiments/core/results/2026-11-02-r27-mode-cross/README.md)已有修正。完整网格/样式、resize 和运输交叉仍由 #27/#29 验收,不能据此声明完整 TUI 通过。 @@ -56,7 +56,7 @@ Qt 构建显式准备并部署[固定微软 CRT 14.51.36247.0](../third_party/ms | 中文 IME 与键盘 | **限定验收**:基础按键/修饰键/UTF-16 与实际接收字节;[固定 ZWJ/keycap 输入](../experiments/shell/results/2026-10-26-unicode-keyboard/validation.json)在默认/候选各两轮精确接收。固定 Microsoft 拼音[产品场景](../research/ime/results/2026-10-24-product-ime-contexts/validation.json)覆盖两区域组合/提交/取消、候选、输出/resize/zoom/受控 DPI、隐藏/关闭、历史阅读和原生控件 | 限定 Windows Server 2025 固定拼音及已有场景;同次组合由八会话工作流另验收。固定字素绘制/选择已由 #39 限定完成,但所有输入法、应用键盘模式、完整 IME 和真实不同 DPI 跨屏仍未完成 | R1 / R2 | | VT 核心 | **实现中,未验收**:默认自研支持基础光标/擦除(含 ECH)、CR/LF/BS/TAB、有限 SGR(粗体/暗色/下划线/反显/删除线/隐藏/上划线)和有界恢复;新增编辑类 `ICH`/`DCH`(`CSI @`/`CSI P`)、`IL`/`DL`(`CSI L`/`CSI M`)、滚动区域 `DECSTBM`(`CSI Pt;Pb r`,归位与非法区域行为按参考实现)、`ED3`(`CSI 3J` 只清回滚)与 `RIS`(`ESC c`),`DSR 6n` 回复、`DECCKM`(`?1`)与 `DECTCEM`(`?25`)已接入 Win32 与 Qt 两个入口,均有[核心契约测试](../experiments/core/edit_csi_test.cpp)与[回复测试](../experiments/core/reply_test.cpp)。显式候选 `.4` 接入 `Term/Processor`、C++ 拥有值适配、稳定行元数据、每缓冲区 margin 与流式字素;[共同轨迹](../experiments/differential/results/2026-10-26-candidate-common-v6/summary.json)及[源 margin 合同](../experiments/shell/results/2026-10-26-margin-state/validation.json)为各自固定版本结果 | [#27](https://github.com/JavaZeroo/termforge/issues/27) 的 origin/wrap、模式/resize、插删与滚动区域交叉未闭合;[#41](https://github.com/JavaZeroo/termforge/issues/41) 系统 ConPTY 已丢失的 margin 不能由末端补回。ECH pending-wrap 差异见[限定合同](research/more-capture-ech.md);`DA1` 仅 strict 回放回复 VT102,recovery 产品路径不回复;`DA2` 与 `OSC 10/11` 回复未实现,原版未知仍不算匹配;`SGR 58` 等下划线颜色属性亦未实现;#36/#38、完整呈现和平台支持独立验收 | R2 / R4 | | Unicode 与文字显示 | **实现中,未验收**:默认自研保留 Unicode 15。显式候选 `.4` 的 [#39 Unicode 17 流式字素合同](research/streaming-graphemes.md)已限定验收:固定 ZWJ/keycap 的单 lead/两列、光标、SGR/查询、原子选择复制、单列 resize 与有界无损分片;实际 ConPTY/DWrite 核对固定样本的单 glyph、字体和网格。正式 Qt/固定 ConPTY 路径已把固定 ASCII/CJK/补充平面/结合标样本贯通到独立 child 精确接收与离屏绘制,CJK/emoji 实际 fallback 面、advance 与无裁剪参考墨迹逐格记录([固定结果](../experiments/shell/results/2026-11-04-qt-unicode-presentation/README.md)) | [#30](https://github.com/JavaZeroo/termforge/issues/30) 仍需完整输入/IME 交叉;所有脚本/整形、通用 fallback、所有原生重放、真彩色/更多属性、Powerline 和真实跨屏未验收。Qt 呈现仅离屏固定样本,依赖宿主 fallback 面,U+200D/U+FE0F 仍被 recovery parser 替换为 U+FFFD(只记录,不批准新策略);分段 helper 的规范数据通过不代表完整终端 Unicode 正确性;[#54](https://github.com/JavaZeroo/termforge/issues/54) 保留已观察的性能成本 | R2 / R4 | -| resize 与 reflow | **实现中,未验收**:默认仍按 ADR-0005 物理行裁剪;[显式保留列宽事务与显示投影](../experiments/core/results/2026-11-02-retained-columns/README.md)通过四例有限寻址诊断;[同 owner 宽高组合保留事务](../experiments/core/results/2026-11-02-retained-combined/README.md)把固定五阶段原版回放从 8/20 阶段修至 20/20,并注册核心门禁;正式 Qt 路径已接入该事务、独立显示页、显示光标与映射命中([只缩宽契约](../experiments/shell/results/2026-11-03-qt-retained-display/README.md) 12/12、Qt 正式 CTest 5/5),默认 resize 入口未替换;[隐藏主屏纯高度变化保留正文的工程合同](../experiments/core/results/2026-11-04-hidden-height-retention/README.md)已注册门禁,[原版七阶段补充审计](../research/ui/results/2026-11-02-resize-hidden-height-reference/README.md)显示返回主屏时按当前宽度裁剪隐藏行;该差异已由用户于 2026-11-04 确认保留完整正文并接受为产品有意差异([ADR-0010](adr/0010-bounded-resize-retention.md)),正式 Qt / 固定 ConPTY 固定七阶段离屏路径已核对保留正文(58 检查,[结果](../experiments/shell/results/2026-11-04-qt-hidden-retention/README.md)),物理呈现/DPI 与完整 reflow 未验收。历史候选在[固定 ConPTY 重放](../experiments/core/results/2026-10-26-resize-replay/validation.json)与连续同步中保持行身份、Find/选区和有界历史,淘汰/清空不复活;候选 `.4` 另覆盖主/备用屏缩到单列 | [ADR-0010](adr/0010-bounded-resize-retention.md)已确认正文保留与显示投影方向,#27/#29 联合交叉仍未完成;[正式路径 B/CD 缩放诊断](../experiments/shell/results/2026-11-02-qt-terminal-modes/README.md)仍红且失败信息保留,已归类为原生重排网格与保留行坐标层差异,同时缩宽+缩高的原版视口/滚动与后续 CUP 仍缺独立证据;隐藏主屏高度的严格审计失败保留、离线补充审计已通过(单次七阶段,0 次重跑),返回时按当前宽度裁剪已确认为产品有意差异(保留完整正文,2026-11-04);固定七阶段正式路径已离屏核对,物理呈现/DPI、其他尺寸/连续输出与完整 reflow 仍待补。固定事务不等于完整 reflow 或 WindTerm 等价。[#156固定交错诊断](../experiments/shell/results/2026-10-29-resize-repaint-witness/README.md)见证默认核心在两种时序下选区失效,历史几何错配已有字节/消费网格证据;[#194](https://github.com/JavaZeroo/termforge/issues/194) 后续按用户接受未复现收口,未证明产品修复,重现时按该项条件重开。系统/实验 ConPTY、备用屏/滚动区、真实 DPI、呈现、性能及 #36/#38 分别验收;默认缩窄不可恢复 | R2 | +| resize 与 reflow | **实现中,未验收**:默认仍按 ADR-0005 物理行裁剪;[显式保留列宽事务与显示投影](../experiments/core/results/2026-11-02-retained-columns/README.md)通过四例有限寻址诊断;[同 owner 宽高组合保留事务](../experiments/core/results/2026-11-02-retained-combined/README.md)把固定五阶段原版回放从 8/20 阶段修至 20/20,并注册核心门禁;正式 Qt 路径已接入该事务、独立显示页、显示光标与映射命中([只缩宽契约](../experiments/shell/results/2026-11-03-qt-retained-display/README.md) 12/12、Qt 正式 CTest 5/5),默认 resize 入口未替换;[隐藏主屏纯高度变化保留正文的工程合同](../experiments/core/results/2026-11-04-hidden-height-retention/README.md)已注册门禁,[原版七阶段补充审计](../research/ui/results/2026-11-02-resize-hidden-height-reference/README.md)显示返回主屏时按当前宽度裁剪隐藏行;该差异已由用户于 2026-11-04 确认保留完整正文并接受为产品有意差异([ADR-0010](adr/0010-bounded-resize-retention.md)),正式 Qt / 固定 ConPTY 固定七阶段离屏路径已核对保留正文(58 检查,[结果](../experiments/shell/results/2026-11-04-qt-hidden-retention/README.md)),物理呈现/DPI 与完整 reflow 未验收。历史候选在[固定 ConPTY 重放](../experiments/core/results/2026-10-26-resize-replay/validation.json)与连续同步中保持行身份、Find/选区和有界历史,淘汰/清空不复活;候选 `.4` 另覆盖主/备用屏缩到单列 | [ADR-0010](adr/0010-bounded-resize-retention.md)已确认正文保留与显示投影方向,#27/#29 联合交叉仍未完成;[正式路径 B/CD 缩放诊断](../experiments/shell/results/2026-11-02-qt-terminal-modes/README.md)仍红且失败信息保留,已归类为原生重排网格与保留行坐标层差异,同时缩宽+缩高的原版视口/滚动与后续 CUP 已获固定**写后端点**直接证据(一次动作 91×20→61×14 后执行 CUP 7;2+DQ,观测 scroll 5、DSR 7;4、62 字符行;无 pre-write 原版正文,裁剪归因未知;见[双轴结果](../research/ui/results/2026-11-04-resize-dual-axis-reference/README.md)),其他尺寸/顺序与原版 ConPTY 原生网格仍未覆盖;隐藏主屏高度的严格审计失败保留、离线补充审计已通过(单次七阶段,0 次重跑),返回时按当前宽度裁剪已确认为产品有意差异(保留完整正文,2026-11-04);固定七阶段正式路径已离屏核对,物理呈现/DPI、其他尺寸/连续输出与完整 reflow 仍待补。固定事务不等于完整 reflow 或 WindTerm 等价。[#156固定交错诊断](../experiments/shell/results/2026-10-29-resize-repaint-witness/README.md)见证默认核心在两种时序下选区失效,历史几何错配已有字节/消费网格证据;[#194](https://github.com/JavaZeroo/termforge/issues/194) 后续按用户接受未复现收口,未证明产品修复,重现时按该项条件重开。系统/实验 ConPTY、备用屏/滚动区、真实 DPI、呈现、性能及 #36/#38 分别验收;默认缩窄不可恢复 | R2 | | 本地 shell 选择 | **实现中,未验收**:`ShellLaunch` 分离可执行文件、参数、工作目录及 shell 命令差异;cmd、Windows PowerShell 5.1、PowerShell 7、显式 Ubuntu WSL 各有[两轮真实启动/目录/尺寸/正常退出证据](../experiments/shell/results/2026-10-24-r2-shell-lifecycle/validation.json),无效程序启动错误可见且可关闭后恢复 cmd。四 shell 测试已补 [WSL 原生尺寸程序返回后的新提示符同步](research/wsl-size-return.md),正常退出证据只在实际全部回收后写入;默认/system 的事件反例、原两轮及完整回归分别留证 | 仅声明上述固定入口和隔离目录;#80 原 hosted stage6 是否同因仍未知,#81 hosted 准备超时另行定位;任意 profile/参数、通用分屏/排序、全部运行时错误提示及管理员 shell 未完成 | R2;权限场景 R4 | | 常用 TUI | **实现中,未验收**:显式候选 + 系统 ConPTY 有固定 Vim 进入、编辑、60×12 resize、退出写盘、主屏恢复及光标回复的[产品证据](../experiments/core/CANDIDATE.md);More 尾部 ECH 与捕获端点有[限定修复](research/more-capture-ech.md) | [#28](https://github.com/JavaZeroo/termforge/issues/28) 的更多应用及完整输入/输出模式未完成;PR #50 实验后端的 GUI Vim [#52](https://github.com/JavaZeroo/termforge/issues/52) 仍有失败;[native 捕获目录复用](research/vim-fixture-isolation.md)已独立定位并修正,不能代替 GUI 或完整 TUI 验收。headless 比较与自有 DIB 不等于真实呈现,`presented_done=null` | R2 | diff --git a/experiments/core/resize_dual_axis_test.cpp b/experiments/core/resize_dual_axis_test.cpp new file mode 100644 index 00000000..481dd7ce --- /dev/null +++ b/experiments/core/resize_dual_axis_test.cpp @@ -0,0 +1,114 @@ +// Bounded literal replay of the 2026-11-04 WindTerm dual-axis observation. +// +// Recorded original sample (research/ui/results/2026-11-04-resize-dual-axis-reference): +// 91x20 body -> one window action 61x14 -> CUP(7;2)+"DQ"+DSR (original DSR +// 7;4, scroll 5, document row 11 " DQ"+58 spaces+"w", 62 chars) -> one window +// action back to 91x20, DSR/scroll still 7;4/5 and the written row unchanged. +// The retained owner keeps the full 92-character row through the dual-axis +// transaction; the fixed write then materializes that edited row to the +// current 61-column footprint, which reproduces the recorded 62-character +// document row and the 7;4 cursor. This is the observed original behavior for +// this fixed write; it is not a general reflow or scroll oracle. +#include "terminal.hpp" +#include "text_document.hpp" +#include +#include +#include +#include +#include + +using namespace termforge; + +namespace { +struct Checks { + size_t assertions = 0, failures = 0; + void expect(bool condition, const char* message) { + ++assertions; + if (condition) return; + ++failures; + std::cerr << "dual-axis replay: " << message << '\n'; + } +}; +constexpr std::string_view source = "\x1b[?6l\x1b[?7h\x1b[r\x1b[0m\x1b[2J" + "\x1b[2;1HHTOP\x1b[6;1HHARD_A\r\nHARD_B\x1b[12;90Huvw\x1b[19;1HLOWEND"; +// Recorded original write/query and its literal reply. The original viewport +// scrolled by 5; the core owner has no viewport scroll layer, so the same +// protocol row/column is replayed directly. +constexpr std::string_view write_and_query = "\x1b[7;2HDQ\x1b[6n"; +std::vector source_lines(const TextDocument& document) { + if (document.size() > 200) throw std::runtime_error("source exceeds the bounded fixture"); + std::vector result; + std::string logical; + for (size_t rank = 0; rank < document.size(); ++rank) { + const auto line = document.line(rank); + logical += line.projection.text; + if (!line.joins_next) { + result.push_back(std::move(logical)); + logical.clear(); + } + } + if (!logical.empty()) result.push_back(std::move(logical)); + while (!result.empty() && result.back().empty()) result.pop_back(); + return result; +} +std::string line_text(const Line& line) { + std::string text; + for (const auto& cell : line) text += cell.text; + return text; +} +std::string take_reply(Parser& parser) { + const auto replies = parser.take_replies(); + return replies.empty() ? std::string{} : replies.front(); +} +} // namespace + +int main() { + try { + Checks checks; + const std::string clipped = " DQ" + std::string(58, ' ') + "w"; + Parser parser(20, 91, 1000); + parser.feed(source); + parser.feed("\x1b[6n"); + checks.expect(take_reply(parser) == "\x1b[19;7R", "initial DSR differs from the recorded 19;7"); + const auto initial = source_lines(TextDocument(parser.screen)); + checks.expect(initial.size() == 18 && initial[11] == std::string(89, ' ') + "uvw", + "initial retained body differs from the fixed 92-character row"); + + // The recorded single dual-axis window action. The original run has no + // pre-write body endpoint: its only post-resize body row includes the + // fixed CUP+write. The retained pre-write row is therefore a TermForge + // engineering invariant, not a verified original difference. + parser.screen.resize_preserving(14, 61); + const auto retained = source_lines(TextDocument(parser.screen)); + checks.expect(retained.size() == 18 && retained[11] == std::string(89, ' ') + "uvw", + "dual-axis resize lost the retained source row before the fixed write"); + + parser.feed(write_and_query); + checks.expect(take_reply(parser) == "\x1b[7;4R", "post-shrink DSR differs from the recorded 7;4"); + checks.expect(parser.screen.row == 6 && parser.screen.column == 3, + "core protocol cursor does not match the recorded 1-based 7;4"); + const auto shrunk = source_lines(TextDocument(parser.screen)); + checks.expect(shrunk.size() == 18, "post-write document row count differs from the recording"); + checks.expect(shrunk[11] == clipped, + "edited row differs from the recorded 62-character clipped row"); + checks.expect(line_text(parser.screen.visible_lines()[6]).find("DQ") != std::string::npos, + "fixed write is not addressable on retained row 11"); + + // The recorded second dual-axis window action; the written row stays at + // 62 characters and the literal DSR stays 7;4. + parser.screen.resize_preserving(20, 91); + parser.feed("\x1b[6n"); + checks.expect(take_reply(parser) == "\x1b[7;4R", "post-restore DSR differs from the recorded 7;4"); + const auto restored = source_lines(TextDocument(parser.screen)); + checks.expect(restored == shrunk, "width restore rewrote the recorded written row"); + checks.expect(restored[11] == clipped, "restored document row differs from the recording"); + parser.finish(); + if (checks.failures) throw std::runtime_error("dual-axis literal replay failures"); + std::cout << "dual-axis literal replay passed; " << checks.assertions + << " assertions against the fixed 2026-11-04 observation\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/research/ui/export_resize_dual_axis_reference.py b/research/ui/export_resize_dual_axis_reference.py new file mode 100644 index 00000000..1d419024 --- /dev/null +++ b/research/ui/export_resize_dual_axis_reference.py @@ -0,0 +1,307 @@ +"""Strict publication of one five-stage width/height protocol and finite source text.""" +from __future__ import annotations +import argparse +import hashlib +import json +from pathlib import Path, PureWindowsPath +import re +import sys +from types import FunctionType, SimpleNamespace + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +from research.ui import export_mode_state_reference as common +from research.ui import export_resize_state_reference as resize_export +from research.ui import resize_dual_axis_reference as probe + +require, sha, digest, integer = common.require, common.sha, common.digest, common.integer +MARKERS = probe.MARKERS +EXPORT_SOURCES = ("research/ui/export_resize_dual_axis_reference.py", "research/ui/export_resize_state_reference.py", + "research/ui/export_mode_state_reference.py") + + +def raw_names(case): + return ("evidence.json", "run.json", "probe.js", "peer.json", probe.RESULT_FILE, "files.json", + "mechanics-check.json", "stderr.txt", "profile-check.json", "cleanup.json", "fixture-ready", + *(f"ack-{n}" for n in range(len(probe.phases(case)))), + *(f"resize-{kind}-{n}" for n in probe.resize_phases(case) for kind in ("request", "ready"))) + + +def read(path): + require(path.stat().st_size <= 4*1024*1024, "Recording exceeds byte bound") + def unique(pairs): + result = {} + for key, value in pairs: + require(key not in result, "Duplicate recording field") + result[key] = value + return result + def nonfinite(_): + raise ValueError("Nonfinite JSON value") + return json.loads(path.read_text(encoding="utf-8"), object_pairs_hook=unique, parse_constant=nonfinite) + + +def adapt(source, changes): + for old, new in changes.items(): + require(source.count(old) == 1, "Protected offline audit adapter changed: "+old) + source = source.replace(old, new) + return source + + +def extract(path, start, end): + source = path.read_text(encoding="utf-8") + require(source.count(start) == source.count(end) == 1, "Protected offline audit function boundary changed") + return start+source.split(start, 1)[1].split(end, 1)[0] + + +def expected_probe(plan, run, source_root): + integer(run["pid"], 2**32-1, 1); integer(run["tid"], 2**32-1, 1) + require(type(run["window_station"]) is str and re.fullmatch(r"TermForgeR1-[0-9]+", run["window_station"]), + "Owned station identity differs") + template = (source_root / "research/ui/qt_r1_text.js").read_text(encoding="utf-8").split("// Scenario driver", 1)[0] + driver = (source_root / "research/ui/resize_dual_axis_reference.js").read_text(encoding="utf-8") + require(driver.count("__RESIZE_DUAL_AXIS_CASE_JSON__") == 1, "Case template anchor differs") + template += driver.replace("__RESIZE_DUAL_AXIS_CASE_JSON__", json.dumps(probe.controller_contract(plan["case_id"]))) + require(hashlib.sha256(template.encode()).hexdigest() == run["template_sha256"], "Probe template differs") + generated = template.replace("__UI_THREAD_ID__", str(run["tid"])).replace("__OWNED_STATION__", run["window_station"]) + generated = generated.replace("__RUN_DIRECTORY_JSON__", json.dumps(PureWindowsPath(plan["out"]).as_posix())) + generated = generated.replace("__HOSTED_CI__", "false").replace("__SCENARIO_TIMEOUT_MS__", "43000") + return generated.encode().replace(b"\n", b"\r\n") + + +# The immutable existing helpers are adapted in fresh namespaces. Each textual +# boundary is checked, and all three offline source files are identified below. +_source = extract(Path(common.__file__), "def provenance_facts(", "\n\ndef export(") +_source = adapt(_source, { + "One original launch attempt; retain after failure and never retry this plan.": probe.ATTEMPT.rstrip("\n"), + "probe.source_identity(root=source_root, source_files=probe.SOURCE_FILES)": + "probe.common.source_identity(root=source_root, source_files=probe.SOURCE_FILES)", +}) +_PROVENANCE = {**vars(common), "ROOT": ROOT, "probe": probe, "read": read} +exec(compile(_source, str(Path(common.__file__)), "exec"), _PROVENANCE) + + +def provenance_facts(directory, plan_path, plan, source_root, reference_source): + function = FunctionType(_PROVENANCE["provenance_facts"].__code__, {**_PROVENANCE, "RAW_NAMES": raw_names(plan["case_id"])}) + return function(directory, plan_path, plan, source_root, reference_source) + + +_marker_projection = FunctionType(common.marker_projection.__code__, {**vars(common), "MARKERS": MARKERS}) + + +def marker_projection(terminal): + result = [] + for marker, raw in zip(_marker_projection(terminal), terminal["markers"]): + line = terminal["lines"][marker["document_line"]] + # The finite source alphabet is entirely ASCII. Python scalar offsets and + # JS UTF-16 offsets therefore coincide here, not for arbitrary Unicode. + require(len(line.encode("utf-16-le"))//2 == len(line), "Unexpected non-ASCII text") + start = integer(raw["line_start_api"], 262144) + require(marker["position"] == start+marker["column"], "Captured API-call arithmetic differs") + rect = marker["rect"] + if rect is not None: + require(0 <= rect["width"] <= 1360 and 0 <= rect["height"] <= 749 and + -1360 <= rect["x"] <= 2720 and -200*749 <= rect["y"] <= 2*749, + "Marker rectangle exceeds fixed window/document bounds") + result.append({"label": marker["label"], "document_line": marker["document_line"], + "line_text_offset_utf16": marker["column"], "line_text_offset_scalar": marker["column"], + "line_text_prefix_utf8_bytes": len(line[:marker["column"]].encode("utf-8")), + "line_start_api_value": start, "requested_position_api_value": marker["position"], "rect": rect}) + return result +geometry = FunctionType(resize_export.geometry.__code__, {**vars(resize_export), "probe": probe}) + + +def _base_lines(): + return {"", "HTOP", "HARD_A", "HARD_B", "LOWEND", " "*89+"uvw"} + + +def dual_document_lines(case, phase): + """Bounded source-derived lines for the fixed dual-axis run. + + The observed original reflow clips a retained 91-cell source row to the + current 61 columns and keeps the single soft-continuation ``w`` cell; the + one fixed write replaces two cells in that first clipped row. Both the + pre-write clipped form and its bounded write variants are enumerated here. + """ + lines = set(probe.synthetic_lines(case, phase)) + for line in _base_lines(): + clipped = line[:61] + if line == " "*89+"uvw": + for column in range(0, 60): + row = " "*61 + modified = row[:column] + "DQ" + row[column+2:] + "w" + lines.update((modified, modified.rstrip(" "))) + for column in range(0, 60): + if len(clipped) >= column+2: + modified = clipped[:column] + "DQ" + clipped[column+2:] + else: + modified = clipped.ljust(column) + "DQ" + lines.update((modified, modified.rstrip(" "), modified.ljust(61))) + return frozenset(lines) + + +def dual_base_lines(case, phase): + base = set(probe.base_synthetic_lines(case, phase)) + base.update(line[:61] for line in _base_lines()) + base.update(line[:61].rstrip(" ") for line in _base_lines()) + return frozenset(base) + + +def dual_validate_document(case, phase, terminal): + require(type(terminal.get("line_count")) is int and 0 <= terminal["line_count"] <= 200 and + type(terminal.get("lines")) is list and len(terminal["lines"]) == terminal["line_count"], "Document bound") + lines = terminal["lines"] + allowed = dual_document_lines(case, phase) + require(all(type(line) is str and line.isascii() and line in allowed for line in lines), + "Only fixed dual-axis source-derived synthetic document lines are allowed") + counts = {marker: sum(line.count(marker) for line in lines) for marker in MARKERS} + require(all(count <= 1 for count in counts.values()), "Repeated dual-axis-source marker") + if phase == 0: + require(all(count == 1 for count in counts.values()), "Initial fixture sentinel missing") + return None + written = [line for line in lines if line not in dual_base_lines(case, phase)] + require(len(written) == 1 and "DQ" in written[0], "Fixed write marker missing or repeated") + return written[0] + +def dual_audit_mechanics(directory, case): + original = probe.validate_document + probe.validate_document = dual_validate_document + try: + return probe.audit_mechanics(directory, case) + finally: + probe.validate_document = original + + +_source = extract(Path(resize_export.__file__), "def capture_facts(", "\n\ndef export(") +_source = adapt(_source, { + "probe.audit_mechanics(directory)": 'dual_audit_mechanics(directory, plan["case_id"])', + '"resize-state-result.json"': 'probe.RESULT_FILE', + "6 <= len(messages) <= 69": 'len(probe.phases(plan["case_id"]))+3 <= len(messages) <= len(probe.phases(plan["case_id"]))+66', + '"scenario": "resize-state-v1"': '"scenario": "resize-dual-axis-v1", "case_id": plan["case_id"]', + "count = len(payloads)-5": 'count = len(payloads)-len(probe.phases(plan["case_id"]))-2', + '"resize_state_phase"': '"resize_dual_axis_phase"', + '"resize_state_result"': '"resize_dual_axis_result"', + "for phase in (1, 2):": 'for phase in probe.resize_phases(plan["case_id"]):', + "probe.phase_records(grids[0][1], grids[0][0])": 'probe.phase_records(plan["case_id"], grids[0][1], grids[0][0])', + "synthetic_lines(grids[0][0])": 'dual_document_lines(plan["case_id"], source["phase"])', + '"dsr_request_bytes": 12': '"dsr_request_bytes": 4*len(probe.phases(plan["case_id"]))', + 'enumerate(peer["resize_boundaries"], 1)': 'zip(probe.RESIZE_PHASES, peer["resize_boundaries"])', + '"phase_messages": 3': '"phase_messages": len(probe.phases(plan["case_id"]))', +}) +_CAPTURE = {**vars(resize_export), "ROOT": ROOT, "probe": probe, "read": read, + "expected_probe": expected_probe, "geometry": geometry, "marker_projection": marker_projection, + "dual_audit_mechanics": dual_audit_mechanics, "dual_document_lines": dual_document_lines} +exec(compile(_source, str(Path(resize_export.__file__)), "exec"), _CAPTURE) + + +def capture_facts(directory, plan, source_root=ROOT): + case = probe.validate_case(plan["case_id"]) + # A fresh binding supplies each immutable case's geometry. The imported + # runtime and frozen exporter namespaces are never mutated. + case_probe = SimpleNamespace(**{**vars(probe), "WINDOWS": probe.windows(case), "RESIZE_PHASES": probe.resize_phases(case)}) + function = FunctionType(_CAPTURE["capture_facts"].__code__, {**_CAPTURE, "probe": case_probe}) + result = function(directory, plan, source_root) + raw = read(directory / probe.RESULT_FILE) + # Publish the sampled geometry guard, not merely the requested endpoint. + # The runtime saved these scalars before asserting geometry; failed runs + # remain private and cannot reach this successful export path. + for index, check in enumerate(result["geometry_checks"]): + observed = raw["geometry_checks"][index] + require(type(observed["observed_grid"]) is list and len(observed["observed_grid"]) == 2, + "Observed geometry grid shape differs") + grid = [integer(observed["observed_grid"][0], 500, 1), integer(observed["observed_grid"][1], 200, 1)] + capacity = integer(observed["terminal_capacity"], 200, 1) + event_count = integer(observed["event_count"], 64, 1) + require(grid == result["frozen_grids"][index] and capacity == grid[1] and + event_count == result["freeze_event_counts"][index], "Observed geometry guard differs") + check.update(observed_grid=grid, terminal_capacity=capacity, event_count=event_count) + roles = [label for label, _ in probe.phases(case)] + for index, (phase, observed) in enumerate(zip(result["phases"], raw["observations"])): + phase["stage_role"] = roles[index] + lines = observed["terminal"]["lines"] + phase["document_projection"]["synthetic_lines_sha256"] = hashlib.sha256(json.dumps(lines, separators=(",", ":")).encode()).hexdigest() + # Validate exact source derivation before publishing any reconstructible + # text. The unrestricted original recording remains private. + dual_validate_document(case, index, observed["terminal"]) + catalog = sorted(dual_document_lines(case, index)) + positions = {line: offset for offset, line in enumerate(catalog)} + phase["document_projection"]["synthetic_text"] = { + "encoding": "UTF-8", "catalog_source": "resize_dual_axis_reference.synthetic_lines(case_id, phase); sorted ASCII scalar order", + "catalog_count": len(catalog), + "catalog_sha256": hashlib.sha256(json.dumps(catalog, separators=(",", ":")).encode()).hexdigest(), + "lines": [{"catalog_index": positions[line], "utf8_hex": line.encode("utf-8").hex(), + "utf8_bytes": len(line.encode("utf-8")), "unicode_scalars": len(line), + "utf16_code_units": len(line.encode("utf-16-le"))//2} for line in lines]} + result["stateful_observation_count"] = len(roles) + result["post_initial_terminal_input"] = plan["post_initial_terminal_input"] + result["independent_dsr_bounds"] = {"row": [1, 200], "column": [1, 500]} + result["text_units"] = {"source": "UTF-8 bytes", "document_line": "original public API index", + "marker_text_offsets": "JS UTF-16 code units; scalar count coincides only for this ASCII source alphabet", + "position_api": "recorded lineStart API value plus JS UTF-16 offset, passed unchanged to visibleRectFromPosition", + "caret_api": "original API scalar values; numbering policy not inferred", + "rectangles": "actual public API pixel rectangles, kept separate from protocol cells", + "protocol_cursor": "DSR values kept independent of text offsets and pixel rectangles", + "cpp_cell_column_mapping": None, "document_position_unit_classification": None, + "ascii_only": True} + return result + + +def export(directory, plan_path, destination, expected_plan_sha256, source_root=ROOT, reference_source=None): + directory, plan_path, destination, source_root = [Path(path).resolve() for path in (directory, plan_path, destination, source_root)] + require(not destination.exists(), "Export destination must be new") + require(sha(plan_path) == digest(expected_plan_sha256), "Authorized plan bytes differ") + plan = read(plan_path) + require(all(type(plan.get(k)) is type(v) and plan[k] == v for k, v in probe.fixed_contract(plan.get("case_id")).items()), + "Fixed resize-dual-axis contract differs") + require(plan.get("execution_authorized") is True, "Plan did not authorize execution") + facts = capture_facts(directory, plan, source_root) + reference_source = Path(plan["source"]) if reference_source is None else Path(reference_source) + provenance = provenance_facts(directory, plan_path, plan, source_root, reference_source.resolve()) + basis = probe.fixed_contract(plan["case_id"])["geometry_basis"] + public_basis = {"dual_axis_grid_previously_observed": False} + for key in ("geometry", "sequential_width_height"): + entry = basis[key] + require(type(entry["execution_source_base_commit"]) is str and + re.fullmatch(r"[0-9a-f]{40}", entry["execution_source_base_commit"]), "Basis source commit differs") + public_basis[key] = {"reference": entry["reference"], + "strict_export_sha256": digest(entry["strict_export_sha256"]), + "execution_source_base_commit": entry["execution_source_base_commit"]} + result = {"schema": "termforge-original-resize-dual-axis-reference-v1", "issue": 29, "related_issue": 27, + "case_id": plan["case_id"], "authorized_plan_sha256": expected_plan_sha256, + "geometry_basis": public_basis, + "offline_export_source_files_sha256": {name: sha(ROOT / name) for name in EXPORT_SOURCES}, + "status": "observation", "observation_kind": "bounded-direct-protocol-and-dual-axis-resize", "actual_original_runs": 1, "maximum_original_runs": 1, + "semantic_acceptance": False, "dual_axis_policy_classification": None, "full_active_grid_verified": False, + "terminal_styles_verified": False, "local_shell_transport_verified": False, "presented_done": None, + "performance_gate": "not_evaluated", "r2_complete": False, "rea_capture_seconds": 45, + "cli_wait_timeout_seconds": 60, "whole_lifecycle_bound_seconds": None, + "deliberate_original_process_termination": True, "natural_gui_exit_verified": False, + "capture": facts, "provenance": provenance, + "write_probes": read(directory / "mechanics-check.json")["write_probes"], + "write_probe_definition": read(directory / "mechanics-check.json")["write_probe_definition"], + "limitations": [ + "One fresh direct Telnet process executes the immutable dual-axis-retention schedule 91x20 -> 61x14 (one window request changing both axes) -> 91x20; every stage is observed before the next resize.", + "After the dual-axis shrink, one fixed CUP(7;2)+\"DQ\" write and DSR are sent; after the dual restore, DSR only. No other CUP, CR/LF, redraw or replacement text is sent.", + "Stage zero requires every preloaded sentinel and DSR19;7; later missing markers and the observed write position are observations, never semantic acceptance.", + "The dual-axis 61x14 endpoint and its NAWS event count/order are saved before assertions; the request is one window action, not an assumed atomic two-axis transport event. No calibration or retries.", + "The one write is recorded through its document line, text offset and DSR; no general original position-unit, scroll or C++ Cell mapping is inferred.", + "The existing geometry and sequential width/height records identify prior evidence only; this run does not inherit their semantic behavior.", + "DSR safety bounds remain row1..200/column1..500 independently of the fourteen-row viewport.", + "Only finite ASCII source-derived catalog strings may be published, with complete reconstructible document arrays. Unrestricted recording stays private.", + "This single schedule does not establish alternate-frame, Unicode, arbitrary dimensions or resize order, further post-resize editing, ConPTY, performance or R2 acceptance, and does not merge the original observation with any confirmed TermForge difference."]} + payload = (json.dumps(result, ensure_ascii=False, indent=2, allow_nan=False)+"\n").encode() + destination.parent.mkdir(parents=True, exist_ok=True) + with destination.open("xb") as stream: + stream.write(payload) + return result + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + for name in ("directory", "plan", "destination"): + parser.add_argument("--"+name, type=Path, required=True) + parser.add_argument("--expected-plan-sha256", required=True) + parser.add_argument("--source-root", type=Path, default=ROOT) + parser.add_argument("--reference-source", type=Path) + args = parser.parse_args() + result = export(args.directory, args.plan, args.destination, args.expected_plan_sha256, args.source_root, args.reference_source) + print(json.dumps({"exported": True, "case_id": result["case_id"], "phases": len(result["capture"]["phases"]), "sha256": sha(args.destination)})) diff --git a/research/ui/resize_dual_axis_reference.js b/research/ui/resize_dual_axis_reference.js new file mode 100644 index 00000000..fe9f733a --- /dev/null +++ b/research/ui/resize_dual_axis_reference.js @@ -0,0 +1,132 @@ +// Every fixed single-axis resize is observed before the next; no input after the ASCII body except DSR. +let stage=0,busy=false,finished=false,due=0,stable=null,stableSince=0,lastPhase=-1; +let gridKey=null,gridSince=0,nawsSeen=0,lastNaws=null,requested=0,resized=0,committed=0; +const contract=__RESIZE_DUAL_AXIS_CASE_JSON__,caseId=contract.case_id; +const counts={'dual-axis-retention':3}; +const phaseCount=counts[caseId],lastStage=phaseCount-1; +const windows=contract.windows,grids=contract.grids,resizePhases=contract.resize_phases; +if(!phaseCount||windows.length!==phaseCount||grids.length!==phaseCount||contract.labels.length!==phaseCount||resizePhases.length!==phaseCount-1||!Array.isArray(contract.marker_labels))throw Error('fixed dual-axis contract'); +const started=Date.now(),observations=[],geometryChecks=[]; +for(const module of [wind,core,gui,widgets]){ + const expected=(__RUN_DIRECTORY_JSON__+'/app/'+module.name).replace(/\\/g,'/').toLowerCase(); + if(module.path.replace(/\\/g,'/').toLowerCase()!==expected)throw Error('pinned Qt module path guard'); +} +function currentTerminal(){ + const raw=evalText(`JSON.stringify((function(){var t=window.activeText;if(!t)return null;function get(k){try{var v=typeof t[k]==='function'?t[k]():t[k];return v===undefined?null:v;}catch(e){return null;}}var n=get('visibleLineCount'),lines=[],markers=[],labels=${JSON.stringify(contract.marker_labels)};if(typeof n!=='number'||n<0||n>200)throw Error('document line bound');for(var i=0;i1024)throw Error('document text bound');lines.push(line);for(var j=0;j=256)throw Error('marker bound');var start=t.lineStart(i),p=start+offset;markers.push({label:labels[j],document_line:i,column:offset,line_start_api:start,position:p,rect:t.visibleRectFromPosition(p)});}}}return {protocol:get('protocolName'),connected:get('isConnected'),top:get('topLine'),scroll:get('scrollTop'),line_count:n,capacity:get('screenLineCapacity'),lines:lines,markers:markers,caret_api:{caretLine:get('caretLine'),caretColumn:get('caretColumn'),caretPosition:get('caretPosition')}};})())`); + return raw?JSON.parse(raw):null; +} +function write(name,value){const f=new File(__RUN_DIRECTORY_JSON__+'/'+name,'w');f.write(value);f.close();} +function windowGeometry(){ + const value=rect(main); + if(!Array.isArray(value)||value.length!==4||!value.every(Number.isInteger)||value[2]Number.isInteger(value)?value:null; + return {phase,requested_window:windows[phase],...windowGeometry(), + observed_grid:peer?[scalar(peer.columns),scalar(peer.rows)]:null, + terminal_capacity:terminal?scalar(terminal.capacity):null, + event_count:peer&&Array.isArray(peer.naws_history)?peer.naws_history.length:null}; +} +function checkGeometry(phase,peer,terminal){ + if(geometryChecks.length!==phase)throw Error('window geometry check order'); + const sample=geometrySample(phase,peer,terminal); + const measured={phase,window_rect:sample.window_rect,window_size:sample.window_size,expected_size:windows[phase], + observed_grid:sample.observed_grid,terminal_capacity:sample.terminal_capacity,event_count:sample.event_count}; + // Persist all measured scalars before any prediction can reject this endpoint. + geometryChecks.push(measured); + if(JSON.stringify(measured.window_size)!==JSON.stringify(windows[phase]))throw Error('public window dimensions differ'); + if(JSON.stringify(measured.observed_grid)!==JSON.stringify(grids[phase]))throw Error('fixed measured grid differs'); + if(measured.terminal_capacity!==grids[phase][1])throw Error('fixed measured capacity differs'); +} +function finish(status,error){ + if(finished)return;finished=true; + const result={status,case_id:caseId,error:error||null,observations,window_rect_format:'qt-qrect-inclusive-ltrb',geometry_checks:geometryChecks, + full_active_grid_verified:false,local_shell_transport_verified:false,presented_done:null}; + if(status==='failed'){ + result.failure_geometry=null; + const phase=Math.min(requested||observations.length,lastStage); + try{result.failure_geometry=geometrySample(phase,null,null);}catch(_){} + // Independent window evidence survives a failing terminal text binding. + if(result.failure_geometry){ + try{ + const peer=peerState(),scalar=value=>Number.isInteger(value)?value:null; + if(peer){result.failure_geometry.observed_grid=[scalar(peer.columns),scalar(peer.rows)]; + result.failure_geometry.event_count=Array.isArray(peer.naws_history)?peer.naws_history.length:null;} + }catch(_){} + try{const terminal=currentTerminal();result.failure_geometry.terminal_capacity=terminal&&Number.isInteger(terminal.capacity)?terminal.capacity:null;}catch(_){} + } + } + write('resize-dual-axis-result.json',JSON.stringify(result));send({kind:'resize_dual_axis_result',...result}); +} +function resetGrid(){gridKey=null;gridSince=0;} +function stableGrid(peer,terminal){ + const history=peer.naws_history; + if(!Array.isArray(history)||history.length64)throw Error('NAWS history bound'); + for(let i=nawsSeen;i!Number.isInteger(n)||n<0||n>65535))throw Error('NAWS event bound'); + const value=JSON.stringify(event);if(lastNaws!==null&&lastNaws!==value)resetGrid();lastNaws=value; + } + nawsSeen=history.length; + const value=JSON.stringify([peer.columns,peer.rows,terminal.capacity]); + if(!peer.columns||peer.rows!==terminal.capacity||JSON.stringify([peer.columns,peer.rows])!==lastNaws){resetGrid();return false;} + if(value!==gridKey){gridKey=value;gridSince=Date.now();return false;} + return Date.now()-gridSince>=600; +} +function requestResize(phase){requested=phase;write('resize-request-'+phase,JSON.stringify({phase,window:windows[phase]}));} +send({kind:'probe_ready',station_isolated:true,scenario:'resize-dual-axis-v1',case_id:caseId}); +Interceptor.attach(user.getExportByName('DispatchMessageW'),{onEnter(){ + if(busy||finished||Process.getCurrentThreadId()!==uiThread||Date.now()41000)throw Error('bounded scenario deadline'); + if(stage===0){if(!ready('ui'))return;resize(main,...windows[0]);stage=1;} + else if(stage===1){scan();expandAll(tree);focus(tree,7);key(tree,0x1000011);stage=2;} + else if(stage===2){key(tree,0x1000004);stage=3;} + else{ + const peer=peerState();if(!peer||!peer.connected){resetGrid();return;} + if(peer.error)throw Error('peer '+peer.error); + if(peer.complete){if(observations.length!==phaseCount)throw Error('incomplete resize-dual-axis observations');finish('observed-fixed-resize-dual-axis-states',null);return;} + const terminal=currentTerminal();if(!terminal||!terminal.connected||terminal.protocol!=='Telnet'){resetGrid();return;} + if(stage===3){ + if(!stableGrid(peer,terminal))return; + checkGeometry(0,peer,terminal);write('fixture-ready',JSON.stringify({grid:[peer.columns,peer.rows],event_count:peer.naws_history.length}));stage=4;return; + } + if(requested>committed&&peer.resize_armed===requested){ + if(resized!==requested){resize(main,...windows[requested]);resized=requested;resetGrid();return;} + if(!stableGrid(peer,terminal))return; + checkGeometry(requested,peer,terminal); + write('resize-ready-'+requested,JSON.stringify({phase:requested,grid:[peer.columns,peer.rows],event_count:peer.naws_history.length}));committed=requested;return; + } + if(peer.phase===null||observations.length>peer.phase)return; + if(peer.case_id!==caseId)throw Error('peer case differs'); + if(peer.phase_label!==contract.labels[peer.phase])throw Error('peer phase label differs'); + if(peer.phase!==lastPhase){lastPhase=peer.phase;stable=null;stableSince=Date.now();} + const record=peer.records[peer.phase];if(!record.cursor_reply)return; + if(terminal.capacity!==record.frozen_grid[1]||JSON.stringify(record.frozen_grid)!==JSON.stringify(grids[peer.phase]))throw Error('phase grid differs'); + if(geometryChecks.length===peer.phase)checkGeometry(peer.phase,peer,terminal); + const geometry=windowGeometry(),state=JSON.stringify([terminal,geometry]); + if(JSON.stringify(geometry.window_size)!==JSON.stringify(windows[peer.phase]))throw Error('phase window dimensions differ'); + if(state!==stable){stable=state;stableSince=Date.now();return;} + if(Date.now()-stableSince<400)return; + if(peer.phase===0){ + if(JSON.stringify(record.cursor_reply)!==JSON.stringify([19,7]))throw Error('initial fixture DSR differs'); + if(!Array.isArray(terminal.lines))throw Error('initial fixture text missing'); + for(const label of contract.marker_labels){ + const count=terminal.lines.reduce((total,line)=>total+line.split(label).length-1,0); + if(count!==1)throw Error('initial fixture sentinel missing or repeated '+label); + } + } + if(observations.length!==peer.phase||observations.length>=phaseCount)throw Error('resize-dual-axis phase order'); + const observation={phase:peer.phase,label:peer.phase_label,terminal,...geometry,cursor_reply:record.cursor_reply, + cursor_reply_private:record.cursor_reply_private,response_hex:record.response_hex,frozen_grid:record.frozen_grid}; + observations.push(observation);send({kind:'resize_dual_axis_phase',...observation}); + write('ack-'+peer.phase,'observed'); + if(resizePhases.includes(peer.phase+1))requestResize(peer.phase+1); + } + due=Date.now()+75; + }catch(error){finish('failed',String(error));}finally{busy=false;} +}}); +setTimeout(()=>finish('failed','bounded observation timer'),42000); diff --git a/research/ui/resize_dual_axis_reference.md b/research/ui/resize_dual_axis_reference.md new file mode 100644 index 00000000..d70ad782 --- /dev/null +++ b/research/ui/resize_dual_axis_reference.md @@ -0,0 +1,36 @@ +# WindTerm 2.7.0:一次窗口动作同时缩宽缩高后的正文、视口与新写入 + +本工具补齐 #27/#29 的一个独立原版缺口:既有[只缩宽 CUP/DSR](results/2026-11-03-reflow-addressing-reference/README.md)、[顺序宽高组合](results/2026-11-02-resize-combined-reference/README.md)与[隐藏帧七阶段](../../experiments/shell/results/2026-11-04-qt-hidden-retention/README.md)都不足以决定“同一窗口动作同时改变宽高”后正文/视口/新 CUP 写入位置。本工具用新的 namespace、源码归档与单次计划做一次固定三阶段直接 Telnet 观察,不修改旧探针或旧结果。 + +## 固定三阶段合同 + +唯一 case:`dual-axis-retention`,一个独立新进程,最多一次原版运行。 + +| 阶段 | 请求/实际窗口,px | 待核 NAWS | 终端输入 | +| --- | --- | --- | --- | +| initial-body | 1360×749 | 91×20 | 固定 75 B 正文,再 DSR | +| dual-shrunk-with-write | 1000×569 | 61×14 | 先一次窗口动作同时缩宽缩高;稳定后 `CUP(7;2)` + `DQ` + DSR | +| dual-restored-unmodified | 1360×749 | 91×20 | 一次窗口动作同时恢复;仅 DSR | + +初始源与既有组合样本相同(75 B,SHA256 `ff48cd41220a72e5da2350dc97c3c8c8c884d48cc4f7fa2328609bf8bc0cb09e`):`HTOP`、CR/LF 分开的 `HARD_A`/`HARD_B`、跨第 91 列边界的 `uvw`、把协议光标放到 `19;7` 的 `LOWEND`。初始阶段稳定后必须有七个 marker 各一次和 DSR `19;7`,否则停止。 + +缩小阶段只在稳定后发送一次固定的 `ESC[7;2HDQ` 与 DSR;恢复阶段只发 DSR。没有其他 CUP、CR/LF、清屏、重绘或文本。`CUP(7;2)` 是冻结前选定的区分位置:它可能落在与保留源行不同的视口行;实际落点只记录、不预判。 + +## 观测与停止 + +- public Qt 窗口 QRect 使用 inclusive LTRB;协议 DSR、NAWS 事件、capacity、API 文档行/位置、ASCII text offset、marker 像素矩形分别记录。请求窗口与实际窗口分开;一次窗口请求不假定只产生一个 NAWS 事件,事件数量与顺序全部保存。 +- 每个稳定端点先保存实际窗口、NAWS、capacity 与事件数,再断言固定预测。窗口或 NAWS 与请求不符、未 armed 的 NAWS、越界事件、陈旧快照、阶段/ack 缺失、初始 fixture 不符或固定期限到达都停止;不校准、不重试、不延长期限。 +- 文档只允许固定源派生的 ASCII 文本:初始 19 行,之后除一行被固定 `DQ` 写入替换外仍为源派生行;写后唯一行、文本 offset、marker 保留/丢失均作为观察,不当作语义通过。 +- 严格结果固定 `status=observation`、`semantic_acceptance=false`;不预判原版内部是否保留完整正文、如何滚动/重排或 CUP 应落到哪一行。 + +## 身份与离线验证 + +新执行源码固定为 10 个文件;新出口固定 3 个源文件。manifest、源码归档、计划、原版包/模块、合成 profile、station/Job 与单次 `.attempted` 守卫沿用既有机制。新的公开结果只包含固定合成输入/文本、DSR、NAWS/几何标量、写后行与哈希。 + +```powershell +python -B -X utf8 -m unittest research.ui.test_resize_dual_axis_reference research.ui.test_export_resize_dual_axis_reference -v +python -B -O -X utf8 -m unittest research.ui.test_resize_dual_axis_reference research.ui.test_export_resize_dual_axis_reference -v +node research/ui/test_resize_dual_axis_stages.js research/ui/resize_dual_axis_reference.js +``` + +离线测试不启动原版、socket、GUI 或 native 构建;离线通过不算原版运行或核心实现验收。本样本不覆盖备用帧、Unicode、其他尺寸/顺序、连续 resize、历史淘汰、ConPTY、呈现或性能。 diff --git a/research/ui/resize_dual_axis_reference.py b/research/ui/resize_dual_axis_reference.py new file mode 100644 index 00000000..b8f8ef90 --- /dev/null +++ b/research/ui/resize_dual_axis_reference.py @@ -0,0 +1,419 @@ +"""Independent fixed width/height observations; unknown semantics remain observations.""" +from __future__ import annotations +import argparse +import hashlib +import json +from functools import lru_cache +from pathlib import Path +import sys + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +from research.ui import mode_state_reference as common +from research.ui import resize_state_reference as baseline + +require, sha = common.require, common.sha +qrect_size = baseline.qrect_size +EXE_SHA256, MANIFEST_SHA256 = common.EXE_SHA256, common.MANIFEST_SHA256 +RESET = b"\x1b[?6l\x1b[?7h\x1b[r\x1b[0m\x1b[2J" +BODY = (RESET + b"\x1b[2;1HHTOP\x1b[6;1HHARD_A\r\nHARD_B" + b"\x1b[12;90Huvw\x1b[19;1HLOWEND") +MARKERS = ("HTOP", "HARD_A", "HARD_B", "u", "v", "w", "LOWEND") +# One fixed probe write after the dual-axis shrink. CUP row 7/column 2 may target +# a viewport row distinct from the retained source row; the unique ASCII pair is +# the only post-body write in this case. +WRITE = b"\x1b[7;2HDQ" +CASES = { + "dual-axis-retention": (("initial-body", 91, 20, BODY), + ("dual-shrunk-with-write", 61, 14, WRITE), + ("dual-restored-unmodified", 91, 20, b"")), +} +RESULT_FILE = "resize-dual-axis-result.json" +ATTEMPT = "One original resize-dual-axis launch attempt; preserve failures and never retry this plan.\n" +SOURCE_FILES = ("research/ui/resize_dual_axis_reference.py", "research/ui/resize_dual_axis_reference.js", + "research/ui/resize_state_reference.py", "research/ui/mode_state_reference.py", + "research/ui/alternate_reference.py", "research/ui/r1_launch.py", "research/ui/launch.py", + "research/ui/qt_r1_text.js", "research/ui/r1_vt_fixture.py", "experiments/conpty/run.py") + + +def validate_case(case): + require(type(case) is str and case in CASES, "Unknown fixed resize-dual-axis case") + return case + + +def windows(case): + validate_case(case) + return [(1360 if columns == 91 else 1000, 749 if rows == 20 else 569) + for _, columns, rows, _ in CASES[case]] + + +def required_grids(case): + validate_case(case) + return [(columns, rows) for _, columns, rows, _ in CASES[case]] + + +def resize_phases(case): + values = required_grids(case) + return tuple(index for index in range(1, len(values)) if values[index] != values[index-1]) + + +def controller_contract(case): + validate_case(case) + return {"case_id": case, "labels": [label for label, _, _, _ in CASES[case]], + "windows": [list(v) for v in windows(case)], "grids": [list(v) for v in required_grids(case)], + "resize_phases": list(resize_phases(case)), "marker_labels": list(MARKERS), + "initial_dsr": [19, 7]} + + +def phases(case, rows=20, columns=91): + validate_case(case) + require(type(rows) is int and type(columns) is int and [columns, rows] == [91, 20], "Fixed initial grid differs") + return [(label, value) for label, _, _, value in CASES[case]] + + +def reply_observation(data): + result = common.reply_observation(data) + if result is not None: + row, column = result["cursor_reply"] + require(1 <= row <= 200 and 1 <= column <= 500, "DSR exceeds independent safety bounds") + return result + + +def phase_records(case, rows=20, columns=91): + return [{"phase": index, "label": label, "source_hex": value.hex(), "source_bytes": len(value), + "source_sha256": hashlib.sha256(value).hexdigest(), "dsr_suffix_hex": "1b5b366e"} + for index, (label, value) in enumerate(phases(case, rows, columns))] + + +def allowed_grid(before, after, value): + """A dual-axis request may arrive as one or several NAWS events. + + Each observed grid must stay on the monotonic interval between the frozen + grid and the requested grid on every axis that changes, and keep the other + axis equal. The multiplicity and order are observations, not atomicity + assumptions. + """ + if not all(type(v) in (list, tuple) and len(v) == 2 and + all(type(n) is int and 1 <= n <= 65535 for n in v) for v in (before, after, value)): + return False + changed = [axis for axis in (0, 1) if before[axis] != after[axis]] + return bool(changed) and all( + min(before[axis], after[axis]) <= value[axis] <= max(before[axis], after[axis]) + if axis in changed else value[axis] == before[axis] for axis in (0, 1)) + + +def peer_class(case): + validate_case(case) + base = baseline.peer_class() + # baseline creates a fresh module namespace for each class, not a shared peer. + base._serve.__globals__["phases"] = lambda rows, columns: phases(case, rows, columns) + base._serve.__globals__["reply_observation"] = reply_observation + + class DualAxisPeer(base): + def _freeze_initial(self): + super()._freeze_initial() + require(self.state["frozen_grid"] == [91, 20], "Fixed initial grid differs") + self.state["case_id"] = case + + def _on_naws(self): + grid = [self.state["columns"], self.state["rows"]] + history = self.state.setdefault("naws_history", []) + require(len(history) < 64 and all(type(n) is int and 0 <= n <= 65535 for n in grid), "NAWS event bound") + history.append(grid) + self._record() + if self.state.get("frozen_grid"): + target = self.state.get("resize_armed") + if target is None: + require(grid == self.state["frozen_grid"], "NAWS changed outside an armed resize") + else: + require(target in resize_phases(case) and + allowed_grid(self.state["frozen_grid"], required_grids(case)[target], grid), + "NAWS changed an unarmed axis or exceeded the fixed interval") + + def _phase_ready(self, index): + if index == -1: + return True + ack = self.directory / f"ack-{index}" + if not ack.exists() or self.state["records"][index]["cursor_reply"] is None: + return False + require(ack.read_text(encoding="utf-8") == "observed", "UI acknowledgement differs") + if index == 0: + require(self.state["records"][0]["cursor_reply"] == [19, 7], "Initial fixture DSR differs") + if index == len(phases(case))-1: + return True + target = index + 1 + if target not in resize_phases(case): + require(self.state["resize_armed"] is None and self.state["frozen_grid"] == list(required_grids(case)[target]), + "Action was not released from its fixed per-stage grid") + self.state["frozen_grids"].append(list(self.state["frozen_grid"])) + self.state["freeze_event_counts"].append(len(self.state["naws_history"])) + return True + request = self.directory / f"resize-request-{target}" + if not request.exists(): + return False + require(json.loads(request.read_text(encoding="utf-8")) == {"phase": target, "window": list(windows(case)[target])}, + "Resize request differs from immutable window sequence") + if self.state["resize_armed"] is None: + self.state["resize_armed"] = target + self.state["resize_boundaries"].append({"phase": target, "before_event_count": len(self.state["naws_history"]), + "requested_window": list(windows(case)[target])}) + self._record() + return False + require(self.state["resize_armed"] == target, "Resize phase differs") + ready = self.directory / f"resize-ready-{target}" + if not ready.exists(): + return False + grid, history = [self.state["columns"], self.state["rows"]], self.state["naws_history"] + require(json.loads(ready.read_text(encoding="utf-8")) == + {"phase": target, "grid": grid, "event_count": len(history)}, "Resize ready snapshot is stale") + boundary = self.state["resize_boundaries"][-1] + require(len(history) > boundary["before_event_count"] and history[-1] == grid == list(required_grids(case)[target]), + "Fixed resized grid or NAWS boundary differs") + boundary["after_event_count"] = len(history) + self.state.update(frozen_grid=grid, resize_armed=None) + self.state["frozen_grids"].append(grid) + self.state["freeze_event_counts"].append(len(history)) + return True + return DualAxisPeer + + +def source_identity(): + return common.source_identity(root=ROOT, source_files=SOURCE_FILES) + + +def probe_fragment(case): + validate_case(case) + source = (ROOT / "research/ui/resize_dual_axis_reference.js").read_text(encoding="utf-8") + require(source.count("__RESIZE_DUAL_AXIS_CASE_JSON__") == 1, "Protected case template changed") + return source.replace("__RESIZE_DUAL_AXIS_CASE_JSON__", json.dumps(controller_contract(case))) + + +def launcher_source(case): + validate_case(case) + source = (ROOT / "research/ui/r1_launch.py").read_text(encoding="utf-8") + replacements = { + "from research.ui.r1_vt_fixture import Peer": + "from research.ui.resize_dual_axis_reference import peer_class, probe_fragment\n Peer = peer_class("+repr(case)+")", + 'Path(__file__).with_name("r1_menu_wrap.js").read_text(encoding="utf-8")': "probe_fragment("+repr(case)+")", + } + for old, new in replacements.items(): + require(source.count(old) == 1, "Protected launcher source changed") + source = source.replace(old, new) + return source + + +def fixed_contract(case): + validate_case(case) + return {"schema": "termforge-resize-dual-axis-plan-v1", "issue": 29, "related_issue": 27, "case_id": case, + "maximum_original_runs": 1, "actual_original_runs": 0, "phase_count": len(phases(case)), + "windows": [list(v) for v in windows(case)], "required_grids": [list(v) for v in required_grids(case)], + "resize_phases": list(resize_phases(case)), "window_rect_format": "qt-qrect-inclusive-ltrb", + "rea_capture_seconds": 45, "cli_wait_timeout_seconds": 60, "whole_lifecycle_bound_seconds": None, + "phases": phase_records(case), "reference_exe_sha256": EXE_SHA256, + "reference_files_manifest_sha256": MANIFEST_SHA256, "semantic_acceptance": False, + "controller_contract": controller_contract(case), "execution_group": "one-dual-axis-resize-case", + "independent_dsr_bounds": {"row": [1, 200], "column": [1, 500]}, + "initial_fixture_dsr": [19, 7], "initial_fixture_markers": list(MARKERS), + "geometry_basis": { + "geometry": {"reference": "research/ui/results/2026-11-02-resize-geometry-reference/strict-export.json", + "strict_export_sha256": "f18a2320c073ed477f773b42076c9968c0c8af8530af19548d5695e66e8cf9b5", + "execution_source_base_commit": "3d4c8c2d6222caf0fc024729d9e0b34c020a69cf"}, + "sequential_width_height": {"reference": "research/ui/results/2026-11-02-resize-combined-reference/strict-export.json", + "strict_export_sha256": "6a1561f3e6f6bc480c6a2f086adf7ed4798d5007b04a7bc71c7ae9deba90cfe6", + "execution_source_base_commit": "2c7c5929e11d911ad69a7e2b7c420120591862ed"}, + "dual_axis_grid_previously_observed": False}, + "dual_axis_geometry_status": "61x14 at 1000x569 in one window request is a fixed prediction checked by saved actual geometry before any semantic assertion; intermediate NAWS event count/order remains an observation", + "post_initial_terminal_input": "After the dual-axis shrink: one fixed CUP(7;2)+\"DQ\" write and DSR. After the dual restore: DSR only. No redraw, CR/LF or other text.", + "scope": "Fresh direct Telnet process; one window action changes both axes. Document/API positions, DSR, scroll and pixel rectangles remain separate observations.", + "write_probe": {"phase": 1, "cup": [7, 2], "text": "DQ", "source_hex": WRITE.hex()}, + "stop": "Wrong fixed geometry/grid, unarmed NAWS, stale snapshots, initial fixture/source/reply/phase mismatch, bounds or deadline failure; no retries"} + + +def bounded_fragments(lines, split): + result = set(lines) + for line in lines: + result.add(line.rstrip(" ")) + for width in ((61, 91) if split else (91,)): + if len(line) <= width: + result.add(line.ljust(width)) + for edge in ((61, 91) if split else (91,)): + if len(line) > edge: + result.update((line[:edge], line[edge:], line[:edge].rstrip(" "), line[edge:].rstrip(" "))) + return result + + +@lru_cache(maxsize=16) +def synthetic_lines(case, phase): + """Finite ASCII source-derived words; loss is observation, not acceptance.""" + validate_case(case) + require(type(phase) is int and 0 <= phase < len(phases(case)), "Phase bound") + base = {"", "HTOP", "HARD_A", "HARD_B", "LOWEND", " "*89+"uvw"} + result = bounded_fragments(base, phase > 0) + if phase >= 1: + # The fixed write replaces exactly two cells at a viewport column; the + # document line is source-derived up to that position, so enumerate the + # bounded set instead of accepting arbitrary edited text. + for line in base: + for column in range(0, 61): + if len(line) >= column+2: + modified = line[:column] + "DQ" + line[column+2:] + else: + modified = line.ljust(column) + "DQ" + result.update((modified, modified.rstrip(" "), modified.ljust(61), modified.ljust(91))) + return frozenset(result) + + +def base_synthetic_lines(case, phase): + validate_case(case) + base = {"", "HTOP", "HARD_A", "HARD_B", "LOWEND", " "*89+"uvw"} + return frozenset(bounded_fragments(base, phase > 0)) + + +def validate_document(case, phase, terminal): + require(type(terminal.get("line_count")) is int and 0 <= terminal["line_count"] <= 200 and + type(terminal.get("lines")) is list and len(terminal["lines"]) == terminal["line_count"], "Document bound") + lines = terminal["lines"] + require(all(type(line) is str and line.isascii() and line in synthetic_lines(case, phase) for line in lines), + "Only fixed dual-axis source-derived synthetic document lines are allowed") + counts = {marker: sum(line.count(marker) for line in lines) for marker in MARKERS} + require(all(count <= 1 for count in counts.values()), "Repeated dual-axis-source marker") + if phase == 0: + require(all(count == 1 for count in counts.values()), "Initial fixture sentinel missing") + else: + written = [line for line in lines if line not in base_synthetic_lines(case, phase)] + require(len(written) == 1 and "DQ" in written[0], "Fixed write marker missing or repeated") + return written[0] if phase >= 1 else None + + +def audit_mechanics(directory, case): + validate_case(case) + count = len(phases(case)) + expected_grids, expected_windows = [list(v) for v in required_grids(case)], [list(v) for v in windows(case)] + read = lambda name: json.loads((directory / name).read_text(encoding="utf-8")) + require((directory / "stderr.txt").read_bytes() == b"" and not (directory / "launcher-failure.json").exists(), + "REA stderr/launcher failure retained") + peer, result, cleanup, profile = [read(name) for name in ("peer.json", RESULT_FILE, "cleanup.json", "profile-check.json")] + require(all(peer.get(k) is True for k in ("connected", "closed", "complete")) and not peer.get("error") and peer.get("case_id") == case, + "Dual-axis peer did not complete cleanly") + require(result.get("status") == "observed-fixed-resize-dual-axis-states" and result.get("case_id") == case and result.get("error") is None and + result.get("window_rect_format") == "qt-qrect-inclusive-ltrb" and result.get("presented_done") is None and + result.get("full_active_grid_verified") is False and result.get("local_shell_transport_verified") is False and + "failure_geometry" not in result, + "Dual-axis UI observation incomplete") + require(type(cleanup.get("active")) is int and cleanup["active"] == 0 and + all(type(cleanup.get(k)) is int and cleanup[k] >= 0 for k in ("total", "terminated")) and + profile == {"only_synthetic_session": True, "session_count": 1}, "Owned cleanup/profile differs") + grids, freezes, history = peer["frozen_grids"], peer["freeze_event_counts"], peer["naws_history"] + require(grids == expected_grids and len(freezes) == count and type(history) is list and 1 <= len(history) <= 64 and + all(type(grid) is list and len(grid) == 2 and all(type(n) is int and 0 <= n <= 65535 for n in grid) for grid in history) and + all(type(n) is int and 1 <= n <= len(history) for n in freezes), "Fixed grids/freeze/NAWS bounds differ") + require(len(peer["resize_boundaries"]) == len(resize_phases(case)) and freezes == sorted(freezes), "Resize boundary count differs") + for index, boundary in zip(resize_phases(case), peer["resize_boundaries"]): + before, after = boundary["before_event_count"], boundary["after_event_count"] + require(type(before) is int and type(after) is int and boundary["phase"] == index and + boundary["requested_window"] == expected_windows[index] and freezes[index-1] <= before < after == freezes[index] <= len(history) and + all(grid == grids[index-1] for grid in history[freezes[index-1]-1:before]) and + all(allowed_grid(grids[index-1], grids[index], grid) for grid in history[before:after]) and history[after-1] == grids[index], "NAWS resize boundary differs") + for index in range(1, count): + if index not in resize_phases(case): + require(grids[index] == grids[index-1] and + all(grid == grids[index] for grid in history[freezes[index-1]-1:freezes[index]]), + "Unarmed per-stage NAWS change retained") + require(all(grid == grids[-1] for grid in history[freezes[-1]-1:]), "Unarmed final NAWS change retained") + records, observations, checks = peer["records"], result["observations"], result["geometry_checks"] + require(len(records) == len(observations) == len(checks) == count and peer["phase"] == count-1 and peer["phase_count"] == count, + "Fixed dual-axis phases incomplete") + replies = bytearray() + write_probes = [] + for index, (expected, actual, observed, check) in enumerate(zip(phase_records(case), records, observations, checks)): + grid, window = expected_grids[index], expected_windows[index] + require(all(actual.get(k) == v for k, v in expected.items()) and actual["frozen_grid"] == grid, "Exact action source differs") + raw = bytes.fromhex(actual["response_hex"]); reply = reply_observation(raw) + if index == 0: + require(reply is not None and reply["cursor_reply"] == [19, 7], "Initial fixture DSR differs") + require(reply is not None and all(actual.get(k) == v for k, v in reply.items()), "DSR differs") + require(all(observed.get(k) == actual[k] for k in ("phase", "label", "frozen_grid", *reply)), "Peer/UI phase differs") + require(check.get("phase") == index and check.get("expected_size") == window and check.get("window_size") == window and + baseline.qrect_size(check.get("window_rect")) == window and observed.get("window_size") == window and + baseline.qrect_size(observed.get("window_rect")) == window, "Exact geometry differs") + terminal = observed["terminal"] + require(check.get("observed_grid") == grid and type(check.get("terminal_capacity")) is int and + check["terminal_capacity"] == grid[1] and type(check.get("event_count")) is int and + check["event_count"] == freezes[index], "Saved actual geometry differs") + require(terminal.get("connected") is True and terminal.get("protocol") == "Telnet" and + type(terminal.get("capacity")) is int and terminal["capacity"] == grid[1], "UI binding differs") + written = validate_document(case, index, terminal) + if index >= 1: + line_index = terminal["lines"].index(written) + write_probes.append({"phase": index, "document_line": line_index, + "line_text_offset": written.index("DQ"), "line": written}) + require(type(terminal.get("markers")) is list and len(terminal["markers"]) <= 256, "Marker bound") + require((directory / f"ack-{index}").read_text(encoding="utf-8") == "observed", "Phase acknowledgment missing") + replies.extend(raw) + require(peer["received_hex"] == replies.hex(), "Unassigned reply bytes retained") + return {"schema": "termforge-resize-dual-axis-mechanics-v1", "case_id": case, "stage_count": count, "dsr_count": count, + "stateful_observations_preserved": True, "frozen_grids": grids, "naws_events": len(history), "job_active": 0, + "write_probes": write_probes, "write_probe_definition": fixed_contract(case)["write_probe"], + "semantic_acceptance": False, "presented_done": None} + + +def prepare_plan(source, out, rea, destination, case, execution_authorized=False): + validate_case(case) + source, out, rea, destination = [Path(path).resolve() for path in (source, out, rea, destination)] + require(type(execution_authorized) is bool, "Execution authorization must be boolean") + require(not out.exists() and not destination.exists(), "Plan/output must be new") + require(out != ROOT / "local" and out.is_relative_to(ROOT / "local") and destination.is_relative_to(ROOT / "local"), + "Plan/output must belong to this worktree local directory") + require(not out.is_relative_to(source) and not source.is_relative_to(out), "Source/output overlap") + require(sha(source / "WindTerm.exe") == EXE_SHA256, "Pinned original executable differs") + plan = {**fixed_contract(case), **source_identity(), **common.host_identity(), "execution_authorized": execution_authorized, + "source": str(source), "out": str(out), "rea": str(rea), "rea_prepared_identity": common.rea_identity(rea), + "launcher_source_sha256": hashlib.sha256(launcher_source(case).encode()).hexdigest()} + destination.parent.mkdir(parents=True, exist_ok=True) + with destination.open("x", encoding="utf-8", newline="\n") as stream: + stream.write(json.dumps(plan, indent=2)+"\n") + return plan + + +def run_plan(path): + path = Path(path).resolve(); plan = json.loads(path.read_text(encoding="utf-8")) + require(plan.get("execution_authorized") is True, "Offline proposal only; execution is not authorized") + case = validate_case(plan.get("case_id")) + require(all(type(plan.get(k)) is type(v) and plan[k] == v for k, v in fixed_contract(case).items()), "Fixed dual-axis contract differs") + require(all(plan.get(k) == v for k, v in source_identity().items()), "Dual-axis source identity changed") + require(all(plan.get(k) == v for k, v in common.host_identity().items()), "Execution host/runtime differs") + require(plan["launcher_source_sha256"] == hashlib.sha256(launcher_source(case).encode()).hexdigest(), "Launcher source differs") + source, out, rea = [Path(plan[k]).resolve() for k in ("source", "out", "rea")] + require(path.is_relative_to(ROOT / "local") and out != ROOT / "local" and out.is_relative_to(ROOT / "local") and + not out.exists(), "Dual-axis run plan/output is not new/local") + require(not out.is_relative_to(source) and not source.is_relative_to(out), "Source/output overlap") + require(plan["rea_prepared_identity"] == common.rea_identity(rea), "Prepared REA entry changed") + require(sha(source / "WindTerm.exe") == EXE_SHA256, "Pinned original executable differs") + with path.with_suffix(".attempted").open("x", encoding="utf-8") as marker: + marker.write(ATTEMPT) + namespace = {"__file__": str(ROOT / "research/ui/r1_launch.py"), "__name__": "termforge_resize_dual_axis_launcher"} + exec(compile(launcher_source(case), namespace["__file__"], "exec"), namespace) + namespace["cli_command"] = lambda _: [plan["rea_prepared_identity"]["node_executable"], str(rea.parent.parent / "rea-agents/scripts/rea.mjs")] + namespace["run"](source, out, rea, 45, False, "menu-wrap") + require(all(plan.get(k) == v for k, v in source_identity().items()), "Dual-axis source identity changed during run") + mechanics = audit_mechanics(out, case) + (out / "mechanics-check.json").write_text(json.dumps(mechanics, indent=2)+"\n", encoding="utf-8", newline="\n") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--plan", type=Path, required=True) + parser.add_argument("--prepare", action="store_true") + parser.add_argument("--case", choices=list(CASES)) + parser.add_argument("--authorize-execution", action="store_true") + for name in ("source", "out", "rea"): + parser.add_argument("--"+name, type=Path) + args = parser.parse_args() + if args.prepare: + require(args.source and args.out and args.rea and args.case, "Source/output/REA/case required") + prepare_plan(args.source, args.out, args.rea, args.plan, args.case, args.authorize_execution) + print(json.dumps({"prepared": True, "case_id": args.case, "phase_count": len(phases(args.case)), "execution_authorized": args.authorize_execution})) + else: + require(not args.authorize_execution and args.case is None and args.source is None and args.out is None and args.rea is None, + "Execution uses only the immutable plan") + run_plan(args.plan) diff --git a/research/ui/results/2026-11-04-resize-dual-axis-reference/README.md b/research/ui/results/2026-11-04-resize-dual-axis-reference/README.md new file mode 100644 index 00000000..549144dd --- /dev/null +++ b/research/ui/results/2026-11-04-resize-dual-axis-reference/README.md @@ -0,0 +1,60 @@ +# WindTerm 2.7.0:同时缩宽缩高并写入后的视口/行/DSR + +[双轴探针](../../resize_dual_axis_reference.md)在受控取证环境完成一次固定三阶段原版运行(`91×20 → 61×14` 同一窗口动作 → `91×20`)。本记录补充 #27/#29 此前缺失的“同时改变宽高”原版端点;**阶段 1 是缩放之后又执行固定 `CUP(7;2)+DQ` 的观测**,没有“缩放后、写入前”的原版正文端点。旧只缩宽、顺序宽高组合与隐藏帧记录均不改写。 + +公开件:[strict-export.json](strict-export.json),SHA256 `95ad9c5bd8d73e278e794fa327e2fd902739c3da9bd4be6add4d560a00a7d18f`;单次计划 SHA256 `3beabf9a44537d43a400209786f4aeccc29a255307a74f4a4c13cf0ac812d55d`,`.attempted` 单次标记见 [plan.attempted.txt](plan.attempted.txt)(SHA256 `ed761217af8e88440bea5afe49a649b79889f25dabd35db03f616895e8f83a34`)。参考 EXE `7b10531d…`、4047 文件清单 `e459adf2…`、源码归档清单 `96d3783a…` 由严格出口核验。 + +首次严格导出因冻结白名单未接受本轮实际出现的“当前列裁剪 + 固定写入”文档行而拒绝(`run_plan` 在 `audit_mechanics` 停止,`mechanics-check.json` 未写出);失败保留,未重跑原版。修正仅限严格的离线行白名单/导出器,随后在同一原始录制、同一计划与同一 `.attempted` 标记上重新生成公开件。 + +## 身份分层 + +| 身份 | 内容 | +|---|---| +| 冻结原版运行源 | 提交 `dee3c78cfda60a99211dcf8f217be6e361172d5f`;10 个执行源哈希见 [validation.json](validation.json) 的 `frozen_original_run`,与严格出口 `execution_source_files_sha256` 一致;计划/attempted 见上 | +| 离线严格出口 | 提交 `2fc3f25cb1887b569c583068a49e874fa4bd9156` 的导出器/白名单;严格出口的 `offline_export_source_files_sha256` 已核对,未把这些后加文件倒填为原运行源 | +| 核心回放/本地测试源 | 同一提交 `2fc3f25` 的 `resize_dual_axis_test.cpp` 与 CMake 注册;不是原版运行源 | +| 文档 | 早期报告提交 `1e41fc7…` 与本次修正提交(见 validation 的 `documentation`) | + +## 三阶段实测 + +| 阶段 | 实际窗口 | NAWS | 事件数 | DSR | scroll | 文档行数 | 文档行 11(零基) | +|---|---|---|---|---|---|---|---| +| 0 initial-body | 1360×749 | 91×20 | 1 | `19;7` | 0 | 19 | `89 空格 + uvw`(92) | +| 1 dual-shrunk-with-write | 1000×569 | 61×14 | 2 | **`7;4`** | **5** | 19 | **` DQ` + 58 空格 + `w`(62)** | +| 2 dual-restored-unmodified | 1360×749 | 91×20 | 3 | **`7;4`** | **5** | 18 | 同上(62) | + +- 阶段 0→1 是**一次窗口动作同时缩宽缩高**;本次只新增一个 NAWS 事件(1→2),不能据此宣称所有情况都原子合并,事件数/顺序按实测保存。 +- 阶段 1 在缩放稳定后又执行了固定 `CUP(7;2)+DQ`:`scroll=5`、DSR `7;4`,文档行 11 为 62 字符(` DQ`+58 空格+`w`)。**无法从本录制判断裁剪/物化发生在 resize、CUP 还是写入的哪一时点**;也没有缩放后、写入前的原版正文观测。 +- 恢复 `91×20` 后,已写入行仍为 62 字符、`scroll`/DSR 仍为 `5`/`7;4`,没有回拉;这是写入后的恢复端点,不单独证明 resize 本身的行为。行 11 的 `w` 矩形从阶段 1 的 `(147,126)` 变为阶段 2 的 `(635,108)`。 + +以上是固定单样本的直接 Telnet UI 观察。`presented_done=null`、`semantic_acceptance=false`,不推断内部 history 条数、完整活动网格、样式或所有尺寸/顺序行为,也不推断裁剪发生的时点。 + +## 核心有界工程回放 + +[`resize_dual_axis_test`](../../../../experiments/core/resize_dual_axis_test.cpp)(CTest `resize_dual_axis_core`)按固定源与协议输入回放:初始 91×20、一次 `resize_preserving(14,61)`、`CUP(7;2)+DQ+DSR`、一次 `resize_preserving(20,91)` 与最终 DSR。11 项检查通过: + +- 初始文档 18 个逻辑行,第 11 行为 92 字符;双轴事务后、固定写入前保留行仍为完整 92 字符。**这是 TermForge 的工程保留不变量,不是原版 pre-write 观测**。 +- 固定写后该编辑行按当前 61 列足迹物化为 ` DQ` + 58 空格 + `w`(62 字符),与记录的原版**写后**行逐字节一致;DSR 均为 `7;4`(零基 row6/column3)。 +- 恢复 91×20 后书写行仍为 62 字符、DSR 仍为 `7;4`,与记录的写后端点相同。 + +这是该固定样本写后端点的工程回放,不是原版 scroll/history 布局、pre-write 裁剪时点或一般 reflow oracle;没有修改生产实现。 + +## 与既有记录的关系 + +- [顺序宽高组合](../2026-11-02-resize-combined-reference/README.md)没有同样的后续写入干预,而本样本正文端点是“缩放+固定写入后”;两者不能作为裁剪归因的对照,不能据此裁决双轴 resize 与顺序 resize 的裁剪差异,也不证明宽高交换律。 +- [原版隐藏帧返回裁剪](../2026-11-02-resize-hidden-height-reference/README.md)记录的是备用屏返回主屏时裁剪隐藏行;本记录是主屏双轴变化并写入后的视口/写入端点,样本与路径不同。 +- TermForge 的确认方向是保留完整正文(ADR-0010);本记录只作为**原版写后端点观察**,不把产品保留行为写成与原版一致,也不把裁剪观察回填为产品合同。 +- 旧六阶段 B/CD native-sync 失败仍不能由本记录解释:这里是 WindTerm Qt UI 的协议/文档/视口观察,不是 ConPTY 原生网格;同时缩宽+缩高后原生网格如何重排、B/C/D 行如何对应仍缺独立证据。旧断言不删除、不放宽。 + +## 复验 + +```powershell +python -B -X utf8 -m unittest research.ui.test_resize_dual_axis_reference research.ui.test_export_resize_dual_axis_reference -v +node research/ui/test_resize_dual_axis_stages.js research/ui/resize_dual_axis_reference.js +``` + +离线命令只校验冻结源、peer/controller 与严格出口守卫,不启动原版;本记录不重跑原版。 + +## 边界 + +单次直接 Telnet、固定 75 B ASCII、主屏、无备用屏/Unicode/连续输出/其他尺寸;不覆盖真实选择/Find、完整网格/样式、ConPTY 传输、物理呈现或性能。**缩放、CUP 与写入对 62 字符行的责任划分未知,原版 pre-write 正文保持未知。** #27/#29/#30/#7 仍 OPEN。 diff --git a/research/ui/results/2026-11-04-resize-dual-axis-reference/plan.attempted.txt b/research/ui/results/2026-11-04-resize-dual-axis-reference/plan.attempted.txt new file mode 100644 index 00000000..0862b5eb --- /dev/null +++ b/research/ui/results/2026-11-04-resize-dual-axis-reference/plan.attempted.txt @@ -0,0 +1 @@ +One original resize-dual-axis launch attempt; preserve failures and never retry this plan. diff --git a/research/ui/results/2026-11-04-resize-dual-axis-reference/strict-export.json b/research/ui/results/2026-11-04-resize-dual-axis-reference/strict-export.json new file mode 100644 index 00000000..11fb9cbb --- /dev/null +++ b/research/ui/results/2026-11-04-resize-dual-axis-reference/strict-export.json @@ -0,0 +1,1169 @@ +{ + "schema": "termforge-original-resize-dual-axis-reference-v1", + "issue": 29, + "related_issue": 27, + "case_id": "dual-axis-retention", + "authorized_plan_sha256": "3beabf9a44537d43a400209786f4aeccc29a255307a74f4a4c13cf0ac812d55d", + "geometry_basis": { + "dual_axis_grid_previously_observed": false, + "geometry": { + "reference": "research/ui/results/2026-11-02-resize-geometry-reference/strict-export.json", + "strict_export_sha256": "f18a2320c073ed477f773b42076c9968c0c8af8530af19548d5695e66e8cf9b5", + "execution_source_base_commit": "3d4c8c2d6222caf0fc024729d9e0b34c020a69cf" + }, + "sequential_width_height": { + "reference": "research/ui/results/2026-11-02-resize-combined-reference/strict-export.json", + "strict_export_sha256": "6a1561f3e6f6bc480c6a2f086adf7ed4798d5007b04a7bc71c7ae9deba90cfe6", + "execution_source_base_commit": "2c7c5929e11d911ad69a7e2b7c420120591862ed" + } + }, + "offline_export_source_files_sha256": { + "research/ui/export_resize_dual_axis_reference.py": "3f4f270d69a465f0a9291c055de238fc605a6aa31fe27a3954919fdcaf9d2a84", + "research/ui/export_resize_state_reference.py": "473e901305354d4ba8b22c08d22fa506c4d40da0e73bed3bf3122e4ff0e3c9b7", + "research/ui/export_mode_state_reference.py": "7e0a1e0e49f149791464c0dec357d3b5260bc2c34ca9f9c1be4edab35626cdf3" + }, + "status": "observation", + "observation_kind": "bounded-direct-protocol-and-dual-axis-resize", + "actual_original_runs": 1, + "maximum_original_runs": 1, + "semantic_acceptance": false, + "dual_axis_policy_classification": null, + "full_active_grid_verified": false, + "terminal_styles_verified": false, + "local_shell_transport_verified": false, + "presented_done": null, + "performance_gate": "not_evaluated", + "r2_complete": false, + "rea_capture_seconds": 45, + "cli_wait_timeout_seconds": 60, + "whole_lifecycle_bound_seconds": null, + "deliberate_original_process_termination": true, + "natural_gui_exit_verified": false, + "capture": { + "phases": [ + { + "phase": 0, + "label": "initial-body", + "source_hex": "1b5b3f366c1b5b3f37681b5b721b5b306d1b5b324a1b5b323b314848544f501b5b363b3148484152445f410d0a484152445f421b5b31323b3930487576771b5b31393b31484c4f57454e44", + "source_bytes": 75, + "source_sha256": "ff48cd41220a72e5da2350dc97c3c8c8c884d48cc4f7fa2328609bf8bc0cb09e", + "dsr_suffix_hex": "1b5b366e", + "cursor_reply": [ + 19, + 7 + ], + "response_hex": "1b5b31393b3752", + "private_dsr_prefix": false, + "frozen_grid": [ + 91, + 20 + ], + "window_rect": [ + 102, + 102, + 1461, + 850 + ], + "window_size": [ + 1360, + 749 + ], + "document_projection": { + "line_count": 19, + "capacity": 20, + "top": null, + "scroll": 0, + "caret_api": { + "caretLine": 17, + "caretColumn": 0, + "caretPosition": 131 + }, + "markers": [ + { + "label": "HTOP", + "document_line": 1, + "line_text_offset_utf16": 0, + "line_text_offset_scalar": 0, + "line_text_prefix_utf8_bytes": 0, + "line_start_api_value": 1, + "requested_position_api_value": 1, + "rect": { + "x": 147, + "y": 18, + "width": 8, + "height": 18 + } + }, + { + "label": "HARD_A", + "document_line": 5, + "line_text_offset_utf16": 0, + "line_text_offset_scalar": 0, + "line_text_prefix_utf8_bytes": 0, + "line_start_api_value": 9, + "requested_position_api_value": 9, + "rect": { + "x": 147, + "y": 90, + "width": 8, + "height": 18 + } + }, + { + "label": "HARD_B", + "document_line": 6, + "line_text_offset_utf16": 0, + "line_text_offset_scalar": 0, + "line_text_prefix_utf8_bytes": 0, + "line_start_api_value": 16, + "requested_position_api_value": 16, + "rect": { + "x": 147, + "y": 108, + "width": 8, + "height": 18 + } + }, + { + "label": "u", + "document_line": 11, + "line_text_offset_utf16": 89, + "line_text_offset_scalar": 89, + "line_text_prefix_utf8_bytes": 89, + "line_start_api_value": 27, + "requested_position_api_value": 116, + "rect": { + "x": 859, + "y": 198, + "width": 8, + "height": 18 + } + }, + { + "label": "v", + "document_line": 11, + "line_text_offset_utf16": 90, + "line_text_offset_scalar": 90, + "line_text_prefix_utf8_bytes": 90, + "line_start_api_value": 27, + "requested_position_api_value": 117, + "rect": { + "x": 867, + "y": 198, + "width": 8, + "height": 18 + } + }, + { + "label": "w", + "document_line": 11, + "line_text_offset_utf16": 91, + "line_text_offset_scalar": 91, + "line_text_prefix_utf8_bytes": 91, + "line_start_api_value": 27, + "requested_position_api_value": 118, + "rect": { + "x": 147, + "y": 216, + "width": 8, + "height": 18 + } + }, + { + "label": "LOWEND", + "document_line": 17, + "line_text_offset_utf16": 0, + "line_text_offset_scalar": 0, + "line_text_prefix_utf8_bytes": 0, + "line_start_api_value": 125, + "requested_position_api_value": 125, + "rect": { + "x": 147, + "y": 324, + "width": 8, + "height": 18 + } + } + ], + "synthetic_lines_sha256": "ac5cc0c46768d14945e8c63103f3d7d56f9a5fc2f5631dd4fe8f485e9ba19efc", + "synthetic_text": { + "encoding": "UTF-8", + "catalog_source": "resize_dual_axis_reference.synthetic_lines(case_id, phase); sorted ASCII scalar order", + "catalog_count": 658, + "catalog_sha256": "3fa7b73143c16ded196c7b5ef48d0f0d02393f41cf1a1fc279199077efd11a46", + "lines": [ + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 427, + "utf8_hex": "48544f50", + "utf8_bytes": 4, + "unicode_scalars": 4, + "utf16_code_units": 4 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 201, + "utf8_hex": "484152445f41", + "utf8_bytes": 6, + "unicode_scalars": 6, + "utf16_code_units": 6 + }, + { + "catalog_index": 310, + "utf8_hex": "484152445f42", + "utf8_bytes": 6, + "unicode_scalars": 6, + "utf16_code_units": 6 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 3, + "utf8_hex": "2020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020757677", + "utf8_bytes": 92, + "unicode_scalars": 92, + "utf16_code_units": 92 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 548, + "utf8_hex": "4c4f57454e44", + "utf8_bytes": 6, + "unicode_scalars": 6, + "utf16_code_units": 6 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + } + ] + } + }, + "stage_role": "initial-body" + }, + { + "phase": 1, + "label": "dual-shrunk-with-write", + "source_hex": "1b5b373b32484451", + "source_bytes": 8, + "source_sha256": "7dbfd84d8b35cc3e9bf04dae08d9106e409e3fa0864806d35c11080d009ee01d", + "dsr_suffix_hex": "1b5b366e", + "cursor_reply": [ + 7, + 4 + ], + "response_hex": "1b5b373b3452", + "private_dsr_prefix": false, + "frozen_grid": [ + 61, + 14 + ], + "window_rect": [ + 102, + 102, + 1101, + 670 + ], + "window_size": [ + 1000, + 569 + ], + "document_projection": { + "line_count": 19, + "capacity": 14, + "top": null, + "scroll": 5, + "caret_api": { + "caretLine": 11, + "caretColumn": 0, + "caretPosition": 30 + }, + "markers": [ + { + "label": "HTOP", + "document_line": 1, + "line_text_offset_utf16": 0, + "line_text_offset_scalar": 0, + "line_text_prefix_utf8_bytes": 0, + "line_start_api_value": 1, + "requested_position_api_value": 1, + "rect": { + "x": 147, + "y": -72, + "width": 8, + "height": 18 + } + }, + { + "label": "HARD_A", + "document_line": 5, + "line_text_offset_utf16": 0, + "line_text_offset_scalar": 0, + "line_text_prefix_utf8_bytes": 0, + "line_start_api_value": 9, + "requested_position_api_value": 9, + "rect": { + "x": 147, + "y": 0, + "width": 8, + "height": 18 + } + }, + { + "label": "HARD_B", + "document_line": 6, + "line_text_offset_utf16": 0, + "line_text_offset_scalar": 0, + "line_text_prefix_utf8_bytes": 0, + "line_start_api_value": 16, + "requested_position_api_value": 16, + "rect": { + "x": 147, + "y": 18, + "width": 8, + "height": 18 + } + }, + { + "label": "w", + "document_line": 11, + "line_text_offset_utf16": 61, + "line_text_offset_scalar": 61, + "line_text_prefix_utf8_bytes": 61, + "line_start_api_value": 27, + "requested_position_api_value": 88, + "rect": { + "x": 147, + "y": 126, + "width": 8, + "height": 18 + } + }, + { + "label": "LOWEND", + "document_line": 17, + "line_text_offset_utf16": 0, + "line_text_offset_scalar": 0, + "line_text_prefix_utf8_bytes": 0, + "line_start_api_value": 95, + "requested_position_api_value": 95, + "rect": { + "x": 147, + "y": 234, + "width": 8, + "height": 18 + } + } + ], + "synthetic_lines_sha256": "0767142d684e0773f9abec05b70b77e19e8f6768fabf51738bf1ec29e3417fdb", + "synthetic_text": { + "encoding": "UTF-8", + "catalog_source": "resize_dual_axis_reference.synthetic_lines(case_id, phase); sorted ASCII scalar order", + "catalog_count": 1030, + "catalog_sha256": "99a2f6a071f35047e6d1237a311c290f40e4df00c24f6c3b7bca147f65eb7710", + "lines": [ + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 679, + "utf8_hex": "48544f50", + "utf8_bytes": 4, + "unicode_scalars": 4, + "utf16_code_units": 4 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 335, + "utf8_hex": "484152445f41", + "utf8_bytes": 6, + "unicode_scalars": 6, + "utf16_code_units": 6 + }, + { + "catalog_index": 501, + "utf8_hex": "484152445f42", + "utf8_bytes": 6, + "unicode_scalars": 6, + "utf16_code_units": 6 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 302, + "utf8_hex": "2044512020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202077", + "utf8_bytes": 62, + "unicode_scalars": 62, + "utf16_code_units": 62 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 863, + "utf8_hex": "4c4f57454e44", + "utf8_bytes": 6, + "unicode_scalars": 6, + "utf16_code_units": 6 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + } + ] + } + }, + "stage_role": "dual-shrunk-with-write" + }, + { + "phase": 2, + "label": "dual-restored-unmodified", + "source_hex": "", + "source_bytes": 0, + "source_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "dsr_suffix_hex": "1b5b366e", + "cursor_reply": [ + 7, + 4 + ], + "response_hex": "1b5b373b3452", + "private_dsr_prefix": false, + "frozen_grid": [ + 91, + 20 + ], + "window_rect": [ + 102, + 102, + 1461, + 850 + ], + "window_size": [ + 1360, + 749 + ], + "document_projection": { + "line_count": 18, + "capacity": 20, + "top": null, + "scroll": 5, + "caret_api": { + "caretLine": 11, + "caretColumn": 0, + "caretPosition": 30 + }, + "markers": [ + { + "label": "HTOP", + "document_line": 1, + "line_text_offset_utf16": 0, + "line_text_offset_scalar": 0, + "line_text_prefix_utf8_bytes": 0, + "line_start_api_value": 1, + "requested_position_api_value": 1, + "rect": { + "x": 147, + "y": -72, + "width": 8, + "height": 18 + } + }, + { + "label": "HARD_A", + "document_line": 5, + "line_text_offset_utf16": 0, + "line_text_offset_scalar": 0, + "line_text_prefix_utf8_bytes": 0, + "line_start_api_value": 9, + "requested_position_api_value": 9, + "rect": { + "x": 147, + "y": 0, + "width": 8, + "height": 18 + } + }, + { + "label": "HARD_B", + "document_line": 6, + "line_text_offset_utf16": 0, + "line_text_offset_scalar": 0, + "line_text_prefix_utf8_bytes": 0, + "line_start_api_value": 16, + "requested_position_api_value": 16, + "rect": { + "x": 147, + "y": 18, + "width": 8, + "height": 18 + } + }, + { + "label": "w", + "document_line": 11, + "line_text_offset_utf16": 61, + "line_text_offset_scalar": 61, + "line_text_prefix_utf8_bytes": 61, + "line_start_api_value": 27, + "requested_position_api_value": 88, + "rect": { + "x": 635, + "y": 108, + "width": 8, + "height": 18 + } + }, + { + "label": "LOWEND", + "document_line": 17, + "line_text_offset_utf16": 0, + "line_text_offset_scalar": 0, + "line_text_prefix_utf8_bytes": 0, + "line_start_api_value": 95, + "requested_position_api_value": 95, + "rect": { + "x": 147, + "y": 216, + "width": 8, + "height": 18 + } + } + ], + "synthetic_lines_sha256": "877bad342f6e3c728a74fac3e75c85898568ed58acd737f2ab59c78cdd057bda", + "synthetic_text": { + "encoding": "UTF-8", + "catalog_source": "resize_dual_axis_reference.synthetic_lines(case_id, phase); sorted ASCII scalar order", + "catalog_count": 1030, + "catalog_sha256": "99a2f6a071f35047e6d1237a311c290f40e4df00c24f6c3b7bca147f65eb7710", + "lines": [ + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 679, + "utf8_hex": "48544f50", + "utf8_bytes": 4, + "unicode_scalars": 4, + "utf16_code_units": 4 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 335, + "utf8_hex": "484152445f41", + "utf8_bytes": 6, + "unicode_scalars": 6, + "utf16_code_units": 6 + }, + { + "catalog_index": 501, + "utf8_hex": "484152445f42", + "utf8_bytes": 6, + "unicode_scalars": 6, + "utf16_code_units": 6 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 302, + "utf8_hex": "2044512020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202077", + "utf8_bytes": 62, + "unicode_scalars": 62, + "utf16_code_units": 62 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 863, + "utf8_hex": "4c4f57454e44", + "utf8_bytes": 6, + "unicode_scalars": 6, + "utf16_code_units": 6 + } + ] + } + }, + "stage_role": "dual-restored-unmodified" + } + ], + "source_bytes": 83, + "dsr_request_bytes": 12, + "application_reply_bytes": 19, + "window_rect_format": "qt-qrect-inclusive-ltrb", + "geometry_checks": [ + { + "phase": 0, + "expected_size": [ + 1360, + 749 + ], + "window_rect": [ + 102, + 102, + 1461, + 850 + ], + "window_size": [ + 1360, + 749 + ], + "observed_grid": [ + 91, + 20 + ], + "terminal_capacity": 20, + "event_count": 1 + }, + { + "phase": 1, + "expected_size": [ + 1000, + 569 + ], + "window_rect": [ + 102, + 102, + 1101, + 670 + ], + "window_size": [ + 1000, + 569 + ], + "observed_grid": [ + 61, + 14 + ], + "terminal_capacity": 14, + "event_count": 2 + }, + { + "phase": 2, + "expected_size": [ + 1360, + 749 + ], + "window_rect": [ + 102, + 102, + 1461, + 850 + ], + "window_size": [ + 1360, + 749 + ], + "observed_grid": [ + 91, + 20 + ], + "terminal_capacity": 20, + "event_count": 3 + } + ], + "frozen_grids": [ + [ + 91, + 20 + ], + [ + 61, + 14 + ], + [ + 91, + 20 + ] + ], + "naws_history": [ + [ + 91, + 20 + ], + [ + 61, + 14 + ], + [ + 91, + 20 + ] + ], + "freeze_event_counts": [ + 1, + 2, + 3 + ], + "resize_boundaries": [ + { + "phase": 1, + "before_event_count": 1, + "after_event_count": 2, + "requested_window": [ + 1000, + 569 + ] + }, + { + "phase": 2, + "before_event_count": 2, + "after_event_count": 3, + "requested_window": [ + 1360, + 749 + ] + } + ], + "job_after": { + "active": 0, + "total": 3, + "terminated": 0 + }, + "provider": { + "id": "frida", + "version": null, + "confidence": "observed", + "evidence_id": "ev_90c7760b89eb2fbba93093ac9f9ff1c907bce8d18ea274b8a878fb70c7db5c75", + "messages": 8, + "readiness_messages": 3, + "phase_messages": 3, + "messages_truncated": false, + "cleanup_error": null + }, + "stateful_observation_count": 3, + "post_initial_terminal_input": "After the dual-axis shrink: one fixed CUP(7;2)+\"DQ\" write and DSR. After the dual restore: DSR only. No redraw, CR/LF or other text.", + "independent_dsr_bounds": { + "row": [ + 1, + 200 + ], + "column": [ + 1, + 500 + ] + }, + "text_units": { + "source": "UTF-8 bytes", + "document_line": "original public API index", + "marker_text_offsets": "JS UTF-16 code units; scalar count coincides only for this ASCII source alphabet", + "position_api": "recorded lineStart API value plus JS UTF-16 offset, passed unchanged to visibleRectFromPosition", + "caret_api": "original API scalar values; numbering policy not inferred", + "rectangles": "actual public API pixel rectangles, kept separate from protocol cells", + "protocol_cursor": "DSR values kept independent of text offsets and pixel rectangles", + "cpp_cell_column_mapping": null, + "document_position_unit_classification": null, + "ascii_only": true + } + }, + "provenance": { + "source_identity": { + "kind": "source-archive", + "manifest_sha256": "96d3783a19e19e50db2c306f1b107373d1e564df2e32b86f8b50fe10dd566434" + }, + "execution_source_base_commit": "dee3c78cfda60a99211dcf8f217be6e361172d5f", + "execution_source_files_sha256": { + "research/ui/resize_dual_axis_reference.py": "5bac5094335f5dbe5bfa4a904dafae13c0507bbf3d0b7e5cf40ec1003d91df66", + "research/ui/resize_dual_axis_reference.js": "e92d7098a170c5ef648e615df8e335c9d8ab1a6ddb5d193c8562958546ac7ea0", + "research/ui/resize_state_reference.py": "628f9e3a5e3ecea6c34829810df2cdf0e93dc221bd3d19b31b3f5028759f07fd", + "research/ui/mode_state_reference.py": "bac745e74a2653412ea7a701e3a1f3defa1fb97fdd7841b73fad483120496ec4", + "research/ui/alternate_reference.py": "94b85d49347e0e9813f7acddf0244f58520a7d1fce14084677c91cf4ddafffea", + "research/ui/r1_launch.py": "54fb41872c6abcc48242a0938044184854b016785cc13659c66b1bf404da0d8a", + "research/ui/launch.py": "6970e0d4f2fe5e7ec4827df7aa1026cd0e74aeb828cdc9b7a0b42b3c887eb4e6", + "research/ui/qt_r1_text.js": "f5820d413fdd72c13dffcefbf4d0ade615abfe15754c6b0b52e1e098814eea6f", + "research/ui/r1_vt_fixture.py": "5853f684d5b8a4c17a30f22a536f04f0eb212813b8d6b3cc3c197df392eb732a", + "experiments/conpty/run.py": "3766879cafe38aabf53cfac8ab2f62985b1976b6fa24c8b6031258bdc914258f" + }, + "launcher_namespace_sha256": "fdf1cd327557c7fdb8c5643c4b0e5092f4d0840adc7ddf8e6b79c7c985a58718", + "reference_identity": { + "exe_sha256": "7b10531dccc7cd614ae559f36f1ef518e0c96335637431b4646354823b2804d0", + "manifest_sha256": "e459adf2f12adb6a1b7f55e66a9da1c22794185e66e848057d21e0c60d669203", + "files_verified": 4047, + "module_files_sha256": { + "WindTerm.exe": "7b10531dccc7cd614ae559f36f1ef518e0c96335637431b4646354823b2804d0", + "Qt5Core.dll": "8d2ff4ce9096ddccc4f4cd62c2e41fc854cfd1b0d6e8d296645a7f5fd4ae565a", + "Qt5Gui.dll": "5e7d2d41b8b92a880e83b8cc0ca173f5da61218604186196787ee1600956be1e", + "Qt5Widgets.dll": "3788c669d4b645e5a576de9fc77fca776bf516d43c89143dc2ca28291ba14358", + "Qt5Qml.dll": "30035484c81590976627f8face9507caa8581a7dc7630cccf6a8d6de65cab707" + } + }, + "launch_host": { + "windows_version": "10.0.26300", + "python_version_number": "3.12.10", + "python_exe_sha256": "4d6f5f81a4bca11191c4c7c6b43632694d0a4ce74e068619d8fdc161d469859a", + "node_exe_sha256": "1c331340fa6cac5e4294725959070a143fa0507195a78ad77eed7943f719aac9", + "rea_package_version": "6.7.0", + "rea_shim_sha256": "d092352a4088e7df223e9fe39039d703411dc5cbd39990f69756ade448ef1de4", + "rea_package_json_sha256": "3f6c47fb12c8388f291a21e7f99a876495d03aee671b3b6a0708dd12d6ebb570", + "rea_entry_sha256": "ed06cee49505b95a3cc5fceb1b4b279167e26df86227c644bb366ffffdbaf738", + "runtime_identity_source": "recorded execution plan; exporter does not execute the captured runtimes" + }, + "raw_files": { + "evidence.json": { + "bytes": 12461, + "sha256": "97c6109581bca7c143b8ff0aaaa8510a91e05e7e0fa64f80911bc1d3346f03d3" + }, + "run.json": { + "bytes": 617, + "sha256": "1660c25aaf48326fe109ce82355fc4e1fbdcf4563ab250fb8fa2faa63326f206" + }, + "probe.js": { + "bytes": 26048, + "sha256": "6a72072c30ce8ac8bbfa401bb1ee1dd7c59fc6f97864a90c75096f53b51bdfad" + }, + "peer.json": { + "bytes": 1751, + "sha256": "177300389661778bae58ec5982892cc4abcc2b5459ebe472c4100bff91bda4ed" + }, + "resize-dual-axis-result.json": { + "bytes": 5309, + "sha256": "88991bb1d644a73b2e9e023a91b41bb4a9ae5a7356fa1de8a163bc3fb4ff376b" + }, + "files.json": { + "bytes": 637666, + "sha256": "a25c87cba935294d5bfd3bbc76c650d31a8473809564d798ad5a889e4aea00e8" + }, + "mechanics-check.json": { + "bytes": 891, + "sha256": "c601bde95eee544b484ab305ac52ac2b20e4cb6ad8c96e75fd93640530c9114c" + }, + "stderr.txt": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "profile-check.json": { + "bytes": 52, + "sha256": "5e522a64e98197722ccd084639b1ce50258b238e03be2f016e81c8af023b5ee1" + }, + "cleanup.json": { + "bytes": 42, + "sha256": "8400f93b6a4a4a50cde35b5d474660590ed215732f55772a9d8afac138da4f1d" + }, + "fixture-ready": { + "bytes": 32, + "sha256": "1f0b8ece17746ce41c81df65e45a6f8480747d9b69858dc905a29ae8d4c449cb" + }, + "ack-0": { + "bytes": 8, + "sha256": "604cee807f644af47487bf2bbab442b94212ac5119f36f995f78e9e4694dae8c" + }, + "ack-1": { + "bytes": 8, + "sha256": "604cee807f644af47487bf2bbab442b94212ac5119f36f995f78e9e4694dae8c" + }, + "ack-2": { + "bytes": 8, + "sha256": "604cee807f644af47487bf2bbab442b94212ac5119f36f995f78e9e4694dae8c" + }, + "resize-request-1": { + "bytes": 31, + "sha256": "4a3944bd5e1f7fc518c7a12685e282c6998e460ab9d87e023ece3e332b1888d1" + }, + "resize-ready-1": { + "bytes": 42, + "sha256": "db412d038712fcb1df8d1601b1397dc2982d6d3c19872ea34b6d1e60a2ae12b1" + }, + "resize-request-2": { + "bytes": 31, + "sha256": "4047eb7c9a836c99a56571e69999fb2173cb23d85412491478a1ea0d3adfde23" + }, + "resize-ready-2": { + "bytes": 42, + "sha256": "dd2232679e6eeeeaa97bc8e4379bf0d86d88bfd95362f68f5fa94b7dc42daabe" + } + }, + "authorized_plan": { + "bytes": 6849, + "sha256": "3beabf9a44537d43a400209786f4aeccc29a255307a74f4a4c13cf0ac812d55d" + } + }, + "write_probes": [ + { + "phase": 1, + "document_line": 11, + "line_text_offset": 1, + "line": " DQ w" + }, + { + "phase": 2, + "document_line": 11, + "line_text_offset": 1, + "line": " DQ w" + } + ], + "write_probe_definition": { + "phase": 1, + "cup": [ + 7, + 2 + ], + "text": "DQ", + "source_hex": "1b5b373b32484451" + }, + "limitations": [ + "One fresh direct Telnet process executes the immutable dual-axis-retention schedule 91x20 -> 61x14 (one window request changing both axes) -> 91x20; every stage is observed before the next resize.", + "After the dual-axis shrink, one fixed CUP(7;2)+\"DQ\" write and DSR are sent; after the dual restore, DSR only. No other CUP, CR/LF, redraw or replacement text is sent.", + "Stage zero requires every preloaded sentinel and DSR19;7; later missing markers and the observed write position are observations, never semantic acceptance.", + "The dual-axis 61x14 endpoint and its NAWS event count/order are saved before assertions; the request is one window action, not an assumed atomic two-axis transport event. No calibration or retries.", + "The one write is recorded through its document line, text offset and DSR; no general original position-unit, scroll or C++ Cell mapping is inferred.", + "The existing geometry and sequential width/height records identify prior evidence only; this run does not inherit their semantic behavior.", + "DSR safety bounds remain row1..200/column1..500 independently of the fourteen-row viewport.", + "Only finite ASCII source-derived catalog strings may be published, with complete reconstructible document arrays. Unrestricted recording stays private.", + "This single schedule does not establish alternate-frame, Unicode, arbitrary dimensions or resize order, further post-resize editing, ConPTY, performance or R2 acceptance, and does not merge the original observation with any confirmed TermForge difference." + ] +} diff --git a/research/ui/results/2026-11-04-resize-dual-axis-reference/validation.json b/research/ui/results/2026-11-04-resize-dual-axis-reference/validation.json new file mode 100644 index 00000000..ba594f4f --- /dev/null +++ b/research/ui/results/2026-11-04-resize-dual-axis-reference/validation.json @@ -0,0 +1,112 @@ +{ + "schema": "termforge-original-resize-dual-axis-validation-v1", + "case_id": "dual-axis-retention", + "source_binding": "Separate identities: the frozen original run used the probe/source snapshot committed at dee3c78; the first strict export failed before mechanics; the offline document-line/exporter correction and the core replay were later committed at 2fc3f25; this validation and results README are documentation corrections. The original run was not repeated, and later files are not backfilled into the frozen run source identity.", + "passed": true, + "original_behavior_oracle": false, + "semantic_acceptance": false, + "full_active_grid_verified": false, + "physical_presentation_verified": false, + "windterm_parity": false, + "r27_complete": false, + "r29_complete": false, + "r2_complete": false, + "presented_done": null, + "frozen_original_run": { + "source_commit": "dee3c78cfda60a99211dcf8f217be6e361172d5f", + "source_tree": "c4f5430ca9ed83238472efca8e0376ea2574c2c8", + "execution_source_files_sha256": { + "research/ui/resize_dual_axis_reference.py": "5bac5094335f5dbe5bfa4a904dafae13c0507bbf3d0b7e5cf40ec1003d91df66", + "research/ui/resize_dual_axis_reference.js": "e92d7098a170c5ef648e615df8e335c9d8ab1a6ddb5d193c8562958546ac7ea0", + "research/ui/resize_state_reference.py": "628f9e3a5e3ecea6c34829810df2cdf0e93dc221bd3d19b31b3f5028759f07fd", + "research/ui/mode_state_reference.py": "bac745e74a2653412ea7a701e3a1f3defa1fb97fdd7841b73fad483120496ec4", + "research/ui/alternate_reference.py": "94b85d49347e0e9813f7acddf0244f58520a7d1fce14084677c91cf4ddafffea", + "research/ui/r1_launch.py": "54fb41872c6abcc48242a0938044184854b016785cc13659c66b1bf404da0d8a", + "research/ui/launch.py": "6970e0d4f2fe5e7ec4827df7aa1026cd0e74aeb828cdc9b7a0b42b3c887eb4e6", + "research/ui/qt_r1_text.js": "f5820d413fdd72c13dffcefbf4d0ade615abfe15754c6b0b52e1e098814eea6f", + "research/ui/r1_vt_fixture.py": "5853f684d5b8a4c17a30f22a536f04f0eb212813b8d6b3cc3c197df392eb732a", + "experiments/conpty/run.py": "3766879cafe38aabf53cfac8ab2f62985b1976b6fa24c8b6031258bdc914258f" + }, + "plan_sha256": "3beabf9a44537d43a400209786f4aeccc29a255307a74f4a4c13cf0ac812d55d", + "attempt_marker_sha256": "ed761217af8e88440bea5afe49a649b79889f25dabd35db03f616895e8f83a34", + "maximum_original_runs": 1, + "actual_original_runs": 1, + "additional_original_runs": 0, + "reference_exe_sha256": "7b10531dccc7cd614ae559f36f1ef518e0c96335637431b4646354823b2804d0", + "reference_files_manifest_sha256": "e459adf2f12adb6a1b7f55e66a9da1c22794185e66e848057d21e0c60d669203", + "source_archive_manifest_sha256": "96d3783a19e19e50db2c306f1b107373d1e564df2e32b86f8b50fe10dd566434", + "windows_version": "10.0.26300", + "python": "3.12.10", + "node_sha256": "1c331340fa6cac5e4294725959070a143fa0507195a78ad77eed7943f719aac9", + "rea_version": "6.7.0" + }, + "offline_strict_export": { + "first_export": "refused by the frozen document-line whitelist on the observed post-write row; run_plan stopped before writing mechanics-check.json; failure preserved", + "correction": "document-line whitelist/exporter only; same raw recording, plan and .attempted marker; no original rerun", + "applicable_source_commit": "2fc3f25cb1887b569c583068a49e874fa4bd9156", + "source_files_sha256": { + "research/ui/export_resize_dual_axis_reference.py": "3f4f270d69a465f0a9291c055de238fc605a6aa31fe27a3954919fdcaf9d2a84", + "research/ui/export_resize_state_reference.py": "473e901305354d4ba8b22c08d22fa506c4d40da0e73bed3bf3122e4ff0e3c9b7", + "research/ui/export_mode_state_reference.py": "7e0a1e0e49f149791464c0dec357d3b5260bc2c34ca9f9c1be4edab35626cdf3" + }, + "public_file": "research/ui/results/2026-11-04-resize-dual-axis-reference/strict-export.json", + "public_sha256": "95ad9c5bd8d73e278e794fa327e2fd902739c3da9bd4be6add4d560a00a7d18f", + "public_bytes": 38190, + "schema": "termforge-original-resize-dual-axis-reference-v1", + "status": "observation", + "phases": 3 + }, + "core_replay": { + "applicable_source_commit": "2fc3f25cb1887b569c583068a49e874fa4bd9156", + "test": "resize_dual_axis_test", + "ctest": "resize_dual_axis_core", + "test_source_sha256": "9afd9762a2d3559c32d30707bb482461cd466ab99b07c16dc665c5b400e5504f", + "cmake_source_sha256": "10939636f534db304108c50b6d7b10af1f59db9953175707f135ab00753586ee", + "assertions": 11, + "failed": 0, + "scope": "Engineering replay of fixed source/protocol inputs: initial body, resize_preserving(14,61), pre-write retained-row invariant, CUP(7;2)+DQ+DSR, resize_preserving(20,91), final DSR. Pre-write full-row retention is a product invariant only; the post-write 62-character row and 7;4 DSR are the fixed observation. No production change." + }, + "documentation": { + "prior_report_commit": "1e41fc729493b2fe2d15d1286b69c5e3b6e89028", + "attribution_correction_commit": "aed633381d309db44ff3e361a27a6818cf724391", + "identity_correction_commit": "e3296fca590db12b722deaa8de25de14eee339fd", + "identity_note": "aed6333 contains the narrowed attribution; e3296fc records the documentation identity. Neither reran the original nor altered the frozen source/plan/attempt identity." + }, + "observed_endpoints": { + "condition": "Endpoint 1 follows the dual-axis window action AND the fixed CUP(7;2)+DQ write; there is no original pre-write body observation.", + "windows": [[1360, 749], [1000, 569], [1360, 749]], + "grids": [[91, 20], [61, 14], [91, 20]], + "naws_event_count": [1, 2, 3], + "dsr": ["19;7", "7;4", "7;4"], + "scroll": [0, 5, 5], + "document_lines": [19, 19, 18], + "document_row_11": [ + "initial: 89 spaces + uvw (92 chars)", + "after resize + CUP(7;2)+DQ: ' DQ' + 58 spaces + w (62 chars)", + "after restore: same 62-char written row" + ], + "write_probe": {"phase": 1, "cup": [7, 2], "text": "DQ", "document_line": 11, "line_text_offset": 1}, + "notes": [ + "The request is one window action changing both axes; the observed NAWS sequence is recorded and not assumed atomic.", + "The observed 62-character row/scroll are post-write results. Whether clipping/materialization happened at resize, CUP, or the write is not observed.", + "The sequential #308 sample had no equivalent follow-up write, so the two samples cannot adjudicate a dual-axis vs sequential resize clipping difference.", + "No original pre-write body endpoint exists; pre-write full-row retention is only the TermForge engineering invariant asserted by the core replay." + ] + }, + "preserved": { + "old_strict_failures_rewritten": false, + "old_six_stage_bcd_assertion_relaxed": false, + "existing_combined_hidden_qt_evidence_changed": false, + "confirmed_intentional_difference_merged_into_original": false, + "original_was_not_rerun": true + }, + "limitations": [ + "One fixed 75 B ASCII direct-Telnet sample; main screen only; no alternate frame, Unicode, other sizes/orders, continuous output, selection/Find, full grid/style, ConPTY transport, physical presentation or performance.", + "The only post-resize body endpoint includes the fixed CUP+write; clipping/materialization attribution between resize, CUP and write remains unknown. There is no original pre-write body observation.", + "The observed original had scroll=5 and a written row of 62 characters; no internal history-row count or general scroll model is inferred.", + "The sequential width-then-height sample is not a comparable control for this write-intervened sample; no dual-axis vs sequential resize cropping conclusion is drawn.", + "The one NAWS event count/order is recorded for this run only; a single window request is not assumed to be an atomic two-axis transport event in general.", + "The old six-stage B/CD native-sync failure is not explained by this WindTerm UI observation and is not relaxed; ConPTY native-grid reflow after a simultaneous change remains unobserved.", + "The product retained-body direction remains the confirmed direction; this record is an original observation with a post-write endpoint, not a claim of WindTerm equality." + ] +} diff --git a/research/ui/test_export_resize_dual_axis_reference.py b/research/ui/test_export_resize_dual_axis_reference.py new file mode 100644 index 00000000..73e56c3f --- /dev/null +++ b/research/ui/test_export_resize_dual_axis_reference.py @@ -0,0 +1,377 @@ +"""Synthetic three-stage dual-axis resize publication guards; no target, sockets, REA or native UI.""" +from __future__ import annotations +import copy +from contextlib import contextmanager +import hashlib +import json +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest +from unittest.mock import patch + +from research.ui import export_resize_dual_axis_reference as exporter +from research.ui import resize_dual_axis_reference as probe +from research.ui import mode_state_reference as original_probe +from research.ui import export_mode_state_reference as original_exporter +from research.ui.test_resize_dual_axis_reference import recording +from research.ui.test_export_mode_state_reference import fixture as mode_fixture, complete_fixture as mode_complete_fixture + + +def save(directory, data, synchronize=True): + if synchronize: + result = data[probe.RESULT_FILE] + payloads = [{"kind": "probe_ready", "station_isolated": True, "scenario": "resize-dual-axis-v1", "case_id": result["case_id"]}, + {"kind": "readiness", "phase": "ui", "ready": True, "has_tree": True, + "has_engine": True, "has_editor": False, "peer_ready": False}] + payloads.extend({"kind": "resize_dual_axis_phase", **copy.deepcopy(row)} for row in result["observations"]) + payloads.append({"kind": "resize_dual_axis_result", **copy.deepcopy(result)}) + data["evidence.json"]["data"]["evidence"]["normalized_result"]["messages"] = [ + {"type": "send", "script_id": "synthetic-script", "payload": value} for value in payloads] + for name, value in data.items(): + (directory / name).write_text(json.dumps(value), encoding="utf-8") + + +def marker_rows(lines): + result, offset = [], 0 + for row, line in enumerate(lines): + for label in exporter.MARKERS: + column = line.find(label) + while column >= 0: + result.append({"label": label, "document_line": row, "column": column, "position": offset+column, "line_start_api": offset, + "rect": {"x": column*8, "y": row*18, "width": 8, "height": 18}}) + column = line.find(label, column+1) + offset += len(line)+1 + return result + + +def fixture(root, case="dual-axis-retention"): + directory, data, _ = mode_fixture(root) + data.pop("mode-state-result.json") + # A complete fixture contains only this protocol's declared acknowledgments. + for path in directory.glob("ack-*"): + path.unlink() + state, result = recording(case) + for observation in result["observations"]: + terminal = observation["terminal"] + terminal["markers"] = marker_rows(terminal["lines"]) + data["peer.json"], data[probe.RESULT_FILE] = state, result + plan = {**probe.fixed_contract(case), "execution_authorized": True, "out": str(directory), "source": str(root / "official")} + run = data["run.json"] + run.update(sha256=probe.EXE_SHA256, files_sha256=probe.MANIFEST_SHA256) + template = (probe.ROOT / "research/ui/qt_r1_text.js").read_text(encoding="utf-8").split("// Scenario driver", 1)[0] + template += probe.probe_fragment(case) + run["template_sha256"] = hashlib.sha256(template.encode()).hexdigest() + generated = exporter.expected_probe(plan, run, probe.ROOT) + (directory / "probe.js").write_bytes(generated) + run["script_sha256"] = hashlib.sha256(generated).hexdigest() + data["evidence.json"]["data"]["evidence"]["normalized_result"]["source_sha256"] = run["script_sha256"] + (directory / "fixture-ready").write_text(json.dumps({"grid": state["frozen_grids"][0], "event_count": state["freeze_event_counts"][0]})) + for index in range(len(probe.phases(case))): + (directory / f"ack-{index}").write_text("observed") + for index in probe.resize_phases(case): + (directory / f"resize-request-{index}").write_text(json.dumps({"phase": index, "window": list(probe.windows(case)[index])})) + (directory / f"resize-ready-{index}").write_text(json.dumps({"phase": index, + "grid": state["frozen_grids"][index], "event_count": state["freeze_event_counts"][index]})) + save(directory, data) + (directory / "mechanics-check.json").write_text(json.dumps(probe.audit_mechanics(directory, case))) + return directory, data, plan + + +@contextmanager +def complete_fixture(root, case="dual-axis-retention"): + with mode_complete_fixture(root) as original: + old_directory, _, old_plan, _, source_root, official = original + with patch.object(probe, "EXE_SHA256", original_probe.EXE_SHA256), \ + patch.object(probe, "MANIFEST_SHA256", original_probe.MANIFEST_SHA256), \ + patch.dict(exporter._PROVENANCE, FILE_COUNT=original_exporter.FILE_COUNT): + new_root = root / "dual-axis" + new_root.mkdir() + directory, data, case_plan = fixture(new_root, case) + plan = {**old_plan, **case_plan, "source": str(official)} + (directory / "app").mkdir() + for name in original_exporter.MODULES: + (directory / "app" / name).write_bytes((official / name).read_bytes()) + (directory / "files.json").write_bytes((old_directory / "files.json").read_bytes()) + profile = "profile/.wind/profiles/default.v10/terminal/user.sessions" + (directory / profile).parent.mkdir(parents=True) + (directory / profile).write_bytes((old_directory / profile).read_bytes()) + for name in probe.SOURCE_FILES: + (source_root / name).parent.mkdir(parents=True, exist_ok=True) + (source_root / name).write_bytes((probe.ROOT / name).read_bytes()) + archive = {"schema": probe.common.ARCHIVE_SCHEMA, "source_base_commit": "b"*40, + "source_files_sha256": {name: probe.sha(source_root / name) for name in probe.SOURCE_FILES}} + (source_root / probe.common.ARCHIVE_MANIFEST).write_text(json.dumps(archive), encoding="utf-8") + plan.update(probe.common.source_identity(root=source_root, source_files=probe.SOURCE_FILES)) + plan["launcher_source_sha256"] = hashlib.sha256(probe.launcher_source(case).encode()).hexdigest() + plan_path = new_root / "plan.json" + plan_path.write_text(json.dumps(plan), encoding="utf-8") + plan_path.with_suffix(".attempted").write_text(probe.ATTEMPT) + yield directory, data, plan, plan_path, source_root, official + + +class ResizeDualAxisExportTests(unittest.TestCase): + def test_fixed_three_stage_contract_and_literal_input_are_independent_of_prior_height_runs(self): + with tempfile.TemporaryDirectory() as temporary: + directory, _, plan = fixture(Path(temporary).resolve()) + result = exporter.capture_facts(directory, plan) + source = (b"\x1b[?6l\x1b[?7h\x1b[r\x1b[0m\x1b[2J\x1b[2;1HHTOP\x1b[6;1HHARD_A\r\nHARD_B" + b"\x1b[12;90Huvw\x1b[19;1HLOWEND") + self.assertEqual(len(source), 75) + self.assertEqual(result["stateful_observation_count"], 3) + self.assertEqual([value["frozen_grid"] for value in result["phases"]], [[91,20],[61,14],[91,20]]) + self.assertEqual([value["window_size"] for value in result["phases"]], [[1360,749],[1000,569],[1360,749]]) + self.assertEqual([value["source_hex"] for value in result["phases"]], [source.hex(), probe.WRITE.hex(), ""]) + self.assertEqual([value["source_sha256"] for value in result["phases"]], + [hashlib.sha256(value).hexdigest() for value in (source,probe.WRITE,b"")]) + self.assertEqual(result["source_bytes"], 75 + len(probe.WRITE)) + self.assertEqual(result["dsr_request_bytes"], 12) + self.assertEqual([phase["stage_role"] for phase in result["phases"]], [ + "initial-body", "dual-shrunk-with-write", "dual-restored-unmodified"]) + self.assertEqual([item["observed_grid"] for item in result["geometry_checks"]], + [[91,20],[61,14],[91,20]]) + self.assertEqual([item["terminal_capacity"] for item in result["geometry_checks"]], [20,14,20]) + self.assertEqual([item["event_count"] for item in result["geometry_checks"]], result["freeze_event_counts"]) + + def test_later_DSR_above_fourteen_rows_is_observed_without_clamping(self): + with tempfile.TemporaryDirectory() as temporary: + directory, data, plan = fixture(Path(temporary).resolve()) + peer = data["peer.json"] + reply = b"\x1b[200;500R" + for record in (peer["records"][1], data[probe.RESULT_FILE]["observations"][1]): + record.update(cursor_reply=[200,500], cursor_reply_private=False, response_hex=reply.hex()) + peer["received_hex"] = "".join(record["response_hex"] for record in peer["records"]) + save(directory,data) + result = exporter.capture_facts(directory,plan) + self.assertEqual(result["phases"][1]["cursor_reply"],[200,500]) + self.assertEqual(result["phases"][1]["document_projection"]["capacity"],14) + + def test_old_case_names_and_ten_row_endpoint_are_not_accepted(self): + for changed in ("height-only", "height-retention-measured", "width-then-height", "ten-row"): + with self.subTest(changed=changed), tempfile.TemporaryDirectory() as temporary: + directory, data, plan = fixture(Path(temporary).resolve()) + if changed == "ten-row": + data["peer.json"]["frozen_grids"][2] = [61,10] + data[probe.RESULT_FILE]["observations"][2]["terminal"]["capacity"] = 10 + else: + plan["case_id"] = changed + save(directory,data) + with self.assertRaises(ValueError): + exporter.capture_facts(directory,plan) + + def test_fixed_case_reconstructs_complete_ASCII_documents_and_exact_sources(self): + for case in probe.CASES: + with self.subTest(case=case), tempfile.TemporaryDirectory() as temporary: + directory, data, plan = fixture(Path(temporary).resolve(), case) + result = exporter.capture_facts(directory, plan) + self.assertEqual(result["stateful_observation_count"], len(probe.phases(case))) + self.assertEqual([p["source_hex"] for p in result["phases"]], [v.hex() for _,v in probe.phases(case)]) + self.assertEqual([p["phase"] for p in result["resize_boundaries"]], list(probe.resize_phases(case))) + self.assertEqual(result["post_initial_terminal_input"], plan["post_initial_terminal_input"]) + self.assertEqual(result["independent_dsr_bounds"], {"row":[1,200],"column":[1,500]}) + self.assertIsNone(result["text_units"]["cpp_cell_column_mapping"]) + self.assertIsNone(result["text_units"]["document_position_unit_classification"]) + for index, phase in enumerate(result["phases"]): + actual = data[probe.RESULT_FILE]["observations"][index]["terminal"]["lines"] + public = phase["document_projection"]["synthetic_text"] + catalog = sorted(exporter.dual_document_lines(case,index)) + self.assertEqual(public["catalog_sha256"], hashlib.sha256(json.dumps(catalog,separators=(',',':')).encode()).hexdigest()) + rebuilt = [bytes.fromhex(row["utf8_hex"]).decode('ascii') for row in public["lines"]] + self.assertEqual(rebuilt, actual) + for row,line in zip(public["lines"],rebuilt): + self.assertEqual(catalog[row["catalog_index"]],line) + self.assertEqual([row["utf8_bytes"],row["unicode_scalars"],row["utf16_code_units"]],[len(line)]*3) + self.assertEqual(phase["document_projection"]["synthetic_lines_sha256"],hashlib.sha256(json.dumps(rebuilt,separators=(',',':')).encode()).hexdigest()) + self.assertEqual(phase["cursor_reply"],[19,7]) + for marker in phase["document_projection"]["markers"]: + self.assertEqual(marker["line_text_offset_scalar"],marker["line_text_offset_utf16"]) + self.assertEqual(marker["line_text_prefix_utf8_bytes"],marker["line_text_offset_utf16"]) + self.assertEqual(marker["requested_position_api_value"],marker["line_start_api_value"]+marker["line_text_offset_utf16"]) + self.assertNotIn("column",marker) + self.assertNotIn(str(directory),json.dumps(result)) + + def test_later_missing_markers_and_split_text_are_observations(self): + for case, phase, lines in (("dual-axis-retention",1,["DQ"]), ("dual-axis-retention",2,["DQ","LOWEND"])): + with self.subTest(case=case,phase=phase), tempfile.TemporaryDirectory() as temporary: + directory,data,plan = fixture(Path(temporary).resolve(),case) + terminal=data[probe.RESULT_FILE]["observations"][phase]["terminal"] + terminal.update(lines=lines,line_count=len(lines),markers=marker_rows(lines));save(directory,data) + (directory/"mechanics-check.json").write_text(json.dumps(probe.audit_mechanics(directory,case))) + result=exporter.capture_facts(directory,plan) + self.assertEqual(result["phases"][phase]["document_projection"]["line_count"],len(lines)) + self.assertEqual(result["phases"][phase]["cursor_reply"],[19,7]) + + def test_missing_initial_fixture_private_text_nonASCII_and_duplicates_fail(self): + for phase,lines in ((0,[""]),(1,["SECRET"]),(1,["é"]),(1,["HTOP","HTOP"]), + (1,[" "*92]),(1,["LOWENDQ"]),(1,["uvwuvw"])): + with self.subTest(phase=phase,lines=lines), tempfile.TemporaryDirectory() as temporary: + directory,data,plan=fixture(Path(temporary).resolve()) + terminal=data[probe.RESULT_FILE]["observations"][phase]["terminal"] + terminal.update(lines=lines,line_count=len(lines),markers=marker_rows(lines));save(directory,data) + with self.assertRaises(ValueError):exporter.capture_facts(directory,plan) + + def test_sampled_geometry_cannot_be_missing_stale_or_disagree_with_frozen_stage(self): + for changed in ("missing", "grid", "capacity", "event", "bool-grid", "failed-result"): + with self.subTest(changed=changed), tempfile.TemporaryDirectory() as temporary: + directory, data, plan = fixture(Path(temporary).resolve()) + result = data[probe.RESULT_FILE] + measured = result["geometry_checks"][1] + if changed == "missing": measured.pop("observed_grid") + elif changed == "grid": measured["observed_grid"] = [91,14] + elif changed == "capacity": measured["terminal_capacity"] = 20 + elif changed == "event": measured["event_count"] = 1 + elif changed == "bool-grid": measured["observed_grid"][0] = True + else: + result["error"] = "Geometry prediction differed" + result["failure_geometry"] = {"window_rect": [0,0,999,568], "observed_grid": [61,13]} + save(directory, data) + with self.assertRaises((ValueError, KeyError)): exporter.capture_facts(directory, plan) + + def test_marker_API_arithmetic_and_bounds_are_not_guessed(self): + for kind in ("missing-start","bad-start","bad-sum","wide-overflow","negative-height","position-bound","label"): + with self.subTest(kind=kind), tempfile.TemporaryDirectory() as temporary: + directory,data,plan=fixture(Path(temporary).resolve()) + marker=data[probe.RESULT_FILE]["observations"][1]["terminal"]["markers"][0] + if kind=="missing-start":marker.pop("line_start_api") + elif kind=="bad-start":marker["line_start_api"]="SECRET" + elif kind=="bad-sum":marker["line_start_api"]+=1 + elif kind=="wide-overflow":marker["rect"]["width"]=1361 + elif kind=="negative-height":marker["rect"]["height"]=-1 + elif kind=="position-bound":marker["position"]=262145 + else:marker["label"]="SECRET" + save(directory,data) + with self.assertRaises((ValueError,KeyError)):exporter.capture_facts(directory,plan) + + def test_source_geometry_capacity_phase_DSR_and_NAWS_tampering_fail(self): + for changed in ("input","case","missing-stage","ack","ready","reply","unarmed","wrong-axis","outside-interval", + "window","capacity","initial-DSR","DSR-overflow"): + with self.subTest(changed=changed), tempfile.TemporaryDirectory() as temporary: + directory,data,plan=fixture(Path(temporary).resolve()) + result,peer=data[probe.RESULT_FILE],data["peer.json"] + if changed=="input":peer["records"][1]["source_hex"]="0d51" + elif changed=="case":result["case_id"]="width-then-height" + elif changed=="missing-stage":result["observations"].pop(1) + elif changed=="ack":(directory/"ack-1").write_text("not observed") + elif changed=="ready":(directory/"resize-ready-1").write_text('{}') + elif changed=="reply":peer["received_hex"]+="1b5b313b3152" + elif changed=="unarmed":peer["naws_history"].append([90,20]) + elif changed in ("wrong-axis","outside-interval"): + index=peer["resize_boundaries"][0]["before_event_count"] + peer["naws_history"][index]=[92,15] if changed=="wrong-axis" else [91,9] + elif changed=="window":result["observations"][1]["window_rect"][3]+=1 + elif changed=="capacity":result["observations"][1]["terminal"]["capacity"]=20 + else:peer["records"][0 if changed=="initial-DSR" else 1]["response_hex"]="1b5b31383b3752" if changed=="initial-DSR" else "1b5b3230313b3752" + save(directory,data) + with self.assertRaises(ValueError):exporter.capture_facts(directory,plan) + + def test_provider_source_identity_partial_results_and_cleanup_fail(self): + for changed in ("exe","manifest","template","script","evidence","truncated","cleanup","source-path","provider-observations","provider-ready"): + with self.subTest(changed=changed), tempfile.TemporaryDirectory() as temporary: + directory,data,plan=fixture(Path(temporary).resolve()) + evidence=data["evidence.json"]["data"]["evidence"] + if changed=="exe":data["run.json"]["sha256"]="0"*64 + elif changed=="manifest":data["run.json"]["files_sha256"]="0"*64 + elif changed=="template":data["run.json"]["template_sha256"]="0"*64 + elif changed=="script":(directory/"probe.js").write_bytes(b"changed") + elif changed=="evidence":evidence["evidence_id"]="SECRET" + elif changed=="truncated":evidence["normalized_result"]["messages_truncated"]=True + elif changed=="cleanup":data["cleanup.json"]["active"]=1 + elif changed=="source-path":evidence["normalized_result"]["source_path"]="C:/private/other.js" + elif changed=="provider-observations":evidence["normalized_result"]["messages"].pop(3) + else:evidence["normalized_result"]["messages"][0]["payload"]["case_id"]="wrong" + save(directory,data,synchronize=not changed.startswith("provider-")) + with self.assertRaises(ValueError):exporter.capture_facts(directory,plan) + + def test_unlisted_metadata_is_not_forwarded(self): + with tempfile.TemporaryDirectory() as temporary: + directory,data,plan=fixture(Path(temporary).resolve()) + before=exporter.capture_facts(directory,plan) + data[probe.RESULT_FILE]["observations"][1]["terminal"]["private_note"]="SECRET" + data["peer.json"]["resize_boundaries"][0]["private_path"]="C:/private" + data[probe.RESULT_FILE]["geometry_checks"][2]["private_note"]="SECRET" + save(directory,data) + self.assertEqual(exporter.capture_facts(directory,plan),before) + + def test_complete_ten_source_archive_and_package_identity(self): + for case in probe.CASES: + with self.subTest(case=case), tempfile.TemporaryDirectory() as temporary, complete_fixture(Path(temporary).resolve(), case) as values: + directory, _, _, plan_path, source_root, official = values + result = exporter.export(directory, plan_path, Path(temporary) / "public/result.json", + exporter.sha(plan_path), source_root, official) + self.assertEqual(result["schema"], "termforge-original-resize-dual-axis-reference-v1") + self.assertEqual(result["case_id"], case) + self.assertEqual(set(result["provenance"]["execution_source_files_sha256"]), set(probe.SOURCE_FILES)) + self.assertEqual(set(result["offline_export_source_files_sha256"]), set(exporter.EXPORT_SOURCES)) + self.assertIsNone(result["dual_axis_policy_classification"]) + self.assertFalse(result["semantic_acceptance"]) + self.assertEqual(result["status"],"observation") + self.assertEqual(result["geometry_basis"], { + "geometry": {"reference": "research/ui/results/2026-11-02-resize-geometry-reference/strict-export.json", + "strict_export_sha256": "f18a2320c073ed477f773b42076c9968c0c8af8530af19548d5695e66e8cf9b5", + "execution_source_base_commit": "3d4c8c2d6222caf0fc024729d9e0b34c020a69cf"}, + "sequential_width_height": {"reference": "research/ui/results/2026-11-02-resize-combined-reference/strict-export.json", + "strict_export_sha256": "6a1561f3e6f6bc480c6a2f086adf7ed4798d5007b04a7bc71c7ae9deba90cfe6", + "execution_source_base_commit": "2c7c5929e11d911ad69a7e2b7c420120591862ed"}, + "dual_axis_grid_previously_observed": False}) + + def test_geometry_basis_tampering_is_rejected_even_with_new_plan_hash(self): + for changed in ("geometry", "sequential_width_height", "source-commit", "previously-observed"): + with self.subTest(changed=changed), tempfile.TemporaryDirectory() as temporary, complete_fixture(Path(temporary).resolve()) as values: + directory, _, plan, plan_path, source_root, official = values + if changed in ("geometry", "sequential_width_height"): + plan["geometry_basis"][changed]["strict_export_sha256"] = "0"*64 + elif changed == "source-commit": + plan["geometry_basis"]["sequential_width_height"]["execution_source_base_commit"] = "0"*40 + else: plan["geometry_basis"]["dual_axis_grid_previously_observed"] = True + plan_path.write_text(json.dumps(plan), encoding="utf-8") + destination = Path(temporary)/"public/result.json" + with self.assertRaisesRegex(ValueError, "contract differs"): + exporter.export(directory,plan_path,destination,exporter.sha(plan_path),source_root,official) + self.assertFalse(destination.exists()) + + def test_changed_package_source_profile_attempt_plan_and_destination_are_rejected(self): + for changed in ("module", "source", "manifest", "profile", "attempt", "plan", "destination"): + with self.subTest(changed=changed), tempfile.TemporaryDirectory() as temporary, complete_fixture(Path(temporary).resolve()) as values: + directory, _, _, plan_path, source_root, official = values + expected = exporter.sha(plan_path) + destination = Path(temporary) / "public/result.json" + if changed == "module": (directory / "app/Qt5Core.dll").write_bytes(b"changed") + elif changed == "source": (source_root / "research/ui/resize_dual_axis_reference.py").write_bytes(b"changed") + elif changed == "manifest": (directory / "files.json").write_text('{}') + elif changed == "profile": (directory / "profile/.wind/profiles/default.v10/terminal/user.sessions").write_text('[]') + elif changed == "attempt": plan_path.with_suffix('.attempted').write_text('not dual-axis') + elif changed == "plan": plan_path.write_bytes(plan_path.read_bytes()+b' ') + elif changed == "destination": destination.parent.mkdir(); destination.write_text('preserved') + with self.assertRaises(ValueError): exporter.export(directory, plan_path, destination, expected, source_root, official) + if changed == "destination": self.assertEqual(destination.read_text(), 'preserved') + else: self.assertFalse(destination.parent.exists()) + + def test_duplicate_fields_and_nonfinite_metadata_fail_closed(self): + with tempfile.TemporaryDirectory() as temporary: + path = Path(temporary) / "record.json" + for content in ('{"phase":1,"phase":2}', '{"x":NaN}', '{"x":Infinity}', '{"x":-Infinity}'): + path.write_text(content) + with self.subTest(content=content), self.assertRaises(ValueError): exporter.read(path) + + def test_cli_refuses_private_text_normally_and_under_optimization(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary).resolve() + directory, data, plan = fixture(root) + data[probe.RESULT_FILE]["observations"][1]["terminal"]["lines"][1] = "SECRET" + save(directory, data) + plan_path = root / "plan.json" + plan_path.write_text(json.dumps(plan)) + for optimize in (False, True): + destination = root / ("optimized/out.json" if optimize else "normal/out.json") + command = [sys.executable, "-B", *(["-O"] if optimize else []), "-X", "utf8", "-m", + "research.ui.export_resize_dual_axis_reference", "--directory", str(directory), "--plan", str(plan_path), + "--expected-plan-sha256", exporter.sha(plan_path), "--destination", str(destination)] + completed = subprocess.run(command, cwd=probe.ROOT, capture_output=True, text=True, encoding="utf-8", timeout=10) + self.assertNotEqual(completed.returncode, 0) + self.assertIn("Only fixed dual-axis source-derived synthetic document lines", completed.stderr) + self.assertFalse(destination.parent.exists()) + + +if __name__ == "__main__": + unittest.main() diff --git a/research/ui/test_resize_dual_axis_reference.py b/research/ui/test_resize_dual_axis_reference.py new file mode 100644 index 00000000..b48e9767 --- /dev/null +++ b/research/ui/test_resize_dual_axis_reference.py @@ -0,0 +1,261 @@ +"""Fixed dual-axis sources, actual protected peer and plan guards; no sockets/target.""" +import hashlib +import json +from pathlib import Path +import shutil +import socket +import tempfile +import unittest +from unittest.mock import patch + +from research.ui import resize_dual_axis_reference as probe + + +def naws(columns, rows): + return bytes([255,250,31])+columns.to_bytes(2,"big")+rows.to_bytes(2,"big")+bytes([255,240]) + + +class Connection: + def __init__(self, peer, mode): + self.peer, self.mode, self.case = peer, mode, peer.case_id + self.initial, self.fragment, self.sent, self.resize_sent, self.calls = True, None, [], set(), 0 + def settimeout(self, _): pass + def close(self): pass + def sendall(self, data): self.sent.append(data) + def recv(self, _): + self.calls += 1 + if self.calls > 100: raise RuntimeError("Synthetic receive bound") + if self.initial: + self.initial = False + return naws(92 if self.mode == "stale-initial" else 91, 20) + state, directory = self.peer.state, self.peer.directory + armed = state.get("resize_armed") + if armed and armed not in self.resize_sent: + self.resize_sent.add(armed) + before = probe.required_grids(self.case)[armed-1] + grid = list(probe.required_grids(self.case)[armed]) + axis = 0 if before[0] != grid[0] else 1 + middle = list(grid); middle[axis] = (before[axis]+grid[axis])//2 + if self.mode == "wrong-axis": grid[1-axis] = before[1-axis] + 1 + if self.mode == "outside-interval": grid[axis] = max(before[axis],grid[axis])+1 + if self.mode == "wrong-endpoint": grid = middle + events = [middle, grid] if self.mode == "batched-resize" else [grid] + count = len(state["naws_history"])+(0 if self.mode == "stale-ready" else len(events)) + (directory/f"resize-ready-{armed}").write_text(json.dumps({"phase":armed,"grid":grid,"event_count":count})) + return b"".join(naws(*event) for event in events) + record = state["records"][-1] + if self.mode == "unarmed": return naws(91,19) + if record["cursor_reply"] is not None: + if self.mode == "no-ack-"+str(record["phase"]): + raise RuntimeError("Synthetic withheld acknowledgment") + (directory/f'ack-{record["phase"]}').write_text("observed") + target = record["phase"]+1 + if target in probe.resize_phases(self.case): + window = list(probe.windows(self.case)[target]) + if self.mode == "wrong-request": window[1] += 1 + (directory/f"resize-request-{target}").write_text(json.dumps({"phase":target,"window":window})) + raise socket.timeout() + if self.mode == "duplicate-reply": return b"\x1b[19;7R\x1b[19;7R" + if self.mode == "foreign-input": return b"SECRET" + if self.fragment is not None: + result, self.fragment = self.fragment, None + return result + self.fragment = (b"8;501R" if self.mode == "dsr-overflow" else + b"0;7R" if self.mode == "dsr-zero" else + b"18;7R" if self.mode == "wrong-initial-dsr" else b"19;7R") + return b"\x1b[" + + +def receive(case="dual-axis-retention", mode="normal"): + with tempfile.TemporaryDirectory() as temporary: + peer_type = probe.peer_class(case); peer = peer_type.__new__(peer_type) + peer.directory = Path(temporary).resolve() + peer.case_id = case + (peer.directory/"fixture-ready").write_text(json.dumps({"grid":[91,20],"event_count":1})) + peer.state = {"connected":False,"columns":None,"rows":None,"phase":None,"records":[]} + peer.stop = type("NeverStopped",(),{"is_set":lambda _:False})() + connection = Connection(peer,mode) + peer.listener = type("Listener",(),{"accept":lambda _:(connection,("127.0.0.1",12345))})() + peer._serve() + return peer.state, connection.sent + + + +def initial_lines(): + lines = [""]*20 + lines[1]="HTOP";lines[5]="HARD_A";lines[6]="HARD_B" + lines[11]=" "*89+"uvw";lines[18]="LOWEND" + return lines + + +def recording(case="dual-axis-retention"): + state, _ = receive(case, "batched-resize") + result={"status":"observed-fixed-resize-dual-axis-states","case_id":case,"error":None,"observations":[], + "window_rect_format":"qt-qrect-inclusive-ltrb","geometry_checks":[],"full_active_grid_verified":False, + "local_shell_transport_verified":False,"presented_done":None} + for record,window in zip(state["records"],probe.windows(case)): + phase=record["phase"];lines=initial_lines() + if phase>=1: + lines[1]=" DQ" + rect=[113,-27,112+window[0],-28+window[1]] + terminal={"connected":True,"protocol":"Telnet","capacity":probe.required_grids(case)[phase][1], + "line_count":len(lines),"lines":lines,"markers":[],"top":None,"scroll":0, + "caret_api":{"caretLine":18,"caretColumn":6,"caretPosition":200}} + result["observations"].append({**{k:record[k] for k in ("phase","label","frozen_grid","cursor_reply","cursor_reply_private","response_hex")}, + "terminal":terminal,"window_rect":rect,"window_size":list(window)}) + result["geometry_checks"].append({"phase":phase,"window_rect":rect,"window_size":list(window),"expected_size":list(window), + "observed_grid":list(probe.required_grids(case)[phase]),"terminal_capacity":terminal["capacity"], + "event_count":state["freeze_event_counts"][phase]}) + return state,result + + +def save_mechanics(directory, state, result): + for name,value in (("peer.json",state),(probe.RESULT_FILE,result),("cleanup.json",{"active":0,"total":1,"terminated":1}), + ("profile-check.json",{"only_synthetic_session":True,"session_count":1})): + (directory/name).write_text(json.dumps(value),encoding="utf-8") + (directory/"stderr.txt").write_bytes(b"") + for phase in range(len(result["observations"])):(directory/f"ack-{phase}").write_text("observed") + + +class DualAxisResizeReferenceTests(unittest.TestCase): + def test_fixed_combined_geometry_and_DSR_only_after_ASCII_source(self): + self.assertEqual([len(probe.phases(c)) for c in probe.CASES],[3]) + expected=(b"\x1b[?6l\x1b[?7h\x1b[r\x1b[0m\x1b[2J\x1b[2;1HHTOP\x1b[6;1HHARD_A\r\nHARD_B" + b"\x1b[12;90Huvw\x1b[19;1HLOWEND") + for case in probe.CASES: + self.assertEqual(probe.phases(case)[0][1],expected) + self.assertEqual(probe.phases(case)[1][1],probe.WRITE);self.assertEqual(probe.phases(case)[2][1],b"") + self.assertEqual(probe.fixed_contract(case)["semantic_acceptance"],False) + self.assertEqual(probe.windows("dual-axis-retention"),[(1360,749),(1000,569),(1360,749)]) + self.assertEqual(probe.required_grids("dual-axis-retention"),[(91,20),(61,14),(91,20)]) + self.assertEqual(probe.fixed_contract("dual-axis-retention")["geometry_basis"]["geometry"]["strict_export_sha256"],"f18a2320c073ed477f773b42076c9968c0c8af8530af19548d5695e66e8cf9b5") + + def test_real_peer_fragmented_DSR_and_bounded_single_axis_NAWS(self): + for case in probe.CASES: + state,sent=receive(case,"batched-resize") + self.assertTrue(state["complete"],state.get("error"));self.assertIsNone(state.get("error")) + self.assertEqual(state["frozen_grids"],[list(g) for g in probe.required_grids(case)]) + self.assertEqual([r["cursor_reply"] for r in state["records"]],[[19,7]]*len(probe.phases(case))) + self.assertEqual(sent[-len(probe.phases(case)):],[value+b"\x1b[6n" for _,value in probe.phases(case)]) + + def test_wrong_axis_interval_endpoint_or_handshake_stops(self): + for case in probe.CASES: + for mode in ("wrong-axis","outside-interval","wrong-endpoint","stale-ready","wrong-request", + "stale-initial","unarmed","wrong-initial-dsr","duplicate-reply","foreign-input","dsr-overflow","dsr-zero"): + with self.subTest(case=case,mode=mode): + state,_=receive(case,mode);self.assertFalse(state.get("complete",False));self.assertTrue(state.get("error")) + + def test_every_before_resize_observation_requires_ack(self): + for case in probe.CASES: + for phase in range(len(probe.phases(case))-1): + state,_=receive(case,"no-ack-"+str(phase)) + self.assertTrue(state.get("error"));self.assertEqual(len(state["records"]),phase+1) + + def test_dual_axis_intervals_allow_partial_events_but_not_out_of_range(self): + self.assertTrue(probe.allowed_grid((91,20),(91,14),(91,15))) + self.assertFalse(probe.allowed_grid((91,20),(91,14),(90,15))) + self.assertTrue(probe.allowed_grid((91,20),(61,14),(76,20))) + self.assertTrue(probe.allowed_grid((91,20),(61,14),(61,17))) + self.assertTrue(probe.allowed_grid((91,20),(61,14),(91,14))) + for value in ((92,20),(61,13),(60,21),(0,14),(61,True),(61,65536)): + self.assertFalse(probe.allowed_grid((91,20),(61,14),value)) + + def test_initial_fixture_is_mandatory_later_loss_is_only_observation(self): + terminal={"line_count":20,"lines":initial_lines()} + for case in probe.CASES: + probe.validate_document(case,0,terminal) + for phase in range(1,len(probe.phases(case))):probe.validate_document(case,phase,{"line_count":1,"lines":["DQ"]}) + with self.assertRaisesRegex(ValueError,"Initial fixture"):probe.validate_document(case,0,{"line_count":1,"lines":[""]}) + for lines in (["SECRET"],["HTOP","HTOP"],["\ufffd"],[" "*500],["uvw","uvw"]): + with self.assertRaises(ValueError):probe.validate_document(case,1,{"line_count":len(lines),"lines":lines}) + + def test_mechanics_rechecks_variable_capacity_fixed_geometry_and_all_boundaries(self): + for case in probe.CASES: + with tempfile.TemporaryDirectory() as temporary: + directory=Path(temporary);state,result=recording(case);save_mechanics(directory,state,result) + self.assertEqual(probe.audit_mechanics(directory,case)["stage_count"],len(probe.phases(case))) + bad=json.loads(json.dumps(result));bad["observations"][1]["terminal"]["capacity"]+=1 + save_mechanics(directory,state,bad) + with self.assertRaises(ValueError):probe.audit_mechanics(directory,case) + bad=json.loads(json.dumps(result));bad["geometry_checks"][1]["window_size"][1]+=1 + save_mechanics(directory,state,bad) + with self.assertRaises(ValueError):probe.audit_mechanics(directory,case) + bad=json.loads(json.dumps(state));bad["naws_history"][-1]=[90,20] + save_mechanics(directory,bad,result) + with self.assertRaises(ValueError):probe.audit_mechanics(directory,case) + save_mechanics(directory,state,result);(directory/"ack-0").unlink() + with self.assertRaises((ValueError,FileNotFoundError)):probe.audit_mechanics(directory,case) + + def test_archived_dependencies_are_unchanged_and_case_namespaces_isolated(self): + before=probe.baseline.phase_records() + a,b=probe.peer_class("dual-axis-retention"),probe.peer_class("dual-axis-retention") + self.assertEqual(len(a._serve.__globals__["phases"](20,91)),3) + self.assertEqual(len(b._serve.__globals__["phases"](20,91)),3) + self.assertEqual(probe.baseline.phase_records(),before) + for case in probe.CASES: + compile(probe.launcher_source(case),"","exec") + self.assertIn(json.dumps(probe.controller_contract(case)),probe.probe_fragment(case)) + + +class DualAxisResizePlanTests(unittest.TestCase): + def setUp(self): + temporary=tempfile.TemporaryDirectory(prefix="resize-dual-axis-tests-");self.addCleanup(temporary.cleanup) + self.root=Path(temporary.name).resolve() + for name in probe.SOURCE_FILES: + target=self.root/name;target.parent.mkdir(parents=True,exist_ok=True);shutil.copyfile(probe.ROOT/name,target) + archive={"schema":probe.common.ARCHIVE_SCHEMA,"source_base_commit":"b"*40, + "source_files_sha256":{name:probe.sha(self.root/name) for name in probe.SOURCE_FILES}} + (self.root/"reference-source.json").write_text(json.dumps(archive)) + self.source=self.root/"original";self.source.mkdir();(self.source/"WindTerm.exe").write_bytes(b"synthetic, not an executable") + for target,name,value in ((probe,"ROOT",self.root),(probe,"EXE_SHA256",probe.sha(self.source/"WindTerm.exe")), + (probe.common,"host_identity",lambda:{"synthetic_host":True}), + (probe.common,"rea_identity",lambda _: {"node_executable":"inert-node"})): + patcher=patch.object(target,name,value);patcher.start();self.addCleanup(patcher.stop) + self.out=self.root/"local/out";self.plan=self.root/"local/plan.json" + + def prepare(self,authorized=False): + return probe.prepare_plan(self.source,self.out,self.root/"rea.cmd",self.plan,"dual-axis-retention",authorized) + + def test_archive_plan_is_offline_and_exactly_ten_source_files(self): + with patch.object(probe.common.subprocess,"check_output",side_effect=AssertionError("No archive Git")): + plan=self.prepare() + self.assertEqual(len(plan["source_files_sha256"]),10) + self.assertEqual(plan["actual_original_runs"],0);self.assertFalse(self.out.exists()) + with self.assertRaisesRegex(ValueError,"not authorized"):probe.run_plan(self.plan) + self.assertFalse(self.plan.with_suffix(".attempted").exists()) + + def test_changed_case_geometry_or_source_is_rejected_before_attempt(self): + plan=self.prepare(True) + for key,value in (("case_id","width-then-height"),("phase_count",99),("required_grids",[]),("phases",[]), + ("source_identity",{"kind":"git-checkout"}),("launcher_source_sha256","0"*64)): + self.plan.write_text(json.dumps({**plan,key:value})) + with self.subTest(key=key),self.assertRaises(ValueError):probe.run_plan(self.plan) + self.assertFalse(self.plan.with_suffix(".attempted").exists()) + + def test_local_boundary_existing_plan_and_existing_attempt_remain_enforced(self): + with self.assertRaisesRegex(ValueError,"local directory"): + probe.prepare_plan(self.source,self.root/"elsewhere",self.root/"rea.cmd",self.plan,"dual-axis-retention") + with patch.object(probe,"launcher_source",return_value="def run(*args):\n pass\n"),patch.object(probe,"audit_mechanics",return_value={}): + self.prepare(True) + with self.assertRaisesRegex(ValueError,"must be new"):self.prepare(True) + # A fake launcher namespace keeps this test independent of target APIs. + self.out.mkdir() + self.out.rmdir() + with patch.object(probe.Path,"write_text",autospec=True) as write_result: + probe.run_plan(self.plan) + write_result.assert_called_once() + with self.assertRaises(FileExistsError):probe.run_plan(self.plan) + + def test_failed_attempt_is_retained_and_cannot_be_replayed(self): + source = "def run(*args):\n raise RuntimeError('synthetic launch failure')\n" + with patch.object(probe,"launcher_source",return_value=source),patch.object(probe,"audit_mechanics") as audit: + self.prepare(True) + with self.assertRaisesRegex(RuntimeError,"synthetic launch failure"): + probe.run_plan(self.plan) + self.assertEqual(self.plan.with_suffix(".attempted").read_text(),probe.ATTEMPT) + self.assertFalse(self.out.exists());audit.assert_not_called() + with self.assertRaises(FileExistsError):probe.run_plan(self.plan) + + +if __name__ == "__main__": + unittest.main() diff --git a/research/ui/test_resize_dual_axis_stages.js b/research/ui/test_resize_dual_axis_stages.js new file mode 100644 index 00000000..9ea10a2a --- /dev/null +++ b/research/ui/test_resize_dual_axis_stages.js @@ -0,0 +1,158 @@ +// Execute the actual combined controller in pure V8: no target, socket or native UI. +'use strict'; +const fs=require('fs'),vm=require('vm'),assert=require('assert/strict'),crypto=require('crypto'); +const original=fs.readFileSync(process.argv[2],'utf8'); +const specs={ + 'dual-axis-retention':{grids:[[91,20],[61,14],[91,20]],labels:['initial-body','dual-shrunk-with-write','dual-restored-unmodified'],resize:[1,2]} +}; +const markerLabels=['HTOP','HARD_A','HARD_B','u','v','w','LOWEND']; +function initialLines(){const a=Array(20).fill('');a[1]='HTOP';a[5]='HARD_A';a[6]='HARD_B';a[11]=' '.repeat(89)+'uvw';a[18]='LOWEND';return a;} +function fixture(caseId='dual-axis-retention'){ + const spec=specs[caseId],contract={case_id:caseId,labels:spec.labels,windows:spec.grids.map(([w,h])=>[w===91?1360:1000,h===20?749:569]), + grids:spec.grids,resize_phases:spec.resize,marker_labels:markerLabels}; + const state={now:0,window:[1360,749],origin:[113,-27],peer:{connected:true,columns:91,rows:20,phase:null,naws_history:[[91,20]]}, + terminal:{connected:true,protocol:'Telnet',capacity:20,line_count:20,lines:initialLines(),markers:[]}}; + const files=new Map(),messages=[],resizes=[],root='/owned/resize-dual-axis'; + const module=name=>({name,path:root+'/app/'+name,base:{add:()=>({readU8:()=>0})}}); + const context={__RUN_DIRECTORY_JSON__:root,uiThread:1,wind:module('WindTerm.exe'),core:module('Qt5Core.dll'),gui:module('Qt5Gui.dll'),widgets:module('Qt5Widgets.dll'), + user:{getExportByName:()=>1},Process:{getCurrentThreadId:()=>1},Date:{now:()=>state.now},send:value=>messages.push(value),setTimeout:()=>1, + File:class{constructor(name){this.name=name;this.data='';}write(value){this.data+=value;}close(){files.set(this.name,{at:state.now,data:this.data});}}, + peerState:()=>state.peer,evalText:source=>{if(state.failTextRead)throw Error('synthetic terminal binding unavailable');return state.textAPI?vm.runInNewContext(source,{window:{activeText:state.textAPI}},{timeout:100}):JSON.stringify(state.terminal);},ready:()=>true, + resize:(_,w,h)=>{state.window=[w,h];resizes.push({at:state.now,size:[w,h]});}, + rect:()=>[...state.origin,state.origin[0]+state.window[0]-1,state.origin[1]+state.window[1]-1], + scan:()=>{},expandAll:()=>{},focus:()=>{},key:()=>{},main:1,tree:2}; + context.Interceptor={attach:(_,callbacks)=>{context.dispatch=callbacks.onEnter;}}; + vm.createContext(context);vm.runInContext(original.replace('__RESIZE_DUAL_AXIS_CASE_JSON__',JSON.stringify(contract)),context,{timeout:1000}); + const tick=now=>{state.now=now;vm.runInContext('dispatch()',context,{timeout:100});}; + const file=name=>files.get(root+'/'+name); + const phase=index=>{ + state.peer.case_id=caseId;state.peer.phase=index;state.peer.phase_label=spec.labels[index];state.peer.frozen_grid=contract.grids[index]; + state.peer.resize_armed=null;state.peer.records??=[]; + state.peer.records[index]={cursor_reply:[19,7],cursor_reply_private:false,response_hex:'1b5b31393b3752',frozen_grid:contract.grids[index]}; + }; + tick(0);tick(100);tick(200);tick(1000);tick(1600);assert(file('fixture-ready')); + const observe=(index,time)=>{ + if(spec.resize.includes(index)){ + assert(file('resize-request-'+index));state.peer.resize_armed=index;tick(time); + state.peer.columns=spec.grids[index][0];state.peer.rows=spec.grids[index][1]; + state.terminal.capacity=state.peer.rows;state.peer.naws_history.push(contract.grids[index]); + tick(time+100);tick(time+700);assert(file('resize-ready-'+index)); + phase(index);tick(time+800);tick(time+1200);assert(file('ack-'+index));return time+1300; + } + phase(index);tick(time);tick(time+400);assert(file('ack-'+index));return time+500; + }; + const through=index=>{let time=1700;for(let i=0;i<=index;i++)time=observe(i,time);return time;}; + return {state,tick,file,phase,observe,through,resizes,messages,spec,contract}; +} +const cases=[];function test(name,action){cases.push({name,action});} +test('fixed dual-axis schedule records all3 states and checks both restorations',()=>{ + let total=0; + for(const caseId of Object.keys(specs)){ + const h=fixture(caseId),last=h.spec.grids.length-1,time=h.through(last);h.state.peer.complete=true;h.tick(time); + const r=JSON.parse(h.file('resize-dual-axis-result.json').data); + assert.equal(r.status,'observed-fixed-resize-dual-axis-states');assert.equal(r.observations.length,last+1); + assert.deepEqual(r.observations.map(o=>o.frozen_grid),h.spec.grids); + assert.deepEqual(h.resizes.map(o=>o.size),h.contract.windows);total+=r.observations.length; + } + assert.equal(total,3); +}); +test('all changes wait for prior observation acknowledgment',()=>{ + for(const caseId of Object.keys(specs)){ + const h=fixture(caseId);let time=h.through(0); + for(let i=1;i{ + for(const bad of ['missing','duplicate','DSR']){ + const h=fixture();h.phase(0); + if(bad==='missing')h.state.terminal.lines[1]=''; + else if(bad==='duplicate')h.state.terminal.lines[0]='HTOP'; + else h.state.peer.records[0].cursor_reply=[18,7]; + h.tick(1700);h.tick(2100);assert.equal(h.file('ack-0'),undefined); + assert.equal(h.file('resize-request-1'),undefined);assert.equal(JSON.parse(h.file('resize-dual-axis-result.json').data).status,'failed'); + } +}); +test('later loss and DSR outside 14-row viewport remain observations',()=>{ + const h=fixture();let time=h.through(0);h.state.terminal.lines=[''];h.state.terminal.line_count=1;h.observe(1,time); + const r=h.messages.filter(x=>x.kind==='resize_dual_axis_phase')[1];assert.deepEqual(r.cursor_reply,[19,7]);assert.deepEqual(r.frozen_grid,[61,14]); +}); +test('both width and height NAWS changes reset all 600ms of stability',()=>{ + for(const index of [1,2]){ + const h=fixture(),time=h.through(index-1);h.state.peer.resize_armed=index;h.tick(time); + const target=h.spec.grids[index],prior=h.spec.grids[index-1]; + h.state.peer.columns=target[0];h.state.peer.rows=target[1];h.state.terminal.capacity=target[1];h.state.peer.naws_history.push(target);h.tick(time+100); + const middle=target.map((n,axis)=>Math.floor((n+prior[axis])/2));h.state.peer.naws_history.push(middle,target);h.tick(time+650);h.tick(time+700); + assert.equal(h.file('resize-ready-'+index),undefined);h.tick(time+1250);assert(h.file('resize-ready-'+index)); + } +}); +test('post-input document changes restart the full 400ms stability interval',()=>{ + const h=fixture();h.phase(0);h.tick(1700);h.tick(2000);assert.equal(h.file('ack-0'),undefined); + h.state.terminal.top=1;h.tick(2050);h.tick(2100);assert.equal(h.file('ack-0'),undefined); + h.tick(2449);assert.equal(h.file('ack-0'),undefined);h.tick(2450);assert(h.file('ack-0')); +}); +test('initial sentinels are checked after bounded observation stability',()=>{ + const h=fixture();h.phase(0);h.state.terminal.lines[1]='';h.tick(1700);assert.equal(h.file('resize-dual-axis-result.json'),undefined); + h.state.terminal.lines[1]='HTOP';h.tick(1900);h.tick(2299);assert.equal(h.file('ack-0'),undefined); + h.tick(2300);assert(h.file('ack-0')); +}); +test('fixed deadline rejects missing input without extending the schedule',()=>{ + const h=fixture();h.tick(41001);assert.equal(h.file('ack-0'),undefined); + const result=JSON.parse(h.file('resize-dual-axis-result.json').data);assert.equal(result.status,'failed');assert.match(result.error,/bounded scenario deadline/); +}); +test('wrong actual endpoint is saved before prediction failure without calibration',()=>{ + for(const bad of ['grid','capacity','geometry']){ + const h=fixture(),time=h.through(0);h.state.peer.resize_armed=1;h.tick(time); + h.state.peer.columns=61;h.state.peer.rows=bad==='grid'?13:14;h.state.terminal.capacity=bad==='capacity'?20:h.state.peer.rows; + h.state.peer.naws_history.push([61,h.state.peer.rows]);if(bad==='geometry')h.state.window[1]++; + h.tick(time+100);h.tick(time+700);h.tick(time+1500);assert.equal(h.file('resize-ready-1'),undefined); + if(bad==='capacity')h.tick(41001); + const result=JSON.parse(h.file('resize-dual-axis-result.json').data);assert.equal(result.status,'failed'); + assert.deepEqual(result.failure_geometry.requested_window,[1000,569]); + assert.deepEqual(result.failure_geometry.window_size,[1000,bad==='geometry'?570:569]); + assert.deepEqual(result.failure_geometry.observed_grid,[61,bad==='grid'?13:14]); + assert.equal(result.failure_geometry.terminal_capacity,bad==='capacity'?20:h.state.peer.rows); + if(bad!=='capacity'){ + assert.equal(result.geometry_checks.length,2); + assert.deepEqual(result.geometry_checks[1].observed_grid,result.failure_geometry.observed_grid); + assert.equal(result.geometry_checks[1].terminal_capacity,result.failure_geometry.terminal_capacity); + } + assert.equal(h.resizes.length,2); + } +}); +test('peer or phase errors retain bounded actual geometry and do not advance',()=>{ + for(const bad of ['peer','case','label']){ + const h=fixture();h.phase(0);if(bad==='case')h.state.peer.case_id='wrong';else if(bad==='label')h.state.peer.phase_label='wrong';else h.state.peer.error='synthetic';h.tick(1700); + assert.equal(h.file('ack-0'),undefined);const result=JSON.parse(h.file('resize-dual-axis-result.json').data);assert.equal(result.status,'failed'); + assert.deepEqual(result.failure_geometry.window_size,[1360,749]);assert.deepEqual(result.failure_geometry.observed_grid,[91,20]); + } +}); +test('failed terminal binding cannot erase independent window and NAWS evidence',()=>{ + const h=fixture(),time=h.through(0);h.state.peer.resize_armed=1;h.tick(time); + h.state.peer.columns=61;h.state.peer.rows=14;h.state.terminal.capacity=14;h.state.peer.naws_history.push([61,14]);h.state.failTextRead=true;h.tick(time+100); + const result=JSON.parse(h.file('resize-dual-axis-result.json').data);assert.equal(result.status,'failed'); + assert.deepEqual(result.failure_geometry.window_size,[1000,569]); + assert.deepEqual(result.failure_geometry.observed_grid,[61,14]);assert.equal(result.failure_geometry.terminal_capacity,null); + assert.equal(h.file('resize-ready-1'),undefined);assert.equal(h.resizes.length,2); +}); +test('missing final observation cannot become completion',()=>{ + const h=fixture(),time=h.through(1);h.state.peer.complete=true;h.tick(time); + assert.equal(JSON.parse(h.file('resize-dual-axis-result.json').data).status,'failed');assert.equal(h.file('ack-2'),undefined); +}); +test('actual text sampler records ASCII API offsets separately from pixels',()=>{ + const h=fixture(),lines=initialLines(); + h.state.textAPI={isConnected:true,protocolName:'Telnet',visibleLineCount:20,screenLineCapacity:20,topLine:null,scrollTop:0, + caretLine:18,caretColumn:6,caretPosition:1806,lineText:i=>lines[i],lineStart:i=>i*100, + visibleRectFromPosition:p=>({x:(p%100)*8,y:Math.floor(p/100)*18,width:8,height:18})}; + h.through(0);const markers=h.messages.find(x=>x.kind==='resize_dual_axis_phase').terminal.markers; + const u=markers.find(x=>x.label==='u');assert.equal(u.column,89);assert.equal(u.line_start_api,1100);assert.equal(u.position,1189); + assert.deepEqual(JSON.parse(JSON.stringify(u.rect)),{x:712,y:198,width:8,height:18}); +}); +const results=[];for(const entry of cases){try{entry.action();results.push({case:entry.name,status:'passed'});} + catch(error){results.push({case:entry.name,status:'failed',error:String(error.message)});}} +const failed=results.filter(v=>v.status==='failed').length; +process.stdout.write(JSON.stringify({schema:'termforge-resize-dual-axis-controller-test-v1',node_version:process.version, + actual_source_sha256:crypto.createHash('sha256').update(original).digest('hex'),case_count:results.length,failed, + child_processes_spawned:0,sockets_opened:0,target_runs:0,results})+'\n');process.exitCode=failed?1:0;