From ce252ae0bdeb5fa34a54c001e10349609ca3b51f Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Wed, 28 Oct 2026 22:41:34 +0800 Subject: [PATCH 1/8] docs: prepare bounded Qt5 raster style observation plan --- docs/research/windterm-1049-raster-plan.md | 57 ++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 docs/research/windterm-1049-raster-plan.md diff --git a/docs/research/windterm-1049-raster-plan.md b/docs/research/windterm-1049-raster-plan.md new file mode 100644 index 00000000..0dd00ae6 --- /dev/null +++ b/docs/research/windterm-1049-raster-plan.md @@ -0,0 +1,57 @@ +# 1049 当前样式:一次受控 Qt 栅格调查计划 + +状态:计划已准备,尚未执行。基点为 `72f40e10a8db0ef7585f8bec82f2be72f78c3d33`;等待 [#123](https://github.com/JavaZeroo/termforge/issues/123) 修订复核后再启动新原版批次。只调查进入、重复进入及退出后的当前前景/背景色,不重复完整模式矩阵,不改核心。 + +## 固定目标与边界 + +- 官方 WindTerm 2.7.0 Windows x64 EXE SHA:`7b10531dccc7cd614ae559f36f1ef518e0c96335637431b4646354823b2804d0`;4,047 文件清单 SHA:`e459adf2f12adb6a1b7f55e66a9da1c22794185e66e848057d21e0c60d669203`。 +- Qt5Widgets SHA:`3788c669d4b645e5a576de9fc77fca776bf516d43c89143dc2ca28291ba14358`;Qt5Gui SHA:`5e7d2d41b8b92a880e83b8cc0ca173f5da61218604186196787ee1600956be1e`。导出、局部指令和 Qt 5.15.2 官方头文件已只读核对。 +- 继续使用独立 WindowStation/Desktop、OwnedJob、独立 HOME/profile 和一个 `127.0.0.1` 合成 Telnet 会话。用户桌面、剪贴板、DPI/显示设置均不参与。 +- **最多新启动原版一次**。新生产脚本、新执行身份和新唯一输出目录,不复用旧批结果。REA capture 45 秒、CLI wait timeout 60 秒;原版启动在 CLI 之前,整程时限另列为未知,不能称 45 秒整程保证。 +- Qt 的 QWidget grab 是受控栅格,不是物理显示/真实 presented。动态注入仅用于正确性;不计时、不声明性能或完整终端样式等价。 + +## 五个最小阶段 + +`ESC` 为 `0x1b`。每阶段末尾追加 `ESC[6n`,收到回复、模式值和栅格记录后才确认下一阶段。标签只用于从独立原版文档 API 定位,比较的字形始终为 `H`。每阶段只抓一次 viewport;第一阶段两个 H 属于同一张校准图。 + +| 阶段 | 精确合成源(不含末尾 DSR) | 目的 | +|---|---|---| +| 0 主屏校准 | `ESC[0m ESC[2J ESC[H ESC[2;3H ESC[31;44m M0 H H`,表格中空格仅按下一段规则编码 | 在固定 SGR31/44 下取得两个同字形 H 与各自前面的显式空格,校准位置、背景和墨迹 | +| 1 首次进入 | `ESC[?1049hA0 H` | 不插入 SGR 或 CUP,观察进入后的当前样式 | +| 2 改变备用样式 | `ESC[4;5H ESC[32;45m A1 H`,间隔规则同下 | 取得另一组 H 的前景/背景颜色参照 | +| 3 重复进入 | `ESC[?1049hA2 H` | 不补 SGR,观察重复进入后的当前样式 | +| 4 退出后写入 | `ESC[?1049lM1 H` | 不补 SGR,观察主屏恢复后新写 H 的当前样式 | + +为消除表格排版歧义,阶段 0 的文字载荷严格是 `M0 H H`,阶段 2 严格是 `A1 H`;控制序列之间、控制序列与载荷之间没有额外空格。实际来源十六进制及 SHA 必须在执行前由新脚本导出并固定。进入、重复进入、退出后均不补发清屏/样式恢复命令。 + +此次使用修订后的 peer,在每条完整 NAWS 解析时立即记录与检查,不能只看 `recv` 块末态。等 UI capacity 与 NAWS 稳定后才冻结源与坐标;冻结后尺寸改变、模式 getter 失败或阶段确认缺失均停止。新批事件级守卫结果只属于新执行,不回填旧 #123。 + +## Qt 调用合同 + +全部在 GUI 线程、原版阶段完成后调用。借用的自有 viewport 必须由父链、名称及模块路径守卫绑定。QRect 输入为 **16B 的 inclusive x1/y1/x2/y2**,全 viewport 为 `[0,0,width-1,height-1]`。 + +| 固定导出 | RVA | 已核对的调用 | +|---|---:|---| +| `?grab@QWidget@@QEAA?AVQPixmap@@AEBVQRect@@@Z`(Widgets) | `0x39520` | `grab(widget, result, rect)`;返回 result | +| `?toImage@QPixmap@@QEBA?AVQImage@@XZ`(Gui) | `0xad6f0` | `toImage(pixmap, result)`;返回 result | +| `?pixel@QImage@@QEBAIHH@Z` | `0x8e560` | `uint32 pixel(image, int x, int y)`,x/y 为 EDX/R8D | +| `?width@QImage@@QEBAHXZ` / `?height@QImage@@QEBAHXZ` | `0x94770` / `0x8d1a0` | `int(pointer)` | +| `?isNull@QImage@@QEBA_NXZ` | `0x8da30` | `bool(pointer)` | +| `?devicePixelRatio@QPixmap@@QEBANXZ` / `?devicePixelRatio@QImage@@QEBANXZ` | `0xabe80` / `0x8c2c0` | `double(pointer)` | +| `??1QImage@@UEAA@XZ` / `??1QPixmap@@UEAA@XZ` | `0x86350` / `0xab020` | 普通析构 `void(pointer)`,不是删除析构 | + +QPixmap 与 QImage 各分配 32B,**不预先构造**,由正常返回的 sret 调用构造。只在相应调用正常返回后标记对象已构造;`finally` 按 image→pixmap 顺序调用普通析构。用 `QImage::pixel` 读取 QRgb,不解释内部 format、stride 或像素内存布局。官方依据:[QWidget](https://raw.githubusercontent.com/qt/qtbase/v5.15.2/src/widgets/kernel/qwidget.h)、[QPixmap](https://raw.githubusercontent.com/qt/qtbase/v5.15.2/src/gui/image/qpixmap.h)、[QImage](https://raw.githubusercontent.com/qt/qtbase/v5.15.2/src/gui/image/qimage.h)、[QPaintDevice](https://raw.githubusercontent.com/qt/qtbase/v5.15.2/src/gui/painting/qpaintdevice.h)。 + +## 坐标、DPR 与墨迹校准 + +1. 独立读取原版 `lineText`/`lineStart`,按唯一标签定位 H 与其前面的显式空格,调用 `visibleRectFromPosition` 获取真实矩形;同时主动调用 `isAltScreen/isMainScreen`。不沿用旧批的 x=147、8×18 或具体屏幕大小作为假定。 +2. 记录实际 viewport 几何、pixmap/image DPR、非空状态、image width/height 和标记矩形。必须能用这组实测量建立逻辑坐标到图像坐标的一致对应,ROI 完全在图像内;非整数边界或尺寸关系不明就保持未知,不平移 ROI 搜索。 +3. 只对 H 和显式空格的小 ROI 用 `QImage::pixel` 采样。校准空间格需给出一致背景;两个校准 H 需有非背景墨迹,且其背景及主要墨迹颜色一致。背景不明确、无墨迹或颜色分布不能稳定区分时停止样式判断。 +4. 记录 `background_rgba`、`dominant_ink_rgba`、有限颜色直方图、ROI 尺寸及私有 ROI 数据哈希。抗锯齿产生的墨迹色仅按栅格统计描述,不冒称 QPainter pen 的原始 RGB。 +5. 阶段 1/3/4 与阶段 0、阶段 2 的同字形 H 比较前景/背景标量。仅在标记绑定、DPR/坐标和墨迹校准均成立时报告具体一致/差异;歧义保持 `null`。这不是容差、性能预算或全颜色空间的验收。 + +空图、空 ROI、无墨迹、坐标/DPR 校准失败、原版异常或失去阶段握手,均保存失败/未知后结束这一次运行。不能改变假设、重新抓取试到绿或额外启动原版。完整栅格/ROI 原始数据、插桩、路径、PID 和原版资源留在 `local/`;公开仅发布可审查合成源、身份与脱敏颜色/尺寸标量,生产校验用显式 `require/ValueError`。 + +## 执行前出口 + +父任务复核 #123 修订后,才为本计划固定新执行 issue、完整新脚本、每阶段字节/SHA、脚本哈希及唯一输出目录,并正式启动这一次。当前没有新原版运行、栅格或颜色结果;计划不解除现有样式未知项。 From 6cc3d1d84e598fce0ca79455f07be1980146c477 Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Wed, 28 Oct 2026 22:42:01 +0800 Subject: [PATCH 2/8] docs: remove raster source notation ambiguity --- docs/research/windterm-1049-raster-plan.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/research/windterm-1049-raster-plan.md b/docs/research/windterm-1049-raster-plan.md index 0dd00ae6..e8e41fcc 100644 --- a/docs/research/windterm-1049-raster-plan.md +++ b/docs/research/windterm-1049-raster-plan.md @@ -16,9 +16,9 @@ | 阶段 | 精确合成源(不含末尾 DSR) | 目的 | |---|---|---| -| 0 主屏校准 | `ESC[0m ESC[2J ESC[H ESC[2;3H ESC[31;44m M0 H H`,表格中空格仅按下一段规则编码 | 在固定 SGR31/44 下取得两个同字形 H 与各自前面的显式空格,校准位置、背景和墨迹 | +| 0 主屏校准 | `ESC[0mESC[2JESC[HESC[2;3HESC[31;44mM0 H H` | 在固定 SGR31/44 下取得两个同字形 H 与各自前面的显式空格,校准位置、背景和墨迹 | | 1 首次进入 | `ESC[?1049hA0 H` | 不插入 SGR 或 CUP,观察进入后的当前样式 | -| 2 改变备用样式 | `ESC[4;5H ESC[32;45m A1 H`,间隔规则同下 | 取得另一组 H 的前景/背景颜色参照 | +| 2 改变备用样式 | `ESC[4;5HESC[32;45mA1 H` | 取得另一组 H 的前景/背景颜色参照 | | 3 重复进入 | `ESC[?1049hA2 H` | 不补 SGR,观察重复进入后的当前样式 | | 4 退出后写入 | `ESC[?1049lM1 H` | 不补 SGR,观察主屏恢复后新写 H 的当前样式 | From ebf076935ab1d1160b11456a93f5f84e5f23223b Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Wed, 28 Oct 2026 22:44:26 +0800 Subject: [PATCH 3/8] fix: preserve recorded probe bytes across Windows checkouts (#123) --- .gitattributes | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitattributes b/.gitattributes index d80940da..d71be01b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,5 +3,7 @@ *.json text eol=lf *.yml text eol=lf *.py text eol=lf +research/ui/*.js text eol=lf +research/ui/reference_sources/*.py.txt text eol=lf *.ps1 text eol=crlf experiments/core/fixtures/*.bin binary From 15c736b9274ed6bd2ba93842d27da8bc2eb87008 Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Wed, 28 Oct 2026 23:03:52 +0800 Subject: [PATCH 4/8] research: prepare one-shot Qt5 raster color probe and offline gates (#126) --- CMakeLists.txt | 1 + docs/research/windterm-1049-raster-plan.md | 4 +- research/ui/export_raster_style_reference.py | 100 ++++++++++++++++ research/ui/raster_style_analysis.py | 104 +++++++++++++++++ research/ui/raster_style_reference.js | 115 ++++++++++++++++++ research/ui/raster_style_reference.py | 117 +++++++++++++++++++ research/ui/test_raster_native_contract.js | 45 +++++++ research/ui/test_raster_style_reference.py | 103 ++++++++++++++++ 8 files changed, 587 insertions(+), 2 deletions(-) create mode 100644 research/ui/export_raster_style_reference.py create mode 100644 research/ui/raster_style_analysis.py create mode 100644 research/ui/raster_style_reference.js create mode 100644 research/ui/raster_style_reference.py create mode 100644 research/ui/test_raster_native_contract.js create mode 100644 research/ui/test_raster_style_reference.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 59e80484..857ee6e6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -299,6 +299,7 @@ if(BUILD_TESTING) termforge_add_python_test(benchmark_tools -m unittest discover -s benchmarks/tests -v) termforge_add_python_test(windterm_alternate_export -m unittest research.ui.test_export_alternate_reference -v) termforge_add_python_test(windterm_alternate_style_reference -m unittest research.ui.test_export_alternate_style_reference research.ui.test_alternate_style_peer -v) + termforge_add_python_test(windterm_raster_style_reference -m unittest research.ui.test_raster_style_reference -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) termforge_add_python_test(text_actions -m unittest experiments.shell.test_actions -v) diff --git a/docs/research/windterm-1049-raster-plan.md b/docs/research/windterm-1049-raster-plan.md index e8e41fcc..7846e467 100644 --- a/docs/research/windterm-1049-raster-plan.md +++ b/docs/research/windterm-1049-raster-plan.md @@ -1,6 +1,6 @@ # 1049 当前样式:一次受控 Qt 栅格调查计划 -状态:计划已准备,尚未执行。基点为 `72f40e10a8db0ef7585f8bec82f2be72f78c3d33`;等待 [#123](https://github.com/JavaZeroo/termforge/issues/123) 修订复核后再启动新原版批次。只调查进入、重复进入及退出后的当前前景/背景色,不重复完整模式矩阵,不改核心。 +状态:[#123](https://github.com/JavaZeroo/termforge/issues/123) 修订已复核,[#126](https://github.com/JavaZeroo/termforge/issues/126) 获一次执行授权,生产探针尚未执行。计划基点为 `72f40e10a8db0ef7585f8bec82f2be72f78c3d33`,并已接入 LF 属性守卫。只调查进入、重复进入及退出后的当前前景/背景色,不重复完整模式矩阵,不改核心。 ## 固定目标与边界 @@ -54,4 +54,4 @@ QPixmap 与 QImage 各分配 32B,**不预先构造**,由正常返回的 sret ## 执行前出口 -父任务复核 #123 修订后,才为本计划固定新执行 issue、完整新脚本、每阶段字节/SHA、脚本哈希及唯一输出目录,并正式启动这一次。当前没有新原版运行、栅格或颜色结果;计划不解除现有样式未知项。 +执行项为 #126。[生产探针](../../research/ui/raster_style_reference.py)及[GUI 驱动](../../research/ui/raster_style_reference.js)必须先提交固定身份,再生成含每阶段 hex/SHA、完整 Qt DLL 哈希、脚本哈希和唯一输出目录的私有计划,并由单次 marker 防止重复启动。当前没有新原版运行、栅格或颜色结果;计划不解除现有样式未知项。离线边界回归已注册为 `windterm_raster_style_reference`,不使用原版资源。 diff --git a/research/ui/export_raster_style_reference.py b/research/ui/export_raster_style_reference.py new file mode 100644 index 00000000..81e2970f --- /dev/null +++ b/research/ui/export_raster_style_reference.py @@ -0,0 +1,100 @@ +"""Explicit offline publication gate for the single #126 raster observation.""" +from __future__ import annotations +import argparse +import hashlib +import json +from pathlib import Path +import re +import sys + +ROOT=Path(__file__).resolve().parents[2] +sys.path.insert(0,str(ROOT)) +from research.ui.raster_style_reference import MODULE_SHA256,SOURCE_FILES,MANIFEST_SHA256,phases +from research.ui.raster_style_analysis import audit_capture,comparisons,require + +SYNTHETIC_LINE=re.compile(r" *(?:(?:M0 H H|A0 H|A1 H|A2 H|M1 H))* *") +REASONS={None,"pinned-module-path","pinned-export-rva","invalid-marker-rectangle","noninteger-roi-boundary","roi-out-of-bounds","space-background-ambiguous","no-marker-ink","dominant-ink-ambiguous","owned-viewport-ambiguous","viewport-dimensions","mode-getters-unavailable","selection-overlaps-observation","marker-binding-ambiguous","extra-grab-for-phase","pixmap-return-address","image-return-address","empty-image","dpr-inconsistent","image-viewport-dpr-inconsistent","caret-location-unknown","marker-text-mismatch","caret-overlaps-roi","h-space-size-inconsistent","calibration-H-colors-differ","reference-styles-not-distinguishable","native-or-observation-error","object-destruction-failed","first-run-guard","scenario-deadline","peer-failed","driver-observation-error","bounded-observation-timer"} + + +def export(directory,plan_path,destination): + sha=lambda path:hashlib.sha256(path.read_bytes()).hexdigest() + read=lambda name:json.loads((directory/name).read_text(encoding="utf-8")) + plan=json.loads(plan_path.read_text(encoding="utf-8")) + run,peer,captured=read("run.json"),read("peer.json"),read("raster-result.json") + outer=read("evidence.json");evidence=outer["data"]["evidence"];normalized=evidence["normalized_result"] + require(plan["issue"]==126 and plan["maximum_original_runs"]==1 and plan["phase_count"]==5,"Plan scope differs") + require(plan["maximum_grabs_per_phase"]==1 and plan["maximum_grabs"]==5 and re.fullmatch(r"[0-9a-f]{40}",plan["source_base_commit"]),"Plan capture/source bound differs") + require(plan["rea_capture_seconds"]==45 and plan["cli_wait_timeout_seconds"]==60 and plan["whole_lifecycle_bound_seconds"] is None,"Plan duration differs") + require(plan["module_sha256"]==MODULE_SHA256 and plan["files_manifest_sha256"]==MANIFEST_SHA256,"Module/package plan identity differs") + require(run["sha256"]==MODULE_SHA256["WindTerm.exe"] and run["files_sha256"]==MANIFEST_SHA256,"Observed package identity differs") + require(set(plan["source_files_sha256"])==set(SOURCE_FILES),"Executed source inventory differs") + require(all(sha(ROOT/name)==expected for name,expected in plan["source_files_sha256"].items()),"Executed source changed") + require(normalized["source_sha256"]==run["script_sha256"]==sha(directory/"probe.js"),"Injected source differs") + require(outer["ok"] and not outer["data"]["cleanup_error"] and not normalized["cleanup_error"] and not normalized["messages_truncated"],"Capture incomplete or cleanup failed") + require(not any(message["type"]=="error" for message in normalized["messages"]),"Provider script error") + require(evidence["provider"]=={"id":"frida","name":"Frida","version":None} and evidence["confidence"]=="observed" and evidence["subject"] is None and evidence["environment"] is None,"Provider identity differs") + require(re.fullmatch(r"ev_[0-9a-f]{64}",evidence["evidence_id"]),"Provider evidence identity differs") + require(all(value in {"Artifact identity is unavailable for this observation.","The target operating system and architecture were not reported by this observation."} for value in evidence["limitations"]),"Unexpected provider limitation text") + messages=[message["payload"] for message in normalized["messages"] if message["type"]=="send" and isinstance(message.get("payload"),dict)] + captured_phases=[{key:value for key,value in message.items() if key!="kind"} for message in messages if message.get("kind")=="raster_phase"] + finals=[{key:value for key,value in message.items() if key!="kind"} for message in messages if message.get("kind")=="raster_result"] + require(captured_phases==captured["captures"] and finals==[captured],"Private captures differ from provider messages") + require(captured["status"] in ("observed-five-raster-phases","partial-raster-observation") and captured["reason"] in REASONS,"Unknown capture status/reason") + guards=captured["module_guards"] + require({guard["name"] for guard in guards}==set(MODULE_SHA256) and len(guards)==4 and all(guard=={"name":guard["name"],"path_guard_passed":True} for guard in guards),"Runtime module path guard failed") + count=len(captured_phases);require(0<=count<=5,"Too many captures") + expected=phases(*reversed(peer["frozen_grid"])) + require(peer["frozen_grid"]==[peer["columns"],peer["rows"]],"Frozen grid changed") + freeze=peer["freeze_event_count"] + require(type(freeze) is int and 0<=freeze<=len(peer["naws_history"])<=64,"NAWS event bound differs") + require(all(grid==peer["frozen_grid"] for grid in peer["naws_history"][freeze:]),"NAWS changed after freeze") + require(all(isinstance(grid,list) and len(grid)==2 and all(type(value) is int and 0>16)&255,(value>>8)&255,value&255,(value>>24)&255] + + +def analyze_pixels(h_pixels,space_pixels): + require(h_pixels and space_pixels and len(h_pixels)<=8192 and len(space_pixels)<=8192,"Empty or oversized ROI") + for value in h_pixels+space_pixels: + rgba(value) + background=space_pixels[0] + require(all(value==background for value in space_pixels),"space-background-ambiguous") + ink=Counter(value for value in h_pixels if value!=background) + require(ink,"no-marker-ink") + ordered=sorted(ink.items(),key=lambda item:(-item[1],item[0])) + require(len(ordered)==1 or ordered[0][1]>ordered[1][1],"dominant-ink-ambiguous") + return {"background_rgba":rgba(background),"dominant_ink_rgba":rgba(ordered[0][0])} + + +def pixel_summary(pixels): + for value in pixels: + rgba(value) + counts=Counter(pixels) + require(00 and h>0 and w*h<=8192 and x>=0 and y>=0 and x+w<=width and y+h<=height,"ROI outside image") + + +def audit_capture(capture): + """Return a scalar-only projection, retaining calibrated/unknown boundaries.""" + public={key:capture[key] for key in ("phase","label","cursor_reply","grab_attempted","constructed","destroyed","calibration","reason")} + require(type(capture["calibration"]) is bool,"Calibration status missing") + require(capture["constructed"] in ([],["pixmap"],["pixmap","image"]),"Construction order differs") + require(capture["destroyed"]==list(reversed(capture["constructed"])) or (not capture["calibration"] and capture["reason"]=="object-destruction-failed"),"Destruction order differs") + image=capture["image"] + if image is not None: + require(all(type(image[key]) is int and 0<=image[key]<=4096 for key in ("width","height","viewport_width","viewport_height")),"Image scalar bound differs") + require(type(image["is_null"]) is bool,"Image null state missing") + require(image["grab_rect"]==[0,0,image["viewport_width"]-1,image["viewport_height"]-1],"Grab QRect is not inclusive") + require(all(image[key] is None or (type(image[key]) in (int,float) and math.isfinite(image[key]) and abs(image[key])<=4) for key in ("pixmap_dpr","image_dpr")),"DPR scalar bound differs") + if capture["calibration"]: + require(capture["grab_attempted"] is True and capture["constructed"]==["pixmap","image"] and capture["destroyed"]==["image","pixmap"],"Calibrated image lifecycle differs") + require(image and image["is_null"] is False,"Empty calibrated image") + pdpr,idpr=image["pixmap_dpr"],image["image_dpr"] + require(type(pdpr) in (int,float) and math.isfinite(pdpr) and 0>>16)&255,(value>>>8)&255,value&255,(value>>>24)&255];} +function terminalState(index){ + const descriptor=targets[index]||{label:'',offsets:[]}; + const source=`JSON.stringify((function(){var t=window.activeText;if(!t)return null;var label=${JSON.stringify(descriptor.label)},offsets=${JSON.stringify(descriptor.offsets)};function get(k){try{var v=typeof t[k]==='function'?t[k]():t[k];return v===undefined?null:v;}catch(e){return null;}}var count=get('visibleLineCount'),lines=[],found=[];if(typeof count==='number'&&count>=0&&count<=200)for(var i=0;i=0){for(var j=0;jNumber.isFinite(x)),'invalid-marker-rectangle'); + const values=[rect.x*dpr,rect.y*dpr,rect.width*dpr,rect.height*dpr]; + must(values.every(Number.isInteger),'noninteger-roi-boundary'); + const [x,y,w,h]=values;must(w>0&&h>0&&w*h<=8192&&x>=0&&y>=0&&x+w<=width&&y+h<=height,'roi-out-of-bounds');return [x,y,w,h]; +} +function readRoi(image,rectangle){const [x,y,w,h]=rectangle,pixels=[];for(let row=y;row>>0);return pixels;} +function colors(h,space){ + const background=space[0];must(space.length>0&&space.every(value=>value===background),'space-background-ambiguous'); + const counts=new Map();for(const value of h)if(value!==background)counts.set(value,(counts.get(value)||0)+1); + must(counts.size>0,'no-marker-ink'); + const ordered=Array.from(counts).sort((a,b)=>b[1]-a[1]||a[0]-b[0]); + must(ordered.length===1||ordered[0][1]>ordered[1][1],'dominant-ink-ambiguous'); + return {background_rgba:rgba(background),dominant_ink_rgba:rgba(ordered[0][0])}; +} +function sampleViewport(index,terminal,cursor){ + const capture={phase:index,label:targets[index].label,cursor_reply:cursor,terminal,grab_attempted:false,module_guards:moduleGuards,image:null,rois:[],constructed:[],destroyed:[],calibration:null,reason:null}; + const pixmap=Memory.alloc(32),image=Memory.alloc(32);let pixmapReady=false,imageReady=false; + try{ + scan();const candidates=cached.filter(row=>row.name==='qt_scrollarea_viewport'&&row.parent===editor.toString());must(candidates.length===1,'owned-viewport-ambiguous'); + const viewport=ptr(candidates[0].ptr),g=rect(viewport),width=g[2]-g[0]+1,height=g[3]-g[1]+1; + must(width>0&&height>0&&width<=4096&&height<=4096,'viewport-dimensions'); + must(typeof terminal.is_alt==='boolean'&&typeof terminal.is_main==='boolean'&&terminal.is_alt!==terminal.is_main,'mode-getters-unavailable'); + must(terminal.selection==='','selection-overlaps-observation'); + must(terminal.markers.length===targets[index].offsets.length,'marker-binding-ambiguous'); + const region=Memory.alloc(16);region.writeS32(0);region.add(4).writeS32(0);region.add(8).writeS32(width-1);region.add(12).writeS32(height-1); + must(!grabs.includes(index),'extra-grab-for-phase');grabs.push(index);capture.grab_attempted=true; + const returnedPixmap=grab(viewport,pixmap,region);pixmapReady=true;capture.constructed.push('pixmap');must(returnedPixmap.equals(pixmap),'pixmap-return-address'); + const returnedImage=toImage(pixmap,image);imageReady=true;capture.constructed.push('image');must(returnedImage.equals(image),'image-return-address'); + const pdpr=pixmapDpr(pixmap),idpr=imageDpr(image),iw=imageWidth(image),ih=imageHeight(image),empty=imageNull(image); + capture.image={viewport_width:width,viewport_height:height,grab_rect:[0,0,width-1,height-1],pixmap_dpr:pdpr,image_dpr:idpr,width:iw,height:ih,is_null:empty}; + must(!empty&&iw>0&&ih>0,'empty-image'); + must(Number.isFinite(pdpr)&&pdpr>0&&pdpr<=4&&pdpr===idpr,'dpr-inconsistent'); + must(Number.isInteger(width*pdpr)&&Number.isInteger(height*pdpr)&&iw===width*pdpr&&ih===height*pdpr,'image-viewport-dpr-inconsistent'); + must(terminal.caret_rect&&[terminal.caret_rect.x,terminal.caret_rect.y,terminal.caret_rect.width,terminal.caret_rect.height].every(Number.isFinite),'caret-location-unknown'); + for(const marker of terminal.markers){ + must(marker.character==='H'&&marker.preceding_character===' ','marker-text-mismatch'); + must(!overlap(marker.h_rect,terminal.caret_rect)&&!overlap(marker.space_rect,terminal.caret_rect),'caret-overlaps-roi'); + const hRect=physicalRect(marker.h_rect,pdpr,iw,ih),spaceRect=physicalRect(marker.space_rect,pdpr,iw,ih); + must(hRect[2]===spaceRect[2]&&hRect[3]===spaceRect[3],'h-space-size-inconsistent'); + const hPixels=readRoi(image,hRect),spacePixels=readRoi(image,spaceRect); + const record={ordinal:marker.ordinal,h_rect:hRect,space_rect:spaceRect,h_pixels:hPixels,space_pixels:spacePixels,colors:null};capture.rois.push(record); + record.colors=colors(hPixels,spacePixels); + } + if(index===0)must(same(capture.rois[0].colors,capture.rois[1].colors),'calibration-H-colors-differ'); + if(index===2)must(!same(capture.rois[0].colors,captures[0].rois[0].colors),'reference-styles-not-distinguishable'); + capture.calibration=true; + }catch(error){capture.calibration=false;capture.reason=error.code||'native-or-observation-error';capture.private_error=String(error);} + finally{ + for(const object of [{ready:imageReady,name:'image',value:image,destroy:destroyImage},{ready:pixmapReady,name:'pixmap',value:pixmap,destroy:destroyPixmap}]){ + if(object.ready)try{object.destroy(object.value);capture.destroyed.push(object.name);} + catch(error){capture.calibration=false;capture.reason='object-destruction-failed';capture.private_error=String(error);} + } + } + return capture; +} +function finish(status,reason){ + if(finished)return;finished=true; + const result={status,reason:reason||null,captures,grab_phases:grabs,module_guards:moduleGuards,presented_done:null}; + const file=new File(__RUN_DIRECTORY_JSON__+'/raster-result.json','w');file.write(JSON.stringify(result));file.close();send({kind:'raster_result',...result}); +} +send({kind:'probe_ready',station_isolated:true,scenario:'five-stage-raster-style',module_guards:moduleGuards}); +Interceptor.attach(user.getExportByName('DispatchMessageW'),{onEnter(){ + if(busy||finished||Process.getCurrentThreadId()!==uiThread||Date.now()finish('partial-raster-observation','bounded-observation-timer'),42000); diff --git a/research/ui/raster_style_reference.py b/research/ui/raster_style_reference.py new file mode 100644 index 00000000..6ef666bc --- /dev/null +++ b/research/ui/raster_style_reference.py @@ -0,0 +1,117 @@ +"""Single-use, owned original-terminal raster correctness observation (#126).""" +from __future__ import annotations +import argparse +import hashlib +import json +from pathlib import Path +import subprocess +import sys + +ROOT=Path(__file__).resolve().parents[2] +sys.path.insert(0,str(ROOT)) +from research.ui.alternate_style_reference import peer_class, require + +MODULE_SHA256={ + "WindTerm.exe":"7b10531dccc7cd614ae559f36f1ef518e0c96335637431b4646354823b2804d0", + "Qt5Core.dll":"8d2ff4ce9096ddccc4f4cd62c2e41fc854cfd1b0d6e8d296645a7f5fd4ae565a", + "Qt5Gui.dll":"5e7d2d41b8b92a880e83b8cc0ca173f5da61218604186196787ee1600956be1e", + "Qt5Widgets.dll":"3788c669d4b645e5a576de9fc77fca776bf516d43c89143dc2ca28291ba14358", +} +SOURCE_FILES=("research/ui/raster_style_reference.py","research/ui/raster_style_reference.js","research/ui/alternate_style_reference.py","research/ui/alternate_reference.py","research/ui/r1_launch.py","research/ui/launch.py","research/ui/qt_r1_text.js") +MANIFEST_SHA256="e459adf2f12adb6a1b7f55e66a9da1c22794185e66e848057d21e0c60d669203" + + +def phases(rows,columns): + require(10<=rows<=60 and 40<=columns<=160,"Controlled grid out of bounds") + return [ + ("main-calibration",b"\x1b[0m\x1b[2J\x1b[H\x1b[2;3H\x1b[31;44mM0 H H"), + ("alternate-entry",b"\x1b[?1049hA0 H"), + ("alternate-reference",b"\x1b[4;5H\x1b[32;45mA1 H"), + ("alternate-repeat-entry",b"\x1b[?1049hA2 H"), + ("main-return",b"\x1b[?1049lM1 H"), + ] + + +_BasePeer=peer_class() +_BasePeer._serve.__globals__["phases"]=phases + + +class Peer(_BasePeer): + def _record(self): + if self.state.get("frozen_grid") and "freeze_event_count" not in self.state: + self.state["freeze_event_count"]=len(self.state.get("naws_history",[])) + super()._record() + + +def sha(path): + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def check_full_hashes(directory,identities=MODULE_SHA256): + measured={name:sha(directory/name) for name in identities} + require(measured==identities,"Pinned executable or Qt DLL full hash differs") + return measured + + +def prepare_plan(source,out,rea,destination): + source,out,rea,destination=[path.resolve() for path in (source,out,rea,destination)] + require(not out.exists() and not destination.exists(),"Plan/output must be new") + require(out.is_relative_to(ROOT/"local") and destination.is_relative_to(ROOT/"local"),"Plan/output must belong to this worktree local directory") + require(rea.is_file(),"Locked REA CLI is missing") + package=rea.parent.parent/"rea-agents/package.json" + require(json.loads(package.read_text(encoding="utf-8"))["version"]=="6.7.0","REA version differs") + measured=check_full_hashes(source) + plan={"schema":"termforge-raster-style-plan-v1","issue":126,"maximum_original_runs":1, + "source_base_commit":subprocess.check_output(["git","rev-parse","HEAD"],cwd=ROOT,text=True).strip(), + "source":str(source),"out":str(out),"rea":str(rea),"module_sha256":measured,"files_manifest_sha256":MANIFEST_SHA256, + "rea_capture_seconds":45,"cli_wait_timeout_seconds":60,"whole_lifecycle_bound_seconds":None, + "source_files_sha256":{name:sha(ROOT/name) for name in SOURCE_FILES}, + "phases":[{"phase":index,"label":label,"source_hex":content.hex(),"source_bytes":len(content),"source_sha256":hashlib.sha256(content).hexdigest(),"dsr_suffix_hex":"1b5b366e"} for index,(label,content) in enumerate(phases(25,91))], + "phase_count":5,"maximum_grabs_per_phase":1,"maximum_grabs":5, + "scope":"Owned original direct-terminal Telnet, GUI-thread QWidget grab; H/space ROI only, no presented/performance claim", + "stop":"Empty image, DPR/coordinates, caret/selection or ink calibration failure stops this one run; no shifted ROI, extra capture or retry"} + destination.parent.mkdir(parents=True,exist_ok=True) + destination.write_text(json.dumps(plan,indent=2)+"\n",encoding="utf-8",newline="\n") + print(json.dumps({"prepared":True,"phase_count":5,"maximum_original_runs":1,"plan":destination.as_posix()})) + + +def run_plan(plan_path): + plan_path=plan_path.resolve() + plan=json.loads(plan_path.read_text(encoding="utf-8")) + require(plan["issue"]==126 and plan["maximum_original_runs"]==1,"Unexpected raster plan") + require(plan["source_base_commit"]==subprocess.check_output(["git","rev-parse","HEAD"],cwd=ROOT,text=True).strip(),"Source commit changed after plan") + require(plan["module_sha256"]==MODULE_SHA256,"Plan module identity differs") + require(plan["source_files_sha256"]=={name:sha(ROOT/name) for name in SOURCE_FILES},"Probe source changed after plan") + require(plan["phases"]==[{"phase":index,"label":label,"source_hex":content.hex(),"source_bytes":len(content),"source_sha256":hashlib.sha256(content).hexdigest(),"dsr_suffix_hex":"1b5b366e"} for index,(label,content) in enumerate(phases(25,91))],"Plan source differs") + source,out,rea=[Path(plan[key]) for key in ("source","out","rea")] + check_full_hashes(source) + require(not out.exists(),"This run output already exists") + # Exclusive marker is deliberately retained after every attempted launch. + with plan_path.with_suffix(".attempted").open("x",encoding="utf-8") as marker: + marker.write("One authorized original launch attempt; never retry this plan.\n") + from research.ui import r1_launch + launcher=Path(r1_launch.__file__) + code=launcher.read_text(encoding="utf-8") + replacements={"from research.ui.r1_vt_fixture import Peer":"from research.ui.raster_style_reference import Peer", + 'Path(__file__).with_name("r1_menu_wrap.js").read_text(encoding="utf-8")':'Path(__file__).with_name("raster_style_reference.js").read_text(encoding="utf-8")'} + for old,new in replacements.items(): + require(code.count(old)==1,"Protected launcher source changed") + code=code.replace(old,new) + namespace={"__file__":str(launcher),"__name__":"termforge_raster_launcher"} + exec(compile(code,str(launcher),"exec"),namespace) + namespace["run"](source,out,rea,45,False,"menu-wrap") + + +if __name__=="__main__": + parser=argparse.ArgumentParser() + parser.add_argument("--plan",type=Path,required=True) + parser.add_argument("--prepare",action="store_true") + parser.add_argument("--source",type=Path) + parser.add_argument("--out",type=Path) + parser.add_argument("--rea",type=Path) + args=parser.parse_args() + if args.prepare: + require(args.source and args.out and args.rea,"Source/output/REA required") + prepare_plan(args.source,args.out,args.rea,args.plan) + else: + run_plan(args.plan) diff --git a/research/ui/test_raster_native_contract.js b/research/ui/test_raster_native_contract.js new file mode 100644 index 00000000..87ef6885 --- /dev/null +++ b/research/ui/test_raster_native_contract.js @@ -0,0 +1,45 @@ +'use strict'; +// Run the production helper bodies with synthetic Qt calls, without Frida/Qt. +const fs=require('fs'),vm=require('vm'),assert=require('assert'); +const source=fs.readFileSync(require('path').join(__dirname,'raster_style_reference.js'),'utf8'); +function helpers(extra={}){ + const context=vm.createContext({Number,Map,JSON,Error,...extra}); + vm.runInContext(source.slice(source.indexOf('function modulePathMatches'),source.indexOf('const moduleGuards')),context); + vm.runInContext(source.slice(source.indexOf('function must'),source.indexOf('function terminalState')),context); + vm.runInContext(source.slice(source.indexOf('function overlap'),source.indexOf('function finish')),context); + return context; +} +function equal(a,b){assert.strictEqual(JSON.stringify(a),JSON.stringify(b));} +const pure=helpers(); +assert(pure.modulePathMatches('D:\\OWNED\\app\\Qt5Gui.dll','d:/owned','Qt5Gui.dll')); +assert(!pure.modulePathMatches('D:/other/app/Qt5Gui.dll','d:/owned','Qt5Gui.dll')); +equal(pure.rgba(0xff123456),[0x12,0x34,0x56,0xff]); +assert.throws(()=>pure.physicalRect({x:.5,y:0,width:1,height:1},1,8,2),/noninteger/); +assert.throws(()=>pure.physicalRect({x:7,y:0,width:2,height:1},1,8,2),/out-of-bounds/); +assert.throws(()=>pure.colors([1,1],[1,1]),/no-marker-ink/); +assert.throws(()=>pure.colors([2,3],[1,1]),/dominant-ink-ambiguous/); +assert.throws(()=>pure.colors([2,2],[1,3]),/space-background-ambiguous/); +function lifecycle(failure){ + const calls=[],allocations=[]; + function pointer(){const result={data:[],equals(other){return result===other;},writeS32(value){result.data[0]=value;},add(offset){return {writeS32(value){result.data[offset/4]=value;}};}};return result;} + const editor={toString(){return 'owned-editor';}},viewport={}; + const markers=[0,1].map(ordinal=>({ordinal,character:'H',preceding_character:' ',h_rect:{x:ordinal*4,y:0,width:2,height:2},space_rect:{x:ordinal*4+2,y:0,width:2,height:2}})); + const context=helpers({targets:[{label:'M0 H H',offsets:[3,5]}],moduleGuards:[],captures:[],grabs:[],editor, + scan(){},cached:[{name:'qt_scrollarea_viewport',parent:'owned-editor',ptr:'owned-viewport'}],ptr(){return viewport;},rect(){return [0,0,7,1];}, + Memory:{alloc(size){allocations.push(size);return pointer();}}, + grab(widget,result,region){calls.push('grab');assert.strictEqual(widget,viewport);equal(region.data,[0,0,7,1]);if(failure==='grab')throw Error('synthetic grab failure');return result;}, + toImage(pixmap,result){calls.push('toImage');if(failure==='toImage')throw Error('synthetic toImage failure');return result;}, + imageWidth(){return 8;},imageHeight(){return 2;},imageNull(){return failure==='empty';},pixmapDpr(){return 1;},imageDpr(){return failure==='dpr'?2:1;}, + pixel(image,x,y){if(failure==='pixel')throw Error('synthetic pixel failure');return y===0&&(x===0||x===4)?0xffaa0000:0xff0000aa;}, + destroyImage(){calls.push('destroy-image');if(failure==='destroy')throw Error('synthetic destructor failure');},destroyPixmap(){calls.push('destroy-pixmap');}}); + const capture=context.sampleViewport(0,{is_alt:false,is_main:true,selection:'',markers,caret_rect:{x:8,y:0,width:1,height:1}},[2,9]); + equal(allocations,[32,32,16]); + if(failure==='grab'){equal(calls,['grab']);equal(capture.constructed,[]);equal(capture.destroyed,[]);} + else if(failure==='toImage'){equal(calls,['grab','toImage','destroy-pixmap']);equal(capture.constructed,['pixmap']);equal(capture.destroyed,['pixmap']);} + else{equal(calls,['grab','toImage','destroy-image','destroy-pixmap']);equal(capture.constructed,['pixmap','image']);} + if(!failure){assert(capture.calibration);equal(capture.destroyed,['image','pixmap']);} + else assert.strictEqual(capture.calibration,false); + equal(context.grabs,[0]); +} +for(const failure of [null,'grab','toImage','empty','dpr','pixel','destroy'])lifecycle(failure); +process.stdout.write('module paths, QRgb, ROI/ink gates,32B sret and normal-return destructor order passed\n'); diff --git a/research/ui/test_raster_style_reference.py b/research/ui/test_raster_style_reference.py new file mode 100644 index 00000000..88004330 --- /dev/null +++ b/research/ui/test_raster_style_reference.py @@ -0,0 +1,103 @@ +"""Offline synthetic boundaries for single-run raster analysis and publication.""" +from __future__ import annotations +import copy +import hashlib +import json +from pathlib import Path +import shutil +import subprocess +import sys +import tempfile +import unittest + +ROOT=Path(__file__).resolve().parents[2] +sys.path.insert(0,str(ROOT)) +from research.ui.raster_style_reference import MODULE_SHA256,MANIFEST_SHA256,SOURCE_FILES,check_full_hashes,phases +from research.ui.raster_style_analysis import analyze_pixels,audit_capture + + +def fixture(root,partial=False): + directory=root/"raw";directory.mkdir();script=b"// synthetic offline unit fixture only\n" + (directory/"probe.js").write_bytes(script);(directory/"stderr.txt").write_bytes(b"") + colors=[(0xffaa0000,0xff0000aa),(0xffaa0000,0xff0000aa),(0xff00aa00,0xffaa00aa),(0xff00aa00,0xffaa00aa),(0xffaa0000,0xff0000aa)] + captures=[];records=[] + for index,(source_label,content) in enumerate(phases(25,91)[:1 if partial else 5]): + label=["M0 H H","A0 H","A1 H","A2 H","M1 H"][index];fg,bg=colors[index];markers=[];rois=[] + for ordinal in range(2 if index==0 else 1): + h=[ordinal*4,0,2,2];space=[ordinal*4+2,0,2,2] + markers.append({"ordinal":ordinal,"character":"H","preceding_character":" ","h_rect":dict(zip(("x","y","width","height"),h)),"space_rect":dict(zip(("x","y","width","height"),space))}) + hp=[fg,bg,fg,bg];sp=[bg]*4 + rois.append({"ordinal":ordinal,"h_rect":h,"space_rect":space,"h_pixels":hp,"space_pixels":sp,"colors":analyze_pixels(hp,sp)}) + capture={"phase":index,"label":label,"cursor_reply":[1,1],"grab_attempted":True,"module_guards":[{"name":name,"path_guard_passed":True} for name in MODULE_SHA256], + "terminal":{"protocol":"Telnet","connected":True,"capacity":25,"line_count":1,"lines":[label],"is_alt":index in (1,2,3),"is_main":index not in (1,2,3),"markers":markers,"selection":"","caret_rect":{"x":8,"y":0,"width":1,"height":1}}, + "image":{"viewport_width":8,"viewport_height":2,"grab_rect":[0,0,7,1],"pixmap_dpr":1,"image_dpr":1,"width":8,"height":2,"is_null":False},"rois":rois, + "constructed":["pixmap","image"],"destroyed":["image","pixmap"],"calibration":True,"reason":None} + if partial: + capture.update(calibration=False,reason="empty-image",rois=[]) + capture["image"].update(width=0,height=0,is_null=True) + else: + (directory/f"ack-{index}").write_text("observed") + captures.append(capture);records.append({"phase":index,"label":source_label,"source_hex":content.hex(),"source_sha256":hashlib.sha256(content).hexdigest(),"cursor_reply":[1,1]}) + result={"status":"partial-raster-observation" if partial else "observed-five-raster-phases","reason":"empty-image" if partial else None,"captures":captures,"grab_phases":list(range(len(captures))),"module_guards":captures[0]["module_guards"],"presented_done":None} + plan={"issue":126,"maximum_original_runs":1,"phase_count":5,"maximum_grabs_per_phase":1,"maximum_grabs":5,"source_base_commit":"0"*40, + "rea_capture_seconds":45,"cli_wait_timeout_seconds":60,"whole_lifecycle_bound_seconds":None,"module_sha256":MODULE_SHA256,"files_manifest_sha256":MANIFEST_SHA256, + "source_files_sha256":{name:hashlib.sha256((ROOT/name).read_bytes()).hexdigest() for name in SOURCE_FILES}} + data={"run.json":{"sha256":MODULE_SHA256["WindTerm.exe"],"files_sha256":MANIFEST_SHA256,"script_sha256":hashlib.sha256(script).hexdigest()}, + "raster-result.json":result,"peer.json":{"frozen_grid":[91,25],"columns":91,"rows":25,"freeze_event_count":2,"naws_history":[[91,24],[91,25]],"records":records,"received_hex":(b"\x1b[1;1R"*len(records)).hex(),"complete":not partial}, + "cleanup.json":{"active":0},"profile-check.json":{"only_synthetic_session":True,"session_count":1}, + "evidence.json":{"ok":True,"data":{"cleanup_error":None,"evidence":{"evidence_id":"ev_"+"0"*64,"provider":{"id":"frida","name":"Frida","version":None},"confidence":"observed","subject":None,"environment":None,"limitations":[],"normalized_result":{"source_sha256":hashlib.sha256(script).hexdigest(),"cleanup_error":None,"messages_truncated":False}}}}} + write_records(directory,data);plan_path=root/"plan.json";plan_path.write_text(json.dumps(plan),encoding="utf-8") + return directory,plan_path,data + + +def write_records(directory,data): + result=data["raster-result.json"] + messages=[{"type":"send","payload":{"kind":"raster_phase",**copy.deepcopy(capture)}} for capture in result["captures"]] + messages.append({"type":"send","payload":{"kind":"raster_result",**copy.deepcopy(result)}}) + data["evidence.json"]["data"]["evidence"]["normalized_result"]["messages"]=messages + for name,value in data.items():(directory/name).write_text(json.dumps(value),encoding="utf-8") + + +class RasterBoundaryTests(unittest.TestCase): + def test_full_file_hash_detects_tail_change(self): + with tempfile.TemporaryDirectory() as temporary: + root=Path(temporary);payload=b"fixed-header-and-body";file=root/"Qt5Gui.dll";file.write_bytes(payload) + expected={file.name:hashlib.sha256(payload).hexdigest()};self.assertEqual(check_full_hashes(root,expected),expected) + file.write_bytes(payload[:-1]+b"X") + with self.assertRaisesRegex(ValueError,"full hash"):check_full_hashes(root,expected) + + @unittest.skipUnless(shutil.which("node"),"Node is required to exercise production JS native-boundary helpers") + def test_production_js_native_contract(self): + result=subprocess.run(["node",str(ROOT/"research/ui/test_raster_native_contract.js")],capture_output=True,text=True,timeout=10) + self.assertEqual(result.returncode,0,result.stderr) + + def test_pixel_analysis_rejects_empty_ambiguous_or_nonuint_samples(self): + for h,space in (([],[1]),([1],[1]),([2,3],[1,1]),([2,2],[1,3]),([-1],[1])): + with self.subTest(h=h,space=space),self.assertRaises(ValueError):analyze_pixels(h,space) + + def check_cli(self,change=None,partial=False): + for optimized in (False,True): + with self.subTest(optimized=optimized),tempfile.TemporaryDirectory() as temporary: + root=Path(temporary);directory,plan,data=fixture(root,partial) + if change:change(data);write_records(directory,data) + output=root/"public/validation.json" + completed=subprocess.run([sys.executable,*(["-O"] if optimized else []),str(ROOT/"research/ui/export_raster_style_reference.py"),"--directory",str(directory),"--plan",str(plan),"--destination",str(output)],cwd=ROOT,capture_output=True,text=True,timeout=10) + if change: + self.assertNotEqual(completed.returncode,0,completed.stdout);self.assertFalse(output.exists());self.assertFalse(output.parent.exists()) + else: + self.assertEqual(completed.returncode,0,completed.stderr);summary=json.loads(output.read_text(encoding="utf-8")) + self.assertEqual(summary["sample_colors_verified"],not partial) + self.assertEqual(summary["observed_phase_count"],1 if partial else 5) + self.assertNotIn("h_pixels",output.read_text());self.assertNotIn("space_pixels",output.read_text()) + if partial:self.assertIsNone(summary["sample_color_comparisons"]) + else:self.assertTrue(summary["sample_color_comparisons"]["return_same_sample_colors_as_main"]) + + def test_valid_synthetic_five_phase_export(self):self.check_cli() + def test_empty_image_remains_unknown_and_stops_after_one(self):self.check_cli(partial=True) + def test_extra_grab_is_rejected(self):self.check_cli(lambda data:data["raster-result.json"]["grab_phases"].append(0)) + def test_module_path_guard_failure_is_rejected(self):self.check_cli(lambda data:data["raster-result.json"]["module_guards"][0].update(path_guard_passed=False)) + def test_nonfixture_text_is_rejected(self):self.check_cli(lambda data:data["raster-result.json"]["captures"][0]["terminal"]["lines"].__setitem__(0,"PRIVATE_MARKER/host/path")) + def test_derived_colors_cannot_disagree_with_pixels(self):self.check_cli(lambda data:data["raster-result.json"]["captures"][0]["rois"][0]["colors"].update(background_rgba=[1,2,3,4])) + + +if __name__=="__main__":unittest.main() From 8d01a029c6136f756fbd0d6a81c315a7918f5006 Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Wed, 28 Oct 2026 23:15:26 +0800 Subject: [PATCH 5/8] research: retain pre-raster guard stop and bound selection counterexamples (#126) --- docs/research/windterm-1049-raster-plan.md | 4 +- ...ndterm-1049-raster-reference-2026-10-28.md | 52 +++++ research/ui/export_raster_style_reference.py | 8 + .../ui/export_selection_counterexample.py | 49 +++++ .../selection-getter-counterexample.json | 22 +++ .../validation.json | 186 ++++++++++++++++++ research/ui/test_raster_style_reference.py | 26 ++- 7 files changed, 341 insertions(+), 6 deletions(-) create mode 100644 docs/research/windterm-1049-raster-reference-2026-10-28.md create mode 100644 research/ui/export_selection_counterexample.py create mode 100644 research/ui/results/2026-10-28-raster-style-reference/selection-getter-counterexample.json create mode 100644 research/ui/results/2026-10-28-raster-style-reference/validation.json diff --git a/docs/research/windterm-1049-raster-plan.md b/docs/research/windterm-1049-raster-plan.md index 7846e467..785d2257 100644 --- a/docs/research/windterm-1049-raster-plan.md +++ b/docs/research/windterm-1049-raster-plan.md @@ -1,6 +1,6 @@ # 1049 当前样式:一次受控 Qt 栅格调查计划 -状态:[#123](https://github.com/JavaZeroo/termforge/issues/123) 修订已复核,[#126](https://github.com/JavaZeroo/termforge/issues/126) 获一次执行授权,生产探针尚未执行。计划基点为 `72f40e10a8db0ef7585f8bec82f2be72f78c3d33`,并已接入 LF 属性守卫。只调查进入、重复进入及退出后的当前前景/背景色,不重复完整模式矩阵,不改核心。 +状态:[#126](https://github.com/JavaZeroo/termforge/issues/126) 的一次执行已结束,在阶段 0 抓图前因 selectionText 守卫停止;颜色仍未知,见[实际结果与边界](windterm-1049-raster-reference-2026-10-28.md)。以下保留执行前计划,不把计划内容视为已观察结果。计划基点为 `72f40e10a8db0ef7585f8bec82f2be72f78c3d33`,并已接入 LF 属性守卫。 ## 固定目标与边界 @@ -54,4 +54,4 @@ QPixmap 与 QImage 各分配 32B,**不预先构造**,由正常返回的 sret ## 执行前出口 -执行项为 #126。[生产探针](../../research/ui/raster_style_reference.py)及[GUI 驱动](../../research/ui/raster_style_reference.js)必须先提交固定身份,再生成含每阶段 hex/SHA、完整 Qt DLL 哈希、脚本哈希和唯一输出目录的私有计划,并由单次 marker 防止重复启动。当前没有新原版运行、栅格或颜色结果;计划不解除现有样式未知项。离线边界回归已注册为 `windterm_raster_style_reference`,不使用原版资源。 +执行项为 #126。[生产探针](../../research/ui/raster_style_reference.py)及[GUI 驱动](../../research/ui/raster_style_reference.js)已先提交固定身份,再生成含每阶段 hex/SHA、完整 Qt DLL 哈希、脚本哈希和唯一输出目录的私有计划,并由单次 marker 防止重复启动。本计划不解除现有样式未知项;实际执行结果另列,不重跑。离线边界回归已注册为 `windterm_raster_style_reference`,不使用原版资源。 diff --git a/docs/research/windterm-1049-raster-reference-2026-10-28.md b/docs/research/windterm-1049-raster-reference-2026-10-28.md new file mode 100644 index 00000000..f796a448 --- /dev/null +++ b/docs/research/windterm-1049-raster-reference-2026-10-28.md @@ -0,0 +1,52 @@ +# 1049 栅格样式调查:阶段 0 守卫停止,颜色仍未知 + +[#126](https://github.com/JavaZeroo/termforge/issues/126) 的一次已授权原版执行在 **抓图之前**结束。阶段 0 的 `selectionText` 返回非空字符串,执行脚本把它当作停止条件;没有调用 QWidget grab、QPixmap toImage 或 QImage pixel,没有生成栅格、ROI 像素、颜色或 presented 结果。当前样式继承与恢复仍未知。 + +这是调查守卫的语义假定有误,不能称为原版存在选区遮挡或样式错误。原执行源码和原始记录保持不变,没有改条件再抓取、补跑阶段或第二次启动原版。 + +## 固定执行身份 + +- 生产源码:`15c736b9274ed6bd2ba93842d27da8bc2eb87008`;[五阶段计划](windterm-1049-raster-plan.md)。在启动前提交源码,再生成唯一计划,计划 SHA 为 `deb78215e2a12108d73b0fc82fa6f5bf8e979f28ba71013d72f28818ef3e085c`。 +- 固定官方 2.7.0 Windows x64 EXE SHA:`7b10531dccc7cd614ae559f36f1ef518e0c96335637431b4646354823b2804d0`;4,047 文件清单 SHA:`e459adf2f12adb6a1b7f55e66a9da1c22794185e66e848057d21e0c60d669203`。 +- 启动前核对 WindTerm/Qt5Core/Qt5Gui/Qt5Widgets 的**完整文件 SHA**,复制后完整清单再次核对。离线重审复制文件,四个完整 SHA 均与计划一致;运行中四个模块实际路径及所需导出 RVA 守卫通过。这不等于实际调用过栅格函数。 +- 独立 WindowStation/Desktop、OwnedJob、独立 HOME/profile 和一个合成 loopback Telnet 会话;没有用户输入桌面、剪贴板或 DPI 变更。计划的单次 marker 保留,实际仅启动原版一次。 +- REA capture=45 秒、CLI wait timeout=60 秒;原版更早启动并另有 2 秒等待,回收在 CLI 返回后进行,不提供整程 45/60 秒保证。动态插桩只用于正确性,不计性能。 + +REA Evidence ID 为 `ev_cedd36bf326d1769fa8d0d7b1cc508314eb22da43ba97faf80debbcf9b085e63`。完整脱敏标量及原件哈希见[白名单结果](../../research/ui/results/2026-10-28-raster-style-reference/validation.json)。 + +## 本次实际发生的步骤 + +五阶段分别计划主屏 SGR31/44 同图双 H 校准、首次 1049 进入、备用 SGR32/45、重复进入及退出后写 H。实际只发送了第一阶段的 31B 源及 `CSI 6n`,没有确认文件释放后四阶段。 + +阶段 0 精确源为 `ESC[0mESC[2JESC[HESC[2;3HESC[31;44mM0 H H`;DSR 回复为 `(2,9)`,主/备用状态为 `false/true`。NAWS 完整事件历史为 `91×24 → 91×25`,源冻结时事件数为 2;这次使用逐条完整 NAWS 检查的修订 peer,没有观察到冻结后的改变。 + +原版文档 API 返回了两个 H 标签位置。`selectionText` 返回 string,长度 10;`caretPosition=9`,caret rect 为 `x=211,y=18,width=0,height=18`。执行中的判断是 `selectionText === ''`,因此以原码 `selection-overlaps-observation` 停止: + +| 指标 | 实际结果 | +|---|---| +| 已观察输入阶段 | 1 / 计划 5 | +| grab 尝试 | 0 | +| QPixmap/QImage 已构造与析构 | 均为 0 | +| 栅格与 H/space ROI 像素 | 无 | +| 颜色校准、比较 | 未执行,保持未知 | +| 原版真实选区或遮挡 | 未证实 | + +原停止码保留原样,但导出另列 `actual_selection_or_overlap_proven=false`。非空 `selectionText` 不是这里可靠的选区判据,也没有因为未读到像素就声称 H 无墨迹。 + +## 既有原件对守卫假定的反证 + +停止后只读复核[既有原版样式/区域观察](https://github.com/JavaZeroo/termforge/blob/dce2ca799a25cf32f3cf2bacf1b9f7c915945a57/docs/research/windterm-styled-margin-reference.md)的本地原件,未运行原版。其 Evidence ID 为 `ev_8fbd9aead9aa5d60f49e05112c31eb542febf65084643c025eb8a52ffd590048`,`evidence.json` SHA 为 `be349abb34cf28f42b634ccf65bd0749e1e9940856059156b794e7968b7ef83c`。 + +该原件的 shell-ready、actual80x24 快照均为 `caret=anchor=97`,但 `selectionText` 长度为 5;styled-final 为 `caret=anchor=400`,但文本长度为 10。这是“非空返回值足以证明有选区”的直接反例。它不替代本批未采集的 anchor,也不证明本批实际选区已折叠;只说明本批使用的守卫判据不成立。下一次调查应先按原版实际 anchor/caret 合同明确选区状态,再决定是否抓取。 + +[两条脱敏反例](../../research/ui/results/2026-10-28-raster-style-reference/selection-getter-counterexample.json)只发布 caret、anchor 和 selection_length 三个值,并绑定原 Evidence ID、文件 SHA 与固定 EXE 身份;没有文字或路径。[专用离线导出器](../../research/ui/export_selection_counterexample.py)从固定原件核对并投影这些值,不启动原版。 + +## 离线审计与剩余边界 + +REA 正常结束,无脚本错误、消息截断、清理错误或 stderr;Job 最终 `active=0,total=3,terminated=0`,合成 profile 仍为一个会话。启动器主动结束原版,不以 Job 计数声称自然退出。provider 的 subject/environment 为 null,包身份由独立准备及模块路径守卫核对。 + +[生产探针](../../research/ui/raster_style_reference.py)、[GUI 脚本](../../research/ui/raster_style_reference.js)和原计划中的执行 SHA 未变。[离线导出](../../research/ui/export_raster_style_reference.py)使用显式 `require/ValueError`,重新核对源字节、消息、对象生命周期、像素派生值及单次抓取边界;本批为 partial,所以没有填充任何颜色结论。普通及 `python -O` 对同一原件导出字节相同。 + +[回归](../../research/ui/test_raster_style_reference.py)在普通/优化解释器下各 11 项通过,覆盖完整文件哈希尾部变更、模块路径、32B sret 正常返回后的析构顺序、ROI/墨迹边界、空图、额外抓取、私有文本拒绝、反例仅投影端点/长度,以及守卫停止不得冒充实际选区/像素证据。原生调用边界使用合成对象验证,不能转记成这次未执行的 Qt 栅格调用已通过。CTest `windterm_raster_style_reference` 单独通过。 + +原始日志、路径、PID、profile、复制资源和可能的 ROI 原始数据均留在 `local/r126-raster-0`;公开只有合成源、身份、停止原因、标量和哈希。进入/退出当前样式、受控栅格、完整网格及真实 presented 均未验证;本次不关闭 #126、#123、#27 或 R2。 diff --git a/research/ui/export_raster_style_reference.py b/research/ui/export_raster_style_reference.py index 81e2970f..b4d6300d 100644 --- a/research/ui/export_raster_style_reference.py +++ b/research/ui/export_raster_style_reference.py @@ -78,6 +78,13 @@ def export(directory,plan_path,destination): cleanup,profile=read("cleanup.json"),read("profile-check.json") require(cleanup["active"]==0 and profile=={"only_synthetic_session":True,"session_count":1},"Owned resources/profile failed") raw_names=("run.json","probe.js","peer.json","raster-result.json","evidence.json","cleanup.json","profile-check.json","stderr.txt") + stop_guard=None + if captured["reason"]=="selection-overlaps-observation" and captured_phases: + selection=captured_phases[-1]["terminal"].get("selection") + stop_guard={"guard":"selectionText must return the empty string","observed_value_type":"string" if isinstance(selection,str) else "null" if selection is None else "other", + "observed_character_count":len(selection) if isinstance(selection,str) else None, + "actual_selection_or_overlap_proven":False, + "interpretation":"The executed guard did not establish an explicitly empty selectionText. Nonempty selectionText does not by itself prove a selection or overlap; getter semantics remain unverified."} summary={"schema":"termforge-original-raster-style-reference-v1","issue":126,"status":captured["status"],"reason":captured["reason"], "scope":"Five-stage owned direct-terminal QWidget raster experiment; only H/space output-color scalars, not internal pen state or presented frames", "source_base_commit":plan["source_base_commit"],"source_files_sha256":plan["source_files_sha256"],"exporter_sha256":sha(Path(__file__)),"analysis_sha256":sha(ROOT/"research/ui/raster_style_analysis.py"), @@ -85,6 +92,7 @@ def export(directory,plan_path,destination): "plan_sha256":sha(plan_path),"actual_original_runs":1,"maximum_original_runs":1,"rea_capture_seconds":45,"cli_wait_timeout_seconds":60,"whole_lifecycle_bound_seconds":None, "frozen_grid":peer["frozen_grid"],"naws_history":peer["naws_history"],"freeze_event_count":freeze,"naws_history_scope":"Each complete valid NAWS event parsed by the owned peer, including repeats","naws_changed_after_freeze":False, "planned_phase_count":5,"observed_phase_count":count,"grab_phases":captured["grab_phases"],"maximum_grabs_per_phase":1,"phases":public_phases, + "stop_guard_observation":stop_guard, "sample_color_comparisons":comparisons(captured_phases) if complete else None, "provider":{"evidence_id":evidence["evidence_id"],"provider":evidence["provider"],"confidence":evidence["confidence"],"subject":evidence["subject"],"environment":evidence["environment"],"messages_truncated":False,"cleanup_error":None,"limitations":evidence["limitations"]}, "lifecycle":{"outer_job":cleanup,"profile":profile,"deliberate_original_termination":True,"natural_gui_exit_verified":False}, diff --git a/research/ui/export_selection_counterexample.py b/research/ui/export_selection_counterexample.py new file mode 100644 index 00000000..73e9d297 --- /dev/null +++ b/research/ui/export_selection_counterexample.py @@ -0,0 +1,49 @@ +"""Project only endpoint/length counterexamples from one pinned prior record.""" +from __future__ import annotations +import argparse +import hashlib +import json +from pathlib import Path + +RAW_SHA256="be349abb34cf28f42b634ccf65bd0749e1e9940856059156b794e7968b7ef83c" +EVIDENCE_ID="ev_8fbd9aead9aa5d60f49e05112c31eb542febf65084643c025eb8a52ffd590048" + + +def require(condition,message): + if not condition:raise ValueError(message) + + +def counterexample_rows(evidence): + selected={} + for message in evidence["normalized_result"]["messages"]: + payload=message.get("payload") + if message["type"]=="send" and isinstance(payload,dict) and payload.get("kind")=="snapshot" and payload.get("phase") in ("shell-ready","styled-final"): + require(payload["phase"] not in selected,"Duplicate prior snapshot") + state=json.loads(payload["text_state"]) + caret,anchor,text=state.get("caret"),state.get("anchor"),state.get("selection") + require(type(caret) is int and type(anchor) is int and 0<=caret==anchor<=1000000,"Counterexample endpoints are not known collapsed integers") + require(isinstance(text,str) and 0 Date: Wed, 28 Oct 2026 23:51:04 +0800 Subject: [PATCH 6/8] research: prepare endpoint-only guard and same-frame raster controls (#129) --- CMakeLists.txt | 1 + .../windterm-raster-selection-plan.md | 49 ++++++ .../ui/export_raster_selection_reference.py | 109 ++++++++++++++ research/ui/raster_selection_analysis.py | 139 ++++++++++++++++++ research/ui/raster_selection_reference.js | 124 ++++++++++++++++ research/ui/raster_selection_reference.py | 120 +++++++++++++++ .../replay.json | 46 ++++++ research/ui/selection_guard_replay.py | 48 ++++++ research/ui/test_raster_selection_native.js | 56 +++++++ .../ui/test_raster_selection_reference.py | 120 +++++++++++++++ 10 files changed, 812 insertions(+) create mode 100644 docs/research/windterm-raster-selection-plan.md create mode 100644 research/ui/export_raster_selection_reference.py create mode 100644 research/ui/raster_selection_analysis.py create mode 100644 research/ui/raster_selection_reference.js create mode 100644 research/ui/raster_selection_reference.py create mode 100644 research/ui/results/2026-10-28-raster-selection-proposal/replay.json create mode 100644 research/ui/selection_guard_replay.py create mode 100644 research/ui/test_raster_selection_native.js create mode 100644 research/ui/test_raster_selection_reference.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 857ee6e6..01f1e4d8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -300,6 +300,7 @@ if(BUILD_TESTING) termforge_add_python_test(windterm_alternate_export -m unittest research.ui.test_export_alternate_reference -v) termforge_add_python_test(windterm_alternate_style_reference -m unittest research.ui.test_export_alternate_style_reference research.ui.test_alternate_style_peer -v) termforge_add_python_test(windterm_raster_style_reference -m unittest research.ui.test_raster_style_reference -v) + termforge_add_python_test(windterm_raster_selection_proposal -m unittest research.ui.test_raster_selection_reference -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) termforge_add_python_test(text_actions -m unittest experiments.shell.test_actions -v) diff --git a/docs/research/windterm-raster-selection-plan.md b/docs/research/windterm-raster-selection-plan.md new file mode 100644 index 00000000..7e45fdf9 --- /dev/null +++ b/docs/research/windterm-raster-selection-plan.md @@ -0,0 +1,49 @@ +# #129:端点守卫与同图颜色正控的独立计划 + +状态:仅离线准备,尚未启动新原版。#126 的执行源 `15c736b`、一次 phase0/0 grab/0 pixel 记录保持不变。本计划是独立新源,等待最终复核及明确执行指令;私有准备计划默认 `execution_authorized=false`,启动入口会拒绝该状态。 + +## 选区守卫与分开回放 + +生产读取已验证的 `selectionCaretP`、`selectionAnchorP`,只接受两个已知、非负、有限的安全整数且相等。先把 `selectionText` 投影为 type/length,再读取端点;非空文本不参与判定,字符串本身不进入新捕获记录。端点不同或未知均停止,不能只删除守卫。 + +[离线回放](../../research/ui/results/2026-10-28-raster-selection-proposal/replay.json)明确分开三个记录:两条历史原版反例的端点相等,选区门槛允许;旧 #126 实际 phase0 没有采集 P 端点,保持 null、门槛拒绝。历史端点没有移植给 #126 的 marker/ROI,门槛允许也不代表任何栅格或整条工作流通过。每个记录独立绑定原 Evidence ID 和原始文件 SHA。 + +保留实际 caret 对 H/space ROI 的重叠拒绝。对于 API 返回的零宽插入点,按至少 1 个逻辑像素的占用保守检查;H 与其前置 space 还必须同高、同行且精确相邻。没有缩小、平移 ROI 或假定光标 blink 隐藏。坐标与 DPR 均从新批实际返回值校准,不复用旧批尺寸。 + +## 五阶段新源 + +S1=`SGR31;44`,S2=`SGR32;45`,D0=`SGR0`。比较对象始终为字形 H 及其前一个显式空格,每阶段仍只抓一次 viewport。 + +| 阶段 | 原样目标及正控 | 最后当前 SGR | +|---|---|---| +| 0 | 先 `0m`、清屏并定位 `(2,3)` 写 `D0 H H`;再定位 `(4,3)`、S1 写 `M0 H H`。一张图内各有双 H 校准 | S1 | +| 1 | `1049h` 后不补 SGR 写 `A0 H`;同一行显式 S1 写 ` A0S1 H`,再 D0 写 ` A0D0 H` | D0 | +| 2 | 定位 `(4,5)`,显式 S2 写 `A1 H H`,双 H 校准 | S2 | +| 3 | 重复 `1049h` 后不补 SGR 写 `A2 H`;同一行依次 D0/S1/S2 写 ` A2D0 H`、` A2S1 H`、` A2S2 H` | **S2**,用于明确退出前状态 | +| 4 | `1049l` 后不补 SGR 写 ` M1 H`;同一行依次 S1/S2/D0 写 ` M1S1 H`、` M1S2 H`、` M1D0 H` | D0 | + +表格中的每个前导空格均是源字节。阶段 4 的第一个空格用于把保留的 M0 标签与新 M1 标签分成完整 token,不改变 SGR。完整源由 [Python 生产器](../../research/ui/raster_selection_reference.py)的 `phases()` 固定,hex、长度和 SHA 写入准备计划;所有行末都额外发送 `CSI 6n`,随后等待独立阶段确认。 + +所有标签按**完整 token**查找,不把 `M1S1 H` 当成 `M1 H`,也不匹配标签前后的其他字符。每个 marker 的 label、组别、字形偏移和编号独立核对。同帧正控必须与原样目标处在同一文档行及同一逻辑绘制行。五阶段最长行小于 40 列,source 冻结后逐条完整 NAWS 守卫不变;不重做模式矩阵。 + +## 比较规则 + +阶段 0 的 D0/M0、阶段 2 的 S2 都先校验同图双 H 的背景与主要墨迹颜色一致。阶段 1、3、4 优先列出目标与**同帧同一行**显式正控的 RGBA 匹配列表。一个匹配可记录其正控名称;多个匹配、没有匹配或颜色不可区分时,唯一分类均为 null。空列表不能推出 reset、默认色或原版缺陷。 + +跨 frame 的 D0/S1/S2 基准匹配另列,不能替代同帧正控,也不消除 UI 高亮/基线差异。字段始终命名 `dominant_ink_rgba` / `background_rgba`,描述受控栅格输出,不冒称内部 pen 或完整 current_style。 + +## 原生与执行边界 + +[GUI 脚本](../../research/ui/raster_selection_reference.js)沿用已只读核实的固定 Qt5Widgets/Qt5Gui 完整 SHA、模块路径和导出 RVA,GUI 线程执行 QWidget::grab→QPixmap::toImage→QImage::pixel。QPixmap/QImage 各 32B、不预构造;正常返回后按 image→pixmap 普通析构。QRect 为 inclusive 边界,不读取或猜测 format/stride。既有 Qt 栅格计划提供静态 ABI 依据;旧 #126 没有实际调用这些函数,不能转记为原生栅格已验证。 + +仍为独立 station/desktop、OwnedJob、合成 loopback/profile、一个新唯一目录。新批最多启动原版一次、每阶段至多一次 grab,共最多五次;capture45、CLI wait60,整程上限另列未知。当前只是离线计划,没有授权标记、attempt marker 或新执行结果。 + +空图、无墨迹、DPR/坐标/端点/caret/标签不成立即保存 partial/unknown 并停止,不移动 ROI、追加抓取或重跑。只公开合成输入、颜色/尺寸标量和 ROI 哈希;原始像素、截图、路径、PID 和资源均留 local。受控栅格不代表 presented 或性能结果。 + +## 复核材料 + +- [显式离线校验与分析](../../research/ui/raster_selection_analysis.py)、[发布门槛](../../research/ui/export_raster_selection_reference.py):不使用可被 `-O` 删除的 assert;未知与不唯一比较不提升为结论。 +- [回放投影器](../../research/ui/selection_guard_replay.py):核对 #126 原文件 SHA 后确认端点确实未捕获;不由 `caretPosition` 补造 P 端点。 +- [公共回归](../../research/ui/test_raster_selection_reference.py)和[原生调用合成边界](../../research/ui/test_raster_selection_native.js):覆盖历史/缺端点分开回放、非空文本但 collapsed 可继续、noncollapsed/未知仍拒绝、零宽 caret 重叠、完整 token、同帧默认色/主色匹配及歧义、32B sret/析构和未获执行授权不得启动。 + +本提案不改旧 #118/#123/#126 树或记录,不把不同运行的成功拼成联合验收,也不解除当前样式未知项。最终源与准备计划交复核后,再根据独立执行指令启动新的一次。 diff --git a/research/ui/export_raster_selection_reference.py b/research/ui/export_raster_selection_reference.py new file mode 100644 index 00000000..2ff0823b --- /dev/null +++ b/research/ui/export_raster_selection_reference.py @@ -0,0 +1,109 @@ +"""Independent #129 endpoint/default/same-frame raster publication gate.""" +from __future__ import annotations +import argparse +import hashlib +import json +from pathlib import Path +import re +import sys + +ROOT=Path(__file__).resolve().parents[2] +sys.path.insert(0,str(ROOT)) +from research.ui.raster_selection_reference import MODULE_SHA256,SOURCE_FILES,MANIFEST_SHA256,phases +from research.ui.raster_selection_analysis import audit_capture,comparisons,require + +SYNTHETIC_LINE=re.compile(r" *(?:(?:D0 H H|M0 H H|A0 H|A0S1 H|A0D0 H|A1 H H|A2 H|A2D0 H|A2S1 H|A2S2 H|M1 H|M1S1 H|M1S2 H|M1D0 H) *)*") +REASONS={None, + "selection-endpoints-unavailable","selection-not-collapsed","default-calibration-H-colors-differ","main-calibration-H-colors-differ","alternate-calibration-H-colors-differ", + "pinned-module-path","pinned-export-rva","invalid-marker-rectangle","noninteger-roi-boundary","roi-out-of-bounds","space-background-ambiguous","no-marker-ink","dominant-ink-ambiguous", + "owned-viewport-ambiguous","viewport-dimensions","mode-getters-unavailable","marker-binding-ambiguous","extra-grab-for-phase","pixmap-return-address","image-return-address","empty-image", + "dpr-inconsistent","image-viewport-dpr-inconsistent","caret-location-unknown","marker-text-mismatch","caret-overlaps-roi","h-space-size-inconsistent","h-space-adjacency-inconsistent","native-or-observation-error", + "object-destruction-failed","first-run-guard","scenario-deadline","peer-failed","driver-observation-error","bounded-observation-timer"} + + +def export(directory,plan_path,destination): + sha=lambda path:hashlib.sha256(path.read_bytes()).hexdigest() + read=lambda name:json.loads((directory/name).read_text(encoding="utf-8")) + plan=json.loads(plan_path.read_text(encoding="utf-8")) + run,peer,captured=read("run.json"),read("peer.json"),read("raster-result.json") + outer=read("evidence.json");evidence=outer["data"]["evidence"];normalized=evidence["normalized_result"] + require(plan["issue"]==129 and plan.get('execution_authorized') is True and plan["maximum_original_runs"]==1 and plan["phase_count"]==5,"Plan scope or execution authorization differs") + require(plan["maximum_grabs_per_phase"]==1 and plan["maximum_grabs"]==5 and re.fullmatch(r"[0-9a-f]{40}",plan["source_base_commit"]),"Plan capture/source bound differs") + require(plan["rea_capture_seconds"]==45 and plan["cli_wait_timeout_seconds"]==60 and plan["whole_lifecycle_bound_seconds"] is None,"Plan duration differs") + require(plan["module_sha256"]==MODULE_SHA256 and plan["files_manifest_sha256"]==MANIFEST_SHA256,"Module/package plan identity differs") + require(run["sha256"]==MODULE_SHA256["WindTerm.exe"] and run["files_sha256"]==MANIFEST_SHA256,"Observed package identity differs") + require(set(plan["source_files_sha256"])==set(SOURCE_FILES),"Executed source inventory differs") + require(all(sha(ROOT/name)==expected for name,expected in plan["source_files_sha256"].items()),"Executed source changed") + require(normalized["source_sha256"]==run["script_sha256"]==sha(directory/"probe.js"),"Injected source differs") + require(outer["ok"] and not outer["data"]["cleanup_error"] and not normalized["cleanup_error"] and not normalized["messages_truncated"],"Capture incomplete or cleanup failed") + require(not any(message["type"]=="error" for message in normalized["messages"]),"Provider script error") + require(evidence["provider"]=={"id":"frida","name":"Frida","version":None} and evidence["confidence"]=="observed" and evidence["subject"] is None and evidence["environment"] is None,"Provider identity differs") + require(re.fullmatch(r"ev_[0-9a-f]{64}",evidence["evidence_id"]),"Provider evidence identity differs") + require(all(value in {"Artifact identity is unavailable for this observation.","The target operating system and architecture were not reported by this observation."} for value in evidence["limitations"]),"Unexpected provider limitation text") + messages=[message["payload"] for message in normalized["messages"] if message["type"]=="send" and isinstance(message.get("payload"),dict)] + captured_phases=[{key:value for key,value in message.items() if key!="kind"} for message in messages if message.get("kind")=="raster_phase"] + finals=[{key:value for key,value in message.items() if key!="kind"} for message in messages if message.get("kind")=="raster_result"] + require(captured_phases==captured["captures"] and finals==[captured],"Private captures differ from provider messages") + require(captured["status"] in ("observed-five-raster-phases","partial-raster-observation") and captured["reason"] in REASONS,"Unknown capture status/reason") + guards=captured["module_guards"] + require({guard["name"] for guard in guards}==set(MODULE_SHA256) and len(guards)==4 and all(guard=={"name":guard["name"],"path_guard_passed":True} for guard in guards),"Runtime module path guard failed") + count=len(captured_phases);require(0<=count<=5,"Too many captures") + expected=phases(*reversed(peer["frozen_grid"])) + require(peer["frozen_grid"]==[peer["columns"],peer["rows"]],"Frozen grid changed") + freeze=peer["freeze_event_count"] + require(type(freeze) is int and 0<=freeze<=len(peer["naws_history"])<=64,"NAWS event bound differs") + require(all(grid==peer["frozen_grid"] for grid in peer["naws_history"][freeze:]),"NAWS changed after freeze") + require(all(isinstance(grid,list) and len(grid)==2 and all(type(value) is int and 0>16)&255,(value>>8)&255,value&255,(value>>24)&255] + + +def analyze_pixels(h_pixels,space_pixels): + require(h_pixels and space_pixels and len(h_pixels)<=8192 and len(space_pixels)<=8192,"Empty or oversized ROI") + for value in h_pixels+space_pixels: + rgba(value) + background=space_pixels[0] + require(all(value==background for value in space_pixels),"space-background-ambiguous") + ink=Counter(value for value in h_pixels if value!=background) + require(ink,"no-marker-ink") + ordered=sorted(ink.items(),key=lambda item:(-item[1],item[0])) + require(len(ordered)==1 or ordered[0][1]>ordered[1][1],"dominant-ink-ambiguous") + return {"background_rgba":rgba(background),"dominant_ink_rgba":rgba(ordered[0][0])} + + +def pixel_summary(pixels): + for value in pixels: + rgba(value) + counts=Counter(pixels) + require(00 and h>0 and w*h<=8192 and x>=0 and y>=0 and x+w<=width and y+h<=height,"ROI outside image") + + +def audit_capture(capture): + """Return a scalar-only projection, retaining calibrated/unknown boundaries.""" + public={key:capture[key] for key in ("phase","label","cursor_reply","grab_attempted","constructed","destroyed","calibration","reason")} + require(type(capture["calibration"]) is bool,"Calibration status missing") + require(capture["constructed"] in ([],["pixmap"],["pixmap","image"]),"Construction order differs") + require(capture["destroyed"]==list(reversed(capture["constructed"])) or (not capture["calibration"] and capture["reason"]=="object-destruction-failed"),"Destruction order differs") + image=capture["image"] + if image is not None: + require(all(type(image[key]) is int and 0<=image[key]<=4096 for key in ("width","height","viewport_width","viewport_height")),"Image scalar bound differs") + require(type(image["is_null"]) is bool,"Image null state missing") + require(image["grab_rect"]==[0,0,image["viewport_width"]-1,image["viewport_height"]-1],"Grab QRect is not inclusive") + require(all(image[key] is None or (type(image[key]) in (int,float) and math.isfinite(image[key]) and abs(image[key])<=4) for key in ("pixmap_dpr","image_dpr")),"DPR scalar bound differs") + if capture["calibration"]: + require(capture["grab_attempted"] is True and capture["constructed"]==["pixmap","image"] and capture["destroyed"]==["image","pixmap"],"Calibrated image lifecycle differs") + require(image and image["is_null"] is False,"Empty calibrated image") + pdpr,idpr=image["pixmap_dpr"],image["image_dpr"] + require(type(pdpr) in (int,float) and math.isfinite(pdpr) and 0=0 and cursor['height']>0,'caret-location-unknown') + occupancy={**cursor,'width':max(1,cursor['width'])} + for logical in (marker['h_rect'],marker['space_rect']): + intersects=logical['x']>>16)&255,(value>>>8)&255,value&255,(value>>>24)&255];} +function requireCollapsedSelection(caret,anchor){must(Number.isSafeInteger(caret)&&Number.isSafeInteger(anchor)&&caret>=0&&anchor>=0,'selection-endpoints-unavailable');must(caret===anchor,'selection-not-collapsed');return true;} +function completeTokenIndices(line,label){var result=[];for(var at=line.indexOf(label);at>=0;at=line.indexOf(label,at+1)){var end=at+label.length;if((at===0||line.charAt(at-1)===' ')&&(end===line.length||line.charAt(end)===' '))result.push(at);}return result;} +function terminalState(index){ + const descriptor=targets[index]||{markers:[]}; + const source=`JSON.stringify((function(){var t=window.activeText;if(!t)return null;var descriptors=${JSON.stringify(descriptor.markers)};${completeTokenIndices.toString()}function get(k){try{var v=typeof t[k]==='function'?t[k]():t[k];return v===undefined?null:v;}catch(e){return null;}}var count=get('visibleLineCount'),lines=[],found=[];if(typeof count==='number'&&count>=0&&count<=200){for(var i=0;i=0?t.visibleRectFromPosition(caret):null};})())`; + const result=evalText(source);return result?JSON.parse(result):null; +} +function overlap(a,b){return a.x=0&&rect.height>0,'caret-location-unknown');return {x:rect.x,y:rect.y,width:Math.max(1,rect.width),height:rect.height};} +function physicalRect(rect,dpr,width,height){ + must(rect&&[rect.x,rect.y,rect.width,rect.height].every(x=>Number.isFinite(x)),'invalid-marker-rectangle'); + const values=[rect.x*dpr,rect.y*dpr,rect.width*dpr,rect.height*dpr]; + must(values.every(Number.isInteger),'noninteger-roi-boundary'); + const [x,y,w,h]=values;must(w>0&&h>0&&w*h<=8192&&x>=0&&y>=0&&x+w<=width&&y+h<=height,'roi-out-of-bounds');return [x,y,w,h]; +} +function readRoi(image,rectangle){const [x,y,w,h]=rectangle,pixels=[];for(let row=y;row>>0);return pixels;} +function colors(h,space){ + const background=space[0];must(space.length>0&&space.every(value=>value===background),'space-background-ambiguous'); + const counts=new Map();for(const value of h)if(value!==background)counts.set(value,(counts.get(value)||0)+1); + must(counts.size>0,'no-marker-ink'); + const ordered=Array.from(counts).sort((a,b)=>b[1]-a[1]||a[0]-b[0]); + must(ordered.length===1||ordered[0][1]>ordered[1][1],'dominant-ink-ambiguous'); + return {background_rgba:rgba(background),dominant_ink_rgba:rgba(ordered[0][0])}; +} +function sampleViewport(index,terminal,cursor){ + const capture={phase:index,label:targets[index].label,cursor_reply:cursor,terminal,grab_attempted:false,module_guards:moduleGuards,image:null,rois:[],constructed:[],destroyed:[],calibration:null,reason:null}; + const pixmap=Memory.alloc(32),image=Memory.alloc(32);let pixmapReady=false,imageReady=false; + try{ + scan();const candidates=cached.filter(row=>row.name==='qt_scrollarea_viewport'&&row.parent===editor.toString());must(candidates.length===1,'owned-viewport-ambiguous'); + const viewport=ptr(candidates[0].ptr),g=rect(viewport),width=g[2]-g[0]+1,height=g[3]-g[1]+1; + must(width>0&&height>0&&width<=4096&&height<=4096,'viewport-dimensions'); + must(typeof terminal.is_alt==='boolean'&&typeof terminal.is_main==='boolean'&&terminal.is_alt!==terminal.is_main,'mode-getters-unavailable'); + requireCollapsedSelection(terminal.selection_caret,terminal.selection_anchor); + must(terminal.markers.length===targets[index].markers.length,'marker-binding-ambiguous'); + const region=Memory.alloc(16);region.writeS32(0);region.add(4).writeS32(0);region.add(8).writeS32(width-1);region.add(12).writeS32(height-1); + must(!grabs.includes(index),'extra-grab-for-phase');grabs.push(index);capture.grab_attempted=true; + const returnedPixmap=grab(viewport,pixmap,region);pixmapReady=true;capture.constructed.push('pixmap');must(returnedPixmap.equals(pixmap),'pixmap-return-address'); + const returnedImage=toImage(pixmap,image);imageReady=true;capture.constructed.push('image');must(returnedImage.equals(image),'image-return-address'); + const pdpr=pixmapDpr(pixmap),idpr=imageDpr(image),iw=imageWidth(image),ih=imageHeight(image),empty=imageNull(image); + capture.image={viewport_width:width,viewport_height:height,grab_rect:[0,0,width-1,height-1],pixmap_dpr:pdpr,image_dpr:idpr,width:iw,height:ih,is_null:empty}; + must(!empty&&iw>0&&ih>0,'empty-image'); + must(Number.isFinite(pdpr)&&pdpr>0&&pdpr<=4&&pdpr===idpr,'dpr-inconsistent'); + must(Number.isInteger(width*pdpr)&&Number.isInteger(height*pdpr)&&iw===width*pdpr&&ih===height*pdpr,'image-viewport-dpr-inconsistent'); + const caret=caretOccupancy(terminal.caret_rect); + for(const marker of terminal.markers){ + must(marker.character==='H'&&marker.preceding_character===' ','marker-text-mismatch'); + must(!overlap(marker.h_rect,caret)&&!overlap(marker.space_rect,caret),'caret-overlaps-roi'); + const hRect=physicalRect(marker.h_rect,pdpr,iw,ih),spaceRect=physicalRect(marker.space_rect,pdpr,iw,ih); + must(hRect[2]===spaceRect[2]&&hRect[3]===spaceRect[3],'h-space-size-inconsistent'); + must(hRect[1]===spaceRect[1]&&spaceRect[0]+spaceRect[2]===hRect[0],'h-space-adjacency-inconsistent'); + const hPixels=readRoi(image,hRect),spacePixels=readRoi(image,spaceRect); + const record={ordinal:marker.ordinal,group:marker.group,sample:marker.sample,h_rect:hRect,space_rect:spaceRect,h_pixels:hPixels,space_pixels:spacePixels,colors:null};capture.rois.push(record); + record.colors=colors(hPixels,spacePixels); + } + if(index===0){must(same(capture.rois[0].colors,capture.rois[1].colors),'default-calibration-H-colors-differ');must(same(capture.rois[2].colors,capture.rois[3].colors),'main-calibration-H-colors-differ');} + if(index===2)must(same(capture.rois[0].colors,capture.rois[1].colors),'alternate-calibration-H-colors-differ'); + capture.calibration=true; + }catch(error){capture.calibration=false;capture.reason=error.code||'native-or-observation-error';capture.private_error=String(error);} + finally{ + for(const object of [{ready:imageReady,name:'image',value:image,destroy:destroyImage},{ready:pixmapReady,name:'pixmap',value:pixmap,destroy:destroyPixmap}]){ + if(object.ready)try{object.destroy(object.value);capture.destroyed.push(object.name);} + catch(error){capture.calibration=false;capture.reason='object-destruction-failed';capture.private_error=String(error);} + } + } + return capture; +} +function finish(status,reason){ + if(finished)return;finished=true; + const result={status,reason:reason||null,captures,grab_phases:grabs,module_guards:moduleGuards,presented_done:null}; + const file=new File(__RUN_DIRECTORY_JSON__+'/raster-result.json','w');file.write(JSON.stringify(result));file.close();send({kind:'raster_result',...result}); +} +send({kind:'probe_ready',station_isolated:true,scenario:'five-stage-raster-selection-default',module_guards:moduleGuards}); +Interceptor.attach(user.getExportByName('DispatchMessageW'),{onEnter(){ + if(busy||finished||Process.getCurrentThreadId()!==uiThread||Date.now()finish('partial-raster-observation','bounded-observation-timer'),42000); diff --git a/research/ui/raster_selection_reference.py b/research/ui/raster_selection_reference.py new file mode 100644 index 00000000..5b86ac26 --- /dev/null +++ b/research/ui/raster_selection_reference.py @@ -0,0 +1,120 @@ +"""Independent endpoint/default/same-frame raster correctness probe (#129).""" +from __future__ import annotations +import argparse +import hashlib +import json +from pathlib import Path +import subprocess +import sys + +ROOT=Path(__file__).resolve().parents[2] +sys.path.insert(0,str(ROOT)) +from research.ui.alternate_style_reference import peer_class, require + +MODULE_SHA256={ + "WindTerm.exe":"7b10531dccc7cd614ae559f36f1ef518e0c96335637431b4646354823b2804d0", + "Qt5Core.dll":"8d2ff4ce9096ddccc4f4cd62c2e41fc854cfd1b0d6e8d296645a7f5fd4ae565a", + "Qt5Gui.dll":"5e7d2d41b8b92a880e83b8cc0ca173f5da61218604186196787ee1600956be1e", + "Qt5Widgets.dll":"3788c669d4b645e5a576de9fc77fca776bf516d43c89143dc2ca28291ba14358", +} +SOURCE_FILES=("research/ui/raster_selection_reference.py","research/ui/raster_selection_reference.js","research/ui/alternate_style_reference.py","research/ui/alternate_reference.py","research/ui/r1_launch.py","research/ui/launch.py","research/ui/qt_r1_text.js") +MANIFEST_SHA256="e459adf2f12adb6a1b7f55e66a9da1c22794185e66e848057d21e0c60d669203" + + +def phases(rows,columns): + require(10<=rows<=60 and 40<=columns<=160,"Controlled grid out of bounds") + return [ + ("default-and-main-calibration",b"\x1b[0m\x1b[2J\x1b[H\x1b[2;3HD0 H H\x1b[4;3H\x1b[31;44mM0 H H"), + ("alternate-entry-with-controls",b"\x1b[?1049hA0 H\x1b[31;44m A0S1 H\x1b[0m A0D0 H"), + ("alternate-reference",b"\x1b[4;5H\x1b[32;45mA1 H H"), + ("alternate-repeat-with-controls",b"\x1b[?1049hA2 H\x1b[0m A2D0 H\x1b[31;44m A2S1 H\x1b[32;45m A2S2 H"), + ("main-return-with-controls",b"\x1b[?1049l M1 H\x1b[31;44m M1S1 H\x1b[32;45m M1S2 H\x1b[0m M1D0 H"), + ] + + +_BasePeer=peer_class() +_BasePeer._serve.__globals__["phases"]=phases + + +class Peer(_BasePeer): + def _record(self): + if self.state.get("frozen_grid") and "freeze_event_count" not in self.state: + self.state["freeze_event_count"]=len(self.state.get("naws_history",[])) + super()._record() + + +def sha(path): + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def check_full_hashes(directory,identities=MODULE_SHA256): + measured={name:sha(directory/name) for name in identities} + require(measured==identities,"Pinned executable or Qt DLL full hash differs") + return measured + + +def prepare_plan(source,out,rea,destination,execution_authorized=False): + source,out,rea,destination=[path.resolve() for path in (source,out,rea,destination)] + require(not out.exists() and not destination.exists(),"Plan/output must be new") + require(out.is_relative_to(ROOT/"local") and destination.is_relative_to(ROOT/"local"),"Plan/output must belong to this worktree local directory") + require(rea.is_file(),"Locked REA CLI is missing") + package=rea.parent.parent/"rea-agents/package.json" + require(json.loads(package.read_text(encoding="utf-8"))["version"]=="6.7.0","REA version differs") + measured=check_full_hashes(source) + plan={"schema":"termforge-raster-selection-plan-v1","issue":129,"maximum_original_runs":1,"execution_authorized":execution_authorized, + "source_base_commit":subprocess.check_output(["git","rev-parse","HEAD"],cwd=ROOT,text=True).strip(), + "source":str(source),"out":str(out),"rea":str(rea),"module_sha256":measured,"files_manifest_sha256":MANIFEST_SHA256, + "rea_capture_seconds":45,"cli_wait_timeout_seconds":60,"whole_lifecycle_bound_seconds":None, + "source_files_sha256":{name:sha(ROOT/name) for name in SOURCE_FILES}, + "phases":[{"phase":index,"label":label,"source_hex":content.hex(),"source_bytes":len(content),"source_sha256":hashlib.sha256(content).hexdigest(),"dsr_suffix_hex":"1b5b366e"} for index,(label,content) in enumerate(phases(25,91))], + "phase_count":5,"maximum_grabs_per_phase":1,"maximum_grabs":5, + "scope":"Independent collapsed-endpoint guard and same-image default/main H references; H/space output colors only, no presented/performance claim", + "selection_guard":"Known nonnegative safe-integer selectionCaretP equals selectionAnchorP; selectionText is retained only as type/length", + "stop":"Empty image, DPR/coordinates, caret/selection or ink calibration failure stops this one run; no shifted ROI, extra capture or retry"} + destination.parent.mkdir(parents=True,exist_ok=True) + destination.write_text(json.dumps(plan,indent=2)+"\n",encoding="utf-8",newline="\n") + print(json.dumps({"prepared":True,"phase_count":5,"maximum_original_runs":1,"plan":destination.as_posix()})) + + +def run_plan(plan_path): + plan_path=plan_path.resolve() + plan=json.loads(plan_path.read_text(encoding="utf-8")) + require(plan["issue"]==129 and plan["maximum_original_runs"]==1,"Unexpected raster plan") + require(plan.get("execution_authorized") is True,"Offline proposal only; new original execution has not been authorized") + require(plan["source_base_commit"]==subprocess.check_output(["git","rev-parse","HEAD"],cwd=ROOT,text=True).strip(),"Source commit changed after plan") + require(plan["module_sha256"]==MODULE_SHA256,"Plan module identity differs") + require(plan["source_files_sha256"]=={name:sha(ROOT/name) for name in SOURCE_FILES},"Probe source changed after plan") + require(plan["phases"]==[{"phase":index,"label":label,"source_hex":content.hex(),"source_bytes":len(content),"source_sha256":hashlib.sha256(content).hexdigest(),"dsr_suffix_hex":"1b5b366e"} for index,(label,content) in enumerate(phases(25,91))],"Plan source differs") + source,out,rea=[Path(plan[key]) for key in ("source","out","rea")] + check_full_hashes(source) + require(not out.exists(),"This run output already exists") + # Exclusive marker is deliberately retained after every attempted launch. + with plan_path.with_suffix(".attempted").open("x",encoding="utf-8") as marker: + marker.write("One authorized original launch attempt; never retry this plan.\n") + from research.ui import r1_launch + launcher=Path(r1_launch.__file__) + code=launcher.read_text(encoding="utf-8") + replacements={"from research.ui.r1_vt_fixture import Peer":"from research.ui.raster_selection_reference import Peer", + 'Path(__file__).with_name("r1_menu_wrap.js").read_text(encoding="utf-8")':'Path(__file__).with_name("raster_selection_reference.js").read_text(encoding="utf-8")'} + for old,new in replacements.items(): + require(code.count(old)==1,"Protected launcher source changed") + code=code.replace(old,new) + namespace={"__file__":str(launcher),"__name__":"termforge_raster_launcher"} + exec(compile(code,str(launcher),"exec"),namespace) + namespace["run"](source,out,rea,45,False,"menu-wrap") + + +if __name__=="__main__": + parser=argparse.ArgumentParser() + parser.add_argument("--plan",type=Path,required=True) + parser.add_argument("--prepare",action="store_true") + parser.add_argument("--authorize-execution",action="store_true",help="Use only after a separate explicit authorization of this new run") + parser.add_argument("--source",type=Path) + parser.add_argument("--out",type=Path) + parser.add_argument("--rea",type=Path) + args=parser.parse_args() + if args.prepare: + require(args.source and args.out and args.rea,"Source/output/REA required") + prepare_plan(args.source,args.out,args.rea,args.plan,args.authorize_execution) + else: + run_plan(args.plan) diff --git a/research/ui/results/2026-10-28-raster-selection-proposal/replay.json b/research/ui/results/2026-10-28-raster-selection-proposal/replay.json new file mode 100644 index 00000000..12ae8d83 --- /dev/null +++ b/research/ui/results/2026-10-28-raster-selection-proposal/replay.json @@ -0,0 +1,46 @@ +{ + "schema": "termforge-selection-gate-replay-v1", + "issue": 129, + "scope": "Offline selection guard only; no original execution, native grab, pixels or combined-run acceptance", + "records": [ + { + "id": "historical-collapsed-0", + "source_evidence_id": "ev_8fbd9aead9aa5d60f49e05112c31eb542febf65084643c025eb8a52ffd590048", + "source_file_sha256": "be349abb34cf28f42b634ccf65bd0749e1e9940856059156b794e7968b7ef83c", + "selection_caret": 97, + "selection_anchor": 97, + "selection_text_type": "string", + "selection_text_length": 5, + "selection_gate_allowed": true, + "reason": null + }, + { + "id": "historical-collapsed-1", + "source_evidence_id": "ev_8fbd9aead9aa5d60f49e05112c31eb542febf65084643c025eb8a52ffd590048", + "source_file_sha256": "be349abb34cf28f42b634ccf65bd0749e1e9940856059156b794e7968b7ef83c", + "selection_caret": 400, + "selection_anchor": 400, + "selection_text_type": "string", + "selection_text_length": 10, + "selection_gate_allowed": true, + "reason": null + }, + { + "id": "r126-actual-phase0-missing-endpoints", + "source_evidence_id": "ev_cedd36bf326d1769fa8d0d7b1cc508314eb22da43ba97faf80debbcf9b085e63", + "source_file_sha256": "6724c93a7da8f0f08d50f360ba43b01cad4f272a88d82919cc0f75c486be18f0", + "selection_caret": null, + "selection_anchor": null, + "selection_text_type": "string", + "selection_text_length": 10, + "selection_gate_allowed": false, + "reason": "selection-endpoints-unavailable" + } + ], + "endpoints_and_rois_from_different_runs_combined": false, + "limitations": [ + "Allowing a historical selection gate does not mean that its raster or ROI pipeline passed.", + "R126 did not capture selectionCaretP/selectionAnchorP. caretPosition is not substituted and no equal endpoint is fabricated.", + "Noncollapsed or unavailable endpoints still reject; selectionText type/length is not a selection verdict." + ] +} diff --git a/research/ui/selection_guard_replay.py b/research/ui/selection_guard_replay.py new file mode 100644 index 00000000..11c19455 --- /dev/null +++ b/research/ui/selection_guard_replay.py @@ -0,0 +1,48 @@ +"""Separate selection-gate replays; never join endpoints with another run's ROI.""" +from __future__ import annotations +import argparse +import hashlib +import json +from pathlib import Path +import sys + +ROOT=Path(__file__).resolve().parents[2] +sys.path.insert(0,str(ROOT)) +from research.ui.raster_selection_analysis import require,require_collapsed + + +def decision(caret,anchor): + try:require_collapsed(caret,anchor) + except ValueError as error:return {"selection_gate_allowed":False,"reason":str(error)} + return {"selection_gate_allowed":True,"reason":None} + + +def make_replay(r126_result,destination): + prior=json.loads((ROOT/"research/ui/results/2026-10-28-raster-style-reference/selection-getter-counterexample.json").read_text(encoding="utf-8")) + current=json.loads((ROOT/"research/ui/results/2026-10-28-raster-style-reference/validation.json").read_text(encoding="utf-8")) + raw=r126_result.read_bytes() + raw_sha=hashlib.sha256(raw).hexdigest() + require(raw_sha==current["raw_files"]["raster-result.json"]["sha256"],"R126 private phase0 identity differs") + original=json.loads(raw) + require(original["status"]=="partial-raster-observation" and original["grab_phases"]==[] and len(original["captures"])==1,"R126 boundary differs") + terminal=original["captures"][0]["terminal"] + require("selection_caret" not in terminal and "selection_anchor" not in terminal,"R126 unexpectedly contains selection endpoints") + rows=[] + for index,row in enumerate(prior["counterexamples"]): + caret,anchor=row["caret"],row["anchor"] + rows.append({"id":f"historical-collapsed-{index}","source_evidence_id":prior["source_evidence_id"],"source_file_sha256":prior["source_file_sha256"], + "selection_caret":caret,"selection_anchor":anchor,"selection_text_type":"string","selection_text_length":row["selection_length"],**decision(caret,anchor)}) + text=terminal.get("selection") + rows.append({"id":"r126-actual-phase0-missing-endpoints","source_evidence_id":current["provider"]["evidence_id"],"source_file_sha256":raw_sha, + "selection_caret":None,"selection_anchor":None,"selection_text_type":"string" if isinstance(text,str) else "null" if text is None else "other", + "selection_text_length":len(text) if isinstance(text,str) else None,**decision(None,None)}) + result={"schema":"termforge-selection-gate-replay-v1","issue":129,"scope":"Offline selection guard only; no original execution, native grab, pixels or combined-run acceptance", + "records":rows,"endpoints_and_rois_from_different_runs_combined":False, + "limitations":["Allowing a historical selection gate does not mean that its raster or ROI pipeline passed.","R126 did not capture selectionCaretP/selectionAnchorP. caretPosition is not substituted and no equal endpoint is fabricated.","Noncollapsed or unavailable endpoints still reject; selectionText type/length is not a selection verdict."]} + destination.parent.mkdir(parents=True,exist_ok=True) + destination.write_text(json.dumps(result,indent=2)+"\n",encoding="utf-8",newline="\n") + print(json.dumps({"replay_records":len(rows),"gate_allowed":[row["selection_gate_allowed"] for row in rows],"new_original_runs":0})) + + +if __name__=="__main__": + parser=argparse.ArgumentParser();parser.add_argument("--r126-result",type=Path,required=True);parser.add_argument("--destination",type=Path,required=True);args=parser.parse_args();make_replay(args.r126_result,args.destination) diff --git a/research/ui/test_raster_selection_native.js b/research/ui/test_raster_selection_native.js new file mode 100644 index 00000000..6d664089 --- /dev/null +++ b/research/ui/test_raster_selection_native.js @@ -0,0 +1,56 @@ +'use strict'; +// Run the production helper bodies with synthetic Qt calls, without Frida/Qt. +const fs=require('fs'),vm=require('vm'),assert=require('assert'); +const source=fs.readFileSync(require('path').join(__dirname,'raster_selection_reference.js'),'utf8'); +function helpers(extra={}){ + const context=vm.createContext({Number,Map,JSON,Error,...extra}); + vm.runInContext(source.slice(source.indexOf('function modulePathMatches'),source.indexOf('const moduleGuards')),context); + vm.runInContext(source.slice(source.indexOf('function must'),source.indexOf('function terminalState')),context); + vm.runInContext(source.slice(source.indexOf('function overlap'),source.indexOf('function finish')),context); + return context; +} +function equal(a,b){assert.strictEqual(JSON.stringify(a),JSON.stringify(b));} +const pure=helpers(); +assert(pure.modulePathMatches('D:\\OWNED\\app\\Qt5Gui.dll','d:/owned','Qt5Gui.dll')); +assert(!pure.modulePathMatches('D:/other/app/Qt5Gui.dll','d:/owned','Qt5Gui.dll')); +equal(pure.rgba(0xff123456),[0x12,0x34,0x56,0xff]); +assert.throws(()=>pure.physicalRect({x:.5,y:0,width:1,height:1},1,8,2),/noninteger/); +assert.throws(()=>pure.physicalRect({x:7,y:0,width:2,height:1},1,8,2),/out-of-bounds/); +assert.throws(()=>pure.colors([1,1],[1,1]),/no-marker-ink/); +assert.throws(()=>pure.colors([2,3],[1,1]),/dominant-ink-ambiguous/); +assert.throws(()=>pure.colors([2,2],[1,3]),/space-background-ambiguous/); +assert(pure.requireCollapsedSelection(97,97)); +assert.throws(()=>pure.requireCollapsedSelection(97,98),/selection-not-collapsed/); +for(const value of [null,undefined,NaN,Infinity,-1,.5,'97',true,9007199254740992])assert.throws(()=>pure.requireCollapsedSelection(value,value),/selection-endpoints-unavailable/); +equal(pure.completeTokenIndices('M1S1 H','M1 H'),[]); +equal(pure.completeTokenIndices(' M1 H M1S1 H','M1 H'),[1]); +equal(pure.completeTokenIndices('XM1 H','M1 H'),[]); +assert(pure.overlap({x:0,y:0,width:2,height:2},pure.caretOccupancy({x:0,y:0,width:0,height:2}))); +assert(!pure.overlap({x:0,y:0,width:2,height:2},pure.caretOccupancy({x:2,y:0,width:0,height:2}))); +const replay=JSON.parse(fs.readFileSync(require('path').join(__dirname,'results/2026-10-28-raster-selection-proposal/replay.json'),'utf8')); +for(const row of replay.records){let allowed=true,reason=null;try{pure.requireCollapsedSelection(row.selection_caret,row.selection_anchor);}catch(error){allowed=false;reason=error.code;}assert.strictEqual(allowed,row.selection_gate_allowed);assert.strictEqual(reason,row.reason);} +function lifecycle(failure){ + const calls=[],allocations=[]; + function pointer(){const result={data:[],equals(other){return result===other;},writeS32(value){result.data[0]=value;},add(offset){return {writeS32(value){result.data[offset/4]=value;}};}};return result;} + const editor={toString(){return 'owned-editor';}},viewport={}; + const markers=[0,1,2,3].map(ordinal=>({ordinal,group:ordinal<2?'default':'main',sample:ordinal%2,character:'H',preceding_character:' ',h_rect:{x:ordinal*4+2,y:0,width:2,height:2},space_rect:{x:ordinal*4+(failure==='adjacency'?1:0),y:0,width:2,height:2}})); + const context=helpers({targets:[{label:'D0/M0 H references',markers:[{},{},{},{}]}],moduleGuards:[],captures:[],grabs:[],editor, + scan(){},cached:[{name:'qt_scrollarea_viewport',parent:'owned-editor',ptr:'owned-viewport'}],ptr(){return viewport;},rect(){return [0,0,15,1];}, + Memory:{alloc(size){allocations.push(size);return pointer();}}, + grab(widget,result,region){calls.push('grab');assert.strictEqual(widget,viewport);equal(region.data,[0,0,15,1]);if(failure==='grab')throw Error('synthetic grab failure');return result;}, + toImage(pixmap,result){calls.push('toImage');if(failure==='toImage')throw Error('synthetic toImage failure');return result;}, + imageWidth(){return 16;},imageHeight(){return 2;},imageNull(){return failure==='empty';},pixmapDpr(){return 1;},imageDpr(){return failure==='dpr'?2:1;}, + pixel(image,x,y){if(failure==='pixel')throw Error('synthetic pixel failure');const foreground=x<8?0xffc0c0c0:0xffaa0000,background=x<8?0xff101010:0xff0000aa;return y===0&&x%4===2?foreground:background;}, + destroyImage(){calls.push('destroy-image');if(failure==='destroy')throw Error('synthetic destructor failure');},destroyPixmap(){calls.push('destroy-pixmap');}}); + const capture=context.sampleViewport(0,{is_alt:false,is_main:true,selection_caret:failure==='unknown'?null:9,selection_anchor:failure==='unknown'?null:failure==='active'?10:9,selection_text_type:'string',selection_text_length:10,markers,caret_rect:{x:failure==='caret'?0:16,y:0,width:0,height:2}},[4,9]); + equal(allocations,capture.grab_attempted?[32,32,16]:[32,32]); + if(failure==='unknown'||failure==='active'){equal(calls,[]);equal(capture.constructed,[]);equal(capture.destroyed,[]);} + else if(failure==='grab'){equal(calls,['grab']);equal(capture.constructed,[]);equal(capture.destroyed,[]);} + else if(failure==='toImage'){equal(calls,['grab','toImage','destroy-pixmap']);equal(capture.constructed,['pixmap']);equal(capture.destroyed,['pixmap']);} + else{equal(calls,['grab','toImage','destroy-image','destroy-pixmap']);equal(capture.constructed,['pixmap','image']);} + if(!failure){assert(capture.calibration);equal(capture.destroyed,['image','pixmap']);} + else assert.strictEqual(capture.calibration,false); + equal(context.grabs,capture.grab_attempted?[0]:[]); +} +for(const failure of [null,'unknown','active','caret','adjacency','grab','toImage','empty','dpr','pixel','destroy'])lifecycle(failure); +process.stdout.write('endpoint-only replay, full-token labels, caret overlap, QRgb, D0/M0 and native lifecycle boundaries passed\n'); diff --git a/research/ui/test_raster_selection_reference.py b/research/ui/test_raster_selection_reference.py new file mode 100644 index 00000000..63a83df6 --- /dev/null +++ b/research/ui/test_raster_selection_reference.py @@ -0,0 +1,120 @@ +"""Offline #129 replay, same-frame controls and no-launch boundaries.""" +from __future__ import annotations +import copy +import hashlib +import json +from pathlib import Path +import shutil +import subprocess +import sys +import tempfile +import unittest + +ROOT=Path(__file__).resolve().parents[2] +sys.path.insert(0,str(ROOT)) +from research.ui.raster_selection_reference import SOURCE_FILES,phases,run_plan +from research.ui.raster_selection_analysis import GROUPS,analyze_pixels,comparisons,require_collapsed +from research.ui.selection_guard_replay import decision +from research.ui.test_raster_style_reference import fixture as base_fixture,write_records + +LABELS=[['D0 H H','M0 H H'],['A0 H','A0S1 H','A0D0 H'],['A1 H H'],['A2 H','A2D0 H','A2S1 H','A2S2 H'],['M1 H','M1S1 H','M1S2 H','M1D0 H']] +CAPTURE_LABELS=['D0/M0 H references','A0 H controls','A1 H H','A2 H controls','M1 H controls'] +PALETTE={'default':(0xffc0c0c0,0xff101010),'main':(0xffaa0000,0xff0000aa),'alternate':(0xff00aa00,0xffaa00aa)} + + +def fixture(root): + directory,plan_path,data=base_fixture(root) + plan=json.loads(plan_path.read_text()) + plan.update(issue=129,execution_authorized=True,source_files_sha256={name:hashlib.sha256((ROOT/name).read_bytes()).hexdigest() for name in SOURCE_FILES}) + plan_path.write_text(json.dumps(plan),encoding='utf-8') + captures=[];records=[] + for phase,(source_label,content) in enumerate(phases(25,91)): + capture=copy.deepcopy(data['raster-result.json']['captures'][phase]) + capture.update(label=CAPTURE_LABELS[phase],rois=[]) + capture['image'].update(viewport_width=16,width=16,grab_rect=[0,0,15,1]) + terminal=capture['terminal'];terminal.pop('selection',None) + lines=LABELS[phase] if phase==0 else [' '.join(LABELS[phase])] + terminal.update(lines=lines,line_count=len(lines),markers=[],selection_caret=9,selection_anchor=9,selection_text_type='string',selection_text_length=10,caret_rect={'x':16,'y':0,'width':0,'height':2}) + counts={} + for ordinal,group in enumerate(GROUPS[phase]): + sample=counts.get(group,0);counts[group]=sample+1 + if phase==0:label=LABELS[phase][0 if group=='default' else 1] + elif phase==2:label=LABELS[phase][0] + else:label=LABELS[phase][ordinal] + offset=3+sample*2 if label.endswith('H H') else len(label)-1 + h=[ordinal*4+2,0,2,2];space=[ordinal*4,0,2,2] + row=LABELS[phase].index(label) if phase==0 else 0 + column=lines[row].index(label)+offset + terminal['markers'].append({'ordinal':ordinal,'label':label,'group':group,'sample':sample,'document_line':row,'column':column,'position':column,'character':'H','preceding_character':' ', + 'h_rect':dict(zip(('x','y','width','height'),h)),'space_rect':dict(zip(('x','y','width','height'),space))}) + family={'control-default':'default','control-main':'main','control-alternate':'alternate','entry':'default','repeat':'main','return':'main'}.get(group,group) + fg,bg=PALETTE[family];hp=[fg,bg,fg,bg];sp=[bg]*4 + capture['rois'].append({'ordinal':ordinal,'group':group,'sample':sample,'h_rect':h,'space_rect':space,'h_pixels':hp,'space_pixels':sp,'colors':analyze_pixels(hp,sp)}) + captures.append(capture) + records.append({'phase':phase,'label':source_label,'source_hex':content.hex(),'source_sha256':hashlib.sha256(content).hexdigest(),'cursor_reply':[1,1]}) + data['raster-result.json']['captures']=captures;data['peer.json']['records']=records + write_records(directory,data) + return directory,plan_path,data + + +class SelectionProposalTests(unittest.TestCase): + def test_actual_records_replay_selection_gate_separately(self): + replay=json.loads((ROOT/'research/ui/results/2026-10-28-raster-selection-proposal/replay.json').read_text()) + self.assertFalse(replay['endpoints_and_rois_from_different_runs_combined']) + self.assertEqual([row['selection_gate_allowed'] for row in replay['records']],[True,True,False]) + for row in replay['records']: + self.assertEqual(decision(row['selection_caret'],row['selection_anchor']),{key:row[key] for key in ('selection_gate_allowed','reason')}) + self.assertEqual(replay['records'][2]['reason'],'selection-endpoints-unavailable') + + def test_unknown_and_noncollapsed_endpoints_reject_without_text_predicate(self): + self.assertTrue(require_collapsed(97,97)) + for values in ((97,98),(None,None),(True,True),(1.5,1.5),(-1,-1),(2**53,2**53)): + with self.subTest(values=values),self.assertRaises(ValueError):require_collapsed(*values) + + def test_proposal_cannot_launch_without_separate_authorization(self): + with tempfile.TemporaryDirectory() as temporary: + plan=Path(temporary)/'proposal.json';plan.write_text(json.dumps({'issue':129,'maximum_original_runs':1,'execution_authorized':False})) + with self.assertRaisesRegex(ValueError,'not been authorized'):run_plan(plan) + self.assertFalse(plan.with_suffix('.attempted').exists()) + + @unittest.skipUnless(shutil.which('node'),'Node required for production JS policy/native boundary tests') + def test_production_js_endpoints_tokens_caret_and_native_lifecycle(self): + completed=subprocess.run(['node',str(ROOT/'research/ui/test_raster_selection_native.js')],capture_output=True,text=True,timeout=10) + self.assertEqual(completed.returncode,0,completed.stderr) + + def test_same_frame_matches_default_main_and_ambiguous_remains_null(self): + with tempfile.TemporaryDirectory() as temporary: + _,_,data=fixture(Path(temporary));captures=data['raster-result.json']['captures'] + result=comparisons(captures) + self.assertEqual([row['unique_same_frame_control'] for row in result['frames']],['SGR0','SGR31;44','SGR31;44']) + self.assertTrue(result['unmatched_does_not_imply_reset']) + captures[1]['rois'][0]['colors']={'background_rgba':[1,2,3,255],'dominant_ink_rgba':[4,5,6,255]} + unmatched=comparisons(captures)['frames'][0] + self.assertEqual(unmatched['same_frame_result'],'unmatched');self.assertIsNone(unmatched['unique_same_frame_control']) + for capture in captures: + for roi in capture['rois']:roi['colors']=copy.deepcopy(captures[0]['rois'][0]['colors']) + result=comparisons(captures) + self.assertTrue(all(row['unique_same_frame_control'] is None and row['same_frame_result']=='ambiguous' for row in result['frames'])) + + def check_cli(self,change=None): + for optimized in (False,True): + with self.subTest(optimized=optimized),tempfile.TemporaryDirectory() as temporary: + root=Path(temporary);directory,plan,data=fixture(root) + if change:change(data);write_records(directory,data) + destination=root/'public/validation.json' + completed=subprocess.run([sys.executable,*(['-O'] if optimized else []),str(ROOT/'research/ui/export_raster_selection_reference.py'),'--directory',str(directory),'--plan',str(plan),'--destination',str(destination)],cwd=ROOT,capture_output=True,text=True,timeout=10) + if change: + self.assertNotEqual(completed.returncode,0,completed.stdout);self.assertFalse(destination.exists());self.assertFalse(destination.parent.exists()) + else: + self.assertEqual(completed.returncode,0,completed.stderr) + summary=json.loads(destination.read_text()) + self.assertEqual(summary['issue'],129);self.assertTrue(summary['sample_colors_verified']) + self.assertEqual(summary['sample_color_comparisons']['frames'][0]['unique_same_frame_control'],'SGR0') + self.assertNotIn('h_pixels',destination.read_text());self.assertNotIn('space_pixels',destination.read_text()) + + def test_synthetic_expanded_source_exports_scalars_only(self):self.check_cli() + def test_uncollapsed_claim_cannot_be_promoted(self):self.check_cli(lambda data:data['raster-result.json']['captures'][0]['terminal'].update(selection_anchor=10)) + def test_raw_selection_text_is_rejected(self):self.check_cli(lambda data:data['raster-result.json']['captures'][0]['terminal'].update(selection='private path/text')) + + +if __name__=='__main__':unittest.main() From 1aa8054c247445860442624cf07c4313f43f938d Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Wed, 28 Oct 2026 23:53:51 +0800 Subject: [PATCH 7/8] docs: freeze non-executable raster selection proposal identity (#129) --- .../windterm-raster-selection-plan.md | 2 + .../plan.json | 77 +++++++++++++++++++ .../ui/test_raster_selection_reference.py | 6 ++ 3 files changed, 85 insertions(+) create mode 100644 research/ui/results/2026-10-28-raster-selection-proposal/plan.json diff --git a/docs/research/windterm-raster-selection-plan.md b/docs/research/windterm-raster-selection-plan.md index 7e45fdf9..8b185c20 100644 --- a/docs/research/windterm-raster-selection-plan.md +++ b/docs/research/windterm-raster-selection-plan.md @@ -2,6 +2,8 @@ 状态:仅离线准备,尚未启动新原版。#126 的执行源 `15c736b`、一次 phase0/0 grab/0 pixel 记录保持不变。本计划是独立新源,等待最终复核及明确执行指令;私有准备计划默认 `execution_authorized=false`,启动入口会拒绝该状态。 +固定生产源为 `e302fa3acb6a2ed2982aad5550e027d37c772da6`。[脱敏准备计划](../../research/ui/results/2026-10-28-raster-selection-proposal/plan.json)列出每阶段 hex/SHA、源哈希与固定 Qt 身份;五阶段长度为 `43/38/20/53/54` 字节,另各追加 DSR。它是不可执行的离线提案,不是已获运行授权的计划;最终明确指令后应另生成新的授权计划及唯一输出,不修改旧批身份。 + ## 选区守卫与分开回放 生产读取已验证的 `selectionCaretP`、`selectionAnchorP`,只接受两个已知、非负、有限的安全整数且相等。先把 `selectionText` 投影为 type/length,再读取端点;非空文本不参与判定,字符串本身不进入新捕获记录。端点不同或未知均停止,不能只删除守卫。 diff --git a/research/ui/results/2026-10-28-raster-selection-proposal/plan.json b/research/ui/results/2026-10-28-raster-selection-proposal/plan.json new file mode 100644 index 00000000..b4bd02a7 --- /dev/null +++ b/research/ui/results/2026-10-28-raster-selection-proposal/plan.json @@ -0,0 +1,77 @@ +{ + "schema": "termforge-raster-selection-plan-v1", + "issue": 129, + "execution_authorized": false, + "source_base_commit": "e302fa3acb6a2ed2982aad5550e027d37c772da6", + "module_sha256": { + "WindTerm.exe": "7b10531dccc7cd614ae559f36f1ef518e0c96335637431b4646354823b2804d0", + "Qt5Core.dll": "8d2ff4ce9096ddccc4f4cd62c2e41fc854cfd1b0d6e8d296645a7f5fd4ae565a", + "Qt5Gui.dll": "5e7d2d41b8b92a880e83b8cc0ca173f5da61218604186196787ee1600956be1e", + "Qt5Widgets.dll": "3788c669d4b645e5a576de9fc77fca776bf516d43c89143dc2ca28291ba14358" + }, + "files_manifest_sha256": "e459adf2f12adb6a1b7f55e66a9da1c22794185e66e848057d21e0c60d669203", + "rea_capture_seconds": 45, + "cli_wait_timeout_seconds": 60, + "whole_lifecycle_bound_seconds": null, + "source_files_sha256": { + "research/ui/raster_selection_reference.py": "f7f254babecf589b7a9f8b2c859698a02bfdaadbc1de221698f92bf4ff19dca5", + "research/ui/raster_selection_reference.js": "47d40c545eb4a688b3d82657417ad310caf1a52e6ba62d01980f1a308d073f69", + "research/ui/alternate_style_reference.py": "3136e620a462a36ea3358c917eb4e207f32aa52db7c75ae14ba8856eba1eafcd", + "research/ui/alternate_reference.py": "94b85d49347e0e9813f7acddf0244f58520a7d1fce14084677c91cf4ddafffea", + "research/ui/r1_launch.py": "54fb41872c6abcc48242a0938044184854b016785cc13659c66b1bf404da0d8a", + "research/ui/launch.py": "6970e0d4f2fe5e7ec4827df7aa1026cd0e74aeb828cdc9b7a0b42b3c887eb4e6", + "research/ui/qt_r1_text.js": "f5820d413fdd72c13dffcefbf4d0ade615abfe15754c6b0b52e1e098814eea6f" + }, + "phases": [ + { + "phase": 0, + "label": "default-and-main-calibration", + "source_hex": "1b5b306d1b5b324a1b5b481b5b323b33484430204820481b5b343b33481b5b33313b34346d4d3020482048", + "source_bytes": 43, + "source_sha256": "0ef069f3327716b0cb410680ca441980b3b455ed102dfb666fd8b72f249ae610", + "dsr_suffix_hex": "1b5b366e" + }, + { + "phase": 1, + "label": "alternate-entry-with-controls", + "source_hex": "1b5b3f3130343968413020481b5b33313b34346d204130533120481b5b306d20413044302048", + "source_bytes": 38, + "source_sha256": "6415951f7219de9f93387c4060307fee943fd4c0549513fff788400936d8a895", + "dsr_suffix_hex": "1b5b366e" + }, + { + "phase": 2, + "label": "alternate-reference", + "source_hex": "1b5b343b35481b5b33323b34356d413120482048", + "source_bytes": 20, + "source_sha256": "ecd2495dd89fe551626069c64fec582eaac774bee29c4100a42ef159dd7009ff", + "dsr_suffix_hex": "1b5b366e" + }, + { + "phase": 3, + "label": "alternate-repeat-with-controls", + "source_hex": "1b5b3f3130343968413220481b5b306d204132443020481b5b33313b34346d204132533120481b5b33323b34356d20413253322048", + "source_bytes": 53, + "source_sha256": "b33165a1da96062941d0f73e09f76c69726b67c14c8098d885b1804472610d7d", + "dsr_suffix_hex": "1b5b366e" + }, + { + "phase": 4, + "label": "main-return-with-controls", + "source_hex": "1b5b3f313034396c204d3120481b5b33313b34346d204d31533120481b5b33323b34356d204d31533220481b5b306d204d3144302048", + "source_bytes": 54, + "source_sha256": "11af8a2f9cede6def3a9fbeadc169683ca321dcf5e34539cc06a9b438bde5574", + "dsr_suffix_hex": "1b5b366e" + } + ], + "phase_count": 5, + "maximum_grabs_per_phase": 1, + "maximum_grabs": 5, + "scope": "Independent collapsed-endpoint guard and same-image default/main H references; H/space output colors only, no presented/performance claim", + "selection_guard": "Known nonnegative safe-integer selectionCaretP equals selectionAnchorP; selectionText is retained only as type/length", + "stop": "Empty image, DPR/coordinates, caret/selection or ink calibration failure stops this one run; no shifted ROI, extra capture or retry", + "plan_role": "Offline proposal only; a separately authorized fresh plan is required for execution", + "actual_original_runs": 0, + "private_plan_sha256": "b6822efa1535158b61ed4ebb2737fe327b8ed7da2b74ed487c7bad6fb24a6795", + "unique_output_label": "r129-raster-0" +} diff --git a/research/ui/test_raster_selection_reference.py b/research/ui/test_raster_selection_reference.py index 63a83df6..e2f8f5ed 100644 --- a/research/ui/test_raster_selection_reference.py +++ b/research/ui/test_raster_selection_reference.py @@ -58,6 +58,12 @@ def fixture(root): class SelectionProposalTests(unittest.TestCase): + def test_public_proposal_is_not_execution_authorization(self): + plan=json.loads((ROOT/'research/ui/results/2026-10-28-raster-selection-proposal/plan.json').read_text()) + self.assertFalse(plan['execution_authorized']);self.assertEqual(plan['actual_original_runs'],0) + self.assertEqual([row['source_hex'] for row in plan['phases']],[source.hex() for _,source in phases(25,91)]) + self.assertEqual([row['source_bytes'] for row in plan['phases']],[43,38,20,53,54]) + def test_actual_records_replay_selection_gate_separately(self): replay=json.loads((ROOT/'research/ui/results/2026-10-28-raster-selection-proposal/replay.json').read_text()) self.assertFalse(replay['endpoints_and_rois_from_different_runs_combined']) From 8ba7dbb49b0bfefa484ab12763cfa7db87877a4c Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Thu, 29 Oct 2026 00:18:32 +0800 Subject: [PATCH 8/8] research: record partial same-frame raster evidence (#129) --- .../windterm-raster-selection-plan.md | 6 +- ...m-raster-selection-reference-2026-10-29.md | 55 + research/ui/raster_selection_analysis.py | 12 +- .../plan.json | 78 + .../validation.json | 1796 +++++++++++++++++ .../ui/test_raster_selection_reference.py | 48 +- 6 files changed, 1987 insertions(+), 8 deletions(-) create mode 100644 docs/research/windterm-raster-selection-reference-2026-10-29.md create mode 100644 research/ui/results/2026-10-29-raster-selection-reference/plan.json create mode 100644 research/ui/results/2026-10-29-raster-selection-reference/validation.json diff --git a/docs/research/windterm-raster-selection-plan.md b/docs/research/windterm-raster-selection-plan.md index 8b185c20..a4303e78 100644 --- a/docs/research/windterm-raster-selection-plan.md +++ b/docs/research/windterm-raster-selection-plan.md @@ -1,6 +1,6 @@ # #129:端点守卫与同图颜色正控的独立计划 -状态:仅离线准备,尚未启动新原版。#126 的执行源 `15c736b`、一次 phase0/0 grab/0 pixel 记录保持不变。本计划是独立新源,等待最终复核及明确执行指令;私有准备计划默认 `execution_authorized=false`,启动入口会拒绝该状态。 +状态:完成最终复核及明确单次授权后,已按固定生产源执行一次,得到[三阶段受控栅格的 partial 结果](windterm-raster-selection-reference-2026-10-29.md)。#126 的执行源 `15c736b`、一次 phase0/0 grab/0 pixel 记录保持不变。下面保留执行前方案;原 `execution_authorized=false` 准备计划未修改,新授权计划另建并绑定准备 HEAD `1aa8054`。 固定生产源为 `e302fa3acb6a2ed2982aad5550e027d37c772da6`。[脱敏准备计划](../../research/ui/results/2026-10-28-raster-selection-proposal/plan.json)列出每阶段 hex/SHA、源哈希与固定 Qt 身份;五阶段长度为 `43/38/20/53/54` 字节,另各追加 DSR。它是不可执行的离线提案,不是已获运行授权的计划;最终明确指令后应另生成新的授权计划及唯一输出,不修改旧批身份。 @@ -38,7 +38,7 @@ S1=`SGR31;44`,S2=`SGR32;45`,D0=`SGR0`。比较对象始终为字形 H 及其 [GUI 脚本](../../research/ui/raster_selection_reference.js)沿用已只读核实的固定 Qt5Widgets/Qt5Gui 完整 SHA、模块路径和导出 RVA,GUI 线程执行 QWidget::grab→QPixmap::toImage→QImage::pixel。QPixmap/QImage 各 32B、不预构造;正常返回后按 image→pixmap 普通析构。QRect 为 inclusive 边界,不读取或猜测 format/stride。既有 Qt 栅格计划提供静态 ABI 依据;旧 #126 没有实际调用这些函数,不能转记为原生栅格已验证。 -仍为独立 station/desktop、OwnedJob、合成 loopback/profile、一个新唯一目录。新批最多启动原版一次、每阶段至多一次 grab,共最多五次;capture45、CLI wait60,整程上限另列未知。当前只是离线计划,没有授权标记、attempt marker 或新执行结果。 +仍为独立 station/desktop、OwnedJob、合成 loopback/profile、一个新唯一目录。新批最多启动原版一次、每阶段至多一次 grab,共最多五次;capture45、CLI wait60,整程上限另列未知。执行前的不可运行提案与[新授权计划的脱敏投影](../../research/ui/results/2026-10-29-raster-selection-reference/plan.json)分别保留;新批实际只尝试 phase0/1/2 的三次 grab。 空图、无墨迹、DPR/坐标/端点/caret/标签不成立即保存 partial/unknown 并停止,不移动 ROI、追加抓取或重跑。只公开合成输入、颜色/尺寸标量和 ROI 哈希;原始像素、截图、路径、PID 和资源均留 local。受控栅格不代表 presented 或性能结果。 @@ -48,4 +48,4 @@ S1=`SGR31;44`,S2=`SGR32;45`,D0=`SGR0`。比较对象始终为字形 H 及其 - [回放投影器](../../research/ui/selection_guard_replay.py):核对 #126 原文件 SHA 后确认端点确实未捕获;不由 `caretPosition` 补造 P 端点。 - [公共回归](../../research/ui/test_raster_selection_reference.py)和[原生调用合成边界](../../research/ui/test_raster_selection_native.js):覆盖历史/缺端点分开回放、非空文本但 collapsed 可继续、noncollapsed/未知仍拒绝、零宽 caret 重叠、完整 token、同帧默认色/主色匹配及歧义、32B sret/析构和未获执行授权不得启动。 -本提案不改旧 #118/#123/#126 树或记录,不把不同运行的成功拼成联合验收,也不解除当前样式未知项。最终源与准备计划交复核后,再根据独立执行指令启动新的一次。 +本方案不改旧 #118/#123/#126 树或记录,不把不同运行的成功拼成联合验收。新批 phase2 因主要墨迹频数并列而停止,未追加抓取或重跑;完整样式、重复进入和退出恢复仍未知,局部输出颜色事实见独立结果。 diff --git a/docs/research/windterm-raster-selection-reference-2026-10-29.md b/docs/research/windterm-raster-selection-reference-2026-10-29.md new file mode 100644 index 00000000..e05aae82 --- /dev/null +++ b/docs/research/windterm-raster-selection-reference-2026-10-29.md @@ -0,0 +1,55 @@ +# #129:三阶段受控栅格,主要墨迹并列后停止 + +[#129](https://github.com/JavaZeroo/termforge/issues/129) 按[固定五阶段方案](windterm-raster-selection-plan.md)完成了一次新原版执行。phase0/1 的 H/space 校准通过;phase2 的第一个 H 有两种最高频非背景色,频数同为 16,按原门槛以 `dominant-ink-ambiguous` 停止。实际共三次 grab,后两个阶段未发送,没有移动 ROI、追加抓取或重跑。 + +局部可观察事实是:首次 `1049h` 后未经 SGR 修改的 A0 H,与**同帧、同一行**显式 `SGR0` 正控的输出颜色相同,与 `SGR31;44` 正控不同。该事实不等于读取了内部 current_style、pen 或证明 reset。备用 S2 完整校准、重复进入与退出后的样式恢复仍未知。 + +## 执行与源身份 + +- 固定生产源为 `e302fa3acb6a2ed2982aad5550e027d37c772da6`,执行前准备 HEAD 为 `1aa8054c247445860442624cf07c4313f43f938d`。七个生产文件分别与固定生产提交、不可运行 proposal 及新授权计划逐字节核对一致。 +- 原 `execution_authorized=false` proposal 未修改。最终复核明确授权后另生成 `true` 的新私有计划,SHA 为 `2fc02922d532766442913d87038cf4c0311c125c5101eb40220429fdf12b2509`;启动前新目录和 attempt marker 均不存在。单次 marker 随后保留,没有第二次运行。 +- [新授权计划的脱敏投影](../../research/ui/results/2026-10-29-raster-selection-reference/plan.json)列出全部五段 `43/38/20/53/54` 字节及各自 hex/SHA;每段另追加 `CSI 6n`。只有前三段实际发送,不把计划输入计作已执行行为。 +- 固定官方 WindTerm 2.7.0 EXE SHA 为 `7b10531dccc7cd614ae559f36f1ef518e0c96335637431b4646354823b2804d0`,4,047 文件清单 SHA 为 `e459adf2f12adb6a1b7f55e66a9da1c22794185e66e848057d21e0c60d669203`。准备与离线重审均核对 WindTerm/Qt5Core/Qt5Gui/Qt5Widgets 四个完整文件 SHA;运行中的实际模块路径及所需导出 RVA 守卫通过。 +- 独立 WindowStation/Desktop、OwnedJob、隔离 HOME/profile、单个合成 loopback Telnet 会话。没有操作用户输入桌面、剪贴板或 DPI。Telnet 直达终端观察不代表 WinPTY/ConPTY local-shell 合同。 + +REA Evidence ID 为 `ev_432abe8baf4fc86efd4994cd12cc3ab5fe0b5120476d22540c5b26e2e3c693a4`。完整白名单标量、源身份及原件 SHA 见[脱敏结果](../../research/ui/results/2026-10-29-raster-selection-reference/validation.json)。旧 #126 的源 `15c736b`、0 grab/0 pixel 停止记录保持独立,未移植历史端点或拼接成功步骤。 + +## 实际观察 + +| 阶段 | DSR 回复 | isAltScreen / isMainScreen | P caret / anchor | 选区文本 type / length | 栅格校准 | +|---|---|---|---|---|---| +| 0 D0/M0 双 H | `(4,9)` | false / true | 19 / 19 | string / 10 | D0、S1 各双 H 一致 | +| 1 A0 与同帧正控 | `(1,19)` | true / false | 38 / 38 | string / 20 | 三个 H 均通过 | +| 2 A1 S2 双 H | `(4,11)` | true / false | 51 / 51 | string / 12 | 第一个 H 墨迹频数并列,停止 | + +三条回复均为无 `?` 前缀的 `CSI row;column R`,没有额外应用字节。逐条完整 NAWS 事件为 `91×24 → 91×25`,冻结事件数 2,冻结后未观察到变化;终端 API capacity 为 25。只出现 ack0/ack1,phase2 未确认,因此没有释放 phase3/phase4。 + +本批主动读取 `selectionCaretP` 与 `selectionAnchorP`,三次都是已知相等的安全整数。非空 `selectionText` 只保留类型与长度,没有参与选区判断或进入公开文本。H/space ROI 与实际 caret 占用不相交,零宽 caret 仍按至少一个逻辑像素保守检查;标签使用完整 token 唯一绑定,没有把 `M1S1` 等前缀当作目标。 + +三张图均为 `888×451`,QPixmap/QImage DPR 均为 1,inclusive grab QRect 为 `[0,0,887,450]`。采样 H 与前置 space 均为 `8×18`,每个 ROI 144 个 QRgb 值。Qt 的 32B sret 对象未预构造,每阶段正常返回后均按 image→pixmap 普通析构。这里观察到的是 QWidget 的受控栅格,未验证物理显示或 presented。 + +## 颜色事实与未知 + +颜色字段为 `background_rgba` 与 `dominant_ink_rgba`,表示 ROI 输出;主要墨迹不等价于字形的真实前景 pen。phase0 的 D0 双 H 输出一致,S1 双 H 输出也一致,且两组可区分。phase1 的同帧比较为: + +| phase1 H | 背景 RGBA | 主要墨迹 RGBA | +|---|---|---| +| 未修改 SGR 的 A0 | `[40,40,50,255]` | `[40,75,140,255]` | +| 显式 S1 (`31;44`) | `[61,104,169,255]` | `[61,95,137,255]` | +| 显式 D0 (`0`) | `[40,40,50,255]` | `[40,75,140,255]` | + +这支持 A0 与同帧 D0 的局部唯一输出匹配。跨 frame 的 phase0 D0 输出与 phase1 D0 并不相同,不能据输入 SGR 编号假定所有 frame 输出恒定,更不能借此填充内部样式值。 + +phase2 的 space 为单一背景 `[138,110,201,255]`。首个 H 中 `[137,110,201,255]` 与 `[138,120,137,255]` 各有 16 个样本并列最高;`colors=null`,第二个 H 尚未采样。没有选择其中一种颜色、改用新统计规则或重新抓取。整批 `sample_colors_verified=false`、五阶段 `sample_color_comparisons=null`,已完成的 phase0/1 校准事实单独保留。 + +阶段 3 重复 `1049h`、阶段 4 `1049l` 后写字均未发生,本批不提供它们的清空、样式继承或恢复结论。完整 current_style、非颜色属性、全部 active grid、local-shell、性能等同与 R2 验收均未验证。 + +## 离线校验与清理 + +CLI 已真正结束后才导出和交付,exit=0,stderr 为空,REA 无脚本错误、消息截断或清理错误;provider 的 subject/environment 仍为 null。Job 为 `active=0,total=3,terminated=0`,合成 profile 仍只有一个会话。启动器主动结束原版,不以 Job 计数声称自然 GUI 退出。 + +第一次普通/`-O` 离线导出都拒绝了真实 `QImage::isNull` 返回值:Frida 把 NativeFunction bool 记录为数字 `0`,审计器原先只接受 Python bool。该失败没有产生公开输出。仅修订[离线分析器](../../research/ui/raster_selection_analysis.py):显式接受精确 bool 或整数 0/1,拒绝 float/string/null/其他整数;公开规范布尔值并另保留 `is_null_raw_value=0`、`is_null_raw_type="number"`。七个生产源、授权计划和全部原件均未修改。 + +[公共回归](../../research/ui/test_raster_selection_reference.py)先复现数值 0 被拒绝,再验证精确类型门槛与普通/优化 CLI;新增三阶段墨迹歧义的合成反例,确保 partial 不提升为五阶段通过。普通及 `python -O` 各 13 项通过,CTest `windterm_raster_selection_proposal` 在 Debug 配置下 1/1 通过。对同一真实原件的两种解释器导出逐字节相同,SHA 为 `c29022645128d14b77b764f473fa7a2c0e467ce5f66db47de8b589128de7fc84`。独立只读审计另核对 provider 消息、源、完整 token、像素派生标量、停止点与清理,结论一致。 + +原始图像/ROI 像素、日志、路径、PID、复制资源和 profile 均留在唯一 `local/r129-raster-authorized-0`;公开只有合成输入、身份、颜色/尺寸/频数标量与哈希。本批保持 partial,不关闭 #129、#126、#123、#27 或 R2。 diff --git a/research/ui/raster_selection_analysis.py b/research/ui/raster_selection_analysis.py index 2680cfe2..88996fc9 100644 --- a/research/ui/raster_selection_analysis.py +++ b/research/ui/raster_selection_analysis.py @@ -54,6 +54,12 @@ def check_rectangle(rectangle,width,height): require(w>0 and h>0 and w*h<=8192 and x>=0 and y>=0 and x+w<=width and y+h<=height,"ROI outside image") +def native_bool(value): + """Frida bool returns may be JSON booleans or numeric 0/1; retain that identity.""" + require(type(value) is bool or (type(value) is int and value in (0,1)),"Image null state must be bool or integer 0/1") + return bool(value) + + def audit_capture(capture): """Return a scalar-only projection, retaining calibrated/unknown boundaries.""" public={key:capture[key] for key in ("phase","label","cursor_reply","grab_attempted","constructed","destroyed","calibration","reason")} @@ -63,17 +69,19 @@ def audit_capture(capture): image=capture["image"] if image is not None: require(all(type(image[key]) is int and 0<=image[key]<=4096 for key in ("width","height","viewport_width","viewport_height")),"Image scalar bound differs") - require(type(image["is_null"]) is bool,"Image null state missing") + is_null=native_bool(image["is_null"]) require(image["grab_rect"]==[0,0,image["viewport_width"]-1,image["viewport_height"]-1],"Grab QRect is not inclusive") require(all(image[key] is None or (type(image[key]) in (int,float) and math.isfinite(image[key]) and abs(image[key])<=4) for key in ("pixmap_dpr","image_dpr")),"DPR scalar bound differs") if capture["calibration"]: require(capture["grab_attempted"] is True and capture["constructed"]==["pixmap","image"] and capture["destroyed"]==["image","pixmap"],"Calibrated image lifecycle differs") - require(image and image["is_null"] is False,"Empty calibrated image") + require(image and is_null is False,"Empty calibrated image") pdpr,idpr=image["pixmap_dpr"],image["image_dpr"] require(type(pdpr) in (int,float) and math.isfinite(pdpr) and 0