From 30283c290e4971638168ff14a2f16776c384c46e Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Mon, 26 Oct 2026 18:04:16 +0800 Subject: [PATCH] research: freeze native margin contract and upstream patch candidate --- CMakeLists.txt | 1 + docs/EVIDENCE.md | 1 + docs/research/conpty-native-margin-path.md | 101 ++++++ experiments/conpty/README.md | 3 + experiments/conpty/margin_contract.py | 101 ++++++ .../validation.json | 323 ++++++++++++++++++ experiments/conpty/test_margin_contract.py | 66 ++++ .../conpty/upstream/MICROSOFT-LICENSE.txt | 21 ++ experiments/conpty/upstream/README.md | 38 +++ .../dc8-native-margin-candidate.patch | 48 +++ 10 files changed, 703 insertions(+) create mode 100644 docs/research/conpty-native-margin-path.md create mode 100644 experiments/conpty/margin_contract.py create mode 100644 experiments/conpty/results/2026-10-26-native-margin-path/validation.json create mode 100644 experiments/conpty/test_margin_contract.py create mode 100644 experiments/conpty/upstream/MICROSOFT-LICENSE.txt create mode 100644 experiments/conpty/upstream/README.md create mode 100644 experiments/conpty/upstream/dc8-native-margin-candidate.patch diff --git a/CMakeLists.txt b/CMakeLists.txt index e864d422..82c26f37 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -291,6 +291,7 @@ if(BUILD_TESTING) termforge_add_python_test(recovery -m unittest experiments.core.test_recovery -v) termforge_add_python_test(measure -m unittest experiments.measure.test_measure -v) termforge_add_python_test(binary_paths -m unittest experiments.test_build_paths -v) + termforge_add_python_test(conpty_margin_contract -m unittest experiments.conpty.test_margin_contract -v) termforge_add_python_test(benchmark_tools -m unittest discover -s benchmarks/tests -v) termforge_add_python_test(differential -m unittest discover -s experiments/differential -p test_*.py -v) termforge_add_python_test(ime_inventory -m unittest research.ime.test_hosted_inventory research.ime.test_prepare_hosted research.ime.test_product_ime -v) diff --git a/docs/EVIDENCE.md b/docs/EVIDENCE.md index c8054fe6..3c0d7b9c 100644 --- a/docs/EVIDENCE.md +++ b/docs/EVIDENCE.md @@ -44,6 +44,7 @@ R1 固定总出口的源码为 `ce9b24d`,早期 `2dc8d6d` / `9b16acc` 仍分 | controller B 正常结束后 cmd 退出未验收(#44) | [原失败脱敏记录](../experiments/shell/results/2026-10-26-controller-exit/validation.json)、[新退出诊断及有界重放](../experiments/shell/results/2026-10-26-controller-exit-observation/validation.json)、[关闭合同](https://github.com/JavaZeroo/termforge/issues/44) | 原 child 收完 32B、退出 0/join/Job0;cmd 只留下 close 后 0xC000013A,close 前未知。新观察分列输入、wait、pre-close 与清理;多轮未复现不构成历史修复,继续阻塞 #38。 | | 原 10 秒纯夹具超时的阶段保留(#40 未关闭) | [限定合同](research/inventory-timeout-checkpoints.md)、[正常与受控超时对照](../research/ime/results/2026-10-26-inventory-timeout-checkpoints/validation.json) | 保持原 10 秒期限;超时后保留有界阶段、原异常和回收。受控挂起对照已通过,但历史 CI 的阶段/根因仍未知,#40 不因此关闭。 | | 纯 AST 夹具的生命周期记录 | [#40](https://github.com/JavaZeroo/termforge/issues/40)、[计时、进度与回收验证](../research/ime/results/2026-10-26-inventory-lifecycle/validation.json)、[专用 CI 40253727745](https://github.com/JavaZeroo/termforge/actions/runs/40253727745) | 旧 10 秒 AST 夹具及专用 CI 记录启动、AST 和 Job0;原超时根因未确定。诊断增强见 #40 阶段保留,不由一次 CI 成功结项。 | +| 原生 margin 逐字段合同与修复候选(#56 仍开放) | [合同、源码与路径](research/conpty-native-margin-path.md)、[独立 oracle](../experiments/conpty/margin_contract.py)、[固定运行](../experiments/conpty/results/2026-10-26-native-margin-path/validation.json) | 四阶段文字/格属性/光标独立判据;系统与官方原生 final 仍红,官方80列两次原5秒生命周期失败保留。共享单 margin 源码支持小 MIT fork 候选;patch 仅可应用,未编译、未运行、不计修复。 | | ConPTY 系统/固定官方后端的同源 margin A/B(#41、#47 未关闭) | [合同与第一方来源](research/conpty-margin-backends.md)、[固定 175B 与 16 case 结果](../experiments/conpty/results/2026-10-26-margin-backends/validation.json) | 相同 175B、两网格/两 mode/两 fresh:系统末端 margin 错,固定官方后端的末端正确;CHAR_INFO 仍错。禁用 VT 由窄屏反例拒绝;仅诊断可行性,不证明 PR #50 或 main 联合整合通过。 | | 工作区几何与交互的固定记录 | [#38](https://github.com/JavaZeroo/termforge/issues/38)、[工作区几何与输出验证](../experiments/shell/results/2026-10-26-workspace-geometry/validation.json)、[resize 限定证据](../experiments/core/results/2026-10-26-resize-replay/validation.json) | 固定 27f376d 的客户区/布局/输出、默认/候选恢复及窄屏记录;其完整回归仍有 #36/#38 红项,不代表当前所有配置或 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) | 短窗口保留相同选择断言;仅指定一次性 hosted VM 准备固定 Ubuntu。夹具强制收尾与产品自然退出分列,未知归属/回收仍失败。 | diff --git a/docs/research/conpty-native-margin-path.md b/docs/research/conpty-native-margin-path.md new file mode 100644 index 00000000..f144898f --- /dev/null +++ b/docs/research/conpty-native-margin-path.md @@ -0,0 +1,101 @@ +# 保留原生与末端 margin 合同的实现路径 + +[#56](https://github.com/JavaZeroo/termforge/issues/56) 的当前交付是可执行的逐字段原生 oracle、固定上游的补丁候选及下一次构建入口。 +**原生缺口仍存在,尚无经过真实运行证明的完整修复路径。** 补丁仅检查可应用,没有编译或替换任何后端。 +这保留 [#41](https://github.com/JavaZeroo/termforge/issues/41) 的原合同与开放状态,不改变 #34 产品决定。 + +## 逐字段合同与测量边界 + +Microsoft [每缓冲区 scrolling margins 声明](https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences#scrolling-margins) +要求主/备用屏分别持有区域。`CHAR_INFO` 同时包含字符和属性; +[ReadConsoleOutput](https://learn.microsoft.com/en-us/windows/console/readconsoleoutput) 读的是调用应用所在控制台的矩形, +[GetConsoleScreenBufferInfo](https://learn.microsoft.com/en-us/windows/console/getconsolescreenbufferinfo) 给出该控制台的网格、窗口和光标。 +这些 API 没有直接返回 DECSTBM 状态的字段,必须以区域内实际滚动和区域外保持来验证;末端正确无法代替调用应用的原生状态。 + +| 必须保留的判据 | 原 175B 能证明什么 | 必须另做的 R2 交叉 | +|---|---|---| +| 主屏 `2;3r`,备用屏 `4;5r` 分属各屏 | 四阶段精确文字与逐格 `CHAR_INFO`;返回主屏继续滚动,区域外 `TOP_FIXED` / `BOTTOM_FIXED` 保持 | 重复切换、默认区域、origin/wrap/pending-wrap 与保存恢复 | +| 主屏返回与光标 | `restored` 文字/属性与 `before` 一致;下表固定光标逐项成立 | 非零起点、光标可见性/形状、应用查询与 API 写入交叉 | +| 属性保留 | 原 source 不设置 SGR;所有格保持初始捕获属性,包括滚动空白格。当前实际初始值为 7 | 明确 SGR/色彩、erase 属性、主/备用屏样式恢复;本夹具不能证明这些变体 | +| 宽度与边界 | `80×24`、`12×7`;窄网格结束标记分成两行,不裁剪尾部取得通过 | 宽字符、极窄/极短、不断 resize、区域边界随尺寸变化 | +| 历史与末端 | 原运输完整保留供独立候选回放;源 API 与末端分别判据 | 主/备用屏历史隔离与淘汰、resize/reflow/选择/Find;原 175B 无足够历史压力 | +| 生命周期 | 原 5 秒 child wait、15 秒外部期限、共享 5 秒 cleanup;自然退出 37、EOF 109、自有内外 Job0、无 force | 最终 Session/GUI/应用矩阵及长期资源 | + +原 source SHA-256 为 `01878a5d54c343007309abb89faeb90c89262a8b55e420c5f267342ed9b27e59`,175 字节不变。 +新增 [独立 oracle](../../experiments/conpty/margin_contract.py) 直接使用以下固定结果,不调用 TermForge parser 推导预期。 +行/列均从零计;未列行为空,`12×7` 的最后标记分为 `TF_MARGIN_DO` / `NE` 两行: + +| 阶段 | 非空行与位置 | 光标 `(row,column)` | +|---|---|---| +| before | 0 `TOP_FIXED`;1 `REGION_A`;2 `REGION_B`;3 `BOTTOM_FIXED` | `(0,0)` | +| alt | 0 `ALT_OUTSIDE`;3 `ALT_C`;4 `ALT_D` | `(4,5)` | +| restored | 与 before 相同 | `(0,0)` | +| final | 0 `TOP_FIXED`;1 `REGION_C`;2 `REGION_D`;3 `BOTTOM_FIXED`;5 起结束标记 | `(6,0)` | + +oracle 同时比较两个原生读入口的文字、完整格/属性和光标。初始颜色由原夹具外部默认决定,因此只验证其跨阶段保持, +不把某次默认颜色推为所有宿主的硬合同。历史、resize、改变样式和末端各阶段光标仍明确列为未测。 + +## 本批固定运行与失败保留 + +[白名单证据](../../experiments/conpty/results/2026-10-26-native-margin-path/validation.json) 固定 main `3d6d44d` 的未修改原生探针、 +单独冻结的候选 `.4` trace、官方同包 DLL/EXE 双 SHA、源码、flags、期限与生命周期;原件只留 `local/`。 +这是源头诊断配置,**不是 #55 的联合产品验收**。本批官方探针 flags 为 0,不能替代 Draft 产品的 Console-width flags 24。 + +| 样本 | 原生与末端结果 | 生命周期 | +|---|---|---| +| 系统 `80×24` / `12×7` VT-on | 原生 before/alt/restored 的全部检查正确,final 文字/格错误而光标正确;末端同样错误 | 均自然 37、EOF109、Job0,无 force | +| 官方 1.25 `12×7` VT-on 补充批 | 原生 final 仍为 `REGION_CIXED` 等错误;末端正确 | 自然 37、EOF109、Job0,无 force | +| 官方 `80×24` VT-on 初始及有界补充各一次 | 两次均未产生四阶段状态,语义结果 unknown | 原 5 秒 wait=false;close 后观察到 `0xC000013A`,不能冒充自然退出;EOF109、外 Job0,无 force | + +系统的 VT-off 两个原反例也随原运行入口保留,不计 VT-on 合同成功。 +首次误选自研 C++ trace 导致 replay 拒绝的配置错误批原样私存并排除;后续补充与原失败分别列出,没有删除失败或延长预算取绿。 +官方 80 列生命周期失败的原因尚未定位,不能据此推出每屏 margin 的原因。 + +## 固定上游的可证伪原因 + +核对对应官方包的提交 `dc8ae365847d41f62d1ebca93815bb00959bc12f`: + +- [`SCREEN_INFORMATION::_CreateAltBuffer`](https://github.com/microsoft/terminal/blob/dc8ae365847d41f62d1ebca93815bb00959bc12f/src/host/screenInfo.cpp#L1787) + 删除新备用屏自己的 state machine,再共享主屏 `_stateMachine`。 +- 同一 [`AdaptDispatch`](https://github.com/microsoft/terminal/blob/dc8ae365847d41f62d1ebca93815bb00959bc12f/src/terminal/adapter/adaptDispatch.hpp#L321) + 有两个保存光标状态,但只有一个 `_scrollMargins`; + [`_SetAlternateScreenBufferMode`](https://github.com/microsoft/terminal/blob/dc8ae365847d41f62d1ebca93815bb00959bc12f/src/terminal/adapter/adaptDispatch.cpp#L1741) + 未保存、重置、恢复该字段,DECSTBM 则写这个字段。 +- [原生 GetSet](https://github.com/microsoft/terminal/blob/dc8ae365847d41f62d1ebca93815bb00959bc12f/src/host/outputStream.cpp#L198) + 负责实际 buffer 切换;原生四阶段与末端可处在不同的状态实现中。 + +**推断:** 备用屏写入 `4;5r` 后,共享字段继续影响恢复的主屏,能解释已见 final 损坏。 +这是固定源码和运行现象共同支持的待证伪原因,尚缺仅改变该状态归属的真实 red→green。 +Microsoft 文档仍声明 per-buffer;目前没有上游确认允许把这种差异当成设计豁免,也没有证据证明其他上游版本已修复。 + +## 路径比较与具体下一步 + +| 路径 | 当前证据与边界 | 维护和采用条件 | +|---|---|---| +| 上游修复后固定正式包 | 最优先的长期分发方式;当前官方包 native 仍红,尚无通过的替代包 | 将独立原 fixture/oracle 和最小修复送审;上游接纳与发布仍未知,采用前按完整合同复验 | +| 小型 MIT fork | [候选 patch 与上游测试](../../experiments/conpty/upstream/README.md) 保存隐藏主屏 margin,新备用屏默认区域,返回恢复;只验证可以应用到完整固定文件 | 未编译/未运行,不得结项。隔离构建 adapter tests、OpenConsole 与配套 DLL;新来源清单/双文件 SHA、显式配置、同 API 族/模块生命期、原矩阵及负例通过后才讨论整合 | +| 直接修改 TermForge 末端或 VT-off | 末端不能修改子进程已见的 `CHAR_INFO`;VT-off 的原生文字和窄屏反例已否定其作为一般 raw 后端 | 无原生合同修复证据,不作为通过路径;不猜补原运输没有的控制序列 | +| 替换为另一 console server/桥接层 | 尚无同输入的原生与末端合格证据;重新实现 Console API/ConDrv/VT 同步的维护面远大于局部上游状态修正 | 必须先证明支持 API/Windows 边界、许可、部署与回收,不能只根据库存在选择 | + +构建入口、原源 SHA、许可、API 生命期、负例和升级维护步骤在 [patch README](../../experiments/conpty/upstream/README.md) 固定。 +下次先完成上游 adapter 测试和独立同源 DLL/EXE 构建,再为新 fork 身份增加审查入口;当前官方身份校验保持拒绝未知文件。 +运行时先保持原175B、四阶段和期限,检查原生、运输与候选各层 red→green,再执行样式、历史与 resize 交叉。 + +公开 API 可用与产品支持范围分开:系统 CreatePseudoConsole 文档的 Windows 10 1809 下限, +不能证明新 DLL/EXE 或其 fork 在全部该范围可运行;固定上游 README 的 Windows Terminal 19041 要求也不能直接作为 ConPTY 包下限。 +本次只有记录的 Windows 11 本机结果;完整最低系统、服务器和更新策略仍需 #34 定案及实际验证。 + +因此 #56、#41、#27、#29、#34 继续开放;#47/#55 的产品整合必须使用最终联合源码和实际后端重验, +本批不声明真实显示、长期资源、WindTerm 运行等价或性能结论。 + +## 复验入口 + +先沿 [原监督器](conpty-margin-backends.md#复现与剩余工作) 产生全新私有目录,再离线执行原生 oracle: + +```powershell +python -m experiments.conpty.margin_contract /_private/80x24-vt-on --columns 80 --rows 24 +python -m unittest experiments.conpty.test_margin_contract -v +``` + +当前 native final 错误时第一条返回 1,这是保留的反例。纯测试包含旧 `REGION_CIXED`、逐阶段属性、光标、 +第二读入口和缺失状态损坏的拒绝检查;它们不调用真实控制台,也不能替代 fork 运行验证。 diff --git a/experiments/conpty/README.md b/experiments/conpty/README.md index 1ca6743e..7cbd8f67 100644 --- a/experiments/conpty/README.md +++ b/experiments/conpty/README.md @@ -68,3 +68,6 @@ python -m unittest experiments.conpty.test_ctrl_c -v ## 每屏 margin 与可替换后端诊断 `conpty_margin_probe` / `experiments.conpty.margin_run` 对同一固定175字节源检查四阶段真实 console capture、ConPTY EOF和候选末端。标准系统后端与固定官方 side-by-side ConPTY 的差异、关闭VT处理的窄网格反例、包身份及复现命令见[后端调查](../../docs/research/conpty-margin-backends.md)。这里只增加有界诊断,产品默认后端未改变;原生源控制台与末端结果分列,不能据末端正确关闭 #41 或宣布 R2 通过。 + +`python -m experiments.conpty.margin_contract --columns 80 --rows 24` 离线检查原175B的四阶段文字、逐格属性和光标; +已知 native final 损坏会返回1。逐字段合同、保留的生命周期失败、固定上游原因和**未编译/未验证**的小补丁候选见[原生处理路径](../../docs/research/conpty-native-margin-path.md)。 diff --git a/experiments/conpty/margin_contract.py b/experiments/conpty/margin_contract.py new file mode 100644 index 00000000..e4334937 --- /dev/null +++ b/experiments/conpty/margin_contract.py @@ -0,0 +1,101 @@ +"""Offline, independent native contract for the unchanged 175-byte margin probe. + +This reads captured evidence; it never launches or changes a console. A passed +fixture is narrower than the full R2 margin, style, history and resize contract. +""" +from __future__ import annotations + +import argparse +import hashlib +import json +from pathlib import Path + +from experiments.conpty.margin_run import SOURCE, SOURCE_SHA256 +from experiments.shell.workflow_checks import validate_console_window + + +STAGES = ("before", "alt", "restored", "final") + + +def expected_native(columns, rows): + """Literal outcomes of the four fixed stages, independent of any parser.""" + if (columns, rows) not in ((80, 24), (12, 7)): + raise ValueError("fixed native grid required") + main = ["TOP_FIXED", "REGION_A", "REGION_B", "BOTTOM_FIXED"] + alternate = ["ALT_OUTSIDE", "", "", "ALT_C", "ALT_D"] + final = ["TOP_FIXED", "REGION_C", "REGION_D", "BOTTOM_FIXED", ""] + final += (["TF_MARGIN_DONE"] if columns == 80 else ["TF_MARGIN_DO", "NE"]) + # DECSTBM homes the cursor. The final CR/LF outside the region clamps to + # the physical bottom at height 7; it does not scroll the margin or screen. + return { + stage: {"lines": lines + [""] * (rows - len(lines)), "cursor": cursor} + for stage, lines, cursor in ( + ("before", main, [0, 0]), + ("alt", alternate, [4, 5]), + ("restored", main, [0, 0]), + ("final", final, [6, 0]), + ) + } + + +def evaluate_native(stages, columns, rows): + expected = expected_native(columns, rows) + if set(stages) != set(STAGES): + raise ValueError("four native stages required") + for capture in stages.values(): + if (capture["columns"], capture["rows"]) != (columns, rows): + raise ValueError("native grid changed") + # The original source does not select SGR or a palette. Preserve the + # initially observed attribute, including blank cells, rather than claim + # that an unchosen color is a portable expected default. + attribute = stages["before"]["cells"][0][0][1] + results = {} + for stage, capture in stages.items(): + target = expected[stage] + wanted_cells = [[(ord(character), attribute) + for character in line.ljust(columns)] + for line in target["lines"]] + fields = { + "text": capture["lines"] == target["lines"], + "cell_text": capture["cell_lines"] == target["lines"], + "cells_and_attributes": capture["cells"] == wanted_cells, + "cursor": [capture["cursor_row"] - capture["top"], + capture["cursor_column"]] == target["cursor"], + } + results[stage] = dict(fields, passed=all(fields.values())) + return { + "schema": "termforge-native-margin-contract-v1", + "columns": columns, "rows": rows, + "source_sha256": SOURCE_SHA256, + "initial_native_attribute": attribute, + "stages": results, + "all_native_stages_passed": all(value["passed"] for value in results.values()), + "unmeasured": ["styled_source", "history", "resize", "terminal_stage_cursors"], + "full_r2_contract_verified": False, + } + + +def inspect_native(directory, columns, rows): + directory = Path(directory) + source = (directory / "source.bin").read_bytes() + if source != SOURCE or hashlib.sha256(source).hexdigest() != SOURCE_SHA256: + raise ValueError("original source changed") + stages = {stage: validate_console_window(directory, expected_role=stage, + expected_generation=1, prefix=stage) + for stage in STAGES} + return evaluate_native(stages, columns, rows) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("directory", type=Path) + parser.add_argument("--columns", type=int, required=True) + parser.add_argument("--rows", type=int, required=True) + args = parser.parse_args() + result = inspect_native(args.directory, args.columns, args.rows) + print(json.dumps(result, indent=2)) + return int(not result["all_native_stages_passed"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/experiments/conpty/results/2026-10-26-native-margin-path/validation.json b/experiments/conpty/results/2026-10-26-native-margin-path/validation.json new file mode 100644 index 00000000..18ef1660 --- /dev/null +++ b/experiments/conpty/results/2026-10-26-native-margin-path/validation.json @@ -0,0 +1,323 @@ +{ + "schema": "termforge-native-margin-path-v1", + "source_commit": "3d6d44d3b3e9a9a3e41da98e8711e9f19a211fd8", + "host": { + "system": "Windows", + "version": "10.0.26200", + "machine": "AMD64" + }, + "probe_source_unchanged": true, + "flags": 0, + "child_wait_ms": 5000, + "outer_timeout_s": 15, + "cleanup_budget_s": 5, + "probe_binary_sha256": "82bd95882be25b304a04ec835e77e6dfac02b8d44c1daf71fed2295f2c024be5", + "candidate_trace_binary_sha256": "6dbe42cfefc8b5df64e652193d3b5863fbf679f12ccfa2e422d9ca1e9e1a5c3d", + "candidate_trace_identity": "0.26.1-dev+termforge.4 (separate frozen build, not joint product acceptance)", + "package": { + "name": "Microsoft.Windows.Console.ConPTY", + "version": "1.25.260930003", + "url": "https://github.com/microsoft/terminal/releases/download/v1.25.2733.0/Microsoft.Windows.Console.ConPTY.1.25.260930003.nupkg", + "bytes": 1750533, + "sha256": "6d6f8b008c655d814c498d8205d4012dcd4d0b6dfc1096461d0a185d1e3caf03", + "files": { + "conpty.dll": "feeef341d891643c62d30b6b07800bc70f0bc148f44cb8c3bee84aa557ae805a", + "OpenConsole.exe": "3d66b23d0a71bb8eed2b77edc8b9df9bf54ce6c8fb74c863a30e760997f80586" + } + }, + "upstream_commit": "dc8ae365847d41f62d1ebca93815bb00959bc12f", + "upstream_source_sha256": { + "src/host/screenInfo.cpp": "fced7e15d63ce8e7cf791d09fd1ba57a46a7bf65430e8770a372340650b483b0", + "src/host/screenInfo.hpp": "a6ea94d71190d758429a8684887d5a60809cee1b5da4c7364facebfe9cfb003c", + "src/host/outputStream.cpp": "b83df3a1dc7f9aa80dad7d5b0d5de242f947e866cfa3b8ab69fdbefbc724181e", + "src/terminal/adapter/adaptDispatch.cpp": "8a3efbcb63c1023dc138e49aed90eeb36ac75f67de1a348ab969440894813560", + "src/terminal/adapter/adaptDispatch.hpp": "db406433e54c4b8857da7aa686ee4cc481e419b4f5b735b742dca5804e00939f", + "src/terminal/adapter/ut_adapter/adapterTest.cpp": "fb622455a23bd6e86a68c648662019c2facac2212c132af386837add82af3489", + "src/winconpty/winconpty.h": "61b125f4a7111b73a28a1d65fb0cfe1748b701707ff2ceda27590aa5af01b840", + "LICENSE": "5d177f23ecfeb0ea8e050b6a5a16355e1ae9a0b286436ca8f83ed08b3795be6b" + }, + "patch": { + "path": "experiments/conpty/upstream/dc8-native-margin-candidate.patch", + "sha256": "dc27377e399bd0217a0b76e16cc89f087c1b781929e4c958ad30c3a475322b81", + "application_check_passed": true, + "compiled": false, + "upstream_test_executed": false, + "runtime_verified": false, + "application_mode": "git apply --unidiff-zero --check" + }, + "samples": [ + { + "backend": "official-flags0", + "grid": "80x24", + "batch": "initial", + "status": "failed", + "failure_code": "native_report", + "native_contract": null, + "host_exit_observation": { + "waited": false, + "exit_code": 3221225786, + "read_error": 109, + "capture_overflow": false, + "forced_reader_cancel": false, + "forced_process_cancel": false, + "cleanup_complete": true, + "active_children_after": 0 + }, + "outer_job_after": { + "active": 0, + "total": 4, + "terminated": 0 + }, + "forced_outer_cleanup": false + }, + { + "backend": "system", + "grid": "12x7", + "status": "completed", + "failure_code": null, + "lifecycle": { + "waited": true, + "exit_code": 37, + "read_error": 109, + "capture_overflow": false, + "forced_reader_cancel": false, + "forced_process_cancel": false, + "cleanup_complete": true, + "active_children_after": 0 + }, + "forced_outer_cleanup": false, + "outer_job_after": { + "active": 0, + "total": 4, + "terminated": 0 + }, + "process_reaped": true, + "streams_drained": true, + "native_contract": { + "schema": "termforge-native-margin-contract-v1", + "columns": 12, + "rows": 7, + "source_sha256": "01878a5d54c343007309abb89faeb90c89262a8b55e420c5f267342ed9b27e59", + "initial_native_attribute": 7, + "stages": { + "before": { + "text": true, + "cell_text": true, + "cells_and_attributes": true, + "cursor": true, + "passed": true + }, + "alt": { + "text": true, + "cell_text": true, + "cells_and_attributes": true, + "cursor": true, + "passed": true + }, + "restored": { + "text": true, + "cell_text": true, + "cells_and_attributes": true, + "cursor": true, + "passed": true + }, + "final": { + "text": false, + "cell_text": false, + "cells_and_attributes": false, + "cursor": true, + "passed": false + } + }, + "all_native_stages_passed": false, + "unmeasured": [ + "styled_source", + "history", + "resize", + "terminal_stage_cursors" + ], + "full_r2_contract_verified": false + }, + "transport_bytes": 431, + "transport_sha256": "cdc049f548b0b864cb2f2214ecf7fb02ad9c46e22d30cd8bd933625651efe963", + "terminal_main_margins_restored": false + }, + { + "backend": "system", + "grid": "80x24", + "status": "completed", + "failure_code": null, + "lifecycle": { + "waited": true, + "exit_code": 37, + "read_error": 109, + "capture_overflow": false, + "forced_reader_cancel": false, + "forced_process_cancel": false, + "cleanup_complete": true, + "active_children_after": 0 + }, + "forced_outer_cleanup": false, + "outer_job_after": { + "active": 0, + "total": 4, + "terminated": 0 + }, + "process_reaped": true, + "streams_drained": true, + "native_contract": { + "schema": "termforge-native-margin-contract-v1", + "columns": 80, + "rows": 24, + "source_sha256": "01878a5d54c343007309abb89faeb90c89262a8b55e420c5f267342ed9b27e59", + "initial_native_attribute": 7, + "stages": { + "before": { + "text": true, + "cell_text": true, + "cells_and_attributes": true, + "cursor": true, + "passed": true + }, + "alt": { + "text": true, + "cell_text": true, + "cells_and_attributes": true, + "cursor": true, + "passed": true + }, + "restored": { + "text": true, + "cell_text": true, + "cells_and_attributes": true, + "cursor": true, + "passed": true + }, + "final": { + "text": false, + "cell_text": false, + "cells_and_attributes": false, + "cursor": true, + "passed": false + } + }, + "all_native_stages_passed": false, + "unmeasured": [ + "styled_source", + "history", + "resize", + "terminal_stage_cursors" + ], + "full_r2_contract_verified": false + }, + "transport_bytes": 631, + "transport_sha256": "6f6377c54ce2c83f0c3f4a3e9f581cdf44798791dd207c05efe7bfa346ede7a2", + "terminal_main_margins_restored": false + }, + { + "backend": "official-flags0", + "grid": "12x7", + "status": "completed", + "failure_code": null, + "lifecycle": { + "waited": true, + "exit_code": 37, + "read_error": 109, + "capture_overflow": false, + "forced_reader_cancel": false, + "forced_process_cancel": false, + "cleanup_complete": true, + "active_children_after": 0 + }, + "forced_outer_cleanup": false, + "outer_job_after": { + "active": 0, + "total": 4, + "terminated": 0 + }, + "process_reaped": true, + "streams_drained": true, + "native_contract": { + "schema": "termforge-native-margin-contract-v1", + "columns": 12, + "rows": 7, + "source_sha256": "01878a5d54c343007309abb89faeb90c89262a8b55e420c5f267342ed9b27e59", + "initial_native_attribute": 7, + "stages": { + "before": { + "text": true, + "cell_text": true, + "cells_and_attributes": true, + "cursor": true, + "passed": true + }, + "alt": { + "text": true, + "cell_text": true, + "cells_and_attributes": true, + "cursor": true, + "passed": true + }, + "restored": { + "text": true, + "cell_text": true, + "cells_and_attributes": true, + "cursor": true, + "passed": true + }, + "final": { + "text": false, + "cell_text": false, + "cells_and_attributes": false, + "cursor": true, + "passed": false + } + }, + "all_native_stages_passed": false, + "unmeasured": [ + "styled_source", + "history", + "resize", + "terminal_stage_cursors" + ], + "full_r2_contract_verified": false + }, + "transport_bytes": 218, + "transport_sha256": "d793c84c56624d6c647e562ef7ba35e21d8998baa43371cb36f95b2add684246", + "terminal_main_margins_restored": true + }, + { + "backend": "official-flags0", + "grid": "80x24", + "status": "failed", + "failure_code": "native_report", + "lifecycle": null, + "forced_outer_cleanup": false, + "outer_job_after": { + "active": 0, + "total": 4, + "terminated": 0 + }, + "process_reaped": true, + "streams_drained": true, + "native_contract": null, + "transport_bytes": 218, + "host_exit_observation": { + "waited": false, + "exit_code": 3221225786, + "read_error": 109, + "capture_overflow": false, + "forced_reader_cancel": false, + "forced_process_cancel": false, + "cleanup_complete": true, + "active_children_after": 0 + } + } + ], + "configuration_error_batch": { + "preserved_privately": true, + "reason": "default C++ trace used instead of Rust candidate; replay rejected, excluded from semantic samples" + }, + "full_r2_contract_verified": false, + "issue56_complete": false +} diff --git a/experiments/conpty/test_margin_contract.py b/experiments/conpty/test_margin_contract.py new file mode 100644 index 00000000..7b13ece5 --- /dev/null +++ b/experiments/conpty/test_margin_contract.py @@ -0,0 +1,66 @@ +import copy +import unittest + +from experiments.conpty.margin_contract import evaluate_native, expected_native + + +def captures(columns, rows): + result = {} + for name, target in expected_native(columns, rows).items(): + result[name] = dict(columns=columns, rows=rows, top=0, + cursor_row=target["cursor"][0], + cursor_column=target["cursor"][1], + lines=list(target["lines"]), cell_lines=list(target["lines"]), + cells=[[(ord(c), 7) for c in line.ljust(columns)] + for line in target["lines"]]) + return result + + +class NativeMarginContractTests(unittest.TestCase): + def test_original_results_and_wrapped_marker_have_independent_literals(self): + for columns, rows in ((80, 24), (12, 7)): + result = evaluate_native(captures(columns, rows), columns, rows) + self.assertTrue(result["all_native_stages_passed"]) + self.assertFalse(result["full_r2_contract_verified"]) + self.assertEqual(expected_native(12, 7)["final"]["lines"], + ["TOP_FIXED", "REGION_C", "REGION_D", "BOTTOM_FIXED", "", + "TF_MARGIN_DO", "NE"]) + + def test_historical_margin_corruption_is_not_accepted(self): + data = captures(80, 24) + # Observed system/official native failure, not a different expected state. + data["final"]["lines"][:5] = ["TOP_FIXED", "REGION_A", "REGION_B", + "REGION_CIXED", "REGION_D"] + result = evaluate_native(data, 80, 24) + self.assertFalse(result["stages"]["final"]["text"]) + self.assertFalse(result["all_native_stages_passed"]) + + def test_each_stage_rejects_attribute_cursor_and_secondary_read_corruption(self): + for stage in ("before", "alt", "restored", "final"): + for field in ("attribute", "cursor", "secondary"): + with self.subTest(stage=stage, field=field): + data = copy.deepcopy(captures(80, 24)) + if field == "attribute": + code, _ = data[stage]["cells"][2][2] + data[stage]["cells"][2][2] = (code, 4) + elif field == "cursor": + data[stage]["cursor_column"] += 1 + else: + data[stage]["cell_lines"][0] += "X" + result = evaluate_native(data, 80, 24) + self.assertFalse(result["stages"][stage]["passed"]) + self.assertFalse(result["all_native_stages_passed"]) + + def test_missing_stage_or_changed_grid_is_rejected(self): + for change in ("missing", "grid"): + data = captures(80, 24) + if change == "missing": + del data["alt"] + else: + data["alt"]["rows"] = 23 + with self.assertRaises(ValueError): + evaluate_native(data, 80, 24) + + +if __name__ == "__main__": + unittest.main() diff --git a/experiments/conpty/upstream/MICROSOFT-LICENSE.txt b/experiments/conpty/upstream/MICROSOFT-LICENSE.txt new file mode 100644 index 00000000..8cb179cd --- /dev/null +++ b/experiments/conpty/upstream/MICROSOFT-LICENSE.txt @@ -0,0 +1,21 @@ +Copyright (c) Microsoft Corporation. All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/experiments/conpty/upstream/README.md b/experiments/conpty/upstream/README.md new file mode 100644 index 00000000..e8a6f716 --- /dev/null +++ b/experiments/conpty/upstream/README.md @@ -0,0 +1,38 @@ +# 固定 ConPTY 的原生 margin 补丁候选 + +`dc8-native-margin-candidate.patch` 是针对 Microsoft Terminal +`dc8ae365847d41f62d1ebca93815bb00959bc12f` 的独立候选改动,随附 +[Microsoft MIT 原许可](MICROSOFT-LICENSE.txt)。原作者版权保留。它目前只通过完整固定文件的 +`git apply --unidiff-zero --check`,**未编译、未运行上游测试、未生成后端、未证明修复**。 +原问题、逐字段合同与有界运行见 [#56 调查](../../../docs/research/conpty-native-margin-path.md)。 + +候选在同一 `AdaptDispatch` 中保存隐藏主屏 margin,新建备用屏使用默认 margin,返回主屏恢复保存值; +重复 `1049h` 不覆盖隐藏主屏值,重复 `1049l` 不清空主屏当前值。它没有改源 fixture、运输、API 表、宽度 flag、 +TermForge 默认后端或现有官方双 SHA 校验。附加的上游 `AlternateScreenMarginsTest` 也尚未执行。 + +## 下一次隔离构建的具体入口 + +在单独的 Microsoft Terminal 源码 checkout 固定上述提交,校验 [validation.json](../results/2026-10-26-native-margin-path/validation.json) +的源文件 SHA 后,先用 `git apply --unidiff-zero --check` 检查、再以同一选项应用本 patch。完整保留上游许可证、依赖锁和构建日志。 +按固定 [上游 README](https://github.com/microsoft/terminal/blob/dc8ae365847d41f62d1ebca93815bb00959bc12f/README.md#building-the-code) +准备工具链;不要执行会更改个人系统的 winget/Developer Mode 安装步骤。优先使用已有工具或隔离构建宿主。 + +从上游根目录加载 `tools/OpenConsole.psm1`、调用 `Set-MsBuildDevEnvironment` 后,逐一构建这些真实项目入口, +使用 `Configuration=Release`、`Platform=x64`、正确的根 `SolutionDir`、restore 和单 worker;每一步非零立即停止: + +- `src/terminal/adapter/ut_adapter/Adapter.UnitTests.vcxproj`:运行 `ConAdapter.Unit.Tests.dll` 的新增测试、 + 已有 scrolling/cursor/reset 测试以及完整 adapter 测试。 +- `src/host/exe/Host.EXE.vcxproj`:生成 `OpenConsole.exe`,包含本次 adapter 修订。 +- `src/winconpty/dll/winconptydll.vcxproj`:从同一完整提交/依赖/配置生成配套 DLL。 + +这些入口来自同一提交的 `OpenConsole.slnx`;不是已执行或已验证的构建命令结果。 +测试入口可用上游 `Invoke-OpenConsoleTests -Test adapter`,其映射在 `tools/tests.xml`,须限制为无交互 unit 路径。 + +构建成功后,为**新的 fork 双文件身份**单独增加显式、默认关闭的测试配置与审查清单;保持当前官方包校验失败即拒绝。 +不能把 fork 冒充官方 1.25,也不能用新的 EXE 混配另一个版本的 DLL。create/resize/close 必须来自同模块, +模块存活至 HPCON、读线程和子进程回收之后。记录来源、补丁、工具链、文件 SHA、许可和失败路径。 + +先用原 175B/四阶段/两尺寸,原 5 秒 child wait、15 秒外部期限、5 秒 cleanup、自有 Job 与 EOF 验证, +同时执行原生 oracle 和实际候选运输回放。再加无备用屏、重复 h/l、默认 margin、样式与光标、 +窄/短 resize、历史隔离、查询与 API 写入交叉的反例。只有获得真实原生与末端 red→green,才能提出采用与上游提交。 +后续要按上游更新 rebase 小补丁、重建同包双文件并执行同一矩阵;无可维护构建/升级证据则不进入产品配置。 diff --git a/experiments/conpty/upstream/dc8-native-margin-candidate.patch b/experiments/conpty/upstream/dc8-native-margin-candidate.patch new file mode 100644 index 00000000..510a7e17 --- /dev/null +++ b/experiments/conpty/upstream/dc8-native-margin-candidate.patch @@ -0,0 +1,48 @@ +--- a/src/terminal/adapter/adaptDispatch.hpp ++++ b/src/terminal/adapter/adaptDispatch.hpp +@@ -330,0 +331,3 @@ ++ // The alternate buffer is freshly created, while the hidden main ++ // buffer retains its own margins until it becomes active again. ++ til::inclusive_rect _mainScrollMargins{}; +--- a/src/terminal/adapter/adaptDispatch.cpp ++++ b/src/terminal/adapter/adaptDispatch.cpp +@@ -1747,0 +1748,5 @@ ++ if (!_usingAltBuffer) ++ { ++ _mainScrollMargins = _scrollMargins; ++ } ++ _scrollMargins = {}; +@@ -1752,0 +1758,4 @@ ++ if (_usingAltBuffer) ++ { ++ _scrollMargins = _mainScrollMargins; ++ } +--- a/src/terminal/adapter/ut_adapter/adapterTest.cpp ++++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp +@@ -2994,0 +2995,26 @@ ++ TEST_METHOD(AlternateScreenMarginsTest) ++ { ++ _testGetSet->PrepData(); ++ _pDispatch->SetTopBottomScrollingMargins(2, 3); ++ const auto mainMargins = _pDispatch->_scrollMargins; ++ ++ // A redundant main-buffer request must not lose the current margins. ++ _pDispatch->ResetMode(DispatchTypes::ASB_AlternateScreenBuffer); ++ VERIFY_ARE_EQUAL(mainMargins, _pDispatch->_scrollMargins); ++ ++ _pDispatch->SetMode(DispatchTypes::ASB_AlternateScreenBuffer); ++ VERIFY_ARE_EQUAL(til::inclusive_rect{}, _pDispatch->_scrollMargins); ++ _pDispatch->SetTopBottomScrollingMargins(4, 5); ++ VERIFY_ARE_EQUAL(3, _pDispatch->_scrollMargins.top); ++ VERIFY_ARE_EQUAL(4, _pDispatch->_scrollMargins.bottom); ++ ++ // A new alternate replaces the previous alternate, not the main state. ++ _pDispatch->SetMode(DispatchTypes::ASB_AlternateScreenBuffer); ++ VERIFY_ARE_EQUAL(til::inclusive_rect{}, _pDispatch->_scrollMargins); ++ _pDispatch->SetTopBottomScrollingMargins(4, 5); ++ _pDispatch->ResetMode(DispatchTypes::ASB_AlternateScreenBuffer); ++ VERIFY_ARE_EQUAL(mainMargins, _pDispatch->_scrollMargins); ++ _pDispatch->ResetMode(DispatchTypes::ASB_AlternateScreenBuffer); ++ VERIFY_ARE_EQUAL(mainMargins, _pDispatch->_scrollMargins); ++ } ++