diff --git a/.github/workflows/alpha-release.yml b/.github/workflows/alpha-release.yml index 6b1970c4..7961d352 100644 --- a/.github/workflows/alpha-release.yml +++ b/.github/workflows/alpha-release.yml @@ -58,6 +58,9 @@ jobs: - name: Build release candidate and test helpers timeout-minutes: 10 run: cmake --build build-alpha --config Release --parallel 2 + - name: Prepare fixed hosted local shell fixture + timeout-minutes: 7 + run: python -m tools.prepare_hosted_shells --out local/alpha/hosted-shells - name: Require complete CTest including actual system clipboard id: ctest timeout-minutes: 20 @@ -182,6 +185,7 @@ jobs: dist/build-info.json local/alpha/smoke/validation.json local/alpha/test-summary.json + local/alpha/hosted-shells/summary.json - name: Preserve bounded public package diagnostics on failure if: failure() uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 @@ -192,6 +196,7 @@ jobs: path: | local/alpha/smoke/validation.json local/alpha/test-summary.json + local/alpha/hosted-shells/summary.json - name: Preserve explicit system workflow evidence allowlist if: always() uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b6fa1705..3dd2f137 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,6 +37,19 @@ jobs: run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release - name: Build run: cmake --build build --config Release --parallel 2 + - name: Verify shell preparation guards and oracles + run: python -m unittest tools.test_prepare_hosted_shells tools.test_owned_job_cleanup -v + - name: Prepare hosted Windows shell prerequisites + if: runner.os == 'Windows' + run: python -m tools.prepare_hosted_shells --out local/hosted-shells + - name: Preserve sanitized shell preparation result + if: always() && runner.os == 'Windows' + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: hosted-shell-preparation + retention-days: 14 + if-no-files-found: warn + path: local/hosted-shells/summary.json - name: Test env: TERMFORGE_REQUIRE_SYSTEM_CLIPBOARD: ${{ runner.os == 'Windows' && '1' || '0' }} @@ -55,6 +68,7 @@ jobs: build/test-artifacts/selection-context/ build/test-artifacts/text-actions/ build/test-artifacts/raw-console-receiver/ + build/test-artifacts/conpty-ctrl-c/run-*/**/sample.json build/test-artifacts/workflow-fixture/run-*/validation.json build/test-artifacts/workflow-fixture/run-*/stdout.json build/test-artifacts/workflow-fixture/run-*/cases/*/capture.bin @@ -82,6 +96,7 @@ jobs: build/test-artifacts/workflow-sessions/run-*/fixtures/*/capture.bin.workflow.json build/test-artifacts/workflow-sessions/run-*/fixtures/*/capture.bin.receiver.json build/test-artifacts/workflow-sessions/run-*/fixtures/*/capture.bin.progress.jsonl + !build/test-artifacts/**/_private/** - name: Full ASCII state replay run: python -m experiments.core.run --out build/test-artifacts/replay-ascii - name: Full Unicode state replay @@ -108,3 +123,4 @@ jobs: !build/test-artifacts/workflow-sessions/**/*.identity !build/test-artifacts/workflow-sessions/**/sample.json !build/test-artifacts/**/_resources-private/** + !build/test-artifacts/**/_private/** diff --git a/.github/workflows/core-candidate.yml b/.github/workflows/core-candidate.yml index d4fc000a..09070262 100644 --- a/.github/workflows/core-candidate.yml +++ b/.github/workflows/core-candidate.yml @@ -7,6 +7,7 @@ on: - 'experiments/core/**' - 'experiments/conpty/**' - 'experiments/shell/**' + - 'experiments/presentation/**' - 'experiments/differential/**' - 'experiments/measure/**' - 'experiments/candidates/alacritty/**' @@ -17,6 +18,7 @@ on: - 'experiments/core/**' - 'experiments/conpty/**' - 'experiments/shell/**' + - 'experiments/presentation/**' - 'experiments/differential/**' - 'experiments/measure/**' - 'experiments/candidates/alacritty/**' @@ -81,6 +83,9 @@ jobs: - name: Build and test candidate shell on Windows if: runner.os == 'Windows' run: cmake --build build --config Release --target shell_test_launcher && ctest --test-dir build -C Release -R candidate_shell_tui --output-on-failure + - name: Verify empty-title GUI and UTF-8 conversion on Windows + if: runner.os == 'Windows' + run: cmake --build build --config Release --target candidate_title_test renderer_metrics_test && ctest --test-dir build -C Release -R "^(candidate_title|renderer_metrics)$" --no-tests=error --output-on-failure - name: Build real application capture on Windows if: runner.os == 'Windows' run: cmake --build build --config Release --target real_app_capture @@ -133,3 +138,4 @@ jobs: build/test-artifacts/candidate-fuzz/ build/test-artifacts/measure-compare/ build/test-artifacts/measure-interactive/ + !build/test-artifacts/**/_private/** diff --git a/CMakeLists.txt b/CMakeLists.txt index 02bed2b0..d5608110 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -104,6 +104,9 @@ if(WIN32) target_link_libraries(core_measure PRIVATE Psapi) add_subdirectory(experiments/receive) add_subdirectory(experiments/conpty) + add_executable(conpty_ctrl_c_probe experiments/conpty/ctrl_c_probe.cpp) + termforge_warnings(conpty_ctrl_c_probe) + target_compile_definitions(conpty_ctrl_c_probe PRIVATE NOMINMAX WIN32_LEAN_AND_MEAN _WIN32_WINNT=0x0A00) add_executable(presentation_probe experiments/presentation/presentation.cpp) termforge_warnings(presentation_probe) target_compile_definitions(presentation_probe PRIVATE NOMINMAX WIN32_LEAN_AND_MEAN UNICODE _UNICODE) @@ -205,6 +208,15 @@ if(BUILD_TESTING) target_link_libraries(runtime_observation_test PRIVATE termforge_core d2d1 dwrite gdi32 user32 imm32 ole32 oleaut32 uuid bcrypt) if(TERMFORGE_BUILD_CANDIDATE_ADAPTER) termforge_candidate_shell(runtime_observation_test) + add_executable(candidate_title_test experiments/shell/candidate_title_test.cpp experiments/shell/shell_launch.cpp) + termforge_warnings(candidate_title_test) + target_compile_definitions(candidate_title_test PRIVATE NOMINMAX WIN32_LEAN_AND_MEAN UNICODE _UNICODE) + target_link_libraries(candidate_title_test PRIVATE termforge_core d2d1 dwrite gdi32 user32 imm32 ole32 oleaut32 uuid bcrypt) + termforge_candidate_shell(candidate_title_test) + add_dependencies(candidate_title_test termforge_shell_assets) + add_test(NAME candidate_title COMMAND candidate_title_test) + set_tests_properties(candidate_title PROPERTIES TIMEOUT 35 + ENVIRONMENT "TERMFORGE_TEST_ARTIFACTS=${PROJECT_BINARY_DIR}/test-artifacts") endif() add_dependencies(runtime_observation_test termforge_shell_assets) add_test(NAME runtime_observation COMMAND runtime_observation_test) @@ -244,6 +256,9 @@ if(BUILD_TESTING) target_link_libraries(workspace_state_test PRIVATE termforge_core) termforge_warnings(workspace_state_test) add_test(NAME workspace_state COMMAND workspace_state_test) + add_executable(transport_completion_test experiments/shell/transport_completion_test.cpp) + termforge_warnings(transport_completion_test) + add_test(NAME transport_completion COMMAND transport_completion_test) if(TERMFORGE_BUILD_CANDIDATE_ADAPTER) add_test(NAME candidate_core COMMAND candidate_core_test) endif() @@ -286,10 +301,12 @@ if(BUILD_TESTING) set_tests_properties(workflow_system_content PROPERTIES TIMEOUT 30) termforge_add_python_test(workflow_system_runner -m unittest research.ime.test_workflow_system_runner -v) set_tests_properties(workflow_system_runner PROPERTIES TIMEOUT 30) + termforge_add_python_test(copy_error_frame -m unittest experiments.shell.test_copy_error_frame -v) if(WIN32) termforge_add_python_test(research_guards -m unittest discover -s research/tests -v) termforge_add_python_test(receive -m unittest experiments.receive.test_receive -v) termforge_add_python_test(conpty -m unittest experiments.conpty.test_conpty -v) + termforge_add_python_test(conpty_ctrl_c -m unittest experiments.conpty.test_ctrl_c -v) termforge_add_python_test(presentation -m unittest experiments.presentation.test_presentation -v) termforge_add_python_test(shell -m unittest experiments.shell.test_shell -v) termforge_add_python_test(selection_bytes -m unittest experiments.shell.test_selection_bytes -v) diff --git a/docs/EVIDENCE.md b/docs/EVIDENCE.md index a361438c..fa7e8d69 100644 --- a/docs/EVIDENCE.md +++ b/docs/EVIDENCE.md @@ -8,6 +8,12 @@ | 结论 | 实现/命令入口 | 已有结果与限制 | |---|---|---| +| 拒绝负例的读取前停止(#46 夹具修正) | [原记录、反事实及 fresh 验证](../experiments/shell/results/2026-10-26-receiver-stop/validation.json)、[公开空数据夹具](../experiments/shell/fixtures/rejected-order-before-read.json) | F2 预期拒绝可先于首读完成停止;旧 oracle 误要求至少读过并成功取消一次。仅拒绝路径接受已证明的零读/空数据/停止并 join/无错误状态,正常读取门槛保留。原记录及 fresh 再现的同一零读字节在旧判定失败、新判定通过;12 项纯测试含字段反例,原模块14/14通过,native EXE身份未变且所有Job0。原完整回归红保留,#44 的退出根因仍未知 | +| 候选 GUI 合法空标题(#45) | [真实 GUI 前后对照](../experiments/shell/results/2026-10-26-empty-title/validation.json)、[转换入口](../experiments/presentation/render.hpp)、[持续回归](../experiments/shell/candidate_title_test.cpp) | 自有 console 实际发出完整空 OSC 标题后,旧转换抛出 invalid text;空 UTF-8 合法转换为空 UTF-16 后,两 fresh GUI 存活且正常回收。混合 Unicode 与四类非法非空反例保留,旧二进制反事实仍失败。首次未观察到空 OSC 的尝试不算复现;旧候选 normal-1 的异常调用点和载荷未知,不能由新复现回填其根因;不是完整 R2 或 WindTerm 等价验收 | +| 工作区恢复的运输完成端点(#38 夹具修正) | [限定合同](research/workspace-restore-transport-oracle.md)、[原失败及默认/候选验证](../experiments/shell/results/2026-10-26-workspace-transport-oracle/validation.json) | 完整 OSC 标题重复标记使旧 rfind 取错位置;真实 ConPTY 另有完整输出后以 column-one CUP 返回提示符的分支。独立判定保留 UTF-8、排除标题和命令回显,32 条断言及默认/候选原场景均通过;12 个样本的哈希、三会话关闭、Job0 与 desktop 删除另经复核。原期限/负载/恢复断言保留,原失败不回填;这是测试端点修正,不是产品恢复故障修复 | +| 持续 cmd 输出与原始 Ctrl-C 回归(#36 未关闭) | [受监督入口](../experiments/conpty/README.md#持续-cmd-输出与-ctrlc-独立回归)、[三次 fresh 公开记录及本地字节复核](../experiments/conpty/results/2026-10-26-ctrl-c-retest/validation.json) | 完整一百万次循环、一次洪流 ETX、立即后续命令、原五秒期限及退出 37;三次原始输入/输出哈希、独立标记与 Job0 已本地重算。cmd 固定版本 9278、conhost 9444;旧失败与本次成功原因未知,未修改产品 Ctrl-C 路径。原始运输仅本地保留,公开哈希不能单独复核全部原始断言;未验原生控制处理函数、独立构建或性能,不能关闭 #36 | +| controller B 正常结束后 cmd 退出未验收(#44) | [原失败脱敏记录](../experiments/shell/results/2026-10-26-controller-exit/validation.json)、[待补诊断及关闭合同](https://github.com/JavaZeroo/termforge/issues/44) | child 精确接收32字节、正常退出0且已join,内外Job0;cmd仅记录close后的0xC000013A,wait_exit布尔和close前状态未知。随后同合同两轮通过不能覆盖原失败;probe未链接候选适配器,根因与候选关联均未证明,继续阻塞#38 | +| 候选文字观测与回显端点(#38 夹具修正) | [同源码前后对照及反事实](../experiments/shell/results/2026-10-26-native-observation/validation.json)、[标准文本投影](../experiments/core/text_projection.hpp) | 候选零宽spacer原被原生观测拼成空格,四类场景稳定卡在中文行;统一project_line后原四类各3fresh通过。运行观测另捕获写完但回显未到时先全选,迟到12字节使选区合法失效;原同一次五秒等待加严为完整实时回显后选择,5fresh/10个窗口通过,原assert保留。原no-echo对照的间歇红与后续绿均保留,因果解释按时序和选区合同推断;不宣称产品选择策略修复、全候选等价或完整R2验收 | | R1完整文字工作流与总出口(限定) | [同次系统组合及独立审计](../experiments/shell/results/2026-10-24-r1-system-workflow/validation.json)、[同源码既有回归](../experiments/shell/results/2026-10-24-r1-system-workflow/regression.json)、[协议与复现](../experiments/shell/WORKFLOW.md) | `ce9b24d`;[主CI 40045678550](https://github.com/JavaZeroo/termforge/actions/runs/40045678550) Windows38/Ubuntu24,本地38/38且内部8项skip另列;[显式verify 40045686776](https://github.com/JavaZeroo/termforge/actions/runs/40045686776)实际八会话两fresh,各26主点/13系统点/28动作、409/512观测、18实例回收。独立核191文件、58源码/5二进制身份、B原件51→57→58及目标隔离、两次actual revision绑定滚动、8夹测/19阶段窗/5短驻留,21真实产物反例全拒、两个最终帧已查看;同源码E2两轮与旧12组24轮/五组IME另审。结合R1-01–07满足限定总出口;无EXE独立重哈希/源到二进制证明,main内部skip未知,presented_done=null,不构成日常版、长期无泄漏或WindTerm性能等价 | | R1 八会话 E2 数据工作流与运行观测 | [协议/自动入口](../experiments/shell/WORKFLOW.md)、[固定源码审计](../experiments/shell/results/2026-10-24-r1-eight-sessions/validation.json)、[既有能力复验](../experiments/shell/results/2026-10-24-r1-eight-sessions/regression.json) | 固定源码与独立复核:`72fe231` / [主CI](https://github.com/JavaZeroo/termforge/actions/runs/40040980384),本地及同源码hosted各两轮。本地35/35(内部8项skip),主CI Windows35/Ubuntu21;[同源码真实IME复验](https://github.com/JavaZeroo/termforge/actions/runs/40040994121)为五模式28检查点。验收范围为每轮26检查点、18会话身份、235/256有界观测,覆盖五路洪流、阅读/Find/输入/缩放/淘汰、5次8→6→8和合成粘贴取消/关闭;实际窗口创建、字节/日志、worker/Job及非输入desktop回收分别复核。此历史data-only批次不等于同次真实CF/IME;完整组合见本表最新总验收 | | E2 launcher-tree 资源接缝(历史批次) | [复用采样器的adapter](../experiments/shell/workflow_resources.py)、[同一固定源码审计](../experiments/shell/results/2026-10-24-r1-eight-sessions/validation.json) | 首个实际launcher样本在GO前取得;原始采样与脱敏observations逐条比对,公开记录可离线重算同时样本内存/handles峰值和CPU,sampler另组。scope为launcher-tree,不是UI-root;短命后代可能漏采、unknown保留。该历史批次观测成本显著且有调度缺口,100ms仅为请求间隔;其phase_windows=null,不能追溯补出校准。新校准与同次工作流见最新行,均不证明低扰动、性能或长期无泄漏 | @@ -42,6 +48,9 @@ | termforge.2 C++ 拥有值适配与候选产品接入 | [接口边界](../experiments/core/CANDIDATE.md)、[验证记录](../experiments/core/results/2026-10-25-candidate-adapter/validation.json)、[v5 共同矩阵](../experiments/differential/results/2026-10-25-candidate-common-v5/summary.json) | 142 单元+1 doc、24 差分、15 计量及 candidate_core/runtime_observation 通过;真实 Vim 两轮进入/编辑/60×12 resize/退出写盘、主屏恢复及真实光标回复经有界 InputQueue 验证。默认构建保留自研回退;完整候选 CTest 为 37/42,#27/#29/#36 和实际呈现未关闭;#37 的独立 Windows CI 验收另列 | | 真实备用屏切换(#27 限定切片) | [五阶段原生快照与候选验证](../experiments/shell/results/2026-10-26-alternate-screen/validation.json)、[候选接口](../experiments/core/CANDIDATE.md) | `7b447b4` 的真实辅助进程执行两次 1049h/1049l,独立核对源 VT、五阶段 `CHAR_INFO`、文字、样式和光标;两轮原生主屏恢复一致,候选历史无备用屏文字。只覆盖固定切换/恢复/历史隔离,origin/wrap、独立 margin、resize、完整 #27/#28 和实际呈现未验收 | | 局部滚动区域(#27 限定切片) | [原生阶段快照、独立 transport 重放与候选验证](../experiments/shell/results/2026-10-26-scroll-region/validation.json)、[候选接口](../experiments/core/CANDIDATE.md) | `d815e80` 的真实辅助进程设置 DECSTBM 并在区域底部换行;区域外保持、C/D 位于正确区域、A/B 不混入主历史。源 VT、三阶段 `CHAR_INFO` 与 `pyte==0.8.2` transport 重放分别核对,不强制原样回显控制序列。origin/wrap、独立 margin、resize、完整 #27/#28 和实际呈现未验收 | +| 每缓冲区 margin 与宿主差异(#27/#41) | [源 VT、原生快照与候选验证](../experiments/shell/results/2026-10-26-margin-state/validation.json)、[termforge.3 共同矩阵](../experiments/differential/results/2026-10-26-candidate-common-v6/summary.json) | 主屏 `2;3r`、备用屏 `4;5r` 的公开 feed 整段/逐字节恢复及 resize/RIS 回归通过;真实 ConPTY 只恢复主屏内容/样式/光标,未恢复主屏 margin,两轮观察均为 `margins_per_buffer_verified=false`,由 #41 保留。[候选 CI 40261449120](https://github.com/JavaZeroo/termforge/actions/runs/40261449120) Windows/Ubuntu 通过。WindTerm 静态 screen 归属不证明运行时恢复;origin/wrap、完整 resize、#27/#28 和 R2 出口未验收 | +| More 尾部 ECH 与捕获时序(#42 限定修复) | [合同及来源](research/more-capture-ech.md)、[修复前后与同输入对照](../experiments/differential/results/2026-10-26-more-ech/validation.json)、[历史触发记录](../experiments/shell/results/2026-10-26-margin-state/validation.json) | 原因分开确认:默认 Recovery 未实现 ECH;旧捕获在完整提示及 reader EOF 前取快照。360/366 字节原件与哈希保留,366B 修前 whole/byte 真失败、修后同输入与候选/pyte 一致;Recovery33项、真实应用/故障8项通过。新完整382B保留全部尾部,单q、退出0、EOF109、无强制取消与Job0分别核对。候选 pending-wrap 仍有明确差异,未自动登记豁免;完整 VT/GUI、WindTerm 等价和 R2 出口未验收 | +| 短窗口选区夹具与 hosted 四 shell 前置(#38) | [固定锚点回归](../experiments/shell/test_shell.py)、[准备入口](../tools/prepare_hosted_shells.py)、[夹具收尾合同](research/hosted-wsl-fixture-cleanup.md)、[云端结果与剩余阻塞](https://github.com/JavaZeroo/termforge/issues/38) | 681/860客户区高度使用相同内容、拖选及淘汰断言;固定Ubuntu缺失时才导入经SHA核验的官方rootfs,两次真实启动分别检查。仅指定一次性VM,精确自有Job的夹具强制收尾与产品自然退出分列;镜像身份未知保持未知,归属/退出或回收未知仍失败。guard/oracle与实际自有Job检查17项通过,不在本机启动WSL;#40原超时根因仍未知,单次CI通过不关闭父项 | | ConPTY resize 重放、历史与 Find/选区(#29) | [限定合同](adr/0005-physical-row-resize.md)、[重放与历史验证](../experiments/core/results/2026-10-26-resize-replay/validation.json)、[候选接口](../experiments/core/CANDIDATE.md) | 861 字节固定重放的 whole/逐字节身份、Find 增量收敛、整篇/局部选区、连续同步、历史上限/清空均有检查;真实文字动作、八会话、Vim 和两组选择场景通过,候选全套 41/42,shell 的 #36/#38 保持开放。WindTerm 44 个定向函数的限定静态证据不冒充运行时比较;无完整 reflow、真实 DPI、呈现或性能结论 | | 复杂文字与窄屏 ConPTY 尾部 | [源/控制台/布局验证](../experiments/shell/results/2026-10-26-unicode-workflow/validation.json)、[只读调查入口](../research/README.md) | `730b5cd` 分别核对源字节、真实控制台 `CHAR_INFO`、运输尾部与独立 `pyte==0.8.2` 布局;默认/候选常规两轮及窄屏完整 F 场景通过,原文分解形式保留,pyte 的 NFC 不冒充原文。完整默认 39/40、候选 41/42,#36 和候选早期输入/关闭仍未闭合;#30 的完整 Unicode/IME、真实 DPI、呈现和 WindTerm 等价均未验收 | | R2 复杂标量序列输入(#30 限定切片) | [真实键盘接收验证](../experiments/shell/results/2026-10-26-unicode-keyboard/validation.json)、[场景与复现](../experiments/shell/README.md) | `fe02400` 的固定 ZWJ/keycap 样本经 WM_CHAR、ConPTY 与真实 `ReadConsoleW` 接收端逐字节一致,默认/候选各两轮。只验收输入保真,不证明字素绘制、选择、完整 IME、应用键盘模式或 WindTerm 等价 | diff --git a/docs/FEATURES.md b/docs/FEATURES.md index f1d248e1..d601b0b9 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -12,14 +12,14 @@ | 能力 | 当前状态与边界 | 目标差距 | 路标 | |---|---|---|---| -| 本地命令输入/输出 | **部分;当前有未解决回归**:Windows cmd → ConPTY → 有界队列 → Recovery → HWND;普通字符、中文字符消息、回车、退格、四方向键和退出回收仍有限验证。R1 历史证据覆盖 Ctrl-C,但当前 Windows 11 上 normal/resize 的持续 `cmd for /L` 中断超时由 [#36](https://github.com/JavaZeroo/termforge/issues/36) 保留 | ETX、实际控制事件和实际 Ctrl+C 键记录均未使当前持续输出场景恢复;不能沿用旧证据宣称当前回归通过。中文消息不等于 IME;完整键盘模式与其他 shell 未验收 | R1 / R2 | +| 本地命令输入/输出 | **部分;历史回归原因未明**:Windows cmd → ConPTY → 有界队列 → Recovery → HWND;普通字符、中文字符消息、回车、退格、四方向键和退出回收仍有限验证。[#36](https://github.com/JavaZeroo/termforge/issues/36) 保留持续 `cmd for /L` 的 normal/resize 历史超时;本次未修改产品代码时各十轮通过,旧探针原二进制也通过;[固定回归入口](../experiments/conpty/README.md#持续-cmd-输出与-ctrlc-独立回归)保留完整负载、单次 ETX、原始字节与回收 | 旧失败与本次成功的因果差异未知,原生控制处理函数交付仍未独立验收;不能把未复现宣称为产品修复。中文消息不等于 IME;完整键盘模式与其他 shell 未验收 | R1 / R2 | | 历史浏览 | **已验证(有限)**:100 行有界物理行历史、只读投影、滚轮/翻页/原生滚动条与每会话阅读锚点;编号内容、隐藏输出、淘汰、缩放与回收已有[重复回归证据](../experiments/shell/results/2026-10-23-r1-foundations/validation.json),符合 [#12](https://github.com/JavaZeroo/termforge/issues/12) 限定合同 | 基础动作对照见统一文字动作;完整组合见已验收的 #18,reflow、外部核心替换与大规模历史性能未验收 | R1;扩展 R2 / R4 | | 选择与复制 | **已验证(有限)**:屏幕/100行有界历史的拖选、词/行、全选、自动滚动及每会话高亮;菜单/快捷键和实际 CF_UNICODETEXT 精确往返。[最终系统审计](../experiments/shell/results/2026-10-24-r1-selection-accepted/validation.json)覆盖占用反馈/保持/重试、live覆盖失效、同组标签、原生rename EDIT复制与关闭后目标隔离;[带样式尾空格/软硬行/受控DPI](../experiments/shell/results/2026-10-24-r1-styled-system-copy/validation.json)另有两轮系统证据 | 普通尾空格经 ConPTY 规范化后不保留;显式空白端点策略未对照,词规则与容量为本项目策略。限定动作与参考差异见统一文字动作,完整组合见 #18 的独立验收;矩形选择归 R4,受控 DPI 不等于真实跨显示器 | R1;扩展 R2 / R4 | | 粘贴 | **已验证(有限)**:有界传输、异步读取、确认与目标绑定;正常系统粘贴、占用/超限错误与重试、读取和发送途中取消/关闭、隐藏目标、洪流及精确接收前缀均有重复验证。[最终系统证据](../experiments/shell/results/2026-10-24-r1-clipboard-accepted/validation.json)保留晚发布原始 1418 和完整回收 | 默认 Return 等限定参考动作已在统一文字动作验收,全流程交叉见 #18 的独立验收;bracketed paste/应用模式归 R2,未宣称全部系统或任意负载验收 | R1;应用模式 R2 | | 查找与定位 | **已验证(有限)**:有界增量字面量搜索、原生查找框、方向/循环/高亮、中文、追加与淘汰;持续输出时可显式使用有效局部命中,运行中查询替换/取消/关闭均有[托管两轮独立证据](../experiments/shell/results/2026-10-24-r1-find-accepted/validation.json) | 局部结果不表示全局末端/循环;动作与受控 DPI 交叉见统一文字动作,组合与资源见 #18 的独立验收。正则、跨会话、完整 Unicode caseless 和大历史规模归 R4 | R1 / R4 | | 统一文字动作 | **已验证(有限)**:共用目标/可用状态,实际终端右键/快捷键、原生 Find/rename 编辑、busy/closing/失效选区反馈及受控 DPI/zoom 保持;[最终系统审计](../experiments/shell/results/2026-10-24-r1-actions-accepted/validation.json)与[逐项对照](research/r1-action-comparison.md)覆盖 Find 实际 Ctrl+C/V、中文重选、默认 Return 和会话隔离 | 复杂 busy/closing 参考行为仍未知,采用明确项目策略;完整流程与八会话有限资源已由 #18 另验收,真实跨显示器及完整 WindTerm 等价未验收 | R1 | | 中文 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、隐藏/关闭,以及历史阅读、Find/rename 双 Escape 与回收 | 限定 Windows Server 2025 固定拼音及测试范围;同一八会话组合已由 #18 另验收,真实跨屏、所有输入法、复杂字素绘制/选择及应用模式归 R2/后续矩阵 | R1 / R2 | -| VT 核心 | **部分**:自研核心支持基础光标/擦除、CR/LF/BS/TAB、有限 SGR、有界未知序列恢复;[ADR-0003](adr/0003-core-reference-and-reuse.md)已决定 R2 新能力复用候选 Term/Processor;`0.26.1-dev+termforge.2` [固定补丁](../experiments/candidates/alacritty/PROVENANCE.md)已纳入上游资源修复、ED1/ED2 和稳定行元数据,[共同轨迹](../experiments/differential/results/2026-10-25-candidate-common-v5/summary.json)为 95 次一致;[C++ 适配](../experiments/core/CANDIDATE.md)已在显式候选构建接入 ShellWindow,真实 Vim/resize/光标回复经 ConPTY 和有界 InputQueue 验证;[两次真实备用屏切换](../experiments/shell/results/2026-10-26-alternate-screen/validation.json)的主屏文字/样式恢复、历史隔离和最终光标有独立快照;[局部滚动区域](../experiments/shell/results/2026-10-26-scroll-region/validation.json)的区域外保持与有限历史语义经真实控制台验证 | 默认构建保留自研回退;完整 R1 候选回归与 #36 输入缺口未关闭,插删、完整模式和最终呈现仍未验收。#27 的 origin/wrap、独立 margin 和 resize 交叉未完成;旧平台支持缺口不因路线决定消失 | R2 / R4 | +| VT 核心 | **部分**:自研核心支持基础光标/擦除(含 ECH)、CR/LF/BS/TAB、有限 SGR、有界未知序列恢复;[ADR-0003](adr/0003-core-reference-and-reuse.md)已决定 R2 新能力复用候选 Term/Processor;`0.26.1-dev+termforge.3` [固定补丁](../experiments/candidates/alacritty/PROVENANCE.md)纳入上游资源修复、ED1/ED2、稳定行元数据和每缓冲区滚动区域,[共同轨迹](../experiments/differential/results/2026-10-26-candidate-common-v6/summary.json)为 95 次一致;[C++ 适配](../experiments/core/CANDIDATE.md)已在显式候选构建接入 ShellWindow,真实 Vim/resize/光标回复经 ConPTY 和有界 InputQueue 验证;备用屏切换、局部滚动和[公开源 margin 修复](../experiments/shell/results/2026-10-26-margin-state/validation.json)有独立验证 | 默认构建保留自研回退;完整 R1 候选回归与 #36/#38 未关闭;ECH 的候选 pending-wrap 差异见[限定合同](research/more-capture-ech.md),插删、完整模式和最终呈现仍未验收。#27 的 origin/wrap 和完整 resize 交叉未完成;#41 的真实 ConPTY 主屏 margin 恢复差异未解决,不能把模型切片当作产品 margin 通过;旧平台支持缺口不因路线决定消失 | R2 / R4 | | Unicode 与文字显示 | **部分**:UTF-8、固定 Unicode 15.0 宽度策略、有限组合标记、16 色/粗体、Consolas 逐格绘制;实际 DWrite glyph run 已检查 Consolas、中文 fallback、固定 Segoe UI Emoji/Symbol 与缺字负例;[候选原文保留与 glyph 限定验证](../experiments/shell/results/2026-10-26-unicode-renderer/validation.json)覆盖真实 ConPTY 样本及超宽文字适配 | [#39](https://github.com/JavaZeroo/termforge/issues/39) 的完整 ZWJ/keycap 聚类与宽度合同、完整字素/整形、通用 fallback、真彩色/更多属性、Powerline;WindTerm 的 Unicode 13 声明与本项目策略差异需明确裁决 | R2 / R4 | | resize 与 reflow | **已验证(有限)**:默认回退按 ADR-0005 物理行裁剪;候选在固定 ConPTY 重放、逐字节及连续同步中保持唯一行身份、Find 和选区,按 ID 与完整行键合并有界历史,真实淘汰不恢复,显式清空不复活;[合同与验证](../experiments/core/results/2026-10-26-resize-replay/validation.json)。真实文字动作、八会话及 Vim 60×12 resize 有独立场景 | 仅已识别的 resize 事务;候选重排不同于默认裁剪,不等于完整 reflow、备用屏/滚动区全语义或 WindTerm 等价。缩窄默认不可恢复;真实 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 | 仅声明上述固定入口和隔离目录;任意 profile/参数、通用分屏/排序、全部运行时错误提示及管理员 shell 未完成 | R2;权限场景 R4 | diff --git a/docs/PLAN.md b/docs/PLAN.md index 4e4b082b..bfb2446d 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -12,7 +12,7 @@ ## 当前站位与下一次交付 -**R1 基础文字交互已按限定合同验收,当前进入R2准备。** 固定源码`ce9b24d`的[完整文字工作流及同源码回归](../experiments/shell/results/2026-10-24-r1-system-workflow/validation.json)覆盖同一次真实八会话Copy→Paste→中文输入、后台负载、两次滚动状态/提交端点、校准阶段资源与回收。实际呈现、长期资源稳定和WindTerm性能对照仍未验收。现有空面板、有限VT核心和局部UI取证不能计作完整产品功能。 +**R1 基础文字交互已按限定合同验收,当前推进 R2,尚未达到开发版出口。** 固定源码`ce9b24d`的[完整文字工作流及同源码回归](../experiments/shell/results/2026-10-24-r1-system-workflow/validation.json)覆盖同一次真实八会话Copy→Paste→中文输入、后台负载、两次滚动状态/提交端点、校准阶段资源与回收。实际呈现、长期资源稳定和WindTerm性能对照仍未验收。现有空面板、有限VT核心和局部UI取证不能计作完整产品功能。 下一次可交付结果是:在明确支持的本地shell中可靠运行常见TUI,重启后恢复工作配置,并提供可复建的Windows开发包。平台、架构和声明的最低系统边界已由[#23](https://github.com/JavaZeroo/termforge/issues/23)的[范围台账](../research/windterm-2.7.0-platform-scope.md)冻结;[ADR-0003](adr/0003-core-reference-and-reuse.md)已决定 R2 新终端能力复用候选 Term/Processor,保留自有传输、呈现和自研差分回退。后续按该路线整合,不把路线决定当作产品能力完成。具体工作包见下文R2,执行项统一由[TRACKING](TRACKING.md)指向issues。 diff --git a/docs/TRACKING.md b/docs/TRACKING.md index cf5fa892..578a6b81 100644 --- a/docs/TRACKING.md +++ b/docs/TRACKING.md @@ -43,7 +43,7 @@ R1已按[限定合同](milestones/R1-local-text.md)验收,[完整组合证据] | R2-01 | [#24](https://github.com/JavaZeroo/termforge/issues/24) | 原生候选干净构建及 headless 状态/历史投影 | 无;父项为核心关卡 #20 | | R2-02 | [#25](https://github.com/JavaZeroo/termforge/issues/25) | 共同轨迹、真实应用、六类差异与持续 fuzz 比较 | #24 | | R2-03 | [#26](https://github.com/JavaZeroo/termforge/issues/26) | 候选成本测量与 ADR-0003 核心路线决定 | #23、#25 | -| R2-04 | [#27](https://github.com/JavaZeroo/termforge/issues/27) | 备用屏、滚动区域及关联模式 | #26 | +| R2-04 | [#27](https://github.com/JavaZeroo/termforge/issues/27) | 备用屏、滚动区域及关联模式 | #26;真实 ConPTY margin 差异由子项 [#41](https://github.com/JavaZeroo/termforge/issues/41) 阻塞 | | R2-05 | [#28](https://github.com/JavaZeroo/termforge/issues/28) | 终端回复、输入/输出模式及固定真实 TUI | #27 | | R2-06 | [#29](https://github.com/JavaZeroo/termforge/issues/29) | resize、逻辑行与历史语义 | #26 | | R2-07 | [#30](https://github.com/JavaZeroo/termforge/issues/30) | 复杂 Unicode、IME、键盘与选择语义;完整流式字素聚类与宽度由子项 [#39](https://github.com/JavaZeroo/termforge/issues/39) 承担 | #26、#39(完整聚类与宽度合同) | @@ -55,15 +55,20 @@ R1已按[限定合同](milestones/R1-local-text.md)验收,[完整组合证据] | R1 回归 | [#36](https://github.com/JavaZeroo/termforge/issues/36) | 定位持续 cmd 输出时 ETX 不中断并恢复 normal/resize 与完整 CTest | 无;不得减少负载或仅延长超时 | | Windows 回归 | [#38](https://github.com/JavaZeroo/termforge/issues/38) | 定位工作区、交互及 CI 的真实失败 | 无;#35 的出口依赖,不能以局部几何结果代替修复 | | Windows CI 回归 | [#40](https://github.com/JavaZeroo/termforge/issues/40) | 复核 `ime_inventory` 原 10 秒超时根因,保留 AST/最终进度与 OwnedJob 回收证据 | 无;作为 #38 的子项阻塞 #38,不能用本机或单次 CI 通过冒充修复 | +| ConPTY margin 差异 | [#41](https://github.com/JavaZeroo/termforge/issues/41) | 裁决宿主/传输的每缓冲区 margin,保留原场景和独立实现证据 | #26;作为 #27 的子项阻塞 #27,不能以模型切片或差异观察代替产品验收 | +| More 尾部 ECH 与捕获时序 | [#42](https://github.com/JavaZeroo/termforge/issues/42) | [限定合同](research/more-capture-ech.md):默认 ECH、完整 More 提示、q/退出/真实 EOF;候选 pending-wrap 差异保留给 #27 | 无;#38 的子项,与 margin 修复分开;完整原始输入和旧失败保留,不静默降级为 match | +| controller 正常退出观测缺口 | [#44](https://github.com/JavaZeroo/termforge/issues/44) | 记录 wait_exit 与 close 前 cmd 状态,定位真实 child→cmd/close 转换 | 无;#38 的子项与出口阻塞;后续同合同重跑成功不覆盖原失败 | +| 候选 GUI 合法空标题 | [#45](https://github.com/JavaZeroo/termforge/issues/45) | 修正空 UTF-8 转换,使用真实 console 标题事件验证 GUI 存活,并保留非法非空输入拒绝 | 无;#38 的子项;新最小复现与旧 normal-1 未定位异常分列 | +| 拒绝负例的读取前停止 | [#46](https://github.com/JavaZeroo/termforge/issues/46) | 验证 append-before-output 被拒绝时接收线程可以在首读前正常停止;只接受完整已证明的空输入停止状态 | 无;#38 的子项;与 #44 的 controller 退出问题分别验收,原完整回归失败保留 | ## 从当前状态接着做 以下为选题顺序;各项已做/待做和验收结果只在对应 issue 维护。`ready-for-agent` 表示可以开工,不代表全仓库所有 ready 项优先级相同。 -1. **按已定核心路线整合。** [#23](https://github.com/JavaZeroo/termforge/issues/23) 已冻结[平台、架构与声明的最低系统边界](../research/windterm-2.7.0-platform-scope.md),#24/#25 已完成候选构建与共同轨迹;#26 已形成 [ADR-0003 决定](adr/0003-core-reference-and-reuse.md)。#27 的 termforge.2 已完成兼容补丁和 [C++ 拥有值适配](../experiments/core/CANDIDATE.md),共同矩阵 95 次一致,ID/版本/淘汰、选择/搜索/视口已独立验证;实际输入队列与默认 shell 整合仍待实施。 +1. **按已定核心路线整合。** [#23](https://github.com/JavaZeroo/termforge/issues/23) 已冻结[平台、架构与声明的最低系统边界](../research/windterm-2.7.0-platform-scope.md),#24/#25 已完成候选构建与共同轨迹;#26 已形成 [ADR-0003 决定](adr/0003-core-reference-and-reuse.md)。#27 的 termforge.3 已完成兼容补丁和 [C++ 拥有值适配](../experiments/core/CANDIDATE.md),共同矩阵 95 次一致,ID/版本/淘汰、选择/搜索/视口已独立验证;显式候选 ShellWindow 与有界输入队列已验证,默认构建保留自研回退;origin/wrap、#41 的宿主 margin 和完整 R1 回归仍待完成。 2. **并行推进独立的显示与会话切片。** [#31](https://github.com/JavaZeroo/termforge/issues/31) 已形成[字体/显示合同](milestones/R2-font-display.md),完成实际 glyph/fallback、私有字体度量/缺失资产检查及两台 96-DPI 显示器的[真实往返证据](../experiments/shell/results/2026-10-24-r2-monitor-font/validation.json);不同 DPI 与实际呈现保持 OPEN。[#32](https://github.com/JavaZeroo/termforge/issues/32) 已交付[四种固定本地 shell 生命周期](../experiments/shell/results/2026-10-24-r2-shell-lifecycle/validation.json)及[主区一个真实左右分屏](../experiments/shell/results/2026-10-24-r2-main-split/validation.json);任意嵌套、拖放/搜索/颜色和分屏持久化继续后续。 3. **按依赖整合用户结果。** 核心路线决定后推进 #27–#30,#32 后推进 #33;真实工作流齐备后由 #34 决定产品架构,最终由 #35 验收完整开发版,不能以单项成功替代 #7 出口。 -4. **持续保留R1系统回归。** 基础动作、五组IME与同次八会话真实工作流分开运行,再核原始字节、目标、阶段资源和回收。当前 [#36](https://github.com/JavaZeroo/termforge/issues/36) 保留 normal/resize 的真实失败,[输入机制研究](research/conpty-ctrl-c-input.md)确认 ETX 正确,并把未决点收窄到洪流写入与 conhost 控制分派之间;它是 #35 的出口前置,不能用本机跳过或旧证据替代修复后的新源码验收。 +4. **持续保留R1系统回归。** 基础动作、五组IME与同次八会话真实工作流分开运行,再核原始字节、目标、阶段资源和回收。[#36](https://github.com/JavaZeroo/termforge/issues/36) 保留 normal/resize 的历史失败;本次未修改产品代码的复验和旧探针原二进制均通过,原因仍未知。[受监督原始 ConPTY 回归](../experiments/conpty/README.md#持续-cmd-输出与-ctrlc-独立回归)固定完整负载、单次 ETX、后续命令与回收,未独立验收原生处理函数;它是 #35 的出口前置,不能把未复现写成已修复。 5. **保持性能与UI调查并行。** 正式WindTerm基线、观测扰动、实际呈现和长期稳定继续按下表推进;真实跨屏、UI其余状态和空面板能力仍按后续合同交付。停止的手工批次不自动补跑。 未提交草稿和未完成的系统验收均不计入交付。开工前核对工作树,保留已有草稿并明确归属;文档整理本身不解除功能验收门槛。 @@ -74,7 +79,7 @@ R1已按[限定合同](milestones/R1-local-text.md)验收,[完整组合证据] |---|---|---| | 全量 2.7.0 目标/平台台账 | [#19](https://github.com/JavaZeroo/termforge/issues/19) | R5 的验收基础,继续并行推进;平台切片 #23 已完成[范围台账](../research/windterm-2.7.0-platform-scope.md),其余功能域和运行观察未因此完成;声明、已观察和未知继续分开 | | 实际外部核心候选与 ADR-0003 重审 | [#20](https://github.com/JavaZeroo/termforge/issues/20) | 核心关卡已完成;R2 新能力按 ADR-0003 复用候选核心,产品整合由 #27–#35 验收 | -| Windows candidate Vim 捕获 | [#37](https://github.com/JavaZeroo/termforge/issues/37) | 保留 `editor wq lifecycle failed` 和后续步骤未运行事实;不得跳过真实应用门禁或据此关闭 R2 路标 | +| Windows candidate Vim 捕获 | [#37](https://github.com/JavaZeroo/termforge/issues/37) | 已按事件文件握手及[双平台 CI](https://github.com/JavaZeroo/termforge/actions/runs/40190890251)限定关闭;原失败保留,更多 TUI 与 R2 出口另验收 | | Server 2022 ConPTY 稳态 +1 句柄问题 | [#21](https://github.com/JavaZeroo/termforge/issues/21) | R4,保留严格探针和已知失败,不扩大支持声明 | | 自动观测、开销校准、完整记录 Schema | [#3](https://github.com/JavaZeroo/termforge/issues/3) | R4 收敛,工具工作可提前推进;停止的手工批次不补跑 | | 正式 WindTerm 性能基线 | [#4](https://github.com/JavaZeroo/termforge/issues/4) | R4,依赖 #3;节流/交互驱动、正确性、无插桩对照与重复批次仍需完成 | diff --git a/docs/research/conpty-ctrl-c-input.md b/docs/research/conpty-ctrl-c-input.md index 1a714f29..fd022fa7 100644 --- a/docs/research/conpty-ctrl-c-input.md +++ b/docs/research/conpty-ctrl-c-input.md @@ -132,3 +132,13 @@ Windows Terminal 安装版本 `1.24.12741.0` 对应 annotated tag [`v1.24.12741. - ConPTY 通道模型: - 控制台信号与模式:、、 - 官方已知限制:、、、 + +## 2026-10-26 接手复核与固定回归入口 + +在固定 `41cbbe3` 的独立工作树上,不修改产品代码,新鲜默认构建的 `normal` 三轮先通过,随后原 `normal` / `resize` 各十个 fresh 场景均通过。新 GO/Job 包装的原始 ConPTY 探针五轮通过;此前失败的旧探针原二进制在受监督的挂起创建包装器下五轮也通过,其文件字节保持不变。没有延长原五秒期限、修改一百万次循环、重复 ETX 或替换输入编码。 + +本次 PowerShell 的版本资源显示字符串均为 `10.0.26100.8875`,但 `VS_FIXEDFILEINFO` 的数值版本为 cmd `10.0.26100.9278`、conhost `10.0.26100.9444`;conhost 数值版本与旧报告一致。显示字符串和数值版本并不等价,文件 SHA-256 在此次核对前后不变,不能据此宣称系统曾更换版本。回归入口同时记录固定数值、资源字符串和文件哈希。当前仍未定位旧失败与现在成功的因果差异,旧失败不能改判成功。 + +已将一次性原始反馈整理为[受监督 cmd 回归入口](../../experiments/conpty/README.md#持续-cmd-输出与-ctrlc-独立回归)。输入/输出、实际系统文件和当前构建身份与回收结果各自落盘,遇到旧失败会保留失败而非跳过。它完成 #36 的可重复探针工作,**没有产品 Ctrl+C 修复,也没有验收原生处理函数的独立信号场景**。旧 9444 失败与根因缺口继续保留,#36 保持 OPEN。 + +最终入口的[三次 fresh 公开样本](../../experiments/conpty/results/2026-10-26-ctrl-c-retest/validation.json)另有本地离线复核:全部原件大小/哈希和独立完整标记重新计算一致,内外回收通过。原始运输没有公开,源码到二进制没有独立构建证明;这些限制随记录保留。 diff --git a/docs/research/hosted-wsl-fixture-cleanup.md b/docs/research/hosted-wsl-fixture-cleanup.md new file mode 100644 index 00000000..30d002f7 --- /dev/null +++ b/docs/research/hosted-wsl-fixture-cleanup.md @@ -0,0 +1,27 @@ +# 一次性 hosted WSL 检查夹具的收尾合同 + +本合同只适用于 `tools.prepare_hosted_shells` 在指定的 GitHub hosted Windows VM 上运行的 `check-before` / `check-after`。它不修改产品 ConPTY 会话、四种本地 shell 的正常退出、超时或资源验收。 + +## 退出与身份是不同的事实 + +[CI 40265785235](https://github.com/JavaZeroo/termforge/actions/runs/40265785235) 中,固定 `Ubuntu` 的 `/bin/sh`、工作目录、唯一 marker 与 shell 退出码 37 均已验证,Python worker 自然退出 0。worker 的自有 Job 仍有两个成员;每个成员均通过精确 Job 归属检查,但 `QueryFullProcessImageNameW` 返回错误 31。一次 `TerminateJobObject` 后 Job 与 PID 清单均为 0,两个保留的进程句柄均已 signaled,退出码为 99。 + +错误 31 不是 init、pico 或 wslhost 的身份证明。镜像查询失败记录为 `image_query_error`,角色继续为 `unknown`;不得把它改写成“已识别 WSL helper”或“自然退出”。`membership_query_error` 与 `exit_query_error` 分别记录归属、退出观测失败,不再与镜像查询混用。 + +微软说明 [`init` 是发行版顶层进程,WSL1 由 wslservice 启动](https://github.com/microsoft/WSL/blob/master/doc/docs/technical-documentation/init.md)。[`NtQueryInformationProcess` 的 class 75](https://learn.microsoft.com/windows/win32/api/winternl/nf-winternl-ntqueryinformationprocess)也可提供 Win32/WSL 子系统诊断。本合同不依赖进一步识别镜像或进程职责;这些资料不证明上述两个进程就是 init。 + +## 显式的夹具政策 + +采用 `owned-disposable-wsl-check-tree-v1`,依据是该夹具拥有并能回收本次检查树。允许收尾的条件必须同时成立: + +- worker 在发送 GO 前已加入本次新建 Job;只有通过真实 hosted、仓库、平台、工作区和管理员守卫的固定 worker 才能运行检查。 +- 执行的是固定 `Ubuntu`、固定 shell/输入与隔离目录;marker、目录、发行版和 shell 退出码验证成功,worker 自然退出 0 且父进程已 reap。worker 超时、被强制终止或验证失败不能获得此政策。 +- [`JobObjectBasicProcessIdList`](https://learn.microsoft.com/windows/win32/api/winnt/ns-winnt-jobobject_basic_process_id_list)清单完整且稳定,其人数与 Job 的剩余 active 数一致。每个打开的进程句柄都通过 `IsProcessInJob(handle, exact_job)` 核实;归属未知仍失败。 +- 只对这个自有 Job 请求一次终止,记录请求数和目标成员数。随后 Job active 和独立 PID 清单均为 0,每个保留句柄均实际 signaled 且退出码可查询;任何一项未知或失败仍失败。 +- `check-after` 必须用新的 worker/Job 再次实际启动并验证同一个 Ubuntu。下载成功、清理成功或第一次 marker 均不能替代第二次启动。 + +这允许角色未知但归属已核实的**夹具成员**被明确强制收尾,不能据此宣称所有成员都是正常驻留 helper。已核实的 wslhost 数量仍单独记录;未知镜像不会增加该数。Job 的 `terminated` 原始 accounting 字段也不作为本次终止人数使用。 + +不扫描全局进程,不按进程名结束任务,不调用 `wsl --shutdown` / `--terminate` / `--unregister`,不转换或替换已有发行版。raw PID、镜像路径、命令输出仅留本任务 `_private`;公开摘要保留角色、独立查询结果、自然退出和强制收尾的区别。只有真实 CI 两次检查及后续产品回归实际通过,才能继续关闭相关执行项。 + +正式入口在守卫通过后仅从环境复制 `ImageOS`、`ImageVersion` 和 `GITHUB_RUN_ID` 到 `hosted_provenance`,供宿主镜像复核;不导出完整环境或其它标识。纯测试直接调用 `prepare` 时此字段可为空,不据此生成真实 hosted 来源。 diff --git a/docs/research/more-capture-ech.md b/docs/research/more-capture-ech.md new file mode 100644 index 00000000..e1500ce5 --- /dev/null +++ b/docs/research/more-capture-ech.md @@ -0,0 +1,44 @@ +# More 捕获端点与 ECH 合同(#42) + +## 原因与实现边界 + +默认核心原先没有 `CSI X` 分派,Recovery 消费但不执行 ECH。366 字节输入的末尾 CR 将列号置为 0,随后 `CSI 79 X` 被忽略,因而留下 `) --`。这与捕获器的另一个问题分开:旧代码在仅看到 `TF_PAGE_1` 时就发送 q,且在进程退出后、reader 结束前保存快照。进程退出不能证明输出已经排干。 + +默认核心现在选择以下最小合同,strict 与 Recovery 共用分派: + +- `CSI n X` 从当前列填充 n 个空格,省略或 0 为 1;仅当前物理行,裁剪到右边界,不移动光标或滚动。 +- 清除 pending wrap。固定 Microsoft Terminal [`7ff11ee1` 的 `AdaptDispatch::EraseCharacters`](https://github.com/microsoft/terminal/blob/7ff11ee107c984e236960ec948b450ccbc10187b/src/terminal/adapter/adaptDispatch.cpp#L731-L753)明确调用 `ResetDelayEOLWrap()`。[Microsoft VT 表](https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences#text-modification)与 [xterm ECH 说明](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html)提供擦除及默认计数的依据。这不是 WindTerm 运行行为已经验证的声明。 +- 沿用项目既有屏模型:擦除使用当前样式,触及宽字任一半时清除整个旧字符,断开组合标记附着;擦除到行尾时清除旧 continuation 元数据。宽字和全样式政策不声称与所有参考等价。 + +保留一个真实候选差异:2×4 输入 `ABCD ESC [ X Z`,默认核心得到首行 `ABCZ`,termforge.3 候选得到首行 `ABC `、次行 `Z`。366 字节实例在 ECH 前有 CR,不涉及此分歧。本项没有把它登记为自动 `reference_difference`,固定 fuzz 也未覆盖该输入;完整模式与核心整合仍归 #27。 + +## 完整原始回归输入 + +两个 fixture 来自 40 行 `TF_PAGE_1`…`TF_PAGE_40` 的真实 more.com ConPTY 输出,只含合成文字、提示与控制序列,未截尾或重建。 + +| fixture | 字节数 | SHA-256 | +|---|---:|---| +| [more-pager-360.bin](../../experiments/core/fixtures/more-pager-360.bin) | 360 | `d035d53385788d2b68dcc0d866b5a2fe68897cdcb28a761bd4d36efcc6df6cf9` | +| [more-pager-366.bin](../../experiments/core/fixtures/more-pager-366.bin) | 366 | `bb9dbe77353245a2aad7ef07e12da9a8371cc7ac56d6b47ebc0bfecee35b9958` | + +来源为既有 #42 [margin 记录](../../experiments/shell/results/2026-10-26-margin-state/validation.json)所指的捕获及相同哈希的历史 360 字节捕获。原始字节继续保留,不把旧记录补写为 EOF 已验证。3×12 独立断言保留第二行 `-- More (56%`;360 字节第三行为 `) --`,366 字节第三行清空。整段、逐字节与不均匀拆包分别核对,不删除 ECH 来制造一致。 + +## 真实应用捕获合同 + +Python 监督器先将原生宿主加入自己的 Job,再发送 GO;原生入口还要求自身已在 Job 中。More 必须输出完整的英文 `-- More (<百分比>%) -- ` 提示,之后才发送一个 q,不使用固定延时替代同步。此实验目前绑定英文提示,其他语言尚未验收。 + +q 写完后要求退出码 0,独立线程经 `ClosePseudoConsole()` 后继续读取并 join。成功还必须有真实 `ReadFile` 终点 `ERROR_BROKEN_PIPE=109`、无强制取消、无捕获溢出及内外 Job 归零。`close()` 返回或 `cleanup_complete` 单独为真不能替代 EOF。[Microsoft 关闭文档](https://learn.microsoft.com/en-us/windows/console/closepseudoconsole)要求关闭后继续读取,[匿名管道文档](https://learn.microsoft.com/en-us/windows/win32/ipc/anonymous-pipe-operations)说明读端与写端关闭的关系。 + +`pager-lifecycle.json` 保存完整提示末偏移、实际 q 字节数、退出码、reader 终点、最终长度和取消状态;Python 独立检查原始提示及这些条件。提示等待期间、关闭前和失败路径先保留实际字节,再进行可能阻塞的关闭;60 秒外层监督只清理自己的 Job。超时留下的前缀标为未完成,不伪造 EOF。 + +新鲜完整样本为 382 字节:提示结束偏移 360,后续 CR、模式关闭和 ECH 全部保留。合法长度和重绘可能变化,验收不锁定长度或改写尾部。三应用比较通过仅代表这些核心轨迹,不代表 GUI、物理呈现或完整 VT 验收。修复前后、同输入候选/pyte 对照和端点证据见[审阅摘要](../../experiments/differential/results/2026-10-26-more-ech/validation.json)。 + +## 复验入口 + +```powershell +cmake --build build --config Release --target recovery_probe terminal_trace real_app_capture +python -m unittest experiments.core.test_recovery -v +python -m unittest discover -s experiments/differential -p test_real_app_matrix.py -v +``` + +真实应用测试要求构建固定候选且 PATH 中可找到 Vim;可选依赖缺失导致的 skip 不代替运行证据。新增断言覆盖默认/0 计数、裁剪、光标、pending wrap、宽字政策,完整历史字节、伪造 EOF/部分提示、GO 门禁及监督器故障;共同矩阵和原 fuzz 裁决保持不变。 diff --git a/docs/research/workspace-restore-transport-oracle.md b/docs/research/workspace-restore-transport-oracle.md new file mode 100644 index 00000000..6f17b292 --- /dev/null +++ b/docs/research/workspace-restore-transport-oracle.md @@ -0,0 +1,30 @@ +# 工作区恢复的独立输出判定 + +`ui-workspace-restore` 的 stage 1 必须同时观察到 cmd 当前目录、PowerShell 当前目录、PowerShell 主版本及底部 cmd 标记,再观察各自返回固定 `TF>` 提示符。这里验证固定测试命令实际完成;它不证明标记最终仍独占屏幕某行,也不借用被测终端核心的解析结果。 + +## 已复现的观测错误 + +旧判定对完整 transport 使用 `rfind(marker)`。一份真实失败中,cmd 已独立输出 `TF_CMD_DIR=restore-cmd 目录\r\n`,后续完整 OSC 0 标题又包含相同标记。最后一次匹配后的字节是 BEL,导致谓词永远为假,尽管后面已有新 `TF>`。其余三个谓词均为真。原始失败文件 SHA-256 为 `af1a2c513b3891ce75acccca6ec2125ab46ab2fc2b312d0a05fd8cd40fa425bc`,保留原件,不回填结果;独立 C++ helper 重放其原始字节时四个谓词均成立。 + +仅剥离控制序列并要求 LF 仍不充分:新的真实默认构建运行中,cmd 的完整标记后直接出现 `ESC [ 6 ; 1 H`、显示光标序列和 `TF>`,没有运输层 CRLF。这份新失败(SHA-256 `339ca19fc01a0a188325fd6b4b594538cfdf6535e9b1e17ca398b6fa4472c8a5`)同样保留;最小反例先失败,再增加受限的 column-one CUP 记录边界。它不假设起始游标行号,也不声称 CUP 移到了新行。 + +## 限定 oracle 合同 + +- 独立扫描 7-bit ESC 形式的完整 OSC(BEL 或 ST 终止)和 CSI。OSC 内容不参与正文匹配,UTF-8 的所有高位正文 bytes 原样保留,包括 `目` 中的 `0x9b`。 +- 标记必须匹配完整正文记录,前后不可附加命令、其它正文或任意子串。LF 结束记录;显式正行号、第 1 列 CUP/HVP 仅可结束完整标记或提示符,不能清除命令前缀或拼接分散的标记片段。其它非空内容遇 CUP 保持未知,直到真实 LF;SGR 和其它 CSI 不会凭空结束标记。 +- 该记录之后必须出现独立的 `TF>` 或 `TF> ` 记录,才证明 shell 返回;输入命令回显、标题内标记、旧提示符和输出行前缀都不满足条件。 +- 完整 CSI 仅按语法封装剥离,不解释或验收其执行语义;其中受限 column-one CUP 的记录边界另按上条处理。不完整或格式错误的封装、未知 ESC 引导形式、DCS 等其它控制字符串保持未证明状态。这只覆盖固定持久化命令已输出和返回提示符的运输证据,不扩展为第二个通用 VT 核心;产品屏幕及恢复断言仍独立保留。 +- 保留原有 8192-byte 观测上限、30 秒总期限、真实目录/版本、返回提示符、恢复成员/标题/缩放/分屏/几何及尺寸检查和真实 Job/desktop 回收断言。 + +`transport_completion_test` 独立覆盖标题重复遮蔽、BEL/ST、SGR、CUP、UTF-8 以及命令回显、OSC/DCS payload、不完整控制、缺失或旧提示符等反例。它修正的是测试 oracle,不构成产品恢复故障修复或完整 R2 验收。 + +```powershell +ctest --test-dir build/default -C Release -R '^transport_completion$' --output-on-failure +$env:TERMFORGE_BIN_DIR = "$PWD/build/default/bin/Release" +$env:PYTHONUTF8 = '1' +python -m unittest experiments.shell.test_shell.ShellTests.test_workspace_v2_multiple_shells_split_and_geometry_restore -v +``` + +候选构建使用相同测试命令和原场景两轮保存/新进程恢复,将 `TERMFORGE_BIN_DIR` 指向显式候选构建。两个构建使用私有非输入 desktop 和自有 Job;无须接触用户剪贴板或输入法。 + +本地最终验证:默认与候选 Release 构建均通过独立 helper 回归和上述原测试方法;每种构建完成两轮保存、两轮新进程恢复和两轮超宽窗口裁剪恢复。原始两份失败字节分别经 C++ helper 重放,四个条件均成立。[公开复核摘要](../../experiments/shell/results/2026-10-26-workspace-transport-oracle/validation.json)记录源文件及输入身份、旧谓词、反例和 fresh 样本哈希;原始运输数据保留本地。此处不代替合并后 CI 或完整候选回归。 diff --git a/experiments/candidates/alacritty/README.md b/experiments/candidates/alacritty/README.md index 5c10702f..00e63a5a 100644 --- a/experiments/candidates/alacritty/README.md +++ b/experiments/candidates/alacritty/README.md @@ -6,10 +6,10 @@ | 项目 | 当前固定值 | 来源 | |---|---|---| -| `alacritty_terminal` | `0.26.1-dev+termforge.2`,Apache-2.0 | [上游 commit `5421470`](https://github.com/alacritty/alacritty/commit/542147067dd016fc5f49120ef0ba605a98a4e3a7)与[来源/补丁台账](PROVENANCE.md) | -| 本地补丁 | ED1/ED2、稳定 Row ID/换行元数据/淘汰、安全完整 Row swap | `vendor/alacritty_terminal/src/`;所有文件哈希进入结果 manifest | +| `alacritty_terminal` | `0.26.1-dev+termforge.3`,Apache-2.0 | [上游 commit `5421470`](https://github.com/alacritty/alacritty/commit/542147067dd016fc5f49120ef0ba605a98a4e3a7)与[来源/补丁台账](PROVENANCE.md) | +| 本地补丁 | ED1/ED2、稳定 Row ID/换行元数据/淘汰、安全完整 Row swap、每缓冲区滚动区域 | `vendor/alacritty_terminal/src/`;所有文件哈希进入结果 manifest | | 工具链 | Rust/cargo 1.92.0 | 本机与候选 CI 显式选择;当前锁定图最高声明 Rust 1.85 | -| 传递依赖 | 59 个注册表包 + 固定 path crate,共 60 个包 | `Cargo.lock` 与 `cargo metadata --locked --format-version 1` | +| 传递依赖 | 58 个注册表包 + 根适配器与固定 vendor 两个 path crate,共 60 个包 | `Cargo.lock` 与 `cargo metadata --locked --format-version 1` | 根 `Cargo.toml` 使用固定 path 依赖并关闭默认 serde feature。上游只复制 manifest、changelog、许可证和约 426 KiB 的 `src/`,不复制约 46 MiB 行为录制;内联测试保留。补丁不冒充 crates.io 原始 0.26.0,也不把第三方代码改标 MIT。候选二进制当前只用于本地/CI 实验,不进入 Alpha 或产品发行包;以后分发须完成全部依赖许可与通知审计。 @@ -32,7 +32,7 @@ cmake --build build/candidate-adapter --config Release ctest --test-dir build/candidate-adapter -C Release --output-on-failure ``` -Linux/macOS 使用相同 Cargo 命令和相应 CMake 配置。默认 trace 为 `target/release/termforge-alacritty-trace[.exe]`;可用 `TERMFORGE_ALACRITTY_TRACE` 显式选择另一构建,缺失文件会失败。候选 workflow 在 Windows/Ubuntu 构建并运行固定 crate 的 142 个单元测试、doc test 和 C++ 适配器。 +Linux/macOS 使用相同 Cargo 命令和相应 CMake 配置。默认 trace 为 `target/release/termforge-alacritty-trace[.exe]`;可用 `TERMFORGE_ALACRITTY_TRACE` 显式选择另一构建,缺失文件会失败。候选 workflow 在 Windows/Ubuntu 构建并运行固定 crate 的 141 个单元测试、1 个 doc test 和 C++ 适配器。 ## Trace、C ABI 与检查内容 @@ -44,8 +44,9 @@ Linux/macOS 使用相同 Cargo 命令和相应 CMake 配置。默认 trace 为 ` ## 当前结果与历史边界 +- termforge.3 的[共同矩阵](../../differential/results/2026-10-26-candidate-common-v6/summary.json)为 95 次一致;[扩展能力](../../differential/results/2026-10-26-capabilities-v5/summary.json)分类不变;[fuzz](../../differential/results/2026-10-26-fuzz-v4/summary.json)为 2 次固定已知差异、6 次一致;[真实应用](../../differential/results/2026-10-26-real-applications-v6/summary.json)固定 3 次一致。[margin 验证](../../shell/results/2026-10-26-margin-state/validation.json)区分公开源 VT 的恢复与真实 ConPTY 的宿主限制;#41 的宿主 margin 和 #42 的 More 尾部 ECH/捕获时序仍未闭合,不是完整产品 TUI。 - termforge.2 的[共同矩阵](../../differential/results/2026-10-25-candidate-common-v5/summary.json)为 95 次、1011 检查点全部一致;旧 ED 差异不再以 `reference_difference` 放行。[扩展能力](../../differential/results/2026-10-25-capabilities-v4/summary.json)保持 15 次自研未支持、5 次一致、5 次 resize 差异;[真实应用](../../differential/results/2026-10-25-real-applications-v3/summary.json)3 次全部一致;[fuzz](../../differential/results/2026-10-25-fuzz-v3/summary.json)为 2 次固定已知差异、6 次一致。均不是完整语义接受或产品 TUI。 -- C++ 适配器验证分块一致性、真实回复、备用屏/滚动区、ED、Unicode、resize、ID/版本、选择、搜索和 HistoryViewport。显式候选 `ShellWindow` 已运行两轮真实 Vim、60×12 resize、主屏恢复及经有界 InputQueue 的真实光标回复,并修复普通空格背景残留;证据见 [validation](../../core/results/2026-10-25-candidate-adapter/validation.json)。默认构建保留自研回退,#36、#29 的真实重放身份失效、完整 R1 回归和实际呈现仍未关闭。 +- C++ 适配器验证分块一致性、真实回复、备用屏/滚动区、ED、Unicode、resize、ID/版本、选择、搜索和 HistoryViewport。显式候选 `ShellWindow` 已运行两轮真实 Vim、60×12 resize、主屏恢复及经有界 InputQueue 的真实光标回复,并修复普通空格背景残留;证据见 [validation](../../core/results/2026-10-25-candidate-adapter/validation.json)。默认构建保留自研回退;#29 的限定 resize/历史切片已有验证,#36/#38 的真实回归、#41 的宿主 margin、完整 R1 回归和实际呈现仍未关闭。 - #25 的[逐项裁决](../../differential/decisions.md)保留旧 0.26.0 差异;#26 的[路线审查](../../../docs/research/2026-10-25-core-route-review.md)与 project/interactive 计量是原始 0.26.0 历史基线,termforge.1 是更早 headless 切片,均不覆盖 termforge.2。 - 默认/ANSI 16 色映射为现有数值;其他颜色以 `-2` 保留未投影。上游 `Cell::is_empty` 合并空格和空单元格,不能裁决显式尾空格。`replies` 是相邻检查点的新回复,不是累计队列。 -- resize/reflow 合同归 #29,不能由候选行为静默替换。候选可构建不等于支持 [#23 平台台账](../../../research/windterm-2.7.0-platform-scope.md)的全部系统;Windows 7 非 ConPTY、macOS 10.13、glibc 2.17、x86 和 ARM 缺口继续归 #34/R4,Windows CI 的 Vim 捕获问题由 #37 单独保留。 +- resize/reflow 合同归 #29,不能由候选行为静默替换。候选可构建不等于支持 [#23 平台台账](../../../research/windterm-2.7.0-platform-scope.md)的全部系统;Windows 7 非 ConPTY、macOS 10.13、glibc 2.17、x86 和 ARM 缺口继续归 #34/R4。#37 的限定 Windows Vim CI 已通过,其他 shell/生命周期回归由 #36/#38 保留。 diff --git a/experiments/conpty/README.md b/experiments/conpty/README.md index 1c374917..cfdd0f3d 100644 --- a/experiments/conpty/README.md +++ b/experiments/conpty/README.md @@ -48,3 +48,19 @@ child 显式设置 `STARTF_USESTDHANDLES` 并将三个标准句柄保持 null, 审阅后的纯合成历史记录位于 [results/2026-10-22](results/2026-10-22/manifest.json),保存源码/二进制身份、系统版本、退出状态与资源计数;后续修复和远端系统验证不回写这些结果。`elapsed_ms` 是含启动/同步/关闭的诊断耗时,不是吞吐、输入延迟或 WindTerm 对照结果。 当前已验收环境为本机 Windows 11 build 26200 和 `windows-2025` runner;不承诺整个 Windows 10/Server 系列。Server 2022 的中文回显已修复,但稳态句柄仍增加 1,未满足资源门槛,定位与修复由 [#21](https://github.com/JavaZeroo/termforge/issues/21)承接。保留[严格兼容性探针](../../.github/workflows/conpty-compat.yml),可由代理执行 `gh workflow run conpty-compat.yml` 自动重跑;失败仍使该 workflow 失败,不用用户操作测试会话。 + +## 持续 cmd 输出与 Ctrl+C 独立回归 + +[#36](https://github.com/JavaZeroo/termforge/issues/36) 的原始 ConPTY 入口由 `conpty_ctrl_c_probe` 和 `experiments.conpty.ctrl_c_run` 提供,不经过窗口、TermForge 解析器或输入队列。先按根贡献指南构建;使用新输出目录: + +```powershell +$env:TERMFORGE_BIN_DIR = (Resolve-Path build/bin/Release).Path +python -m experiments.conpty.ctrl_c_run --out local/ctrl-c-new --repeats 3 +python -m unittest experiments.conpty.test_ctrl_c -v +``` + +也可用 `--exe` 指定本次构建的 `conpty_ctrl_c_probe.exe`。Windows 根 CTest 注册为 `conpty_ctrl_c`。原生入口等待外层 Job 的 GO,正常使用 Python 监督入口;测试不使用真实输入桌面或系统剪贴板。 + +合同保留一百万次 `cmd for /L`,实际输出至少持续一秒后发送单个 ETX,立即发送后续命令,五秒内检查独立完整输出行;命令回显或循环自然结束不算中断。监督器检查退出与自有 Job 回收,分别记录输入/输出身份、探针/源码身份和本次自有 cmd/conhost 映像的系统文件身份。原始诊断只留在输出目录的 `_private` 子目录;公开文件不包含路径、PID 或原始异常文本。 + +此入口验收的是固定 cmd 场景,不能单独证明所有原生控制处理函数、真实物理按键或所有 Windows 版本通过。当前条件未复现旧失败,也不能据重复通过关闭 #36;环境对照与未决点见[输入机制研究](../../docs/research/conpty-ctrl-c-input.md)。 diff --git a/experiments/conpty/ctrl_c_probe.cpp b/experiments/conpty/ctrl_c_probe.cpp new file mode 100644 index 00000000..1cd9d78c --- /dev/null +++ b/experiments/conpty/ctrl_c_probe.cpp @@ -0,0 +1,174 @@ +// Original, bounded cmd/ConPTY interrupt diagnostic. MIT; see LICENSE. +#include "session.hpp" +#include +#include +#include + +namespace { +using Clock = std::chrono::steady_clock; +constexpr unsigned loop_count = 1000000; +constexpr unsigned marker_timeout_ms = 5000; + +// A marker must occupy its own completed output line. Matching an arbitrary +// substring would accept the line editor's echo of "echo TF_AFTER_FLOOD". +std::string plain_text(const std::string& raw) { + std::string text; + for (size_t at = 0; at < raw.size();) { + const auto value = static_cast(raw[at++]); + if (value == 27 && at < raw.size()) { + const char kind = raw[at++]; + if (kind == '[') { + while (at < raw.size()) { + const auto c = static_cast(raw[at++]); + if (c >= 0x40 && c <= 0x7e) break; + } + } else if (kind == ']') { + while (at < raw.size()) { + if (raw[at++] == '\a') break; + if (at >= 2 && raw[at - 2] == 27 && raw[at - 1] == '\\') break; + } + } + } else if (value != '\r' && value != '\a') text.push_back(static_cast(value)); + } + return text; +} + +unsigned marker_lines(const std::string& raw, const std::string& expected) { + const auto text = plain_text(raw); + unsigned count = 0; + for (size_t at = 0; at < text.size();) { + const auto end = text.find('\n', at); + if (end == std::string::npos) break; + if (text.compare(at, end - at, expected) == 0) ++count; + at = end + 1; + } + return count; +} + +unsigned highest_busy(const std::string& raw) { + const auto text = plain_text(raw); + unsigned highest = 0; + for (size_t at = text.find("TF_BUSY_"); at != std::string::npos; at = text.find("TF_BUSY_", at + 8)) { + size_t end = at + 8; + unsigned number = 0; + while (end < text.size() && text[end] >= '0' && text[end] <= '9') { + if (number > loop_count) break; + number = number * 10 + static_cast(text[end++] - '0'); + } + if (end > at + 8 && end < text.size() && text[end] == '\n') highest = (std::max)(highest, number); + } + return highest; +} + +bool wait_marker(tf_conpty::Session& session, const std::string& marker) { + const auto deadline = Clock::now() + std::chrono::milliseconds(marker_timeout_ms); + do { + if (marker_lines(session.output(), marker) == 1) return true; + Sleep(10); + } while (Clock::now() < deadline); + return false; +} + +long long elapsed_ms(Clock::time_point start) { + return std::chrono::duration_cast(Clock::now() - start).count(); +} +void require(bool value) { if (!value) throw std::runtime_error("diagnostic stage failed"); } +} // namespace + +int main() { + std::cout << std::boolalpha; + // No PTY client, ConPTY, raw file or input exists before the parent's gate. + // Windows may give this console-subsystem host its own headless conhost. + char gate[4]{}; + size_t gate_bytes = 0; + while (gate_bytes < sizeof(gate)) { + DWORD count = 0; + if (!ReadFile(GetStdHandle(STD_INPUT_HANDLE), gate + gate_bytes, 1, &count, nullptr) || count != 1) break; + if (gate[gate_bytes++] == '\n') break; + } + const std::string_view gate_value(gate, gate_bytes); + if (gate_value != "GO\n" && gate_value != "GO\r\n") { + std::cout << "{\"schema\":\"termforge-ctrl-c-native-v1\",\"status\":\"guard_rejected\"}\n"; + return 2; + } + BOOL contained = FALSE; + if (!IsProcessInJob(GetCurrentProcess(), nullptr, &contained) || !contained) { + std::cout << "{\"schema\":\"termforge-ctrl-c-native-v1\",\"status\":\"job_required\"}\n"; + return 2; + } + tf_conpty::Session session; + const auto begin = Clock::now(); + std::ofstream input("input.bin", std::ios::binary); + size_t input_bytes = 0, flood_input_bytes = 0; + bool idle = false, flood = false, after = false, exited = false; + long long busy_hold_ms = 0, after_wait_ms = -1; + const char* phase = "input_artifact"; + const char* failure = "none"; + auto send = [&](const std::string& bytes, bool flood_input = false) { + size_t confirmed = 0; + session.send(bytes, [&](size_t count) { + input.write(bytes.data() + confirmed, static_cast(count)); + confirmed += count; input_bytes += count; + if (flood_input) flood_input_bytes += count; + }); + input.flush(); require(static_cast(input)); + }; + try { + require(static_cast(input)); + wchar_t directory[MAX_PATH]{}; + phase = "system_directory"; + const auto length = GetSystemDirectoryW(directory, MAX_PATH); + require(length && length < MAX_PATH); + phase = "start"; + session.start(std::wstring(directory) + L"\\cmd.exe", L"/d /q /k \"prompt TF$G&cls\""); + phase = "prompt"; require(session.wait_text("TF>")); + phase = "idle_input"; + send("@echo off\r"); send(std::string(1, '\x03')); send("echo TF_AFTER_IDLE\r"); + phase = "idle_marker"; idle = wait_marker(session, "TF_AFTER_IDLE"); require(idle); + phase = "flood_input"; + send("for /L %i in (1,1,1000000) do @echo TF_BUSY_%i\r"); + phase = "flood_output"; + const auto first_busy_deadline = Clock::now() + std::chrono::milliseconds(marker_timeout_ms); + while (!(flood = highest_busy(session.output()) > 0) && Clock::now() < first_busy_deadline) Sleep(10); + require(flood); + const auto first_busy = Clock::now(); + Sleep(1000); + busy_hold_ms = elapsed_ms(first_busy); + phase = "interrupt_input"; + const auto interrupt = Clock::now(); + send(std::string(1, '\x03'), true); + send("echo TF_AFTER_FLOOD\r", true); // no inserted delay or alternate encoding + phase = "interrupt_marker"; + after = wait_marker(session, "TF_AFTER_FLOOD"); after_wait_ms = elapsed_ms(interrupt); + require(after && highest_busy(session.output()) < loop_count); + phase = "exit_input"; send("exit 37\r"); + phase = "exit"; exited = session.wait_exit(5000); require(exited && session.exit_code == 37); + } catch (...) { failure = phase; } + // Preserve the pre-cleanup observation separately from close-generated I/O. + const auto output = session.output(); + const auto highest = highest_busy(output); + const auto markers = marker_lines(output, "TF_AFTER_FLOOD"); + std::ofstream raw("output.bin", std::ios::binary); + raw.write(output.data(), static_cast(output.size())); raw.close(); + input.close(); + const bool artifacts = static_cast(raw) && static_cast(input); + session.close(); + const bool valid = failure == std::string("none") && artifacts && idle && flood && after && exited && + markers == 1 && highest > 0 && highest < loop_count && busy_hold_ms >= 1000 && + session.cleanup_complete && !session.capture_overflow && + (session.read_error == ERROR_SUCCESS || session.read_error == ERROR_BROKEN_PIPE); + std::cout << "{\"schema\":\"termforge-ctrl-c-native-v1\",\"status\":\"" << (valid ? "passed" : "failed") + << "\",\"failure_code\":\"" << failure << "\",\"idle_marker\":" << idle + << ",\"flood_observed\":" << flood << ",\"after_marker\":" << after + << ",\"after_marker_lines\":" << markers << ",\"highest_busy\":" << highest + << ",\"busy_hold_ms\":" << busy_hold_ms << ",\"after_wait_ms\":" << after_wait_ms + << ",\"input_bytes\":" << input_bytes << ",\"flood_input_bytes\":" << flood_input_bytes + << ",\"output_bytes\":" << output.size() << ",\"exit_code\":" << session.exit_code + << ",\"read_error\":" << session.read_error << ",\"capture_overflow\":" << session.capture_overflow + << ",\"active_children_after\":" << session.active_after + << ",\"cleanup_complete\":" << session.cleanup_complete + << ",\"forced_process_cancel\":" << session.forced_process_cancel + << ",\"forced_reader_cancel\":" << session.forced_reader_cancel + << ",\"elapsed_ms\":" << elapsed_ms(begin) << "}\n"; + return valid ? 0 : 1; +} diff --git a/experiments/conpty/ctrl_c_run.py b/experiments/conpty/ctrl_c_run.py new file mode 100644 index 00000000..1c21069e --- /dev/null +++ b/experiments/conpty/ctrl_c_run.py @@ -0,0 +1,446 @@ +"""Bounded raw cmd/ConPTY Ctrl-C diagnostic; no input desktop or reference app.""" +from __future__ import annotations + +import argparse +import ctypes +from ctypes import wintypes +from datetime import datetime, timezone +import hashlib +import json +import os +from pathlib import Path +import re +import subprocess +import threading +import time +import traceback + +from experiments.build_paths import ROOT, binary +from experiments.conpty.run import OwnedJob + +SCHEMA = "termforge-conpty-ctrl-c-v1" +LOOP = b"for /L %i in (1,1,1000000) do @echo TF_BUSY_%i\r" +INITIAL = b"@echo off\r\x03echo TF_AFTER_IDLE\r" +FOLLOWUP = b"\x03echo TF_AFTER_FLOOD\r" +COMPLETE_INPUT = INITIAL + LOOP + FOLLOWUP + b"exit 37\r" +NATIVE_FIELDS = { + "schema", "status", "failure_code", "idle_marker", "flood_observed", "after_marker", + "after_marker_lines", "highest_busy", "busy_hold_ms", "after_wait_ms", "input_bytes", + "flood_input_bytes", "output_bytes", "exit_code", "read_error", "capture_overflow", + "active_children_after", "cleanup_complete", "forced_process_cancel", "forced_reader_cancel", "elapsed_ms", +} +FAILURE_CODES = { + "none", "input_artifact", "system_directory", "start", "prompt", "idle_input", "idle_marker", + "flood_input", "flood_output", "interrupt_input", "interrupt_marker", "exit_input", "exit", +} +BOOL_FIELDS = {"idle_marker", "flood_observed", "after_marker", "capture_overflow", + "cleanup_complete", "forced_process_cancel", "forced_reader_cancel"} +STRING_FIELDS = {"schema", "status", "failure_code"} + + +def sha256(path): + return hashlib.sha256(Path(path).read_bytes()).hexdigest() + + +def plain_text(raw): + """Independent ASCII marker oracle; command echo is never a marker line.""" + text = raw.decode("utf-8", "replace") + text = re.sub(r"\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)", "", text) + text = re.sub(r"\x1b\[[0-?]*[ -/]*[@-~]", "", text) + return text.replace("\r", "").replace("\a", "") + + +def validate_trace(raw, sent): + if sent != COMPLETE_INPUT: + raise ValueError("input_identity") + text = plain_text(raw) + lines = text.split("\n")[:-1] # a partial last line cannot establish execution + if lines.count("TF_AFTER_IDLE") != 1 or lines.count("TF_AFTER_FLOOD") != 1: + raise ValueError("marker_identity") + busy = [(i, int(match.group(1))) for i, line in enumerate(lines) + if (match := re.fullmatch(r"TF_BUSY_(\d+)", line))] + if not busy or not 0 < max(number for _, number in busy) < 1000000: + raise ValueError("loop_not_interrupted") + idle, after = lines.index("TF_AFTER_IDLE"), lines.index("TF_AFTER_FLOOD") + if not idle < busy[0][0] <= busy[-1][0] < after: + raise ValueError("marker_order") + return {"idle_marker_lines": 1, "after_marker_lines": 1, + "highest_busy": max(number for _, number in busy), "busy_lines": len(busy)} + + +def system_directory(): + api = ctypes.WinDLL("kernel32", use_last_error=True) + api.GetSystemDirectoryW.argtypes = [wintypes.LPWSTR, wintypes.UINT] + api.GetSystemDirectoryW.restype = wintypes.UINT + buffer = ctypes.create_unicode_buffer(32768) + size = api.GetSystemDirectoryW(buffer, len(buffer)) + if not size or size >= len(buffer): + raise OSError("system_directory") + return Path(buffer.value) + + +def file_version(path): + api = ctypes.WinDLL("version", use_last_error=True) + api.GetFileVersionInfoSizeW.argtypes = [wintypes.LPCWSTR, ctypes.POINTER(wintypes.DWORD)] + api.GetFileVersionInfoSizeW.restype = wintypes.DWORD + api.GetFileVersionInfoW.argtypes = [wintypes.LPCWSTR, wintypes.DWORD, wintypes.DWORD, ctypes.c_void_p] + api.VerQueryValueW.argtypes = [ctypes.c_void_p, wintypes.LPCWSTR, ctypes.POINTER(ctypes.c_void_p), + ctypes.POINTER(wintypes.UINT)] + unused = wintypes.DWORD() + size = api.GetFileVersionInfoSizeW(str(path), ctypes.byref(unused)) + if not size: + raise OSError("file_version") + data = ctypes.create_string_buffer(size) + pointer, length = ctypes.c_void_p(), wintypes.UINT() + if not api.GetFileVersionInfoW(str(path), 0, size, data) or not api.VerQueryValueW( + data, "\\", ctypes.byref(pointer), ctypes.byref(length)) or length.value < 52: + raise OSError("file_version") + fixed = ctypes.cast(pointer, ctypes.POINTER(wintypes.DWORD * 13)).contents + if fixed[0] != 0xFEEF04BD: + raise OSError("file_version") + versions = {} + for name, offset in (("fixed_file_version", 2), ("fixed_product_version", 4)): + versions[name] = ".".join(str(value) for value in ( + fixed[offset] >> 16, fixed[offset] & 65535, fixed[offset + 1] >> 16, fixed[offset + 1] & 65535)) + # The localized resource string can differ from VS_FIXEDFILEINFO. Preserve + # both; never interpret a display-string difference as a new binary. + if not api.VerQueryValueW(data, "\\VarFileInfo\\Translation", ctypes.byref(pointer), ctypes.byref(length)): + raise OSError("file_version_translation") + if length.value < 4 or length.value % 4: + raise OSError("file_version_translation") + words = ctypes.cast(pointer, ctypes.POINTER(wintypes.WORD * (length.value // 2))).contents + language, codepage = words[0], words[1] + for key in ("FileVersion", "ProductVersion"): + subblock = f"\\StringFileInfo\\{language:04x}{codepage:04x}\\{key}" + if not api.VerQueryValueW(data, subblock, ctypes.byref(pointer), ctypes.byref(length)) or not length.value: + raise OSError("file_version_string") + versions["file_version_string" if key == "FileVersion" else "product_version_string"] = ctypes.wstring_at( + pointer.value, length.value).rstrip("\0") + return versions + + +def system_identity(directory): + return {name: {"sha256": sha256(directory / name), **file_version(directory / name)} + for name in ("cmd.exe", "conhost.exe")} + + +class OwnedImages: + """Query only process IDs returned by this exact parent's owned Job.""" + def __init__(self, job, directory): + self.job, self.directory = job, directory + self.stop = threading.Event() + self.observed = {"cmd.exe": False, "conhost.exe": False} + self.error = False + self.thread = threading.Thread(target=self.run, daemon=True) + + def sample(self): + class Ids(ctypes.Structure): + _fields_ = [("assigned", wintypes.DWORD), ("count", wintypes.DWORD), + ("ids", ctypes.c_size_t * 64)] + ids = Ids() + if not self.job.api.QueryInformationJobObject(self.job.handle, 3, ctypes.byref(ids), + ctypes.sizeof(ids), None) or ids.count > 64: + raise OSError("owned_job_query") + api = ctypes.WinDLL("kernel32", use_last_error=True) + api.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD] + api.OpenProcess.restype = wintypes.HANDLE + api.QueryFullProcessImageNameW.argtypes = [wintypes.HANDLE, wintypes.DWORD, + wintypes.LPWSTR, ctypes.POINTER(wintypes.DWORD)] + api.CloseHandle.argtypes = [wintypes.HANDLE] + for pid in ids.ids[:ids.count]: + handle = api.OpenProcess(0x1000, False, pid) + if not handle: # a short-lived owned process may have exited + continue + try: + buffer, size = ctypes.create_unicode_buffer(32768), wintypes.DWORD(32768) + if not api.QueryFullProcessImageNameW(handle, 0, buffer, ctypes.byref(size)): + continue + image = Path(buffer.value) + if image.name.lower() in self.observed: + role = image.name.lower() + if os.path.normcase(str(image)) != os.path.normcase(str(self.directory / role)): + raise OSError("system_image_mismatch") + self.observed[role] = True + finally: + api.CloseHandle(handle) + + def run(self): + try: + while not self.stop.is_set(): + self.sample() + self.stop.wait(0.02) + except Exception: + self.error = True + + def start(self): + self.thread.start() + + def finish(self): + self.stop.set() + self.thread.join(timeout=2) + if self.thread.is_alive(): + self.error = True + + +def validate_sample(row): + native, trace = row.get("native", {}), row.get("trace", {}) + if row.get("cleanup_errors") or not re.fullmatch(r"[0-9a-f]{64}", row.get("probe_sha256") or ""): + raise ValueError("cleanup_or_probe_identity") + if row.get("schema") != SCHEMA or row.get("timed_out") or row.get("returncode") != 0: + raise ValueError("execution") + if (set(native) != NATIVE_FIELDS or native.get("schema") != "termforge-ctrl-c-native-v1" or + native.get("status") != "passed" or native.get("failure_code") != "none"): + raise ValueError("native_identity") + if any(native.get(key) is not True for key in ( + "idle_marker", "flood_observed", "after_marker", "cleanup_complete")): + raise ValueError("native_lifecycle") + if any(native.get(key) is not False for key in ( + "capture_overflow", "forced_process_cancel", "forced_reader_cancel")): + raise ValueError("native_cleanup") + if (native.get("exit_code") != 37 or native.get("active_children_after") != 0 or + native.get("read_error") not in (0, 109) or native.get("input_bytes") != len(COMPLETE_INPUT) or + native.get("flood_input_bytes") != len(FOLLOWUP) or native.get("after_marker_lines") != 1 or + not 1000 <= native.get("busy_hold_ms", -1) < 15000 or + not 0 <= native.get("after_wait_ms", -1) <= 5000 or + not 0 < native.get("highest_busy", 0) < 1000000 or + native.get("highest_busy") != trace.get("highest_busy") or trace.get("after_marker_lines") != 1): + raise ValueError("interrupt_contract") + if row.get("job_after", {}).get("active") != 0 or row.get("forced_job_cleanup"): + raise ValueError("outer_cleanup") + if row.get("image_query_error") or row.get("owned_images") != {"cmd.exe": True, "conhost.exe": True}: + raise ValueError("system_image_identity") + if row.get("system_files_before") != row.get("system_files_after"): + raise ValueError("system_identity_changed") + if set(row.get("system_files_before", {})) != {"cmd.exe", "conhost.exe"}: + raise ValueError("system_identity") + for identity in row.get("system_files_before", {}).values(): + if not re.fullmatch(r"\d+\.\d+\.\d+\.\d+", identity.get("fixed_file_version", "")) or not re.fullmatch( + r"[0-9a-f]{64}", identity.get("sha256", "")): + raise ValueError("system_identity") + + +def checked_native(value): + """Do not copy arbitrary executable output or error strings into public JSON.""" + if (not isinstance(value, dict) or set(value) != NATIVE_FIELDS or + value.get("schema") != "termforge-ctrl-c-native-v1" or + value.get("status") not in ("passed", "failed") or value.get("failure_code") not in FAILURE_CODES): + raise ValueError("native_identity") + for key in BOOL_FIELDS: + if type(value[key]) is not bool: + raise ValueError("native_identity") + for key in NATIVE_FIELDS - BOOL_FIELDS - STRING_FIELDS: + if type(value[key]) is not int or not (-1 if key == "after_wait_ms" else 0) <= value[key] <= 0xFFFFFFFF: + raise ValueError("native_identity") + return value + + +def execute(exe, out, *, timeout=15, gate=b"GO\n"): + """Always write a sanitized sample; raise on a normal workload failure. + + gate=None deliberately withholds GO for the watchdog regression. Guard + cases return their failed/rejected record, without accepting the workload. + """ + if os.name != "nt" or not 0 < timeout <= 30: + raise ValueError("Windows and a bounded timeout are required") + exe, out = Path(exe).resolve(), Path(out).resolve() + out.mkdir(parents=True, exist_ok=False) + private = out / "_private" + private.mkdir() + row = {"schema": SCHEMA, "status": "failed", "failure_code": "execution", + "timed_out": False, "handler_delivery_verified": False, "performance_claim": False, + "probe_sha256": None, "cleanup_errors": [], "job_after_cleanup": None, + "process_reaped": None} + job = process = images = None + assigned = False + stdout = stderr = b"" + cleanup_details = [] + started = time.monotonic() + + def cleanup_fault(code): + row["cleanup_errors"].append(code) + row["status"] = "failed" + if row["failure_code"] != "cleanup_failed": + row["failure_before_cleanup"] = row["failure_code"] + row["failure_code"] = "cleanup_failed" + cleanup_details.append(traceback.format_exc()) + + try: + row["probe_sha256"] = sha256(exe) + directory = system_directory() + row["system_files_before"] = system_identity(directory) + job = OwnedJob() + process = subprocess.Popen([str(exe)], cwd=private, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + creationflags=subprocess.CREATE_NO_WINDOW) + job.assign(process.pid) + assigned = True + images = OwnedImages(job, directory) + images.start() + try: + if gate is None: + process.wait(timeout=timeout) + stdout, stderr = process.communicate(timeout=2) + else: + stdout, stderr = process.communicate(gate, timeout=timeout) + except subprocess.TimeoutExpired: + row["timed_out"] = True + row["job_at_timeout"] = job.accounting() + job.terminate() + stdout, stderr = process.communicate(timeout=5) + row["returncode"] = process.returncode + row["job_after"] = job.drain() + if row["job_after"]["active"]: + row["forced_job_cleanup"] = True + job.terminate() + row["job_after"] = job.drain() + images.finish() + row["owned_images"] = images.observed + row["image_query_error"] = images.error + row["system_files_after"] = system_identity(directory) + if gate == b"GO\n": + row["native"] = checked_native(json.loads(stdout)) + try: + row["trace"] = validate_trace((private / "output.bin").read_bytes(), + (private / "input.bin").read_bytes()) + except (ValueError, OSError): + row["failure_code"] = "trace_contract" + raise ValueError("trace_contract") from None + validate_sample(row) + row.update(status="passed", failure_code="none") + else: + row["failure_code"] = "guard_test" + except BaseException: + # Preserve the original exception even if private evidence is unwritable. + try: + (private / "exception.txt").write_text(traceback.format_exc(), encoding="utf-8") + except Exception: + cleanup_fault("private_exception_write") + raise + finally: + # Every cleanup stage is independent. In particular, a failed terminate + # or pipe reap must never skip sampler shutdown, kill-on-close or JSON. + try: + if process is not None: + try: + running = process.poll() is None + except Exception: + running = True + cleanup_fault("process_poll") + if running: + try: + if assigned: + job.terminate() + else: + process.kill() # no GO was sent; no children exist + except Exception: + cleanup_fault("process_terminate") + try: + stdout, stderr = process.communicate(timeout=5) + except Exception: + cleanup_fault("process_communicate") + if images is not None: + try: + images.finish() + except Exception: + cleanup_fault("image_sampler_finish") + if job is not None and assigned: + try: + row["job_after_cleanup"] = job.drain() + except Exception: + # Unknown is explicit; a failed query never establishes zero. + row["job_after_cleanup"] = None + cleanup_fault("job_accounting") + if row["job_after_cleanup"] is not None and row["job_after_cleanup"]["active"]: + try: + job.terminate() + except Exception: + cleanup_fault("job_terminate") + try: + row["job_after_cleanup"] = job.drain() + if row["job_after_cleanup"]["active"]: + cleanup_fault("job_not_empty") + except Exception: + row["job_after_cleanup"] = None + cleanup_fault("job_accounting") + finally: + # This is the final containment fallback when explicit termination + # fails. It is attempted before writing any diagnostic artifacts. + if job is not None: + try: + job.close() + except Exception: + cleanup_fault("job_close") + if process is not None: + try: + if process.poll() is None: + stdout, stderr = process.communicate(timeout=5) + row["process_reaped"] = process.poll() is not None + if not row["process_reaped"]: + cleanup_fault("process_not_reaped") + except Exception: + row["process_reaped"] = None + cleanup_fault("process_reap_after_close") + for name, data in (("stdout.json", stdout), ("stderr.txt", stderr)): + try: + (private / name).write_bytes(data) + except Exception: + cleanup_fault("private_stream_write") + if cleanup_details: + try: + (private / "cleanup.exception.txt").write_text("\n".join(cleanup_details), encoding="utf-8") + except Exception: + cleanup_fault("private_cleanup_write") + row["wall_seconds"] = time.monotonic() - started + row["artifacts"] = {} + for name in ("input.bin", "output.bin", "stdout.json", "stderr.txt"): + try: + if (private / name).is_file(): + row["artifacts"][name] = {"sha256": sha256(private / name), "bytes": (private / name).stat().st_size} + except Exception: + cleanup_fault("artifact_identity") + try: + (out / "sample.json").write_text(json.dumps(row, indent=2) + "\n", encoding="utf-8") + except Exception: + cleanup_fault("public_sample_write") + if row["cleanup_errors"]: + raise ValueError("cleanup_failed") + return row + + +def main(): + cli = argparse.ArgumentParser(description=__doc__) + cli.add_argument("--exe", type=Path) + cli.add_argument("--out", type=Path, required=True) + cli.add_argument("--repeats", type=int, default=3) + args = cli.parse_args() + if os.name != "nt" or not 1 <= args.repeats <= 20: + cli.error("Windows and 1..20 fresh repeats are required") + exe = args.exe or binary("conpty_ctrl_c_probe") + args.out.mkdir(parents=True, exist_ok=False) + sources = ["experiments/conpty/ctrl_c_probe.cpp", "experiments/conpty/session.hpp", + "experiments/conpty/ctrl_c_run.py", "experiments/conpty/run.py", "CMakeLists.txt"] + manifest = {"schema": SCHEMA, "status": "failed", "started_utc": datetime.now(timezone.utc).isoformat(), + "probe_sha256": sha256(exe), "source_sha256": {name: sha256(ROOT / name) for name in sources}, + "source_binding": "recorded_inputs_not_independent_build_attestation", + "workload": {"loop_count": 1000000, "busy_hold_min_ms": 1000, "flood_etx_count": 1, + "followup_delay_ms": 0, "marker_timeout_ms": 5000, "outer_timeout_seconds": 15}, + "handler_delivery_verified": False, "performance_claim": False, "requested_repeats": args.repeats, + "passed_repeats": 0} + try: + for repeat in range(args.repeats): + execute(exe, args.out / f"sample-{repeat}") + manifest["passed_repeats"] += 1 + print(f"cmd raw Ctrl-C {repeat + 1}/{args.repeats}: passed", flush=True) + manifest["status"] = "passed" + except Exception: + manifest["failure_code"] = "sample_failed" + print("cmd raw Ctrl-C: failed; inspect private diagnostic artifacts", flush=True) + return 1 + finally: + manifest["finished_utc"] = datetime.now(timezone.utc).isoformat() + (args.out / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/experiments/conpty/real_app_capture.cpp b/experiments/conpty/real_app_capture.cpp index 5a16f11d..b37c4b68 100644 --- a/experiments/conpty/real_app_capture.cpp +++ b/experiments/conpty/real_app_capture.cpp @@ -32,9 +32,42 @@ static void wait_event(tf_conpty::Session& session, const std::filesystem::path& } } +static size_t wait_more_prompt(tf_conpty::Session& session, const std::filesystem::path& capture) { + const auto deadline = GetTickCount64() + 10000; + size_t saved = 0; + write_bytes(capture, {}); + for (;;) { + const auto bytes = session.output(); + if (bytes.size() != saved) { + write_bytes(capture, bytes); saved = bytes.size(); + } + const auto begin = bytes.find("-- More ("); + if (begin != std::string::npos) { + auto end = begin + 9; + while (end < bytes.size() && bytes[end] >= '0' && bytes[end] <= '9') ++end; + if (end > begin + 9 && end <= begin + 12 && bytes.compare(end, 6, "%) -- ") == 0) + return end + 6; + } + require(!session.wait_exit(0), "pager exited before complete prompt"); + require(GetTickCount64() < deadline, "pager complete English prompt missing"); + Sleep(1); // poll the actual complete prompt, never substitute a delay + } +} + +static void require_output_eof(const tf_conpty::Session& session) { + // close() joins the continuously draining reader. Broken-pipe is its actual + // ReadFile endpoint; forced cancellation and process exit alone are weaker. + require(session.cleanup_complete && session.read_error == ERROR_BROKEN_PIPE && + !session.forced_reader_cancel && !session.forced_process_cancel, "capture output EOF was not observed"); +} + int main(int argc, char** argv) { try { require(argc == 3, "usage: real_app_capture output-directory vim-executable"); + char gate[4]{}; + if (!std::cin.getline(gate, sizeof(gate)) || std::string(gate) != "GO") return 2; + BOOL contained = FALSE; + if (!IsProcessInJob(GetCurrentProcess(), nullptr, &contained) || !contained) return 2; const std::filesystem::path out = argv[1], vim = argv[2]; require(std::filesystem::create_directories(out), "output directory already exists"); const auto root = out.parent_path(); @@ -42,29 +75,58 @@ int main(int argc, char** argv) { { tf_conpty::Session session; session.start(L"C:\\Windows\\System32\\cmd.exe", L"/d /q /c \"echo TF_REAL_SHELL\""); - require(session.wait_exit(10000) && session.exit_code == 0, "real shell lifecycle failed"); + const bool shell_exited = session.wait_exit(10000); + write_bytes(out / "shell.bin", session.output()); + require(shell_exited && session.exit_code == 0, "real shell lifecycle failed"); + session.close(); + require_output_eof(session); require(!session.capture_overflow, "real shell capture overflow"); const auto output = session.output(); require(output.find("TF_REAL_SHELL") != std::string::npos, "real shell output missing"); write_bytes(out / "shell.bin", output); shell_bytes = output.size(); - session.close(); - require(session.cleanup_complete, "real shell cleanup failed"); } { std::string input; for (int line = 1; line <= 40; ++line) input += "TF_PAGE_" + std::to_string(line) + "\r\n"; write_bytes(root / "pager.txt", input); tf_conpty::Session session; - session.start(L"C:\\Windows\\System32\\more.com", L"pager.txt", {}, root.wstring()); - require(session.wait_text("TF_PAGE_1", 10000), "pager initial output missing"); - session.send("q"); - require(session.wait_exit(10000) && session.exit_code == 0, "pager q lifecycle failed"); - require(!session.capture_overflow, "pager capture overflow"); - write_bytes(out / "pager.bin", session.output()); - pager_bytes = session.output().size(); + std::string stage = "launch"; + size_t prompt_end = 0, q_bytes = 0; + bool valid = false; + try { + session.start(L"C:\\Windows\\System32\\more.com", L"pager.txt", {}, root.wstring()); + stage = "complete-prompt"; prompt_end = wait_more_prompt(session, out / "pager.bin"); + stage = "q-input"; session.send("q", [&](size_t bytes) { q_bytes += bytes; }); + stage = "q-exit"; + require(session.wait_exit(10000) && session.exit_code == 0, "pager q lifecycle failed"); + // Preserve actual bytes even if the subsequent close/EOF stage + // needs the external watchdog. This prefix is never complete. + write_bytes(out / "pager.bin", session.output()); + stage = "output-eof"; session.close(); require_output_eof(session); + require(!session.capture_overflow, "pager capture overflow"); + valid = q_bytes == 1; + } catch (...) { + write_bytes(out / "pager.bin", session.output()); + try { session.cancel(); } catch (...) {} + session.close(); + } session.close(); - require(session.cleanup_complete, "pager cleanup failed"); + const auto output = session.output(); + write_bytes(out / "pager.bin", output); pager_bytes = output.size(); + std::ofstream diagnostic(out / "pager-lifecycle.json", std::ios::trunc); + diagnostic << "{\"schema\":\"termforge-more-capture-v1\",\"valid\":" << (valid ? "true" : "false") + << ",\"stage\":"; termforge::Screen::quoted(diagnostic, stage); + diagnostic << ",\"prompt_end_offset\":" << prompt_end << ",\"q_bytes\":" << q_bytes + << ",\"exit_code\":" << session.exit_code << ",\"output_bytes\":" << output.size() + << ",\"read_error\":" << session.read_error + << ",\"output_eof\":" << (session.read_error == ERROR_BROKEN_PIPE ? "true" : "false") + << ",\"cleanup_complete\":" << (session.cleanup_complete ? "true" : "false") + << ",\"active_children_after\":" << session.active_after + << ",\"forced_reader_cancel\":" << (session.forced_reader_cancel ? "true" : "false") + << ",\"forced_process_cancel\":" << (session.forced_process_cancel ? "true" : "false") << "}\n"; + require(diagnostic.good(), "pager lifecycle evidence write failed"); + require(valid, "pager lifecycle failed; preserved complete captured bytes and stage"); } { tf_conpty::Session session; @@ -84,11 +146,12 @@ int main(int argc, char** argv) { stage = "write-quit"; session.send("\x1b:wq\r"); require(session.wait_exit(15000) && session.exit_code == 0, "editor wq lifecycle failed"); + write_bytes(out / "editor.bin", session.output()); + session.close(); require_output_eof(session); stage = "capture"; require(!session.capture_overflow, "editor capture overflow"); write_bytes(out / "editor.bin", session.output()); editor_bytes = session.output().size(); - session.close(); stage = "cleanup"; require(session.cleanup_complete, "editor cleanup failed"); std::ifstream edited(root / "editor.txt", std::ios::binary); @@ -96,6 +159,7 @@ int main(int argc, char** argv) { stage = "file-content"; require(content.find("TF_VIM_REAL") != std::string::npos, "editor file content missing"); } catch (const std::exception& error) { + write_bytes(out / "editor.bin", session.output()); try { session.cancel(); } catch (...) {} session.close(); const auto output = session.output(); diff --git a/experiments/conpty/results/2026-10-26-ctrl-c-retest/manifest.json b/experiments/conpty/results/2026-10-26-ctrl-c-retest/manifest.json new file mode 100644 index 00000000..167ed203 --- /dev/null +++ b/experiments/conpty/results/2026-10-26-ctrl-c-retest/manifest.json @@ -0,0 +1,27 @@ +{ + "schema": "termforge-conpty-ctrl-c-v1", + "status": "passed", + "started_utc": "2026-10-26T03:27:38.423953+00:00", + "probe_sha256": "0e8db147c1418ac4c6b0e5b1b7343198987ca4bd7b1deedf0cac4cdb64905184", + "source_sha256": { + "experiments/conpty/ctrl_c_probe.cpp": "5e7022cb266818db206279a3d3075fff338688bcd16742b2cae38a09936b06f5", + "experiments/conpty/session.hpp": "f143a858df22302a2bc4a794263ff7769f4d0cfd9537307d916feb8ad638c6b6", + "experiments/conpty/ctrl_c_run.py": "698e76ac95b7de74b0479b5dbc79596137ad770c28b9185140ca790389752c9b", + "experiments/conpty/run.py": "c8eef1e36b6d200b2d508cef91eaa5ca79a91fa87c5daf6265b2475c31dce329", + "CMakeLists.txt": "f79c1f8c887e65c4248408725f051ee5ac8dbe93e91e0ba755be2f4edf7610db" + }, + "source_binding": "recorded_inputs_not_independent_build_attestation", + "workload": { + "loop_count": 1000000, + "busy_hold_min_ms": 1000, + "flood_etx_count": 1, + "followup_delay_ms": 0, + "marker_timeout_ms": 5000, + "outer_timeout_seconds": 15 + }, + "handler_delivery_verified": false, + "performance_claim": false, + "requested_repeats": 3, + "passed_repeats": 3, + "finished_utc": "2026-10-26T03:27:42.132210+00:00" +} diff --git a/experiments/conpty/results/2026-10-26-ctrl-c-retest/sample-0.json b/experiments/conpty/results/2026-10-26-ctrl-c-retest/sample-0.json new file mode 100644 index 00000000..bdcee114 --- /dev/null +++ b/experiments/conpty/results/2026-10-26-ctrl-c-retest/sample-0.json @@ -0,0 +1,107 @@ +{ + "schema": "termforge-conpty-ctrl-c-v1", + "status": "passed", + "failure_code": "none", + "timed_out": false, + "handler_delivery_verified": false, + "performance_claim": false, + "probe_sha256": "0e8db147c1418ac4c6b0e5b1b7343198987ca4bd7b1deedf0cac4cdb64905184", + "cleanup_errors": [], + "job_after_cleanup": { + "active": 0, + "total": 4, + "terminated": 0 + }, + "process_reaped": true, + "system_files_before": { + "cmd.exe": { + "sha256": "97ac98b1a92c286054cce55239cfccdfc23a5517bd07fe693072c9ca96c7dabb", + "fixed_file_version": "10.0.26100.9278", + "fixed_product_version": "10.0.26100.9278", + "file_version_string": "10.0.26100.8875 (WinBuild.160101.0800)", + "product_version_string": "10.0.26100.8875" + }, + "conhost.exe": { + "sha256": "e449bce01f275cd08f3d4e64bb73b3b43ae845a0dbdb3e6131426e66537705e5", + "fixed_file_version": "10.0.26100.9444", + "fixed_product_version": "10.0.26100.9444", + "file_version_string": "10.0.26100.8875 (WinBuild.160101.0800)", + "product_version_string": "10.0.26100.8875" + } + }, + "returncode": 0, + "job_after": { + "active": 0, + "total": 4, + "terminated": 0 + }, + "owned_images": { + "cmd.exe": true, + "conhost.exe": true + }, + "image_query_error": false, + "system_files_after": { + "cmd.exe": { + "sha256": "97ac98b1a92c286054cce55239cfccdfc23a5517bd07fe693072c9ca96c7dabb", + "fixed_file_version": "10.0.26100.9278", + "fixed_product_version": "10.0.26100.9278", + "file_version_string": "10.0.26100.8875 (WinBuild.160101.0800)", + "product_version_string": "10.0.26100.8875" + }, + "conhost.exe": { + "sha256": "e449bce01f275cd08f3d4e64bb73b3b43ae845a0dbdb3e6131426e66537705e5", + "fixed_file_version": "10.0.26100.9444", + "fixed_product_version": "10.0.26100.9444", + "file_version_string": "10.0.26100.8875 (WinBuild.160101.0800)", + "product_version_string": "10.0.26100.8875" + } + }, + "native": { + "schema": "termforge-ctrl-c-native-v1", + "status": "passed", + "failure_code": "none", + "idle_marker": true, + "flood_observed": true, + "after_marker": true, + "after_marker_lines": 1, + "highest_busy": 8466, + "busy_hold_ms": 1010, + "after_wait_ms": 32, + "input_bytes": 106, + "flood_input_bytes": 21, + "output_bytes": 659025, + "exit_code": 37, + "read_error": 109, + "capture_overflow": false, + "active_children_after": 0, + "cleanup_complete": true, + "forced_process_cancel": false, + "forced_reader_cancel": false, + "elapsed_ms": 1156 + }, + "trace": { + "idle_marker_lines": 1, + "after_marker_lines": 1, + "highest_busy": 8466, + "busy_lines": 8466 + }, + "wall_seconds": 1.2189999998081475, + "artifacts": { + "input.bin": { + "sha256": "ee596b3083d46aba705ab9e63482733c30d17b606fd927b99e695c95632387f0", + "bytes": 106 + }, + "output.bin": { + "sha256": "9463f0ad032289f0f210ccda84e99ab0cacb1ac6f7c16288e72f36d06d87b107", + "bytes": 659025 + }, + "stdout.json": { + "sha256": "ae6f14e37d31be6b65124b4d9e4b7ba58e0351b3ad127a7f371a25ccf3f9cefa", + "bytes": 471 + }, + "stderr.txt": { + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "bytes": 0 + } + } +} diff --git a/experiments/conpty/results/2026-10-26-ctrl-c-retest/sample-1.json b/experiments/conpty/results/2026-10-26-ctrl-c-retest/sample-1.json new file mode 100644 index 00000000..3da0ec9b --- /dev/null +++ b/experiments/conpty/results/2026-10-26-ctrl-c-retest/sample-1.json @@ -0,0 +1,107 @@ +{ + "schema": "termforge-conpty-ctrl-c-v1", + "status": "passed", + "failure_code": "none", + "timed_out": false, + "handler_delivery_verified": false, + "performance_claim": false, + "probe_sha256": "0e8db147c1418ac4c6b0e5b1b7343198987ca4bd7b1deedf0cac4cdb64905184", + "cleanup_errors": [], + "job_after_cleanup": { + "active": 0, + "total": 4, + "terminated": 0 + }, + "process_reaped": true, + "system_files_before": { + "cmd.exe": { + "sha256": "97ac98b1a92c286054cce55239cfccdfc23a5517bd07fe693072c9ca96c7dabb", + "fixed_file_version": "10.0.26100.9278", + "fixed_product_version": "10.0.26100.9278", + "file_version_string": "10.0.26100.8875 (WinBuild.160101.0800)", + "product_version_string": "10.0.26100.8875" + }, + "conhost.exe": { + "sha256": "e449bce01f275cd08f3d4e64bb73b3b43ae845a0dbdb3e6131426e66537705e5", + "fixed_file_version": "10.0.26100.9444", + "fixed_product_version": "10.0.26100.9444", + "file_version_string": "10.0.26100.8875 (WinBuild.160101.0800)", + "product_version_string": "10.0.26100.8875" + } + }, + "returncode": 0, + "job_after": { + "active": 0, + "total": 4, + "terminated": 0 + }, + "owned_images": { + "cmd.exe": true, + "conhost.exe": true + }, + "image_query_error": false, + "system_files_after": { + "cmd.exe": { + "sha256": "97ac98b1a92c286054cce55239cfccdfc23a5517bd07fe693072c9ca96c7dabb", + "fixed_file_version": "10.0.26100.9278", + "fixed_product_version": "10.0.26100.9278", + "file_version_string": "10.0.26100.8875 (WinBuild.160101.0800)", + "product_version_string": "10.0.26100.8875" + }, + "conhost.exe": { + "sha256": "e449bce01f275cd08f3d4e64bb73b3b43ae845a0dbdb3e6131426e66537705e5", + "fixed_file_version": "10.0.26100.9444", + "fixed_product_version": "10.0.26100.9444", + "file_version_string": "10.0.26100.8875 (WinBuild.160101.0800)", + "product_version_string": "10.0.26100.8875" + } + }, + "native": { + "schema": "termforge-ctrl-c-native-v1", + "status": "passed", + "failure_code": "none", + "idle_marker": true, + "flood_observed": true, + "after_marker": true, + "after_marker_lines": 1, + "highest_busy": 9935, + "busy_hold_ms": 1010, + "after_wait_ms": 17, + "input_bytes": 106, + "flood_input_bytes": 21, + "output_bytes": 773670, + "exit_code": 37, + "read_error": 109, + "capture_overflow": false, + "active_children_after": 0, + "cleanup_complete": true, + "forced_process_cancel": false, + "forced_reader_cancel": false, + "elapsed_ms": 1135 + }, + "trace": { + "idle_marker_lines": 1, + "after_marker_lines": 1, + "highest_busy": 9935, + "busy_lines": 9935 + }, + "wall_seconds": 1.202999999979511, + "artifacts": { + "input.bin": { + "sha256": "ee596b3083d46aba705ab9e63482733c30d17b606fd927b99e695c95632387f0", + "bytes": 106 + }, + "output.bin": { + "sha256": "d95f7601695b5d9eed4613fbd20e57b32dea9a60babc95cc7b5d31884b27ba9d", + "bytes": 773670 + }, + "stdout.json": { + "sha256": "d8c91e416bf96614656258729ee4482e6efd222a19f73332aa0b386d7a5b8f82", + "bytes": 471 + }, + "stderr.txt": { + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "bytes": 0 + } + } +} diff --git a/experiments/conpty/results/2026-10-26-ctrl-c-retest/sample-2.json b/experiments/conpty/results/2026-10-26-ctrl-c-retest/sample-2.json new file mode 100644 index 00000000..a0e59c06 --- /dev/null +++ b/experiments/conpty/results/2026-10-26-ctrl-c-retest/sample-2.json @@ -0,0 +1,107 @@ +{ + "schema": "termforge-conpty-ctrl-c-v1", + "status": "passed", + "failure_code": "none", + "timed_out": false, + "handler_delivery_verified": false, + "performance_claim": false, + "probe_sha256": "0e8db147c1418ac4c6b0e5b1b7343198987ca4bd7b1deedf0cac4cdb64905184", + "cleanup_errors": [], + "job_after_cleanup": { + "active": 0, + "total": 4, + "terminated": 0 + }, + "process_reaped": true, + "system_files_before": { + "cmd.exe": { + "sha256": "97ac98b1a92c286054cce55239cfccdfc23a5517bd07fe693072c9ca96c7dabb", + "fixed_file_version": "10.0.26100.9278", + "fixed_product_version": "10.0.26100.9278", + "file_version_string": "10.0.26100.8875 (WinBuild.160101.0800)", + "product_version_string": "10.0.26100.8875" + }, + "conhost.exe": { + "sha256": "e449bce01f275cd08f3d4e64bb73b3b43ae845a0dbdb3e6131426e66537705e5", + "fixed_file_version": "10.0.26100.9444", + "fixed_product_version": "10.0.26100.9444", + "file_version_string": "10.0.26100.8875 (WinBuild.160101.0800)", + "product_version_string": "10.0.26100.8875" + } + }, + "returncode": 0, + "job_after": { + "active": 0, + "total": 4, + "terminated": 0 + }, + "owned_images": { + "cmd.exe": true, + "conhost.exe": true + }, + "image_query_error": false, + "system_files_after": { + "cmd.exe": { + "sha256": "97ac98b1a92c286054cce55239cfccdfc23a5517bd07fe693072c9ca96c7dabb", + "fixed_file_version": "10.0.26100.9278", + "fixed_product_version": "10.0.26100.9278", + "file_version_string": "10.0.26100.8875 (WinBuild.160101.0800)", + "product_version_string": "10.0.26100.8875" + }, + "conhost.exe": { + "sha256": "e449bce01f275cd08f3d4e64bb73b3b43ae845a0dbdb3e6131426e66537705e5", + "fixed_file_version": "10.0.26100.9444", + "fixed_product_version": "10.0.26100.9444", + "file_version_string": "10.0.26100.8875 (WinBuild.160101.0800)", + "product_version_string": "10.0.26100.8875" + } + }, + "native": { + "schema": "termforge-ctrl-c-native-v1", + "status": "passed", + "failure_code": "none", + "idle_marker": true, + "flood_observed": true, + "after_marker": true, + "after_marker_lines": 1, + "highest_busy": 7344, + "busy_hold_ms": 1011, + "after_wait_ms": 33, + "input_bytes": 106, + "flood_input_bytes": 21, + "output_bytes": 571559, + "exit_code": 37, + "read_error": 109, + "capture_overflow": false, + "active_children_after": 0, + "cleanup_complete": true, + "forced_process_cancel": false, + "forced_reader_cancel": false, + "elapsed_ms": 1171 + }, + "trace": { + "idle_marker_lines": 1, + "after_marker_lines": 1, + "highest_busy": 7344, + "busy_lines": 7344 + }, + "wall_seconds": 1.25, + "artifacts": { + "input.bin": { + "sha256": "ee596b3083d46aba705ab9e63482733c30d17b606fd927b99e695c95632387f0", + "bytes": 106 + }, + "output.bin": { + "sha256": "42c2791d797b34026e7def7648b914371a452658e02073d3fe9e899260fba2c9", + "bytes": 571559 + }, + "stdout.json": { + "sha256": "d4a1cad91bbd4d7fdf27f086e274757f6ecb5a0988bfece37ee2f6295efd4838", + "bytes": 471 + }, + "stderr.txt": { + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "bytes": 0 + } + } +} diff --git a/experiments/conpty/results/2026-10-26-ctrl-c-retest/validation.json b/experiments/conpty/results/2026-10-26-ctrl-c-retest/validation.json new file mode 100644 index 00000000..ddc4f3aa --- /dev/null +++ b/experiments/conpty/results/2026-10-26-ctrl-c-retest/validation.json @@ -0,0 +1,35 @@ +{ + "schema": "termforge-ctrl-c-local-audit-v1", + "status": "bounded-probe-passed-historical-failure-unresolved", + "recorded_source_commit": "c44ae98", + "source_binding": "recorded_inputs_not_independent_build_attestation", + "samples": [ + { + "sample": 0, + "raw_hashes_and_sizes_recomputed": true, + "raw_trace_recomputed": true, + "handler_delivery_verified": false + }, + { + "sample": 1, + "raw_hashes_and_sizes_recomputed": true, + "raw_trace_recomputed": true, + "handler_delivery_verified": false + }, + { + "sample": 2, + "raw_hashes_and_sizes_recomputed": true, + "raw_trace_recomputed": true, + "handler_delivery_verified": false + } + ], + "raw_transport_published": false, + "issue_36_closed": false, + "handler_delivery_verified": false, + "performance_claim": false, + "limitations": [ + "Historical failure is not reproduced; no product Ctrl-C fix.", + "Raw transport was checked locally and remains private; public hashes cannot alone reproduce that audit.", + "No independent native control-handler delivery, source-to-binary attestation, or performance claim." + ] +} diff --git a/experiments/conpty/test_ctrl_c.py b/experiments/conpty/test_ctrl_c.py new file mode 100644 index 00000000..f1204e42 --- /dev/null +++ b/experiments/conpty/test_ctrl_c.py @@ -0,0 +1,189 @@ +"""Independent raw-output oracle and owned, unattended cmd interrupt gate.""" +import copy +import json +import os +from pathlib import Path +import tempfile +import unittest +from unittest import mock + +from experiments.build_paths import ROOT, binary +from experiments.conpty.ctrl_c_run import COMPLETE_INPUT, execute, validate_sample, validate_trace +from experiments.conpty import ctrl_c_run as runner + + +class CtrlCOracleTests(unittest.TestCase): + def test_command_echo_does_not_establish_marker_execution(self): + for raw in (b"TF>echo TF_AFTER_IDLE\r\nTF>echo TF_AFTER_FLOOD\r\n", + b"TF_AFTER_IDLE\r\nTF_BUSY_42\r\necho TF_AFTER_FLOOD\r\n", + b"TF_AFTER_IDLE\r\nTF_BUSY_42\r\nTF_AFTER_FLOOD"): + with self.subTest(raw=raw), self.assertRaises(ValueError): + validate_trace(raw, COMPLETE_INPUT) + + def test_marker_order_and_natural_completion_are_rejected(self): + for raw in (b"TF_AFTER_IDLE\nTF_BUSY_1000000\nTF_AFTER_FLOOD\n", + b"TF_AFTER_IDLE\nTF_AFTER_FLOOD\nTF_BUSY_3\n", + b"TF_BUSY_3\nTF_AFTER_IDLE\nTF_AFTER_FLOOD\n", + b"TF_AFTER_IDLE\nTF_BUSY_3\nTF_AFTER_FLOOD\nTF_AFTER_FLOOD\n", + b"TF_AFTER_IDLE\nTF_AFTER_FLOOD\n"): + with self.subTest(raw=raw), self.assertRaises(ValueError): + validate_trace(raw, COMPLETE_INPUT) + + def test_full_output_lines_and_exact_input_are_required(self): + raw = (b"TF>echo TF_AFTER_IDLE\r\n\x1b[0mTF_AFTER_IDLE\r\nTF_BUSY_123\r\n" + b"echo TF_AFTER_FLOOD\r\n\x1b[?25hTF_AFTER_FLOOD\r\n") + result = validate_trace(raw, COMPLETE_INPUT) + self.assertEqual(result, dict(idle_marker_lines=1, after_marker_lines=1, + highest_busy=123, busy_lines=1)) + for wrong in (COMPLETE_INPUT.replace(b"1000000", b"1000"), + COMPLETE_INPUT.replace(b"\x03echo TF_AFTER_FLOOD", b"\x03\x03echo TF_AFTER_FLOOD"), + COMPLETE_INPUT[:-1]): + with self.assertRaises(ValueError): + validate_trace(raw, wrong) + + +@unittest.skipUnless(os.name == "nt", "Windows runner cleanup is mocked without starting native processes") +class CtrlCCleanupFailureTests(unittest.TestCase): + def mocked_run(self, temporary, process, job, images, *, gate=b"GO\n"): + exe = Path(temporary) / "mock-probe.exe" + exe.write_bytes(b"identity-only fixture; never executed") + out = Path(temporary) / "sample" + identity = {"cmd.exe": {"sha256": "a" * 64, "fixed_file_version": "1.2.3.4"}, + "conhost.exe": {"sha256": "b" * 64, "fixed_file_version": "1.2.3.4"}} + with mock.patch.object(runner, "system_directory", return_value=Path(temporary)), \ + mock.patch.object(runner, "system_identity", return_value=identity), \ + mock.patch.object(runner, "OwnedJob", return_value=job), \ + mock.patch.object(runner, "OwnedImages", return_value=images), \ + mock.patch.object(runner.subprocess, "Popen", return_value=process): + return runner.execute(exe, out, gate=gate) + + def mocks(self): + process, job, images = mock.Mock(), mock.Mock(), mock.Mock() + process.pid, process.returncode = 123, None + job.accounting.return_value = {"active": 1, "total": 1, "terminated": 0} + job.drain.return_value = {"active": 0, "total": 1, "terminated": 0} + images.observed, images.error = {"cmd.exe": False, "conhost.exe": False}, False + return process, job, images + + def test_terminate_and_reap_failures_preserve_original_and_unknown_accounting(self): + process, job, images = self.mocks() + original = OSError("private original termination failure") + process.poll.return_value = None + process.communicate.side_effect = [runner.subprocess.TimeoutExpired("private-command", 1)] * 3 + job.terminate.side_effect = [original, OSError("private cleanup termination failure")] + job.drain.side_effect = OSError("private accounting failure") + with tempfile.TemporaryDirectory() as temporary: + with self.assertRaises(OSError) as raised: + self.mocked_run(temporary, process, job, images) + self.assertIs(raised.exception, original) + row = json.loads((Path(temporary) / "sample/sample.json").read_text()) + self.assertEqual(row["status"], "failed") + self.assertEqual(row["failure_code"], "cleanup_failed") + self.assertIsNone(row["job_after_cleanup"]) + self.assertIsNone(row["process_reaped"]) + self.assertIn("process_terminate", row["cleanup_errors"]) + self.assertIn("process_communicate", row["cleanup_errors"]) + self.assertIn("job_accounting", row["cleanup_errors"]) + self.assertNotIn("private original", json.dumps(row)) + self.assertIn("private original", (Path(temporary) / "sample/_private/exception.txt").read_text()) + with self.assertRaises(ValueError): + validate_sample(row) + images.finish.assert_called_once() + job.close.assert_called_once() + + def test_sampler_failure_cannot_skip_job_close_or_public_evidence(self): + process, job, images = self.mocks() + original = ValueError("private original protocol failure") + process.poll.side_effect = [None, 0, 0] + process.communicate.side_effect = [original, runner.subprocess.TimeoutExpired("private-command", 1)] + images.finish.side_effect = OSError("private sampler failure") + with tempfile.TemporaryDirectory() as temporary: + with self.assertRaises(ValueError) as raised: + self.mocked_run(temporary, process, job, images) + self.assertIs(raised.exception, original) + row = json.loads((Path(temporary) / "sample/sample.json").read_text()) + self.assertIn("image_sampler_finish", row["cleanup_errors"]) + self.assertEqual(row["job_after_cleanup"]["active"], 0) + self.assertTrue(row["process_reaped"]) + self.assertEqual(row["status"], "failed") + job.close.assert_called_once() + + def test_cleanup_fault_after_normal_body_return_is_raised_and_never_accepted(self): + process, job, images = self.mocks() + process.returncode = 2 + process.poll.side_effect = [None, 0, 0] + process.communicate.side_effect = [(b"guard rejected", b""), + runner.subprocess.TimeoutExpired("private-command", 1)] + job.terminate.side_effect = OSError("private cleanup termination failure") + with tempfile.TemporaryDirectory() as temporary: + with self.assertRaisesRegex(ValueError, "^cleanup_failed$"): + self.mocked_run(temporary, process, job, images, gate=b"") + row = json.loads((Path(temporary) / "sample/sample.json").read_text()) + self.assertEqual(row["failure_code"], "cleanup_failed") + self.assertEqual(row["status"], "failed") + self.assertEqual(row["cleanup_errors"], ["process_terminate", "process_communicate"]) + self.assertEqual(len(row["probe_sha256"]), 64) + with self.assertRaises(ValueError): + validate_sample(row) + images.finish.assert_called() + job.close.assert_called_once() + + +@unittest.skipUnless(os.name == "nt", "raw ConPTY requires Windows") +class CtrlCIntegrationTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.exe = binary("conpty_ctrl_c_probe") + parent = Path(os.environ.get("TERMFORGE_TEST_ARTIFACTS", ROOT / "build/test-artifacts")) / "conpty-ctrl-c" + parent.mkdir(parents=True, exist_ok=True) + cls.out = Path(tempfile.mkdtemp(prefix="run-", dir=parent)) + + def test_original_cmd_flood_interrupt_and_cleanup_three_fresh(self): + for repeat in range(3): + row = execute(self.exe, self.out / f"cmd-{repeat}") + self.assertEqual(row["status"], "passed") + self.assertFalse(row["handler_delivery_verified"]) + self.assertEqual(row["job_after_cleanup"]["active"], 0) + for key, value in (("after_marker", False), ("highest_busy", 1000000), + ("busy_hold_ms", 999), ("flood_input_bytes", 20), + ("exit_code", 0), ("cleanup_complete", False), + ("forced_reader_cancel", True)): + bad = copy.deepcopy(row) + bad["native"][key] = value + with self.subTest(field=key), self.assertRaises(ValueError): + validate_sample(bad) + for key, value in (("image_query_error", True), ("owned_images", {"cmd.exe": True}), + ("job_after", {"active": 1}), ("timed_out", True)): + bad = copy.deepcopy(row) + bad[key] = value + with self.subTest(field=key), self.assertRaises(ValueError): + validate_sample(bad) + + def test_eof_and_wrong_gate_cannot_start_conpty(self): + for number, gate in enumerate((b"", b"NOT_GO\n", b"G" * 64 + b"\n")): + out = self.out / f"guard-{number}" + row = execute(self.exe, out, gate=gate) + self.assertEqual(row["returncode"], 2) + # CREATE_NO_WINDOW can create the console host's own headless + # conhost. A PTY client or an additional ConPTY is not permitted. + self.assertIn(row["job_after"]["total"], (1, 2)) + self.assertEqual(row["job_after_cleanup"]["active"], 0) + self.assertFalse(row["owned_images"]["cmd.exe"]) + self.assertFalse((out / "_private/input.bin").exists()) + self.assertFalse((out / "_private/output.bin").exists()) + native = json.loads((out / "_private/stdout.json").read_bytes()) + self.assertEqual(native["status"], "guard_rejected") + + def test_withheld_gate_timeout_recovers_only_owned_host(self): + row = execute(self.exe, self.out / "withheld-go", gate=None, timeout=0.3) + self.assertTrue(row["timed_out"]) + self.assertIn(row["job_at_timeout"]["total"], (1, 2)) + self.assertEqual(row["job_at_timeout"]["active"], row["job_at_timeout"]["total"]) + self.assertEqual(row["job_at_timeout"]["terminated"], 0) + self.assertFalse(row["owned_images"]["cmd.exe"]) + self.assertEqual(row["job_after_cleanup"]["active"], 0) + self.assertFalse((self.out / "withheld-go/_private/input.bin").exists()) + + +if __name__ == "__main__": + unittest.main() diff --git a/experiments/core/CANDIDATE.md b/experiments/core/CANDIDATE.md index edaeed32..1800c7f7 100644 --- a/experiments/core/CANDIDATE.md +++ b/experiments/core/CANDIDATE.md @@ -1,6 +1,6 @@ # 候选 C++ 终端适配器 -`CandidateTerminal` 是 #27 的拥有值适配层,不是自研核心的替代品声明。它把固定 `0.26.1-dev+termforge.2` 的 `Term/Processor` 接到现有 `Screen`/`Line`;默认 CMake 保留自研核心且不需要 Rust,候选产品由 `TERMFORGE_BUILD_CANDIDATE_ADAPTER=ON` 显式启用。 +`CandidateTerminal` 是 #27 的拥有值适配层,不是自研核心的替代品声明。它把固定 `0.26.1-dev+termforge.3` 的 `Term/Processor` 接到现有 `Screen`/`Line`;默认 CMake 保留自研核心且不需要 Rust,候选产品由 `TERMFORGE_BUILD_CANDIDATE_ADAPTER=ON` 显式启用。 ## 构建与验证 @@ -31,6 +31,8 @@ Linux 使用相应 CMake 配置。候选 workflow 在 Windows/Ubuntu 构建 `can `candidate-scroll` 通过真实辅助进程设置 DECSTBM 2;3r 并在区域底部连续换行,保存 before、region、final 的真实控制台状态。两轮[限定验证](../shell/results/2026-10-26-scroll-region/validation.json)要求区域外文字保持、C/D 位于正确区域、A/B 不混入主历史,光标与原生最终状态一致。ConPTY 的 transport 可能采用不同重绘方式,因此独立使用固定源 VT、原生阶段快照及 `pyte==0.8.2` 重放,不要求原样回显控制序列。origin/wrap、主/备用屏各自 margin 和 resize 交叉仍未验收。 +`candidate-margins` 固定主屏 `2;3r`、备用屏 `4;5r`,保存源 VT、四阶段真实控制台快照和 transport。[限定证据](../shell/results/2026-10-26-margin-state/validation.json)分别检查公开 feed 的主屏 margin 恢复、resize/RIS 边界与真实 ConPTY 的宿主差异。源 VT 的整段/逐字节候选回归通过;当前真实 ConPTY 只恢复主屏内容/样式/光标,未恢复主屏 margin,最终出现 `REGION_CIXED`,由 [#41](https://github.com/JavaZeroo/termforge/issues/41) 保留。真实观察返回 `margins_per_buffer_verified=false`,不是能力通过;源 LF 保持列与 transport 的 CRLF 分别核对,未修改输入或注入恢复命令。 + `candidate-unicode` 另以真实辅助进程输出固定分解组合、单 emoji、ZWJ 和 keycap,保存源字节、真实控制台 `CHAR_INFO`、ConPTY transport、候选单元格及实际 DWrite glyph。两轮[限定验证](../shell/results/2026-10-26-unicode-renderer/validation.json)要求原文保留、glyph 无缺字且布局不超出原网格;过宽文字只缩小字号。当前 ZWJ 分格和 keycap 宽度是观测结果,不是完整聚类合同;完整流式字素与宽度由 [#39](https://github.com/JavaZeroo/termforge/issues/39) 阻塞,不能将本场景写成完整 Unicode/IME 验收。 原有 `runtime_observation` 也在候选适配构建运行,保留滚动后的逐行内容、retained/full RGB 一致、no-op 状态端点及会话清理断言。所有此类检查只使用自有窗口的 DIB,不截图,`presented_done` 仍未知。 @@ -39,4 +41,4 @@ Linux 使用相应 CMake 配置。候选 workflow 在 Windows/Ubuntu 构建 `can `candidate_core_test` 覆盖 whole/逐字节、Unicode/组合标记、备用屏恢复、真实光标回复、空载荷事件、稳定 ID/版本/淘汰、选择/搜索/HistoryViewport、软硬换行、滚动区、ED1/ED2、resize 和错误参数。新增真实 ConPTY 重放的唯一行身份、Find 增量收敛、整篇/局部选区、连续同步、历史合并/清空及超限释放检查。证据见[适配验证](results/2026-10-25-candidate-adapter/validation.json)和[resize/历史验证](results/2026-10-26-resize-replay/validation.json)。 -候选整合不表示 #27 已关闭;#29 的限定 resize/历史实现与证据不表示完整 R1 或 R2 已通过。完整 CTest 及 #36/#38 的原始失败在新记录同步;#28 的更多真实 TUI、#31 的 DPI/呈现、#35 的交付和性能验收保持开放。#37 的独立 Windows CI 验收已通过。 +候选整合不表示 #27 已关闭;#29 的限定 resize/历史实现与证据不表示完整 R1 或 R2 已通过。完整 CTest 及 #36/#38/#42 的原始失败在新记录同步;#28 的更多真实 TUI、#31 的 DPI/呈现、#35 的交付和性能验收保持开放。#37 的独立 Windows CI 验收已通过。 diff --git a/experiments/core/README.md b/experiments/core/README.md index 55f8a87b..97d8b71b 100644 --- a/experiments/core/README.md +++ b/experiments/core/README.md @@ -18,6 +18,7 @@ | `CSI A/B/C/D` | 相对移动,省略或 0 表示 1,限制到屏幕边界 | | `CSI H/f`、`G`、`d` | 行列定位、列定位、行定位;输入为 1 起始坐标,省略或 0 表示 1 | | `CSI J/K` 的 0、1、2 | 向后、向前或全部擦除,包含当前位置;填充空格,不移动光标,不清空历史 | +| `CSI n X` | 擦除当前位置起 n 格;省略或 0 为 1,裁剪到本行右边界,不移动光标,清除待换行;[来源与 More 回归](../../docs/research/more-capture-ech.md) | | UTF-8 | 跨块解码 1–4 字节;严格拒绝过长编码、代理区、越界、孤立续字节及截断输入 | | `CSI m` | 0 重置、1/22 粗体开关;30–37/40–47 和 90–97/100–107 前景/背景色;39/49 恢复默认色 | diff --git a/experiments/core/RECOVERY.md b/experiments/core/RECOVERY.md index 4d1bd528..8283180c 100644 --- a/experiments/core/RECOVERY.md +++ b/experiments/core/RECOVERY.md @@ -55,7 +55,7 @@ build/bin/Release/recovery_probe.exe input.bin 24 80 100 1,7,2,4096 recovery 可选第七个参数 `finish_offset` 在指定字节偏移执行 EOF 屏障,然后继续输入,用于验证截断清理与恢复;探针每个屏障重复调用 `finish()`。输入上限 64 MiB,分块模式每项 1..1,048,576 字节、最多 1,024 项;子进程测试超时 15 秒。每条失败保留输入、命令参数和 stdout/stderr 到 `TERMFORGE_TEST_ARTIFACTS/recovery`,未设变量时使用 `build/test-artifacts/recovery`。 -30 项测试的期望文本和样式独立手写;非法 UTF-8 子段额外用 Python 标准解码器交叉检查。短案例遍历固定块长并用 seed `20261023` 产生非均匀拆包;长案例覆盖百万字节 ESC intermediate、CSI 数字、OSC 正文。还覆盖取消、内嵌 ESC、C1 边界、无效 SGR 原子性、标量/组合标记限制、EOF 重入和恢复后的合法文本。任意字节流测试验证不同拆包结果一致,并在显式取消后执行已知合法文本。严格模式负面用例与共同子集快照也保持回归。 +33 项测试的期望文本和样式独立手写;新增 ECH 计数、边界、光标、pending wrap、宽字政策及完整 More 字节断言,来源与保留差异见 [ECH 合同](../../docs/research/more-capture-ech.md)。非法 UTF-8 子段额外用 Python 标准解码器交叉检查。短案例遍历固定块长并用 seed `20261023` 产生非均匀拆包;长案例覆盖百万字节 ESC intermediate、CSI 数字、OSC 正文。还覆盖取消、内嵌 ESC、C1 边界、无效 SGR 原子性、标量/组合标记限制、EOF 重入和恢复后的合法文本。任意字节流测试验证不同拆包结果一致,并在显式取消后执行已知合法文本。严格模式负面用例与共同子集快照也保持回归。 ## 依据与边界 diff --git a/experiments/core/candidate_terminal_test.cpp b/experiments/core/candidate_terminal_test.cpp index a3a79a65..7ae9b805 100644 --- a/experiments/core/candidate_terminal_test.cpp +++ b/experiments/core/candidate_terminal_test.cpp @@ -3,6 +3,7 @@ #include "text_document.hpp" #include "text_search.hpp" #include "text_selection.hpp" +#include "text_projection.hpp" #include #include #include @@ -39,6 +40,8 @@ void fragmentation() { require(whole.screen.history[0][2].width == 0 && whole.screen.history[0][2].written, "wide character spacer was not preserved as written"); require(whole.screen.history[0][3].marks == 1, "combining mark missing"); + require(termforge::project_line(whole.screen.history[0]).text == "A中e\xCC\x81😀", + "canonical text projection must omit wide spacer payloads and preserve literal scalars"); for (const auto& cell : whole.screen.history[0]) require(cell.width != 0 || cell.written, "unused cell was incorrectly treated as a wide spacer"); require(whole.screen.history[0][7].width == 1 && whole.screen.history[0][7].text == " " && diff --git a/experiments/core/fixtures/more-pager-360.bin b/experiments/core/fixtures/more-pager-360.bin new file mode 100644 index 00000000..55bfa8f0 --- /dev/null +++ b/experiments/core/fixtures/more-pager-360.bin @@ -0,0 +1,24 @@ +[?9001h[?1004h[?25l]0;C:\Windows\System32\more.com[?25hTF_PAGE_1 +TF_PAGE_2 +TF_PAGE_3 +TF_PAGE_4 +TF_PAGE_5 +TF_PAGE_6 +TF_PAGE_7 +TF_PAGE_8 +TF_PAGE_9 +TF_PAGE_10 +TF_PAGE_11 +TF_PAGE_12 +TF_PAGE_13 +TF_PAGE_14 +TF_PAGE_15 +TF_PAGE_16 +TF_PAGE_17 +TF_PAGE_18 +TF_PAGE_19 +TF_PAGE_20 +TF_PAGE_21 +TF_PAGE_22 +TF_PAGE_23 +-- More (56%) -- \ No newline at end of file diff --git a/experiments/core/fixtures/more-pager-366.bin b/experiments/core/fixtures/more-pager-366.bin new file mode 100644 index 00000000..d163abe1 --- /dev/null +++ b/experiments/core/fixtures/more-pager-366.bin @@ -0,0 +1,24 @@ +[?9001h[?1004h[?25l]0;C:\Windows\System32\more.com[?25hTF_PAGE_1 +TF_PAGE_2 +TF_PAGE_3 +TF_PAGE_4 +TF_PAGE_5 +TF_PAGE_6 +TF_PAGE_7 +TF_PAGE_8 +TF_PAGE_9 +TF_PAGE_10 +TF_PAGE_11 +TF_PAGE_12 +TF_PAGE_13 +TF_PAGE_14 +TF_PAGE_15 +TF_PAGE_16 +TF_PAGE_17 +TF_PAGE_18 +TF_PAGE_19 +TF_PAGE_20 +TF_PAGE_21 +TF_PAGE_22 +TF_PAGE_23 +-- More (56%) --  \ No newline at end of file diff --git a/experiments/core/screen.hpp b/experiments/core/screen.hpp index 2b8145b9..fea876c0 100644 --- a/experiments/core/screen.hpp +++ b/experiments/core/screen.hpp @@ -229,6 +229,13 @@ class Screen { erase_range(lines[row], mode == 0 ? column : 0, mode == 1 ? column + 1 : width()); wrap_pending = false; invalidate_attachment(); } + void erase_characters(int count) { + if (count < 1) throw InputError("invalid erase character count"); + erase_range(lines[row], column, column + (std::min)(count, width() - column)); + // ECH edits the current physical row without moving the cursor. The + // Microsoft Terminal ECH contract explicitly cancels delayed wrapping. + wrap_pending = false; invalidate_attachment(); + } void erase_display(int mode) { if (mode > 2) throw InputError("unsupported erase mode"); for (int r = 0; r < height(); ++r) diff --git a/experiments/core/terminal.hpp b/experiments/core/terminal.hpp index 0047ccc0..17dfe8b8 100644 --- a/experiments/core/terminal.hpp +++ b/experiments/core/terminal.hpp @@ -39,6 +39,7 @@ class Parser { case 'H': case 'f': screen.move(n - 1, params.size() < 2 || params[1] == 0 ? 0 : params[1] - 1); break; case 'J': screen.erase_display(params[0]); break; case 'K': screen.erase_line(params[0]); break; + case 'X': screen.erase_characters(n); break; default: throw InputError("unsupported CSI command"); } } diff --git a/experiments/core/test_recovery.py b/experiments/core/test_recovery.py index c0fab577..5d2b2d28 100644 --- a/experiments/core/test_recovery.py +++ b/experiments/core/test_recovery.py @@ -23,9 +23,9 @@ def tearDown(self): if self.last_run and any(test is self for test, _ in result.failures + result.errors): self.preserve(*self.last_run) - def run_input(self, data, chunks, *, mode="recovery", cols=80, finish=None): + def run_input(self, data, chunks, *, mode="recovery", rows=2, cols=80, finish=None): self.path.write_bytes(data) - command = [str(binary("recovery_probe")), str(self.path), "2", str(cols), "2", + command = [str(binary("recovery_probe")), str(self.path), str(rows), str(cols), "2", ",".join(map(str, chunks)), mode] if finish is not None: command.append(str(finish)) @@ -91,6 +91,49 @@ def expect(self, data, text, *, stats=None, cols=80, finish=None, long=False): def test_unknown_escape_and_intermediates(self): self.expect(b"A\x1b7\x1b(B\x1b#8Z", "AZ", stats={"ignored_sequences": 3}) + def test_ech_default_zero_count_clamp_and_cursor_are_independently_expected(self): + for sequence, expected in ((b"\x1b[X", "a cd"), (b"\x1b[0X", "a cd"), + (b"\x1b[2X", "a d"), (b"\x1b[79X", "a ")): + data = b"abcd\r\nWXYZ\x1b[1;2H" + sequence + for mode in ("strict", "recovery"): + for chunks in ([1], [2, 3, 7], [4096]): + with self.subTest(sequence=sequence, mode=mode, chunks=chunks): + result = self.run_input(data, chunks, mode=mode, cols=4) + self.assertEqual(result.returncode, 0, result.stderr) + value = json.loads(result.stdout) + self.assertEqual(value["screen"]["lines"], [expected, "WXYZ"]) + self.assertEqual((value["screen"]["row"], value["screen"]["column"]), (0, 1)) + self.assertFalse(value["screen"]["wrap_pending"]) + + def test_ech_resets_pending_wrap_and_preserves_existing_wide_glyph_policy(self): + cases = [(b"ABCD\x1b[X", ["ABC ", " "], 3, False), + (b"ABCD\x1b[XZ", ["ABCZ", " "], 3, True), + ("中A\x1b[1G\x1b[X".encode(), [" A ", " "], 0, False), + ("中A\x1b[2G\x1b[X".encode(), [" A ", " "], 1, False)] + for data, expected, column, pending in cases: + for chunks in ([1], [3, 1, 7], [4096]): + with self.subTest(data=data, chunks=chunks): + screen = json.loads(self.run_input(data, chunks, cols=4).stdout)["screen"] + self.assertEqual(screen["lines"], expected) + self.assertEqual((screen["row"], screen["column"], screen["wrap_pending"]), (0, column, pending)) + + def test_both_real_more_captures_keep_their_complete_bytes_and_expected_ech_result(self): + fixtures = ROOT / "experiments/core/fixtures" + identities = (("360", "d035d53385788d2b68dcc0d866b5a2fe68897cdcb28a761bd4d36efcc6df6cf9", ") -- ", 5), + ("366", "bb9dbe77353245a2aad7ef07e12da9a8371cc7ac56d6b47ebc0bfecee35b9958", " ", 0)) + for count, digest, last, column in identities: + data = (fixtures / f"more-pager-{count}.bin").read_bytes() + self.assertEqual(len(data), int(count)) + self.assertEqual(hashlib.sha256(data).hexdigest(), digest) + if count == "366": + self.assertTrue(data.endswith(b"\r\x1b[79X")) + for chunks in ([1], [2, 7, 31], [4096]): + with self.subTest(bytes=count, chunks=chunks): + value = json.loads(self.run_input(data, chunks, rows=3, cols=12).stdout) + self.assertEqual(value["screen"]["lines"], ["TF_PAGE_23 ", "-- More (56%", last]) + self.assertEqual((value["screen"]["row"], value["screen"]["column"]), (2, column)) + self.assertFalse(value["screen"]["wrap_pending"]) + def test_unknown_csi_consumes_private_and_intermediate(self): self.expect(b"A\x1b[?1049h\x1b[1 q\x1b[6nZ", "AZ", stats={"ignored_sequences": 3}) diff --git a/experiments/differential/README.md b/experiments/differential/README.md index 2b54d300..31e4d613 100644 --- a/experiments/differential/README.md +++ b/experiments/differential/README.md @@ -86,6 +86,8 @@ python -m unittest discover -s experiments/differential -p test_candidate_compar ## 真实应用核心轨迹(#25) +More 捕获现在等待完整英文提示,再发送一个 q;进程退出后继续读到真实管道 EOF,所有尾部模式关闭与 ECH 原样保存。捕获宿主有 GO/Job 监督;提示偏移、q 字节数、EOF、退出与回收由独立门槛核对。[#42 合同与固定字节回归](../../docs/research/more-capture-ech.md)明确默认 ECH 和保留的候选 pending-wrap 差异,不以部分捕获或自动差异豁免代替通过。 + `real_app_capture` 通过 ConPTY 启动真实 Windows cmd、`more.com` 和 Vim,分别验证正常退出、`q` 退出及无配置/无交换文件/无 viminfo 的编辑 `:wq` 与文件内容。Vim 握手要求 `VimEnter` 写入 `READY`、`InsertEnter` 写入 `INSERT`,并观察到 `[New]` 和实际插入文本,再发送 Escape 与 `:wq`,不以一个 ESC 或固定延时代替状态;等待真实事件文件,不依赖消息是否被重绘覆盖。manifest 记录真实编辑器路径和事件验证结果,事件文件一并保留。失败时保存阶段、错误、退出码、合成输出和 Job 清理结果,真实负例验证这些证据可用。`real_app_matrix.py` 将捕获字节原样交给双方核心,自研使用产品同源的 Recovery 模式;它不是截图或 GUI 验收。 [2026-10-25 摘要](results/2026-10-25-real-applications/summary.json)记录应用版本、二进制/源码哈希和退出/文件结果:分页器和 Vim 为 `match`,shell 仅有已裁决的 ED2 历史策略 `reference_difference`。新增 3 个门槛后,差分、能力和真实应用测试合计 19 项通过;Windows candidate workflow 强制构建并执行,其他平台或缺少 Vim/捕获依赖时明确跳过可选测试。 @@ -120,4 +122,10 @@ python -m unittest discover -s experiments/differential -p test_candidate_compar `0.26.1-dev+termforge.2` 增加滚动复用/初始 ID 顺序、历史淘汰和实际 C++/C ABI 适配。[v5 共同矩阵](results/2026-10-25-candidate-common-v5/summary.json)继续 95 次全部一致,[真实应用 v3](results/2026-10-25-real-applications-v3/summary.json)3 次一致,[fuzz v3](results/2026-10-25-fuzz-v3/summary.json)为 2 已知差异+6 一致,[扩展能力 v4](results/2026-10-25-capabilities-v4/summary.json)分类不变。142 个固定 crate 单元测试、doc test、23 个差分 Python、15 个计量与 C++ candidate_core 均通过;[适配验证](../../experiments/core/results/2026-10-25-candidate-adapter/validation.json)另行记录。默认 shell 和产品 TUI 仍未验收。 +## termforge.3 每缓冲区 margin 与宿主差异(#27/#41) + +`0.26.1-dev+termforge.3` 随主/备用网格保存独立滚动区域,并在 resize/RIS 同时重置。[v6 共同矩阵](results/2026-10-26-candidate-common-v6/summary.json)为 95 次一致,[扩展能力 v5](results/2026-10-26-capabilities-v5/summary.json)分类不变,[fuzz v4](results/2026-10-26-fuzz-v4/summary.json)为 2 已知差异+6 一致,[真实应用 v6](results/2026-10-26-real-applications-v6/summary.json)的固定新鲜捕获为 3 次一致。141 个固定 crate 单元测试、1 个 doc test 与 C++ `candidate_core` 均通过。 + +[margin 验证](../../experiments/shell/results/2026-10-26-margin-state/validation.json)分别保留源 VT 的整段/逐字节恢复、resize/RIS 和真实 ConPTY 的宿主差异。真实宿主未恢复主屏 margin 由 #41 保留,差异观察不作为能力通过。另一 366 字节 More 捕获的尾部 `CR + CSI 79X` 暴露 Recovery 擦除差异与捕获同步问题,旧、新候选均与 pyte 一致,由 #42 保留,不自动改判 match 或归因于 margin 修复。 + 路线判断见 [ADR-0003](../../docs/adr/0003-core-reference-and-reuse.md)。 diff --git a/experiments/differential/real_app_matrix.py b/experiments/differential/real_app_matrix.py index f75689ab..a8e55804 100644 --- a/experiments/differential/real_app_matrix.py +++ b/experiments/differential/real_app_matrix.py @@ -8,12 +8,14 @@ import platform import shutil import subprocess +import re from cases import Case from compare_candidate import run_case from experiments.build_paths import binary as native_binary from run import ROOT, sha, write_json from experiments.candidates.alacritty.run import VERSION as CANDIDATE_VERSION, vendor_sources +from experiments.conpty.run import OwnedJob class FileCase(Case): @@ -34,22 +36,88 @@ def capture_binary(): def run_capture(binary, vim, output): if output.exists(): raise ValueError("capture output directory already exists") + job = process = None + assigned = False + stdout = stderr = b"" + supervision = dict(schema="termforge-real-app-capture-supervision-v1", timed_out=False, + returncode=None, job_after=None, cleanup_errors=[]) try: - process = subprocess.run([str(binary), str(output), str(vim)], capture_output=True, timeout=60) - except subprocess.TimeoutExpired as error: - raise RuntimeError("real application capture timed out") from error - output.mkdir(parents=True, exist_ok=True) - (output / "capture.stdout.txt").write_bytes(process.stdout) - (output / "capture.stderr.txt").write_bytes(process.stderr) - if process.returncode != 0: - raise RuntimeError(f"real application capture failed: {process.returncode}") + job = OwnedJob() + process = subprocess.Popen([str(binary), str(output), str(vim)], stdin=subprocess.PIPE, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + creationflags=subprocess.CREATE_NO_WINDOW) + job.assign(process.pid) + assigned = True + try: + stdout, stderr = process.communicate(b"GO\n", timeout=60) + except subprocess.TimeoutExpired: + supervision["timed_out"] = True + finally: + try: + if process is not None and process.poll() is None: + try: + if assigned: + job.terminate() + else: + process.kill() # no GO was sent; no PTY clients exist + except OSError: + supervision["cleanup_errors"].append("terminate_failed") + try: + stdout, stderr = process.communicate(timeout=5) + except (OSError, subprocess.TimeoutExpired): + supervision["cleanup_errors"].append("reap_failed") + if job is not None and assigned: + try: + supervision["job_after"] = job.drain() + if supervision["job_after"]["active"]: + job.terminate() + supervision["job_after"] = job.drain() + if supervision["job_after"]["active"]: + supervision["cleanup_errors"].append("job_not_empty") + except OSError: + supervision["job_after"] = None + supervision["cleanup_errors"].append("accounting_failed") + finally: + if job is not None: + try: + job.close() # final kill-on-close fallback if termination failed + except OSError: + supervision["cleanup_errors"].append("job_close_failed") + if process is not None: + try: + if process.poll() is None: + stdout, stderr = process.communicate(timeout=5) + supervision["returncode"] = process.poll() + except (OSError, subprocess.TimeoutExpired): + supervision["cleanup_errors"].append("reap_after_close_failed") + output.mkdir(parents=True, exist_ok=True) + (output / "capture.stdout.txt").write_bytes(stdout) + (output / "capture.stderr.txt").write_bytes(stderr) + write_json(output / "supervision.json", supervision) + if supervision["timed_out"] or supervision["cleanup_errors"] or supervision["job_after"] is None \ + or supervision["job_after"]["active"] or supervision["returncode"] != 0: + raise RuntimeError("real application capture failed") manifest = json.loads((output / "manifest.json").read_text(encoding="utf-8")) if manifest.get("shell_exit") != 0 or manifest.get("pager_exit") != 0 or manifest.get("editor_exit") != 0 \ or manifest.get("editor_file_verified") is not True: raise ValueError("real application lifecycle or editor result was not verified") + validate_pager_capture(output) return manifest +def validate_pager_capture(output): + raw = (output / "pager.bin").read_bytes() + lifecycle = json.loads((output / "pager-lifecycle.json").read_text(encoding="utf-8")) + prompt = re.search(rb"-- More \([0-9]{1,3}%\) -- ", raw) + if (not prompt or lifecycle.get("schema") != "termforge-more-capture-v1" or lifecycle.get("valid") is not True or + lifecycle.get("prompt_end_offset") != prompt.end() or lifecycle.get("q_bytes") != 1 or + lifecycle.get("output_bytes") != len(raw) or lifecycle.get("exit_code") != 0 or + lifecycle.get("output_eof") is not True or lifecycle.get("read_error") != 109 or + lifecycle.get("cleanup_complete") is not True or lifecycle.get("active_children_after") != 0 or + lifecycle.get("forced_reader_cancel") is not False or lifecycle.get("forced_process_cancel") is not False): + raise ValueError("pager prompt, exit or actual output EOF was not verified") + + def run_suite(core_binary, candidate_binary, capture, vim, output): output.mkdir(parents=True, exist_ok=False) capture_output = output / "capture" @@ -65,7 +133,7 @@ def run_suite(core_binary, candidate_binary, capture, vim, output): records.append(report) passed = all(record["status"] in ("match", "reference_difference") for record in records) sources = ["experiments/conpty/real_app_capture.cpp", "experiments/conpty/session.hpp", - "experiments/core/screen.hpp", "experiments/core/trace.cpp", + "experiments/core/screen.hpp", "experiments/core/terminal.hpp", "experiments/core/trace.cpp", "experiments/differential/real_app_matrix.py", "experiments/differential/compare_candidate.py", "experiments/differential/decisions.md", "experiments/candidates/alacritty/run.py"] + vendor_sources() diff --git a/experiments/differential/results/2026-10-26-candidate-common-v6/summary.json b/experiments/differential/results/2026-10-26-candidate-common-v6/summary.json new file mode 100644 index 00000000..75afd2d0 --- /dev/null +++ b/experiments/differential/results/2026-10-26-candidate-common-v6/summary.json @@ -0,0 +1,4739 @@ +{ + "schema": 1, + "candidate": "alacritty_terminal", + "version": "0.26.1-dev+termforge.3", + "platform": "Windows-11-10.0.26200-SP0", + "python": "3.12.10", + "core_trace_sha256": "eeb32a223a192b45f40719d5f0126f61163fb01ac5fcf16de4a1059ae17f5b2d", + "candidate_trace_sha256": "e398d78853a62275aa29d1d7b63121956768e6a42a69d01b7719bfce3593fb44", + "source_sha256": { + "experiments/core/trace.cpp": "d77b41acb0cfc261c47d9972d7b3170bc0049bc8001e2e0e1cf55ec15b19a9ec", + "experiments/core/terminal.hpp": "892997bc2b869a8655d272e9ff049c4102bb1366f1ac709cff8c3d4a41657396", + "experiments/core/screen.hpp": "60dee7105f9cd58611896e5fd83797533d0115a575833d00a16e8ae874d002ed", + "experiments/core/unicode_width.hpp": "d48bf60090a9c6a4263bb46ac587b13814b52c0dbe9d2649cee20e832a1e1926", + "experiments/differential/run.py": "cab52159eff32d42aba9ad88bd17c81b1a0872c11649ee79921e79bb764182a5", + "experiments/differential/cases.py": "c777e837a87895d299449e6611f810b6f351540112f6fdcfbd31aea027c89bb8", + "experiments/differential/compare_candidate.py": "b823369ec73dd6d4de4678898271dc26357587ff3ff1dccb340c228ea93d75b3", + "experiments/differential/candidate-known-differences.json": "ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356", + "experiments/candidates/alacritty/src/main.rs": "7fcf080886118e0505aae774b9f41f9ba7458384377a2dd457fc73de57ae21f9", + "experiments/candidates/alacritty/run.py": "fc3b4ff1485eee9eb4525bbc9d4b5911eb4a5836e5e16c04d83a1aaf2847a665", + "experiments/candidates/alacritty/Cargo.toml": "c5f89fda48ab73f30adfdcc4437fb3eeb1779647886d85364d91395bf8beec86", + "experiments/candidates/alacritty/Cargo.lock": "f14e3d50d997f4e20e2be3cdb25838825747aeb8ca50022b5fc6277887c5d8ce", + "experiments/candidates/alacritty/vendor/alacritty_terminal/CHANGELOG.md": "97231d9beda30b536650c566169c692fa846be3f7b0405add3c9741f95f3af62", + "experiments/candidates/alacritty/vendor/alacritty_terminal/Cargo.toml": "af09057a0404d3034a0e06103af3d2218cafd43e8e99ec68a2ab1c0e5afe04c0", + "experiments/candidates/alacritty/vendor/alacritty_terminal/LICENSE-APACHE": "8884289e3969c089cd556c53fa1d3f51d86b29e52a8c1d660a55ce8292986910", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/event.rs": "e76c212f36bbfc43ea037a825dbc9c5b1174f5836fe8b69dfd158aadf3fc4048", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/event_loop.rs": "6da2897b76d8c0e7f989daa1463fd6fdeb3469a0a63e0c6ba703a9562663163c", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/mod.rs": "be15d08b03a28a271aa927b7149346a1b5d29e92e9186891bf246fdd2a4c90fc", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/resize.rs": "a9a9a285ed28d75b547f86554174c192f74f2a0fb1b331f43a5d2433bcd5d9d7", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/row.rs": "2f963299e52525828d9d0bf1de758d793661c908eb7c126aa8556a1c9945a83d", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/storage.rs": "27a9b5101833d57423ae34ccef3d82c1a1cf9f51cbc42cc836a6fce79c088348", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/tests.rs": "5f38819134540eeb0dff7047f57f5d06250ce59493839c66ec4cd56e43db62f8", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/index.rs": "46908b68508f4e4ea90313f0536a88c357f2d17ad211b14d44b7f92dfdaa4402", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/lib.rs": "b9cae8ed53a4be8f738111f8727dd86218e66c88df38f32f4904e56651a2bee1", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/selection.rs": "0d1373577602646e84a4ab3448c65ee181987327bdabceff3c827e76dd9bd0d8", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/sync.rs": "4410fbae64e8f80321b3569a1495fb323e3319812c441f9eac54048c65ff93fb", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/cell.rs": "a569c233790530e6502ca031d0d3ed488b5aa2356e872da16ca689d64cb7b62c", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/color.rs": "ad2e33b60f397fe30e5e8f2dde2096bad8f5e256d464ba279cbda4a93cbbe224", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/mod.rs": "fa1382d465c2b35361201d87303573ba4660de96fc01e2091b6075b934e6564c", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/search.rs": "84e04c5f0da21009dbbcfc2f8033e322d6240c8d227028b5ce8a784f1c55ccaf", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/thread.rs": "e2f9fdc5d950bbea6c710c7a974f2998127af6b434344c363afb38e849f86144", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/tty/mod.rs": "8cca30ba29cee2df84603f560a8d26624a48979d96d78f1b6729a470ff34d8a1", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/tty/unix.rs": "2e8d0edf1aea816ac95c1e442d60025ebdd20dacaa99d0fd4f7c7cf78ac369e4", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/tty/windows/blocking.rs": "60121a65df120c4a227520d6672231c9e3c515cb75241241e6b37a0c5bc186ca", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/tty/windows/child.rs": "4782f173c639bff6891f474b068fee64b6e2ad411edabdf496bbfe6554ceb32e", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/tty/windows/conpty.rs": "62abd7e72af144ae1f1c359538e18cceaa97741382efa94e001f397510eef416", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/tty/windows/mod.rs": "b08803dae0f192bc6737aff22992a37368ac505c7133554c495ffcacb4116733", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/vi_mode.rs": "f52c0d3248707c68295880acd7ad96610467110d6813f1172d4ae767365add2e" + }, + "runs": 95, + "checkpoints": 1011, + "counts": { + "match": 95 + }, + "passed": true, + "execution_passed": true, + "semantic_acceptance": false, + "differences_adjudicated": true, + "performance_measured": false, + "records": [ + { + "case": "ascii_wrap_scroll", + "plan": "byte", + "input_sha256": "0856b4b97a7193da5e6c3aa34fb9b3a52c5db3b041b2fda2618a987a68557693", + "bytes": 44, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "checkpoints": 45, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + } + }, + { + "case": "ascii_wrap_scroll", + "plan": "whole", + "input_sha256": "0856b4b97a7193da5e6c3aa34fb9b3a52c5db3b041b2fda2618a987a68557693", + "bytes": 44, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 44 + ], + "checkpoints": 2, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "ascii_wrap_scroll", + "plan": "seed:42", + "input_sha256": "0856b4b97a7193da5e6c3aa34fb9b3a52c5db3b041b2fda2618a987a68557693", + "bytes": 44, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 4, + 1, + 9, + 8, + 8, + 5, + 4, + 3, + 2 + ], + "checkpoints": 10, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "ascii_wrap_scroll", + "plan": "seed:20261022", + "input_sha256": "0856b4b97a7193da5e6c3aa34fb9b3a52c5db3b041b2fda2618a987a68557693", + "bytes": 44, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 12, + 10, + 6, + 2, + 8, + 6 + ], + "checkpoints": 7, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "ascii_wrap_scroll", + "plan": "seed:8675309", + "input_sha256": "0856b4b97a7193da5e6c3aa34fb9b3a52c5db3b041b2fda2618a987a68557693", + "bytes": 44, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 13, + 7, + 17, + 7 + ], + "checkpoints": 5, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "cursor_addressing", + "plan": "byte", + "input_sha256": "f4094d6529b95401fb8d572e489480c717eb3899f02f793127c9ee695ddc5b6a", + "bytes": 36, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "checkpoints": 37, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + } + }, + { + "case": "cursor_addressing", + "plan": "whole", + "input_sha256": "f4094d6529b95401fb8d572e489480c717eb3899f02f793127c9ee695ddc5b6a", + "bytes": 36, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 36 + ], + "checkpoints": 2, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "cursor_addressing", + "plan": "seed:42", + "input_sha256": "f4094d6529b95401fb8d572e489480c717eb3899f02f793127c9ee695ddc5b6a", + "bytes": 36, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 4, + 1, + 9, + 8, + 8, + 5, + 1 + ], + "checkpoints": 8, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "cursor_addressing", + "plan": "seed:20261022", + "input_sha256": "f4094d6529b95401fb8d572e489480c717eb3899f02f793127c9ee695ddc5b6a", + "bytes": 36, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 12, + 10, + 6, + 2, + 6 + ], + "checkpoints": 6, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "cursor_addressing", + "plan": "seed:8675309", + "input_sha256": "f4094d6529b95401fb8d572e489480c717eb3899f02f793127c9ee695ddc5b6a", + "bytes": 36, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 13, + 7, + 16 + ], + "checkpoints": 4, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "cursor_defaults_clamping", + "plan": "byte", + "input_sha256": "822ace471d765f0b77a3e3ddfa4712fbe31a6bab147aa38522ea00f2d34ef6aa", + "bytes": 30, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "checkpoints": 31, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + } + }, + { + "case": "cursor_defaults_clamping", + "plan": "whole", + "input_sha256": "822ace471d765f0b77a3e3ddfa4712fbe31a6bab147aa38522ea00f2d34ef6aa", + "bytes": 30, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 30 + ], + "checkpoints": 2, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "cursor_defaults_clamping", + "plan": "seed:42", + "input_sha256": "822ace471d765f0b77a3e3ddfa4712fbe31a6bab147aa38522ea00f2d34ef6aa", + "bytes": 30, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 4, + 1, + 9, + 8, + 8 + ], + "checkpoints": 6, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "cursor_defaults_clamping", + "plan": "seed:20261022", + "input_sha256": "822ace471d765f0b77a3e3ddfa4712fbe31a6bab147aa38522ea00f2d34ef6aa", + "bytes": 30, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 12, + 10, + 6, + 2 + ], + "checkpoints": 5, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "cursor_defaults_clamping", + "plan": "seed:8675309", + "input_sha256": "822ace471d765f0b77a3e3ddfa4712fbe31a6bab147aa38522ea00f2d34ef6aa", + "bytes": 30, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 13, + 7, + 10 + ], + "checkpoints": 4, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "cr_lf_bs_tab", + "plan": "byte", + "input_sha256": "e04b29bb707215b3089c41d6eab786bd03cf921e9ae5ff474777ef41b3158121", + "bytes": 15, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "checkpoints": 16, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + } + }, + { + "case": "cr_lf_bs_tab", + "plan": "whole", + "input_sha256": "e04b29bb707215b3089c41d6eab786bd03cf921e9ae5ff474777ef41b3158121", + "bytes": 15, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 15 + ], + "checkpoints": 2, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "cr_lf_bs_tab", + "plan": "seed:42", + "input_sha256": "e04b29bb707215b3089c41d6eab786bd03cf921e9ae5ff474777ef41b3158121", + "bytes": 15, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 4, + 1, + 9, + 1 + ], + "checkpoints": 5, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "cr_lf_bs_tab", + "plan": "seed:20261022", + "input_sha256": "e04b29bb707215b3089c41d6eab786bd03cf921e9ae5ff474777ef41b3158121", + "bytes": 15, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 12, + 3 + ], + "checkpoints": 3, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "cr_lf_bs_tab", + "plan": "seed:8675309", + "input_sha256": "e04b29bb707215b3089c41d6eab786bd03cf921e9ae5ff474777ef41b3158121", + "bytes": 15, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 13, + 2 + ], + "checkpoints": 3, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "erase_line", + "plan": "byte", + "input_sha256": "b33a611a47271618830794f04e5e04aadaf0411e7cc33d61cd6b86ce41ef6615", + "bytes": 29, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "checkpoints": 30, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + } + }, + { + "case": "erase_line", + "plan": "whole", + "input_sha256": "b33a611a47271618830794f04e5e04aadaf0411e7cc33d61cd6b86ce41ef6615", + "bytes": 29, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 29 + ], + "checkpoints": 2, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "erase_line", + "plan": "seed:42", + "input_sha256": "b33a611a47271618830794f04e5e04aadaf0411e7cc33d61cd6b86ce41ef6615", + "bytes": 29, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 4, + 1, + 9, + 8, + 7 + ], + "checkpoints": 6, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "erase_line", + "plan": "seed:20261022", + "input_sha256": "b33a611a47271618830794f04e5e04aadaf0411e7cc33d61cd6b86ce41ef6615", + "bytes": 29, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 12, + 10, + 6, + 1 + ], + "checkpoints": 5, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "erase_line", + "plan": "seed:8675309", + "input_sha256": "b33a611a47271618830794f04e5e04aadaf0411e7cc33d61cd6b86ce41ef6615", + "bytes": 29, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 13, + 7, + 9 + ], + "checkpoints": 4, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "erase_display", + "plan": "byte", + "input_sha256": "5424cfea349aab2968d6436f0a1079a23d26de6f976f3817bf823235a1f25b32", + "bytes": 32, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "checkpoints": 33, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + } + }, + { + "case": "erase_display", + "plan": "whole", + "input_sha256": "5424cfea349aab2968d6436f0a1079a23d26de6f976f3817bf823235a1f25b32", + "bytes": 32, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 32 + ], + "checkpoints": 2, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "erase_display", + "plan": "seed:42", + "input_sha256": "5424cfea349aab2968d6436f0a1079a23d26de6f976f3817bf823235a1f25b32", + "bytes": 32, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 4, + 1, + 9, + 8, + 8, + 2 + ], + "checkpoints": 7, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "erase_display", + "plan": "seed:20261022", + "input_sha256": "5424cfea349aab2968d6436f0a1079a23d26de6f976f3817bf823235a1f25b32", + "bytes": 32, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 12, + 10, + 6, + 2, + 2 + ], + "checkpoints": 6, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "erase_display", + "plan": "seed:8675309", + "input_sha256": "5424cfea349aab2968d6436f0a1079a23d26de6f976f3817bf823235a1f25b32", + "bytes": 32, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 13, + 7, + 12 + ], + "checkpoints": 4, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "palette_bold_reset", + "plan": "byte", + "input_sha256": "91c9414b344127ba4f69f4468e68d68e98078ddc83aa5b17757bf1c3af4f1020", + "bytes": 38, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "checkpoints": 39, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + } + }, + { + "case": "palette_bold_reset", + "plan": "whole", + "input_sha256": "91c9414b344127ba4f69f4468e68d68e98078ddc83aa5b17757bf1c3af4f1020", + "bytes": 38, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 38 + ], + "checkpoints": 2, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "palette_bold_reset", + "plan": "seed:42", + "input_sha256": "91c9414b344127ba4f69f4468e68d68e98078ddc83aa5b17757bf1c3af4f1020", + "bytes": 38, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 4, + 1, + 9, + 8, + 8, + 5, + 3 + ], + "checkpoints": 8, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "palette_bold_reset", + "plan": "seed:20261022", + "input_sha256": "91c9414b344127ba4f69f4468e68d68e98078ddc83aa5b17757bf1c3af4f1020", + "bytes": 38, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 12, + 10, + 6, + 2, + 8 + ], + "checkpoints": 6, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "palette_bold_reset", + "plan": "seed:8675309", + "input_sha256": "91c9414b344127ba4f69f4468e68d68e98078ddc83aa5b17757bf1c3af4f1020", + "bytes": 38, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 13, + 7, + 17, + 1 + ], + "checkpoints": 5, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "styled_erase", + "plan": "byte", + "input_sha256": "f522b79326ac196bdb9332faac6b70410bce2feafec3ab65da506c50663bb3e7", + "bytes": 17, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "checkpoints": 18, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + } + }, + { + "case": "styled_erase", + "plan": "whole", + "input_sha256": "f522b79326ac196bdb9332faac6b70410bce2feafec3ab65da506c50663bb3e7", + "bytes": 17, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 17 + ], + "checkpoints": 2, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "styled_erase", + "plan": "seed:42", + "input_sha256": "f522b79326ac196bdb9332faac6b70410bce2feafec3ab65da506c50663bb3e7", + "bytes": 17, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 4, + 1, + 9, + 3 + ], + "checkpoints": 5, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "styled_erase", + "plan": "seed:20261022", + "input_sha256": "f522b79326ac196bdb9332faac6b70410bce2feafec3ab65da506c50663bb3e7", + "bytes": 17, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 12, + 5 + ], + "checkpoints": 3, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "styled_erase", + "plan": "seed:8675309", + "input_sha256": "f522b79326ac196bdb9332faac6b70410bce2feafec3ab65da506c50663bb3e7", + "bytes": 17, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 13, + 4 + ], + "checkpoints": 3, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "sgr_pending_wrap", + "plan": "byte", + "input_sha256": "857cda328dbf7034319df00b3eb1d69fd14154b3eb1eb450d8d5edd6bebf45ee", + "bytes": 22, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "checkpoints": 23, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + } + }, + { + "case": "sgr_pending_wrap", + "plan": "whole", + "input_sha256": "857cda328dbf7034319df00b3eb1d69fd14154b3eb1eb450d8d5edd6bebf45ee", + "bytes": 22, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 22 + ], + "checkpoints": 2, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "sgr_pending_wrap", + "plan": "seed:42", + "input_sha256": "857cda328dbf7034319df00b3eb1d69fd14154b3eb1eb450d8d5edd6bebf45ee", + "bytes": 22, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 4, + 1, + 9, + 8 + ], + "checkpoints": 5, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "sgr_pending_wrap", + "plan": "seed:20261022", + "input_sha256": "857cda328dbf7034319df00b3eb1d69fd14154b3eb1eb450d8d5edd6bebf45ee", + "bytes": 22, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 12, + 10 + ], + "checkpoints": 3, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "sgr_pending_wrap", + "plan": "seed:8675309", + "input_sha256": "857cda328dbf7034319df00b3eb1d69fd14154b3eb1eb450d8d5edd6bebf45ee", + "bytes": 22, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 13, + 7, + 2 + ], + "checkpoints": 4, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "unicode_split", + "plan": "byte", + "input_sha256": "809abf257cda8996a060fcdbe93e6bb7f150390b0cb09d28ee230b28db3fc0dd", + "bytes": 21, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "checkpoints": 22, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + } + }, + { + "case": "unicode_split", + "plan": "whole", + "input_sha256": "809abf257cda8996a060fcdbe93e6bb7f150390b0cb09d28ee230b28db3fc0dd", + "bytes": 21, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 21 + ], + "checkpoints": 2, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "unicode_split", + "plan": "seed:42", + "input_sha256": "809abf257cda8996a060fcdbe93e6bb7f150390b0cb09d28ee230b28db3fc0dd", + "bytes": 21, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 4, + 1, + 9, + 7 + ], + "checkpoints": 5, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "unicode_split", + "plan": "seed:20261022", + "input_sha256": "809abf257cda8996a060fcdbe93e6bb7f150390b0cb09d28ee230b28db3fc0dd", + "bytes": 21, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 12, + 9 + ], + "checkpoints": 3, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "unicode_split", + "plan": "seed:8675309", + "input_sha256": "809abf257cda8996a060fcdbe93e6bb7f150390b0cb09d28ee230b28db3fc0dd", + "bytes": 21, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 13, + 7, + 1 + ], + "checkpoints": 4, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "combining_canonical_equivalence", + "plan": "byte", + "input_sha256": "26416d6c4a629c01731f41efcc21c83c1f6e07b6b2c16b442b5382bbef421979", + "bytes": 8, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "checkpoints": 9, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + } + }, + { + "case": "combining_canonical_equivalence", + "plan": "whole", + "input_sha256": "26416d6c4a629c01731f41efcc21c83c1f6e07b6b2c16b442b5382bbef421979", + "bytes": 8, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 8 + ], + "checkpoints": 2, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "combining_canonical_equivalence", + "plan": "seed:42", + "input_sha256": "26416d6c4a629c01731f41efcc21c83c1f6e07b6b2c16b442b5382bbef421979", + "bytes": 8, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 4, + 1, + 3 + ], + "checkpoints": 4, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "combining_canonical_equivalence", + "plan": "seed:20261022", + "input_sha256": "26416d6c4a629c01731f41efcc21c83c1f6e07b6b2c16b442b5382bbef421979", + "bytes": 8, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 8 + ], + "checkpoints": 2, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "combining_canonical_equivalence", + "plan": "seed:8675309", + "input_sha256": "26416d6c4a629c01731f41efcc21c83c1f6e07b6b2c16b442b5382bbef421979", + "bytes": 8, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 8 + ], + "checkpoints": 2, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "styled_wide_history", + "plan": "byte", + "input_sha256": "3295057f376b5df7d8100b310d730d2246ce8c2b60f4ac1af5952233c011a931", + "bytes": 27, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "checkpoints": 28, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + } + }, + { + "case": "styled_wide_history", + "plan": "whole", + "input_sha256": "3295057f376b5df7d8100b310d730d2246ce8c2b60f4ac1af5952233c011a931", + "bytes": 27, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 27 + ], + "checkpoints": 2, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "styled_wide_history", + "plan": "seed:42", + "input_sha256": "3295057f376b5df7d8100b310d730d2246ce8c2b60f4ac1af5952233c011a931", + "bytes": 27, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 4, + 1, + 9, + 8, + 5 + ], + "checkpoints": 6, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "styled_wide_history", + "plan": "seed:20261022", + "input_sha256": "3295057f376b5df7d8100b310d730d2246ce8c2b60f4ac1af5952233c011a931", + "bytes": 27, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 12, + 10, + 5 + ], + "checkpoints": 4, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "styled_wide_history", + "plan": "seed:8675309", + "input_sha256": "3295057f376b5df7d8100b310d730d2246ce8c2b60f4ac1af5952233c011a931", + "bytes": 27, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 13, + 7, + 7 + ], + "checkpoints": 4, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "seeded_common_stream", + "plan": "byte", + "input_sha256": "fb25d5ab863766dd231c792915310ca7eebee36d58a1dd890dead151a7fd074a", + "bytes": 239, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "checkpoints": 240, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + } + }, + { + "case": "seeded_common_stream", + "plan": "whole", + "input_sha256": "fb25d5ab863766dd231c792915310ca7eebee36d58a1dd890dead151a7fd074a", + "bytes": 239, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 239 + ], + "checkpoints": 2, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "seeded_common_stream", + "plan": "seed:42", + "input_sha256": "fb25d5ab863766dd231c792915310ca7eebee36d58a1dd890dead151a7fd074a", + "bytes": 239, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 4, + 1, + 9, + 8, + 8, + 5, + 4, + 3, + 14, + 2, + 1, + 3, + 7, + 8, + 17, + 1, + 7, + 14, + 8, + 15, + 9, + 1, + 6, + 14, + 11, + 9, + 5, + 7, + 11, + 4, + 3, + 13, + 4, + 3 + ], + "checkpoints": 35, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "seeded_common_stream", + "plan": "seed:20261022", + "input_sha256": "fb25d5ab863766dd231c792915310ca7eebee36d58a1dd890dead151a7fd074a", + "bytes": 239, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 12, + 10, + 6, + 2, + 8, + 8, + 8, + 1, + 5, + 2, + 7, + 7, + 10, + 4, + 7, + 5, + 17, + 4, + 7, + 15, + 1, + 4, + 12, + 5, + 12, + 16, + 2, + 2, + 5, + 5, + 15, + 3, + 4, + 8 + ], + "checkpoints": 35, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "seeded_common_stream", + "plan": "seed:8675309", + "input_sha256": "fb25d5ab863766dd231c792915310ca7eebee36d58a1dd890dead151a7fd074a", + "bytes": 239, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 13, + 7, + 17, + 17, + 9, + 8, + 12, + 3, + 1, + 16, + 11, + 16, + 2, + 8, + 12, + 12, + 5, + 7, + 9, + 13, + 8, + 8, + 9, + 5, + 11 + ], + "checkpoints": 26, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "wide_at_right_margin", + "plan": "byte", + "input_sha256": "70a863d4ded137ba697685097ce0d5172392c17d9265544206c3c6f1e444e9a5", + "bytes": 14, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "checkpoints": 15, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + } + }, + { + "case": "wide_at_right_margin", + "plan": "whole", + "input_sha256": "70a863d4ded137ba697685097ce0d5172392c17d9265544206c3c6f1e444e9a5", + "bytes": 14, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 14 + ], + "checkpoints": 2, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "wide_at_right_margin", + "plan": "seed:42", + "input_sha256": "70a863d4ded137ba697685097ce0d5172392c17d9265544206c3c6f1e444e9a5", + "bytes": 14, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 4, + 1, + 9 + ], + "checkpoints": 4, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "wide_at_right_margin", + "plan": "seed:20261022", + "input_sha256": "70a863d4ded137ba697685097ce0d5172392c17d9265544206c3c6f1e444e9a5", + "bytes": 14, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 12, + 2 + ], + "checkpoints": 3, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "wide_at_right_margin", + "plan": "seed:8675309", + "input_sha256": "70a863d4ded137ba697685097ce0d5172392c17d9265544206c3c6f1e444e9a5", + "bytes": 14, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 13, + 1 + ], + "checkpoints": 3, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "wide_lead_overwrite", + "plan": "byte", + "input_sha256": "8ac4252536df91fa15f7fb224d1838b786e8e439f31624403807e63c5d2e46cd", + "bytes": 5, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 1, + 1, + 1, + 1, + 1 + ], + "checkpoints": 6, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + } + }, + { + "case": "wide_lead_overwrite", + "plan": "whole", + "input_sha256": "8ac4252536df91fa15f7fb224d1838b786e8e439f31624403807e63c5d2e46cd", + "bytes": 5, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 5 + ], + "checkpoints": 2, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "wide_lead_overwrite", + "plan": "seed:42", + "input_sha256": "8ac4252536df91fa15f7fb224d1838b786e8e439f31624403807e63c5d2e46cd", + "bytes": 5, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 4, + 1 + ], + "checkpoints": 3, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "wide_lead_overwrite", + "plan": "seed:20261022", + "input_sha256": "8ac4252536df91fa15f7fb224d1838b786e8e439f31624403807e63c5d2e46cd", + "bytes": 5, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 5 + ], + "checkpoints": 2, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "wide_lead_overwrite", + "plan": "seed:8675309", + "input_sha256": "8ac4252536df91fa15f7fb224d1838b786e8e439f31624403807e63c5d2e46cd", + "bytes": 5, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 5 + ], + "checkpoints": 2, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "wide_trailer_overwrite", + "plan": "byte", + "input_sha256": "982eca16d0707242ccd4f27d534eade3ddb0ee87a6995bebf308319143bcecce", + "bytes": 8, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "checkpoints": 9, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + } + }, + { + "case": "wide_trailer_overwrite", + "plan": "whole", + "input_sha256": "982eca16d0707242ccd4f27d534eade3ddb0ee87a6995bebf308319143bcecce", + "bytes": 8, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 8 + ], + "checkpoints": 2, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "wide_trailer_overwrite", + "plan": "seed:42", + "input_sha256": "982eca16d0707242ccd4f27d534eade3ddb0ee87a6995bebf308319143bcecce", + "bytes": 8, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 4, + 1, + 3 + ], + "checkpoints": 4, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "wide_trailer_overwrite", + "plan": "seed:20261022", + "input_sha256": "982eca16d0707242ccd4f27d534eade3ddb0ee87a6995bebf308319143bcecce", + "bytes": 8, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 8 + ], + "checkpoints": 2, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "wide_trailer_overwrite", + "plan": "seed:8675309", + "input_sha256": "982eca16d0707242ccd4f27d534eade3ddb0ee87a6995bebf308319143bcecce", + "bytes": 8, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 8 + ], + "checkpoints": 2, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "combining_at_pending_wrap", + "plan": "byte", + "input_sha256": "bc8d7a6f46ef3ac8a2021fbf740322711b5c3da37439cbbbdab793fa7921006f", + "bytes": 14, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "checkpoints": 15, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + } + }, + { + "case": "combining_at_pending_wrap", + "plan": "whole", + "input_sha256": "bc8d7a6f46ef3ac8a2021fbf740322711b5c3da37439cbbbdab793fa7921006f", + "bytes": 14, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 14 + ], + "checkpoints": 2, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "combining_at_pending_wrap", + "plan": "seed:42", + "input_sha256": "bc8d7a6f46ef3ac8a2021fbf740322711b5c3da37439cbbbdab793fa7921006f", + "bytes": 14, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 4, + 1, + 9 + ], + "checkpoints": 4, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "combining_at_pending_wrap", + "plan": "seed:20261022", + "input_sha256": "bc8d7a6f46ef3ac8a2021fbf740322711b5c3da37439cbbbdab793fa7921006f", + "bytes": 14, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 12, + 2 + ], + "checkpoints": 3, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "combining_at_pending_wrap", + "plan": "seed:8675309", + "input_sha256": "bc8d7a6f46ef3ac8a2021fbf740322711b5c3da37439cbbbdab793fa7921006f", + "bytes": 14, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 13, + 1 + ], + "checkpoints": 3, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "vertical_move_at_pending_wrap", + "plan": "byte", + "input_sha256": "4334087f6e148b3c01e0fbf1f743cba0326568903c8c8e993e91d14f22ac6c50", + "bytes": 15, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "checkpoints": 16, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + } + }, + { + "case": "vertical_move_at_pending_wrap", + "plan": "whole", + "input_sha256": "4334087f6e148b3c01e0fbf1f743cba0326568903c8c8e993e91d14f22ac6c50", + "bytes": 15, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 15 + ], + "checkpoints": 2, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "vertical_move_at_pending_wrap", + "plan": "seed:42", + "input_sha256": "4334087f6e148b3c01e0fbf1f743cba0326568903c8c8e993e91d14f22ac6c50", + "bytes": 15, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 4, + 1, + 9, + 1 + ], + "checkpoints": 5, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "vertical_move_at_pending_wrap", + "plan": "seed:20261022", + "input_sha256": "4334087f6e148b3c01e0fbf1f743cba0326568903c8c8e993e91d14f22ac6c50", + "bytes": 15, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 12, + 3 + ], + "checkpoints": 3, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "vertical_move_at_pending_wrap", + "plan": "seed:8675309", + "input_sha256": "4334087f6e148b3c01e0fbf1f743cba0326568903c8c8e993e91d14f22ac6c50", + "bytes": 15, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 13, + 2 + ], + "checkpoints": 3, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "styled_scroll_blank", + "plan": "byte", + "input_sha256": "0d63e102dcd7787333a73f1cec97a3ac6d1d3e6280601ab950cd9aebe376cb24", + "bytes": 14, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "checkpoints": 15, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + } + }, + { + "case": "styled_scroll_blank", + "plan": "whole", + "input_sha256": "0d63e102dcd7787333a73f1cec97a3ac6d1d3e6280601ab950cd9aebe376cb24", + "bytes": 14, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 14 + ], + "checkpoints": 2, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "styled_scroll_blank", + "plan": "seed:42", + "input_sha256": "0d63e102dcd7787333a73f1cec97a3ac6d1d3e6280601ab950cd9aebe376cb24", + "bytes": 14, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 4, + 1, + 9 + ], + "checkpoints": 4, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "styled_scroll_blank", + "plan": "seed:20261022", + "input_sha256": "0d63e102dcd7787333a73f1cec97a3ac6d1d3e6280601ab950cd9aebe376cb24", + "bytes": 14, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 12, + 2 + ], + "checkpoints": 3, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "styled_scroll_blank", + "plan": "seed:8675309", + "input_sha256": "0d63e102dcd7787333a73f1cec97a3ac6d1d3e6280601ab950cd9aebe376cb24", + "bytes": 14, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 13, + 1 + ], + "checkpoints": 3, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + } + ] +} diff --git a/experiments/differential/results/2026-10-26-capabilities-v5/summary.json b/experiments/differential/results/2026-10-26-capabilities-v5/summary.json new file mode 100644 index 00000000..d3f1b27a --- /dev/null +++ b/experiments/differential/results/2026-10-26-capabilities-v5/summary.json @@ -0,0 +1,1642 @@ +{ + "schema": 1, + "candidate": "alacritty_terminal", + "version": "0.26.1-dev+termforge.3", + "platform": "Windows-11-10.0.26200-SP0", + "core_trace_sha256": "eeb32a223a192b45f40719d5f0126f61163fb01ac5fcf16de4a1059ae17f5b2d", + "candidate_trace_sha256": "e398d78853a62275aa29d1d7b63121956768e6a42a69d01b7719bfce3593fb44", + "source_sha256": { + "experiments/core/trace.cpp": "d77b41acb0cfc261c47d9972d7b3170bc0049bc8001e2e0e1cf55ec15b19a9ec", + "experiments/differential/compare_candidate.py": "b823369ec73dd6d4de4678898271dc26357587ff3ff1dccb340c228ea93d75b3", + "experiments/differential/capability_matrix.py": "6b2bfe90101a56234389977d049dbfb880ecd3175c6d2d3ee35a7f8d8ac3e147", + "experiments/differential/candidate-known-differences.json": "ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356", + "experiments/candidates/alacritty/src/main.rs": "7fcf080886118e0505aae774b9f41f9ba7458384377a2dd457fc73de57ae21f9", + "experiments/candidates/alacritty/run.py": "fc3b4ff1485eee9eb4525bbc9d4b5911eb4a5836e5e16c04d83a1aaf2847a665", + "experiments/candidates/alacritty/vendor/alacritty_terminal/CHANGELOG.md": "97231d9beda30b536650c566169c692fa846be3f7b0405add3c9741f95f3af62", + "experiments/candidates/alacritty/vendor/alacritty_terminal/Cargo.toml": "af09057a0404d3034a0e06103af3d2218cafd43e8e99ec68a2ab1c0e5afe04c0", + "experiments/candidates/alacritty/vendor/alacritty_terminal/LICENSE-APACHE": "8884289e3969c089cd556c53fa1d3f51d86b29e52a8c1d660a55ce8292986910", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/event.rs": "e76c212f36bbfc43ea037a825dbc9c5b1174f5836fe8b69dfd158aadf3fc4048", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/event_loop.rs": "6da2897b76d8c0e7f989daa1463fd6fdeb3469a0a63e0c6ba703a9562663163c", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/mod.rs": "be15d08b03a28a271aa927b7149346a1b5d29e92e9186891bf246fdd2a4c90fc", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/resize.rs": "a9a9a285ed28d75b547f86554174c192f74f2a0fb1b331f43a5d2433bcd5d9d7", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/row.rs": "2f963299e52525828d9d0bf1de758d793661c908eb7c126aa8556a1c9945a83d", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/storage.rs": "27a9b5101833d57423ae34ccef3d82c1a1cf9f51cbc42cc836a6fce79c088348", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/tests.rs": "5f38819134540eeb0dff7047f57f5d06250ce59493839c66ec4cd56e43db62f8", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/index.rs": "46908b68508f4e4ea90313f0536a88c357f2d17ad211b14d44b7f92dfdaa4402", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/lib.rs": "b9cae8ed53a4be8f738111f8727dd86218e66c88df38f32f4904e56651a2bee1", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/selection.rs": "0d1373577602646e84a4ab3448c65ee181987327bdabceff3c827e76dd9bd0d8", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/sync.rs": "4410fbae64e8f80321b3569a1495fb323e3319812c441f9eac54048c65ff93fb", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/cell.rs": "a569c233790530e6502ca031d0d3ed488b5aa2356e872da16ca689d64cb7b62c", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/color.rs": "ad2e33b60f397fe30e5e8f2dde2096bad8f5e256d464ba279cbda4a93cbbe224", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/mod.rs": "fa1382d465c2b35361201d87303573ba4660de96fc01e2091b6075b934e6564c", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/search.rs": "84e04c5f0da21009dbbcfc2f8033e322d6240c8d227028b5ce8a784f1c55ccaf", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/thread.rs": "e2f9fdc5d950bbea6c710c7a974f2998127af6b434344c363afb38e849f86144", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/tty/mod.rs": "8cca30ba29cee2df84603f560a8d26624a48979d96d78f1b6729a470ff34d8a1", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/tty/unix.rs": "2e8d0edf1aea816ac95c1e442d60025ebdd20dacaa99d0fd4f7c7cf78ac369e4", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/tty/windows/blocking.rs": "60121a65df120c4a227520d6672231c9e3c515cb75241241e6b37a0c5bc186ca", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/tty/windows/child.rs": "4782f173c639bff6891f474b068fee64b6e2ad411edabdf496bbfe6554ceb32e", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/tty/windows/conpty.rs": "62abd7e72af144ae1f1c359538e18cceaa97741382efa94e001f397510eef416", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/tty/windows/mod.rs": "b08803dae0f192bc6737aff22992a37368ac505c7133554c495ffcacb4116733", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/vi_mode.rs": "f52c0d3248707c68295880acd7ad96610467110d6813f1172d4ae767365add2e" + }, + "runs": 25, + "checkpoints": 77, + "counts": { + "core_unsupported": 15, + "match": 5, + "resize_difference": 5 + }, + "passed": true, + "execution_passed": true, + "semantic_acceptance": false, + "performance_measured": false, + "unsupported_is_not_a_pass": true, + "resize_covered": true, + "real_application_covered": false, + "records": [ + { + "case": "alternate_screen", + "plan": "byte", + "input_sha256": "6ae16cedd522763cbbefccf3424d3195350d2d149d06a1e815e52ab73f96fe76", + "bytes": 24, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "checkpoints": 0, + "status": "core_unsupported", + "core_returncode": 2, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "capability": "alternate_screen", + "expected_core_support": false, + "core_arguments": [], + "candidate_arguments": [], + "replies": [], + "candidate_final_rows": [ + "A", + "BZ", + "" + ], + "candidate_final_dimensions": [ + 3, + 12 + ], + "core_final_dimensions": [] + }, + { + "case": "alternate_screen", + "plan": "whole", + "input_sha256": "6ae16cedd522763cbbefccf3424d3195350d2d149d06a1e815e52ab73f96fe76", + "bytes": 24, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 24 + ], + "checkpoints": 0, + "status": "core_unsupported", + "core_returncode": 2, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "capability": "alternate_screen", + "expected_core_support": false, + "core_arguments": [], + "candidate_arguments": [], + "replies": [], + "candidate_final_rows": [ + "A", + "BZ", + "" + ], + "candidate_final_dimensions": [ + 3, + 12 + ], + "core_final_dimensions": [] + }, + { + "case": "alternate_screen", + "plan": "seed:42", + "input_sha256": "6ae16cedd522763cbbefccf3424d3195350d2d149d06a1e815e52ab73f96fe76", + "bytes": 24, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 4, + 1, + 9, + 8, + 2 + ], + "checkpoints": 0, + "status": "core_unsupported", + "core_returncode": 2, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "capability": "alternate_screen", + "expected_core_support": false, + "core_arguments": [], + "candidate_arguments": [], + "replies": [], + "candidate_final_rows": [ + "A", + "BZ", + "" + ], + "candidate_final_dimensions": [ + 3, + 12 + ], + "core_final_dimensions": [] + }, + { + "case": "alternate_screen", + "plan": "seed:20261022", + "input_sha256": "6ae16cedd522763cbbefccf3424d3195350d2d149d06a1e815e52ab73f96fe76", + "bytes": 24, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 12, + 10, + 2 + ], + "checkpoints": 0, + "status": "core_unsupported", + "core_returncode": 2, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "capability": "alternate_screen", + "expected_core_support": false, + "core_arguments": [], + "candidate_arguments": [], + "replies": [], + "candidate_final_rows": [ + "A", + "BZ", + "" + ], + "candidate_final_dimensions": [ + 3, + 12 + ], + "core_final_dimensions": [] + }, + { + "case": "alternate_screen", + "plan": "seed:8675309", + "input_sha256": "6ae16cedd522763cbbefccf3424d3195350d2d149d06a1e815e52ab73f96fe76", + "bytes": 24, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 13, + 7, + 4 + ], + "checkpoints": 0, + "status": "core_unsupported", + "core_returncode": 2, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "capability": "alternate_screen", + "expected_core_support": false, + "core_arguments": [], + "candidate_arguments": [], + "replies": [], + "candidate_final_rows": [ + "A", + "BZ", + "" + ], + "candidate_final_dimensions": [ + 3, + 12 + ], + "core_final_dimensions": [] + }, + { + "case": "scroll_region", + "plan": "byte", + "input_sha256": "090b82f96737c6c2e43b7efdfda5119bf4b1b957c30159faaf4308666e5e6b60", + "bytes": 33, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "checkpoints": 0, + "status": "core_unsupported", + "core_returncode": 2, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "capability": "scroll_region", + "expected_core_support": false, + "core_arguments": [], + "candidate_arguments": [], + "replies": [], + "candidate_final_rows": [ + "B", + "D", + "X" + ], + "candidate_final_dimensions": [ + 3, + 12 + ], + "core_final_dimensions": [] + }, + { + "case": "scroll_region", + "plan": "whole", + "input_sha256": "090b82f96737c6c2e43b7efdfda5119bf4b1b957c30159faaf4308666e5e6b60", + "bytes": 33, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 33 + ], + "checkpoints": 0, + "status": "core_unsupported", + "core_returncode": 2, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "capability": "scroll_region", + "expected_core_support": false, + "core_arguments": [], + "candidate_arguments": [], + "replies": [], + "candidate_final_rows": [ + "B", + "D", + "X" + ], + "candidate_final_dimensions": [ + 3, + 12 + ], + "core_final_dimensions": [] + }, + { + "case": "scroll_region", + "plan": "seed:42", + "input_sha256": "090b82f96737c6c2e43b7efdfda5119bf4b1b957c30159faaf4308666e5e6b60", + "bytes": 33, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 4, + 1, + 9, + 8, + 8, + 3 + ], + "checkpoints": 0, + "status": "core_unsupported", + "core_returncode": 2, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "capability": "scroll_region", + "expected_core_support": false, + "core_arguments": [], + "candidate_arguments": [], + "replies": [], + "candidate_final_rows": [ + "B", + "D", + "X" + ], + "candidate_final_dimensions": [ + 3, + 12 + ], + "core_final_dimensions": [] + }, + { + "case": "scroll_region", + "plan": "seed:20261022", + "input_sha256": "090b82f96737c6c2e43b7efdfda5119bf4b1b957c30159faaf4308666e5e6b60", + "bytes": 33, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 12, + 10, + 6, + 2, + 3 + ], + "checkpoints": 0, + "status": "core_unsupported", + "core_returncode": 2, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "capability": "scroll_region", + "expected_core_support": false, + "core_arguments": [], + "candidate_arguments": [], + "replies": [], + "candidate_final_rows": [ + "B", + "D", + "X" + ], + "candidate_final_dimensions": [ + 3, + 12 + ], + "core_final_dimensions": [] + }, + { + "case": "scroll_region", + "plan": "seed:8675309", + "input_sha256": "090b82f96737c6c2e43b7efdfda5119bf4b1b957c30159faaf4308666e5e6b60", + "bytes": 33, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 13, + 7, + 13 + ], + "checkpoints": 0, + "status": "core_unsupported", + "core_returncode": 2, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "capability": "scroll_region", + "expected_core_support": false, + "core_arguments": [], + "candidate_arguments": [], + "replies": [], + "candidate_final_rows": [ + "B", + "D", + "X" + ], + "candidate_final_dimensions": [ + 3, + 12 + ], + "core_final_dimensions": [] + }, + { + "case": "cursor_query", + "plan": "byte", + "input_sha256": "cb7ba0ac55e9696a24eab6a33bbff33ce3b16c5b2e9630762390ed419eb7f4d5", + "bytes": 5, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 1, + 1, + 1, + 1, + 1 + ], + "checkpoints": 0, + "status": "core_unsupported", + "core_returncode": 2, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "capability": "cursor_query", + "expected_core_support": false, + "core_arguments": [], + "candidate_arguments": [], + "replies": [ + "\u001b[1;2R" + ], + "candidate_final_rows": [ + "A", + "", + "" + ], + "candidate_final_dimensions": [ + 3, + 12 + ], + "core_final_dimensions": [] + }, + { + "case": "cursor_query", + "plan": "whole", + "input_sha256": "cb7ba0ac55e9696a24eab6a33bbff33ce3b16c5b2e9630762390ed419eb7f4d5", + "bytes": 5, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 5 + ], + "checkpoints": 0, + "status": "core_unsupported", + "core_returncode": 2, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "capability": "cursor_query", + "expected_core_support": false, + "core_arguments": [], + "candidate_arguments": [], + "replies": [ + "\u001b[1;2R" + ], + "candidate_final_rows": [ + "A", + "", + "" + ], + "candidate_final_dimensions": [ + 3, + 12 + ], + "core_final_dimensions": [] + }, + { + "case": "cursor_query", + "plan": "seed:42", + "input_sha256": "cb7ba0ac55e9696a24eab6a33bbff33ce3b16c5b2e9630762390ed419eb7f4d5", + "bytes": 5, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 4, + 1 + ], + "checkpoints": 0, + "status": "core_unsupported", + "core_returncode": 2, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "capability": "cursor_query", + "expected_core_support": false, + "core_arguments": [], + "candidate_arguments": [], + "replies": [ + "\u001b[1;2R" + ], + "candidate_final_rows": [ + "A", + "", + "" + ], + "candidate_final_dimensions": [ + 3, + 12 + ], + "core_final_dimensions": [] + }, + { + "case": "cursor_query", + "plan": "seed:20261022", + "input_sha256": "cb7ba0ac55e9696a24eab6a33bbff33ce3b16c5b2e9630762390ed419eb7f4d5", + "bytes": 5, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 5 + ], + "checkpoints": 0, + "status": "core_unsupported", + "core_returncode": 2, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "capability": "cursor_query", + "expected_core_support": false, + "core_arguments": [], + "candidate_arguments": [], + "replies": [ + "\u001b[1;2R" + ], + "candidate_final_rows": [ + "A", + "", + "" + ], + "candidate_final_dimensions": [ + 3, + 12 + ], + "core_final_dimensions": [] + }, + { + "case": "cursor_query", + "plan": "seed:8675309", + "input_sha256": "cb7ba0ac55e9696a24eab6a33bbff33ce3b16c5b2e9630762390ed419eb7f4d5", + "bytes": 5, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 5 + ], + "checkpoints": 0, + "status": "core_unsupported", + "core_returncode": 2, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "capability": "cursor_query", + "expected_core_support": false, + "core_arguments": [], + "candidate_arguments": [], + "replies": [ + "\u001b[1;2R" + ], + "candidate_final_rows": [ + "A", + "", + "" + ], + "candidate_final_dimensions": [ + 3, + 12 + ], + "core_final_dimensions": [] + }, + { + "case": "recovery_stream", + "plan": "byte", + "input_sha256": "c223697f5d82b5ded52404fa40bb26086e1f302d5e6eda0b1dacf639edf04511", + "bytes": 23, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "checkpoints": 24, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "capability": "recovery_stream", + "expected_core_support": true, + "core_arguments": [ + "--recovery" + ], + "candidate_arguments": [], + "replies": [], + "candidate_final_rows": [ + "ABC�D", + "", + "" + ], + "candidate_final_dimensions": [ + 3, + 12 + ], + "core_final_dimensions": [ + 3, + 12 + ] + }, + { + "case": "recovery_stream", + "plan": "whole", + "input_sha256": "c223697f5d82b5ded52404fa40bb26086e1f302d5e6eda0b1dacf639edf04511", + "bytes": 23, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 23 + ], + "checkpoints": 2, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "capability": "recovery_stream", + "expected_core_support": true, + "core_arguments": [ + "--recovery" + ], + "candidate_arguments": [], + "replies": [], + "candidate_final_rows": [ + "ABC�D", + "", + "" + ], + "candidate_final_dimensions": [ + 3, + 12 + ], + "core_final_dimensions": [ + 3, + 12 + ], + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "recovery_stream", + "plan": "seed:42", + "input_sha256": "c223697f5d82b5ded52404fa40bb26086e1f302d5e6eda0b1dacf639edf04511", + "bytes": 23, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 4, + 1, + 9, + 8, + 1 + ], + "checkpoints": 6, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "capability": "recovery_stream", + "expected_core_support": true, + "core_arguments": [ + "--recovery" + ], + "candidate_arguments": [], + "replies": [], + "candidate_final_rows": [ + "ABC�D", + "", + "" + ], + "candidate_final_dimensions": [ + 3, + 12 + ], + "core_final_dimensions": [ + 3, + 12 + ], + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "recovery_stream", + "plan": "seed:20261022", + "input_sha256": "c223697f5d82b5ded52404fa40bb26086e1f302d5e6eda0b1dacf639edf04511", + "bytes": 23, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 12, + 10, + 1 + ], + "checkpoints": 4, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "capability": "recovery_stream", + "expected_core_support": true, + "core_arguments": [ + "--recovery" + ], + "candidate_arguments": [], + "replies": [], + "candidate_final_rows": [ + "ABC�D", + "", + "" + ], + "candidate_final_dimensions": [ + 3, + 12 + ], + "core_final_dimensions": [ + 3, + 12 + ], + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "recovery_stream", + "plan": "seed:8675309", + "input_sha256": "c223697f5d82b5ded52404fa40bb26086e1f302d5e6eda0b1dacf639edf04511", + "bytes": 23, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 13, + 7, + 3 + ], + "checkpoints": 4, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "capability": "recovery_stream", + "expected_core_support": true, + "core_arguments": [ + "--recovery" + ], + "candidate_arguments": [], + "replies": [], + "candidate_final_rows": [ + "ABC�D", + "", + "" + ], + "candidate_final_dimensions": [ + 3, + 12 + ], + "core_final_dimensions": [ + 3, + 12 + ], + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null + }, + { + "case": "resize_stream", + "plan": "byte", + "input_sha256": "66c3b671a270160eac126dfbe30211488efb70a66794a1936690c68a84374450", + "bytes": 18, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "checkpoints": 20, + "status": "resize_difference", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": { + "checkpoint": 19, + "offset": 18, + "path": ".screen.column", + "expected": 5, + "actual": 4 + }, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "capability": "resize_stream", + "expected_core_support": true, + "core_arguments": [ + "--resize=2x6" + ], + "candidate_arguments": [ + "--resize=2x6" + ], + "replies": [], + "candidate_final_rows": [ + "123456", + "7890" + ], + "candidate_final_dimensions": [ + 2, + 6 + ], + "core_final_dimensions": [ + 2, + 6 + ] + }, + { + "case": "resize_stream", + "plan": "whole", + "input_sha256": "66c3b671a270160eac126dfbe30211488efb70a66794a1936690c68a84374450", + "bytes": 18, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 18 + ], + "checkpoints": 3, + "status": "resize_difference", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": { + "checkpoint": 2, + "offset": 18, + "path": ".screen.column", + "expected": 5, + "actual": 4 + }, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "capability": "resize_stream", + "expected_core_support": true, + "core_arguments": [ + "--resize=2x6" + ], + "candidate_arguments": [ + "--resize=2x6" + ], + "replies": [], + "candidate_final_rows": [ + "123456", + "7890" + ], + "candidate_final_dimensions": [ + 2, + 6 + ], + "core_final_dimensions": [ + 2, + 6 + ] + }, + { + "case": "resize_stream", + "plan": "seed:42", + "input_sha256": "66c3b671a270160eac126dfbe30211488efb70a66794a1936690c68a84374450", + "bytes": 18, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 4, + 1, + 9, + 4 + ], + "checkpoints": 6, + "status": "resize_difference", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": { + "checkpoint": 5, + "offset": 18, + "path": ".screen.column", + "expected": 5, + "actual": 4 + }, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "capability": "resize_stream", + "expected_core_support": true, + "core_arguments": [ + "--resize=2x6" + ], + "candidate_arguments": [ + "--resize=2x6" + ], + "replies": [], + "candidate_final_rows": [ + "123456", + "7890" + ], + "candidate_final_dimensions": [ + 2, + 6 + ], + "core_final_dimensions": [ + 2, + 6 + ] + }, + { + "case": "resize_stream", + "plan": "seed:20261022", + "input_sha256": "66c3b671a270160eac126dfbe30211488efb70a66794a1936690c68a84374450", + "bytes": 18, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 12, + 6 + ], + "checkpoints": 4, + "status": "resize_difference", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": { + "checkpoint": 3, + "offset": 18, + "path": ".screen.column", + "expected": 5, + "actual": 4 + }, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "capability": "resize_stream", + "expected_core_support": true, + "core_arguments": [ + "--resize=2x6" + ], + "candidate_arguments": [ + "--resize=2x6" + ], + "replies": [], + "candidate_final_rows": [ + "123456", + "7890" + ], + "candidate_final_dimensions": [ + 2, + 6 + ], + "core_final_dimensions": [ + 2, + 6 + ] + }, + { + "case": "resize_stream", + "plan": "seed:8675309", + "input_sha256": "66c3b671a270160eac126dfbe30211488efb70a66794a1936690c68a84374450", + "bytes": 18, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 13, + 5 + ], + "checkpoints": 4, + "status": "resize_difference", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": { + "checkpoint": 3, + "offset": 18, + "path": ".screen.column", + "expected": 5, + "actual": 4 + }, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "capability": "resize_stream", + "expected_core_support": true, + "core_arguments": [ + "--resize=2x6" + ], + "candidate_arguments": [ + "--resize=2x6" + ], + "replies": [], + "candidate_final_rows": [ + "123456", + "7890" + ], + "candidate_final_dimensions": [ + 2, + 6 + ], + "core_final_dimensions": [ + 2, + 6 + ] + } + ] +} diff --git a/experiments/differential/results/2026-10-26-fuzz-v4/summary.json b/experiments/differential/results/2026-10-26-fuzz-v4/summary.json new file mode 100644 index 00000000..9897908c --- /dev/null +++ b/experiments/differential/results/2026-10-26-fuzz-v4/summary.json @@ -0,0 +1,271 @@ +{ + "schema": 1, + "platform": "Windows-11-10.0.26200-SP0", + "candidate": "alacritty_terminal", + "version": "0.26.1-dev+termforge.3", + "core_trace_sha256": "eeb32a223a192b45f40719d5f0126f61163fb01ac5fcf16de4a1059ae17f5b2d", + "candidate_trace_sha256": "e398d78853a62275aa29d1d7b63121956768e6a42a69d01b7719bfce3593fb44", + "source_sha256": { + "experiments/differential/fuzz_matrix.py": "53ad5d32df5d2833d232a158c5176473a712b2521b2220f128e548b6bc3d5e15", + "experiments/differential/compare_candidate.py": "b823369ec73dd6d4de4678898271dc26357587ff3ff1dccb340c228ea93d75b3", + "experiments/differential/fuzz-known-failures.json": "0daa2db840c6266098f790c53f5a88d070f573a604599b94742083e9522483a7", + "experiments/core/trace.cpp": "d77b41acb0cfc261c47d9972d7b3170bc0049bc8001e2e0e1cf55ec15b19a9ec", + "experiments/candidates/alacritty/run.py": "fc3b4ff1485eee9eb4525bbc9d4b5911eb4a5836e5e16c04d83a1aaf2847a665", + "experiments/candidates/alacritty/vendor/alacritty_terminal/CHANGELOG.md": "97231d9beda30b536650c566169c692fa846be3f7b0405add3c9741f95f3af62", + "experiments/candidates/alacritty/vendor/alacritty_terminal/Cargo.toml": "af09057a0404d3034a0e06103af3d2218cafd43e8e99ec68a2ab1c0e5afe04c0", + "experiments/candidates/alacritty/vendor/alacritty_terminal/LICENSE-APACHE": "8884289e3969c089cd556c53fa1d3f51d86b29e52a8c1d660a55ce8292986910", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/event.rs": "e76c212f36bbfc43ea037a825dbc9c5b1174f5836fe8b69dfd158aadf3fc4048", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/event_loop.rs": "6da2897b76d8c0e7f989daa1463fd6fdeb3469a0a63e0c6ba703a9562663163c", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/mod.rs": "be15d08b03a28a271aa927b7149346a1b5d29e92e9186891bf246fdd2a4c90fc", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/resize.rs": "a9a9a285ed28d75b547f86554174c192f74f2a0fb1b331f43a5d2433bcd5d9d7", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/row.rs": "2f963299e52525828d9d0bf1de758d793661c908eb7c126aa8556a1c9945a83d", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/storage.rs": "27a9b5101833d57423ae34ccef3d82c1a1cf9f51cbc42cc836a6fce79c088348", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/tests.rs": "5f38819134540eeb0dff7047f57f5d06250ce59493839c66ec4cd56e43db62f8", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/index.rs": "46908b68508f4e4ea90313f0536a88c357f2d17ad211b14d44b7f92dfdaa4402", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/lib.rs": "b9cae8ed53a4be8f738111f8727dd86218e66c88df38f32f4904e56651a2bee1", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/selection.rs": "0d1373577602646e84a4ab3448c65ee181987327bdabceff3c827e76dd9bd0d8", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/sync.rs": "4410fbae64e8f80321b3569a1495fb323e3319812c441f9eac54048c65ff93fb", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/cell.rs": "a569c233790530e6502ca031d0d3ed488b5aa2356e872da16ca689d64cb7b62c", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/color.rs": "ad2e33b60f397fe30e5e8f2dde2096bad8f5e256d464ba279cbda4a93cbbe224", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/mod.rs": "fa1382d465c2b35361201d87303573ba4660de96fc01e2091b6075b934e6564c", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/search.rs": "84e04c5f0da21009dbbcfc2f8033e322d6240c8d227028b5ce8a784f1c55ccaf", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/thread.rs": "e2f9fdc5d950bbea6c710c7a974f2998127af6b434344c363afb38e849f86144", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/tty/mod.rs": "8cca30ba29cee2df84603f560a8d26624a48979d96d78f1b6729a470ff34d8a1", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/tty/unix.rs": "2e8d0edf1aea816ac95c1e442d60025ebdd20dacaa99d0fd4f7c7cf78ac369e4", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/tty/windows/blocking.rs": "60121a65df120c4a227520d6672231c9e3c515cb75241241e6b37a0c5bc186ca", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/tty/windows/child.rs": "4782f173c639bff6891f474b068fee64b6e2ad411edabdf496bbfe6554ceb32e", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/tty/windows/conpty.rs": "62abd7e72af144ae1f1c359538e18cceaa97741382efa94e001f397510eef416", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/tty/windows/mod.rs": "b08803dae0f192bc6737aff22992a37368ac505c7133554c495ffcacb4116733", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/vi_mode.rs": "f52c0d3248707c68295880acd7ad96610467110d6813f1172d4ae767365add2e" + }, + "seeds": 4, + "runs": 8, + "counts": { + "known_failure": 2, + "match": 6 + }, + "passed": true, + "execution_passed": true, + "semantic_acceptance": false, + "performance_measured": false, + "records": [ + { + "case": "seed_0", + "plan": "whole", + "input_sha256": "3aa86b8143d72771f7fc9d93bd498538afd8dfb8c6fa82ba5c8d329401ed6ee0", + "bytes": 22, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 22 + ], + "checkpoints": 2, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + } + }, + { + "case": "seed_0", + "plan": "byte", + "status": "match", + "input_sha256": "3aa86b8143d72771f7fc9d93bd498538afd8dfb8c6fa82ba5c8d329401ed6ee0", + "checkpoints": 23, + "first_difference": null, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null, + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null + }, + { + "case": "seed_1", + "plan": "whole", + "input_sha256": "1e09a9d44f8a20b14d88dd232ee07ee286e1b3301070afc72c6f0a66b338b335", + "bytes": 7, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 7 + ], + "checkpoints": 2, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + } + }, + { + "case": "seed_1", + "plan": "byte", + "status": "match", + "input_sha256": "1e09a9d44f8a20b14d88dd232ee07ee286e1b3301070afc72c6f0a66b338b335", + "checkpoints": 8, + "first_difference": null, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null, + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null + }, + { + "case": "seed_2", + "plan": "whole", + "input_sha256": "9d1e0e2d9459d06523ad13e28a4093c2316baafe7aec5b25f30eba2e113599c4", + "bytes": 1, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 1 + ], + "checkpoints": 2, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + } + }, + { + "case": "seed_2", + "plan": "byte", + "status": "match", + "input_sha256": "9d1e0e2d9459d06523ad13e28a4093c2316baafe7aec5b25f30eba2e113599c4", + "checkpoints": 2, + "first_difference": null, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null, + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null + }, + { + "case": "seed_3", + "plan": "whole", + "input_sha256": "a0d3958efdd10df4e6df69b27f728e86993367f42e946ac9436bead5bc416b50", + "bytes": 12, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 12 + ], + "checkpoints": 2, + "status": "known_failure", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": { + "checkpoint": 1, + "offset": 12, + "path": ".screen.cells[0][0].text", + "expected": "�", + "actual": "中" + }, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + } + }, + { + "case": "seed_3", + "plan": "byte", + "status": "known_failure", + "input_sha256": "a0d3958efdd10df4e6df69b27f728e86993367f42e946ac9436bead5bc416b50", + "checkpoints": 13, + "first_difference": null, + "core_fragmentation_difference": null, + "candidate_fragmentation_difference": null, + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null + } + ] +} diff --git a/experiments/differential/results/2026-10-26-more-ech/validation.json b/experiments/differential/results/2026-10-26-more-ech/validation.json new file mode 100644 index 00000000..6de254d0 --- /dev/null +++ b/experiments/differential/results/2026-10-26-more-ech/validation.json @@ -0,0 +1,436 @@ +{ + "schema": "termforge-more-ech-validation-v1", + "status": "bounded-more-ech-passed-candidate-pending-difference-retained", + "generated_utc": "2026-10-26T03:51:30.662399+00:00", + "implementation_commit": "245545ff6fc805c81279e7d3a656080acaea7124", + "base_commit": "2bd7e45a38237e46efc16e852351c35ead6fa823", + "source_sha256": { + "experiments/conpty/real_app_capture.cpp": "95b16e22e0c8fc9cf763093900f1982a01ad6808a679f00e52c3761195099fe6", + "experiments/conpty/session.hpp": "f143a858df22302a2bc4a794263ff7769f4d0cfd9537307d916feb8ad638c6b6", + "experiments/core/screen.hpp": "e8ef71e86198d6f36cb885580a88f36d06ff7419d6e2a10e8c4452ef9caa5f77", + "experiments/core/terminal.hpp": "e5ca6b065b38b98ba4bb32f579e4f0e8e126a7872de147ff37c2c68aac414358", + "experiments/core/trace.cpp": "91e6f4594ae47a9e0c60a435c71687666ec114373899d033fa59e1e46b9945d2", + "experiments/core/test_recovery.py": "2aa90e46c70de55b062311bb7d4b7053a6247c5902cfa1f75903a8d2a52bd269", + "experiments/differential/real_app_matrix.py": "4bb8ddadbcde608e84592b46c409ec26177972fd9a4f097c42ee2aeb5f4746ba", + "experiments/differential/test_real_app_matrix.py": "da042175d895ecae3ffb7eb679cb9cb4413efaabba39949b7bb03cc1ab46170b", + "experiments/differential/compare_candidate.py": "b823369ec73dd6d4de4678898271dc26357587ff3ff1dccb340c228ea93d75b3" + }, + "source_binding": "recorded_inputs_not_independent_build_attestation", + "binaries": { + "pre_fix_trace_sha256": "9803b12e6dd74566a95fb5e1c55ef934c8b125d7d3bc4b57426f4f2d7ae00b73", + "post_fix_trace_sha256": "60631c77d41e538fb815d7991fd6476dc032892f0efa958853b6f81eb53501a0", + "candidate_sha256": "e398d78853a62275aa29d1d7b63121956768e6a42a69d01b7719bfce3593fb44", + "capture_sha256": "47b3f28f8506eeddf473e1614633a2ad1dae7ef2c7ae1cf1f3b25dd54d85d482" + }, + "pre_fix_source_sha256": { + "trace.cpp": "91e6f4594ae47a9e0c60a435c71687666ec114373899d033fa59e1e46b9945d2", + "screen.hpp": "60dee7105f9cd58611896e5fd83797533d0115a575833d00a16e8ae874d002ed", + "terminal.hpp": "892997bc2b869a8655d272e9ff049c4102bb1366f1ac709cff8c3d4a41657396", + "unicode_width.hpp": "d48bf60090a9c6a4263bb46ac587b13814b52c0dbe9d2649cee20e832a1e1926" + }, + "reference": { + "candidate_version": "0.26.1-dev+termforge.3", + "candidate_identity_reference": "experiments/differential/results/2026-10-26-real-applications-v6/summary.json", + "pyte_version": "0.8.2", + "pyte_source_sha256": { + "screens.py": "43bcd10d5555f6a8e53a1f9c0e557a47ec801b3914533d5b51af5828a5242ecb", + "streams.py": "a8c667e0f288b20064c3d8600aaf792f562cdfa84b4a39bc852ce05acc04c14d" + } + }, + "normalization": "NFC and blank-cell projection; explicit spaces, mode bits and terminal replies excluded; pyte comparison below covers final visible rows and cursor only", + "fixtures": [ + { + "fixture": "experiments/core/fixtures/more-pager-360.bin", + "bytes": 360, + "sha256": "d035d53385788d2b68dcc0d866b5a2fe68897cdcb28a761bd4d36efcc6df6cf9", + "suffix_hex": "465f504147455f32330d0a2d2d204d6f7265202835362529202d2d20", + "runs": [ + { + "version": "pre_fix", + "plan": "whole", + "status": "match", + "checkpoints": 2, + "first_difference": null, + "default_final_rows": [ + "TF_PAGE_23", + "--More(56%", + ")--" + ], + "candidate_final_rows": [ + "TF_PAGE_23", + "--More(56%", + ")--" + ], + "pyte_final_rows": [ + "TF_PAGE_23", + "--More(56%", + ")--" + ], + "default_cursor": [ + 5, + 2 + ], + "candidate_cursor": [ + 5, + 2 + ], + "pyte_cursor": [ + 5, + 2 + ], + "default_matches_pyte_visible_rows": true, + "candidate_matches_pyte_visible_rows": true + }, + { + "version": "pre_fix", + "plan": "byte", + "status": "match", + "checkpoints": 361, + "first_difference": null, + "default_final_rows": [ + "TF_PAGE_23", + "--More(56%", + ")--" + ], + "candidate_final_rows": [ + "TF_PAGE_23", + "--More(56%", + ")--" + ], + "pyte_final_rows": [ + "TF_PAGE_23", + "--More(56%", + ")--" + ], + "default_cursor": [ + 5, + 2 + ], + "candidate_cursor": [ + 5, + 2 + ], + "pyte_cursor": [ + 5, + 2 + ], + "default_matches_pyte_visible_rows": true, + "candidate_matches_pyte_visible_rows": true + }, + { + "version": "post_fix", + "plan": "whole", + "status": "match", + "checkpoints": 2, + "first_difference": null, + "default_final_rows": [ + "TF_PAGE_23", + "--More(56%", + ")--" + ], + "candidate_final_rows": [ + "TF_PAGE_23", + "--More(56%", + ")--" + ], + "pyte_final_rows": [ + "TF_PAGE_23", + "--More(56%", + ")--" + ], + "default_cursor": [ + 5, + 2 + ], + "candidate_cursor": [ + 5, + 2 + ], + "pyte_cursor": [ + 5, + 2 + ], + "default_matches_pyte_visible_rows": true, + "candidate_matches_pyte_visible_rows": true + }, + { + "version": "post_fix", + "plan": "byte", + "status": "match", + "checkpoints": 361, + "first_difference": null, + "default_final_rows": [ + "TF_PAGE_23", + "--More(56%", + ")--" + ], + "candidate_final_rows": [ + "TF_PAGE_23", + "--More(56%", + ")--" + ], + "pyte_final_rows": [ + "TF_PAGE_23", + "--More(56%", + ")--" + ], + "default_cursor": [ + 5, + 2 + ], + "candidate_cursor": [ + 5, + 2 + ], + "pyte_cursor": [ + 5, + 2 + ], + "default_matches_pyte_visible_rows": true, + "candidate_matches_pyte_visible_rows": true + } + ] + }, + { + "fixture": "experiments/core/fixtures/more-pager-366.bin", + "bytes": 366, + "sha256": "bb9dbe77353245a2aad7ef07e12da9a8371cc7ac56d6b47ebc0bfecee35b9958", + "suffix_hex": "5f32330d0a2d2d204d6f7265202835362529202d2d200d1b5b373958", + "runs": [ + { + "version": "pre_fix", + "plan": "whole", + "status": "difference", + "checkpoints": 2, + "first_difference": { + "checkpoint": 1, + "offset": 366, + "path": ".screen.cells[2][0].text", + "expected": ")", + "actual": "" + }, + "default_final_rows": [ + "TF_PAGE_23", + "--More(56%", + ")--" + ], + "candidate_final_rows": [ + "TF_PAGE_23", + "--More(56%", + "" + ], + "pyte_final_rows": [ + "TF_PAGE_23", + "--More(56%", + "" + ], + "default_cursor": [ + 0, + 2 + ], + "candidate_cursor": [ + 0, + 2 + ], + "pyte_cursor": [ + 0, + 2 + ], + "default_matches_pyte_visible_rows": false, + "candidate_matches_pyte_visible_rows": true + }, + { + "version": "pre_fix", + "plan": "byte", + "status": "difference", + "checkpoints": 367, + "first_difference": { + "checkpoint": 366, + "offset": 366, + "path": ".screen.cells[2][0].text", + "expected": ")", + "actual": "" + }, + "default_final_rows": [ + "TF_PAGE_23", + "--More(56%", + ")--" + ], + "candidate_final_rows": [ + "TF_PAGE_23", + "--More(56%", + "" + ], + "pyte_final_rows": [ + "TF_PAGE_23", + "--More(56%", + "" + ], + "default_cursor": [ + 0, + 2 + ], + "candidate_cursor": [ + 0, + 2 + ], + "pyte_cursor": [ + 0, + 2 + ], + "default_matches_pyte_visible_rows": false, + "candidate_matches_pyte_visible_rows": true + }, + { + "version": "post_fix", + "plan": "whole", + "status": "match", + "checkpoints": 2, + "first_difference": null, + "default_final_rows": [ + "TF_PAGE_23", + "--More(56%", + "" + ], + "candidate_final_rows": [ + "TF_PAGE_23", + "--More(56%", + "" + ], + "pyte_final_rows": [ + "TF_PAGE_23", + "--More(56%", + "" + ], + "default_cursor": [ + 0, + 2 + ], + "candidate_cursor": [ + 0, + 2 + ], + "pyte_cursor": [ + 0, + 2 + ], + "default_matches_pyte_visible_rows": true, + "candidate_matches_pyte_visible_rows": true + }, + { + "version": "post_fix", + "plan": "byte", + "status": "match", + "checkpoints": 367, + "first_difference": null, + "default_final_rows": [ + "TF_PAGE_23", + "--More(56%", + "" + ], + "candidate_final_rows": [ + "TF_PAGE_23", + "--More(56%", + "" + ], + "pyte_final_rows": [ + "TF_PAGE_23", + "--More(56%", + "" + ], + "default_cursor": [ + 0, + 2 + ], + "candidate_cursor": [ + 0, + 2 + ], + "pyte_cursor": [ + 0, + 2 + ], + "default_matches_pyte_visible_rows": true, + "candidate_matches_pyte_visible_rows": true + } + ] + } + ], + "performance_measured": false, + "presented_done": null, + "full_vt_acceptance": false, + "fresh_capture": { + "bytes": 382, + "sha256": "bdf54e2c317c0e9639956ffb66ab63d5b987dd8b8514bd08bde3d867b3efb5ef", + "suffix_hex": "0d1b5b3f393030316c1b5b3f313030346c1b5b373958", + "lifecycle": { + "schema": "termforge-more-capture-v1", + "valid": true, + "stage": "output-eof", + "prompt_end_offset": 360, + "q_bytes": 1, + "exit_code": 0, + "output_bytes": 382, + "read_error": 109, + "output_eof": true, + "cleanup_complete": true, + "active_children_after": 0, + "forced_reader_cancel": false, + "forced_process_cancel": false + }, + "supervision": { + "schema": "termforge-real-app-capture-supervision-v1", + "timed_out": false, + "returncode": 0, + "job_after": { + "active": 0, + "total": 8, + "terminated": 0 + }, + "cleanup_errors": [] + }, + "real_applications_counts": { + "match": 3 + }, + "capture_source_sha256": "95b16e22e0c8fc9cf763093900f1982a01ad6808a679f00e52c3761195099fe6", + "capture_binary_sha256": "47b3f28f8506eeddf473e1614633a2ad1dae7ef2c7ae1cf1f3b25dd54d85d482" + }, + "retained_candidate_difference": { + "input_hex": "414243441b5b585a", + "rows": 2, + "columns": 4, + "history": 2, + "default_final_rows": [ + "ABCZ", + " " + ], + "candidate_final_rows": [ + "ABC ", + "Z" + ], + "default_cursor": [ + 3, + 0 + ], + "candidate_cursor": [ + 1, + 1 + ], + "registered_as_reference_difference": false, + "covered_by_current_fixed_fuzz": false, + "owner_issue": 27, + "policy": "Default follows the fixed Microsoft Terminal reset-delayed-wrap behavior; no global ECH equivalence claim" + }, + "validation": { + "recovery_unittests": 33, + "real_application_and_supervisor_tests": 8, + "common_matrix": { + "match": 95 + }, + "fuzz": { + "known_failure": 2, + "match": 6 + }, + "scope": "Local Windows 11; English More prompt; source-fixed transport and parser; no GUI or WindTerm runtime comparison" + } +} diff --git a/experiments/differential/results/2026-10-26-real-applications-v6/summary.json b/experiments/differential/results/2026-10-26-real-applications-v6/summary.json new file mode 100644 index 00000000..4101dfbd --- /dev/null +++ b/experiments/differential/results/2026-10-26-real-applications-v6/summary.json @@ -0,0 +1,206 @@ +{ + "schema": 1, + "platform": "Windows-11-10.0.26200-SP0", + "candidate": "alacritty_terminal", + "version": "0.26.1-dev+termforge.3", + "applications": { + "shell": "Windows cmd.exe", + "pager": "Windows more.com", + "editor": "VIM - Vi IMproved 9.1 (2024 Jan 02, compiled Jul 09 2024 07:07:42)" + }, + "capture_sha256": "f2318c900de6802312ed3ed3b73624fb5644d47b3eab013dbb0e01c6a226ff9c", + "core_trace_sha256": "eeb32a223a192b45f40719d5f0126f61163fb01ac5fcf16de4a1059ae17f5b2d", + "candidate_trace_sha256": "e398d78853a62275aa29d1d7b63121956768e6a42a69d01b7719bfce3593fb44", + "source_sha256": { + "experiments/conpty/real_app_capture.cpp": "8b606be3a916a634d5fb9fd73238763af1afb6d39b2c3793a8a970dc0c01a944", + "experiments/conpty/session.hpp": "f143a858df22302a2bc4a794263ff7769f4d0cfd9537307d916feb8ad638c6b6", + "experiments/core/screen.hpp": "60dee7105f9cd58611896e5fd83797533d0115a575833d00a16e8ae874d002ed", + "experiments/core/trace.cpp": "d77b41acb0cfc261c47d9972d7b3170bc0049bc8001e2e0e1cf55ec15b19a9ec", + "experiments/differential/real_app_matrix.py": "b3ebc063355320283b8974f88248516b3cc250f6e6d5ce3aff8446d0e266e1d2", + "experiments/differential/compare_candidate.py": "b823369ec73dd6d4de4678898271dc26357587ff3ff1dccb340c228ea93d75b3", + "experiments/differential/decisions.md": "5676d502941f1d6e1d3a8e467653e2fc20d73794e9b6d04d0a777885cf107160", + "experiments/candidates/alacritty/run.py": "fc3b4ff1485eee9eb4525bbc9d4b5911eb4a5836e5e16c04d83a1aaf2847a665", + "experiments/candidates/alacritty/vendor/alacritty_terminal/CHANGELOG.md": "97231d9beda30b536650c566169c692fa846be3f7b0405add3c9741f95f3af62", + "experiments/candidates/alacritty/vendor/alacritty_terminal/Cargo.toml": "af09057a0404d3034a0e06103af3d2218cafd43e8e99ec68a2ab1c0e5afe04c0", + "experiments/candidates/alacritty/vendor/alacritty_terminal/LICENSE-APACHE": "8884289e3969c089cd556c53fa1d3f51d86b29e52a8c1d660a55ce8292986910", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/event.rs": "e76c212f36bbfc43ea037a825dbc9c5b1174f5836fe8b69dfd158aadf3fc4048", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/event_loop.rs": "6da2897b76d8c0e7f989daa1463fd6fdeb3469a0a63e0c6ba703a9562663163c", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/mod.rs": "be15d08b03a28a271aa927b7149346a1b5d29e92e9186891bf246fdd2a4c90fc", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/resize.rs": "a9a9a285ed28d75b547f86554174c192f74f2a0fb1b331f43a5d2433bcd5d9d7", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/row.rs": "2f963299e52525828d9d0bf1de758d793661c908eb7c126aa8556a1c9945a83d", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/storage.rs": "27a9b5101833d57423ae34ccef3d82c1a1cf9f51cbc42cc836a6fce79c088348", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/tests.rs": "5f38819134540eeb0dff7047f57f5d06250ce59493839c66ec4cd56e43db62f8", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/index.rs": "46908b68508f4e4ea90313f0536a88c357f2d17ad211b14d44b7f92dfdaa4402", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/lib.rs": "b9cae8ed53a4be8f738111f8727dd86218e66c88df38f32f4904e56651a2bee1", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/selection.rs": "0d1373577602646e84a4ab3448c65ee181987327bdabceff3c827e76dd9bd0d8", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/sync.rs": "4410fbae64e8f80321b3569a1495fb323e3319812c441f9eac54048c65ff93fb", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/cell.rs": "a569c233790530e6502ca031d0d3ed488b5aa2356e872da16ca689d64cb7b62c", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/color.rs": "ad2e33b60f397fe30e5e8f2dde2096bad8f5e256d464ba279cbda4a93cbbe224", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/mod.rs": "fa1382d465c2b35361201d87303573ba4660de96fc01e2091b6075b934e6564c", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/search.rs": "84e04c5f0da21009dbbcfc2f8033e322d6240c8d227028b5ce8a784f1c55ccaf", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/thread.rs": "e2f9fdc5d950bbea6c710c7a974f2998127af6b434344c363afb38e849f86144", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/tty/mod.rs": "8cca30ba29cee2df84603f560a8d26624a48979d96d78f1b6729a470ff34d8a1", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/tty/unix.rs": "2e8d0edf1aea816ac95c1e442d60025ebdd20dacaa99d0fd4f7c7cf78ac369e4", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/tty/windows/blocking.rs": "60121a65df120c4a227520d6672231c9e3c515cb75241241e6b37a0c5bc186ca", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/tty/windows/child.rs": "4782f173c639bff6891f474b068fee64b6e2ad411edabdf496bbfe6554ceb32e", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/tty/windows/conpty.rs": "62abd7e72af144ae1f1c359538e18cceaa97741382efa94e001f397510eef416", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/tty/windows/mod.rs": "b08803dae0f192bc6737aff22992a37368ac505c7133554c495ffcacb4116733", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/vi_mode.rs": "f52c0d3248707c68295880acd7ad96610467110d6813f1172d4ae767365add2e" + }, + "capture_manifest": { + "shell_exit": 0, + "pager_exit": 0, + "editor_exit": 0, + "shell_bytes": 85, + "pager_bytes": 360, + "editor_bytes": 1999, + "editor_file_verified": true, + "startup_event_verified": true, + "insert_event_verified": true, + "editor_path": "C:/Program Files/Git/usr/bin/vim.exe" + }, + "runs": 3, + "counts": { + "match": 3 + }, + "passed": true, + "execution_passed": true, + "real_application_covered": true, + "semantic_acceptance": false, + "performance_measured": false, + "presented_done": null, + "records": [ + { + "case": "shell", + "plan": "whole", + "input_sha256": "5f27c3d595f5c6e971d40e3e88e35e906f044bfcae4befa8e0745e1a8a78b442", + "bytes": 85, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 85 + ], + "checkpoints": 2, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "candidate_final_rows": [ + "TF_REAL_SHEL", + "L", + "" + ] + }, + { + "case": "pager", + "plan": "whole", + "input_sha256": "d035d53385788d2b68dcc0d866b5a2fe68897cdcb28a761bd4d36efcc6df6cf9", + "bytes": 360, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 360 + ], + "checkpoints": 2, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "candidate_final_rows": [ + "TF_PAGE_23", + "--More(56%", + ")--" + ] + }, + { + "case": "editor", + "plan": "whole", + "input_sha256": "3c316ef0154e66d86b337c620f10dcd240408a290c21d4b05b3b44051c5d1e55", + "bytes": 1999, + "rows": 3, + "columns": 12, + "history": 3, + "chunk_sizes": [ + 1999 + ], + "checkpoints": 2, + "status": "match", + "core_returncode": 0, + "candidate_returncode": 0, + "core_error": null, + "candidate_error": null, + "first_difference": null, + "difference_direction": "expected=TermForge core; actual=Alacritty candidate; neither is an oracle", + "normalization": "NFC text; blank/empty cells share a projection; explicit-space semantics are excluded; mode_bits/replies are not compared here", + "reproduce": { + "core": [ + "terminal_trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ], + "candidate": [ + "termforge-alacritty-trace", + "input.bin", + "3", + "12", + "3", + "chunks.txt" + ] + }, + "candidate_final_rows": [ + "1L,12Bwri", + "tten", + "" + ] + } + ] +} diff --git a/experiments/differential/test_real_app_matrix.py b/experiments/differential/test_real_app_matrix.py index 82965844..f8a00546 100644 --- a/experiments/differential/test_real_app_matrix.py +++ b/experiments/differential/test_real_app_matrix.py @@ -1,13 +1,90 @@ """Real Windows shell/pager/editor traces; never substituted with synthetic TUI.""" import json +import copy import os from pathlib import Path import tempfile import unittest +from unittest import mock from compare_candidate import candidate_trace_binary import real_app_matrix from run import ROOT, trace_binary +from experiments.conpty.run import OwnedJob + + +@unittest.skipUnless(os.name == "nt", "Windows-owned capture supervisor") +class CaptureSupervisorFailureTests(unittest.TestCase): + def test_assignment_failure_never_releases_go_and_keeps_original_error(self): + with tempfile.TemporaryDirectory() as temporary: + output = Path(temporary) / "capture" + process, job = mock.Mock(), mock.Mock() + original = OSError("private assignment failure") + process.poll.side_effect = [None, 0, 0] + process.communicate.return_value = (b"", b"") + job.assign.side_effect = original + with mock.patch.object(real_app_matrix, "OwnedJob", return_value=job), \ + mock.patch.object(real_app_matrix.subprocess, "Popen", return_value=process): + with self.assertRaises(OSError) as raised: + real_app_matrix.run_capture(Path("never-executed.exe"), Path("never-executed-vim.exe"), output) + self.assertIs(raised.exception, original) + self.assertTrue(all(not call.args for call in process.communicate.call_args_list)) + self.assertFalse((output / "pager.bin").exists()) + process.kill.assert_called_once() + job.close.assert_called_once() + + def test_timeout_and_termination_failure_keep_actual_prefix_and_close_owned_job(self): + with tempfile.TemporaryDirectory() as temporary: + output = Path(temporary) / "capture" + prefix = (ROOT / "experiments/core/fixtures/more-pager-360.bin").read_bytes() + process, job = mock.Mock(), mock.Mock() + process.pid = 123 + process.poll.side_effect = [None, 0, 0] + process.communicate.side_effect = [real_app_matrix.subprocess.TimeoutExpired("capture", 60), + (b"captured partial result", b"private timeout detail")] + job.terminate.side_effect = OSError("private terminate detail") + job.drain.return_value = {"active": 0, "total": 4, "terminated": 0} + def start(*args, **kwargs): + output.mkdir() + (output / "pager.bin").write_bytes(prefix) + return process + with mock.patch.object(real_app_matrix, "OwnedJob", return_value=job), \ + mock.patch.object(real_app_matrix.subprocess, "Popen", side_effect=start): + with self.assertRaises(RuntimeError): + real_app_matrix.run_capture(Path("never-executed.exe"), Path("never-executed-vim.exe"), output) + self.assertEqual((output / "pager.bin").read_bytes(), prefix) + supervision = json.loads((output / "supervision.json").read_text()) + self.assertTrue(supervision["timed_out"]) + self.assertIn("terminate_failed", supervision["cleanup_errors"]) + self.assertNotIn("private terminate", json.dumps(supervision)) + job.close.assert_called_once() + + +@unittest.skipUnless(os.name == "nt", "Windows native capture gate") +class CaptureNativeGateTests(unittest.TestCase): + def test_bad_eof_and_oversized_go_leave_no_capture_or_pty_client(self): + executable = real_app_matrix.capture_binary() + with tempfile.TemporaryDirectory() as temporary: + for number, gate in enumerate((b"", b"NOT_GO\n", b"G" * 64 + b"\n")): + output = Path(temporary) / f"capture-{number}" + job = OwnedJob() + process = None + try: + process = real_app_matrix.subprocess.Popen([str(executable), str(output), "never-executed-vim.exe"], + stdin=real_app_matrix.subprocess.PIPE, stdout=real_app_matrix.subprocess.PIPE, + stderr=real_app_matrix.subprocess.PIPE, creationflags=real_app_matrix.subprocess.CREATE_NO_WINDOW) + job.assign(process.pid) + process.communicate(gate, timeout=5) + self.assertEqual(process.returncode, 2) + self.assertFalse(output.exists()) + accounting = job.drain() + self.assertEqual(accounting["active"], 0) + self.assertIn(accounting["total"], (1, 2)) # host's own headless console is permitted + finally: + if process is not None and process.poll() is None: + job.terminate() + process.communicate(timeout=5) + job.close() class RealApplicationMatrixTests(unittest.TestCase): @@ -46,6 +123,35 @@ def test_real_lifecycle_and_core_comparison(self): self.assertTrue(manifest["insert_event_verified"]) self.assertEqual((self.artifacts / "suite/editor-startup.txt").read_bytes(), b"READY\n") self.assertEqual((self.artifacts / "suite/editor-insert.txt").read_bytes(), b"INSERT\n") + lifecycle = json.loads((self.artifacts / "suite/capture/pager-lifecycle.json").read_text()) + self.assertTrue(lifecycle["valid"]) + self.assertTrue(lifecycle["output_eof"]) + self.assertEqual(lifecycle["read_error"], 109) + self.assertEqual(lifecycle["q_bytes"], 1) + self.assertEqual(lifecycle["active_children_after"], 0) + supervision = json.loads((self.artifacts / "suite/capture/supervision.json").read_text()) + self.assertFalse(supervision["timed_out"]) + self.assertEqual(supervision["job_after"]["active"], 0) + self.assertEqual(supervision["cleanup_errors"], []) + + def test_pager_eof_and_prompt_claims_cannot_hide_missing_or_partial_capture(self): + capture = self.artifacts / "suite/capture" + raw = (capture / "pager.bin").read_bytes() + lifecycle = json.loads((capture / "pager-lifecycle.json").read_text()) + with tempfile.TemporaryDirectory(dir=self.artifacts) as temporary: + out = Path(temporary) + for field, wrong in (("valid", False), ("output_eof", False), ("read_error", 0), + ("q_bytes", 0), ("forced_reader_cancel", True), + ("output_bytes", len(raw) - 6), ("prompt_end_offset", 9)): + bad = copy.deepcopy(lifecycle); bad[field] = wrong + (out / "pager.bin").write_bytes(raw) + (out / "pager-lifecycle.json").write_text(json.dumps(bad)) + with self.subTest(field=field), self.assertRaises(ValueError): + real_app_matrix.validate_pager_capture(out) + (out / "pager.bin").write_bytes(raw[:100]) + (out / "pager-lifecycle.json").write_text(json.dumps(lifecycle)) + with self.assertRaises(ValueError): + real_app_matrix.validate_pager_capture(out) def test_failure_preserves_editor_stage_trace_and_cleanup(self): executable = Path("C:/Windows/System32/cmd.exe") diff --git a/experiments/presentation/render.hpp b/experiments/presentation/render.hpp index 84ab0eb7..de1ad2ff 100644 --- a/experiments/presentation/render.hpp +++ b/experiments/presentation/render.hpp @@ -21,6 +21,7 @@ static double ms(Clock::time_point start) { return std::chrono::duration(text.size()), nullptr, 0); require(n != 0, "invalid text"); std::wstring value(n, 0); require(MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, text.data(), static_cast(text.size()), value.data(), n) == n, "UTF-16 conversion failed"); diff --git a/experiments/presentation/renderer_metrics_test.cpp b/experiments/presentation/renderer_metrics_test.cpp index 0ccc28c6..8ceaa0ba 100644 --- a/experiments/presentation/renderer_metrics_test.cpp +++ b/experiments/presentation/renderer_metrics_test.cpp @@ -2,6 +2,17 @@ #include "../shell/ui_font.hpp" #include +static void verify_text_conversion() { + require(utf16("").empty(), "empty UTF-8 must convert to an empty UTF-16 string"); + require(utf16("A中e\xcc\x81😀") == L"A中e\u0301\U0001f600", "valid UTF-8 scalars changed during conversion"); + for (const auto& invalid : {std::string("\xff"), std::string("\xc0\x80"), + std::string("\xed\xa0\x80"), std::string("\xe4\xb8")}) { + bool rejected = false; + try { (void)utf16(invalid); } catch (const std::runtime_error&) { rejected = true; } + require(rejected, "nonempty invalid UTF-8 must remain rejected"); + } +} + static void expect_rejected(const std::string& backend, int dpi, int zoom) { bool rejected = false; try { Renderer renderer(backend, dpi, zoom); } @@ -135,6 +146,7 @@ static void scenario(const std::string& backend, int dpi, int zoom, float baseli int main(int argc, char** argv) { try { + verify_text_conversion(); if (argc == 2 && std::string(argv[1]) == "--expect-missing-font") { try { WorkspaceFont font; } catch (const std::runtime_error& error) { diff --git a/experiments/shell/README.md b/experiments/shell/README.md index 08bcbf76..e28c2b36 100644 --- a/experiments/shell/README.md +++ b/experiments/shell/README.md @@ -92,7 +92,7 @@ E2驱动是唯一默认传入`resources=True`的场景驱动,依赖项目Pytho resize 两类场景各重复三次,通过真实 `SetWindowPos` 缩放并由子进程查询实际尺寸;包括快速连续请求、洪流中缩放、随后 Ctrl-C 或关闭。正常 resize 验证 60×12、100×28、72×18;关闭场景只查询前两个尺寸。脚本还将自有窗口移至非输入 desktop 右下边缘,验证 retained DIB 不依赖可见更新矩形。每次绘制与全量重绘作 RGB 对比;这些带检查的样本不能作为低开销延迟数据。 -每次运行依次验证普通命令、中文、退格、左右方向键编辑,再运行一百万轮的输出命令。至少持续一秒且消费 64 KiB 后发送 Ctrl-C,再执行 `echo TF_AFTER`;确认对应状态与已提交帧后 `exit 37`。当前 Windows 11 上 normal/resize 在该中断阶段超时,由 #36 保留,完整 CTest 不能记为通过。另测输出中关闭,正常/关闭各重复三次,检查 Job 活跃进程归零与自有 desktop 消失。监督器用真实存活 shell 验证超时清理。独立队列探针验证容量、背压、顺序、停止唤醒与输入拒绝。短回归不代表长时无泄漏。 +每次运行依次验证普通命令、中文、退格、左右方向键编辑,再运行一百万轮的输出命令。至少持续一秒且消费 64 KiB 后发送 Ctrl-C,再执行 `echo TF_AFTER`;确认对应状态与已提交帧后 `exit 37`。[#36](https://github.com/JavaZeroo/termforge/issues/36) 保留历史中断超时与根因缺口;本次 normal/resize 各十轮未复现,不能写成产品修复。独立的[原始 ConPTY 回归](../conpty/README.md#持续-cmd-输出与-ctrlc-独立回归)另保留输入/输出与系统文件身份。另测输出中关闭,正常/关闭各重复三次,检查 Job 活跃进程归零与自有 desktop 消失。监督器用真实存活 shell 验证超时清理。独立队列探针验证容量、背压、顺序、停止唤醒与输入拒绝。短回归不代表长时无泄漏。 产品真实 IME 单独由 [`research.ime.product_runner`](../../research/ime/README.md) 显式托管运行,使用严格受控的真实 `SendInput`,不在这些私有 desktop 场景中合成 IME 消息。五个 `ui-ime-*` 场景有独立输入桌面/焦点/代次检查和内外 Job/GO 门控,开发机拒绝运行;历史/Find 与 rename 双 Escape 已有[固定源码系统审计](../../research/ime/results/2026-10-24-product-ime-contexts/validation.json)。 @@ -100,6 +100,10 @@ resize 两类场景各重复三次,通过真实 `SetWindowPos` 缩放并由子 默认监督时限为 40 秒,原生基础场景为 25 秒,主区、DPI、选择和查找场景为 30 秒;`ui-clipboard-system` 的监督/原生场景分别为 60/45 秒,watchdog 场景在 3 秒后强制回收。stdout/stderr、失败状态、最终自有 BMP、身份与环境均保留。CTest 产物在 `TERMFORGE_TEST_ARTIFACTS/shell`,默认 `build/test-artifacts/shell`;runner 输出目录必须不存在。只有审核过的合成记录可公开,日常用户会话不会自动录制或上传。 +`ui-shells` 要求四种固定入口全部可用,缺少 Ubuntu 不作为跳过。指定一次性 GitHub Windows VM 在运行严格测试前,由 [prepare_hosted_shells](../../tools/prepare_hosted_shells.py) 检查真实启动;仅 Ubuntu 未注册时导入固定 SHA-256 的官方 rootfs。已有 Ubuntu 启动失败仍失败,不替换发行版、不启用系统功能或更改默认项。该入口拒绝普通本机环境;纯 guard/oracle 检查用 `python -m unittest tools.test_prepare_hosted_shells -v`,不执行导入。 + +四 shell 场景的 `shells-progress.jsonl` 保留阶段、退出及回收事实。提前退出立即失败,最多 8 KiB 的真实输出尾部只写入场景 `_private/local-shells/`;CI 不上传 `_private`。选择场景先把原固定内容锚点滚入可见区再拖动,另以 681/860 两种客户区高度回归;内容、淘汰和复制断言保持相同。 + E2产物在`TERMFORGE_TEST_ARTIFACTS/workflow-sessions/run-*`;公开白名单包含合成source/raw/progress、`sessions.json`、脱敏`sample.public.json`、`resource-observations.jsonl`和`resource-summary.json`。原始`sample.json`、controller `stdout.private.json`、`.identity`、`transport.bin`、私有stderr和整个`_resources-private/`均不上传,失败兜底也排除。资源公开记录在CI本地与raw逐条比对,下载后可离线重算脱敏观测;无法从公开artifact重哈希未上传的原始传输和二进制。 ## 依据 @@ -113,6 +117,7 @@ E2产物在`TERMFORGE_TEST_ARTIFACTS/workflow-sessions/run-*`;公开白名单 | `candidate-tui` / `candidate-input-escape` | 仅显式候选构建:真实 Vim 进入/插入/60×12 resize/退出写盘,主屏恢复、独立 `CSI 6n` 光标回复及独立 ESC 接收 | `test_candidate_tui.py` 各连续两轮;固定 `candidate-vim.vim` 以短 `-S` 命令加载,避免交互式 cmd 拆分长命令;以 Vim 写入的真实 `VimEnter`/`InsertEnter` 事件文件为启动握手,以 `Ctrl-O` 调用命令保存实际 `&columns/&lines`,核对脚本、文件、子进程尺寸及 InputQueue 字节;有界合成输出另存 `candidate-output.bin`,私有 desktop/Job,无截图。不把该路径写成 Vim Escape 或备用屏验收,不替代 #28/#29 或实际呈现验收 | | `candidate-alt` | 仅显式候选构建:两次 1049h/1049l、主屏文字/样式/光标恢复、历史隔离 | [两轮限定验证](results/2026-10-26-alternate-screen/validation.json)独立核对源 VT、五阶段真实 `CHAR_INFO`、transport 和候选状态;原生恢复快照一致,备用屏文字不进入主历史。夹具按实际控制台状态有界同步,不把切换前快照误记为切换后;origin/wrap、独立 margin、resize 和 #27 整项未验收,`presented_done=null` | | `candidate-scroll` | 仅显式候选构建:DECSTBM 局部滚动、区域外保持、有限历史语义 | [两轮限定验证](results/2026-10-26-scroll-region/validation.json)独立核对源 VT、三阶段真实 `CHAR_INFO`、候选保留文档及 `pyte==0.8.2` transport 重放;区域外不变、C/D 位于正确区域、A/B 不混入主历史。origin/wrap、独立 margin、resize 和 #27 整项未验收,`presented_done=null` | +| `candidate-margins` | 仅显式候选构建:主屏 `2;3r`、备用屏 `4;5r`、恢复后局部滚动及四阶段真实控制台快照 | [限定验证](results/2026-10-26-margin-state/validation.json)分别核对公开源 VT 的恢复、resize/RIS 与真实 ConPTY 的宿主差异;当前宿主出现 `REGION_CIXED`,由 [#41](https://github.com/JavaZeroo/termforge/issues/41) 保留。两轮差异观察均为 `margins_per_buffer_verified=false`,不是能力通过;无截图或前台输入,`presented_done=null` | | `candidate-unicode` | 仅显式候选构建:固定分解组合、单 emoji、ZWJ/keycap 的真实 ConPTY 文本、子控制台 `CHAR_INFO`、候选单元格和实际 DWrite glyph | [两轮限定验证](results/2026-10-26-unicode-renderer/validation.json)独立核对源、transport、单元格和 glyph;过宽文字只在原网格内缩小字号。ZWJ/keycap 只验收原文保留和 glyph,不宣称聚类或宽度合同已通过,后者归 [#39](https://github.com/JavaZeroo/termforge/issues/39);`presented_done=null` | | `ui-workflow-sessions` | 8真实会话/拒绝第9个、五路有限洪流、阅读/Find/输入/缩放/淘汰、合成粘贴取消/关闭、5次8→6→8 | `test_workflow_sessions`独立literal/raw/journal/UI和回收校验;每轮26检查点、18身份、235/256观测,资源scope=launcher-tree。data-only,不代替同次实际CF/IME、native阶段校准或操作提交端点 | | `ui` / `ui-edit` / `ui-restore` | 多标签、五处分隔线、重命名、捕获/Escape 回滚、新进程恢复布局 | 真实子进程尺寸、保存文件、焦点与回收;只新建会话,不恢复原进程,恢复范围见工作区保存 | @@ -135,6 +140,8 @@ E2产物在`TERMFORGE_TEST_ARTIFACTS/workflow-sessions/run-*`;公开白名单 | `ui-paste-system` | 真实 Unicode 系统剪贴板 → 快捷键 → 确认 → 接收端 | 只在指定一次性 hosted 环境运行;本地未运行/跳过不计系统验收 | | `ui-clipboard-system` | 经产品 Paste 验证占用/超限错误与重试、读取途中取消/关闭 | 延迟渲染 owner 握手、真实接收端字节、错误画面及 helper/Job 回收;仅一次性 hosted 环境运行;场景存在不等于整项通过,当前结果见 [#14](https://github.com/JavaZeroo/termforge/issues/14) | +`ui-copy-system` 的嵌套 `copy_contention` 另有错误帧几何版本:v1 历史结果继续只接受 1360×860,不得补写现场几何;新 native 生产 v2,在错误发生并绘制时独立查询前后 `GetClientRect` 和 `GetObjectW` 的实际 DIB 尺寸、stride、格式,要求它们与物理 surface 一致。Python 用同一份现场元数据核对完整 BMP 头和精确字节数,接受宿主合法裁剪后的尺寸,同时拒绝缺失或相互矛盾的元数据。纯 oracle 测试和为旧 BMP 构造的模拟 v2 数据只验证校验器,不会把旧结果升级为真实 v2 观测;新的现场合同必须由新源码在指定 hosted VM 实际执行后验收。 + 系统剪贴板边界另由 `python -m unittest experiments.shell.test_paste -v` 验证。畸形终止符用例保留 `terminator-before/after/reader.utf16le`:先核对发布前确实没有 NUL,再独立捕获系统返回的完整分配。仅接受“原分配未变且读取器拒绝”或“最后一个单元被系统改为 NUL 且读取器精确返回其前缀”两种有字节证据的结果;其他转换仍失败并留存原始内容。跨平台 `paste_test` 直接测试生产读取器共用的有界解析逻辑,包括无终止符、超限、分配末端与字符上限,不能用系统可能规范化了夹具代替解析边界测试。 原生 `layout_state` 测试覆盖严格读取、截断、非法数值、Unicode 路径和替换失败保留。`renderer_metrics` 覆盖 GDI/DirectWrite × 96/120/144/192 DPI × 50/100/125/200% 共 32 组,检查非整数边界、实际绘字与 dirty/full 一致;DWrite glyph run 另断言 ASCII/组合字符的 Consolas 身份、中文非 Consolas fallback 和缺字负例,不把请求字体或 GDI 对照当作实际 fallback 证明。同一测试还验证私有 Roboto Mono 的 GDI face、9pt em 高度和实际行高,并拒绝越界 DPI;其资产依赖与 `termforge_shell` 共用。测试会在构建目录复制一个不含 `assets/` 的独立进程,确认缺失字体以指定错误明确失败,等待期限 10 秒,结束后删除副本。 diff --git a/experiments/shell/candidate_title_test.cpp b/experiments/shell/candidate_title_test.cpp new file mode 100644 index 00000000..90ef7a63 --- /dev/null +++ b/experiments/shell/candidate_title_test.cpp @@ -0,0 +1,138 @@ +// Real console title changes through the owned candidate ShellWindow transport. +#define WinMain included_shell_entry +#include "shell.cpp" +#undef WinMain +#include "ime_desktop_process.hpp" + +namespace { +std::filesystem::path executable_path() { + wchar_t path[32768]{}; + const auto size = GetModuleFileNameW(nullptr, path, 32768); + require(size && size < 32768, "title fixture executable path unavailable"); + return std::filesystem::path(std::wstring(path, size)); +} + +void require_owned_job() { + BOOL owned = FALSE; + require(IsProcessInJob(GetCurrentProcess(), nullptr, &owned) && owned, + "title fixture requires an owned Job"); +} + +int console_title_client(const std::string& event_name) { + require_owned_job(); + constexpr std::string_view prefix = "Local\\TermForgeTitle-"; + require(event_name.size() > prefix.size() && event_name.size() <= 96 && event_name.rfind(prefix, 0) == 0, + "title release event identity invalid"); + for (size_t index = prefix.size(); index < event_name.size(); ++index) + require((event_name[index] >= '0' && event_name[index] <= '9') || event_name[index] == '-', + "title release event suffix invalid"); + tf_conpty::Handle release(OpenEventW(SYNCHRONIZE, FALSE, utf16(event_name).c_str())); + require(release.get() != nullptr, "title release event missing"); + DWORD mode = 0; + require(GetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), &mode), + "title fixture requires its own real console"); + require(SetConsoleTitleW(L"TF_TITLE_NONEMPTY"), "nonempty console title failed"); + require(SetConsoleTitleW(L""), "empty console title failed"); + constexpr char marker[] = "\r\nTF_TITLE_EMPTY_DONE\r\n"; + DWORD written = 0; + require(WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), marker, sizeof(marker)-1, &written, nullptr) && + written == sizeof(marker)-1, "title fixture marker write failed"); + require(WaitForSingleObject(release.get(), 5000) == WAIT_OBJECT_0, "title fixture release deadline"); + return 0; +} + +bool empty_title_sequence(const std::string& bytes) { + return bytes.find("\x1b]0;\a") != std::string::npos || + bytes.find("\x1b]2;\a") != std::string::npos || + bytes.find("\x1b]0;\x1b\\") != std::string::npos || + bytes.find("\x1b]2;\x1b\\") != std::string::npos; +} + +template +void title_wait(ShellWindow& window, Predicate predicate, const char* reason) { + const auto deadline = GetTickCount64() + 5000; + while (!predicate()) { + require(GetTickCount64() < deadline, reason); + MSG message{}; + for (int count = 0; count < 256 && PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE); ++count) { + if (!window.route_find_message(message) && !window.route_paste_message(message)) { + TranslateMessage(&message); DispatchMessageW(&message); + } + } + MsgWaitForMultipleObjects(0, nullptr, FALSE, 1, QS_ALLINPUT); + } +} + +int title_window_case() { + require_owned_job(); + require(tf_shell::is_ime_desktop_child(), "title fixture requires its owned non-input desktop"); + require(SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2), + "title fixture DPI setup failed"); + wchar_t configured[32768]{}; + const auto size = GetEnvironmentVariableW(L"TERMFORGE_TEST_ARTIFACTS", configured, 32768); + require(size < 32768, "title artifact root exceeds bound"); + const auto base = size ? std::filesystem::path(std::wstring(configured, size)) : + std::filesystem::current_path() / L"test-artifacts"; + const auto private_root = base / L"candidate-title" / L"_private"; + std::filesystem::create_directories(private_root); + const auto out = private_root / (L"run-" + std::to_wstring(GetCurrentProcessId()) + L"-" + std::to_wstring(GetTickCount64())); + require(std::filesystem::create_directory(out), "title fixture evidence must be fresh"); + const auto event_name = L"Local\\TermForgeTitle-" + std::to_wstring(GetCurrentProcessId()) + L"-" + std::to_wstring(GetTickCount64()); + tf_conpty::Handle release(CreateEventW(nullptr, TRUE, FALSE, event_name.c_str())); + const auto event_error = GetLastError(); + require(release.get() && event_error != ERROR_ALREADY_EXISTS, "title release event must be new"); + ShellWindow window("", ""); + window.create(); ShowWindow(window.handle(), SW_SHOWNOACTIVATE); + title_wait(window, [&] { return window.ready() && window.settled(); }, "title fixture shell ready deadline"); + window.begin_output_observation(); + size_t saved = SIZE_MAX; + const auto save_prefix = [&] { + const auto& bytes = window.observed_output(); + if (saved == bytes.size()) return; + std::ofstream file(out / L"transport.bin", std::ios::binary | std::ios::trunc); + file.write(bytes.data(), static_cast(bytes.size())); + file.close(); require(bool(file), "title fixture prefix write failed"); saved = bytes.size(); + }; + try { + window.command(L"\"" + executable_path().wstring() + L"\" --title-client \"" + event_name + L"\"\r"); + title_wait(window, [&] { + save_prefix(); + return !IsWindow(window.handle()) || + (empty_title_sequence(window.observed_output()) && window.live_contains_line("TF_TITLE_EMPTY_DONE")); + }, "title fixture actual empty OSC and marker deadline"); + save_prefix(); + const bool empty = empty_title_sequence(window.observed_output()); + const bool marker = window.live_contains_line("TF_TITLE_EMPTY_DONE"); + const bool alive = IsWindow(window.handle()) != FALSE; + require(SetEvent(release.get()), "title fixture release signal failed"); + std::cout << "{\"title_case\":\"real-console-empty\",\"empty_title_osc_observed\":" << (empty?"true":"false") + << ",\"after_marker_observed\":" << (marker?"true":"false") + << ",\"window_alive_after_empty\":" << (alive?"true":"false") << "}\n"; + window.close(); + title_wait(window, [&] { return !IsWindow(window.handle()); }, "title fixture close deadline"); + const auto result = window.collect(); + require(empty && marker && alive && result.complete && result.active_children == 0, + "real empty title must preserve candidate UI and owned cleanup"); + std::cout << "{\"title_case_cleanup\":true,\"session_job_active_after\":0}\n"; + return 0; + } catch (...) { + save_prefix(); + window.close(); + throw; + } +} +} + +int main(int argc, char** argv) { + try { + if (argc == 3 && std::string(argv[1]) == "--title-client") return console_title_client(argv[2]); + if (argc == 2 && std::string(argv[1]) == "--owned-desktop-child") { + return title_window_case(); + } + require(argc == 1, "unexpected title fixture arguments"); + for (int round = 0; round < 2; ++round) + require(tf_shell::run_ime_desktop_child(15000) == 0, "owned candidate title case failed"); + std::cout << "{\"candidate_empty_title\":\"passed\",\"owned_job_zero\":true,\"desktop_deleted\":true}\n"; + return 0; + } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; } +} diff --git a/experiments/shell/fixtures/rejected-order-before-read.json b/experiments/shell/fixtures/rejected-order-before-read.json new file mode 100644 index 00000000..493957cd --- /dev/null +++ b/experiments/shell/fixtures/rejected-order-before-read.json @@ -0,0 +1,78 @@ +{ + "manifest": { + "schema": "termforge-workflow-fixture-v1", + "fixture": "r108-console-v1", + "role": "F2", + "generation": 5, + "status": "failed", + "failure": "workflow command order invalid", + "source_bytes": 0, + "source_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "records": 0, + "worker_joined": true, + "console_window": false, + "receiver": { + "units": 0, + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "joined": true, + "error": 0 + }, + "stages": [] + }, + "receiver": { + "schema": "termforge-raw-console-receiver-v1", + "state": "stopped", + "stop_requested": true, + "worker_joined": true, + "last_read_success": false, + "last_read_count": 0, + "last_read_error": 0, + "worker_error": 0, + "failure_step": "", + "zero_read_accepted": false, + "reads_entered": 0, + "reads_completed": 0, + "cancel_calls": 1, + "cancel_successes": 0, + "last_cancel_error": 1168, + "received_units": 0, + "saved_bytes": 0, + "unexpected_cancel_observed": false, + "unexpected_cancel_error": 0, + "last_wait_result": 0, + "last_wait_error": 0 + }, + "events": [ + { + "schema": "termforge-workflow-progress-v1", + "seq": 1, + "elapsed_ms": 0, + "event": "ready", + "phase": null, + "source_bytes": 0, + "records": 0, + "source_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "schema": "termforge-workflow-progress-v1", + "seq": 2, + "elapsed_ms": 0, + "event": "event-rejected", + "phase": "append", + "source_bytes": 0, + "records": 0, + "source_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "schema": "termforge-workflow-progress-v1", + "seq": 3, + "elapsed_ms": 16, + "event": "stopped", + "phase": null, + "source_bytes": 0, + "records": 0, + "source_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + ] +} diff --git a/experiments/shell/local_shell_diagnostics.hpp b/experiments/shell/local_shell_diagnostics.hpp new file mode 100644 index 00000000..8c5048f0 --- /dev/null +++ b/experiments/shell/local_shell_diagnostics.hpp @@ -0,0 +1,34 @@ +// Synthetic ui-shells diagnostics. Raw output is private and independently bounded. +#pragma once +#include +#include +#include +#include + +namespace tf_shell_test { +inline void local_shell_progress(const std::filesystem::path& directory, std::string_view role, + std::string_view event, int stage, + std::optional exit_code = std::nullopt, + bool cleanup_complete = false, unsigned long active_children = 0, + std::string_view output = {}) { + require(role=="cmd" || role=="powershell" || role=="pwsh" || role=="wsl", "invalid local shell diagnostic role"); + require(event=="started" || event=="cwd-verified" || event=="exited", "invalid local shell diagnostic event"); + require(output.size()<=8192, "local shell diagnostic output exceeds its bound"); + std::ofstream progress(directory/"shells-progress.jsonl",std::ios::binary|std::ios::app); + progress<<"{\"schema\":\"termforge-local-shell-progress-v1\",\"role\":\""<(output.size())); + raw.close(); require(bool(raw), "local shell private output write failed"); + } +} +} // namespace tf_shell_test diff --git a/experiments/shell/results/2026-10-26-controller-exit/validation.json b/experiments/shell/results/2026-10-26-controller-exit/validation.json new file mode 100644 index 00000000..3c77f5b4 --- /dev/null +++ b/experiments/shell/results/2026-10-26-controller-exit/validation.json @@ -0,0 +1,124 @@ +{ + "schema": "termforge-candidate-workflow-fixture-red-v1", + "source_commit": "6b768b708b015afba59467662f659085078beaca", + "test": "experiments.shell.test_workflow.WorkflowFixtureTests.test_two_fresh_controller_rounds", + "original_validation": { + "schema": "termforge-workflow-controller-validation-v1", + "round": 0, + "data_only": true, + "ime_verified": false, + "clipboard_verified": false, + "executable_sha256": "ff3f05950a6f025d701f665c06368053e50bedc6b60d30d4ffda80edca795769", + "client_sha256": "ed9bd891d4ca6c602efc304b2af26146911055d35277f67bcccae6eff6013d3e", + "source_sha256": { + "experiments/shell/workflow_controller.hpp": "61005e442aa1729759dd1c1b8f0919826cb2e71f225fae53f0ea0c9cc28d38eb", + "experiments/shell/workflow_fixture_test.cpp": "29d35fd143ac215b6746f810e1915be17ad6c407623d0768121f1799e38c553c", + "experiments/shell/test_workflow.py": "a8a49ea7e6e1947a30b61b569e415e0c33c0ce642bb173b92ca125a87ab81b8a", + "experiments/shell/workflow_checks.py": "3f3bfb6d589ce7bc11736953ef55a3b676bd4baa61a8e22e8d6684520aa9f0e9" + }, + "returncode": 2, + "outer_job_after": { + "active": 0, + "total": 11, + "terminated": 0 + }, + "forced_cleanup": false, + "timed_out": false, + "process_reaped": true, + "error_kind": "AssertionError", + "status": "failed", + "cases": [] + }, + "host_passed": false, + "cases": [ + { + "name": "B graceful", + "role": "B", + "generation": 21, + "launched_through_existing_cmd": true, + "identity_verified": true, + "child_joined": true, + "observed_generation": 21, + "phases_complete": false, + "parent_order_rejected": false, + "target_job_cancelled": false, + "abnormal_completion_rejected": false, + "stopped_event": true, + "child_finished": true, + "child_exit_code": 0, + "cmd_exit_code": 3221225786, + "inner_job_active_after": 0, + "cleanup_complete": true, + "passed": false, + "transport_bytes": 1112, + "transport_sha256": "d36a189b507eb4eb21cdd2627c6cc4a94c60979a454d76924bec67f8285c04ae", + "capture_bytes": 32, + "capture_sha256": "ebdddc513596b1850a94bc79ad8c6f50017d760794cb5ba5bfcdc020f96b0b6f", + "preclose_cmd_exit_code_observed": null, + "wait_exit_result_observed": null + }, + { + "name": "A phases", + "role": "A", + "generation": 22, + "launched_through_existing_cmd": true, + "identity_verified": true, + "child_joined": true, + "observed_generation": 22, + "phases_complete": true, + "parent_order_rejected": true, + "target_job_cancelled": false, + "abnormal_completion_rejected": false, + "stopped_event": true, + "child_finished": true, + "child_exit_code": 0, + "cmd_exit_code": 0, + "inner_job_active_after": 0, + "cleanup_complete": true, + "passed": true, + "transport_bytes": 19572, + "transport_sha256": "4465d17cd6ea3df76e29fc8a8461b21f3d737a5afd2c00926fe2f4be3cede555", + "capture_bytes": 0, + "capture_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "preclose_cmd_exit_code_observed": null, + "wait_exit_result_observed": null + }, + { + "name": "B closed", + "role": "B", + "generation": 23, + "launched_through_existing_cmd": true, + "identity_verified": true, + "child_joined": true, + "observed_generation": 23, + "phases_complete": false, + "parent_order_rejected": false, + "target_job_cancelled": true, + "abnormal_completion_rejected": true, + "stopped_event": false, + "child_finished": false, + "child_exit_code": 91, + "cmd_exit_code": 91, + "inner_job_active_after": 0, + "cleanup_complete": true, + "passed": true, + "transport_bytes": 1014, + "transport_sha256": "bf8c316576124d446a1e36bbb1f3e5bb40c8437da6ddae20e917cfce9fbba510", + "capture_bytes": 0, + "capture_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "preclose_cmd_exit_code_observed": null, + "wait_exit_result_observed": null + } + ], + "interpretation": { + "root_cause_established": false, + "candidate_adapter_linked_into_probe": false, + "cmd_command_echo_disabled": true, + "missing_echo_does_not_prove_missing_input": true, + "hypothesis": "child-to-cmd input/close transition requires independent observation", + "later_replay_cannot_replace_original_red": true + }, + "public_matching_probe_source_commit": "274f3b2", + "raw_transport_published": false, + "source_binding": "recorded_inputs_not_independent_build_attestation" +} diff --git a/experiments/shell/results/2026-10-26-empty-title/original-normal-invalid-text.json b/experiments/shell/results/2026-10-26-empty-title/original-normal-invalid-text.json new file mode 100644 index 00000000..2125b09c --- /dev/null +++ b/experiments/shell/results/2026-10-26-empty-title/original-normal-invalid-text.json @@ -0,0 +1,58 @@ +{ + "schema": "termforge-original-candidate-normal-invalid-text-v1", + "source_commit": "4211763", + "source_tree": "e321dd2cd8defdd49821924bc09cb35abb6b4418", + "candidate_adapter": true, + "source_sha256": { + "experiments/presentation/render.hpp": "7645f92cff86d00864802cf901a802ca7f66da16f79510e7d6ab92a5f437115d", + "experiments/shell/shell.cpp": "c680d175326348afaf907560d8abf587d126acdef43f583da13190b2f45c9ac3", + "experiments/core/candidate_terminal.cpp": "b50dfbd3a2cf95e2b2cd46979759b2a776ed142c0e2a932ca1cae4fef235f2a4", + "experiments/candidates/alacritty/src/ffi.rs": "9d89e5d7e84cbbd0b4a5d0499f9b5eb6b739537c1dab8c154cc0351dec0dd927" + }, + "binary_sha256": { + "build\\bin\\Release\\termforge_shell.exe": "86b4503661af9786784c237773d22d80f6d279fc994958f77a2cf3a48289c6ef", + "build\\bin\\Release\\terminal_trace.exe": "d39a57555820ad756e6e18e3bbd4914c8b16583055eb8270a9f3ea87db9c9836", + "build\\bin\\Release\\runtime_observation_test.exe": "ff7001ed009b365d78c83405a0a385eeaf6a1e3829b7bf9453a92a1365206c46", + "build\\bin\\Release\\conpty_ctrl_c_probe.exe": "b3ca3c4d629cbbbb6b71298c2afccc76ea3707adcff88e1856403af87b3f6f42", + "build\\bin\\Release\\candidate_core_test.exe": "a2a4cf7456caa65b57caef0aaa3df73dbeea7f50cc3f4ccf40ee86716f2b150f", + "experiments\\candidates\\alacritty\\target\\release\\termforge-alacritty-trace.exe": "b96618ca2a06c3eda414a468e5b9479cae7da009089fa87e6c94b7deec7b1b66" + }, + "case": { + "mode": "normal", + "repeat": 1, + "status": "failed", + "native_stage": 5, + "consumed_bytes": 293447, + "process_exit_code": 2, + "outer_timed_out": false, + "failure_code": "invalid_text", + "complete_tf_after_output_line_observed": true, + "cmd_memory_diagnostic_observed": true, + "job_after_cleanup": { + "active": 0, + "total": 6, + "terminated": 0 + }, + "desktop_gone": true + }, + "not_recorded": { + "conversion_callsite": null, + "utf16_payload_byte_length": null, + "candidate_event_kind": null, + "candidate_event_payload_length": null, + "conversion_native_error": null + }, + "evidence_sha256": { + "sample.json": "d39355e2b445e5d4672087d506d16f68e4c42929ea47b5be8404fea96d6a53e5", + "frame.bmp": "c01af82120d3e8c34fd9b4d114dfe7a9a85c1479e820593201eead82a772f8ab", + "stderr_utf8": "c9de0d47de61c5672ce1a1b352999cd367e09384302c4f64cec0fafd7fcf2d14", + "candidate-junit.xml": "c9cc7115c93957fbbbf5fc6d970a8af529420e6b286579ee626258b4d22efdf1", + "LastTest-complete.log": "a5ab0d6b45ae2e5e518eb918968d5a15a824b8f7cf5ff543385e26cb1705b58b" + }, + "limits": { + "original_root_cause_established": false, + "old_event_recovered_from_later_repro": false, + "host_oom_established": false, + "new_empty_title_repro_is_separate": true + } +} diff --git a/experiments/shell/results/2026-10-26-empty-title/validation.json b/experiments/shell/results/2026-10-26-empty-title/validation.json new file mode 100644 index 00000000..2e1cabf4 --- /dev/null +++ b/experiments/shell/results/2026-10-26-empty-title/validation.json @@ -0,0 +1,139 @@ +{ + "schema": "termforge-candidate-empty-title-v1", + "issue": "https://github.com/JavaZeroo/termforge/issues/45", + "implementation_commit": "b9da64ea491e9ea14149e6b75a8b106c9ad21f3b", + "base_commit": "fba3659", + "scope": "Legal empty UTF-8 title conversion through a real owned console and candidate GUI", + "source_sha256_before": { + "experiments/presentation/render.hpp": "7645f92cff86d00864802cf901a802ca7f66da16f79510e7d6ab92a5f437115d", + "experiments/presentation/renderer_metrics_test.cpp": "e9452774f16df6045cf0db47e4d2097821005b07be5dbde8288eb7f690246eb7", + "experiments/shell/candidate_title_test.cpp": "60066ba29b6f153c78ac96fb2dbc61b68f025623d2b84c9cb83afb3604cd5362", + "experiments/shell/shell.cpp": "c680d175326348afaf907560d8abf587d126acdef43f583da13190b2f45c9ac3", + "CMakeLists.txt": "1af4d3df2fdcd74c3013ab13fd310a29496b64167ba7edf637896efdcf431fa8" + }, + "source_sha256_after": { + "experiments/presentation/render.hpp": "cd91d049e5e985f85d5400b7be51021efbf0271c781fd1d15a0c0126baa74812", + "experiments/presentation/renderer_metrics_test.cpp": "e9452774f16df6045cf0db47e4d2097821005b07be5dbde8288eb7f690246eb7", + "experiments/shell/candidate_title_test.cpp": "60066ba29b6f153c78ac96fb2dbc61b68f025623d2b84c9cb83afb3604cd5362", + "experiments/shell/shell.cpp": "c680d175326348afaf907560d8abf587d126acdef43f583da13190b2f45c9ac3", + "CMakeLists.txt": "1af4d3df2fdcd74c3013ab13fd310a29496b64167ba7edf637896efdcf431fa8" + }, + "same_test_sources_before_and_after": true, + "binary_sha256_before": { + "candidate_title_test.exe": "4fc7d45f781c5fdc34412a24e2de68879a9e0cf1506bed3c7f70d7e2e370b28f", + "renderer_metrics_test.exe": "695d6d0c5f56689014061bc0e2b90f806cbc3773192f93d30986a292aab52b6f" + }, + "binary_sha256_after": { + "candidate_title_test.exe": "e0453aa277f548025517250b6c6d86a9cff326d1bc240da7c4dbdb8e76a47a7c", + "renderer_metrics_test.exe": "73c2a2ac0c83766e6f9a840fd474f2aac00370340ccb6910d517b6c4df2dd30d" + }, + "preliminary_non_reproducing_capture": { + "transport_prefix": { + "bytes": 336, + "sha256": "3b56a2b72930994e676b906f42ea24dbd472968808e093868c797dd53becbc02", + "empty_osc_0_bel_observed": false, + "utf8_decodable": true + }, + "failure_code": "complete_empty_osc_not_observed", + "reason": "The short-lived client capture contained only cmd start/return titles; no complete empty OSC was observed.", + "invalid_text_observed": false, + "used_as_bug_reproduction": false + }, + "effective_pre_fix_gui_reproduction": { + "transport_prefix": { + "bytes": 355, + "sha256": "f3e8d444a935b57f6fd01f69609dadbdce48a2715a4d88229209fcf7acd47386", + "empty_osc_0_bel_observed": true, + "utf8_decodable": true + }, + "input_empty_title_sequence_hex": "1b5d303b07", + "client": "SetConsoleTitleW(nonempty), SetConsoleTitleW(empty), fixed marker, owned event release", + "complete_empty_osc_observed": true, + "after_marker_observed": true, + "collected_without_error": false, + "failure_code": "invalid_text", + "ctest_seconds": 0.448809 + }, + "conversion_regression": { + "valid_empty": "empty UTF-16 string", + "valid_mixed_scalars": "A中é😀", + "invalid_nonempty_hex_rejected": [ + "ff", + "c080", + "eda080", + "e4b8" + ], + "mb_err_invalid_chars_retained": true, + "renderer_metrics_original_32_scenarios_passed": true + }, + "post_fix_real_gui": { + "fresh_owned_cases": 2, + "case_observations": [ + { + "title_case": "real-console-empty", + "empty_title_osc_observed": true, + "after_marker_observed": true, + "window_alive_after_empty": true + }, + { + "title_case": "real-console-empty", + "empty_title_osc_observed": true, + "after_marker_observed": true, + "window_alive_after_empty": true + } + ], + "transport_prefixes": [ + { + "bytes": 355, + "sha256": "a7be4ae40a12c7beb8937d139ed5db74e8f3c411e16c5b39457c273f2b479537", + "empty_osc_0_bel_observed": true, + "utf8_decodable": true + }, + { + "bytes": 355, + "sha256": "3493e5d2989e22e6210861e8c9b3a1c4ac15046d12906b4527dbaf16fc7b8dbf", + "empty_osc_0_bel_observed": true, + "utf8_decodable": true + } + ], + "session_job_active_after_each": 0, + "parent_owned_job_zero_verified": true, + "owned_desktop_deletion_verified": true + }, + "old_binary_counterfactual_after_fix": { + "native_exit_code": 1, + "seconds": 0.4220000000204891, + "binary_sha256": "4fc7d45f781c5fdc34412a24e2de68879a9e0cf1506bed3c7f70d7e2e370b28f", + "log_sha256": "d1a02243dd02bda37cb7488e2716ba0379e2948a667a2a7ace414d93781eb425" + }, + "supervision": { + "owned_non_input_desktop": true, + "assign_before_resume": true, + "child_watchdog_ms": 15000, + "phase_deadline_ms": 5000, + "client_release_deadline_ms": 5000, + "raw_transport_scope": "local ignored _private prefix; no EOF or presentation claim" + }, + "old_421_normal_1_is_separate": { + "safe_summary": "original-normal-invalid-text.json", + "safe_summary_sha256": "b2776fb41ba78436d5b79c3d41d8899ef5b9ad0f406e5bc982c000852c7cd196", + "candidate_event_kind": null, + "candidate_event_payload_length": null, + "conversion_callsite": null, + "original_root_cause_established": false, + "host_oom_established": false + }, + "evidence_sha256": { + "pre-fix-junit.xml": "84e03d976243b1aa7d80c4ca4b049d86195e62888ce95e957e1952efae8e3dc6", + "pre-held-junit.xml": "b9f911b299c7d6e4aa2896a18af4f312eb0904b474e2175d119232f519b833dc", + "post-fix-junit.xml": "78ef7f3b9f18e090b5bfa08253ca60de18fef3d89d46dc66666510f40244b64a", + "pre-held.log": "2c604cc0adaf12f68ee6d6123a6ea862b02eb4afa314d72774fdd37bc91ebd86", + "post-fix.log": "efb3bf60092d655c77127220720f0fda8d9f234d798b1e2616ed6db620ec4cc1", + "pre-binary-counterfactual.log": "d1a02243dd02bda37cb7488e2716ba0379e2948a667a2a7ace414d93781eb425" + }, + "limits": { + "post_fix_full_candidate_ctest_included": false, + "complete_r2_or_windterm_parity_established": false, + "presented_done": null + } +} diff --git a/experiments/shell/results/2026-10-26-margin-state/validation.json b/experiments/shell/results/2026-10-26-margin-state/validation.json new file mode 100644 index 00000000..938bc433 --- /dev/null +++ b/experiments/shell/results/2026-10-26-margin-state/validation.json @@ -0,0 +1,666 @@ +{ + "schema": "termforge-margin-state-validation-v1", + "status": "candidate-source-contract-passed-conpty-difference-open", + "generated_utc": "2026-10-26T02:56:42Z", + "implementation_commit": "3ba2b750a1986dc7eb1874f815da8057a72393d3", + "candidate": { + "name": "alacritty_terminal", + "version": "0.26.1-dev+termforge.3", + "upstream_commit": "542147067dd016fc5f49120ef0ba605a98a4e3a7", + "license": "Apache-2.0", + "dependency_packages": { + "registry": 58, + "path": 2, + "total": 60 + } + }, + "environment": { + "platform": "Windows-11-10.0.26200-SP0", + "python": "3.12.10", + "rustc": "rustc 1.92.0 (ded5c06cf 2025-12-08)" + }, + "source_contract": { + "source_bytes": 175, + "source_sha256": "01878a5d54c343007309abb89faeb90c89262a8b55e420c5f267342ed9b27e59", + "chunk_sizes": [ + 58, + 63, + 8, + 46 + ], + "whole_and_bytewise_final_screen_equal": true, + "stages": { + "before": { + "cursor": [ + 0, + 0 + ], + "alternate": false, + "lines": [ + "TOP_FIXED", + "REGION_A", + "REGION_B", + "BOTTOM_FIXED", + "", + "", + "" + ], + "history": [] + }, + "alternate": { + "cursor": [ + 4, + 10 + ], + "alternate": true, + "lines": [ + "ALT_OUTSIDE", + "", + "", + "ALT_C", + " ALT_D", + "", + "" + ], + "history": [] + }, + "restored": { + "cursor": [ + 0, + 0 + ], + "alternate": false, + "lines": [ + "TOP_FIXED", + "REGION_A", + "REGION_B", + "BOTTOM_FIXED", + "", + "", + "" + ], + "history": [] + }, + "final": { + "cursor": [ + 6, + 0 + ], + "alternate": false, + "lines": [ + "TOP_FIXED", + "REGION_C", + " REGION_D", + "BOTTOM_FIXED", + "", + "TF_MARGIN_DONE", + "" + ], + "history": [] + } + }, + "main_margin_restored": true, + "margins_per_buffer_verified": true, + "lf_column_preservation": "单独 LF 保持列;实际 ConPTY transport 的 CRLF 单独核对,未改写源输入。", + "pyte_used_as_margin_oracle": false, + "local_trace": "build/margin-trace-sv_5qxte", + "candidate_trace_sha256": "e398d78853a62275aa29d1d7b63121956768e6a42a69d01b7719bfce3593fb44", + "reports": { + "whole": { + "bytes": 175, + "rows": 24, + "columns": 80, + "history": 100, + "chunk_sizes": [ + 58, + 63, + 8, + 46 + ], + "input_sha256": "01878a5d54c343007309abb89faeb90c89262a8b55e420c5f267342ed9b27e59", + "returncode": 0, + "error": null, + "checkpoints": 5, + "passed": true + }, + "byte": { + "bytes": 175, + "rows": 24, + "columns": 80, + "history": 100, + "chunk_sizes": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "input_sha256": "01878a5d54c343007309abb89faeb90c89262a8b55e420c5f267342ed9b27e59", + "returncode": 0, + "error": null, + "checkpoints": 176, + "passed": true + } + } + }, + "public_adapter": { + "command": "build/candidate-adapter/bin/Release/candidate_core_test.exe", + "returncode": 0, + "stdout": "candidate terminal adapter checks passed", + "binary_sha256": "c7084984e2bef9ecc8f3e8b981ed12dcc6b886525d1bfea476e8b479fce30f21", + "margin_resize_and_reset_verified": true + }, + "real_conpty_observations": { + "source": "build/test-artifacts/candidate-shell/run-o97hkqm3", + "repeat_count": 2, + "negative_cases_per_repeat": [ + "main-margin", + "alt-margin", + "restored", + "history", + "source", + "transport", + "cursor", + "frame", + "missing-stage", + "host-limit" + ], + "source_fixture_unchanged": true, + "observation_only": true, + "margins_per_buffer_verified": false, + "native_main_margins_restored": false, + "windterm_runtime_equivalence_verified": false, + "presented_done": null, + "shell_binary_sha256": "1c7446d0eb43f479f85c4f21502bb989a0e8489122be8cd84f93953f7fd33ac6", + "runs": [ + { + "repeat": 0, + "summary": { + "scoped_validation_passed": true, + "observation_only": true, + "margins_per_buffer_verified": false, + "native_main_margins_restored": false, + "conpty_margin_difference_observed": true, + "candidate_matches_native": true, + "alternate_scroll_region_verified": true, + "scroll_region_verified": false, + "outside_preserved": false, + "native_main_restored": true, + "native_viewport_top": 0, + "windterm_runtime_equivalence_verified": false, + "origin_and_wrap_modes_verified": false, + "resize_cross_verified": false, + "full_r2_contract_verified": false, + "presented_done": null, + "source_sha256": "01878a5d54c343007309abb89faeb90c89262a8b55e420c5f267342ed9b27e59", + "transport_sha256": "d32c038bad791e29644ce98bc837777e9664b3caa2ae7b5c72b9922f7d66bd8f", + "transport_bytes": 1088 + }, + "final_lines": [ + "TOP_FIXED", + "REGION_A", + "REGION_B", + "REGION_CIXED", + "REGION_D", + "TF_MARGIN_DONE" + ], + "native": { + "before": { + "text_sha256": "4279b6605b0fb08728ce75e50e7b24eaf2d75e39e3d44b12b0f41d2e6ba8343b", + "cell_sha256": "fe9016196e03b264cce22f628f67b778fe1e6a3aeb160ed53effc153b1ff6651", + "cursor": [ + 0, + 0 + ] + }, + "alt": { + "text_sha256": "f6492387ed4760be9851cccd504ac8f6f21624d55dce9671b3be4560ada0b5ac", + "cell_sha256": "0fd582bf6115b38b1dbe57084658f686392c74b4d6b1340f945cfcbca4ff7635", + "cursor": [ + 4, + 5 + ] + }, + "restored": { + "text_sha256": "4279b6605b0fb08728ce75e50e7b24eaf2d75e39e3d44b12b0f41d2e6ba8343b", + "cell_sha256": "fe9016196e03b264cce22f628f67b778fe1e6a3aeb160ed53effc153b1ff6651", + "cursor": [ + 0, + 0 + ] + }, + "final": { + "text_sha256": "605961db2489fcac706c6b5fc52a68f56acc558ac097cea21f9dd874400be438", + "cell_sha256": "00896cdf02c29564c82f4b2f7b118e6c1ac5cb8a0b8f8f5a0d91be3556a94421", + "cursor": [ + 6, + 0 + ] + } + }, + "native_restore_matches_before": true + }, + { + "repeat": 1, + "summary": { + "scoped_validation_passed": true, + "observation_only": true, + "margins_per_buffer_verified": false, + "native_main_margins_restored": false, + "conpty_margin_difference_observed": true, + "candidate_matches_native": true, + "alternate_scroll_region_verified": true, + "scroll_region_verified": false, + "outside_preserved": false, + "native_main_restored": true, + "native_viewport_top": 0, + "windterm_runtime_equivalence_verified": false, + "origin_and_wrap_modes_verified": false, + "resize_cross_verified": false, + "full_r2_contract_verified": false, + "presented_done": null, + "source_sha256": "01878a5d54c343007309abb89faeb90c89262a8b55e420c5f267342ed9b27e59", + "transport_sha256": "c0bc6270e37e656d6c85fdeae992fb9fbfb26934a6b26d964cb8e6de4796e43b", + "transport_bytes": 1088 + }, + "final_lines": [ + "TOP_FIXED", + "REGION_A", + "REGION_B", + "REGION_CIXED", + "REGION_D", + "TF_MARGIN_DONE" + ], + "native": { + "before": { + "text_sha256": "4279b6605b0fb08728ce75e50e7b24eaf2d75e39e3d44b12b0f41d2e6ba8343b", + "cell_sha256": "fe9016196e03b264cce22f628f67b778fe1e6a3aeb160ed53effc153b1ff6651", + "cursor": [ + 0, + 0 + ] + }, + "alt": { + "text_sha256": "f6492387ed4760be9851cccd504ac8f6f21624d55dce9671b3be4560ada0b5ac", + "cell_sha256": "0fd582bf6115b38b1dbe57084658f686392c74b4d6b1340f945cfcbca4ff7635", + "cursor": [ + 4, + 5 + ] + }, + "restored": { + "text_sha256": "4279b6605b0fb08728ce75e50e7b24eaf2d75e39e3d44b12b0f41d2e6ba8343b", + "cell_sha256": "fe9016196e03b264cce22f628f67b778fe1e6a3aeb160ed53effc153b1ff6651", + "cursor": [ + 0, + 0 + ] + }, + "final": { + "text_sha256": "605961db2489fcac706c6b5fc52a68f56acc558ac097cea21f9dd874400be438", + "cell_sha256": "00896cdf02c29564c82f4b2f7b118e6c1ac5cb8a0b8f8f5a0d91be3556a94421", + "cursor": [ + 6, + 0 + ] + } + }, + "native_restore_matches_before": true + } + ] + }, + "static_reference": { + "microsoft_document": "https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences#scrolling-region", + "microsoft_terminal_commit": "7ff11ee107c984e236960ec948b450ccbc10187b", + "adapt_dispatch_has_single_margin_field": true, + "windterm_target_sha256": "7b10531dccc7cd614ae559f36f1ef518e0c96335637431b4646354823b2804d0", + "ghidra_version": "12.1.4", + "read_only_no_analysis": true, + "target_executed": false, + "target_unchanged": true, + "decompiled_function_count": 6, + "observation": "DECSTBM 写入所选 screen 的上下边界;1049 路径选择 screen,直接函数未见专门 margin 拷贝。", + "limitation": "screen 状态归属是静态推断;未闭合 callee/间接调用和运行时恢复行为,不宣称 WindTerm 等价。" + }, + "targeted_ctest": { + "candidate": { + "passed": 6, + "total": 6, + "elapsed_seconds": 44.77 + }, + "default": { + "passed": 4, + "total": 4, + "elapsed_seconds": 31.16 + } + }, + "rust_workspace": { + "vendor_unit_passed": 141, + "vendor_doc_passed": 1, + "root_test_targets": 3, + "root_tests_per_target": 2, + "command": "cargo test --manifest-path experiments/candidates/alacritty/Cargo.toml --release --locked --workspace" + }, + "matrices": { + "common": { + "path": "experiments/differential/results/2026-10-26-candidate-common-v6/summary.json", + "runs": 95, + "counts": { + "match": 95 + } + }, + "capabilities": { + "path": "experiments/differential/results/2026-10-26-capabilities-v5/summary.json", + "runs": 25, + "counts": { + "core_unsupported": 15, + "match": 5, + "resize_difference": 5 + } + }, + "fuzz": { + "path": "experiments/differential/results/2026-10-26-fuzz-v4/summary.json", + "runs": 8, + "counts": { + "known_failure": 2, + "match": 6 + } + } + }, + "real_applications": { + "fresh_capture": { + "summary": "experiments/differential/results/2026-10-26-real-applications-v6/summary.json", + "runs": 3, + "counts": {"match": 3}, + "pager_bytes": 360, + "editor_file_verified": true, + "startup_event_verified": true, + "insert_event_verified": true, + "semantic_acceptance": false, + "presented_done": null + }, + "trailing_ech_investigation": { + "issue": 42, + "source": "build/test-artifacts/real-applications/run-xrdvymz2/suite", + "pager_bytes": 366, + "pager_sha256": "bb9dbe77353245a2aad7ef07e12da9a8371cc7ac56d6b47ebc0bfecee35b9958", + "suffix_hex": "0d1b5b373958", + "first_difference": {"path": ".screen.cells[2][0].text", "expected": ")", "actual": ""}, + "termforge2_same_capture": "difference", + "termforge2_binary_sha256": "4a86b398e90fd3b5ffc4dcc0d0496e9664ef24ed3086b58b3d5754a53188e090", + "termforge3_same_capture": "difference", + "termforge3_binary_sha256": "e398d78853a62275aa29d1d7b63121956768e6a42a69d01b7719bfce3593fb44", + "pyte": {"version": "0.8.2", "cursor": [0, 2], + "final_display": ["TF_PAGE_23 ", "-- More (56%", " "]}, + "candidate_final_rows": ["TF_PAGE_23", "--More(56%", ""], + "comparison": "独立 pyte 与候选的归一投影一致;Recovery 留下 ),需按 ECH/wrap 合同裁决,不能自动改判 match。", + "capture_synchronization": "当前仅等待 TF_PAGE_1 即发送 q;完整 More 提示与尾部排干尚未验证。", + "not_attributed_to_margin_patch": true, + "accepted_as_match": false + } + }, + "full_ctest": { + "combined_log_sha256": "c8a2c16bcbaa299bf6d39510cd89427dee134b01f20f7e7da65339019a97072d", + "default": {"passed": 38, "total": 40, "elapsed_seconds": 381.06, + "failed_suites": [{"name": "differential", "issues": [42]}, {"name": "shell", "issues": [36]}]}, + "candidate": {"passed": 41, "total": 42, "elapsed_seconds": 436.85, + "failed_suites": [{"name": "shell", "issues": [36, 38]}]}, + "local_hosted_skips_not_counted_as_passes": 3, + "all_passed": false, + "known_failures_preserved": true + }, + "ci": { + "candidate": { + "run_id": 40261449120, + "conclusion": "success", + "job_ids": [121374525002, 121374525208], + "scope": "Ubuntu 24.04 and Windows Server 2025 fixed candidate" + }, + "main": { + "run_id": 40261449223, + "status": "completed", + "conclusion": "failure", + "updated_at": "2026-10-26T03:01:48Z", + "job_ids": [121374525528, 121374525686], + "ubuntu_conclusion": "success", + "windows": { + "tests_passed": 39, + "tests_total": 40, + "ime_inventory": {"result": "passed", "elapsed_seconds": 25.59}, + "failed_suite": "shell", + "failure_scope": "ui-shells stage 4 and two selection fixture visibility checks", + "issue": 38 + }, + "log_sha256": "33a531f114b29dfe12e052e6af37d11dae2217b0b84e7ad7c8db2b6f1bf9a04e" + } + }, + "source_sha256": { + "experiments/candidates/alacritty/Cargo.lock": "f14e3d50d997f4e20e2be3cdb25838825747aeb8ca50022b5fc6277887c5d8ce", + "experiments/candidates/alacritty/Cargo.toml": "c5f89fda48ab73f30adfdcc4437fb3eeb1779647886d85364d91395bf8beec86", + "experiments/candidates/alacritty/run.py": "fc3b4ff1485eee9eb4525bbc9d4b5911eb4a5836e5e16c04d83a1aaf2847a665", + "experiments/candidates/alacritty/src/ffi.rs": "9d89e5d7e84cbbd0b4a5d0499f9b5eb6b739537c1dab8c154cc0351dec0dd927", + "experiments/candidates/alacritty/src/main.rs": "7fcf080886118e0505aae774b9f41f9ba7458384377a2dd457fc73de57ae21f9", + "experiments/candidates/alacritty/test_candidate.py": "d3b2e136bf12628803ed69d348dbfa6de2464011d00b9c3461c138981f51f22e", + "experiments/candidates/alacritty/vendor/alacritty_terminal/CHANGELOG.md": "97231d9beda30b536650c566169c692fa846be3f7b0405add3c9741f95f3af62", + "experiments/candidates/alacritty/vendor/alacritty_terminal/Cargo.toml": "af09057a0404d3034a0e06103af3d2218cafd43e8e99ec68a2ab1c0e5afe04c0", + "experiments/candidates/alacritty/vendor/alacritty_terminal/LICENSE-APACHE": "8884289e3969c089cd556c53fa1d3f51d86b29e52a8c1d660a55ce8292986910", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/event.rs": "e76c212f36bbfc43ea037a825dbc9c5b1174f5836fe8b69dfd158aadf3fc4048", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/event_loop.rs": "6da2897b76d8c0e7f989daa1463fd6fdeb3469a0a63e0c6ba703a9562663163c", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/mod.rs": "be15d08b03a28a271aa927b7149346a1b5d29e92e9186891bf246fdd2a4c90fc", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/resize.rs": "a9a9a285ed28d75b547f86554174c192f74f2a0fb1b331f43a5d2433bcd5d9d7", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/row.rs": "2f963299e52525828d9d0bf1de758d793661c908eb7c126aa8556a1c9945a83d", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/storage.rs": "27a9b5101833d57423ae34ccef3d82c1a1cf9f51cbc42cc836a6fce79c088348", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/tests.rs": "5f38819134540eeb0dff7047f57f5d06250ce59493839c66ec4cd56e43db62f8", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/index.rs": "46908b68508f4e4ea90313f0536a88c357f2d17ad211b14d44b7f92dfdaa4402", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/lib.rs": "b9cae8ed53a4be8f738111f8727dd86218e66c88df38f32f4904e56651a2bee1", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/selection.rs": "0d1373577602646e84a4ab3448c65ee181987327bdabceff3c827e76dd9bd0d8", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/sync.rs": "4410fbae64e8f80321b3569a1495fb323e3319812c441f9eac54048c65ff93fb", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/cell.rs": "a569c233790530e6502ca031d0d3ed488b5aa2356e872da16ca689d64cb7b62c", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/color.rs": "ad2e33b60f397fe30e5e8f2dde2096bad8f5e256d464ba279cbda4a93cbbe224", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/mod.rs": "fa1382d465c2b35361201d87303573ba4660de96fc01e2091b6075b934e6564c", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/search.rs": "84e04c5f0da21009dbbcfc2f8033e322d6240c8d227028b5ce8a784f1c55ccaf", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/thread.rs": "e2f9fdc5d950bbea6c710c7a974f2998127af6b434344c363afb38e849f86144", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/tty/mod.rs": "8cca30ba29cee2df84603f560a8d26624a48979d96d78f1b6729a470ff34d8a1", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/tty/unix.rs": "2e8d0edf1aea816ac95c1e442d60025ebdd20dacaa99d0fd4f7c7cf78ac369e4", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/tty/windows/blocking.rs": "60121a65df120c4a227520d6672231c9e3c515cb75241241e6b37a0c5bc186ca", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/tty/windows/child.rs": "4782f173c639bff6891f474b068fee64b6e2ad411edabdf496bbfe6554ceb32e", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/tty/windows/conpty.rs": "62abd7e72af144ae1f1c359538e18cceaa97741382efa94e001f397510eef416", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/tty/windows/mod.rs": "b08803dae0f192bc6737aff22992a37368ac505c7133554c495ffcacb4116733", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/vi_mode.rs": "f52c0d3248707c68295880acd7ad96610467110d6813f1172d4ae767365add2e", + "experiments/core/candidate_terminal.cpp": "b50dfbd3a2cf95e2b2cd46979759b2a776ed142c0e2a932ca1cae4fef235f2a4", + "experiments/core/candidate_terminal.hpp": "46d3e6d662a86fd6fdf656c9dfca3c1d04825cc77b57fba6068f3927f95d8459", + "experiments/core/candidate_terminal_test.cpp": "978bf6d91ab03e7f7fdf1c6b841b5727aea765a7284e9b992676860e948b022a", + "experiments/shell/margin_checks.py": "d571ea1cd1d99c41a0ae1d2939549becfca3c6d95dc431a4dcbd0255e4259f1c", + "experiments/shell/run.py": "531ecb08189b490357a4266caa5ade46df66497a1ea2ba70e5d968216a16508d", + "experiments/shell/shell.cpp": "5002016fb220ab7bf6f6142c0d661226fb5984a45da4aaa6aca401970c93a493", + "experiments/shell/test_candidate_tui.py": "f2f990ee3d60aef62f37a524b8c0ed46d752d862d84c669b7e740250c4f5a772", + "experiments/shell/text_client.cpp": "aea8ed527e6e79fbe61446afadbb25d4fa0b7ddef4966930583ac8fa04cadd15" + }, + "screenshots_or_user_desktop_input": false, + "raw_transport_and_proprietary_exports_committed": false, + "full_r2_contract_verified": false, + "issues": { + "27": "OPEN", + "28": "OPEN", + "35": "OPEN", + "41": "OPEN", + "42": "OPEN" + }, + "remaining_gaps": [ + "真实 ConPTY 主屏 margin 恢复由 #41 保留,差异观察不是能力通过。", + "WindTerm 运行时恢复与等价性未证明;静态符号不替代运行时证据。", + "origin/wrap、异常序列及完整真实 resize 交叉尚未验收。", + "More 尾部 CR+CSI79X 的捕获同步与 Recovery 擦除差异由 #42 保留;同捕获旧、新候选及 pyte 对照不把它归因于 margin 修复。", + "#36/#38 的真实回归、#28 的更多 TUI、实际呈现及 #35/R2 出口仍未完成。" + ] +} diff --git a/experiments/shell/results/2026-10-26-native-observation/validation.json b/experiments/shell/results/2026-10-26-native-observation/validation.json new file mode 100644 index 00000000..72a0a6c5 --- /dev/null +++ b/experiments/shell/results/2026-10-26-native-observation/validation.json @@ -0,0 +1,601 @@ +{ + "schema": "termforge-native-observation-handshakes-v1", + "scope": "Native observation and test-driver contracts; no candidate grid or product selection policy change", + "implementation_commits": { + "canonical_row_observation": "24b58f62ea6a82166be286ad54063e0861f1a924", + "actual_echo_before_selection": "948618c6914888fcebca06690370744f632394a7" + }, + "source_sha256_at_948618c": { + "experiments/shell/shell.cpp": "c680d175326348afaf907560d8abf587d126acdef43f583da13190b2f45c9ac3", + "experiments/shell/runtime_observation_test.cpp": "577b5e9368d945c65a8628f55eb882cf93bb4e0da9d06d234d217ec646beb90d", + "experiments/core/candidate_terminal_test.cpp": "c60329aa998f66761716b4532e180d4aa1b29033fc8aa62a930dccfc8a7de88f", + "experiments/core/text_projection.hpp": "093300058775376b6b9b298c3d65cdcc22ab4cd5d3f12b1789afcd7c66405611", + "experiments/core/text_selection.hpp": "7d41d58d434ad3c32ce2f67c043e337277abbbc69e4a7a2b6bdf7e519f9f5462", + "experiments/core/CANDIDATE.md": "a09d9873ec570548014bd5b419878deeb0a3ec6851826af4ecb412abc7c46c8b" + }, + "original_full_validation": { + "source_commit": "6b768b708b015afba59467662f659085078beaca", + "source_tree": "67906a7e0f61b225a0b270c4209ed7cb1351390f", + "candidate_adapter": true, + "ctest_count": 43, + "passed": 40, + "failed": [ + "runtime_observation", + "workflow_fixture", + "shell" + ], + "internal_skips": 8, + "binary_sha256": { + "build\\bin\\Release\\termforge_shell.exe": "0e43f488f3c052d7c57eb522223034e4eb41a557d783f57a36c7d43382e62279", + "build\\bin\\Release\\terminal_trace.exe": "237e1af32e86f565546c2aae3661e32fe08311c50111e734040d9ab20110df79", + "build\\bin\\Release\\candidate_core_test.exe": "e5e279fff28dc67587d849ca101b839200b414ec8dcb3bec6d2c5a7914cd9ba2", + "experiments\\candidates\\alacritty\\target\\release\\termforge-alacritty-trace.exe": "206ad63b2047e556757c742bd9ee5e9172d37f59f6420e9ea120d850e23fa93b" + }, + "evidence_sha256": { + "candidate-junit.xml": "bbbf4c1731e8814b08e9ebf29a31ed93c070446f3a5de425499a0ae49a98fefa", + "ctest.log": "86127894764e566cfe07475029af58a19bda772a31b283b02c320b390175c770", + "tools.log": "9977ce8341e229743e43da9bc7e8f36adde02281bfbc87672050fc58378df546", + "LastTest-complete.log": "b8fa6a7f691b678a23e731821f1f8677e6553c59c1fbbc434f4ffbf4868c2f13" + }, + "subsequent_changes_do_not_rewrite_original_red": true + }, + "canonical_row_observation": { + "contract": "width 0 is a wide-scalar trailing cell; canonical project_line omits its payload", + "adapter_trailing_text_was_not_required_to_be_empty": true, + "original_failure": { + "modes": [ + "close", + "resize-close", + "normal", + "resize" + ], + "stage": 2, + "expected_literal": "\u4e2d\u6587\u00e9", + "raw_cell_concatenation_literal": "\u4e2d \u6587 \u00e9", + "flood_or_etx_stage_reached": false, + "external_watchdog_timed_out": false, + "active_owned_processes_after_each_failure": 0 + }, + "same_source_bracket": [ + { + "stage": "default-before", + "mode": "close", + "accepted": true, + "executable_sha256": "5744a0cfbb2d6cdc42fe4e96bc3befecd0b04830a4c45fecdcc88af0e9035e3f", + "process_exit_code": 0, + "outer_timed_out": false, + "active_owned_processes_after": 0 + }, + { + "stage": "default-before", + "mode": "normal", + "accepted": true, + "executable_sha256": "5744a0cfbb2d6cdc42fe4e96bc3befecd0b04830a4c45fecdcc88af0e9035e3f", + "process_exit_code": 0, + "outer_timed_out": false, + "active_owned_processes_after": 0 + }, + { + "stage": "candidate", + "mode": "close", + "accepted": false, + "executable_sha256": "0e43f488f3c052d7c57eb522223034e4eb41a557d783f57a36c7d43382e62279", + "process_exit_code": 2, + "outer_timed_out": false, + "active_owned_processes_after": 0 + }, + { + "stage": "candidate", + "mode": "normal", + "accepted": false, + "executable_sha256": "0e43f488f3c052d7c57eb522223034e4eb41a557d783f57a36c7d43382e62279", + "process_exit_code": 2, + "outer_timed_out": false, + "active_owned_processes_after": 0 + }, + { + "stage": "default-after", + "mode": "close", + "accepted": true, + "executable_sha256": "5744a0cfbb2d6cdc42fe4e96bc3befecd0b04830a4c45fecdcc88af0e9035e3f", + "process_exit_code": 0, + "outer_timed_out": false, + "active_owned_processes_after": 0 + }, + { + "stage": "default-after", + "mode": "normal", + "accepted": true, + "executable_sha256": "5744a0cfbb2d6cdc42fe4e96bc3befecd0b04830a4c45fecdcc88af0e9035e3f", + "process_exit_code": 0, + "outer_timed_out": false, + "active_owned_processes_after": 0 + } + ], + "fix": "Native line_is, has and live_contains_line reuse project_line; original literals, loads and deadlines remain", + "independent_literal_projection": { + "input": "A\u4e2de\u0301\ud83d\ude00", + "expected": "A\u4e2de\u0301\ud83d\ude00", + "candidate_core_test_passed": true + }, + "post_fix_original_four_unittests": { + "unittests": 4, + "fresh_scenarios": 12, + "status": "passed", + "shell_executable_sha256": "81ec43940d087db7abeaa233c47fc61d4cf628f5b601ef42871d42c907375a28", + "samples": [ + { + "mode": "close", + "status": "passed", + "exit_code": 0, + "native_cleanup_complete": true, + "native_active_children_after": 0, + "command_verified": true, + "unicode_verified": true, + "backspace_verified": true, + "arrows_verified": true, + "ctrl_c_verified": false, + "outer_job_active_after": 0 + }, + { + "mode": "close", + "status": "passed", + "exit_code": 0, + "native_cleanup_complete": true, + "native_active_children_after": 0, + "command_verified": true, + "unicode_verified": true, + "backspace_verified": true, + "arrows_verified": true, + "ctrl_c_verified": false, + "outer_job_active_after": 0 + }, + { + "mode": "close", + "status": "passed", + "exit_code": 0, + "native_cleanup_complete": true, + "native_active_children_after": 0, + "command_verified": true, + "unicode_verified": true, + "backspace_verified": true, + "arrows_verified": true, + "ctrl_c_verified": false, + "outer_job_active_after": 0 + }, + { + "mode": "normal", + "status": "passed", + "exit_code": 0, + "native_cleanup_complete": true, + "native_active_children_after": 0, + "command_verified": true, + "unicode_verified": true, + "backspace_verified": true, + "arrows_verified": true, + "ctrl_c_verified": true, + "outer_job_active_after": 0 + }, + { + "mode": "normal", + "status": "passed", + "exit_code": 0, + "native_cleanup_complete": true, + "native_active_children_after": 0, + "command_verified": true, + "unicode_verified": true, + "backspace_verified": true, + "arrows_verified": true, + "ctrl_c_verified": true, + "outer_job_active_after": 0 + }, + { + "mode": "normal", + "status": "passed", + "exit_code": 0, + "native_cleanup_complete": true, + "native_active_children_after": 0, + "command_verified": true, + "unicode_verified": true, + "backspace_verified": true, + "arrows_verified": true, + "ctrl_c_verified": true, + "outer_job_active_after": 0 + }, + { + "mode": "resize", + "status": "passed", + "exit_code": 0, + "native_cleanup_complete": true, + "native_active_children_after": 0, + "command_verified": true, + "unicode_verified": true, + "backspace_verified": true, + "arrows_verified": true, + "ctrl_c_verified": true, + "outer_job_active_after": 0 + }, + { + "mode": "resize", + "status": "passed", + "exit_code": 0, + "native_cleanup_complete": true, + "native_active_children_after": 0, + "command_verified": true, + "unicode_verified": true, + "backspace_verified": true, + "arrows_verified": true, + "ctrl_c_verified": true, + "outer_job_active_after": 0 + }, + { + "mode": "resize", + "status": "passed", + "exit_code": 0, + "native_cleanup_complete": true, + "native_active_children_after": 0, + "command_verified": true, + "unicode_verified": true, + "backspace_verified": true, + "arrows_verified": true, + "ctrl_c_verified": true, + "outer_job_active_after": 0 + }, + { + "mode": "resize-close", + "status": "passed", + "exit_code": 0, + "native_cleanup_complete": true, + "native_active_children_after": 0, + "command_verified": true, + "unicode_verified": true, + "backspace_verified": true, + "arrows_verified": true, + "ctrl_c_verified": false, + "outer_job_active_after": 0 + }, + { + "mode": "resize-close", + "status": "passed", + "exit_code": 0, + "native_cleanup_complete": true, + "native_active_children_after": 0, + "command_verified": true, + "unicode_verified": true, + "backspace_verified": true, + "arrows_verified": true, + "ctrl_c_verified": false, + "outer_job_active_after": 0 + }, + { + "mode": "resize-close", + "status": "passed", + "exit_code": 0, + "native_cleanup_complete": true, + "native_active_children_after": 0, + "command_verified": true, + "unicode_verified": true, + "backspace_verified": true, + "arrows_verified": true, + "ctrl_c_verified": false, + "outer_job_active_after": 0 + } + ], + "log_sha256": "a65e0314232edc2647977238f19fa2163383c860650ee5abef3544c27ea27d48" + } + }, + "runtime_echo_handshake": { + "original_failed_field_values_were_not_recorded": true, + "original_red_summary_sha256": "749b8f9cc434e45015de1e6cf96551e1a5bf19502589cf3cf99076cac3e448ce", + "first_diagnostic_batch": { + "source_base": "24b58f62ea6a82166be286ad54063e0861f1a924", + "source_overrides_sha256": { + "experiments/shell/shell.cpp": "c680d175326348afaf907560d8abf587d126acdef43f583da13190b2f45c9ac3", + "experiments/shell/runtime_observation_test.cpp": "27a8357e1063d2a6a1560b716259d54d11fe0b1caae8584068b021f9c9508f50", + "experiments/core/candidate_terminal_test.cpp": "c60329aa998f66761716b4532e180d4aa1b29033fc8aa62a930dccfc8a7de88f" + }, + "binary_sha256": "9f2e1f57949bcba22f50c25f79527546c02d8d35eb3c096757c6c3c8e1f8a853", + "fresh_runs": 4, + "exit_counts": { + "0": 3, + "1": 1 + }, + "runs": [ + { + "run": 0, + "exit_code": 0, + "log_sha256": "593e45292328e69c2d09d47ae94d0fe99c3f20002c7a90a9ebff2e63705c2e17", + "echo_stages": [] + }, + { + "run": 1, + "exit_code": 0, + "log_sha256": "54a5bc1470374a71bdafdcb0ad0e93aebfc60ba469b3e7343e595cfb75e3a279", + "echo_stages": [] + }, + { + "run": 2, + "exit_code": 0, + "log_sha256": "105cb436bd5eec456913c634048cf21bee601ea743db1373779278317b6bd205", + "echo_stages": [] + }, + { + "run": 3, + "exit_code": 1, + "log_sha256": "d178e5984eef51547f50d6c680befeb9a2088b6930835c569e55fe23f5e9a2dc", + "echo_stages": [] + } + ] + }, + "stage_instrumented_control_before_barrier": { + "source_base": "24b58f62ea6a82166be286ad54063e0861f1a924", + "source_overrides_sha256": { + "experiments/shell/runtime_observation_test.cpp": "ea5864846f81c39897c6d10c4d774401bca04f5c39f39677704ffbe768552f5b", + "experiments/shell/shell.cpp": "c680d175326348afaf907560d8abf587d126acdef43f583da13190b2f45c9ac3" + }, + "binary_sha256": "c28d3d497d8f69c68c6bb280c4c5081fb54fd0a5335a67515c2db21b46faa309", + "fresh_runs": 5, + "exit_counts": { + "0": 4, + "1": 1 + }, + "runs": [ + { + "run": 0, + "exit_code": 0, + "log_sha256": "fa682ac703c8098012ec72a08ce81cbecbd917ba63949da790bc5f170ee7c0b2", + "echo_stages": [] + }, + { + "run": 1, + "exit_code": 0, + "log_sha256": "ad895fe2b9285b640b7a39e65177ade407cc681f5da082d2f87cc429fbfa31c0", + "echo_stages": [] + }, + { + "run": 2, + "exit_code": 0, + "log_sha256": "9cb8ad7b9be521124a51cd055080b3a358a73cbfb33f1596d0c729991dff52d4", + "echo_stages": [] + }, + { + "run": 3, + "exit_code": 0, + "log_sha256": "9d275bbc90269e8e4bcc73d0640869eb5e92b76d1f27afc94f6f675b05cc7523", + "echo_stages": [] + }, + { + "run": 4, + "exit_code": 1, + "log_sha256": "a2e5e00b276940d7f331ffb7877fae8e2721c3803e06b20772aa4ccab8f843ce", + "echo_stages": [ + { + "echo_visible_before_selection": false, + "selected_before_find": true, + "echo_visible_at_failed_tasks": true, + "consumed_before_selection": 403, + "consumed_at_failed_tasks": 415 + } + ] + } + ] + }, + "observed_failure": { + "echo_stages": { + "echo_visible_before_selection": false, + "selected_before_find": true, + "echo_visible_at_failed_tasks": true, + "consumed_before_selection": 403, + "consumed_at_failed_tasks": 415 + }, + "selection_at_tasks": null, + "input_queued_bytes": 0, + "input_in_flight_bytes": 0, + "last_paste_state": "completed", + "last_paste_sent_bytes": 12, + "search": { + "state": "complete", + "result_count": 2, + "find_query_bytes": 19, + "editor_present": true + }, + "pending_prepared_bytes": 22, + "pending_source_units": 23, + "pending_prepared_capacity_bytes": 79, + "paste": null + }, + "finding": "Input writing had completed. The 12-byte screen echo arrived after selecting; the previously present selection range was then absent.", + "causal_interpretation": "The fixture selected before its own echo. The existing version-bound selection is permitted to invalidate when the selected row changes.", + "interpretation_uses_stage_order_and_selection_contract": true, + "fixture_fix": "The existing five-second wait now requires both transfer completion and the complete live TF>RUNTIME_SENT line before selecting.", + "wait_budget_ms": 5000, + "owned_child_watchdog_ms": 15000, + "original_compound_assertion_unchanged": true, + "original_input_unchanged": true, + "fixed_delay_added": false, + "selection_product_policy_changed": false, + "with_barrier": { + "source_base": "24b58f62ea6a82166be286ad54063e0861f1a924", + "source_overrides_sha256": { + "experiments/shell/runtime_observation_test.cpp": "577b5e9368d945c65a8628f55eb882cf93bb4e0da9d06d234d217ec646beb90d", + "experiments/shell/shell.cpp": "c680d175326348afaf907560d8abf587d126acdef43f583da13190b2f45c9ac3" + }, + "binary_sha256": "06613071928baa678c2f43eac58780e88ca74ba08cf3fbdbf171d4eeeb8ef8b2", + "fresh_runs": 5, + "exit_counts": { + "0": 5 + }, + "implementation_commit": "948618c6914888fcebca06690370744f632394a7", + "successful_owned_window_lifecycles": 10, + "runs": [ + { + "run": 0, + "exit_code": 0, + "log_sha256": "0b696ec8a5ba71ba08fa6065aab76fd61038ed40ee3b842f8d632cc6c33ac16f", + "echo_stages": [ + { + "echo_visible_before_selection": true, + "selected_before_find": true, + "echo_visible_at_tasks": true, + "consumed_before_selection": 415, + "consumed_at_tasks": 415 + }, + { + "echo_visible_before_selection": true, + "selected_before_find": true, + "echo_visible_at_tasks": true, + "consumed_before_selection": 415, + "consumed_at_tasks": 415 + } + ], + "successful_owned_window_lifecycles": 2, + "owned_job_zero_and_desktop_deleted_verified": true + }, + { + "run": 1, + "exit_code": 0, + "log_sha256": "24e57feffa098d993ef55e5bd35103d387ae997abd8bfde9b7894756e952898e", + "echo_stages": [ + { + "echo_visible_before_selection": true, + "selected_before_find": true, + "echo_visible_at_tasks": true, + "consumed_before_selection": 415, + "consumed_at_tasks": 415 + }, + { + "echo_visible_before_selection": true, + "selected_before_find": true, + "echo_visible_at_tasks": true, + "consumed_before_selection": 415, + "consumed_at_tasks": 415 + } + ], + "successful_owned_window_lifecycles": 2, + "owned_job_zero_and_desktop_deleted_verified": true + }, + { + "run": 2, + "exit_code": 0, + "log_sha256": "dc015ea02be1b18b56ea32de552ca6e250998367e6405f30c78937216aad2db1", + "echo_stages": [ + { + "echo_visible_before_selection": true, + "selected_before_find": true, + "echo_visible_at_tasks": true, + "consumed_before_selection": 415, + "consumed_at_tasks": 415 + }, + { + "echo_visible_before_selection": true, + "selected_before_find": true, + "echo_visible_at_tasks": true, + "consumed_before_selection": 415, + "consumed_at_tasks": 415 + } + ], + "successful_owned_window_lifecycles": 2, + "owned_job_zero_and_desktop_deleted_verified": true + }, + { + "run": 3, + "exit_code": 0, + "log_sha256": "97a917c4c25ebdb1e89c4db6e128e8a9836311d00ad99942265d59b1b020fe0a", + "echo_stages": [ + { + "echo_visible_before_selection": true, + "selected_before_find": true, + "echo_visible_at_tasks": true, + "consumed_before_selection": 415, + "consumed_at_tasks": 415 + }, + { + "echo_visible_before_selection": true, + "selected_before_find": true, + "echo_visible_at_tasks": true, + "consumed_before_selection": 415, + "consumed_at_tasks": 415 + } + ], + "successful_owned_window_lifecycles": 2, + "owned_job_zero_and_desktop_deleted_verified": true + }, + { + "run": 4, + "exit_code": 0, + "log_sha256": "29d09f7e5201c0a0c8661c23701446e2a30c26a55d920675a68551f115b7945b", + "echo_stages": [ + { + "echo_visible_before_selection": true, + "selected_before_find": true, + "echo_visible_at_tasks": true, + "consumed_before_selection": 548, + "consumed_at_tasks": 548 + }, + { + "echo_visible_before_selection": true, + "selected_before_find": true, + "echo_visible_at_tasks": true, + "consumed_before_selection": 415, + "consumed_at_tasks": 415 + } + ], + "successful_owned_window_lifecycles": 2, + "owned_job_zero_and_desktop_deleted_verified": true + } + ] + }, + "same_no_barrier_binary_counterfactual_after": { + "source_base": "24b58f62ea6a82166be286ad54063e0861f1a924", + "source_overrides_sha256": { + "experiments/shell/runtime_observation_test.cpp": "ea5864846f81c39897c6d10c4d774401bca04f5c39f39677704ffbe768552f5b", + "experiments/shell/shell.cpp": "c680d175326348afaf907560d8abf587d126acdef43f583da13190b2f45c9ac3" + }, + "binary_sha256": "c28d3d497d8f69c68c6bb280c4c5081fb54fd0a5335a67515c2db21b46faa309", + "fresh_runs": 5, + "exit_counts": { + "0": 5 + }, + "runs": [ + { + "run": 0, + "exit_code": 0, + "log_sha256": "37db8581624dd1c1e0aae7b243e423c4bfca5f06867fd4cf514267ef4bc4d62f", + "echo_stages": [] + }, + { + "run": 1, + "exit_code": 0, + "log_sha256": "0269f6a37d53ed3a8baf22206f33caf63a14634bef1d10ad163fce683c783c08", + "echo_stages": [] + }, + { + "run": 2, + "exit_code": 0, + "log_sha256": "0ce661270e7f9ca2d688a8cd0415acd62111cfeaefd105a8118f4d7e7d329419", + "echo_stages": [] + }, + { + "run": 3, + "exit_code": 0, + "log_sha256": "bc7b6eb16d5ee543d886cc2d7bd233628b176635992941a10be929b021636efb", + "echo_stages": [] + }, + { + "run": 4, + "exit_code": 0, + "log_sha256": "596e09986deab33c19e64334606b0c38c00409fcc46fb38ad64efa4de99898ad", + "echo_stages": [] + } + ] + }, + "intermittency_limit": "The no-barrier control failed once in its first five-run batch and passed all five later runs; no failure-rate or performance claim follows." + }, + "limits": { + "final_integrated_full_ctest_rerun_included_here": false, + "complete_candidate_parity_established": false, + "windterm_runtime_equivalence_verified": false, + "presentation_completion_measured": false, + "controller_cmd_return_race_resolved": false + } +} diff --git a/experiments/shell/results/2026-10-26-receiver-stop/validation.json b/experiments/shell/results/2026-10-26-receiver-stop/validation.json new file mode 100644 index 00000000..8d7ece16 --- /dev/null +++ b/experiments/shell/results/2026-10-26-receiver-stop/validation.json @@ -0,0 +1,141 @@ +{ + "schema": "termforge-receiver-stop-oracle-validation-v1", + "issue": 46, + "base_commit": "c6461356ed5930bc40e0606b5c0440fc9497ef38", + "scope": "Python expected rejected-order oracle only; native receiver, workloads, deadlines and normal-case gates unchanged", + "original_red": { + "source_commit": "0e1fb58518a51bc71c756eee8ab344ec9f0d2bcc", + "test": "workflow_fixture/test_two_fresh_owned_data_only_rounds", + "round": 0, + "case": "F2-order", + "generation": 5, + "python_reason": "receiver read join", + "native_host_exit": 0, + "native_host_cases_passed": 5, + "client_expected_exit": 2, + "source_bytes": 0, + "capture_bytes": 0, + "transport_bytes": 231, + "transport_sha256": "4a23045f3071059d0bfb4664d799dc40ee1523b4986f403c7affae2de63c254a", + "fixture": "experiments/shell/fixtures/rejected-order-before-read.json", + "fixture_sha256": "91263594574f86cf5869893e918741bf7414c0e209a3c9868b9fa7bbf1df1b72", + "outer_job_active_after": 0, + "inner_job_active_after": 0, + "process_reaped": true, + "timed_out": false, + "forced_cleanup": false, + "full_run_remains_failed": true, + "ctest_log_sha256": "b43276db04eb89cfee755bf836dc5289a07aa544645092728c826e9e47ef6e58" + }, + "receiver_contract": { + "observed_state": "stopped and joined before first ReadConsoleW entry", + "all_common_cleanup_and_actual_totals_checks_retained": true, + "zero_read_exception_context": "validate_rejected_order only, after empty capture/source and protocol rejection checks", + "required_read_counts": [ + 0, + 0 + ], + "required_cancel_calls_min": 1, + "required_cancel_successes": 0, + "required_last_cancel_error": 1168, + "last_read_fields": "success=false, count=0, error=0, zero_read_accepted=false; no last read occurred" + }, + "binary_reuse": { + "native_source_commit": "0e1fb58518a51bc71c756eee8ab344ec9f0d2bcc", + "native_source_blobs_identical_at_base": true, + "workflow_fixture_test_sha256": "dca14f0c1106370bb0a84baa1803c3c6c382054bba4dc653cdc0ee2a9017a444", + "shell_text_client_sha256": "5004f73faa2668ad3ce77a32e83fc80b525c6dac46bb78868a00ded7bdf251e7", + "native_source_sha256": { + "experiments/shell/raw_console_receiver.hpp": "9ea35252c33a938a7dc76d4ea7c6db72d04a3fd010c1ce6b0289c9747e50afcc", + "experiments/shell/workflow_fixture.hpp": "332d73699320a6f503fc212417a4d3820b82cc2d3feb3d1fffe3930660d57453", + "experiments/shell/workflow_fixture_test.cpp": "29d35fd143ac215b6746f810e1915be17ad6c407623d0768121f1799e38c553c", + "experiments/shell/workflow_controller.hpp": "61005e442aa1729759dd1c1b8f0919826cb2e71f225fae53f0ea0c9cc28d38eb", + "experiments/shell/text_client.cpp": "aea8ed527e6e79fbe61446afadbb25d4fa0b7ddef4966930583ac8fa04cadd15", + "experiments/conpty/session.hpp": "f143a858df22302a2bc4a794263ff7769f4d0cfd9537307d916feb8ad638c6b6" + } + }, + "pure_red_green": { + "command": "python -m unittest experiments.shell.test_workflow.WorkflowOracleTest -v", + "before": { + "tests": 12, + "errors": 1, + "reason": "receiver read join", + "log_sha256": "50462c2e1853e217289a318c4476ce08bd80f125e8f269b45a460d3ad8969666" + }, + "after": { + "tests": 12, + "errors": 0, + "log_sha256": "a84d432b76e71c075af3ecddeb9bb98c1369dec39f7523daf799a9e6705791e1" + }, + "negative_cases": "22 receiver field corruptions, nonempty raw/source, normal A/B zero-read states remain rejected" + }, + "same_bytes_counterfactual": { + "schema": "termforge-receiver-oracle-counterfactual-v1", + "original": { + "old": { + "accepted": false, + "reason": "receiver read join" + }, + "new": { + "accepted": true, + "reason": null + } + }, + "fresh": [ + { + "round": 1, + "reads_entered": 0, + "reads_completed": 0, + "cancel_successes": 0, + "last_cancel_error": 1168, + "old_oracle": { + "accepted": false, + "reason": "receiver read join" + }, + "new_oracle": { + "accepted": true, + "reason": null + } + }, + { + "round": 0, + "reads_entered": 1, + "reads_completed": 1, + "cancel_successes": 1, + "last_cancel_error": 0, + "old_oracle": { + "accepted": true, + "reason": null + }, + "new_oracle": { + "accepted": true, + "reason": null + } + } + ] + }, + "fresh_workflow": { + "command": "python -m unittest experiments.shell.test_workflow -v", + "tests": 14, + "passed": 14, + "skipped": 0, + "elapsed_seconds": 24.263, + "host_rounds": 4, + "native_cases": 16, + "all_outer_jobs_active_after": 0, + "all_inner_jobs_active_after": 0, + "all_processes_reaped": true, + "timeouts": 0, + "forced_cleanups": 0, + "log_sha256": "94e3020fda71e46077b32c8219bd68c4ba7d30e9c3ca261fd23a3132808cec55" + }, + "limits": { + "candidate_core_cause": null, + "same_cause_as_issue44": null, + "shared_cause_with_issue44_established": false, + "issue44_cause_remains_unknown": true, + "old_full_result_replaced": false, + "new_full_suite_run": false, + "performance_or_target_parity_claim": false + } +} diff --git a/experiments/shell/results/2026-10-26-workspace-transport-oracle/validation.json b/experiments/shell/results/2026-10-26-workspace-transport-oracle/validation.json new file mode 100644 index 00000000..9191db6c --- /dev/null +++ b/experiments/shell/results/2026-10-26-workspace-transport-oracle/validation.json @@ -0,0 +1,141 @@ +{ + "schema": "termforge-workspace-transport-oracle-v1", + "date": "2026-10-26", + "scope": "Test completion endpoint correction only; no product restore fix or complete R2/candidate acceptance claim.", + "source_commit": "e3633cc4e9aca1d0723fa49a484d9f4d56664fce", + "source_hash_policy": "SHA-256 of Git blob bytes (canonical repository content)", + "source_sha256": { + "CMakeLists.txt": "3f392fe061ba73084f51597b6898778f8ea89ea69cbe083cfbd9d28ab5e763fd", + "experiments/shell/workspace_persistence_scenario.hpp": "7f70d755549718d0fddb94b43cce12f3955727fcc51599a8bb124bf3e59ec263", + "experiments/shell/transport_completion.hpp": "acb72d8d85b0118ea6e2e5d44eda1154937bc616b867fdcc4fb7b89b67bcb9fe", + "experiments/shell/transport_completion_test.cpp": "2a5a35dfcd5034d4258cfeef9cbe58c736ccaa354c5a6f9580a11e418cd27780" + }, + "preserved_contract": { + "observation_cap_bytes": 8192, + "native_deadline_ms": 30000, + "cwd_version_return_prompt_geometry_resize_checks": "unchanged" + }, + "private_inputs": [ + { + "id": "osc-title-shadow", + "failing_oracle": "baseline rfind marker and suffix gate", + "file_bytes": 1735, + "file_sha256": "af1a2c513b3891ce75acccca6ec2125ab46ab2fc2b312d0a05fd8cd40fa425bc", + "observed_streams": { + "cmd": {"bytes": 214, "sha256": "5e9fa7ea30608341b726aa7b1662fa898fe8a728e88f7b2099c08494d61e163e"}, + "powershell": {"bytes": 343, "sha256": "fc2961b0e4bbf2067bd2ff16a661635ccbc321840a902cf513862229c6e6456b"}, + "bottom": {"bytes": 168, "sha256": "bafd1b1ab688a8c4365363f5565b35cffcced3fd6721d72517379cd8d3f751ee"} + }, + "original_native_result": "stage-1 timeout (unchanged)", + "legacy_rfind_predicates": { + "cmd_dir": false, + "powershell_dir": true, + "powershell_major": true, + "bottom": true + }, + "repaired_native_helper_replay": [true, true, true, true] + }, + { + "id": "lf-only-cup-counterexample", + "failing_oracle": "intermediate OSC/CSI strip with LF-only records", + "file_bytes": 1440, + "file_sha256": "339ca19fc01a0a188325fd6b4b594538cfdf6535e9b1e17ca398b6fa4472c8a5", + "observed_streams": { + "cmd": {"bytes": 96, "sha256": "e1f91c63bb069864e818cc371e93965f76c0440e6c3b8d2970f681ce35d91158"}, + "powershell": {"bytes": 343, "sha256": "fc2961b0e4bbf2067bd2ff16a661635ccbc321840a902cf513862229c6e6456b"}, + "bottom": {"bytes": 48, "sha256": "25f9f8759ea0abe64e2d9c4ca160d5b95e359ee035f1641ddc5cdfc30a64f334"} + }, + "original_native_result": "stage-1 timeout (unchanged)", + "legacy_rfind_predicates": { + "cmd_dir": true, + "powershell_dir": true, + "powershell_major": true, + "bottom": true + }, + "repaired_native_helper_replay": [true, true, true, true] + } + ], + "branch_explanation": { + "osc": "A later complete OSC title repeats the earlier complete output marker and makes legacy rfind false.", + "cup": "A fresh capture emits a complete marker followed by column-one CUP and a prompt. CUP is a record boundary; initial cursor row and final visible row are not inferred." + }, + "pure_regression": { + "assertions": 32, + "positive": 6, + "negative": 26, + "default": "passed", + "candidate": "passed", + "pre_fix_red": "OSC BEL title must not shadow actual output", + "lf_only_red": "ConPTY column-one CUP frames an exact output line", + "negative_scope": [ + "echo and CUP echo redraw", + "OSC BEL/ST or DCS payload", + "partial marker across CUP", + "SGR or non-column-one CUP cannot complete marker", + "incomplete or malformed controls", + "missing, stale or substring prompt" + ] + }, + "fresh_original_method": [ + { + "build": "default", + "method": "ShellTests.test_workspace_v2_multiple_shells_split_and_geometry_restore", + "rounds": 2, + "native_saves": 2, + "native_fresh_restores": 2, + "native_clamped_restores": 2, + "result": "passed", + "elapsed_seconds": 15.718, + "all_native_samples": { + "exit_code": 0, + "sessions_closed": 3, + "active_children_after": 0, + "settings_save_failed": false, + "job_after_active": 0, + "job_forced_terminations": 0, + "desktop_gone": true + }, + "sample_sha256": { + "workspace-v2-clamped-0": "7be75dc10c46ef2b60b4342a9a7a7c68b9abc7c6131b459b48222c0ee64ae75c", + "workspace-v2-clamped-1": "59cbc9d44440267297433d7451dc979acc55339733d7ea0c7a07d6b6167b8f4f", + "workspace-v2-restore-0": "69a5d716ae376ae4eddbdd85803c5ea2c6956355fddad3e1458d744bfadc017c", + "workspace-v2-restore-1": "05b1c718e3f8f600a36be5e111b5ccb154d7d32441942b98eef6f9a29adca911", + "workspace-v2-save-0": "99b39d103548bd56145d31da0c8f3175334338d9af3ba76b0313bc2323444ba1", + "workspace-v2-save-1": "f98dbb425e5f7cae035030050549843c4f7f700e0e3d1ef62fdc1dac04db5373" + } + }, + { + "build": "candidate", + "method": "ShellTests.test_workspace_v2_multiple_shells_split_and_geometry_restore", + "rounds": 2, + "native_saves": 2, + "native_fresh_restores": 2, + "native_clamped_restores": 2, + "result": "passed", + "elapsed_seconds": 15.571, + "all_native_samples": { + "exit_code": 0, + "sessions_closed": 3, + "active_children_after": 0, + "settings_save_failed": false, + "job_after_active": 0, + "job_forced_terminations": 0, + "desktop_gone": true + }, + "sample_sha256": { + "workspace-v2-clamped-0": "9b475da5bd750836ce97fe564d914c8db257cf52dde284f85700fb4695cb654a", + "workspace-v2-clamped-1": "43a9d2fdbb015b90e4974d378c6f1e695d838ea147c65a591df6828ece496c02", + "workspace-v2-restore-0": "c4c90a76ac27e65142e0185335582d90bff77edf6895891ef2a83fd4c47b9b51", + "workspace-v2-restore-1": "da563eb1da603d2bd594549aef5c6a2e52dee60d6733bbcfa84d038af8f52e37", + "workspace-v2-save-0": "28f14503548006c80a2065470aae50b325cee69d60082b330a97e52852b9bfc8", + "workspace-v2-save-1": "f7742ff19340a911de3c7e10e63c2cef1e43be9ce2c2076ab804835f02eb2046" + } + } + ], + "limitations": [ + "Original failed runs remain failed; helper replay does not rewrite historical observations.", + "Raw transport and original failure JSON remain private.", + "Private non-input desktops and owned Jobs only; no user system clipboard or IME.", + "Merged-tree CI and complete candidate regression are separate checks." + ] +} diff --git a/experiments/shell/runtime_observation_test.cpp b/experiments/shell/runtime_observation_test.cpp index 681e795b..c5ddaa00 100644 --- a/experiments/shell/runtime_observation_test.cpp +++ b/experiments/shell/runtime_observation_test.cpp @@ -58,23 +58,38 @@ static int runtime_window_case() { live.published_revision > initial.published_revision && !initial.window_exists, "independent live values survive subsequent UI output"); window.synthetic_paste(u"RUNTIME_SENT"); - runtime_wait(window, [&] { return !window.paste_busy(); }, "runtime synthetic transfer completes deadline"); + runtime_wait(window, [&] { + return !window.paste_busy() && window.live_contains_line("TF>RUNTIME_SENT"); + }, "runtime synthetic transfer and actual echo complete deadline"); const auto sent = window.runtime_snapshot(); require(!sent.paste && sent.last_terminal_paste && sent.last_terminal_paste->state == tf_shell::PasteState::completed && sent.last_terminal_paste->source_units == size_t{12} && sent.last_terminal_paste->total_bytes == 12 && sent.last_terminal_paste->sent_bytes == 12 && !sent.last_terminal_paste->in_flight_bytes && sent.last_terminal_paste->retained_capacity_bytes <= 32, "terminal transfer counters survive product shared_ptr reset"); - window.select_all_text(); window.request_find(); + const bool echo_before_selection = window.live_contains_line("TF>RUNTIME_SENT"); + const auto consumed_before_selection = window.runtime_snapshot().consumed_bytes; + window.select_all_text(); + const bool selected_before_find = window.runtime_snapshot().selection.range.has_value(); + window.request_find(); require(SetWindowTextW(window.find_edit_handle(), L"TF_RUNTIME_OBSERVED") != FALSE, "runtime find literal query setup"); runtime_wait(window, [&] { return window.search_status() == termforge::SearchStatus::complete && window.current_find_range().has_value(); }, "runtime actual find finishes deadline"); window.synthetic_paste(u"TF_RUNTIME_PROMPT\r\nNEXT"); const auto tasks = window.runtime_snapshot(); - require(tasks.selection.range && tasks.search.editor_present && tasks.search.result_count >= 1 && + const bool tasks_valid = tasks.selection.range && tasks.search.editor_present && tasks.search.result_count >= 1 && tasks.search.find_query_bytes == 19 && tasks.pending_prepared_paste && tasks.paste_prompt_present && tasks.pending_prepared_bytes == 22 && tasks.pending_source_units == size_t{23} && !tasks.paste && - tasks.pending_prepared_capacity_bytes >= tasks.pending_prepared_bytes, + tasks.pending_prepared_capacity_bytes >= tasks.pending_prepared_bytes; + std::cerr << "runtime_echo_stage={\"echo_visible_before_selection\":" << (echo_before_selection?"true":"false") + << ",\"selected_before_find\":" << (selected_before_find?"true":"false") + << ",\"echo_visible_at_tasks\":" << (window.live_contains_line("TF>RUNTIME_SENT")?"true":"false") + << ",\"consumed_before_selection\":" << consumed_before_selection + << ",\"consumed_at_tasks\":" << tasks.consumed_bytes << "}\n"; + if (!tasks_valid) { + std::cerr << "runtime_tasks=" << tf_shell::runtime_snapshot_json(tasks) << '\n'; + } + require(tasks_valid, "actual search selection and unsent confirmation payload are observed without copying text"); window.close(); const auto before_close = window.runtime_before_close(); diff --git a/experiments/shell/selection_checks.py b/experiments/shell/selection_checks.py index 4747f276..a54abba1 100644 --- a/experiments/shell/selection_checks.py +++ b/experiments/shell/selection_checks.py @@ -78,6 +78,46 @@ def _validate_copy_popup(data: dict) -> bool: return True +def _copy_error_frame_dimensions(value: dict) -> tuple[int, int]: + schema = value.get("schema") + if schema == "termforge-product-copy-contention-v1": + if "error_frame_geometry" in value: + raise ValueError("legacy Copy contention cannot claim newly observed client geometry") + return 1360, 860 + if schema != "termforge-product-copy-contention-v2": + raise ValueError("occupied Copy frame requires a known contention identity") + geometry = value.get("error_frame_geometry") + if (not isinstance(geometry, dict) or + geometry.get("schema") != "termforge-copy-error-client-geometry-v1" or + geometry.get("coordinate_space") != "physical-client-pixels"): + raise ValueError("occupied Copy frame lacks capture-time native client geometry") + for field, size in (("client_rect", 4), ("client_rect_after", 4), ("physical_size", 2), + ("bitmap_size", 2), ("bitmap_format", 2)): + values = geometry.get(field) + if not isinstance(values, list) or len(values) != size or any(type(n) is not int for n in values): + raise ValueError("occupied Copy native geometry observation is missing or malformed") + left, top, width, height = geometry["client_rect"] + # Workspace's native maximum tracking bounds are 6020 x 5300 pixels. + if (left != 0 or top != 0 or not 0 < width <= 6020 or not 0 < height <= 5300 or + geometry["client_rect_after"] != [0, 0, width, height] or + geometry["physical_size"] != [width, height] or geometry["bitmap_size"] != [width, height] or + geometry["bitmap_format"] != [1, 32] or type(geometry.get("bitmap_stride_bytes")) is not int or + geometry["bitmap_stride_bytes"] != width * 4): + raise ValueError("occupied Copy client, physical surface and actual DIB geometry disagree") + return width, height + + +def _validate_copy_error_frame(value: dict, frame: bytes) -> tuple[int, int]: + width, height = _copy_error_frame_dimensions(value) + size = 54 + width * height * 4 + if type(frame) is not bytes or len(frame) != size or frame[:2] != b"BM": + raise ValueError("actual occupied Copy visible error frame is missing or truncated") + expected = (b"BM", size, 0, 0, 54, 40, width, -height, 1, 32, 0, 0, 0, 0, 0, 0) + if struct.unpack_from("<2sIHHIIiiHHIIiiII", frame) != expected: + raise ValueError("actual occupied Copy error frame geometry or native format is wrong") + return width, height + + def _validate_copy_contention(data: dict) -> dict | None: if data.get("schema") in ("termforge-window-selection-v3", "termforge-window-selection-v4"): if "copy_contention" in data: @@ -85,12 +125,14 @@ def _validate_copy_contention(data: dict) -> dict | None: return None value = data.get("copy_contention") if (data.get("schema") != "termforge-window-selection-v5" or data.get("system_clipboard") is not True or - not isinstance(value, dict) or value.get("schema") != "termforge-product-copy-contention-v1" or + not isinstance(value, dict) or value.get("schema") not in + ("termforge-product-copy-contention-v1", "termforge-product-copy-contention-v2") or value.get("entry") != "queued-Ctrl-Shift-C" or value.get("target") != "main" or any(value.get(field) is not True for field in ("owner_occupied_before", "owner_occupied_at_error", "error_visible")) or type(value.get("native_error")) is not int or not 1 <= value["native_error"] <= 0xffffffff or value.get("failure_step") != "OpenClipboard" or value.get("error_frame") != "selection-copy-occupied-error.bmp"): raise ValueError("real occupied product Copy entry, owner, visible original native error or error frame is missing") + _copy_error_frame_dimensions(value) status = f"Copy failed: OpenClipboard: {value['native_error']}" if value.get("error_status") != status or value.get("native_window_title") != "TermForge — " + status: raise ValueError("product Copy original OpenClipboard error was lost or relabelled") @@ -295,13 +337,14 @@ def validate_selection_workflow(directory: Path, system_clipboard: bool, run_rec if menu != data["copy_popup_menu"]: raise ValueError("actual native Copy popup journal differs from the final manifest") if summary["interactions"]["copy_contention"]: - frame = (directory / "selection-copy-occupied-error.bmp").read_bytes() - if len(frame) != 54 + 1360 * 860 * 4 or frame[:2] != b"BM": + width, height = _copy_error_frame_dimensions(data["copy_contention"]) + expected_size = 54 + width * height * 4 + frame_path = directory / "selection-copy-occupied-error.bmp" + if not frame_path.is_file() or frame_path.is_symlink() or frame_path.stat().st_size != expected_size: raise ValueError("actual occupied Copy visible error frame is missing or truncated") - size, offset, dib, width, height = struct.unpack_from("lines) { - std::string text; for (const auto& cell : row) text += cell.text; + std::string text = termforge::project_line(row, false).text; while (!text.empty() && text.back() == ' ') text.pop_back(); if (text == expected) return true; } @@ -208,7 +208,7 @@ class ShellWindow { } bool has(const std::string& value) const { for (const auto& row : snapshot_->lines) { - std::string text; for (const auto& cell : row) text += cell.text; + const std::string text = termforge::project_line(row, false).text; if (text.find(value) != std::string::npos) return true; } return false; @@ -1165,7 +1165,7 @@ class ShellWindow { bool contains_line(const std::string& text) const { return line_is(text); } bool live_contains_line(const std::string& expected) const { for(const auto& line:parser_.screen.lines) { - std::string text; for(const auto& cell:line) text+=cell.text; + std::string text = termforge::project_line(line, false).text; while(!text.empty() && text.back()==' ') text.pop_back(); if(text==expected) return true; } diff --git a/experiments/shell/test_copy_error_frame.py b/experiments/shell/test_copy_error_frame.py new file mode 100644 index 00000000..0faed7d8 --- /dev/null +++ b/experiments/shell/test_copy_error_frame.py @@ -0,0 +1,74 @@ +"""Synthetic bitmap/geometry protocol tests; these are not native clipboard evidence.""" +import copy +import struct +import unittest + +from experiments.shell import selection_checks as oracle + + +def geometry(width, height): + return {"schema": "termforge-copy-error-client-geometry-v1", "coordinate_space": "physical-client-pixels", + "client_rect": [0, 0, width, height], "client_rect_after": [0, 0, width, height], + "physical_size": [width, height], "bitmap_size": [width, height], + "bitmap_stride_bytes": width * 4, "bitmap_format": [1, 32]} + + +def contention(width=1008, height=681): + return {"schema": "termforge-product-copy-contention-v2", "error_frame_geometry": geometry(width, height)} + + +def bitmap(width, height): + size = 54 + width * height * 4 + return struct.pack("<2sIHHIIiiHHIIiiII", b"BM", size, 0, 0, 54, 40, width, -height, + 1, 32, 0, 0, 0, 0, 0, 0) + bytes(size - 54) + + +class CopyErrorFrameOracleTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.short = bitmap(1008, 681) + cls.original = bitmap(1360, 860) + + def test_v2_requires_native_geometry_matching_every_bitmap_byte(self): + for width, height, frame in ((1008, 681, self.short), (1360, 860, self.original)): + self.assertEqual(oracle._validate_copy_error_frame(contention(width, height), frame), + (width, height)) + + def test_legacy_v1_keeps_its_original_fixed_geometry_and_cannot_claim_v2_metadata(self): + legacy = {"schema": "termforge-product-copy-contention-v1"} + self.assertEqual(oracle._validate_copy_error_frame(legacy, self.original), (1360, 860)) + for value, frame in ((legacy, self.short), ({**legacy, "error_frame_geometry": geometry(1360, 860)}, self.original)): + with self.assertRaises(ValueError): + oracle._validate_copy_error_frame(value, frame) + + def test_missing_or_conflicting_independent_geometry_is_rejected(self): + valid = contention() + bad = copy.deepcopy(valid); del bad["error_frame_geometry"] + with self.assertRaises(ValueError): oracle._validate_copy_error_frame(bad, self.short) + for field, wrong in (("schema", "unobserved"), ("coordinate_space", "logical-client-dips"), + ("client_rect", [0, 0, 1009, 681]), ("client_rect", [1, 0, 1008, 681]), + ("client_rect", [0, 0, True, 681]), ("client_rect", [0, 0, 6021, 681]), + ("client_rect_after", [0, 0, 1008, 682]), ("physical_size", [1008, 682]), + ("bitmap_size", [1009, 681]), ("bitmap_stride_bytes", 4036), + ("bitmap_format", [1, 24])): + with self.subTest(field=field, wrong=wrong): + bad = copy.deepcopy(valid); bad["error_frame_geometry"][field] = wrong + with self.assertRaises(ValueError): oracle._validate_copy_error_frame(bad, self.short) + for field in valid["error_frame_geometry"]: + with self.subTest(missing=field): + bad = copy.deepcopy(valid); del bad["error_frame_geometry"][field] + with self.assertRaises(ValueError): oracle._validate_copy_error_frame(bad, self.short) + + def test_truncation_extra_payload_and_changed_native_bmp_header_are_rejected(self): + for frame in (self.short[:53], self.short[:-1], self.short + b"\0"): + with self.assertRaises(ValueError): oracle._validate_copy_error_frame(contention(), frame) + fields = list(struct.unpack_from("<2sIHHIIiiHHIIiiII", self.short)) + for index, wrong in ((0, b"XX"), (1, len(self.short)-4), (2, 1), (3, 1), (4, 58), + (5, 124), (6, 1009), (7, 681), (8, 2), (9, 24), (10, 1), (11, 4032*681)): + with self.subTest(header_field=index): + changed = fields.copy(); changed[index] = wrong + frame = struct.pack("<2sIHHIIiiHHIIiiII", *changed) + self.short[54:] + with self.assertRaises(ValueError): oracle._validate_copy_error_frame(contention(), frame) + + +if __name__ == "__main__": unittest.main() diff --git a/experiments/shell/test_shell.py b/experiments/shell/test_shell.py index 5b47605b..09782b1c 100644 --- a/experiments/shell/test_shell.py +++ b/experiments/shell/test_shell.py @@ -178,6 +178,21 @@ def test_mouse_selection_copy_model_zoom_sessions_and_eviction(self): with self.subTest(raw_selection_bytes=index), self.assertRaises(ValueError): validate_selection_workflow(rejected, False, record) + def test_selection_autoscroll_with_short_and_tall_workspace(self): + for height in (681, 860): + with self.subTest(client_height=height): + state = self.out / f"selection-height-{height}.layout" + state.write_bytes(narrow_workspace_fixture().replace( + b"window-height 681", f"window-height {height}".encode("ascii"))) + out = self.out / f"selection-height-{height}" + execute(self.exe, "ui-selection", out, settings_source=state) + data = json.loads((out / "selection.json").read_text(encoding="utf-8")) + starts = [row for row in data["interactions"] if row["phase"] in ( + "autoscroll-up-start", "autoscroll-down-start", "autoscroll-capture-start")] + self.assertEqual([row["anchor_line"] for row in starts], [75, 50, 40]) + self.assertTrue(all(row["top"] <= row["anchor_line"] < row["top"] + row["page"] + for row in starts)) + def test_find_real_edit_navigation_unicode_output_isolation_and_cancel(self): for repeat in range(2): out = self.out / f"find-{repeat}" @@ -302,6 +317,7 @@ def test_mouse_selection_to_real_system_clipboard(self): record = execute(self.exe, "ui-copy-system", out) data = json.loads((out / "selection.json").read_text(encoding="utf-8")) self.assertEqual(data["schema"], "termforge-window-selection-v5") + self.assertEqual(data["copy_contention"]["schema"], "termforge-product-copy-contention-v2") self.assertTrue(record["selection_workflow"]["interactions"]["actual_copy_popup"]) self.assertTrue(record["selection_workflow"]["interactions"]["copy_contention"]) for field, wrong in (("actual_return_command", 15), ("target_command", 15), diff --git a/experiments/shell/test_workflow.py b/experiments/shell/test_workflow.py index c53b66af..80bebd16 100644 --- a/experiments/shell/test_workflow.py +++ b/experiments/shell/test_workflow.py @@ -80,7 +80,66 @@ def synthetic_console(directory, role='F1', generation=1, columns=80, lines=None return metadata +def captured_rejected_order(directory): + """The 0e1fb585 failure record, with no process identities or private paths.""" + fixture = Path(__file__).with_name('fixtures') / 'rejected-order-before-read.json' + record = json.loads(fixture.read_text(encoding='utf-8')) + for suffix in ('', '.source.bin'): + (directory / ('capture.bin' + suffix)).write_bytes(b'') + for key, suffix in [('manifest', '.workflow.json'), ('receiver', '.receiver.json')]: + (directory / ('capture.bin' + suffix)).write_text(json.dumps(record[key]), encoding='utf-8') + (directory / 'capture.bin.progress.jsonl').write_text( + ''.join(json.dumps(event) + '\n' for event in record['events']), encoding='utf-8') + return record + + class WorkflowOracleTest(unittest.TestCase): + def test_captured_rejected_order_can_stop_before_first_read(self): + with tempfile.TemporaryDirectory() as temporary: + directory = Path(temporary) + captured_rejected_order(directory) + result = validate_rejected_order(directory, expected_generation=5) + self.assertTrue(result['expected_rejection']) + self.assertFalse(result['normal_completion']) + self.assertEqual(result['reason'], 'workflow command order invalid') + + def test_before_first_read_rejection_requires_complete_clean_stop(self): + defects = [('reads_entered', 1), ('reads_entered', False), ('reads_completed', 1), + ('worker_joined', False), ('stop_requested', False), ('worker_error', 5), + ('failure_step', 'ReadConsoleW'), ('state', 'failed'), + ('received_units', 1), ('saved_bytes', 1), + ('cancel_calls', 0), ('cancel_successes', 1), ('last_cancel_error', 0), + ('last_cancel_error', 5), ('unexpected_cancel_observed', True), + ('unexpected_cancel_error', 5), ('last_wait_result', 258), ('last_wait_error', 5), + ('last_read_success', True), ('last_read_count', 1), ('last_read_error', 995), + ('zero_read_accepted', True)] + for field, value in defects: + with self.subTest(field=field, value=value), tempfile.TemporaryDirectory() as temporary: + directory = Path(temporary) + record = captured_rejected_order(directory) + record['receiver'][field] = value + (directory / 'capture.bin.receiver.json').write_text(json.dumps(record['receiver']), encoding='utf-8') + with self.assertRaises(ValueError): + validate_rejected_order(directory, expected_generation=5) + for suffix in ('', '.source.bin'): + with self.subTest(nonempty=suffix), tempfile.TemporaryDirectory() as temporary: + directory = Path(temporary) + captured_rejected_order(directory) + (directory / ('capture.bin' + suffix)).write_bytes(b'x') + with self.assertRaises(ValueError): + validate_rejected_order(directory, expected_generation=5) + + def test_before_first_read_is_not_accepted_for_normal_fixture_cases(self): + for role in ('A', 'B'): + with self.subTest(role=role), tempfile.TemporaryDirectory() as temporary: + directory = Path(temporary) + captured = captured_rejected_order(directory) + synthetic_case(directory, role) + (directory / 'capture.bin.receiver.json').write_text(json.dumps(captured['receiver']), encoding='utf-8') + with self.assertRaises(ValueError): + validate_fixture_case(directory, expected_role=role, expected_generation=1, + expected_input=b'' if role == 'A' else (PREFIX + '你好').encode()) + def test_console_window_requires_bounded_identity_and_complete_unicode(self): with tempfile.TemporaryDirectory() as temporary: directory = Path(temporary) diff --git a/experiments/shell/transport_completion.hpp b/experiments/shell/transport_completion.hpp new file mode 100644 index 00000000..81541ce3 --- /dev/null +++ b/experiments/shell/transport_completion.hpp @@ -0,0 +1,86 @@ +#pragma once +#include +#include + +namespace tf_shell_test { +// An independent transport oracle for the fixed persistence commands. This is +// deliberately not a terminal screen/parser: it only removes complete OSC/CSI +// envelopes and recognizes exact literal output lines followed by our prompt. +// The fixed ConPTY output can separate its marker and prompt with an explicit +// column-one CUP. This is a record boundary, without assuming the initial cursor +// row or claiming that the marker still occupies a separate visible screen row. +// Complete CSI envelopes are omitted without interpreting their semantics. +// Unknown ESC forms, malformed/incomplete envelopes and other control strings +// remain unproven. ShellWindow retains its independent 8192-byte observation cap. +inline bool transport_output_completed(std::string_view raw, std::string_view marker) { + if(marker.empty()) return false; + for(const unsigned char byte:marker) if(byte<0x20 || byte==0x7f) return false; + const auto prompt=[](std::string_view line) { return line=="TF>" || line=="TF> "; }; + std::string line; + bool output_seen=false,prompt_seen=false,literal_line=true; + const auto end_line=[&] { + if(literal_line && line==marker) output_seen=true; + else if(literal_line && output_seen && prompt(line)) prompt_seen=true; + line.clear(); literal_line=true; + }; + for(std::size_t i=0;i(raw[i++]); + if(byte==0x1b) { + if(i==raw.size()) return false; + const char kind=raw[i++]; + if(kind==']') { + bool terminated=false; + while(i(raw[i++]); + if(next>=0x40 && next<=0x7e) { + if(next=='H' || next=='f') { + const auto values=raw.substr(parameters,i-parameters-1); + const auto separator=values.find(';'); + if(separator!=std::string_view::npos && separator>0 && values.substr(separator+1)=="1") { + bool digits=true,positive=false; + for(const unsigned char digit:values.substr(0,separator)) { + if(digit<'0' || digit>'9') digits=false; + if(digit>='1' && digit<='9') positive=true; + } + if(digits && positive) { + if(literal_line && (line==marker || prompt(line))) end_line(); + // Do not discard echo prefixes or combine + // partial marker fragments across a CUP. + // Only a real LF resets an ambiguous record. + else if(!line.empty()) literal_line=false; + } + } + } + terminated=true; break; + } + if(next>=0x20 && next<=0x2f) intermediate=true; + else if(next<0x30 || next>0x3f || intermediate) return false; + } + if(!terminated) return false; + } else return false; + } else if(byte=='\r') { + // CR alone may overwrite an echo; only a following LF proves the + // literal output line has completed. + if(i==raw.size() || raw[i]!='\n') return false; + } else if(byte=='\n') { + end_line(); + } else { + if(byte<0x20 || byte==0x7f) return false; + line.push_back(static_cast(byte)); + } + } + return prompt_seen || (literal_line && output_seen && prompt(line)); +} +} diff --git a/experiments/shell/transport_completion_test.cpp b/experiments/shell/transport_completion_test.cpp new file mode 100644 index 00000000..d144335e --- /dev/null +++ b/experiments/shell/transport_completion_test.cpp @@ -0,0 +1,63 @@ +#include "transport_completion.hpp" +#include +#include +#include + +static void check(bool value, const char* detail) { + if(!value) throw std::runtime_error(detail); +} + +int main() { + try { + using tf_shell_test::transport_output_completed; + const std::string marker="TF_CMD_DIR=restore-cmd \xe7\x9b\xae\xe5\xbd\x95"; + const std::string line=marker+"\r\n"; + const std::string title="\x1b]0;cmd.exe - echo "+marker; + // This is the captured failure pattern, without machine paths. The + // final marker belongs to OSC, after the actual output line. + check(transport_output_completed("for %I in (\"%CD%\") do @echo TF_CMD_DIR=%~nxI\r\n"+ + line+title+"\a\x1b]0;cmd.exe\a\r\nTF>",marker),"OSC BEL title must not shadow actual output"); + check(transport_output_completed(line+title+"\x1b\\\r\nTF> ",marker),"OSC ST title must not shadow actual output"); + check(transport_output_completed("\x1b[?25l\x1b[93m"+marker+"\x1b[m\r\n\x1b[?25hTF> ",marker), + "complete CSI and SGR are omitted from literal lines"); + check(transport_output_completed("TF_PS_DIR=restore-powershell \xe7\xa9\xba\xe6\xa0\xbc\r\nTF_PS_MAJOR=5\r\nTF> ", + "TF_PS_MAJOR=5"),"multiple independent output lines"); + check(transport_output_completed("echo TF_RESTORE_BOTTOM\r\n\x1b]0;echo TF_RESTORE_BOTTOM\a" + "TF_RESTORE_BOTTOM\r\n\x1b]0;cmd.exe\a\r\nTF>","TF_RESTORE_BOTTOM"),"echo and output remain distinct"); + check(transport_output_completed("for %I in (\"%CD%\") do @echo TF_CMD_DIR=%~nxI\r\n"+ + marker+"\x1b[6;1H\x1b[?25hTF>",marker),"ConPTY column-one CUP frames an exact output line"); + + check(!transport_output_completed("TF>echo "+line+"TF>",marker),"command echo is not independent output"); + check(!transport_output_completed(title+"\a\r\nTF>",marker),"OSC BEL payload is not output"); + check(!transport_output_completed(title+"\x1b\\\r\nTF>",marker),"OSC ST payload is not output"); + check(!transport_output_completed("prefix "+line+"TF>",marker),"marker must begin the output line"); + check(!transport_output_completed(marker+" suffix\r\nTF>",marker),"marker must end the output line"); + check(!transport_output_completed(marker+"\x1b[31m suffix\r\nTF>",marker),"CSI does not terminate an output line"); + check(!transport_output_completed("echo "+marker+"\x1b[6;1HTF>",marker),"CUP does not turn command echo into output"); + check(!transport_output_completed("echo \x1b[6;1H"+marker+"\x1b[7;1HTF>",marker), + "CUP cannot discard an echo prefix to invent marker output"); + check(!transport_output_completed("TF>echo \x1b[6;1H"+marker+"\x1b[7;1HTF>",marker), + "CUP cannot discard a prompt and echo prefix"); + check(!transport_output_completed("TF_CMD_DIR=\x1b[6;1Hrestore-cmd \xe7\x9b\xae\xe5\xbd\x95\x1b[7;1HTF>",marker), + "CUP-separated partial fragments cannot assemble marker output"); + check(!transport_output_completed(marker+"\x1b[6;2HTF>",marker),"non-column-one CUP does not complete output"); + check(!transport_output_completed(marker+"\x1b[6;1H",marker),"CUP still requires the returned prompt"); + check(!transport_output_completed("TF>\r\n"+line,marker),"old prompt does not prove return"); + check(!transport_output_completed(line,marker),"fresh prompt required"); + check(!transport_output_completed(line+"prefix TF>",marker),"prompt substring is not a fresh prompt"); + check(!transport_output_completed(line+"TF>echo next",marker),"echo after prompt is not an idle prompt"); + check(!transport_output_completed(marker+"\rTF>",marker),"carriage return alone does not complete an output line"); + check(!transport_output_completed(line+"\x1b]0;unterminated TF>",marker),"incomplete OSC cannot supply prompt"); + check(!transport_output_completed(line+"TF>\x1b]0;unterminated",marker),"incomplete trailing OSC remains unknown"); + check(!transport_output_completed(line+"TF>\x1b[31",marker),"incomplete CSI remains unknown"); + check(!transport_output_completed(line+"TF>\x1b",marker),"incomplete escape remains unknown"); + check(!transport_output_completed(marker+"\x1b]0;\r\nTF>",marker),"incomplete OSC cannot complete marker line"); + check(!transport_output_completed(line+"TF>\x1b[1 2m",marker),"malformed CSI rejected"); + check(!transport_output_completed(line+"TF>\x1bPunknown\x1b\\",marker),"unsupported control strings remain unknown"); + check(!transport_output_completed(line+"TF>",""),"empty marker rejected"); + check(!transport_output_completed("TF>\r\nTF>","TF>\r\nTF>"),"marker cannot contain controls"); + std::cout<<"transport completion checks passed\n"; + } catch(const std::exception& error) { + std::cerr<= (2 if raw else 1) and reads == _uint(receiver.get('reads_completed'), 'reads completed'), 'receiver read join') + completed = _uint(receiver.get('reads_completed'), 'reads completed') calls = _uint(receiver.get('cancel_calls'), 'cancel calls') successes = _uint(receiver.get('cancel_successes'), 'cancel successes') + if allow_unentered_stop and reads == 0: + # Append-before-output may reject and stop the receiver before its + # worker enters ReadConsoleW. There is then no last read to cancel. + _require(raw == b'' and completed == 0, 'unentered receiver read join') + _require(calls >= 1 and successes == 0 and + _uint(receiver.get('last_cancel_error'), 'cancel error') == 1168, 'unentered receiver cancellation') + _require(receiver.get('last_read_success') is False and + _uint(receiver.get('last_read_count'), 'last read count') == 0 and + _uint(receiver.get('last_read_error'), 'last read error') == 0 and + receiver.get('zero_read_accepted') is False, 'unentered receiver has no final read') + return + _require(reads >= (2 if raw else 1) and reads == completed, 'receiver read join') _require(calls >= successes >= 1 and _uint(receiver.get('last_cancel_error'), 'cancel error') in (0, 1168), 'receiver cancellation') success = receiver.get('last_read_success') _require(type(success) is bool and _uint(receiver.get('last_read_count'), 'last read count') == 0, 'receiver final read') @@ -140,7 +152,7 @@ def validate_rejected_order(directory, *, expected_generation): _uint(event.get('records'), 'event records') == 0 and event.get('source_sha256') == _sha(b''), 'rejection event payload') receiver = json.loads(_read(directory / 'capture.bin.receiver.json', 65536)) - _validate_receiver(manifest, receiver, b'') + _validate_receiver(manifest, receiver, b'', allow_unentered_stop=True) return dict(role='F2', generation=expected_generation, expected_rejection=True, reason='workflow command order invalid', normal_completion=False) diff --git a/experiments/shell/workspace_interactions.hpp b/experiments/shell/workspace_interactions.hpp index 346b63e4..fee0688e 100644 --- a/experiments/shell/workspace_interactions.hpp +++ b/experiments/shell/workspace_interactions.hpp @@ -2,6 +2,7 @@ #pragma once #include #include "edit_actions.hpp" +#include "local_shell_diagnostics.hpp" inline Workspace::Divider Workspace::divider_at(int x,int y) const { if(state_.focus || closing_) return Divider::none; @@ -94,6 +95,15 @@ inline void Workspace::collect_child(ShellWindow* child) { try { const Result result=child->collect(); require(result.complete && result.active_children==0,"workspace child cleanup failed"); + if(test_mode_=="ui-shells" && !image_.empty()) { + const auto kind=tabs[i].launch?tabs[i].launch->kind:L"cmd"; + const auto role=kind==L"powershell"?"powershell":kind==L"pwsh"?"pwsh":kind==L"wsl"?"wsl":"cmd"; + tf_shell_test::local_shell_progress(std::filesystem::path(image_).parent_path(),role,"exited",stage_, + result.exit_code,result.complete,result.active_children,child->observed_output()); + if(!closing_ && failure_.empty() && (stage_<6 || result.exit_code!=0)) + failure_="workspace local shell exited before verification or normal exit: "+std::string(role)+ + " stage="+std::to_string(stage_)+" exit="+std::to_string(result.exit_code); + } } catch(const std::exception& error) { failure_=error.what(); } if(workflow_) { try { workflow_collected(child,tabs[i].id); } diff --git a/experiments/shell/workspace_persistence_scenario.hpp b/experiments/shell/workspace_persistence_scenario.hpp index 28598900..f7f73dbd 100644 --- a/experiments/shell/workspace_persistence_scenario.hpp +++ b/experiments/shell/workspace_persistence_scenario.hpp @@ -1,13 +1,9 @@ // Owned data-only persistence scenarios; no bitmap or user-desktop input. #pragma once +#include "transport_completion.hpp" inline bool workspace_output_completed(ShellWindow* shell,const std::string& marker) { - if(marker.empty()) return false; - const auto& raw=shell->observed_output(); - const auto output=raw.rfind(marker),prompt=raw.rfind("TF>"); - if(output==std::string::npos || prompt==std::string::npos || output>=prompt) return false; - const auto suffix=output+marker.size(); - return suffixobserved_output(),marker); } inline std::string Workspace::workspace_geometry() { diff --git a/experiments/shell/workspace_selection_scenario.hpp b/experiments/shell/workspace_selection_scenario.hpp index 30388748..20862a88 100644 --- a/experiments/shell/workspace_selection_scenario.hpp +++ b/experiments/shell/workspace_selection_scenario.hpp @@ -25,7 +25,7 @@ inline void Workspace::scenario_selection() { static std::optional copy_owner_release; static DWORD occupied_sequence_before=0, occupied_sequence_error=0, occupied_sequence_joined=0, occupied_sequence_retry=0; static DWORD copy_native_error=0; - static std::string copy_error_status, copy_error_title; + static std::string copy_error_status, copy_error_title, copy_error_geometry; static bool owner_before=false, owner_at_error=false, copy_error_visible=false; if(restore_keys) { require(SetKeyboardState(saved_keys.data())!=FALSE,"selection keyboard state restoration failed"); restore_keys=false; } const auto line_text=[](const auto& line) { @@ -162,6 +162,20 @@ inline void Workspace::scenario_selection() { }; const auto begin_autoscroll=[&](unsigned anchor,int direction) { select(0,Group::main); anchor_line=anchor; scroll_direction=direction; + // A native PageUp exposes fewer fixture rows on a short workspace. + // Put the same literal anchor at the opposite edge of the drag using + // real scrollbar messages. Separate drags then also begin at different + // pixels instead of accidentally becoming a double-click on short pages. + const auto initial=main->history_state(); + const size_t offset=direction<0?initial.page-1:0; + const size_t target=(std::min)(initial.maximum_top,anchor>offset?anchor-offset:size_t{0}); + for(size_t steps=0;;++steps) { + const auto state=main->history_state(); + if(state.top==target) break; + require(steps<82,"selection anchor reveal exceeded the fixed fixture"); + SendMessageW(main->handle(),WM_VSCROLL,targethistory_state().top!=state.top,"selection anchor reveal made no scrollbar progress"); + } const int row=row_of(main,fixture_line(anchor)); mouse(main,WM_LBUTTONDOWN,pixel(main,0,row)); require(GetCapture()==main->handle(),"autoscroll drag has no actual capture"); @@ -175,7 +189,7 @@ inline void Workspace::scenario_selection() { interaction_samples.clear(); popup_gesture.reset(); popup_evidence.clear(); copy_popup_evidence.clear(); copy_selection_before.clear(); copy_sequence_before=copy_sequence_after=0; - copy_owner.reset(); copy_owner_release.reset(); copy_native_error=0; copy_error_status.clear(); copy_error_title.clear(); + copy_owner.reset(); copy_owner_release.reset(); copy_native_error=0; copy_error_status.clear(); copy_error_title.clear(); copy_error_geometry.clear(); occupied_sequence_before=occupied_sequence_error=occupied_sequence_joined=occupied_sequence_retry=0; owner_before=owner_at_error=copy_error_visible=false; if(system) tf_shell_test::require_hosted_clipboard(); @@ -202,13 +216,18 @@ inline void Workspace::scenario_selection() { click(bottom,word); record("line",entry(true),copy(bottom,true)); record("zoom-before"); bottom->change_zoom(25); stage_=3; } else if(stage_==3 && bottom->settled() && main && main->settled()) { + // Zoom can move the still-valid selected row above a short viewport. + // Reveal it before observing the existing visible-highlight contract. + SendMessageW(bottom->handle(),WM_VSCROLL,SB_TOP,0); record("zoom-after"); select(0,Group::main); const int other=row_of(main,"OTHER text"); drag(main,pixel(main,0,other),pixel(main,11,other)); record("main-selected",entry(false),copy(main,false)); select(0,Group::bottom); record("bottom-restored",entry(true),copy(bottom,true)); - select(0,Group::main); bottom->fixture_command(L"echo TF_SELECT_APPEND\r"); stage_=4; + select(0,Group::main); + SendMessageW(bottom->handle(),WM_VSCROLL,SB_BOTTOM,0); + bottom->fixture_command(L"echo TF_SELECT_APPEND\r"); stage_=4; } else if(stage_==4 && bottom->live_contains_line("TF_SELECT_APPEND")) { record("append-retained"); bottom->fixture_command(L"for /L %i in (1,1,500) do @echo TF_SEL_%i\r"); stage_=5; @@ -332,11 +351,34 @@ inline void Workspace::scenario_selection() { copy_error_title=="TermForge — "+copy_error_status && main->has_selection() && selection_value.status==termforge::SelectionCopyStatus::ok && selection_value.text==copy_selection_before, "occupied product Copy failed without preserving its actual visible error, selected target or clipboard sequence"); - Bitmap frame(physical_width_,physical_height_); paint(frame.dc); + RECT client{}; + require(GetClientRect(window_,&client)!=FALSE && client.left==0 && client.top==0 && + client.right==physical_width_ && client.bottom==physical_height_ && client.right>0 && client.bottom>0, + "Copy error client rectangle differs from the physical workspace surface"); + Bitmap frame(physical_width_,physical_height_); + BITMAP actual_bitmap{}; + require(GetObjectW(frame.bitmap,static_cast(sizeof(actual_bitmap)),&actual_bitmap)==static_cast(sizeof(actual_bitmap)) && + actual_bitmap.bmWidth==client.right && actual_bitmap.bmHeight==client.bottom && + actual_bitmap.bmPlanes==1 && actual_bitmap.bmBitsPixel==32 && actual_bitmap.bmWidthBytes==client.right*4, + "Copy error actual DIB geometry differs from its native client rectangle"); + paint(frame.dc); for(const auto group:{Group::bottom,Group::main}) { const RECT rect=physical(group==Group::main?main_terminal_:terminal_); selected(group)->render_to(frame.dc,rect.left,rect.top); } frame.save((directory/"selection-copy-occupied-error.bmp").string()); + RECT client_after{}; + require(GetClientRect(window_,&client_after)!=FALSE && EqualRect(&client,&client_after)!=FALSE && + frame.width==physical_width_ && frame.height==physical_height_, + "Copy error client geometry changed during its frame capture"); + std::ostringstream geometry; + geometry<<"{\"schema\":\"termforge-copy-error-client-geometry-v1\",\"coordinate_space\":\"physical-client-pixels\"" + <<",\"client_rect\":["<release(); stage_=25; } else if(stage_==25 && system && copy_owner && copy_owner->released()) { copy_owner_release=copy_owner->finish(); copy_owner.reset(); @@ -380,11 +422,13 @@ inline void Workspace::scenario_selection() { require(copy_owner_release.has_value() && !copy_owner,"occupied Copy owner was not joined before final evidence"); const auto& release=*copy_owner_release; const auto& observed=release.observation; const auto flag=[&](const char* name,bool value) { out<<",\""<ready()) { + active()->begin_output_observation(); + tf_shell_test::local_shell_progress(directory,"cmd","started",stage_); active()->command(L"if /I \"%CD%\"==\"" + expected_directory + L"\" echo TF_CMD_CWD_OK\r"); stage_ = 1; } else if (stage_ == 1 && active()->contains_line("TF_CMD_CWD_OK")) { + tf_shell_test::local_shell_progress(directory,"cmd","cwd-verified",stage_); add(Group::main, with_directory(tf_shell::powershell(false))); + active()->begin_output_observation(); + tf_shell_test::local_shell_progress(directory,"powershell","started",stage_); active()->command(L"if ($PWD.Path -eq '" + expected_directory + L"') { 'TF_PS51_CWD_OK' }\r"); stage_ = 2; } else if (stage_ == 2 && active()->contains_line("TF_PS51_CWD_OK")) { + tf_shell_test::local_shell_progress(directory,"powershell","cwd-verified",stage_); add(Group::bottom, with_directory(tf_shell::powershell(true))); + active()->begin_output_observation(); + tf_shell_test::local_shell_progress(directory,"pwsh","started",stage_); active()->command(L"if ($PWD.Path -eq '" + expected_directory + L"') { 'TF_PS7_CWD_OK' }\r"); stage_ = 3; } else if (stage_ == 3 && active()->contains_line("TF_PS7_CWD_OK")) { + tf_shell_test::local_shell_progress(directory,"pwsh","cwd-verified",stage_); add(Group::main, tf_shell::wsl(L"Ubuntu", expected_directory)); + active()->begin_output_observation(); + tf_shell_test::local_shell_progress(directory,"wsl","started",stage_); active()->command(L"printf '\\nTF_WSL\\n'; test \"$PWD\" = '" + std::wstring(expected_wsl_directory.begin(), expected_wsl_directory.end()) + L"' && printf 'TF_WSL_CWD_OK\\n'\r"); stage_ = 4; } else if (stage_ == 4 && active()->contains_line("TF_WSL_CWD_OK")) { + tf_shell_test::local_shell_progress(directory,"wsl","cwd-verified",stage_); check_size(); stage_ = 5; } else if (stage_ == 5 && sizes_ready()) { diff --git a/tools/owned_job_cleanup.py b/tools/owned_job_cleanup.py new file mode 100644 index 00000000..ffd702f5 --- /dev/null +++ b/tools/owned_job_cleanup.py @@ -0,0 +1,144 @@ +"""Read-only identity audit of one already-owned Job; never enumerate global processes. + +API contract: https://learn.microsoft.com/windows/win32/api/winnt/ns-winnt-jobobject_basic_process_id_list +Fixture policy and limits: docs/research/hosted-wsl-fixture-cleanup.md +Image identity is diagnostic; exact Job membership and actual exit are cleanup evidence. +""" +from __future__ import annotations + +import ctypes +from ctypes import wintypes +from pathlib import PureWindowsPath +import time + + +class CleanupAuditError(RuntimeError): + pass + + +def image_role(image, system_directory, program_files): + """Only exact first-party WSL host locations qualify for fixture cleanup.""" + image = PureWindowsPath(image) + roots = (PureWindowsPath(system_directory), PureWindowsPath(program_files) / "WSL") + if any(image == root / "wslhost.exe" for root in roots): + return "windows-wsl-host", "system32" if image.parent == roots[0] else "program-files-wsl" + # Name-only observations are diagnostic, never sufficient for acceptance. + return ("unverified-init" if image.name.casefold() == "init" else "unknown"), "unverified" + + +class JobMembers: + LIMIT = 256 + + def __init__(self, job, root_pid): + self.job, self.api, self.root_pid = job, job.api, root_pid + self.handles, self.private, self.members = [], [], [] + self.api.IsProcessInJob.argtypes = [wintypes.HANDLE, wintypes.HANDLE, ctypes.POINTER(wintypes.BOOL)] + self.api.IsProcessInJob.restype = wintypes.BOOL + self.api.QueryFullProcessImageNameW.argtypes = [wintypes.HANDLE, wintypes.DWORD, wintypes.LPWSTR, + ctypes.POINTER(wintypes.DWORD)] + self.api.QueryFullProcessImageNameW.restype = wintypes.BOOL + self.api.WaitForSingleObject.argtypes = [wintypes.HANDLE, wintypes.DWORD] + self.api.WaitForSingleObject.restype = wintypes.DWORD + self.api.GetExitCodeProcess.argtypes = [wintypes.HANDLE, ctypes.POINTER(wintypes.DWORD)] + self.api.GetExitCodeProcess.restype = wintypes.BOOL + self.api.GetSystemDirectoryW.argtypes = [wintypes.LPWSTR, wintypes.UINT] + self.api.GetSystemDirectoryW.restype = wintypes.UINT + system = ctypes.create_unicode_buffer(32768) + size = self.api.GetSystemDirectoryW(system, len(system)) + if not 0 < size < len(system): + raise CleanupAuditError("system_directory") + import os + self.pids = self.process_ids() + try: + for index, pid in enumerate(self.pids): + public = {"schema": "termforge-owned-job-member-v2", "member": index, + "role": "root-worker" if pid == root_pid else "unknown", + "exact_job_membership_verified": False, "image_scope": "unverified", + "membership_query_error": None, "image_query_error": None, "exit_query_error": None, + "exited_after_cleanup": None, "exit_code_after_cleanup": None} + private = {"member": index, "pid": pid, "image": None} + self.members.append(public); self.private.append(private) + handle = self.api.OpenProcess(0x1000 | 0x00100000, False, pid) + self.handles.append(handle) + if not handle: + public["membership_query_error"] = ctypes.get_last_error() or 1 + continue + contained = wintypes.BOOL() + if not self.api.IsProcessInJob(handle, job.handle, ctypes.byref(contained)) or not contained.value: + public["membership_query_error"] = ctypes.get_last_error() or 1 + continue + public["exact_job_membership_verified"] = True + public["membership_query_error"] = 0 + image = ctypes.create_unicode_buffer(32768) + count = wintypes.DWORD(len(image)) + if not self.api.QueryFullProcessImageNameW(handle, 0, image, ctypes.byref(count)): + public["image_query_error"] = ctypes.get_last_error() or 1 + continue + public["image_query_error"] = 0 + private["image"] = image.value + if pid != root_pid: + public["role"], public["image_scope"] = image_role(image.value, system.value, os.environ["ProgramFiles"]) + self.stable = self.pids == self.process_ids() + except BaseException: + self.close() + raise + + def process_ids(self): + class ProcessIds(ctypes.Structure): + _fields_ = [("assigned", wintypes.DWORD), ("listed", wintypes.DWORD), + ("ids", ctypes.c_size_t * self.LIMIT)] + result = ProcessIds() + if not self.api.QueryInformationJobObject(self.job.handle, 3, ctypes.byref(result), + ctypes.sizeof(result), None): + raise CleanupAuditError("job_process_ids") + if result.listed != result.assigned or result.listed > self.LIMIT: + raise CleanupAuditError("job_process_list_incomplete") + values = sorted(result.ids[:result.listed]) + if len(values) != len(set(values)) or any(pid <= 0 for pid in values): + raise CleanupAuditError("job_process_list_identity") + return values + + def observe_exit(self, timeout=1): + # Job active==0 and each process handle signaled are separate facts. + # Use one shared bound, rather than multiplying it by member count. + deadline = time.monotonic() + timeout + for public, handle in zip(self.members, self.handles): + if not handle: + continue + remaining = max(0, int((deadline-time.monotonic())*1000)) + state = self.api.WaitForSingleObject(handle, remaining) + if state not in (0, 258): + public["exit_query_error"] = ctypes.get_last_error() or 1 + continue + public["exited_after_cleanup"] = state == 0 + if state == 0: + code = wintypes.DWORD() + if self.api.GetExitCodeProcess(handle, ctypes.byref(code)): + public["exit_code_after_cleanup"] = code.value + public["exit_query_error"] = 0 + else: + public["exit_query_error"] = ctypes.get_last_error() or 1 + + def close(self): + for handle in self.handles: + if handle: + self.api.CloseHandle(handle) + self.handles = [] + + +def helper_cleanup_accepted(step, successful_worker, cleanup): + """Fixture-only policy; product shell normal-exit contracts are unaffected.""" + before, after = cleanup.get("job_before"), cleanup.get("job_after") + members = cleanup.get("members", []) + return (step in ("check-before", "check-after") and successful_worker is True and + cleanup.get("root_natural_exit") is True and cleanup.get("root_reaped") is True and + cleanup.get("root_exit_code") == 0 and cleanup.get("root_direct_kill_requested") is False and + cleanup.get("membership_stable") is True and isinstance(members, list) and bool(members) and + isinstance(before, dict) and isinstance(after, dict) and before.get("active", 0) > 0 and + before["active"] == len(members) and after.get("active") == 0 and + cleanup.get("job_after_pid_count") == 0 and cleanup.get("job_termination_requests") == 1 and + cleanup.get("member_cleanup_requested_count") == len(members) and + all(isinstance(member, dict) and member.get("exact_job_membership_verified") is True and + member.get("membership_query_error") == 0 and member.get("exit_query_error") == 0 and + member.get("exited_after_cleanup") is True and + type(member.get("exit_code_after_cleanup")) is int for member in members)) diff --git a/tools/prepare_hosted_shells.py b/tools/prepare_hosted_shells.py new file mode 100644 index 00000000..65a29e16 --- /dev/null +++ b/tools/prepare_hosted_shells.py @@ -0,0 +1,450 @@ +"""Prepare fixed shell prerequisites only on the designated disposable CI VM. + +Never enable Windows features, reboot, convert/remove an existing distro, or +change WSL defaults. Existing Ubuntu startup failure is a real failure. +""" +from __future__ import annotations + +import argparse +import ctypes +import hashlib +import json +import os +from pathlib import Path, PureWindowsPath +import re +import shlex +import subprocess +import sys +import time +import traceback +import urllib.request + +from experiments.conpty.run import OwnedJob +from tools.owned_job_cleanup import JobMembers, helper_cleanup_accepted + +ROOT = Path(__file__).resolve().parents[1] +SCHEMA = "termforge-hosted-shell-preparation-v1" +ROOTFS_VERSION = "noble-20240423-amd64-wsl1" +ROOTFS_URL = "https://cloud-images.ubuntu.com/wsl/releases/noble/20240423/ubuntu-noble-wsl-amd64-wsl.rootfs.tar.gz" +ROOTFS_SHA256 = "8251e27ffff381a4af5f41dcb94d867de3e0d9774a9241908ab34555d99315ea" +MAX_DOWNLOAD = 400 * 1024 * 1024 +STEPS = {"preflight": 45, "download": 120, "import": 90, "check-before": 25, "check-after": 25} + + +class PreparationError(Exception): + """The argument is a fixed public failure code, never raw command output.""" + + +def require_hosted(environment): + expected = {"GITHUB_ACTIONS": "true", "RUNNER_ENVIRONMENT": "github-hosted", + "RUNNER_OS": "Windows", "GITHUB_REPOSITORY": "JavaZeroo/termforge"} + if (any(environment.get(key) != value for key, value in expected.items()) or + environment.get("ImageOS") not in ("win25", "win25-vs2026") or + not re.fullmatch(r"\d{8}\.\d+\.\d+", environment.get("ImageVersion", "")) or + not re.fullmatch(r"\d+", environment.get("GITHUB_RUN_ID", ""))): + raise PreparationError("hosted_guard") + + +def public_hosted_provenance(environment): + require_hosted(environment) + return {"image_os": environment["ImageOS"], "image_version": environment["ImageVersion"], + "github_run_id": environment["GITHUB_RUN_ID"]} + + +def require_paths(workspace, out): + workspace, out = Path(workspace).resolve(), Path(out).resolve() + if workspace != ROOT or out == workspace / "local" or not out.is_relative_to(workspace / "local"): + raise PreparationError("workspace_guard") + return out + + +def require_runtime(): + require_hosted(os.environ) + if os.name != "nt" or not ctypes.windll.shell32.IsUserAnAdmin(): + raise PreparationError("windows_admin_guard") + return Path(os.environ["GITHUB_WORKSPACE"]).resolve() + + +def write_json(path, value): + temporary = path.with_suffix(path.suffix + ".tmp") + temporary.write_text(json.dumps(value, indent=2) + "\n", encoding="utf-8") + os.replace(temporary, path) + + +def linux_path(path): + path = PureWindowsPath(path) + if not path.is_absolute() or not re.fullmatch(r"[A-Za-z]:", path.drive): + raise PreparationError("cwd_identity") + return "/mnt/" + path.drive[0].lower() + "/" + "/".join(path.parts[1:]) + + +def decode_wsl(raw): + if len(raw) > 65536: + raise PreparationError("wsl_report_size") + if raw.startswith((b"\xff\xfe", b"\xfe\xff")): + return raw.decode("utf-16") + return raw.decode("utf-16-le" if b"\x00" in raw else "utf-8-sig") + + +def ubuntu_present(raw): + try: + names = decode_wsl(raw).splitlines() + except UnicodeError: + raise PreparationError("wsl_list_encoding") from None + if len(names) > 100 or any(len(name) > 256 or any(ord(c) < 32 for c in name) for name in names): + raise PreparationError("wsl_list_identity") + return any(name.strip().casefold() == "ubuntu" for name in names) + + +def ubuntu_registered(): + # Check the named registration via the first-party API. An empty `wsl -l` + # can return a nonzero status; do not confuse that with an existing distro + # which failed to start, or delete/replace an existing registration. + api = ctypes.WinDLL("api-ms-win-wsl-api-l1-1-0.dll", use_last_error=True) + api.WslIsDistributionRegistered.argtypes = [ctypes.c_wchar_p] + api.WslIsDistributionRegistered.restype = ctypes.c_int + return bool(api.WslIsDistributionRegistered("Ubuntu")) + + +def classify_registration(code, output, registered): + if registered: + if code or not ubuntu_present(output): + raise PreparationError("wsl_list") + return "ubuntu-existing" + if code == 0 and ubuntu_present(output): + raise PreparationError("wsl_list") + return "ubuntu-absent" + + +def wsl_command(executable, cwd): + # Match the product's explicit distribution, Windows --cd and final exec sh. + return [str(executable), "--cd", str(cwd), "-d", "Ubuntu", "-e", "sh", "-c", + "export PS1='TF> '; export LANG=C.UTF-8; exec sh"] + + +def wsl_input(cwd): + expected = shlex.quote(linux_path(cwd)) + return ("test \"$WSL_DISTRO_NAME\" = Ubuntu || exit 71\n" + "test -x /bin/sh || exit 72\n" + f"test \"$PWD\" = {expected} || exit 73\n" + "printf 'TERMFORGE_HOSTED_WSL_READY\\n'\nexit 37\n").encode("utf-8") + + +def validate_marker(stdout, returncode, marker, expected_code=0): + try: + lines = stdout.decode("utf-8-sig").splitlines() + except UnicodeError: + raise PreparationError("marker_encoding") from None + if returncode != expected_code or lines != [marker]: + raise PreparationError("shell_startup") + + +def import_command(executable, install, archive): + return [str(executable), "--import", "Ubuntu", str(install), str(archive), "--version", "1"] + + +def copy_verified_rootfs(response, destination, *, expected_hash=ROOTFS_SHA256, + limit=MAX_DOWNLOAD, clock=time.monotonic): + """Stream bounded bytes and reject an unverified file before it can be imported.""" + started, count, digest = clock(), 0, hashlib.sha256() + with Path(destination).open("xb") as stream: + while True: + if clock() - started >= 120: + raise PreparationError("download_deadline") + chunk = response.read(1024 * 1024) + if clock() - started >= 120: + raise PreparationError("download_deadline") + if not chunk: + break + count += len(chunk) + if count > limit: + raise PreparationError("download_size") + stream.write(chunk) + digest.update(chunk) + if not count or digest.hexdigest() != expected_hash: + raise PreparationError("download_hash") + + +def run_command(command, directory, name, *, cwd, timeout=10, data=None): + # This function only runs inside a GO-gated worker already in the parent's + # exact Job. No shell string construction or global-process cleanup. + stdout_path, stderr_path = directory / (name + ".stdout"), directory / (name + ".stderr") + with stdout_path.open("xb") as stdout, stderr_path.open("xb") as stderr: + try: + result = subprocess.run(command, input=data, stdout=stdout, stderr=stderr, + cwd=cwd, timeout=timeout, creationflags=subprocess.CREATE_NO_WINDOW) + except subprocess.TimeoutExpired: + raise PreparationError("command_deadline") from None + if stdout_path.stat().st_size > 65536: + raise PreparationError("command_report_size") + return result.returncode, stdout_path.read_bytes() + + +def require_worker_job(): + if os.name != "nt": + raise PreparationError("worker_job_guard") + api = ctypes.WinDLL("kernel32", use_last_error=True) + api.GetCurrentProcess.restype = ctypes.c_void_p + api.IsProcessInJob.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_int)] + api.IsProcessInJob.restype = ctypes.c_int + contained = ctypes.c_int() + if not api.IsProcessInJob(api.GetCurrentProcess(), None, ctypes.byref(contained)) or not contained.value: + raise PreparationError("worker_job_guard") + + +def worker(step, out): + if sys.stdin.buffer.readline(4) != b"GO\n": + return 90 + require_worker_job() + workspace = require_runtime() + require_paths(workspace, out) + private = out / "_private" / step + if not private.is_dir(): + raise PreparationError("worker_directory_guard") + system = Path(os.environ["SystemRoot"]) / "System32" + wsl = system / "wsl.exe" + cwd = out / "_private" / "launch cwd" + cwd.mkdir(exist_ok=True) + archive = out / "_private" / "ubuntu.rootfs.tar.gz" + if step == "preflight": + commands = [([str(system / "cmd.exe"), "/d", "/q", "/c", "echo TERMFORGE_HOSTED_CMD_READY"], + "cmd", "TERMFORGE_HOSTED_CMD_READY"), + ([str(system / "WindowsPowerShell/v1.0/powershell.exe"), "-NoLogo", "-NoProfile", + "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", + "[Console]::WriteLine('TERMFORGE_HOSTED_PS51_READY')"], "ps51", "TERMFORGE_HOSTED_PS51_READY"), + ([str(Path(os.environ["ProgramFiles"]) / "PowerShell/7/pwsh.exe"), "-NoLogo", "-NoProfile", + "-NonInteractive", "-Command", "[Console]::WriteLine('TERMFORGE_HOSTED_PS7_READY')"], + "ps7", "TERMFORGE_HOSTED_PS7_READY")] + for command, name, marker in commands: + code, output = run_command(command, private, name, cwd=cwd) + validate_marker(output, code, marker) + code, output = run_command([str(wsl), "--list", "--quiet"], private, "list", cwd=cwd) + return classify_registration(code, output, ubuntu_registered()) + if step == "download": + request = urllib.request.Request(ROOTFS_URL, headers={"User-Agent": "TermForge-hosted-shell-preparation"}) + with urllib.request.urlopen(request, timeout=15) as response: + if response.status != 200 or response.geturl() != ROOTFS_URL: + raise PreparationError("download_response") + length = response.headers.get("Content-Length") + if length is not None and (not length.isdigit() or not 0 < int(length) <= MAX_DOWNLOAD): + raise PreparationError("download_size") + partial = archive.with_suffix(".download") + copy_verified_rootfs(response, partial) + os.replace(partial, archive) + return "rootfs-verified" + if step == "import": + code, output = run_command([str(wsl), "--list", "--quiet"], private, "list-before-import", cwd=cwd) + if classify_registration(code, output, ubuntu_registered()) != "ubuntu-absent": + raise PreparationError("ubuntu_already_present") + # Recheck the fixed archive before the single, fixed-version import. + if not archive.is_file() or archive.stat().st_size > MAX_DOWNLOAD: + raise PreparationError("download_size") + digest = hashlib.sha256() + with archive.open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + if digest.hexdigest() != ROOTFS_SHA256: + raise PreparationError("download_hash") + install = out / "_private" / "ubuntu-root" + install.mkdir(exist_ok=False) + code, _ = run_command(import_command(wsl, install, archive), private, "import", cwd=cwd, timeout=80) + if code: + raise PreparationError("wsl_import") + return "ubuntu-imported-wsl1" + if step in ("check-before", "check-after"): + code, output = run_command([str(wsl), "--list", "--quiet"], private, "list", cwd=cwd) + if classify_registration(code, output, ubuntu_registered()) != "ubuntu-existing": + raise PreparationError("ubuntu_missing") + code, output = run_command(wsl_command(wsl, cwd), private, "startup", cwd=cwd, + timeout=20, data=wsl_input(cwd)) + validate_marker(output, code, "TERMFORGE_HOSTED_WSL_READY", expected_code=37) + return "ubuntu-sh-cwd-verified" + raise PreparationError("worker_step") + + +RESULTS_BY_STEP = {"preflight": {"ubuntu-existing", "ubuntu-absent"}, + "download": {"rootfs-verified"}, "import": {"ubuntu-imported-wsl1"}, + "check-before": {"ubuntu-sh-cwd-verified"}, "check-after": {"ubuntu-sh-cwd-verified"}} +FAILURES = {"hosted_guard", "workspace_guard", "windows_admin_guard", "worker_job_guard", "cwd_identity", "wsl_report_size", + "wsl_list_encoding", "wsl_list_identity", "marker_encoding", "shell_startup", "download_deadline", + "download_size", "download_hash", "command_deadline", "command_report_size", "worker_directory_guard", + "wsl_list", "download_response", "ubuntu_already_present", "wsl_import", "ubuntu_missing", "worker_step"} + + +def run_step(step, out): + private = out / "_private" / step + private.mkdir(parents=True, exist_ok=False) + record = {"schema": SCHEMA, "step": step, "status": "failed", "failure_code": "worker_failed"} + job = process = None + assigned = False + cleanup = {"root_natural_exit": False, "root_reaped": False, "root_exit_code": None, + "root_direct_kill_requested": False, "job_termination_requests": 0, + "job_before": None, "job_after": None, "job_after_pid_count": None, + "membership_stable": False, "members": [], "member_cleanup_requested_count": 0, + "verified_wsl_helper_count": 0, + "helper_cleanup_policy": "not-observed", "audit_status": "not-observed"} + record["cleanup"] = cleanup + stdout = stderr = b"" + def terminate_job(): + cleanup["job_termination_requests"] += 1 + job.terminate() + try: + job = OwnedJob() + command = [sys.executable, "-m", "tools.prepare_hosted_shells", "--out", str(out), "--worker", step] + process = subprocess.Popen(command, cwd=ROOT, stdin=subprocess.PIPE, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, creationflags=subprocess.CREATE_NO_WINDOW) + job.assign(process.pid) + assigned = True + try: + stdout, stderr = process.communicate(b"GO\n", timeout=STEPS[step]) + cleanup["root_natural_exit"] = True + except subprocess.TimeoutExpired: + record["failure_code"] = "worker_deadline" + terminate_job() + stdout, stderr = process.communicate(timeout=5) + if cleanup["root_natural_exit"] and process.returncode == 0 and len(stdout) <= 4096: + value = json.loads(stdout) + if set(value) == {"result"} and value["result"] in RESULTS_BY_STEP[step]: + record.update(status="passed", failure_code="none", result=value["result"]) + elif len(stdout) <= 4096: + try: + value = json.loads(stdout) + if set(value) == {"failure_code"} and value["failure_code"] in FAILURES: + record["failure_code"] = value["failure_code"] + except (ValueError, TypeError): + pass + except Exception: + (private / "exception.txt").write_text(traceback.format_exc(), encoding="utf-8") + finally: + if job is not None: + audit = None + try: + if process is not None and process.poll() is None: + if assigned: + terminate_job() + else: + cleanup["root_direct_kill_requested"] = True + process.kill() # no GO was sent + stdout, stderr = process.communicate(timeout=5) + cleanup["root_reaped"] = process is not None and process.poll() is not None + cleanup["root_exit_code"] = process.returncode if cleanup["root_reaped"] else None + if assigned: + cleanup["job_before"] = job.drain() + audit = JobMembers(job, process.pid) + cleanup["membership_stable"] = audit.stable + cleanup["members"] = audit.members + cleanup["verified_wsl_helper_count"] = sum( + member["role"] == "windows-wsl-host" and + member["exact_job_membership_verified"] and member["membership_query_error"] == 0 and + member["image_query_error"] == 0 + for member in audit.members) + write_json(private / "cleanup-members.json", audit.private) + if cleanup["job_before"]["active"]: + cleanup["member_cleanup_requested_count"] = len(audit.members) + terminate_job() + cleanup["job_after"] = job.drain() + cleanup["job_after_pid_count"] = len(audit.process_ids()) + audit.observe_exit() + cleanup["audit_status"] = "observed" + if (cleanup["job_after"]["active"] or cleanup["job_after_pid_count"] or + not cleanup["membership_stable"] or + len(audit.members) != cleanup["job_before"]["active"]): + record.update(status="failed", failure_code="cleanup_failed") + if cleanup["job_before"]["active"]: + if helper_cleanup_accepted(step, record["status"] == "passed", cleanup): + cleanup["helper_cleanup_policy"] = "owned-disposable-wsl-check-tree-v1" + else: + cleanup["helper_cleanup_policy"] = "unverified-remaining-members" + record.update(status="failed", failure_code="cleanup_failed") + else: + cleanup["helper_cleanup_policy"] = "natural-empty-job" + except Exception: + cleanup["audit_status"] = "failed" + record.update(status="failed", failure_code="cleanup_failed") + (private / "cleanup.exception.txt").write_text(traceback.format_exc(), encoding="utf-8") + # Failure remains a failure even if a final owned-job cleanup succeeds. + if assigned: + try: + terminate_job() + cleanup["job_after"] = job.drain() + except Exception: + pass + finally: + if audit is not None: + audit.close() + job.close() + (private / "worker.stdout").write_bytes(stdout) + (private / "worker.stderr").write_bytes(stderr) + return record + + +def prepare(out, *, hosted_provenance=None): + report = {"schema": SCHEMA, "status": "failed", "fixed_import_rootfs_version": ROOTFS_VERSION, + "fixed_import_rootfs_sha256": ROOTFS_SHA256, "rootfs_verified": False, + "hosted_provenance": hosted_provenance, "steps": [], "failure_code": "none"} + def step(name): + record = run_step(name, out) + report["steps"].append(record) + write_json(out / "summary.json", report) + if record["status"] != "passed": + raise PreparationError(record["failure_code"]) + if record.get("result") not in RESULTS_BY_STEP[name]: + raise PreparationError("worker_step") + return record.get("result") + try: + existing = step("preflight") == "ubuntu-existing" + if not existing: + step("download") + report["rootfs_verified"] = True + step("import") + # An unusable pre-existing distro fails here. Never delete or replace it. + step("check-before") + step("check-after") + report["status"] = "passed" + except PreparationError as error: + report["failure_code"] = str(error) + except Exception: + report["failure_code"] = "preparation_exception" + (out / "_private" / "preparation.exception.txt").write_text(traceback.format_exc(), encoding="utf-8") + finally: + write_json(out / "summary.json", report) + return report + + +def main(): + cli = argparse.ArgumentParser(description=__doc__) + cli.add_argument("--out", type=Path, required=True) + cli.add_argument("--worker", choices=STEPS, help=argparse.SUPPRESS) + args = cli.parse_args() + if args.worker: + try: + result = worker(args.worker, args.out.resolve()) + if isinstance(result, int): + return result + print(json.dumps({"result": result}), flush=True) + return 0 + except PreparationError as error: + print(json.dumps({"failure_code": str(error) if str(error) in FAILURES else "worker_step"}), flush=True) + return 1 + except Exception: + # Raw traceback stays in the parent's private stderr file. + traceback.print_exc() + return 1 + try: + workspace = require_runtime() + out = require_paths(workspace, args.out) + out.mkdir(parents=True, exist_ok=False) + except (PreparationError, OSError, KeyError): + print(json.dumps({"schema": SCHEMA, "status": "failed", "failure_code": "preparation_guard"})) + return 2 + try: + report = prepare(out, hosted_provenance=public_hosted_provenance(os.environ)) + except Exception: + print(json.dumps({"schema": SCHEMA, "status": "failed", "failure_code": "preparation_exception"})) + return 1 + print(json.dumps({"schema": SCHEMA, "status": report["status"], "failure_code": report["failure_code"]})) + return 0 if report["status"] == "passed" else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/test_owned_job_cleanup.py b/tools/test_owned_job_cleanup.py new file mode 100644 index 00000000..e4dabc66 --- /dev/null +++ b/tools/test_owned_job_cleanup.py @@ -0,0 +1,153 @@ +"""Pure fixture policy regressions plus one own-Job API check; never start WSL.""" +import copy +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest +from unittest import mock + +from tools import prepare_hosted_shells as prep +from tools.owned_job_cleanup import JobMembers, helper_cleanup_accepted, image_role + + +def known_host(): + return {"member": 0, "role": "windows-wsl-host", "image_scope": "system32", + "exact_job_membership_verified": True, "membership_query_error": 0, + "image_query_error": 0, "exit_query_error": 0, + "exited_after_cleanup": True, "exit_code_after_cleanup": 99} + + +class CleanupPolicyTests(unittest.TestCase): + def test_image_query_failure_is_not_failure_to_reap_an_owned_fixture_tree(self): + # Sanitized facts from CI 40265785235: these are policy inputs only. + # No hosted environment is exported and no WSL command runs here. + members = [{**known_host(), "member": index, "role": "unknown", "image_scope": "unverified", + "membership_query_error": 0, "image_query_error": 31, + "exit_query_error": 0} for index in range(2)] + cleanup = {"root_natural_exit": True, "root_reaped": True, "root_exit_code": 0, + "root_direct_kill_requested": False, "membership_stable": True, + "job_before": {"active": 2, "total": 26, "terminated": 0}, + "job_after": {"active": 0, "total": 26, "terminated": 0}, "job_after_pid_count": 0, + "job_termination_requests": 1, "member_cleanup_requested_count": 2, "members": members} + self.assertTrue(helper_cleanup_accepted("check-before", True, cleanup)) + self.assertTrue(helper_cleanup_accepted("check-after", True, cleanup)) + self.assertEqual([member["role"] for member in cleanup["members"]], ["unknown", "unknown"]) + self.assertEqual([member["image_query_error"] for member in cleanup["members"]], [31, 31]) + for field, value in (("membership_query_error", 5), ("exit_query_error", 5), + ("exact_job_membership_verified", False), ("exited_after_cleanup", False)): + with self.subTest(field=field): + bad = copy.deepcopy(cleanup); bad["members"][0][field] = value + self.assertFalse(helper_cleanup_accepted("check-before", True, bad)) + + def test_image_name_alone_never_qualifies_an_unknown_helper(self): + for image in (r"D:\synthetic\wslhost.exe", r"C:\Windows\System32\init", + r"C:\Windows\System32\wsl.exe", r"D:\synthetic\init"): + self.assertNotEqual(image_role(image, r"C:\Windows\System32", r"C:\Program Files")[0], + "windows-wsl-host") + self.assertEqual(image_role(r"C:\Windows\System32\wslhost.exe", r"C:\Windows\System32", + r"C:\Program Files"), ("windows-wsl-host", "system32")) + + def test_only_owned_and_reaped_check_trees_can_use_fixture_cleanup_policy(self): + good = {"root_natural_exit": True, "root_reaped": True, "root_exit_code": 0, + "root_direct_kill_requested": False, "member_cleanup_requested_count": 1, + "membership_stable": True, "job_before": {"active": 1, "total": 3, "terminated": 0}, + "job_after": {"active": 0, "total": 3, "terminated": 0}, "job_after_pid_count": 0, + "job_termination_requests": 1, "members": [known_host()]} + self.assertTrue(helper_cleanup_accepted("check-before", True, good)) + self.assertTrue(helper_cleanup_accepted("check-after", True, good)) + for field, value in (("root_natural_exit", False), ("root_reaped", False), ("root_exit_code", 99), + ("root_direct_kill_requested", True), ("member_cleanup_requested_count", 0), + ("membership_stable", False), ("job_after_pid_count", 1), + ("job_termination_requests", 2), ("members", []), ("job_after", {"active": 1})): + with self.subTest(field=field): + bad = copy.deepcopy(good); bad[field] = value + self.assertFalse(helper_cleanup_accepted("check-before", True, bad)) + for field, value in (("exact_job_membership_verified", False), ("membership_query_error", 5), + ("exit_query_error", 5), + ("exited_after_cleanup", False), ("exit_code_after_cleanup", None)): + with self.subTest(member_field=field): + bad = copy.deepcopy(good); bad["members"][0][field] = value + self.assertFalse(helper_cleanup_accepted("check-before", True, bad)) + self.assertFalse(helper_cleanup_accepted("check-before", False, good)) + for step in ("preflight", "download", "import"): + self.assertFalse(helper_cleanup_accepted(step, True, good)) + + def fixture_run(self, module=prep, role="windows-wsl-host", root_code=0): + process = mock.Mock(pid=123, returncode=root_code) + process.poll.return_value = root_code + process.communicate.return_value = (b'{"result":"ubuntu-sh-cwd-verified"}', b"") + job = mock.Mock() + job.drain.side_effect = [{"active": 1, "total": 3, "terminated": 0}, + {"active": 0, "total": 3, "terminated": 0}] + audit = mock.Mock(stable=True, members=[{**known_host(), "role": role}]) + audit.private = [{"member": 0, "pid": 456, "image": "synthetic-not-executed"}] + audit.process_ids.return_value = [] + with tempfile.TemporaryDirectory() as temporary, \ + mock.patch.object(module, "OwnedJob", return_value=job), \ + mock.patch.object(module, "JobMembers", return_value=audit, create=True), \ + mock.patch.object(module.subprocess, "CREATE_NO_WINDOW", 0, create=True), \ + mock.patch.object(module.subprocess, "Popen", return_value=process): + result = module.run_step("check-before", Path(temporary)) + return result, job + + def test_real_run_step_seam_distinguishes_worker_success_and_cleanup(self): + result, job = self.fixture_run() + self.assertEqual(result["status"], "passed") + self.assertEqual(result["cleanup"]["root_exit_code"], 0) + self.assertEqual(result["cleanup"]["job_before"]["active"], 1) + self.assertEqual(result["cleanup"]["job_after"]["active"], 0) + self.assertEqual(result["cleanup"]["helper_cleanup_policy"], "owned-disposable-wsl-check-tree-v1") + job.terminate.assert_called_once() + unknown, _ = self.fixture_run(role="unknown") + self.assertEqual(unknown["status"], "passed") + self.assertEqual(unknown["cleanup"]["members"][0]["role"], "unknown") + failed_worker, _ = self.fixture_run(root_code=7) + self.assertEqual(failed_worker["status"], "failed") + + +@unittest.skipUnless(os.name == "nt", "read-only owned Job identity API requires Windows") +class OwnJobApiTests(unittest.TestCase): + def test_ordinary_owned_python_helper_is_audited_unknown_and_reaped(self): + job = prep.OwnedJob() + process = audit = None + readiness = tempfile.TemporaryDirectory(prefix="termforge-job-ready-") + try: + script = ("import subprocess,sys,time\nfrom pathlib import Path\n" + "assert sys.stdin.buffer.readline()==b'GO\\n'\n" + "child = 'import sys,time; from pathlib import Path; '\n" + "child += 'Path(sys.argv[1]).write_bytes(bytes([82,69,65,68,89])); time.sleep(30)'\n" + "subprocess.Popen([sys.executable,'-c',child,sys.argv[1]]," + "stdin=subprocess.DEVNULL,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)\n" + "ready=Path(sys.argv[1]); deadline=time.monotonic()+4\n" + "while not ready.exists() and time.monotonic()