From 8272d21c5b54db9121ef112c9e8be278cff8cdce Mon Sep 17 00:00:00 2001 From: JavaZeroo-Bot <133958205+JavaZeroo-Bot@users.noreply.github.com> Date: Thu, 12 Nov 2026 01:18:40 +0800 Subject: [PATCH 1/3] research(ui): fix the BCD calibration peer dual-axis resize contract The calibration peer inherited the baseline BoundPeer width-only guard, which raised "Resize changed row count" when the fixed 80x24 -> 60x12 resize NAWS arrived, so phase 1 never completed and the 2026-11-09 calibration stopped after phase 0 with frozen_grid still [80,24]. Override the NAWS gate inside the calibration probe only; the baseline width-only contract stays unchanged. The calibration contract allows exactly the plan size-change sequence 80x24 -> 60x12 -> 80x24: phases 1/3 are resize events and each armed resize must be acknowledged by a new post-request NAWS at the target grid (stale or same-size events never count); phase 2 is a same-size observation checkpoint with no resize action and no new NAWS required, duplicate same-size events are recorded only, and any unexpected size change fails. Expectations come from the fixed plan constants, never from observed ranges. Record initial_grid, expected_size_change_sequence, naws_size_changes, phase_ready_events, phase2_observed_naws and phase2_new_naws_required=false, and audit them; the four fixed windows/grids and all prior assertions stay unchanged. Offline peer simulation covers the dual-axis flow with and without a checkpoint duplicate plus stale-ready, unarmed-change, wrong-grid and wrong-request rejections, and the mechanics audit tamper checks. This does not close #27 or #29, does not choose A/B/C, and does not change product behavior. Co-authored-by: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> --- CMakeLists.txt | 1 + research/ui/resize_bcd_calibration.js | 94 ++++++ research/ui/resize_bcd_calibration.py | 365 +++++++++++++++++++++ research/ui/test_resize_bcd_calibration.py | 171 ++++++++++ 4 files changed, 631 insertions(+) create mode 100644 research/ui/resize_bcd_calibration.js create mode 100644 research/ui/resize_bcd_calibration.py create mode 100644 research/ui/test_resize_bcd_calibration.py diff --git a/CMakeLists.txt b/CMakeLists.txt index ed925eb..3041a40 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -525,6 +525,7 @@ add_test(NAME edit_csi_core COMMAND edit_csi_test) termforge_add_python_test(windterm_mode_state_reference -m unittest research.ui.test_mode_state_reference research.ui.test_export_mode_state_reference research.ui.test_mode_reentry_reference research.ui.test_export_mode_reentry_reference -v) termforge_add_python_test(windterm_attribute_state_reference -m unittest research.ui.test_attribute_state_reference research.ui.test_export_attribute_state_reference -v) termforge_add_python_test(windterm_resize_state_reference -m unittest research.ui.test_resize_state_reference research.ui.test_export_resize_state_reference -v) + termforge_add_python_test(windterm_resize_bcd_calibration -m unittest research.ui.test_resize_bcd_calibration -v) termforge_add_python_test(windterm_resize_address_reference -m unittest research.ui.test_resize_address_reference research.ui.test_export_resize_address_reference -v) termforge_add_python_test(windterm_resize_address_optimized -O -m unittest research.ui.test_resize_address_reference research.ui.test_export_resize_address_reference -v) termforge_add_python_test(windterm_resize_edit_reference -m unittest research.ui.test_resize_edit_reference research.ui.test_export_resize_edit_reference research.ui.test_audit_resize_address_erase -v) diff --git a/research/ui/resize_bcd_calibration.js b/research/ui/resize_bcd_calibration.js new file mode 100644 index 0000000..f8f73d4 --- /dev/null +++ b/research/ui/resize_bcd_calibration.js @@ -0,0 +1,94 @@ +// Two resize boundaries; observe the shrunk cursor before releasing one action. +let stage=0,busy=false,finished=false,due=0,stable=null,stableSince=0,lastPhase=-1; +let gridKey=null,gridSince=0,nawsSeen=0,lastNaws=null,requested=0,resized=0,committed=0; +const caseId=__RESIZE_BCD_CALIBRATION_JSON__; +const started=Date.now(),observations=[],geometryChecks=[]; +const windows=[[1228,839],[988,509],[988,509],[1228,839]],grids=[[80,24],[60,12],[60,12],[80,24]]; +for(const module of [wind,core,gui,widgets]){ + const expected=(__RUN_DIRECTORY_JSON__+'/app/'+module.name).replace(/\\/g,'/').toLowerCase(); + if(module.path.replace(/\\/g,'/').toLowerCase()!==expected)throw Error('pinned Qt module path guard'); +} +function currentTerminal(){ + const raw=evalText(`JSON.stringify((function(){var t=window.activeText;if(!t)return null;function get(k){try{var v=typeof t[k]==='function'?t[k]():t[k];return v===undefined?null:v;}catch(e){return null;}}var n=get('visibleLineCount'),lines=[],markers=[],labels=[];if(typeof n!=='number'||n<0||n>200)throw Error('document line bound');for(var i=0;i1024)throw Error('document text bound');lines.push(line);for(var j=0;j=256)throw Error('marker bound');var p=t.lineStart(i)+offset;markers.push({label:labels[j],document_line:i,column:offset,position:p,rect:t.visibleRectFromPosition(p)});}}}return {protocol:get('protocolName'),connected:get('isConnected'),top:get('topLine'),scroll:get('scrollTop'),line_count:n,capacity:get('screenLineCapacity'),lines:lines,markers:markers,caret_api:{caretLine:get('caretLine'),caretColumn:get('caretColumn'),caretPosition:get('caretPosition')}};})())`); + return raw?JSON.parse(raw):null; +} +function write(name,value){const f=new File(__RUN_DIRECTORY_JSON__+'/'+name,'w');f.write(value);f.close();} +function windowGeometry(){ + const value=rect(main); + if(!Array.isArray(value)||value.length!==4||!value.every(Number.isInteger)||value[2]64)throw Error('NAWS history bound'); + for(let i=nawsSeen;i!Number.isInteger(n)||n<0||n>65535))throw Error('NAWS event bound'); + const value=JSON.stringify(event);if(lastNaws!==null&&lastNaws!==value)resetGrid();lastNaws=value; + } + nawsSeen=history.length; + const value=JSON.stringify([peer.columns,peer.rows,terminal.capacity]); + if(!peer.columns||peer.rows!==terminal.capacity||JSON.stringify([peer.columns,peer.rows])!==lastNaws){resetGrid();return false;} + if(value!==gridKey){gridKey=value;gridSince=Date.now();return false;} + return Date.now()-gridSince>=600; +} +function requestResize(phase){requested=phase;write('resize-request-'+phase,JSON.stringify({phase,window:windows[phase]}));} +send({kind:'probe_ready',station_isolated:true,scenario:'resize-address-v1',case_id:caseId}); +Interceptor.attach(user.getExportByName('DispatchMessageW'),{onEnter(){ + if(busy||finished||Process.getCurrentThreadId()!==uiThread||Date.now()41000)throw Error('bounded scenario deadline'); + if(stage===0){if(!ready('ui'))return;resize(main,...windows[0]);stage=1;} + else if(stage===1){scan();expandAll(tree);focus(tree,7);key(tree,0x1000011);stage=2;} + else if(stage===2){key(tree,0x1000004);stage=3;} + else{ + const peer=peerState();if(!peer||!peer.connected){resetGrid();return;} + if(peer.error)throw Error('peer '+peer.error); + if(peer.complete){if(observations.length!==4)throw Error('incomplete BCD-address observations');finish('observed-four-bcd-calibration-states',null);return;} + const terminal=currentTerminal();if(!terminal||!terminal.connected||terminal.protocol!=='Telnet'){resetGrid();return;} + if(stage===3){ + if(!stableGrid(peer,terminal))return; + if(peer.columns!==80||peer.rows!==24)throw Error('fixed initial grid differs'); + checkWindow(0);write('fixture-ready',JSON.stringify({grid:[peer.columns,peer.rows],event_count:peer.naws_history.length}));stage=4;return; + } + if(requested>committed&&peer.resize_armed===requested){ + if(resized!==requested){resize(main,...windows[requested]);resized=requested;resetGrid();return;} + if(!stableGrid(peer,terminal))return; + if(peer.columns!==grids[requested][0]||peer.rows!==grids[requested][1])throw Error('fixed resized grid differs'); + checkWindow(requested); + write('resize-ready-'+requested,JSON.stringify({phase:requested,grid:[peer.columns,peer.rows],event_count:peer.naws_history.length}));committed=requested;return; + } + if(peer.phase===null||observations.length>peer.phase)return; + if(peer.case_id!==caseId)throw Error('peer case differs'); + if(peer.phase!==lastPhase){lastPhase=peer.phase;stable=null;stableSince=Date.now();} + const record=peer.records[peer.phase];if(!record.cursor_reply)return; + if(terminal.capacity!==record.frozen_grid[1]||JSON.stringify(record.frozen_grid)!==JSON.stringify(grids[peer.phase]))throw Error('phase grid differs'); + if(peer.phase===2&&geometryChecks.length===2)checkWindow(2); + const geometry=windowGeometry(),state=JSON.stringify([terminal,geometry]); + if(JSON.stringify(geometry.window_size)!==JSON.stringify(windows[peer.phase]))throw Error('phase window dimensions differ'); + if(state!==stable){stable=state;stableSince=Date.now();return;} + if(Date.now()-stableSince<400)return; + if(observations.length!==peer.phase||observations.length>=4)throw Error('resize-address phase order'); + const observation={phase:peer.phase,label:peer.phase_label,terminal,...geometry,cursor_reply:record.cursor_reply, + cursor_reply_private:record.cursor_reply_private,response_hex:record.response_hex,frozen_grid:record.frozen_grid}; + observations.push(observation);send({kind:'resize_address_phase',...observation});write('ack-'+peer.phase,'observed'); + if(peer.phase===0||peer.phase===2)requestResize(peer.phase+1); + } + due=Date.now()+75; + }catch(error){finish('failed',String(error));}finally{busy=false;} +}}); +setTimeout(()=>finish('failed','bounded observation timer'),42000); diff --git a/research/ui/resize_bcd_calibration.py b/research/ui/resize_bcd_calibration.py new file mode 100644 index 0000000..3afe39b --- /dev/null +++ b/research/ui/resize_bcd_calibration.py @@ -0,0 +1,365 @@ +"""Bounded geometry-only calibration, no body or CUP payload. + +The initial 80x24 body puts B/C/D at source rows 7/9/10. The fixed 80x24 -> +60x12 resize is followed by a pre-write body snapshot, then one `CSI 12;1H` +RESIZED write and DSR, then a restore snapshot. The direct-Telnet observation +is WindTerm protocol/display behavior, not Windows local-shell/native-grid proof; +preparation never starts the original program. +""" +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 import mode_state_reference as common +from research.ui import resize_state_reference as baseline + +require, sha = common.require, common.sha +qrect_size = baseline.qrect_size +EXE_SHA256, MANIFEST_SHA256 = common.EXE_SHA256, common.MANIFEST_SHA256 + +# 80x24 initial body: B/C/D at source rows 7/9/10; the fixed action is CUP 12;1 +# RESIZED after a 60x12 shrink, followed by a restore phase. +RESET = b"\x1b[?6l\x1b[?7h\x1b[r\x1b[0m\x1b[2J" + +BODY = b"" +CASES = {"calibrate": b""} +WINDOWS = ((1228, 839), (988, 509), (988, 509), (1228, 839)) +GRIDS = ((80, 24), (60, 12), (60, 12), (80, 24)) +RESIZE_PHASES = (1, 3) +# The shared controller writes this file name and message kinds; both probes use +# the BCD-specific qt_r1_text controller. +RESULT_FILE = "bcd-calibration-result.json" +CONTROLLER = "research/ui/resize_bcd_calibration.js" +CONTROLLER_ANCHOR = "__RESIZE_BCD_CALIBRATION_JSON__" +ATTEMPT = "One original BCD pre-write/address observation attempt; preserve failures and never retry this plan.\n" +SOURCE_FILES = ("research/ui/resize_bcd_calibration.py", CONTROLLER, + "research/ui/resize_state_reference.py", "research/ui/mode_state_reference.py", + "research/ui/alternate_reference.py", "research/ui/r1_launch.py", "research/ui/launch.py", + "research/ui/qt_r1_text.js", "research/ui/r1_vt_fixture.py", "experiments/conpty/run.py") +BASE_LINES = ("", " " * 79 + "B", " " * 79 + "C", "D") + + +def validate_case(case): + require(type(case) is str and case in CASES, "Unknown fixed BCD-addressing case") + return case + + +def phases(case, rows=24, columns=80): + validate_case(case) + require(type(rows) is int and type(columns) is int and [columns, rows] == list(GRIDS[0]), "Fixed initial grid differs") + return [("initial-80x24", b""), ("shrunk-60x12", b""), + ("shrunk-60x12-second", b""), ("restored-80x24", b"")] + + +def phase_records(case, rows=24, columns=80): + return [{"phase": index, "label": label, "source_hex": value.hex(), "source_bytes": len(value), + "source_sha256": hashlib.sha256(value).hexdigest(), "dsr_suffix_hex": "1b5b366e"} + for index, (label, value) in enumerate(phases(case, rows, columns))] + + +def peer_class(case): + validate_case(case) + base = baseline.peer_class() + # baseline creates a fresh module namespace for each class, not a shared peer. + base._serve.__globals__["phases"] = lambda rows, columns: phases(case, rows, columns) + + class BcdPeer(base): + """Calibration peer for the dual-axis 80x24 -> 60x12 -> 60x12 -> 80x24 plan. + + The inherited baseline contract freezes the initial row count and only + allows width shrinks, which contradicts this plan's 24 -> 12 -> 12 -> 24 + rows; the calibration contract below therefore overrides the NAWS gate + without touching the baseline class. Size changes must follow exactly + the plan sequence 80x24 -> 60x12 -> 80x24: phases 1/3 are resize events + and each armed resize must be acknowledged by a new post-request NAWS at + the target grid (stale or same-size events never count); phase 2 is a + same-size observation checkpoint with no resize action and no new NAWS + required, where duplicate same-size events are recorded only and any + unexpected size change fails. Expectations always come from the fixed + plan constants, never from observed ranges. + """ + + def _freeze_initial(self): + super()._freeze_initial() + require(self.state["frozen_grid"] == list(GRIDS[0]), "Fixed initial grid differs") + self.state["case_id"] = case + self.state["initial_grid"] = list(self.state["frozen_grid"]) + self.state["expected_size_change_sequence"] = [list(GRIDS[0]), list(GRIDS[1]), list(GRIDS[3])] + self.state["phase2_new_naws_required"] = False + self.state["phase_ready_events"] = [] + self.state.setdefault("naws_size_changes", []) + self.state["frozen_grid_history"] = self.state["frozen_grids"] + + def _on_naws(self): + grid = [self.state["columns"], self.state["rows"]] + history = self.state.setdefault("naws_history", []) + require(len(history) < 64 and all(type(n) is int and 0 <= n <= 65535 for n in grid), "NAWS event bound") + history.append(grid) + self._record() + changes = self.state.setdefault("naws_size_changes", []) + if not changes or changes[-1] != grid: + changes.append(list(grid)) + if self.state.get("frozen_grid"): + armed = self.state.get("resize_armed") + if grid == self.state["frozen_grid"]: + # Same-size repeat (phase-2 checkpoint or a duplicate during an + # armed resize): record only, never a new-size acknowledgement. + return + require(armed is not None, "NAWS changed outside an armed resize") + require(grid == list(GRIDS[armed]), "Unexpected resized grid") + + def _phase_ready(self, index): + if index == -1: + return True + ack = self.directory / f"ack-{index}" + if not ack.exists() or self.state["records"][index]["cursor_reply"] is None: + return False + require(ack.read_text(encoding="utf-8") == "observed", "UI acknowledgement differs") + if index == 3: + # The phase-3 transition was already logged when the restore resize + # was acknowledged (target == 3 below); this is only the final + # acceptance of the fourth observation, not a new freeze. + return True + target = index + 1 + if target == 2: + require(self.state["resize_armed"] is None and self.state["frozen_grid"] == list(GRIDS[2]), + "Action was not released from the fixed shrunk grid") + self.state["frozen_grids"].append(list(self.state["frozen_grid"])) + self.state["freeze_event_counts"].append(len(self.state["naws_history"])) + self.state["phase_ready_events"].append({"phase": 2, "frozen_grid": list(self.state["frozen_grid"]), + "event_count": len(self.state["naws_history"]), + "new_naws_required": False}) + return True + request = self.directory / f"resize-request-{target}" + if not request.exists(): + return False + require(json.loads(request.read_text(encoding="utf-8")) == {"phase": target, "window": list(WINDOWS[target])}, + "Resize request differs from immutable window sequence") + if self.state["resize_armed"] is None: + self.state["resize_armed"] = target + boundary = {"phase": target, "before_event_count": len(self.state["naws_history"]), + "requested_window": list(WINDOWS[target])} + if target == 3: + # Same-size checkpoint observations recorded between the phase-1 + # freeze and this arming; duplicates only, not acknowledgements. + boundary["phase2_observed_naws"] = [list(event) for event in + self.state["naws_history"][self.state["freeze_event_counts"][1]:]] + self.state["resize_boundaries"].append(boundary) + self._record() + return False + require(self.state["resize_armed"] == target, "Resize phase differs") + ready = self.directory / f"resize-ready-{target}" + if not ready.exists(): + return False + grid, history = [self.state["columns"], self.state["rows"]], self.state["naws_history"] + require(json.loads(ready.read_text(encoding="utf-8")) == + {"phase": target, "grid": grid, "event_count": len(history)}, "Resize ready snapshot is stale") + boundary = self.state["resize_boundaries"][-1] + require(len(history) > boundary["before_event_count"] and history[-1] == grid == list(GRIDS[target]), + "Fixed resized grid or NAWS boundary differs") + boundary["after_event_count"] = len(history) + self.state.update(frozen_grid=grid, resize_armed=None) + self.state["frozen_grids"].append(grid) + self.state["freeze_event_counts"].append(len(history)) + self.state["phase_ready_events"].append({"phase": target, "frozen_grid": list(grid), + "event_count": len(history), "new_naws": True}) + return True + return BcdPeer + + +def source_identity(): + return common.source_identity(root=ROOT, source_files=SOURCE_FILES) + + +def probe_fragment(case): + validate_case(case) + source = (ROOT / CONTROLLER).read_text(encoding="utf-8") + require(source.count(CONTROLLER_ANCHOR) == 1, "Protected case template changed") + return source.replace(CONTROLLER_ANCHOR, json.dumps(case)) + + +def launcher_source(case): + validate_case(case) + source = (ROOT / "research/ui/r1_launch.py").read_text(encoding="utf-8") + replacements = { + "from research.ui.r1_vt_fixture import Peer": + "from research.ui.resize_bcd_calibration import peer_class, probe_fragment\n Peer = peer_class("+repr(case)+")", + 'Path(__file__).with_name("r1_menu_wrap.js").read_text(encoding="utf-8")': "probe_fragment("+repr(case)+")", + } + for old, new in replacements.items(): + require(source.count(old) == 1, "Protected launcher source changed") + source = source.replace(old, new) + return source + + +def fixed_contract(case): + validate_case(case) + return {"schema": "termforge-resize-bcd-calibration-plan-v1", "issue": 29, "related_issue": 27, "case_id": case, + "maximum_original_runs": 1, "actual_original_runs": 0, "phase_count": 4, + "windows": [list(v) for v in WINDOWS], "required_grids": [list(v) for v in GRIDS], + "resize_phases": list(RESIZE_PHASES), "window_rect_format": "qt-qrect-inclusive-ltrb", + "rea_capture_seconds": 45, "cli_wait_timeout_seconds": 60, "whole_lifecycle_bound_seconds": None, + "phases": phase_records(case), "reference_exe_sha256": EXE_SHA256, + "reference_files_manifest_sha256": MANIFEST_SHA256, "semantic_acceptance": False, + "action_phase": 2, "pre_action_phases": [0, 1], "post_initial_terminal_input": + "DSR at every phase; pre-write body snapshot is phase 1; exactly one declared CUP+RESIZED action after it is acknowledged", + "scope": "Fresh direct Telnet process per case; WindTerm protocol/display evidence only, not local-shell/native-grid proof; document, DSR and public marker rectangles remain separate", + "stop": "Wrong fixed geometry/grid, unarmed NAWS, stale snapshots, source/reply/phase mismatch, bounds or deadline failure; no retries"} + + +def synthetic_lines(case, phase): + """Finite source-derived text candidates, not predictions of the original policy. + + The initial body plus the single CUP 12;1 RESIZED prefix overwrite on one fixed + line. The marker is only at column 0 and keeps any tail after its seven bytes. + """ + validate_case(case) + require(type(phase) is int and 0 <= phase < 4, "Phase bound") + originals = set(BASE_LINES) + if phase < 2: + return originals + result = set(originals) + for line in originals: + result.add(("RESIZED"+line[7:]) if len(line) >= 7 else "RESIZED") + return result + + +def validate_document(case, phase, terminal): + require(type(terminal.get("line_count")) is int and 0 <= terminal["line_count"] <= 200 and + type(terminal.get("lines")) is list and len(terminal["lines"]) == terminal["line_count"], "Document bound") + require(all(type(line) is str and line in synthetic_lines(case, phase) for line in terminal["lines"]), + "Only fixed action-derived synthetic document lines are allowed") + require(sum(line.count("RESIZED") for line in terminal["lines"]) <= (0 if phase < 2 else 1), + "Repeated or premature action marker") + + +def audit_mechanics(directory, case): + validate_case(case) + read = lambda name: json.loads((directory / name).read_text(encoding="utf-8")) + require((directory / "stderr.txt").read_bytes() == b"" and not (directory / "launcher-failure.json").exists(), + "REA stderr/launcher failure retained") + peer, result, cleanup, profile = [read(name) for name in ("peer.json", RESULT_FILE, "cleanup.json", "profile-check.json")] + require(all(peer.get(k) is True for k in ("connected", "closed", "complete")) and not peer.get("error") and peer.get("case_id") == case, + "BCD peer did not complete cleanly") + require(result.get("status") == "observed-four-bcd-calibration-states" and result.get("case_id") == case and result.get("error") is None and + result.get("window_rect_format") == "qt-qrect-inclusive-ltrb" and result.get("presented_done") is None and + result.get("full_active_grid_verified") is False and result.get("local_shell_transport_verified") is False, + "BCD UI observation incomplete") + require(type(cleanup.get("active")) is int and cleanup["active"] == 0 and + all(type(cleanup.get(k)) is int and cleanup[k] >= 0 for k in ("total", "terminated")) and + profile == {"only_synthetic_session": True, "session_count": 1}, "Owned cleanup/profile differs") + grids, freezes, history = peer["frozen_grids"], peer["freeze_event_counts"], peer["naws_history"] + require(grids == [list(v) for v in GRIDS] and len(freezes) == 4 and type(history) is list and 1 <= len(history) <= 64 and + all(type(grid) is list and len(grid) == 2 and all(type(n) is int and 0 <= n <= 65535 for n in grid) for grid in history) and + all(type(n) is int and 1 <= n <= len(history) for n in freezes), "Fixed grids/freeze/NAWS bounds differ") + require(len(peer["resize_boundaries"]) == 2 and freezes[1] <= freezes[2], "Resize boundary count differs") + for index, boundary in zip(RESIZE_PHASES, peer["resize_boundaries"]): + before, after = boundary["before_event_count"], boundary["after_event_count"] + require(type(before) is int and type(after) is int and boundary["phase"] == index and + boundary["requested_window"] == list(WINDOWS[index]) and freezes[index-1] <= before < after == freezes[index] <= len(history) and + all(grid == grids[index-1] for grid in history[freezes[index-1]-1:before]) and + history[after-1] == grids[index], "NAWS resize boundary differs") + require(all(grid == grids[1] for grid in history[freezes[1]-1:freezes[2]]) and + all(grid == grids[3] for grid in history[freezes[3]-1:]), "Unarmed action/final NAWS change retained") + require(peer.get("initial_grid") == list(GRIDS[0]) and peer.get("phase2_new_naws_required") is False and + peer.get("frozen_grid_history") == grids, "Calibration checkpoint fields differ") + require(peer.get("expected_size_change_sequence") == [list(GRIDS[0]), list(GRIDS[1]), list(GRIDS[3])] and + peer.get("naws_size_changes") == peer["expected_size_change_sequence"], "Calibration size-change sequence differs") + events = peer.get("phase_ready_events") + require(type(events) is list and [event.get("phase") for event in events] == [1, 2, 3] and + events[1].get("new_naws_required") is False and + all(event.get("new_naws") is True for event in (events[0], events[2])), "Calibration phase-ready events differ") + checkpoint = peer["resize_boundaries"][1].get("phase2_observed_naws") + require(type(checkpoint) is list and all(event == list(GRIDS[2]) for event in checkpoint), + "Phase-2 checkpoint observations differ") + records, observations, checks = peer["records"], result["observations"], result["geometry_checks"] + require(len(records) == len(observations) == len(checks) == 4 and peer["phase"] == 3 and peer["phase_count"] == 4, + "Four phases incomplete") + replies = bytearray() + for index, (expected, actual, observed, check) in enumerate(zip(phase_records(case), records, observations, checks)): + grid, window = list(GRIDS[index]), list(WINDOWS[index]) + require(all(actual.get(k) == v for k, v in expected.items()) and actual["frozen_grid"] == grid, "Exact action source differs") + raw = bytes.fromhex(actual["response_hex"]); reply = common.reply_observation(raw) + require(reply is not None and all(actual.get(k) == v for k, v in reply.items()), "DSR differs") + require(all(observed.get(k) == actual[k] for k in ("phase", "label", "frozen_grid", *reply)), "Peer/UI phase differs") + require(check.get("phase") == index and check.get("expected_size") == window and check.get("window_size") == window and + baseline.qrect_size(check.get("window_rect")) == window and observed.get("window_size") == window and + baseline.qrect_size(observed.get("window_rect")) == window, "Exact geometry differs") + terminal = observed["terminal"] + require(terminal.get("connected") is True and terminal.get("protocol") == "Telnet" and + type(terminal.get("capacity")) is int and terminal["capacity"] == GRIDS[index][1], "UI binding differs") + validate_document(case, index, terminal) + require(type(terminal.get("markers")) is list and len(terminal["markers"]) <= 256, "Marker bound") + require((directory / f"ack-{index}").read_text(encoding="utf-8") == "observed", "Phase acknowledgment missing") + replies.extend(raw) + require(peer["received_hex"] == replies.hex(), "Unassigned reply bytes retained") + return {"schema": "termforge-resize-bcd-calibration-mechanics-v1", "case_id": case, "stage_count": 4, "dsr_count": 4, + "pre_action_stage_count": 2, "frozen_grids": grids, "naws_events": len(history), "job_active": 0, + "semantic_acceptance": False, "presented_done": None} + + +def prepare_plan(source, out, rea, destination, case, execution_authorized=False): + validate_case(case) + source, out, rea, destination = [Path(path).resolve() for path in (source, out, rea, destination)] + require(type(execution_authorized) is bool, "Execution authorization must be boolean") + require(not out.exists() and not destination.exists(), "Plan/output must be new") + require(out != ROOT / "local" and out.is_relative_to(ROOT / "local") and destination.is_relative_to(ROOT / "local"), + "Plan/output must belong to this worktree local directory") + require(not out.is_relative_to(source) and not source.is_relative_to(out), "Source/output overlap") + require(sha(source / "WindTerm.exe") == EXE_SHA256, "Pinned original executable differs") + plan = {**fixed_contract(case), **source_identity(), **common.host_identity(), "execution_authorized": execution_authorized, + "source": str(source), "out": str(out), "rea": str(rea), "rea_prepared_identity": common.rea_identity(rea), + "launcher_source_sha256": hashlib.sha256(launcher_source(case).encode()).hexdigest()} + destination.parent.mkdir(parents=True, exist_ok=True) + with destination.open("x", encoding="utf-8", newline="\n") as stream: + stream.write(json.dumps(plan, indent=2)+"\n") + return plan + + +def run_plan(path): + path = Path(path).resolve(); plan = json.loads(path.read_text(encoding="utf-8")) + require(plan.get("execution_authorized") is True, "Offline proposal only; execution is not authorized") + case = validate_case(plan.get("case_id")) + require(all(type(plan.get(k)) is type(v) and plan[k] == v for k, v in fixed_contract(case).items()), "Fixed addressing contract differs") + require(all(plan.get(k) == v for k, v in source_identity().items()), "Address source identity changed") + require(all(plan.get(k) == v for k, v in common.host_identity().items()), "Execution host/runtime differs") + require(plan["launcher_source_sha256"] == hashlib.sha256(launcher_source(case).encode()).hexdigest(), "Launcher source differs") + source, out, rea = [Path(plan[k]).resolve() for k in ("source", "out", "rea")] + require(path.is_relative_to(ROOT / "local") and out != ROOT / "local" and out.is_relative_to(ROOT / "local") and + not out.exists(), "Address run plan/output is not new/local") + require(not out.is_relative_to(source) and not source.is_relative_to(out), "Source/output overlap") + require(plan["rea_prepared_identity"] == common.rea_identity(rea), "Prepared REA entry changed") + require(sha(source / "WindTerm.exe") == EXE_SHA256, "Pinned original executable differs") + with path.with_suffix(".attempted").open("x", encoding="utf-8") as marker: + marker.write(ATTEMPT) + namespace = {"__file__": str(ROOT / "research/ui/r1_launch.py"), "__name__": "termforge_resize_reflow_addressing_launcher"} + exec(compile(launcher_source(case), namespace["__file__"], "exec"), namespace) + namespace["cli_command"] = lambda _: [plan["rea_prepared_identity"]["node_executable"], str(rea.parent.parent / "rea-agents/scripts/rea.mjs")] + namespace["run"](source, out, rea, 45, False, "menu-wrap") + require(all(plan.get(k) == v for k, v in source_identity().items()), "Address source identity changed during run") + mechanics = audit_mechanics(out, case) + (out / "mechanics-check.json").write_text(json.dumps(mechanics, indent=2)+"\n", encoding="utf-8", newline="\n") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--plan", type=Path, required=True) + parser.add_argument("--prepare", action="store_true") + parser.add_argument("--case", choices=list(CASES)) + parser.add_argument("--authorize-execution", action="store_true") + for name in ("source", "out", "rea"): + parser.add_argument("--"+name, type=Path) + args = parser.parse_args() + if args.prepare: + require(args.source and args.out and args.rea and args.case, "Source/output/REA/case required") + prepare_plan(args.source, args.out, args.rea, args.plan, args.case, args.authorize_execution) + print(json.dumps({"prepared": True, "case_id": args.case, "phase_count": 4, "execution_authorized": args.authorize_execution})) + else: + require(not args.authorize_execution and args.case is None and args.source is None and args.out is None and args.rea is None, + "Execution uses only the immutable plan") + run_plan(args.plan) diff --git a/research/ui/test_resize_bcd_calibration.py b/research/ui/test_resize_bcd_calibration.py new file mode 100644 index 0000000..0f2f8bf --- /dev/null +++ b/research/ui/test_resize_bcd_calibration.py @@ -0,0 +1,171 @@ +"""Offline guards for the dual-axis calibration peer contract; no sockets or original runtime.""" +import json +from pathlib import Path +import socket +import tempfile +import unittest + +from research.ui import resize_bcd_calibration as probe + + +def naws(columns, rows): + return bytes([255, 250, 31]) + columns.to_bytes(2, 'big') + rows.to_bytes(2, 'big') + bytes([255, 240]) + + +DSR = b'\x1b[1;1R' + + +class Connection: + def __init__(self, peer, mode): + self.peer, self.mode = peer, mode + self.initial, self.resize_sent, self.acked, self.sent = True, set(), set(), [] + + def settimeout(self, _): + pass + + def close(self): + pass + + def sendall(self, data): + self.sent.append(data) + + def recv(self, _): + if self.initial: + self.initial = False + return naws(80, 24) + state, directory = self.peer.state, self.peer.directory + record = state['records'][-1] if state['records'] else None + if record is not None and record['cursor_reply'] is None: + return DSR + if record is not None and record['phase'] not in self.acked: + self.acked.add(record['phase']) + (directory / f"ack-{record['phase']}").write_text('observed') + if record['phase'] in (0, 2): + target = record['phase'] + 1 + window = list(probe.WINDOWS[target]) + if self.mode == 'wrong-request': + window[0] += 1 + (directory / f"resize-request-{target}").write_text(json.dumps({'phase': target, 'window': window})) + if record['phase'] == 1 and self.mode == 'checkpoint-duplicate': + return naws(60, 12) + if record['phase'] == 1 and self.mode == 'unarmed-change': + return naws(70, 20) + raise socket.timeout() + armed = state.get('resize_armed') + if armed and armed not in self.resize_sent: + self.resize_sent.add(armed) + grid = [60, 12] if armed == 1 else [80, 24] + if self.mode == 'wrong-grid' and armed == 1: + grid = [60, 24] + count = len(state['naws_history']) + (0 if self.mode == 'stale-ready' else 1) + (directory / f"resize-ready-{armed}").write_text(json.dumps( + {'phase': armed, 'grid': grid, 'event_count': count})) + return naws(*grid) + raise socket.timeout() + + +def receive(mode='normal'): + with tempfile.TemporaryDirectory() as temporary: + peer_type = probe.peer_class('calibrate') + peer = peer_type.__new__(peer_type) + peer.directory = Path(temporary) + (peer.directory / 'fixture-ready').write_text(json.dumps({'grid': [80, 24], 'event_count': 1})) + peer.state = {'connected': False, 'columns': None, 'rows': None, 'phase': None, 'records': []} + peer.stop = type('NeverStopped', (), {'is_set': lambda _: False})() + connection = Connection(peer, mode) + peer.listener = type('Listener', (), {'accept': lambda _: (connection, ('127.0.0.1', 12345))})() + peer._serve() + return peer.state, connection.sent + + +class CalibrationPeerContractTests(unittest.TestCase): + def test_dual_axis_calibration_completes_with_checkpoint_duplicate(self): + state, sent = receive('checkpoint-duplicate') + self.assertNotIn('error', state) + self.assertTrue(state['complete'] and state['closed']) + self.assertEqual(state['frozen_grids'], [[80, 24], [60, 12], [60, 12], [80, 24]]) + self.assertEqual(state['freeze_event_counts'], [1, 2, 3, 4]) + self.assertEqual(state['naws_history'], [[80, 24], [60, 12], [60, 12], [80, 24]]) + self.assertEqual(state['naws_size_changes'], [[80, 24], [60, 12], [80, 24]]) + self.assertEqual(state['initial_grid'], [80, 24]) + self.assertEqual(state['expected_size_change_sequence'], [[80, 24], [60, 12], [80, 24]]) + self.assertIs(state['phase2_new_naws_required'], False) + self.assertEqual(state['frozen_grid_history'], state['frozen_grids']) + self.assertEqual([event['phase'] for event in state['phase_ready_events']], [1, 2, 3]) + self.assertIs(state['phase_ready_events'][1]['new_naws_required'], False) + self.assertTrue(all(event['new_naws'] for event in (state['phase_ready_events'][0], state['phase_ready_events'][2]))) + boundaries = state['resize_boundaries'] + self.assertEqual(len(boundaries), 2) + self.assertEqual(boundaries[0]['before_event_count'], 1) + self.assertEqual(boundaries[0]['after_event_count'], 2) + self.assertEqual(boundaries[1]['before_event_count'], 3) + self.assertEqual(boundaries[1]['after_event_count'], 4) + self.assertEqual(boundaries[1]['phase2_observed_naws'], [[60, 12]]) + self.assertEqual(sent[1:], [b'\x1b[6n'] * 4) + self.assertEqual(state['received_hex'], (DSR * 4).hex()) + + def test_calibration_completes_without_checkpoint_duplicate(self): + state, _ = receive() + self.assertNotIn('error', state) + self.assertTrue(state['complete']) + self.assertEqual(state['freeze_event_counts'], [1, 2, 2, 3]) + self.assertEqual(state['naws_size_changes'], [[80, 24], [60, 12], [80, 24]]) + self.assertEqual(state['resize_boundaries'][1]['phase2_observed_naws'], []) + + def test_unexpected_size_change_fails(self): + cases = {'wrong-grid': 'Unexpected resized grid', + 'unarmed-change': 'outside an armed resize', + 'stale-ready': 'stale', + 'wrong-request': 'immutable window'} + for mode, message in cases.items(): + with self.subTest(mode=mode): + state, sent = receive(mode) + self.assertIn(message, state['error']) + self.assertTrue(state['closed']) + self.assertFalse(state.get('complete', False)) + self.assertLess(len(sent), 6) + + def test_mechanics_audit_accepts_contract_and_rejects_tampering(self): + state, _ = receive('checkpoint-duplicate') + result = {'status': 'observed-four-bcd-calibration-states', 'case_id': 'calibrate', 'error': None, + 'presented_done': None, 'full_active_grid_verified': False, 'local_shell_transport_verified': False, + 'window_rect_format': 'qt-qrect-inclusive-ltrb', 'geometry_checks': [], 'observations': []} + for record, window in zip(state['records'], probe.WINDOWS): + raw_rect = [113, -27, 113 + window[0] - 1, -27 + window[1] - 1] + result['geometry_checks'].append({'phase': record['phase'], 'window_rect': raw_rect, + 'window_size': list(window), 'expected_size': list(window)}) + result['observations'].append({**{k: record[k] for k in + ('phase', 'label', 'frozen_grid', 'cursor_reply', 'cursor_reply_private', 'response_hex')}, + 'window_rect': raw_rect, 'window_size': list(window), 'terminal': {'connected': True, 'protocol': 'Telnet', + 'capacity': record['frozen_grid'][1], 'line_count': 0, 'lines': [], 'markers': []}}) + with tempfile.TemporaryDirectory() as temporary: + directory = Path(temporary) + values = {'peer.json': state, 'bcd-calibration-result.json': result, + 'cleanup.json': {'active': 0, 'total': 4, 'terminated': 0}, + 'profile-check.json': {'only_synthetic_session': True, 'session_count': 1}} + for name, value in values.items(): + (directory / name).write_text(json.dumps(value)) + (directory / 'stderr.txt').write_bytes(b'') + for index in range(4): + (directory / f'ack-{index}').write_text('observed') + mechanics = probe.audit_mechanics(directory, 'calibrate') + self.assertEqual(mechanics['naws_events'], 4) + self.assertFalse(mechanics['semantic_acceptance']) + state['naws_size_changes'] = [[80, 24], [60, 12], [60, 12], [80, 24]] + (directory / 'peer.json').write_text(json.dumps(state)) + with self.assertRaisesRegex(ValueError, 'size-change sequence'): + probe.audit_mechanics(directory, 'calibrate') + state['naws_size_changes'] = [[80, 24], [60, 12], [80, 24]] + state['phase_ready_events'][1]['new_naws_required'] = True + (directory / 'peer.json').write_text(json.dumps(state)) + with self.assertRaisesRegex(ValueError, 'phase-ready events'): + probe.audit_mechanics(directory, 'calibrate') + state['phase_ready_events'][1]['new_naws_required'] = False + state['resize_boundaries'][1]['phase2_observed_naws'] = [[80, 24]] + (directory / 'peer.json').write_text(json.dumps(state)) + with self.assertRaisesRegex(ValueError, 'checkpoint observations'): + probe.audit_mechanics(directory, 'calibrate') + + +if __name__ == '__main__': + unittest.main() From 3a3009c257fc26a55ba77a44f9a05c857f3a7673 Mon Sep 17 00:00:00 2001 From: JavaZeroo-Bot <133958205+JavaZeroo-Bot@users.noreply.github.com> Date: Thu, 12 Nov 2026 01:18:58 +0800 Subject: [PATCH 2/3] research(ui): fix the BCD pre-write fixture NAWS gate The v1 B/C/D pre-write probe reused the same baseline BoundPeer row-count guard as the calibration peer, so its fixed 80x24 -> 60x12 -> 60x12 -> 80x24 resize would hit the same "Resize changed row count" failure at phase 1 (static conclusion from the shared inheritance; the old v1 run never reached that point). Apply the identical calibration NAWS contract override inside this probe: size changes must follow the plan sequence 80x24 -> 60x12 -> 80x24, armed resizes need a new post-request target-grid NAWS, the phase-2 checkpoint requires no new NAWS and records same-size duplicates only, and any unexpected size change fails. Specific differences from the calibration probe: only the _on_naws override and its naws_size_changes recording are added here; the windows, grids, phase bodies, document allowlist and the literal mechanics acceptance are unchanged, and no calibration-only audit fields are required. An offline socket-free simulation of the four-phase NAWS/DSR/ack flow now completes with frozen grids [[80,24],[60,12],[60,12],[80,24]]. This does not close #27 or #29, does not choose A/B/C, and does not change product behavior; the old v1 plan, attempt marker and raw evidence stay untouched. Co-authored-by: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> --- research/ui/resize_bcd_address_reference.py | 29 +++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/research/ui/resize_bcd_address_reference.py b/research/ui/resize_bcd_address_reference.py index b24be81..7fcfb7c 100644 --- a/research/ui/resize_bcd_address_reference.py +++ b/research/ui/resize_bcd_address_reference.py @@ -69,11 +69,40 @@ def peer_class(case): base._serve.__globals__["phases"] = lambda rows, columns: phases(case, rows, columns) class BcdPeer(base): + """B/C/D peer; see resize_bcd_calibration.BcdPeer for the shared contract. + + The baseline width-only row-count guard contradicts this plan's fixed + 80x24 -> 60x12 -> 60x12 -> 80x24 dual-axis resize, so the NAWS gate is + overridden with the same calibration contract used by the geometry + calibration: phases 1/3 are resize events requiring a new post-request + target-grid NAWS, phase 2 is a same-size checkpoint (no new NAWS + required, duplicates recorded only), and any unexpected size change + fails. The literal acceptance below is unchanged. + """ + def _freeze_initial(self): super()._freeze_initial() require(self.state["frozen_grid"] == list(GRIDS[0]), "Fixed initial grid differs") self.state["case_id"] = case + def _on_naws(self): + grid = [self.state["columns"], self.state["rows"]] + history = self.state.setdefault("naws_history", []) + require(len(history) < 64 and all(type(n) is int and 0 <= n <= 65535 for n in grid), "NAWS event bound") + history.append(grid) + self._record() + changes = self.state.setdefault("naws_size_changes", []) + if not changes or changes[-1] != grid: + changes.append(list(grid)) + if self.state.get("frozen_grid"): + armed = self.state.get("resize_armed") + if grid == self.state["frozen_grid"]: + # Same-size repeat (phase-2 checkpoint or a duplicate during an + # armed resize): record only, never a new-size acknowledgement. + return + require(armed is not None, "NAWS changed outside an armed resize") + require(grid == list(GRIDS[armed]), "Unexpected resized grid") + def _phase_ready(self, index): if index == -1: return True From 3ad36e3b910b59abe1c4a2c4523bc7e3483f0628 Mon Sep 17 00:00:00 2001 From: JavaZeroo-Bot <133958205+JavaZeroo-Bot@users.noreply.github.com> Date: Thu, 12 Nov 2026 01:35:51 +0800 Subject: [PATCH 3/3] research(ui): record the BCD calibration retry action budget Ledger for the single authorized calibration retry after the dual-axis peer contract fix: identity/environment checks stay read-only and bounded, the fixed source deploys only to a new staging copy (src-0 and the old plan/attempt/raw stay untouched), the display change is recorded and restored, and the frozen new plan runs once with first-failure stop. The v2 formal retry remains gated on a fully successful calibration. This does not close #27 or #29 and does not choose A/B/C. Co-authored-by: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> --- .../ACTION-BUDGET.md | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 research/ui/results/2026-11-12-bcd-calibration-retry/ACTION-BUDGET.md diff --git a/research/ui/results/2026-11-12-bcd-calibration-retry/ACTION-BUDGET.md b/research/ui/results/2026-11-12-bcd-calibration-retry/ACTION-BUDGET.md new file mode 100644 index 0000000..18fe737 --- /dev/null +++ b/research/ui/results/2026-11-12-bcd-calibration-retry/ACTION-BUDGET.md @@ -0,0 +1,33 @@ +# B/CD 校准重试:预写动作预算与停止条件(2026-11-12) + +## 授权与基线 + +- 授权依据:issue #29 comment 6741368867 的修订离线计划 + 用户最新指示:允许**一次**新增校准执行(新增额度 1,首失败停止);校准成功后才执行原尚未使用的唯一 v2 正式取证授权(仍有效,不需重新请求)。 +- 基线:main `abc71fe5a3577c5cfb54fe4869506c24fb7288e0`(#332 合并);工作分支 `agent/bcd-k3/record-hidden-retention-2e7373117ea0`。 +- 修复提交:`8272d21`(校准 peer 双轴合同修复 + 离线测试)、`3a3009c`(v2 夹具同一守卫修复)。 +- 新源哈希:`resize_bcd_calibration.py` = `e3e2d261ca9f08237852a928acc6418a54665f1991de873916149288f7a0735f`;`resize_bcd_calibration.js` = `296f02e4f384f8a722e62b7f8b39f3f70918d5bbc4ba95d35ff8b1c82f946efe`(未变);v2 夹具 `resize_bcd_address_reference.py` = `71a5c17378e55e54954ae5632f1684ed0bdfb0656cbb3a1c2d755d10b4699e92`。 +- 旧失败/plan/attempt/raw 全部保留:v1 plan `7718a4b8…`、marker `20fd56d7…`、校准 plan `954636ee…`(2026-11-09)不重放、不改写。 +- 严格 SSH 主机身份校验;不绕过 known_hosts,不重启主机/服务,不改电源,不触碰个人桌面/剪贴板,不改设置。 + +## 阶段与动作预算 + +1. **身份/可达(只读,最多 8 条远程命令)**:SSH 登录、hostname、VM 状态、guest 会话标识。任一不符即停止。 +2. **环境身份(只读,最多 10 条)**:固定 WindTerm 2.7.0 EXE SHA-256(`7b10531d…`)、旧 plan/marker 哈希未变、staging 源副本 `r29-bcd-calibration-src-0` 存在且哈希未变、显示当前 1024×768。不得修改或重录旧文件。 +3. **部署修复源**:guest 内新建 staging 副本 `r29-bcd-calibration-src-1`(复制 src-0),仅覆盖 `research/ui/resize_bcd_calibration.py` 为修复版,并更新 `reference-source.json` 清单中该文件哈希(其余文件哈希不变);上传后逐文件复核哈希。src-0 不动。 +4. **显示调整(一次,需记录并恢复)**:1024×768 → 1600×1200(同 2026-11-09 路径);记录 change_result 与 bounds。 +5. **冻结新计划**:`python -m research.ui.resize_bcd_calibration --prepare --authorize-execution --case calibrate` 生成新 plan(新源哈希、`execution_authorized=true`),记录 plan SHA-256;`run_plan` 以 `.attempted` 守卫只允许一次。 +6. **一次校准执行(≤300s)**:4-phase 固定窗口/网格;phase1/3 必须有 armed 后新目标 NAWS;phase2 同尺寸 checkpoint;任何窗口/DSR/phase/frozen 不符即停;不因失败重跑/换窗/改预期。 +7. **恢复显示**:1600×1200 → 1024×768,记录 restore_result 与 bounds。 +8. **仅在校准 4/4 成功后**:冻结 v2 新源/计划/身份/attempt(窗口取自已校准几何),执行唯一 v2 正式取证;失败即停。 + +## 停止条件 + +- host key 不匹配、SSH 登录失败、非专用取证 VM/guest、WindTerm 2.7.0/执行源不符。 +- 旧 plan/attempt/raw 已被改写(v2 尚未执行时只记录,不重复)。 +- 环境不可用(主机不可达/VM 不在运行/guest 不响应):报告具体阻塞,不重启主机/服务或改设置。 +- 校准任何 phase/字段不符、peer 未 clean、`.attempted` 已存在。 +- 同一故障额度用尽;不再重试/换名/滚动增加预算。 + +## 记录 + +每次动作记录时间、命令、只读结果/哈希、是否改变环境;私有 raw 留本地(专用 VM),公开材料仅身份与脱敏结果;显示临时调整必须记录并恢复。