From 3ca03eb25f0d8eadfbb319ee93fefca5bdda7335 Mon Sep 17 00:00:00 2001 From: JavaZeroo-Bot <133958205+JavaZeroo-Bot@users.noreply.github.com> Date: Tue, 3 Nov 2026 00:53:30 +0800 Subject: [PATCH 1/8] test(research): freeze sequential width and height retention probe Co-authored-by: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> --- CMakeLists.txt | 6 + .../ui/export_resize_combined_reference.py | 239 +++++++++++ research/ui/resize_combined_reference.js | 132 ++++++ research/ui/resize_combined_reference.md | 55 +++ research/ui/resize_combined_reference.py | 381 ++++++++++++++++++ .../test_export_resize_combined_reference.py | 378 +++++++++++++++++ research/ui/test_resize_combined_reference.py | 259 ++++++++++++ research/ui/test_resize_combined_stages.js | 158 ++++++++ 8 files changed, 1608 insertions(+) create mode 100644 research/ui/export_resize_combined_reference.py create mode 100644 research/ui/resize_combined_reference.js create mode 100644 research/ui/resize_combined_reference.md create mode 100644 research/ui/resize_combined_reference.py create mode 100644 research/ui/test_export_resize_combined_reference.py create mode 100644 research/ui/test_resize_combined_reference.py create mode 100644 research/ui/test_resize_combined_stages.js diff --git a/CMakeLists.txt b/CMakeLists.txt index 00532d86..1216ac67 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -505,6 +505,8 @@ add_test(NAME edit_csi_core COMMAND edit_csi_test) termforge_add_python_test(windterm_resize_geometry_optimized -O -m unittest research.ui.test_resize_geometry_reference research.ui.test_export_resize_geometry_reference -v) termforge_add_python_test(windterm_resize_height_measured_reference -m unittest research.ui.test_resize_height_measured_reference research.ui.test_export_resize_height_measured_reference -v) termforge_add_python_test(windterm_resize_height_measured_optimized -O -m unittest research.ui.test_resize_height_measured_reference research.ui.test_export_resize_height_measured_reference -v) + termforge_add_python_test(windterm_resize_combined_reference -m unittest research.ui.test_resize_combined_reference research.ui.test_export_resize_combined_reference -v) + termforge_add_python_test(windterm_resize_combined_optimized -O -m unittest research.ui.test_resize_combined_reference research.ui.test_export_resize_combined_reference -v) find_program(TERMFORGE_NODE_EXECUTABLE NAMES node) if(TERMFORGE_NODE_EXECUTABLE) add_test(NAME windterm_resize_address_controller COMMAND ${TERMFORGE_NODE_EXECUTABLE} @@ -551,6 +553,10 @@ add_test(NAME edit_csi_core COMMAND edit_csi_test) ${CMAKE_SOURCE_DIR}/research/ui/test_resize_height_measured_stages.js ${CMAKE_SOURCE_DIR}/research/ui/resize_height_measured_reference.js) set_tests_properties(windterm_resize_height_measured_controller PROPERTIES TIMEOUT 15) + add_test(NAME windterm_resize_combined_controller COMMAND ${TERMFORGE_NODE_EXECUTABLE} + ${CMAKE_SOURCE_DIR}/research/ui/test_resize_combined_stages.js + ${CMAKE_SOURCE_DIR}/research/ui/resize_combined_reference.js) + set_tests_properties(windterm_resize_combined_controller PROPERTIES TIMEOUT 15) endif() # Pure offline regressions only; OwnedNodeTests and frozen batches are never launched here. termforge_add_python_test(node_contract_export -m unittest research.ui.test_export_node_contract_deadline research.ui.test_node_contract_deadline.CheckpointTests research.ui.test_node_contract_deadline.LauncherVerdictTests -v) diff --git a/research/ui/export_resize_combined_reference.py b/research/ui/export_resize_combined_reference.py new file mode 100644 index 00000000..0ef0bab2 --- /dev/null +++ b/research/ui/export_resize_combined_reference.py @@ -0,0 +1,239 @@ +"""Strict publication of one five-stage width/height protocol and finite source text.""" +from __future__ import annotations +import argparse +import hashlib +import json +from pathlib import Path, PureWindowsPath +import re +import sys +from types import FunctionType, SimpleNamespace + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +from research.ui import export_mode_state_reference as common +from research.ui import export_resize_state_reference as resize_export +from research.ui import resize_combined_reference as probe + +require, sha, digest, integer = common.require, common.sha, common.digest, common.integer +MARKERS = probe.MARKERS +EXPORT_SOURCES = ("research/ui/export_resize_combined_reference.py", "research/ui/export_resize_state_reference.py", + "research/ui/export_mode_state_reference.py") + + +def raw_names(case): + return ("evidence.json", "run.json", "probe.js", "peer.json", probe.RESULT_FILE, "files.json", + "mechanics-check.json", "stderr.txt", "profile-check.json", "cleanup.json", "fixture-ready", + *(f"ack-{n}" for n in range(len(probe.phases(case)))), + *(f"resize-{kind}-{n}" for n in probe.resize_phases(case) for kind in ("request", "ready"))) + + +def read(path): + require(path.stat().st_size <= 4*1024*1024, "Recording exceeds byte bound") + def unique(pairs): + result = {} + for key, value in pairs: + require(key not in result, "Duplicate recording field") + result[key] = value + return result + def nonfinite(_): + raise ValueError("Nonfinite JSON value") + return json.loads(path.read_text(encoding="utf-8"), object_pairs_hook=unique, parse_constant=nonfinite) + + +def adapt(source, changes): + for old, new in changes.items(): + require(source.count(old) == 1, "Protected offline audit adapter changed: "+old) + source = source.replace(old, new) + return source + + +def extract(path, start, end): + source = path.read_text(encoding="utf-8") + require(source.count(start) == source.count(end) == 1, "Protected offline audit function boundary changed") + return start+source.split(start, 1)[1].split(end, 1)[0] + + +def expected_probe(plan, run, source_root): + integer(run["pid"], 2**32-1, 1); integer(run["tid"], 2**32-1, 1) + require(type(run["window_station"]) is str and re.fullmatch(r"TermForgeR1-[0-9]+", run["window_station"]), + "Owned station identity differs") + template = (source_root / "research/ui/qt_r1_text.js").read_text(encoding="utf-8").split("// Scenario driver", 1)[0] + driver = (source_root / "research/ui/resize_combined_reference.js").read_text(encoding="utf-8") + require(driver.count("__RESIZE_COMBINED_CASE_JSON__") == 1, "Case template anchor differs") + template += driver.replace("__RESIZE_COMBINED_CASE_JSON__", json.dumps(probe.controller_contract(plan["case_id"]))) + require(hashlib.sha256(template.encode()).hexdigest() == run["template_sha256"], "Probe template differs") + generated = template.replace("__UI_THREAD_ID__", str(run["tid"])).replace("__OWNED_STATION__", run["window_station"]) + generated = generated.replace("__RUN_DIRECTORY_JSON__", json.dumps(PureWindowsPath(plan["out"]).as_posix())) + generated = generated.replace("__HOSTED_CI__", "false").replace("__SCENARIO_TIMEOUT_MS__", "43000") + return generated.encode().replace(b"\n", b"\r\n") + + +# The immutable existing helpers are adapted in fresh namespaces. Each textual +# boundary is checked, and all three offline source files are identified below. +_source = extract(Path(common.__file__), "def provenance_facts(", "\n\ndef export(") +_source = adapt(_source, { + "One original launch attempt; retain after failure and never retry this plan.": probe.ATTEMPT.rstrip("\n"), + "probe.source_identity(root=source_root, source_files=probe.SOURCE_FILES)": + "probe.common.source_identity(root=source_root, source_files=probe.SOURCE_FILES)", +}) +_PROVENANCE = {**vars(common), "ROOT": ROOT, "probe": probe, "read": read} +exec(compile(_source, str(Path(common.__file__)), "exec"), _PROVENANCE) + + +def provenance_facts(directory, plan_path, plan, source_root, reference_source): + function = FunctionType(_PROVENANCE["provenance_facts"].__code__, {**_PROVENANCE, "RAW_NAMES": raw_names(plan["case_id"])}) + return function(directory, plan_path, plan, source_root, reference_source) + + +_marker_projection = FunctionType(common.marker_projection.__code__, {**vars(common), "MARKERS": MARKERS}) + + +def marker_projection(terminal): + result = [] + for marker, raw in zip(_marker_projection(terminal), terminal["markers"]): + line = terminal["lines"][marker["document_line"]] + # The finite source alphabet is entirely ASCII. Python scalar offsets and + # JS UTF-16 offsets therefore coincide here, not for arbitrary Unicode. + require(len(line.encode("utf-16-le"))//2 == len(line), "Unexpected non-ASCII text") + start = integer(raw["line_start_api"], 262144) + require(marker["position"] == start+marker["column"], "Captured API-call arithmetic differs") + rect = marker["rect"] + if rect is not None: + require(0 <= rect["width"] <= 1360 and 0 <= rect["height"] <= 749 and + -1360 <= rect["x"] <= 2720 and -200*749 <= rect["y"] <= 2*749, + "Marker rectangle exceeds fixed window/document bounds") + result.append({"label": marker["label"], "document_line": marker["document_line"], + "line_text_offset_utf16": marker["column"], "line_text_offset_scalar": marker["column"], + "line_text_prefix_utf8_bytes": len(line[:marker["column"]].encode("utf-8")), + "line_start_api_value": start, "requested_position_api_value": marker["position"], "rect": rect}) + return result +geometry = FunctionType(resize_export.geometry.__code__, {**vars(resize_export), "probe": probe}) +_source = extract(Path(resize_export.__file__), "def capture_facts(", "\n\ndef export(") +_source = adapt(_source, { + "probe.audit_mechanics(directory)": 'probe.audit_mechanics(directory, plan["case_id"])', + '"resize-state-result.json"': 'probe.RESULT_FILE', + "6 <= len(messages) <= 69": 'len(probe.phases(plan["case_id"]))+3 <= len(messages) <= len(probe.phases(plan["case_id"]))+66', + '"scenario": "resize-state-v1"': '"scenario": "resize-combined-v1", "case_id": plan["case_id"]', + "count = len(payloads)-5": 'count = len(payloads)-len(probe.phases(plan["case_id"]))-2', + '"resize_state_phase"': '"resize_combined_phase"', + '"resize_state_result"': '"resize_combined_result"', + "for phase in (1, 2):": 'for phase in probe.resize_phases(plan["case_id"]):', + "probe.phase_records(grids[0][1], grids[0][0])": 'probe.phase_records(plan["case_id"], grids[0][1], grids[0][0])', + "synthetic_lines(grids[0][0])": 'probe.synthetic_lines(plan["case_id"], source["phase"])', + '"dsr_request_bytes": 12': '"dsr_request_bytes": 4*len(probe.phases(plan["case_id"]))', + 'enumerate(peer["resize_boundaries"], 1)': 'zip(probe.RESIZE_PHASES, peer["resize_boundaries"])', + '"phase_messages": 3': '"phase_messages": len(probe.phases(plan["case_id"]))', +}) +_CAPTURE = {**vars(resize_export), "ROOT": ROOT, "probe": probe, "read": read, + "expected_probe": expected_probe, "geometry": geometry, "marker_projection": marker_projection} +exec(compile(_source, str(Path(resize_export.__file__)), "exec"), _CAPTURE) + + +def capture_facts(directory, plan, source_root=ROOT): + case = probe.validate_case(plan["case_id"]) + # A fresh binding supplies each immutable case's geometry. The imported + # runtime and frozen exporter namespaces are never mutated. + case_probe = SimpleNamespace(**{**vars(probe), "WINDOWS": probe.windows(case), "RESIZE_PHASES": probe.resize_phases(case)}) + function = FunctionType(_CAPTURE["capture_facts"].__code__, {**_CAPTURE, "probe": case_probe}) + result = function(directory, plan, source_root) + raw = read(directory / probe.RESULT_FILE) + # Publish the sampled geometry guard, not merely the requested endpoint. + # The runtime saved these scalars before asserting geometry; failed runs + # remain private and cannot reach this successful export path. + for index, check in enumerate(result["geometry_checks"]): + observed = raw["geometry_checks"][index] + require(type(observed["observed_grid"]) is list and len(observed["observed_grid"]) == 2, + "Observed geometry grid shape differs") + grid = [integer(observed["observed_grid"][0], 500, 1), integer(observed["observed_grid"][1], 200, 1)] + capacity = integer(observed["terminal_capacity"], 200, 1) + event_count = integer(observed["event_count"], 64, 1) + require(grid == result["frozen_grids"][index] and capacity == grid[1] and + event_count == result["freeze_event_counts"][index], "Observed geometry guard differs") + check.update(observed_grid=grid, terminal_capacity=capacity, event_count=event_count) + roles = [label for label, _ in probe.phases(case)] + for index, (phase, observed) in enumerate(zip(result["phases"], raw["observations"])): + phase["stage_role"] = roles[index] + lines = observed["terminal"]["lines"] + phase["document_projection"]["synthetic_lines_sha256"] = hashlib.sha256(json.dumps(lines, separators=(",", ":")).encode()).hexdigest() + # Validate exact source derivation before publishing any reconstructible + # text. The unrestricted original recording remains private. + probe.validate_document(case, index, observed["terminal"]) + catalog = sorted(probe.synthetic_lines(case, index)) + positions = {line: offset for offset, line in enumerate(catalog)} + phase["document_projection"]["synthetic_text"] = { + "encoding": "UTF-8", "catalog_source": "resize_combined_reference.synthetic_lines(case_id, phase); sorted ASCII scalar order", + "catalog_count": len(catalog), + "catalog_sha256": hashlib.sha256(json.dumps(catalog, separators=(",", ":")).encode()).hexdigest(), + "lines": [{"catalog_index": positions[line], "utf8_hex": line.encode("utf-8").hex(), + "utf8_bytes": len(line.encode("utf-8")), "unicode_scalars": len(line), + "utf16_code_units": len(line.encode("utf-16-le"))//2} for line in lines]} + result["stateful_observation_count"] = len(roles) + result["post_initial_terminal_input"] = "DSR only" + result["independent_dsr_bounds"] = {"row": [1, 200], "column": [1, 500]} + result["text_units"] = {"source": "UTF-8 bytes", "document_line": "original public API index", + "marker_text_offsets": "JS UTF-16 code units; scalar count coincides only for this ASCII source alphabet", + "position_api": "recorded lineStart API value plus JS UTF-16 offset, passed unchanged to visibleRectFromPosition", + "caret_api": "original API scalar values; numbering policy not inferred", + "rectangles": "actual public API pixel rectangles, kept separate from protocol cells", + "protocol_cursor": "DSR values kept independent of text offsets and pixel rectangles", + "cpp_cell_column_mapping": None, "document_position_unit_classification": None, + "ascii_only": True} + return result + + +def export(directory, plan_path, destination, expected_plan_sha256, source_root=ROOT, reference_source=None): + directory, plan_path, destination, source_root = [Path(path).resolve() for path in (directory, plan_path, destination, source_root)] + require(not destination.exists(), "Export destination must be new") + require(sha(plan_path) == digest(expected_plan_sha256), "Authorized plan bytes differ") + plan = read(plan_path) + require(all(type(plan.get(k)) is type(v) and plan[k] == v for k, v in probe.fixed_contract(plan.get("case_id")).items()), + "Fixed resize-combined contract differs") + require(plan.get("execution_authorized") is True, "Plan did not authorize execution") + facts = capture_facts(directory, plan, source_root) + reference_source = Path(plan["source"]) if reference_source is None else Path(reference_source) + provenance = provenance_facts(directory, plan_path, plan, source_root, reference_source.resolve()) + basis = probe.fixed_contract(plan["case_id"])["geometry_basis"] + public_basis = {"combined_grid_previously_observed": False} + for key in ("geometry", "pure_height"): + entry = basis[key] + require(type(entry["execution_source_base_commit"]) is str and + re.fullmatch(r"[0-9a-f]{40}", entry["execution_source_base_commit"]), "Basis source commit differs") + public_basis[key] = {"reference": entry["reference"], + "strict_export_sha256": digest(entry["strict_export_sha256"]), + "execution_source_base_commit": entry["execution_source_base_commit"]} + result = {"schema": "termforge-original-resize-combined-reference-v1", "issue": 29, "related_issue": 27, + "case_id": plan["case_id"], "authorized_plan_sha256": expected_plan_sha256, + "geometry_basis": public_basis, + "offline_export_source_files_sha256": {name: sha(ROOT / name) for name in EXPORT_SOURCES}, + "status": "observation", "observation_kind": "bounded-direct-protocol-and-combined-resize", "actual_original_runs": 1, "maximum_original_runs": 1, + "semantic_acceptance": False, "combined_policy_classification": None, "full_active_grid_verified": False, + "terminal_styles_verified": False, "local_shell_transport_verified": False, "presented_done": None, + "performance_gate": "not_evaluated", "r2_complete": False, "rea_capture_seconds": 45, + "cli_wait_timeout_seconds": 60, "whole_lifecycle_bound_seconds": None, + "deliberate_original_process_termination": True, "natural_gui_exit_verified": False, + "capture": facts, "provenance": provenance, "limitations": [ + "One fresh direct Telnet process executes the immutable width-height-retention schedule 91x20 -> 61x20 -> 61x14 -> 61x20 -> 91x20; every stage is observed before the next resize.", + "After stage zero, terminal input is DSR only: no CUP, CR/LF, redraw, replacement text or post-resize write.", + "Stage zero requires every preloaded sentinel and DSR19;7; later missing markers are observations, never semantic acceptance.", + "The separate geometry and pure-height observations identify prior evidence only. The combined 61x14 endpoint and subsequent restorations are verified in this run, not inherited from those observations. Exact geometry and NAWS remain mandatory, with no calibration or retries.", + "Document indices, API positions, ASCII byte/UTF16 offsets, DSR and pixel rectangles are recorded separately; no general original position-unit or C++ Cell mapping is inferred.", + "DSR safety bounds remain row1..200/column1..500 independently of the fourteen-row viewport.", + "Only finite ASCII source-derived catalog strings may be published, with complete reconstructible document arrays. Unrestricted recording stays private.", + "This single schedule does not establish alternate-frame, Unicode, arbitrary dimensions or resize order, post-resize editing, ConPTY, performance or R2 acceptance."]} + payload = (json.dumps(result, ensure_ascii=False, indent=2, allow_nan=False)+"\n").encode() + destination.parent.mkdir(parents=True, exist_ok=True) + with destination.open("xb") as stream: + stream.write(payload) + return result + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + for name in ("directory", "plan", "destination"): + parser.add_argument("--"+name, type=Path, required=True) + parser.add_argument("--expected-plan-sha256", required=True) + parser.add_argument("--source-root", type=Path, default=ROOT) + parser.add_argument("--reference-source", type=Path) + args = parser.parse_args() + result = export(args.directory, args.plan, args.destination, args.expected_plan_sha256, args.source_root, args.reference_source) + print(json.dumps({"exported": True, "case_id": result["case_id"], "phases": len(result["capture"]["phases"]), "sha256": sha(args.destination)})) diff --git a/research/ui/resize_combined_reference.js b/research/ui/resize_combined_reference.js new file mode 100644 index 00000000..c9f3ee66 --- /dev/null +++ b/research/ui/resize_combined_reference.js @@ -0,0 +1,132 @@ +// Every fixed single-axis resize is observed before the next; no input after the ASCII body except DSR. +let stage=0,busy=false,finished=false,due=0,stable=null,stableSince=0,lastPhase=-1; +let gridKey=null,gridSince=0,nawsSeen=0,lastNaws=null,requested=0,resized=0,committed=0; +const contract=__RESIZE_COMBINED_CASE_JSON__,caseId=contract.case_id; +const counts={'width-height-retention':5}; +const phaseCount=counts[caseId],lastStage=phaseCount-1; +const windows=contract.windows,grids=contract.grids,resizePhases=contract.resize_phases; +if(!phaseCount||windows.length!==phaseCount||grids.length!==phaseCount||contract.labels.length!==phaseCount||resizePhases.length!==phaseCount-1||!Array.isArray(contract.marker_labels))throw Error('fixed combined contract'); +const started=Date.now(),observations=[],geometryChecks=[]; +for(const module of [wind,core,gui,widgets]){ + const expected=(__RUN_DIRECTORY_JSON__+'/app/'+module.name).replace(/\\/g,'/').toLowerCase(); + if(module.path.replace(/\\/g,'/').toLowerCase()!==expected)throw Error('pinned Qt module path guard'); +} +function currentTerminal(){ + const raw=evalText(`JSON.stringify((function(){var t=window.activeText;if(!t)return null;function get(k){try{var v=typeof t[k]==='function'?t[k]():t[k];return v===undefined?null:v;}catch(e){return null;}}var n=get('visibleLineCount'),lines=[],markers=[],labels=${JSON.stringify(contract.marker_labels)};if(typeof n!=='number'||n<0||n>200)throw Error('document line bound');for(var i=0;i1024)throw Error('document text bound');lines.push(line);for(var j=0;j=256)throw Error('marker bound');var start=t.lineStart(i),p=start+offset;markers.push({label:labels[j],document_line:i,column:offset,line_start_api:start,position:p,rect:t.visibleRectFromPosition(p)});}}}return {protocol:get('protocolName'),connected:get('isConnected'),top:get('topLine'),scroll:get('scrollTop'),line_count:n,capacity:get('screenLineCapacity'),lines:lines,markers:markers,caret_api:{caretLine:get('caretLine'),caretColumn:get('caretColumn'),caretPosition:get('caretPosition')}};})())`); + return raw?JSON.parse(raw):null; +} +function write(name,value){const f=new File(__RUN_DIRECTORY_JSON__+'/'+name,'w');f.write(value);f.close();} +function windowGeometry(){ + const value=rect(main); + if(!Array.isArray(value)||value.length!==4||!value.every(Number.isInteger)||value[2]Number.isInteger(value)?value:null; + return {phase,requested_window:windows[phase],...windowGeometry(), + observed_grid:peer?[scalar(peer.columns),scalar(peer.rows)]:null, + terminal_capacity:terminal?scalar(terminal.capacity):null, + event_count:peer&&Array.isArray(peer.naws_history)?peer.naws_history.length:null}; +} +function checkGeometry(phase,peer,terminal){ + if(geometryChecks.length!==phase)throw Error('window geometry check order'); + const sample=geometrySample(phase,peer,terminal); + const measured={phase,window_rect:sample.window_rect,window_size:sample.window_size,expected_size:windows[phase], + observed_grid:sample.observed_grid,terminal_capacity:sample.terminal_capacity,event_count:sample.event_count}; + // Persist all measured scalars before any prediction can reject this endpoint. + geometryChecks.push(measured); + if(JSON.stringify(measured.window_size)!==JSON.stringify(windows[phase]))throw Error('public window dimensions differ'); + if(JSON.stringify(measured.observed_grid)!==JSON.stringify(grids[phase]))throw Error('fixed measured grid differs'); + if(measured.terminal_capacity!==grids[phase][1])throw Error('fixed measured capacity differs'); +} +function finish(status,error){ + if(finished)return;finished=true; + const result={status,case_id:caseId,error:error||null,observations,window_rect_format:'qt-qrect-inclusive-ltrb',geometry_checks:geometryChecks, + full_active_grid_verified:false,local_shell_transport_verified:false,presented_done:null}; + if(status==='failed'){ + result.failure_geometry=null; + const phase=Math.min(requested||observations.length,lastStage); + try{result.failure_geometry=geometrySample(phase,null,null);}catch(_){} + // Independent window evidence survives a failing terminal text binding. + if(result.failure_geometry){ + try{ + const peer=peerState(),scalar=value=>Number.isInteger(value)?value:null; + if(peer){result.failure_geometry.observed_grid=[scalar(peer.columns),scalar(peer.rows)]; + result.failure_geometry.event_count=Array.isArray(peer.naws_history)?peer.naws_history.length:null;} + }catch(_){} + try{const terminal=currentTerminal();result.failure_geometry.terminal_capacity=terminal&&Number.isInteger(terminal.capacity)?terminal.capacity:null;}catch(_){} + } + } + write('resize-combined-result.json',JSON.stringify(result));send({kind:'resize_combined_result',...result}); +} +function resetGrid(){gridKey=null;gridSince=0;} +function stableGrid(peer,terminal){ + const history=peer.naws_history; + if(!Array.isArray(history)||history.length64)throw Error('NAWS history bound'); + for(let i=nawsSeen;i!Number.isInteger(n)||n<0||n>65535))throw Error('NAWS event bound'); + const value=JSON.stringify(event);if(lastNaws!==null&&lastNaws!==value)resetGrid();lastNaws=value; + } + nawsSeen=history.length; + const value=JSON.stringify([peer.columns,peer.rows,terminal.capacity]); + if(!peer.columns||peer.rows!==terminal.capacity||JSON.stringify([peer.columns,peer.rows])!==lastNaws){resetGrid();return false;} + if(value!==gridKey){gridKey=value;gridSince=Date.now();return false;} + return Date.now()-gridSince>=600; +} +function requestResize(phase){requested=phase;write('resize-request-'+phase,JSON.stringify({phase,window:windows[phase]}));} +send({kind:'probe_ready',station_isolated:true,scenario:'resize-combined-v1',case_id:caseId}); +Interceptor.attach(user.getExportByName('DispatchMessageW'),{onEnter(){ + if(busy||finished||Process.getCurrentThreadId()!==uiThread||Date.now()41000)throw Error('bounded scenario deadline'); + if(stage===0){if(!ready('ui'))return;resize(main,...windows[0]);stage=1;} + else if(stage===1){scan();expandAll(tree);focus(tree,7);key(tree,0x1000011);stage=2;} + else if(stage===2){key(tree,0x1000004);stage=3;} + else{ + const peer=peerState();if(!peer||!peer.connected){resetGrid();return;} + if(peer.error)throw Error('peer '+peer.error); + if(peer.complete){if(observations.length!==phaseCount)throw Error('incomplete resize-combined observations');finish('observed-fixed-resize-combined-states',null);return;} + const terminal=currentTerminal();if(!terminal||!terminal.connected||terminal.protocol!=='Telnet'){resetGrid();return;} + if(stage===3){ + if(!stableGrid(peer,terminal))return; + checkGeometry(0,peer,terminal);write('fixture-ready',JSON.stringify({grid:[peer.columns,peer.rows],event_count:peer.naws_history.length}));stage=4;return; + } + if(requested>committed&&peer.resize_armed===requested){ + if(resized!==requested){resize(main,...windows[requested]);resized=requested;resetGrid();return;} + if(!stableGrid(peer,terminal))return; + checkGeometry(requested,peer,terminal); + write('resize-ready-'+requested,JSON.stringify({phase:requested,grid:[peer.columns,peer.rows],event_count:peer.naws_history.length}));committed=requested;return; + } + if(peer.phase===null||observations.length>peer.phase)return; + if(peer.case_id!==caseId)throw Error('peer case differs'); + if(peer.phase_label!==contract.labels[peer.phase])throw Error('peer phase label differs'); + if(peer.phase!==lastPhase){lastPhase=peer.phase;stable=null;stableSince=Date.now();} + const record=peer.records[peer.phase];if(!record.cursor_reply)return; + if(terminal.capacity!==record.frozen_grid[1]||JSON.stringify(record.frozen_grid)!==JSON.stringify(grids[peer.phase]))throw Error('phase grid differs'); + if(geometryChecks.length===peer.phase)checkGeometry(peer.phase,peer,terminal); + const geometry=windowGeometry(),state=JSON.stringify([terminal,geometry]); + if(JSON.stringify(geometry.window_size)!==JSON.stringify(windows[peer.phase]))throw Error('phase window dimensions differ'); + if(state!==stable){stable=state;stableSince=Date.now();return;} + if(Date.now()-stableSince<400)return; + if(peer.phase===0){ + if(JSON.stringify(record.cursor_reply)!==JSON.stringify([19,7]))throw Error('initial fixture DSR differs'); + if(!Array.isArray(terminal.lines))throw Error('initial fixture text missing'); + for(const label of contract.marker_labels){ + const count=terminal.lines.reduce((total,line)=>total+line.split(label).length-1,0); + if(count!==1)throw Error('initial fixture sentinel missing or repeated '+label); + } + } + if(observations.length!==peer.phase||observations.length>=phaseCount)throw Error('resize-combined 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_combined_phase',...observation}); + write('ack-'+peer.phase,'observed'); + if(resizePhases.includes(peer.phase+1))requestResize(peer.phase+1); + } + due=Date.now()+75; + }catch(error){finish('failed',String(error));}finally{busy=false;} +}}); +setTimeout(()=>finish('failed','bounded observation timer'),42000); diff --git a/research/ui/resize_combined_reference.md b/research/ui/resize_combined_reference.md new file mode 100644 index 00000000..c83bf3bd --- /dev/null +++ b/research/ui/resize_combined_reference.md @@ -0,0 +1,55 @@ +# WindTerm 2.7.0:先缩宽、再缩高、分步恢复的独立观察 + +本工具调查宽度保留的正文在高度变化后是否仍存在,以及恢复高度、恢复宽度分别怎样影响正文、显示与协议光标。它使用新的 namespace、源码归档和单次计划,不修改已运行的 `resize_height_reference`、`resize_geometry_reference` 或 `resize_height_measured_reference`。 + +## 已知来源与本次未知 + +- [独立窗口几何](results/2026-11-02-resize-geometry-reference/README.md):执行源 `3d4c8c2d6222caf0fc024729d9e0b34c020a69cf`,严格公开件 SHA256 `f18a2320c073ed477f773b42076c9968c0c8af8530af19548d5695e66e8cf9b5`,只观察 `1360×749 → 1360×569` 的 `91×20 → 91×14`。 +- [纯高度正文观察](results/2026-11-02-resize-height-measured-reference/README.md):执行源 `50a560739e45d00ee31177d84b7c54a144f5273e`,严格公开件 SHA256 `32d3f6cf2d1c6746ff517b684ecd8c631c86ecc87874209ba5f831ea244730e0`,在 `91×20 → 91×14 → 91×20` 保留正文,DSR `19;7 → 14;7 → 14;7`,恢复高度后查询矩形保持缩高位置。 + +上述事实不证明 **1000×569 对应 61×14**,也不证明先缩宽后的高度行为。本工具将这组尺寸作为固定待检验预测,`geometry_basis.combined_grid_previously_observed=false`;运行时保存实际测量后再检查,不自动调整窗口、行数、splitter 或期限。 + +旧 10 行预测失败仍保留原源码和计划;旧 `width-then-height` 没有运行。本工具不是把旧失败预期改成 14 行后重跑。 + +## 固定五阶段合同 + +唯一 case:`width-height-retention`,一个独立新进程,最多一次原版运行。 + +| 阶段 | 请求/实际窗口必须为,px | 待核 NAWS,列×行 | 终端输入 | +| --- | --- | --- | --- | +| initial-body | 1360×749 | 91×20 | 固定 75 B 正文,再 DSR | +| width-shrunk-unmodified | 1000×749 | 61×20 | 仅 DSR | +| height-shrunk-unmodified | 1000×569 | 61×14 | 仅 DSR | +| height-restored-unmodified | 1000×749 | 61×20 | 仅 DSR | +| width-restored-unmodified | 1360×749 | 91×20 | 仅 DSR | + +各步只改变一个窗口轴。初始源与纯高度观察相同: + +```text +1b5b3f366c1b5b3f37681b5b721b5b306d1b5b324a1b5b323b314848544f501b5b363b3148484152445f410d0a484152445f421b5b31323b3930487576771b5b31393b31484c4f57454e44 +``` + +75 B 的 SHA256 为 `ff48cd41220a72e5da2350dc97c3c8c8c884d48cc4f7fa2328609bf8bc0cb09e`。预置 `HTOP`、CR/LF 分开的 `HARD_A`/`HARD_B`、从第 90 列开始跨过原 91 列边界的 `uvw`,以及把协议光标放在 `19;7` 的 `LOWEND`。初始阶段稳定后必须有七个 marker 各一次和 DSR `19;7`,否则停止。 + +后四阶段没有 CUP、CR/LF、补写、清屏或重绘。DSR、完整文档、marker 保留与恢复行为均只观察,不从纯高度样本或标准代填预期。DSR 的独立安全界限仍是行 1..200、列 1..500,不由当前 14 行 viewport 夹断。 + +## 保存、同步与停止 + +- public Qt 窗口 QRect 使用 inclusive LTRB;协议 DSR、NAWS、capacity、API 文档行/位置、ASCII 文本 offset、marker 像素矩形分别记录。每个稳定端点先保存实际窗口、NAWS、capacity 与事件数,再断言是否符合固定预测。 +- 失败结果额外保留 `failure_geometry`:请求和实际窗口、可取得的 NAWS/capacity/事件数。窗口、peer、terminal 独立读取;终端文本绑定失败不会删除已取得的窗口和 NAWS,未取得的字段为 null。该失败快照不进入成功严格出口,也不构成语义通过。 +- peer 只在已 armed 的单轴区间接受尺寸变化;每步末端 NAWS 与 capacity 稳定 600 ms 后释放该阶段 DSR,正文和窗口稳定 400 ms 后才确认观察。前一阶段 ack 是下一步的前置,不跳过阶段。 +- 维度预测不符、改变未授权轴、越界 NAWS、陈旧事件、缺少阶段/ack、初始 fixture 无效、来源改变或固定期限到达都停止;不在 run 内校准、重试或延长。保留 41/42/43/45/60 s 各层界限,全生命周期界限仍为 null。 +- 后续完整文档仅允许固定 ASCII 源生成的有限目录,包含 61/91 列的确定切分与空白 padding;公开件逐行记录 UTF-8 hex、目录索引与完整数组 SHA。目录外文本拒绝,未知内容保留为失败,不扩大旧目录取得通过。 +- 后续 marker 丢失可保留为观察,但不作为语义成功。严格结果固定 `status=observation`、`semantic_acceptance=false`;不预判高度变化是否应截尾、重排、移入 history 或回拉。 + +## 身份与离线验证 + +新执行源码固定为 10 个文件;新出口固定记录 3 个源文件。manifest、源码归档、计划、原版包/模块、合成 profile、station/Job 与单次 `.attempted` 守卫沿用已验证机制,旧源码字节不变。源码提交和打包由执行者统一完成,之后才建立一次新计划。 + +```powershell +python -B -X utf8 -m unittest research.ui.test_resize_combined_reference research.ui.test_export_resize_combined_reference -v +python -B -O -X utf8 -m unittest research.ui.test_resize_combined_reference research.ui.test_export_resize_combined_reference -v +node research/ui/test_resize_combined_stages.js research/ui/resize_combined_reference.js +``` + +离线 Python 执行内存连接中的真实 peer,Node 执行实际 controller;不会启动原版、socket、GUI 或 native 构建。离线通过只说明守卫与采样流程可测,不算原版运行或核心实现验收。本样本不包含备用帧、编辑、Unicode、历史淘汰、ConPTY、实际 glyph 呈现、性能或完整 R2。 diff --git a/research/ui/resize_combined_reference.py b/research/ui/resize_combined_reference.py new file mode 100644 index 00000000..8476b68e --- /dev/null +++ b/research/ui/resize_combined_reference.py @@ -0,0 +1,381 @@ +"""Independent fixed width/height observations; unknown semantics remain observations.""" +from __future__ import annotations +import argparse +import hashlib +import json +from functools import lru_cache +from pathlib import Path +import sys + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +from research.ui import mode_state_reference as common +from research.ui import resize_state_reference as baseline + +require, sha = common.require, common.sha +qrect_size = baseline.qrect_size +EXE_SHA256, MANIFEST_SHA256 = common.EXE_SHA256, common.MANIFEST_SHA256 +RESET = b"\x1b[?6l\x1b[?7h\x1b[r\x1b[0m\x1b[2J" +BODY = (RESET + b"\x1b[2;1HHTOP\x1b[6;1HHARD_A\r\nHARD_B" + b"\x1b[12;90Huvw\x1b[19;1HLOWEND") +MARKERS = ("HTOP", "HARD_A", "HARD_B", "u", "v", "w", "LOWEND") +CASES = { + "width-height-retention": (("initial-body", 91, 20, BODY), + ("width-shrunk-unmodified", 61, 20, b""), + ("height-shrunk-unmodified", 61, 14, b""), + ("height-restored-unmodified", 61, 20, b""), + ("width-restored-unmodified", 91, 20, b"")), +} +RESULT_FILE = "resize-combined-result.json" +ATTEMPT = "One original resize-combined launch attempt; preserve failures and never retry this plan.\n" +SOURCE_FILES = ("research/ui/resize_combined_reference.py", "research/ui/resize_combined_reference.js", + "research/ui/resize_state_reference.py", "research/ui/mode_state_reference.py", + "research/ui/alternate_reference.py", "research/ui/r1_launch.py", "research/ui/launch.py", + "research/ui/qt_r1_text.js", "research/ui/r1_vt_fixture.py", "experiments/conpty/run.py") + + +def validate_case(case): + require(type(case) is str and case in CASES, "Unknown fixed resize-combined case") + return case + + +def windows(case): + validate_case(case) + return [(1360 if columns == 91 else 1000, 749 if rows == 20 else 569) + for _, columns, rows, _ in CASES[case]] + + +def required_grids(case): + validate_case(case) + return [(columns, rows) for _, columns, rows, _ in CASES[case]] + + +def resize_phases(case): + values = required_grids(case) + return tuple(index for index in range(1, len(values)) if values[index] != values[index-1]) + + +def controller_contract(case): + validate_case(case) + return {"case_id": case, "labels": [label for label, _, _, _ in CASES[case]], + "windows": [list(v) for v in windows(case)], "grids": [list(v) for v in required_grids(case)], + "resize_phases": list(resize_phases(case)), "marker_labels": list(MARKERS), + "initial_dsr": [19, 7]} + + +def phases(case, rows=20, columns=91): + validate_case(case) + require(type(rows) is int and type(columns) is int and [columns, rows] == [91, 20], "Fixed initial grid differs") + return [(label, value) for label, _, _, value in CASES[case]] + + +def reply_observation(data): + result = common.reply_observation(data) + if result is not None: + row, column = result["cursor_reply"] + require(1 <= row <= 200 and 1 <= column <= 500, "DSR exceeds independent safety bounds") + return result + + +def phase_records(case, rows=20, columns=91): + return [{"phase": index, "label": label, "source_hex": value.hex(), "source_bytes": len(value), + "source_sha256": hashlib.sha256(value).hexdigest(), "dsr_suffix_hex": "1b5b366e"} + for index, (label, value) in enumerate(phases(case, rows, columns))] + + +def allowed_grid(before, after, value): + if not all(type(v) in (list, tuple) and len(v) == 2 and + all(type(n) is int and 1 <= n <= 65535 for n in v) for v in (before, after, value)): + return False + changed = [axis for axis in (0, 1) if before[axis] != after[axis]] + return len(changed) == 1 and all( + min(before[axis], after[axis]) <= value[axis] <= max(before[axis], after[axis]) + if axis == changed[0] else value[axis] == before[axis] for axis in (0, 1)) + + +def peer_class(case): + validate_case(case) + base = baseline.peer_class() + # baseline creates a fresh module namespace for each class, not a shared peer. + base._serve.__globals__["phases"] = lambda rows, columns: phases(case, rows, columns) + base._serve.__globals__["reply_observation"] = reply_observation + + class CombinedPeer(base): + def _freeze_initial(self): + super()._freeze_initial() + require(self.state["frozen_grid"] == [91, 20], "Fixed initial grid differs") + self.state["case_id"] = case + + def _on_naws(self): + grid = [self.state["columns"], self.state["rows"]] + history = self.state.setdefault("naws_history", []) + require(len(history) < 64 and all(type(n) is int and 0 <= n <= 65535 for n in grid), "NAWS event bound") + history.append(grid) + self._record() + if self.state.get("frozen_grid"): + target = self.state.get("resize_armed") + if target is None: + require(grid == self.state["frozen_grid"], "NAWS changed outside an armed resize") + else: + require(target in resize_phases(case) and + allowed_grid(self.state["frozen_grid"], required_grids(case)[target], grid), + "NAWS changed an unarmed axis or exceeded the fixed interval") + + def _phase_ready(self, index): + if index == -1: + return True + ack = self.directory / f"ack-{index}" + if not ack.exists() or self.state["records"][index]["cursor_reply"] is None: + return False + require(ack.read_text(encoding="utf-8") == "observed", "UI acknowledgement differs") + if index == 0: + require(self.state["records"][0]["cursor_reply"] == [19, 7], "Initial fixture DSR differs") + if index == len(phases(case))-1: + return True + target = index + 1 + if target not in resize_phases(case): + require(self.state["resize_armed"] is None and self.state["frozen_grid"] == list(required_grids(case)[target]), + "Action was not released from its fixed per-stage grid") + self.state["frozen_grids"].append(list(self.state["frozen_grid"])) + self.state["freeze_event_counts"].append(len(self.state["naws_history"])) + return True + request = self.directory / f"resize-request-{target}" + if not request.exists(): + return False + require(json.loads(request.read_text(encoding="utf-8")) == {"phase": target, "window": list(windows(case)[target])}, + "Resize request differs from immutable window sequence") + if self.state["resize_armed"] is None: + self.state["resize_armed"] = target + self.state["resize_boundaries"].append({"phase": target, "before_event_count": len(self.state["naws_history"]), + "requested_window": list(windows(case)[target])}) + self._record() + return False + require(self.state["resize_armed"] == target, "Resize phase differs") + ready = self.directory / f"resize-ready-{target}" + if not ready.exists(): + return False + grid, history = [self.state["columns"], self.state["rows"]], self.state["naws_history"] + require(json.loads(ready.read_text(encoding="utf-8")) == + {"phase": target, "grid": grid, "event_count": len(history)}, "Resize ready snapshot is stale") + boundary = self.state["resize_boundaries"][-1] + require(len(history) > boundary["before_event_count"] and history[-1] == grid == list(required_grids(case)[target]), + "Fixed resized grid or NAWS boundary differs") + boundary["after_event_count"] = len(history) + self.state.update(frozen_grid=grid, resize_armed=None) + self.state["frozen_grids"].append(grid) + self.state["freeze_event_counts"].append(len(history)) + return True + return CombinedPeer + + +def source_identity(): + return common.source_identity(root=ROOT, source_files=SOURCE_FILES) + + +def probe_fragment(case): + validate_case(case) + source = (ROOT / "research/ui/resize_combined_reference.js").read_text(encoding="utf-8") + require(source.count("__RESIZE_COMBINED_CASE_JSON__") == 1, "Protected case template changed") + return source.replace("__RESIZE_COMBINED_CASE_JSON__", json.dumps(controller_contract(case))) + + +def launcher_source(case): + validate_case(case) + source = (ROOT / "research/ui/r1_launch.py").read_text(encoding="utf-8") + replacements = { + "from research.ui.r1_vt_fixture import Peer": + "from research.ui.resize_combined_reference import peer_class, probe_fragment\n Peer = peer_class("+repr(case)+")", + 'Path(__file__).with_name("r1_menu_wrap.js").read_text(encoding="utf-8")': "probe_fragment("+repr(case)+")", + } + for old, new in replacements.items(): + require(source.count(old) == 1, "Protected launcher source changed") + source = source.replace(old, new) + return source + + +def fixed_contract(case): + validate_case(case) + return {"schema": "termforge-resize-combined-plan-v1", "issue": 29, "related_issue": 27, "case_id": case, + "maximum_original_runs": 1, "actual_original_runs": 0, "phase_count": len(phases(case)), + "windows": [list(v) for v in windows(case)], "required_grids": [list(v) for v in required_grids(case)], + "resize_phases": list(resize_phases(case)), "window_rect_format": "qt-qrect-inclusive-ltrb", + "rea_capture_seconds": 45, "cli_wait_timeout_seconds": 60, "whole_lifecycle_bound_seconds": None, + "phases": phase_records(case), "reference_exe_sha256": EXE_SHA256, + "reference_files_manifest_sha256": MANIFEST_SHA256, "semantic_acceptance": False, + "controller_contract": controller_contract(case), "execution_group": "one-combined-resize-case", + "independent_dsr_bounds": {"row": [1, 200], "column": [1, 500]}, + "initial_fixture_dsr": [19, 7], "initial_fixture_markers": list(MARKERS), + "geometry_basis": { + "geometry": {"reference": "research/ui/results/2026-11-02-resize-geometry-reference/strict-export.json", + "strict_export_sha256": "f18a2320c073ed477f773b42076c9968c0c8af8530af19548d5695e66e8cf9b5", + "execution_source_base_commit": "3d4c8c2d6222caf0fc024729d9e0b34c020a69cf"}, + "pure_height": {"reference": "research/ui/results/2026-11-02-resize-height-measured-reference/strict-export.json", + "strict_export_sha256": "32d3f6cf2d1c6746ff517b684ecd8c631c86ecc87874209ba5f831ea244730e0", + "execution_source_base_commit": "50a560739e45d00ee31177d84b7c54a144f5273e"}, + "combined_grid_previously_observed": False}, + "combined_geometry_status": "61x14 at1000x569 and the full sequence are fixed predictions checked in this new run; save actual geometry before assertion, without calibration or retries", + "post_initial_terminal_input": "DSR only; no CUP, redraw, CR/LF or replacement text", + "scope": "Fresh direct Telnet process per case; document/API positions, DSR and pixel rectangles remain separate observations", + "stop": "Wrong fixed geometry/grid, unarmed or wrong-axis NAWS, stale snapshots, initial fixture/source/reply/phase mismatch, bounds or deadline failure; no retries"} + + +def bounded_fragments(lines, split): + result = set(lines) + for line in lines: + result.add(line.rstrip(" ")) + for width in ((61, 91) if split else (91,)): + if len(line) <= width: + result.add(line.ljust(width)) + for edge in ((61, 91) if split else (91,)): + if len(line) > edge: + result.update((line[:edge], line[edge:], line[:edge].rstrip(" "), line[edge:].rstrip(" "))) + return result + + +@lru_cache(maxsize=16) +def synthetic_lines(case, phase): + """Finite ASCII source-derived words; loss is observation, not acceptance.""" + validate_case(case) + require(type(phase) is int and 0 <= phase < len(phases(case)), "Phase bound") + base = {"", "HTOP", "HARD_A", "HARD_B", "LOWEND", " "*89+"uvw"} + return frozenset(bounded_fragments(base, phase > 0)) + + +def validate_document(case, phase, terminal): + require(type(terminal.get("line_count")) is int and 0 <= terminal["line_count"] <= 200 and + type(terminal.get("lines")) is list and len(terminal["lines"]) == terminal["line_count"], "Document bound") + lines = terminal["lines"] + require(all(type(line) is str and line.isascii() and line in synthetic_lines(case, phase) for line in lines), + "Only fixed combined-source synthetic document lines are allowed") + counts = {marker: sum(line.count(marker) for line in lines) for marker in MARKERS} + require(all(count <= 1 for count in counts.values()), "Repeated combined-source marker") + if phase == 0: + require(all(count == 1 for count in counts.values()), "Initial fixture sentinel missing") + + +def audit_mechanics(directory, case): + validate_case(case) + count = len(phases(case)) + expected_grids, expected_windows = [list(v) for v in required_grids(case)], [list(v) for v in windows(case)] + read = lambda name: json.loads((directory / name).read_text(encoding="utf-8")) + require((directory / "stderr.txt").read_bytes() == b"" and not (directory / "launcher-failure.json").exists(), + "REA stderr/launcher failure retained") + peer, result, cleanup, profile = [read(name) for name in ("peer.json", RESULT_FILE, "cleanup.json", "profile-check.json")] + require(all(peer.get(k) is True for k in ("connected", "closed", "complete")) and not peer.get("error") and peer.get("case_id") == case, + "Combined peer did not complete cleanly") + require(result.get("status") == "observed-fixed-resize-combined-states" and result.get("case_id") == case and result.get("error") is None and + result.get("window_rect_format") == "qt-qrect-inclusive-ltrb" and result.get("presented_done") is None and + result.get("full_active_grid_verified") is False and result.get("local_shell_transport_verified") is False and + "failure_geometry" not in result, + "Combined UI observation incomplete") + require(type(cleanup.get("active")) is int and cleanup["active"] == 0 and + all(type(cleanup.get(k)) is int and cleanup[k] >= 0 for k in ("total", "terminated")) and + profile == {"only_synthetic_session": True, "session_count": 1}, "Owned cleanup/profile differs") + grids, freezes, history = peer["frozen_grids"], peer["freeze_event_counts"], peer["naws_history"] + require(grids == expected_grids and len(freezes) == count and type(history) is list and 1 <= len(history) <= 64 and + all(type(grid) is list and len(grid) == 2 and all(type(n) is int and 0 <= n <= 65535 for n in grid) for grid in history) and + all(type(n) is int and 1 <= n <= len(history) for n in freezes), "Fixed grids/freeze/NAWS bounds differ") + require(len(peer["resize_boundaries"]) == len(resize_phases(case)) and freezes == sorted(freezes), "Resize boundary count differs") + for index, boundary in zip(resize_phases(case), peer["resize_boundaries"]): + before, after = boundary["before_event_count"], boundary["after_event_count"] + require(type(before) is int and type(after) is int and boundary["phase"] == index and + boundary["requested_window"] == expected_windows[index] and freezes[index-1] <= before < after == freezes[index] <= len(history) and + all(grid == grids[index-1] for grid in history[freezes[index-1]-1:before]) and + all(allowed_grid(grids[index-1], grids[index], grid) for grid in history[before:after]) and history[after-1] == grids[index], "NAWS resize boundary differs") + for index in range(1, count): + if index not in resize_phases(case): + require(grids[index] == grids[index-1] and + all(grid == grids[index] for grid in history[freezes[index-1]-1:freezes[index]]), + "Unarmed per-stage NAWS change retained") + require(all(grid == grids[-1] for grid in history[freezes[-1]-1:]), "Unarmed final NAWS change retained") + records, observations, checks = peer["records"], result["observations"], result["geometry_checks"] + require(len(records) == len(observations) == len(checks) == count and peer["phase"] == count-1 and peer["phase_count"] == count, + "Fixed combined phases incomplete") + replies = bytearray() + for index, (expected, actual, observed, check) in enumerate(zip(phase_records(case), records, observations, checks)): + grid, window = expected_grids[index], expected_windows[index] + require(all(actual.get(k) == v for k, v in expected.items()) and actual["frozen_grid"] == grid, "Exact action source differs") + raw = bytes.fromhex(actual["response_hex"]); reply = reply_observation(raw) + if index == 0: + require(reply is not None and reply["cursor_reply"] == [19, 7], "Initial fixture DSR differs") + require(reply is not None and all(actual.get(k) == v for k, v in reply.items()), "DSR differs") + require(all(observed.get(k) == actual[k] for k in ("phase", "label", "frozen_grid", *reply)), "Peer/UI phase differs") + require(check.get("phase") == index and check.get("expected_size") == window and check.get("window_size") == window and + baseline.qrect_size(check.get("window_rect")) == window and observed.get("window_size") == window and + baseline.qrect_size(observed.get("window_rect")) == window, "Exact geometry differs") + terminal = observed["terminal"] + require(check.get("observed_grid") == grid and type(check.get("terminal_capacity")) is int and + check["terminal_capacity"] == grid[1] and type(check.get("event_count")) is int and + check["event_count"] == freezes[index], "Saved actual geometry differs") + require(terminal.get("connected") is True and terminal.get("protocol") == "Telnet" and + type(terminal.get("capacity")) is int and terminal["capacity"] == grid[1], "UI binding differs") + 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-combined-mechanics-v1", "case_id": case, "stage_count": count, "dsr_count": count, + "stateful_observations_preserved": True, "frozen_grids": grids, "naws_events": len(history), "job_active": 0, + "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 combined contract differs") + require(all(plan.get(k) == v for k, v in source_identity().items()), "Combined 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(), "Combined 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_combined_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()), "Combined source identity changed during run") + mechanics = audit_mechanics(out, case) + (out / "mechanics-check.json").write_text(json.dumps(mechanics, indent=2)+"\n", encoding="utf-8", newline="\n") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--plan", type=Path, required=True) + parser.add_argument("--prepare", action="store_true") + parser.add_argument("--case", choices=list(CASES)) + parser.add_argument("--authorize-execution", action="store_true") + for name in ("source", "out", "rea"): + parser.add_argument("--"+name, type=Path) + args = parser.parse_args() + if args.prepare: + require(args.source and args.out and args.rea and args.case, "Source/output/REA/case required") + prepare_plan(args.source, args.out, args.rea, args.plan, args.case, args.authorize_execution) + print(json.dumps({"prepared": True, "case_id": args.case, "phase_count": len(phases(args.case)), "execution_authorized": args.authorize_execution})) + else: + require(not args.authorize_execution and args.case is None and args.source is None and args.out is None and args.rea is None, + "Execution uses only the immutable plan") + run_plan(args.plan) diff --git a/research/ui/test_export_resize_combined_reference.py b/research/ui/test_export_resize_combined_reference.py new file mode 100644 index 00000000..154fd46c --- /dev/null +++ b/research/ui/test_export_resize_combined_reference.py @@ -0,0 +1,378 @@ +"""Synthetic five-stage resize publication guards; no target, sockets, REA or native UI.""" +from __future__ import annotations +import copy +from contextlib import contextmanager +import hashlib +import json +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest +from unittest.mock import patch + +from research.ui import export_resize_combined_reference as exporter +from research.ui import resize_combined_reference as probe +from research.ui import mode_state_reference as original_probe +from research.ui import export_mode_state_reference as original_exporter +from research.ui.test_resize_combined_reference import recording +from research.ui.test_export_mode_state_reference import fixture as mode_fixture, complete_fixture as mode_complete_fixture + + +def save(directory, data, synchronize=True): + if synchronize: + result = data[probe.RESULT_FILE] + payloads = [{"kind": "probe_ready", "station_isolated": True, "scenario": "resize-combined-v1", "case_id": result["case_id"]}, + {"kind": "readiness", "phase": "ui", "ready": True, "has_tree": True, + "has_engine": True, "has_editor": False, "peer_ready": False}] + payloads.extend({"kind": "resize_combined_phase", **copy.deepcopy(row)} for row in result["observations"]) + payloads.append({"kind": "resize_combined_result", **copy.deepcopy(result)}) + data["evidence.json"]["data"]["evidence"]["normalized_result"]["messages"] = [ + {"type": "send", "script_id": "synthetic-script", "payload": value} for value in payloads] + for name, value in data.items(): + (directory / name).write_text(json.dumps(value), encoding="utf-8") + + +def marker_rows(lines): + result, offset = [], 0 + for row, line in enumerate(lines): + for label in exporter.MARKERS: + column = line.find(label) + while column >= 0: + result.append({"label": label, "document_line": row, "column": column, "position": offset+column, "line_start_api": offset, + "rect": {"x": column*8, "y": row*18, "width": 8, "height": 18}}) + column = line.find(label, column+1) + offset += len(line)+1 + return result + + +def fixture(root, case="width-height-retention"): + directory, data, _ = mode_fixture(root) + data.pop("mode-state-result.json") + # A complete fixture contains only this protocol's declared acknowledgments. + for path in directory.glob("ack-*"): + path.unlink() + state, result = recording(case) + for observation in result["observations"]: + terminal = observation["terminal"] + terminal["markers"] = marker_rows(terminal["lines"]) + data["peer.json"], data[probe.RESULT_FILE] = state, result + plan = {**probe.fixed_contract(case), "execution_authorized": True, "out": str(directory), "source": str(root / "official")} + run = data["run.json"] + run.update(sha256=probe.EXE_SHA256, files_sha256=probe.MANIFEST_SHA256) + template = (probe.ROOT / "research/ui/qt_r1_text.js").read_text(encoding="utf-8").split("// Scenario driver", 1)[0] + template += probe.probe_fragment(case) + run["template_sha256"] = hashlib.sha256(template.encode()).hexdigest() + generated = exporter.expected_probe(plan, run, probe.ROOT) + (directory / "probe.js").write_bytes(generated) + run["script_sha256"] = hashlib.sha256(generated).hexdigest() + data["evidence.json"]["data"]["evidence"]["normalized_result"]["source_sha256"] = run["script_sha256"] + (directory / "fixture-ready").write_text(json.dumps({"grid": state["frozen_grids"][0], "event_count": state["freeze_event_counts"][0]})) + for index in range(len(probe.phases(case))): + (directory / f"ack-{index}").write_text("observed") + for index in probe.resize_phases(case): + (directory / f"resize-request-{index}").write_text(json.dumps({"phase": index, "window": list(probe.windows(case)[index])})) + (directory / f"resize-ready-{index}").write_text(json.dumps({"phase": index, + "grid": state["frozen_grids"][index], "event_count": state["freeze_event_counts"][index]})) + save(directory, data) + (directory / "mechanics-check.json").write_text(json.dumps(probe.audit_mechanics(directory, case))) + return directory, data, plan + + +@contextmanager +def complete_fixture(root, case="width-height-retention"): + with mode_complete_fixture(root) as original: + old_directory, _, old_plan, _, source_root, official = original + with patch.object(probe, "EXE_SHA256", original_probe.EXE_SHA256), \ + patch.object(probe, "MANIFEST_SHA256", original_probe.MANIFEST_SHA256), \ + patch.dict(exporter._PROVENANCE, FILE_COUNT=original_exporter.FILE_COUNT): + new_root = root / "combined" + new_root.mkdir() + directory, data, case_plan = fixture(new_root, case) + plan = {**old_plan, **case_plan, "source": str(official)} + (directory / "app").mkdir() + for name in original_exporter.MODULES: + (directory / "app" / name).write_bytes((official / name).read_bytes()) + (directory / "files.json").write_bytes((old_directory / "files.json").read_bytes()) + profile = "profile/.wind/profiles/default.v10/terminal/user.sessions" + (directory / profile).parent.mkdir(parents=True) + (directory / profile).write_bytes((old_directory / profile).read_bytes()) + for name in probe.SOURCE_FILES: + (source_root / name).parent.mkdir(parents=True, exist_ok=True) + (source_root / name).write_bytes((probe.ROOT / name).read_bytes()) + archive = {"schema": probe.common.ARCHIVE_SCHEMA, "source_base_commit": "b"*40, + "source_files_sha256": {name: probe.sha(source_root / name) for name in probe.SOURCE_FILES}} + (source_root / probe.common.ARCHIVE_MANIFEST).write_text(json.dumps(archive), encoding="utf-8") + plan.update(probe.common.source_identity(root=source_root, source_files=probe.SOURCE_FILES)) + plan["launcher_source_sha256"] = hashlib.sha256(probe.launcher_source(case).encode()).hexdigest() + plan_path = new_root / "plan.json" + plan_path.write_text(json.dumps(plan), encoding="utf-8") + plan_path.with_suffix(".attempted").write_text(probe.ATTEMPT) + yield directory, data, plan, plan_path, source_root, official + + +class ResizeCombinedExportTests(unittest.TestCase): + def test_fixed_five_stage_contract_and_literal_input_are_independent_of_prior_height_runs(self): + with tempfile.TemporaryDirectory() as temporary: + directory, _, plan = fixture(Path(temporary).resolve()) + result = exporter.capture_facts(directory, plan) + source = (b"\x1b[?6l\x1b[?7h\x1b[r\x1b[0m\x1b[2J\x1b[2;1HHTOP\x1b[6;1HHARD_A\r\nHARD_B" + b"\x1b[12;90Huvw\x1b[19;1HLOWEND") + self.assertEqual(len(source), 75) + self.assertEqual(result["stateful_observation_count"], 5) + self.assertEqual([value["frozen_grid"] for value in result["phases"]], [[91,20],[61,20],[61,14],[61,20],[91,20]]) + self.assertEqual([value["window_size"] for value in result["phases"]], [[1360,749],[1000,749],[1000,569],[1000,749],[1360,749]]) + self.assertEqual([value["source_hex"] for value in result["phases"]], [source.hex(), "", "", "", ""]) + self.assertEqual([value["source_sha256"] for value in result["phases"]], + [hashlib.sha256(value).hexdigest() for value in (source,b"",b"",b"",b"")]) + self.assertEqual(result["source_bytes"], 75) + self.assertEqual(result["dsr_request_bytes"], 20) + self.assertEqual([phase["stage_role"] for phase in result["phases"]], [ + "initial-body", "width-shrunk-unmodified", "height-shrunk-unmodified", + "height-restored-unmodified", "width-restored-unmodified"]) + self.assertEqual([item["observed_grid"] for item in result["geometry_checks"]], + [[91,20],[61,20],[61,14],[61,20],[91,20]]) + self.assertEqual([item["terminal_capacity"] for item in result["geometry_checks"]], [20,20,14,20,20]) + self.assertEqual([item["event_count"] for item in result["geometry_checks"]], result["freeze_event_counts"]) + + def test_later_DSR_above_fourteen_rows_is_observed_without_clamping(self): + with tempfile.TemporaryDirectory() as temporary: + directory, data, plan = fixture(Path(temporary).resolve()) + peer = data["peer.json"] + reply = b"\x1b[200;500R" + for record in (peer["records"][2], data[probe.RESULT_FILE]["observations"][2]): + record.update(cursor_reply=[200,500], cursor_reply_private=False, response_hex=reply.hex()) + peer["received_hex"] = "".join(record["response_hex"] for record in peer["records"]) + save(directory,data) + result = exporter.capture_facts(directory,plan) + self.assertEqual(result["phases"][2]["cursor_reply"],[200,500]) + self.assertEqual(result["phases"][2]["document_projection"]["capacity"],14) + + def test_old_case_names_and_ten_row_endpoint_are_not_accepted(self): + for changed in ("height-only", "height-retention-measured", "width-then-height", "ten-row"): + with self.subTest(changed=changed), tempfile.TemporaryDirectory() as temporary: + directory, data, plan = fixture(Path(temporary).resolve()) + if changed == "ten-row": + data["peer.json"]["frozen_grids"][2] = [61,10] + data[probe.RESULT_FILE]["observations"][2]["terminal"]["capacity"] = 10 + else: + plan["case_id"] = changed + save(directory,data) + with self.assertRaises(ValueError): + exporter.capture_facts(directory,plan) + + def test_fixed_case_reconstructs_complete_ASCII_documents_and_exact_sources(self): + for case in probe.CASES: + with self.subTest(case=case), tempfile.TemporaryDirectory() as temporary: + directory, data, plan = fixture(Path(temporary).resolve(), case) + result = exporter.capture_facts(directory, plan) + self.assertEqual(result["stateful_observation_count"], len(probe.phases(case))) + self.assertEqual([p["source_hex"] for p in result["phases"]], [v.hex() for _,v in probe.phases(case)]) + self.assertEqual([p["phase"] for p in result["resize_boundaries"]], list(probe.resize_phases(case))) + self.assertEqual(result["post_initial_terminal_input"], "DSR only") + self.assertEqual(result["independent_dsr_bounds"], {"row":[1,200],"column":[1,500]}) + self.assertIsNone(result["text_units"]["cpp_cell_column_mapping"]) + self.assertIsNone(result["text_units"]["document_position_unit_classification"]) + for index, phase in enumerate(result["phases"]): + actual = data[probe.RESULT_FILE]["observations"][index]["terminal"]["lines"] + public = phase["document_projection"]["synthetic_text"] + catalog = sorted(probe.synthetic_lines(case,index)) + self.assertEqual(public["catalog_sha256"], hashlib.sha256(json.dumps(catalog,separators=(',',':')).encode()).hexdigest()) + rebuilt = [bytes.fromhex(row["utf8_hex"]).decode('ascii') for row in public["lines"]] + self.assertEqual(rebuilt, actual) + for row,line in zip(public["lines"],rebuilt): + self.assertEqual(catalog[row["catalog_index"]],line) + self.assertEqual([row["utf8_bytes"],row["unicode_scalars"],row["utf16_code_units"]],[len(line)]*3) + self.assertEqual(phase["document_projection"]["synthetic_lines_sha256"],hashlib.sha256(json.dumps(rebuilt,separators=(',',':')).encode()).hexdigest()) + self.assertEqual(phase["cursor_reply"],[19,7]) + for marker in phase["document_projection"]["markers"]: + self.assertEqual(marker["line_text_offset_scalar"],marker["line_text_offset_utf16"]) + self.assertEqual(marker["line_text_prefix_utf8_bytes"],marker["line_text_offset_utf16"]) + self.assertEqual(marker["requested_position_api_value"],marker["line_start_api_value"]+marker["line_text_offset_utf16"]) + self.assertNotIn("column",marker) + self.assertNotIn(str(directory),json.dumps(result)) + + def test_later_missing_markers_and_split_text_are_observations(self): + for case, phase, lines in (("width-height-retention",1,[""]), ("width-height-retention",2,["LOWEND"]), + ("width-height-retention",3,[" "*89+"uv","w"]), ("width-height-retention",4,["HARD_A","HARD_B"])): + with self.subTest(case=case,phase=phase), tempfile.TemporaryDirectory() as temporary: + directory,data,plan = fixture(Path(temporary).resolve(),case) + terminal=data[probe.RESULT_FILE]["observations"][phase]["terminal"] + terminal.update(lines=lines,line_count=len(lines),markers=marker_rows(lines));save(directory,data) + result=exporter.capture_facts(directory,plan) + self.assertEqual(result["phases"][phase]["document_projection"]["line_count"],len(lines)) + self.assertEqual(result["phases"][phase]["cursor_reply"],[19,7]) + + def test_missing_initial_fixture_private_text_nonASCII_and_duplicates_fail(self): + for phase,lines in ((0,[""]),(1,["SECRET"]),(1,["é"]),(1,["HTOP","HTOP"]), + (1,[" "*92]),(1,["LOWENDQ"]),(1,["uvwuvw"])): + with self.subTest(phase=phase,lines=lines), tempfile.TemporaryDirectory() as temporary: + directory,data,plan=fixture(Path(temporary).resolve()) + terminal=data[probe.RESULT_FILE]["observations"][phase]["terminal"] + terminal.update(lines=lines,line_count=len(lines),markers=marker_rows(lines));save(directory,data) + with self.assertRaises(ValueError):exporter.capture_facts(directory,plan) + + def test_sampled_geometry_cannot_be_missing_stale_or_disagree_with_frozen_stage(self): + for changed in ("missing", "grid", "capacity", "event", "bool-grid", "failed-result"): + with self.subTest(changed=changed), tempfile.TemporaryDirectory() as temporary: + directory, data, plan = fixture(Path(temporary).resolve()) + result = data[probe.RESULT_FILE] + measured = result["geometry_checks"][2] + if changed == "missing": measured.pop("observed_grid") + elif changed == "grid": measured["observed_grid"] = [91,14] + elif changed == "capacity": measured["terminal_capacity"] = 20 + elif changed == "event": measured["event_count"] = 1 + elif changed == "bool-grid": measured["observed_grid"][0] = True + else: + result["error"] = "Geometry prediction differed" + result["failure_geometry"] = {"window_rect": [0,0,999,568], "observed_grid": [61,13]} + save(directory, data) + with self.assertRaises((ValueError, KeyError)): exporter.capture_facts(directory, plan) + + def test_marker_API_arithmetic_and_bounds_are_not_guessed(self): + for kind in ("missing-start","bad-start","bad-sum","wide-overflow","negative-height","position-bound","label"): + with self.subTest(kind=kind), tempfile.TemporaryDirectory() as temporary: + directory,data,plan=fixture(Path(temporary).resolve()) + marker=data[probe.RESULT_FILE]["observations"][1]["terminal"]["markers"][0] + if kind=="missing-start":marker.pop("line_start_api") + elif kind=="bad-start":marker["line_start_api"]="SECRET" + elif kind=="bad-sum":marker["line_start_api"]+=1 + elif kind=="wide-overflow":marker["rect"]["width"]=1361 + elif kind=="negative-height":marker["rect"]["height"]=-1 + elif kind=="position-bound":marker["position"]=262145 + else:marker["label"]="SECRET" + save(directory,data) + with self.assertRaises((ValueError,KeyError)):exporter.capture_facts(directory,plan) + + def test_source_geometry_capacity_phase_DSR_and_NAWS_tampering_fail(self): + for changed in ("input","case","missing-stage","ack","ready","reply","unarmed","wrong-axis","outside-interval", + "window","capacity","initial-DSR","DSR-overflow"): + with self.subTest(changed=changed), tempfile.TemporaryDirectory() as temporary: + directory,data,plan=fixture(Path(temporary).resolve()) + result,peer=data[probe.RESULT_FILE],data["peer.json"] + if changed=="input":peer["records"][1]["source_hex"]="0d51" + elif changed=="case":result["case_id"]="width-then-height" + elif changed=="missing-stage":result["observations"].pop(1) + elif changed=="ack":(directory/"ack-1").write_text("not observed") + elif changed=="ready":(directory/"resize-ready-1").write_text('{}') + elif changed=="reply":peer["received_hex"]+="1b5b313b3152" + elif changed=="unarmed":peer["naws_history"].append([90,20]) + elif changed in ("wrong-axis","outside-interval"): + index=peer["resize_boundaries"][0]["before_event_count"] + peer["naws_history"][index]=[90,15] if changed=="wrong-axis" else [91,9] + elif changed=="window":result["observations"][1]["window_rect"][3]+=1 + elif changed=="capacity":result["observations"][1]["terminal"]["capacity"]=14 + else:peer["records"][0 if changed=="initial-DSR" else 1]["response_hex"]="1b5b31383b3752" if changed=="initial-DSR" else "1b5b3230313b3752" + save(directory,data) + with self.assertRaises(ValueError):exporter.capture_facts(directory,plan) + + def test_provider_source_identity_partial_results_and_cleanup_fail(self): + for changed in ("exe","manifest","template","script","evidence","truncated","cleanup","source-path","provider-observations","provider-ready"): + with self.subTest(changed=changed), tempfile.TemporaryDirectory() as temporary: + directory,data,plan=fixture(Path(temporary).resolve()) + evidence=data["evidence.json"]["data"]["evidence"] + if changed=="exe":data["run.json"]["sha256"]="0"*64 + elif changed=="manifest":data["run.json"]["files_sha256"]="0"*64 + elif changed=="template":data["run.json"]["template_sha256"]="0"*64 + elif changed=="script":(directory/"probe.js").write_bytes(b"changed") + elif changed=="evidence":evidence["evidence_id"]="SECRET" + elif changed=="truncated":evidence["normalized_result"]["messages_truncated"]=True + elif changed=="cleanup":data["cleanup.json"]["active"]=1 + elif changed=="source-path":evidence["normalized_result"]["source_path"]="C:/private/other.js" + elif changed=="provider-observations":evidence["normalized_result"]["messages"].pop(3) + else:evidence["normalized_result"]["messages"][0]["payload"]["case_id"]="wrong" + save(directory,data,synchronize=not changed.startswith("provider-")) + with self.assertRaises(ValueError):exporter.capture_facts(directory,plan) + + def test_unlisted_metadata_is_not_forwarded(self): + with tempfile.TemporaryDirectory() as temporary: + directory,data,plan=fixture(Path(temporary).resolve()) + before=exporter.capture_facts(directory,plan) + data[probe.RESULT_FILE]["observations"][1]["terminal"]["private_note"]="SECRET" + data["peer.json"]["resize_boundaries"][0]["private_path"]="C:/private" + data[probe.RESULT_FILE]["geometry_checks"][2]["private_note"]="SECRET" + save(directory,data) + self.assertEqual(exporter.capture_facts(directory,plan),before) + + def test_complete_ten_source_archive_and_package_identity(self): + for case in probe.CASES: + with self.subTest(case=case), tempfile.TemporaryDirectory() as temporary, complete_fixture(Path(temporary).resolve(), case) as values: + directory, _, _, plan_path, source_root, official = values + result = exporter.export(directory, plan_path, Path(temporary) / "public/result.json", + exporter.sha(plan_path), source_root, official) + self.assertEqual(result["schema"], "termforge-original-resize-combined-reference-v1") + self.assertEqual(result["case_id"], case) + self.assertEqual(set(result["provenance"]["execution_source_files_sha256"]), set(probe.SOURCE_FILES)) + self.assertEqual(set(result["offline_export_source_files_sha256"]), set(exporter.EXPORT_SOURCES)) + self.assertIsNone(result["combined_policy_classification"]) + self.assertFalse(result["semantic_acceptance"]) + self.assertEqual(result["status"],"observation") + self.assertEqual(result["geometry_basis"], { + "geometry": {"reference": "research/ui/results/2026-11-02-resize-geometry-reference/strict-export.json", + "strict_export_sha256": "f18a2320c073ed477f773b42076c9968c0c8af8530af19548d5695e66e8cf9b5", + "execution_source_base_commit": "3d4c8c2d6222caf0fc024729d9e0b34c020a69cf"}, + "pure_height": {"reference": "research/ui/results/2026-11-02-resize-height-measured-reference/strict-export.json", + "strict_export_sha256": "32d3f6cf2d1c6746ff517b684ecd8c631c86ecc87874209ba5f831ea244730e0", + "execution_source_base_commit": "50a560739e45d00ee31177d84b7c54a144f5273e"}, + "combined_grid_previously_observed": False}) + + def test_geometry_basis_tampering_is_rejected_even_with_new_plan_hash(self): + for changed in ("geometry", "pure_height", "source-commit", "previously-observed"): + with self.subTest(changed=changed), tempfile.TemporaryDirectory() as temporary, complete_fixture(Path(temporary).resolve()) as values: + directory, _, plan, plan_path, source_root, official = values + if changed in ("geometry", "pure_height"): + plan["geometry_basis"][changed]["strict_export_sha256"] = "0"*64 + elif changed == "source-commit": + plan["geometry_basis"]["pure_height"]["execution_source_base_commit"] = "0"*40 + else: plan["geometry_basis"]["combined_grid_previously_observed"] = True + plan_path.write_text(json.dumps(plan), encoding="utf-8") + destination = Path(temporary)/"public/result.json" + with self.assertRaisesRegex(ValueError, "contract differs"): + exporter.export(directory,plan_path,destination,exporter.sha(plan_path),source_root,official) + self.assertFalse(destination.exists()) + + def test_changed_package_source_profile_attempt_plan_and_destination_are_rejected(self): + for changed in ("module", "source", "manifest", "profile", "attempt", "plan", "destination"): + with self.subTest(changed=changed), tempfile.TemporaryDirectory() as temporary, complete_fixture(Path(temporary).resolve()) as values: + directory, _, _, plan_path, source_root, official = values + expected = exporter.sha(plan_path) + destination = Path(temporary) / "public/result.json" + if changed == "module": (directory / "app/Qt5Core.dll").write_bytes(b"changed") + elif changed == "source": (source_root / "research/ui/resize_combined_reference.py").write_bytes(b"changed") + elif changed == "manifest": (directory / "files.json").write_text('{}') + elif changed == "profile": (directory / "profile/.wind/profiles/default.v10/terminal/user.sessions").write_text('[]') + elif changed == "attempt": plan_path.with_suffix('.attempted').write_text('not combined') + elif changed == "plan": plan_path.write_bytes(plan_path.read_bytes()+b' ') + elif changed == "destination": destination.parent.mkdir(); destination.write_text('preserved') + with self.assertRaises(ValueError): exporter.export(directory, plan_path, destination, expected, source_root, official) + if changed == "destination": self.assertEqual(destination.read_text(), 'preserved') + else: self.assertFalse(destination.parent.exists()) + + def test_duplicate_fields_and_nonfinite_metadata_fail_closed(self): + with tempfile.TemporaryDirectory() as temporary: + path = Path(temporary) / "record.json" + for content in ('{"phase":1,"phase":2}', '{"x":NaN}', '{"x":Infinity}', '{"x":-Infinity}'): + path.write_text(content) + with self.subTest(content=content), self.assertRaises(ValueError): exporter.read(path) + + def test_cli_refuses_private_text_normally_and_under_optimization(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary).resolve() + directory, data, plan = fixture(root) + data[probe.RESULT_FILE]["observations"][1]["terminal"]["lines"][1] = "SECRET" + save(directory, data) + plan_path = root / "plan.json" + plan_path.write_text(json.dumps(plan)) + for optimize in (False, True): + destination = root / ("optimized/out.json" if optimize else "normal/out.json") + command = [sys.executable, "-B", *(["-O"] if optimize else []), "-X", "utf8", "-m", + "research.ui.export_resize_combined_reference", "--directory", str(directory), "--plan", str(plan_path), + "--expected-plan-sha256", exporter.sha(plan_path), "--destination", str(destination)] + completed = subprocess.run(command, cwd=probe.ROOT, capture_output=True, text=True, encoding="utf-8", timeout=10) + self.assertNotEqual(completed.returncode, 0) + self.assertIn("Only fixed combined-source synthetic document lines", completed.stderr) + self.assertFalse(destination.parent.exists()) + + +if __name__ == "__main__": + unittest.main() diff --git a/research/ui/test_resize_combined_reference.py b/research/ui/test_resize_combined_reference.py new file mode 100644 index 00000000..b4f20cd6 --- /dev/null +++ b/research/ui/test_resize_combined_reference.py @@ -0,0 +1,259 @@ +"""Fixed combined sources, actual protected peer and plan guards; no sockets/target.""" +import hashlib +import json +from pathlib import Path +import shutil +import socket +import tempfile +import unittest +from unittest.mock import patch + +from research.ui import resize_combined_reference as probe + + +def naws(columns, rows): + return bytes([255,250,31])+columns.to_bytes(2,"big")+rows.to_bytes(2,"big")+bytes([255,240]) + + +class Connection: + def __init__(self, peer, mode): + self.peer, self.mode, self.case = peer, mode, peer.case_id + self.initial, self.fragment, self.sent, self.resize_sent, self.calls = True, None, [], set(), 0 + def settimeout(self, _): pass + def close(self): pass + def sendall(self, data): self.sent.append(data) + def recv(self, _): + self.calls += 1 + if self.calls > 100: raise RuntimeError("Synthetic receive bound") + if self.initial: + self.initial = False + return naws(92 if self.mode == "stale-initial" else 91, 20) + state, directory = self.peer.state, self.peer.directory + armed = state.get("resize_armed") + if armed and armed not in self.resize_sent: + self.resize_sent.add(armed) + before = probe.required_grids(self.case)[armed-1] + grid = list(probe.required_grids(self.case)[armed]) + axis = 0 if before[0] != grid[0] else 1 + middle = list(grid); middle[axis] = (before[axis]+grid[axis])//2 + if self.mode == "wrong-axis": grid[1-axis] += 1 + if self.mode == "outside-interval": grid[axis] = max(before[axis],grid[axis])+1 + if self.mode == "wrong-endpoint": grid = middle + events = [middle, grid] if self.mode == "batched-resize" else [grid] + count = len(state["naws_history"])+(0 if self.mode == "stale-ready" else len(events)) + (directory/f"resize-ready-{armed}").write_text(json.dumps({"phase":armed,"grid":grid,"event_count":count})) + return b"".join(naws(*event) for event in events) + record = state["records"][-1] + if self.mode == "unarmed": return naws(91,19) + if record["cursor_reply"] is not None: + if self.mode == "no-ack-"+str(record["phase"]): + raise RuntimeError("Synthetic withheld acknowledgment") + (directory/f'ack-{record["phase"]}').write_text("observed") + target = record["phase"]+1 + if target in probe.resize_phases(self.case): + window = list(probe.windows(self.case)[target]) + if self.mode == "wrong-request": window[1] += 1 + (directory/f"resize-request-{target}").write_text(json.dumps({"phase":target,"window":window})) + raise socket.timeout() + if self.mode == "duplicate-reply": return b"\x1b[19;7R\x1b[19;7R" + if self.mode == "foreign-input": return b"SECRET" + if self.fragment is not None: + result, self.fragment = self.fragment, None + return result + self.fragment = (b"8;501R" if self.mode == "dsr-overflow" else + b"0;7R" if self.mode == "dsr-zero" else + b"18;7R" if self.mode == "wrong-initial-dsr" else b"19;7R") + return b"\x1b[" + + +def receive(case="width-height-retention", mode="normal"): + with tempfile.TemporaryDirectory() as temporary: + peer_type = probe.peer_class(case); peer = peer_type.__new__(peer_type) + peer.directory = Path(temporary).resolve() + peer.case_id = case + (peer.directory/"fixture-ready").write_text(json.dumps({"grid":[91,20],"event_count":1})) + peer.state = {"connected":False,"columns":None,"rows":None,"phase":None,"records":[]} + peer.stop = type("NeverStopped",(),{"is_set":lambda _:False})() + connection = Connection(peer,mode) + peer.listener = type("Listener",(),{"accept":lambda _:(connection,("127.0.0.1",12345))})() + peer._serve() + return peer.state, connection.sent + + + +def initial_lines(): + lines = [""]*20 + lines[1]="HTOP";lines[5]="HARD_A";lines[6]="HARD_B" + lines[11]=" "*89+"uvw";lines[18]="LOWEND" + return lines + + +def recording(case="width-height-retention"): + state, _ = receive(case, "batched-resize") + result={"status":"observed-fixed-resize-combined-states","case_id":case,"error":None,"observations":[], + "window_rect_format":"qt-qrect-inclusive-ltrb","geometry_checks":[],"full_active_grid_verified":False, + "local_shell_transport_verified":False,"presented_done":None} + for record,window in zip(state["records"],probe.windows(case)): + phase=record["phase"];lines=initial_lines() + rect=[113,-27,112+window[0],-28+window[1]] + terminal={"connected":True,"protocol":"Telnet","capacity":probe.required_grids(case)[phase][1], + "line_count":len(lines),"lines":lines,"markers":[],"top":None,"scroll":0, + "caret_api":{"caretLine":18,"caretColumn":6,"caretPosition":200}} + result["observations"].append({**{k:record[k] for k in ("phase","label","frozen_grid","cursor_reply","cursor_reply_private","response_hex")}, + "terminal":terminal,"window_rect":rect,"window_size":list(window)}) + result["geometry_checks"].append({"phase":phase,"window_rect":rect,"window_size":list(window),"expected_size":list(window), + "observed_grid":list(probe.required_grids(case)[phase]),"terminal_capacity":terminal["capacity"], + "event_count":state["freeze_event_counts"][phase]}) + return state,result + + +def save_mechanics(directory, state, result): + for name,value in (("peer.json",state),(probe.RESULT_FILE,result),("cleanup.json",{"active":0,"total":1,"terminated":1}), + ("profile-check.json",{"only_synthetic_session":True,"session_count":1})): + (directory/name).write_text(json.dumps(value),encoding="utf-8") + (directory/"stderr.txt").write_bytes(b"") + for phase in range(len(result["observations"])):(directory/f"ack-{phase}").write_text("observed") + + +class CombinedResizeReferenceTests(unittest.TestCase): + def test_fixed_combined_geometry_and_DSR_only_after_ASCII_source(self): + self.assertEqual([len(probe.phases(c)) for c in probe.CASES],[5]) + expected=(b"\x1b[?6l\x1b[?7h\x1b[r\x1b[0m\x1b[2J\x1b[2;1HHTOP\x1b[6;1HHARD_A\r\nHARD_B" + b"\x1b[12;90Huvw\x1b[19;1HLOWEND") + for case in probe.CASES: + self.assertEqual(probe.phases(case)[0][1],expected) + self.assertTrue(all(not value for _,value in probe.phases(case)[1:])) + self.assertEqual(probe.fixed_contract(case)["semantic_acceptance"],False) + self.assertEqual(probe.windows("width-height-retention"),[(1360,749),(1000,749),(1000,569),(1000,749),(1360,749)]) + self.assertEqual(probe.required_grids("width-height-retention"),[(91,20),(61,20),(61,14),(61,20),(91,20)]) + self.assertEqual(probe.fixed_contract("width-height-retention")["geometry_basis"]["geometry"]["strict_export_sha256"],"f18a2320c073ed477f773b42076c9968c0c8af8530af19548d5695e66e8cf9b5") + + def test_real_peer_fragmented_DSR_and_bounded_single_axis_NAWS(self): + for case in probe.CASES: + state,sent=receive(case,"batched-resize") + self.assertTrue(state["complete"],state.get("error"));self.assertIsNone(state.get("error")) + self.assertEqual(state["frozen_grids"],[list(g) for g in probe.required_grids(case)]) + self.assertEqual([r["cursor_reply"] for r in state["records"]],[[19,7]]*len(probe.phases(case))) + self.assertEqual(sent[-len(probe.phases(case)):],[value+b"\x1b[6n" for _,value in probe.phases(case)]) + + def test_wrong_axis_interval_endpoint_or_handshake_stops(self): + for case in probe.CASES: + for mode in ("wrong-axis","outside-interval","wrong-endpoint","stale-ready","wrong-request", + "stale-initial","unarmed","wrong-initial-dsr","duplicate-reply","foreign-input","dsr-overflow","dsr-zero"): + with self.subTest(case=case,mode=mode): + state,_=receive(case,mode);self.assertFalse(state.get("complete",False));self.assertTrue(state.get("error")) + + def test_every_before_resize_observation_requires_ack(self): + for case in probe.CASES: + for phase in range(len(probe.phases(case))-1): + state,_=receive(case,"no-ack-"+str(phase)) + self.assertTrue(state.get("error"));self.assertEqual(len(state["records"]),phase+1) + + def test_single_axis_intervals_never_authorize_both_axes(self): + self.assertTrue(probe.allowed_grid((91,20),(91,14),(91,15))) + self.assertTrue(probe.allowed_grid((91,14),(91,20),(91,20))) + for value in ((90,15),(91,9),(91,21),(0,15),(True,15)): + self.assertFalse(probe.allowed_grid((91,20),(91,10),value)) + self.assertFalse(probe.allowed_grid((91,20),(61,10),(76,15))) + + def test_initial_fixture_is_mandatory_later_loss_is_only_observation(self): + terminal={"line_count":20,"lines":initial_lines()} + for case in probe.CASES: + probe.validate_document(case,0,terminal) + for phase in range(1,len(probe.phases(case))):probe.validate_document(case,phase,{"line_count":1,"lines":[""]}) + with self.assertRaisesRegex(ValueError,"Initial fixture"):probe.validate_document(case,0,{"line_count":1,"lines":[""]}) + for lines in (["SECRET"],["HTOP","HTOP"],["\ufffd"],[" "*500],["uvw","uvw"]): + with self.assertRaises(ValueError):probe.validate_document(case,1,{"line_count":len(lines),"lines":lines}) + + def test_mechanics_rechecks_variable_capacity_fixed_geometry_and_all_boundaries(self): + for case in probe.CASES: + with tempfile.TemporaryDirectory() as temporary: + directory=Path(temporary);state,result=recording(case);save_mechanics(directory,state,result) + self.assertEqual(probe.audit_mechanics(directory,case)["stage_count"],len(probe.phases(case))) + bad=json.loads(json.dumps(result));bad["observations"][1]["terminal"]["capacity"]+=1 + save_mechanics(directory,state,bad) + with self.assertRaises(ValueError):probe.audit_mechanics(directory,case) + bad=json.loads(json.dumps(result));bad["geometry_checks"][1]["window_size"][1]+=1 + save_mechanics(directory,state,bad) + with self.assertRaises(ValueError):probe.audit_mechanics(directory,case) + bad=json.loads(json.dumps(state));before=bad["resize_boundaries"][0]["before_event_count"] + fixed_axis = 1 # The first transition changes width only. + bad["naws_history"][before][fixed_axis]+=1 + save_mechanics(directory,bad,result) + with self.assertRaises(ValueError):probe.audit_mechanics(directory,case) + save_mechanics(directory,state,result);(directory/"ack-0").unlink() + with self.assertRaises((ValueError,FileNotFoundError)):probe.audit_mechanics(directory,case) + + def test_archived_dependencies_are_unchanged_and_case_namespaces_isolated(self): + before=probe.baseline.phase_records() + a,b=probe.peer_class("width-height-retention"),probe.peer_class("width-height-retention") + self.assertEqual(len(a._serve.__globals__["phases"](20,91)),5) + self.assertEqual(len(b._serve.__globals__["phases"](20,91)),5) + self.assertEqual(probe.baseline.phase_records(),before) + for case in probe.CASES: + compile(probe.launcher_source(case),"","exec") + self.assertIn(json.dumps(probe.controller_contract(case)),probe.probe_fragment(case)) + + +class CombinedResizePlanTests(unittest.TestCase): + def setUp(self): + temporary=tempfile.TemporaryDirectory(prefix="resize-combined-tests-");self.addCleanup(temporary.cleanup) + self.root=Path(temporary.name).resolve() + for name in probe.SOURCE_FILES: + target=self.root/name;target.parent.mkdir(parents=True,exist_ok=True);shutil.copyfile(probe.ROOT/name,target) + archive={"schema":probe.common.ARCHIVE_SCHEMA,"source_base_commit":"b"*40, + "source_files_sha256":{name:probe.sha(self.root/name) for name in probe.SOURCE_FILES}} + (self.root/"reference-source.json").write_text(json.dumps(archive)) + self.source=self.root/"original";self.source.mkdir();(self.source/"WindTerm.exe").write_bytes(b"synthetic, not an executable") + for target,name,value in ((probe,"ROOT",self.root),(probe,"EXE_SHA256",probe.sha(self.source/"WindTerm.exe")), + (probe.common,"host_identity",lambda:{"synthetic_host":True}), + (probe.common,"rea_identity",lambda _: {"node_executable":"inert-node"})): + patcher=patch.object(target,name,value);patcher.start();self.addCleanup(patcher.stop) + self.out=self.root/"local/out";self.plan=self.root/"local/plan.json" + + def prepare(self,authorized=False): + return probe.prepare_plan(self.source,self.out,self.root/"rea.cmd",self.plan,"width-height-retention",authorized) + + def test_archive_plan_is_offline_and_exactly_ten_source_files(self): + with patch.object(probe.common.subprocess,"check_output",side_effect=AssertionError("No archive Git")): + plan=self.prepare() + self.assertEqual(len(plan["source_files_sha256"]),10) + self.assertEqual(plan["actual_original_runs"],0);self.assertFalse(self.out.exists()) + with self.assertRaisesRegex(ValueError,"not authorized"):probe.run_plan(self.plan) + self.assertFalse(self.plan.with_suffix(".attempted").exists()) + + def test_changed_case_geometry_or_source_is_rejected_before_attempt(self): + plan=self.prepare(True) + for key,value in (("case_id","width-then-height"),("phase_count",99),("required_grids",[]),("phases",[]), + ("source_identity",{"kind":"git-checkout"}),("launcher_source_sha256","0"*64)): + self.plan.write_text(json.dumps({**plan,key:value})) + with self.subTest(key=key),self.assertRaises(ValueError):probe.run_plan(self.plan) + self.assertFalse(self.plan.with_suffix(".attempted").exists()) + + def test_local_boundary_existing_plan_and_existing_attempt_remain_enforced(self): + with self.assertRaisesRegex(ValueError,"local directory"): + probe.prepare_plan(self.source,self.root/"elsewhere",self.root/"rea.cmd",self.plan,"width-height-retention") + with patch.object(probe,"launcher_source",return_value="def run(*args):\n pass\n"),patch.object(probe,"audit_mechanics",return_value={}): + self.prepare(True) + with self.assertRaisesRegex(ValueError,"must be new"):self.prepare(True) + # A fake launcher namespace keeps this test independent of target APIs. + self.out.mkdir() + self.out.rmdir() + with patch.object(probe.Path,"write_text",autospec=True) as write_result: + probe.run_plan(self.plan) + write_result.assert_called_once() + with self.assertRaises(FileExistsError):probe.run_plan(self.plan) + + def test_failed_attempt_is_retained_and_cannot_be_replayed(self): + source = "def run(*args):\n raise RuntimeError('synthetic launch failure')\n" + with patch.object(probe,"launcher_source",return_value=source),patch.object(probe,"audit_mechanics") as audit: + self.prepare(True) + with self.assertRaisesRegex(RuntimeError,"synthetic launch failure"): + probe.run_plan(self.plan) + self.assertEqual(self.plan.with_suffix(".attempted").read_text(),probe.ATTEMPT) + self.assertFalse(self.out.exists());audit.assert_not_called() + with self.assertRaises(FileExistsError):probe.run_plan(self.plan) + + +if __name__ == "__main__": + unittest.main() diff --git a/research/ui/test_resize_combined_stages.js b/research/ui/test_resize_combined_stages.js new file mode 100644 index 00000000..091b5336 --- /dev/null +++ b/research/ui/test_resize_combined_stages.js @@ -0,0 +1,158 @@ +// Execute the actual combined controller in pure V8: no target, socket or native UI. +'use strict'; +const fs=require('fs'),vm=require('vm'),assert=require('assert/strict'),crypto=require('crypto'); +const original=fs.readFileSync(process.argv[2],'utf8'); +const specs={ + 'width-height-retention':{grids:[[91,20],[61,20],[61,14],[61,20],[91,20]],labels:['initial-body','width-shrunk-unmodified','height-shrunk-unmodified','height-restored-unmodified','width-restored-unmodified'],resize:[1,2,3,4]} +}; +const markerLabels=['HTOP','HARD_A','HARD_B','u','v','w','LOWEND']; +function initialLines(){const a=Array(20).fill('');a[1]='HTOP';a[5]='HARD_A';a[6]='HARD_B';a[11]=' '.repeat(89)+'uvw';a[18]='LOWEND';return a;} +function fixture(caseId='width-height-retention'){ + const spec=specs[caseId],contract={case_id:caseId,labels:spec.labels,windows:spec.grids.map(([w,h])=>[w===91?1360:1000,h===20?749:569]), + grids:spec.grids,resize_phases:spec.resize,marker_labels:markerLabels}; + const state={now:0,window:[1360,749],origin:[113,-27],peer:{connected:true,columns:91,rows:20,phase:null,naws_history:[[91,20]]}, + terminal:{connected:true,protocol:'Telnet',capacity:20,line_count:20,lines:initialLines(),markers:[]}}; + const files=new Map(),messages=[],resizes=[],root='/owned/resize-combined'; + const module=name=>({name,path:root+'/app/'+name,base:{add:()=>({readU8:()=>0})}}); + const context={__RUN_DIRECTORY_JSON__:root,uiThread:1,wind:module('WindTerm.exe'),core:module('Qt5Core.dll'),gui:module('Qt5Gui.dll'),widgets:module('Qt5Widgets.dll'), + user:{getExportByName:()=>1},Process:{getCurrentThreadId:()=>1},Date:{now:()=>state.now},send:value=>messages.push(value),setTimeout:()=>1, + File:class{constructor(name){this.name=name;this.data='';}write(value){this.data+=value;}close(){files.set(this.name,{at:state.now,data:this.data});}}, + peerState:()=>state.peer,evalText:source=>{if(state.failTextRead)throw Error('synthetic terminal binding unavailable');return state.textAPI?vm.runInNewContext(source,{window:{activeText:state.textAPI}},{timeout:100}):JSON.stringify(state.terminal);},ready:()=>true, + resize:(_,w,h)=>{state.window=[w,h];resizes.push({at:state.now,size:[w,h]});}, + rect:()=>[...state.origin,state.origin[0]+state.window[0]-1,state.origin[1]+state.window[1]-1], + scan:()=>{},expandAll:()=>{},focus:()=>{},key:()=>{},main:1,tree:2}; + context.Interceptor={attach:(_,callbacks)=>{context.dispatch=callbacks.onEnter;}}; + vm.createContext(context);vm.runInContext(original.replace('__RESIZE_COMBINED_CASE_JSON__',JSON.stringify(contract)),context,{timeout:1000}); + const tick=now=>{state.now=now;vm.runInContext('dispatch()',context,{timeout:100});}; + const file=name=>files.get(root+'/'+name); + const phase=index=>{ + state.peer.case_id=caseId;state.peer.phase=index;state.peer.phase_label=spec.labels[index];state.peer.frozen_grid=contract.grids[index]; + state.peer.resize_armed=null;state.peer.records??=[]; + state.peer.records[index]={cursor_reply:[19,7],cursor_reply_private:false,response_hex:'1b5b31393b3752',frozen_grid:contract.grids[index]}; + }; + tick(0);tick(100);tick(200);tick(1000);tick(1600);assert(file('fixture-ready')); + const observe=(index,time)=>{ + if(spec.resize.includes(index)){ + assert(file('resize-request-'+index));state.peer.resize_armed=index;tick(time); + state.peer.columns=spec.grids[index][0];state.peer.rows=spec.grids[index][1]; + state.terminal.capacity=state.peer.rows;state.peer.naws_history.push(contract.grids[index]); + tick(time+100);tick(time+700);assert(file('resize-ready-'+index)); + phase(index);tick(time+800);tick(time+1200);assert(file('ack-'+index));return time+1300; + } + phase(index);tick(time);tick(time+400);assert(file('ack-'+index));return time+500; + }; + const through=index=>{let time=1700;for(let i=0;i<=index;i++)time=observe(i,time);return time;}; + return {state,tick,file,phase,observe,through,resizes,messages,spec,contract}; +} +const cases=[];function test(name,action){cases.push({name,action});} +test('fixed combined schedule records all5 states and checks both restorations',()=>{ + let total=0; + for(const caseId of Object.keys(specs)){ + const h=fixture(caseId),last=h.spec.grids.length-1,time=h.through(last);h.state.peer.complete=true;h.tick(time); + const r=JSON.parse(h.file('resize-combined-result.json').data); + assert.equal(r.status,'observed-fixed-resize-combined-states');assert.equal(r.observations.length,last+1); + assert.deepEqual(r.observations.map(o=>o.frozen_grid),h.spec.grids); + assert.deepEqual(h.resizes.map(o=>o.size),h.contract.windows);total+=r.observations.length; + } + assert.equal(total,5); +}); +test('all changes wait for prior observation acknowledgment',()=>{ + for(const caseId of Object.keys(specs)){ + const h=fixture(caseId);let time=h.through(0); + for(let i=1;i{ + for(const bad of ['missing','duplicate','DSR']){ + const h=fixture();h.phase(0); + if(bad==='missing')h.state.terminal.lines[1]=''; + else if(bad==='duplicate')h.state.terminal.lines[0]='HTOP'; + else h.state.peer.records[0].cursor_reply=[18,7]; + h.tick(1700);h.tick(2100);assert.equal(h.file('ack-0'),undefined); + assert.equal(h.file('resize-request-1'),undefined);assert.equal(JSON.parse(h.file('resize-combined-result.json').data).status,'failed'); + } +}); +test('later loss and DSR outside 14-row viewport remain observations',()=>{ + const h=fixture();let time=h.through(1);h.state.terminal.lines=[''];h.state.terminal.line_count=1;h.observe(2,time); + const r=h.messages.filter(x=>x.kind==='resize_combined_phase')[2];assert.deepEqual(r.cursor_reply,[19,7]);assert.deepEqual(r.frozen_grid,[61,14]); +}); +test('both width and height NAWS changes reset all 600ms of stability',()=>{ + for(const index of [1,2,3,4]){ + const h=fixture(),time=h.through(index-1);h.state.peer.resize_armed=index;h.tick(time); + const target=h.spec.grids[index],prior=h.spec.grids[index-1]; + h.state.peer.columns=target[0];h.state.peer.rows=target[1];h.state.terminal.capacity=target[1];h.state.peer.naws_history.push(target);h.tick(time+100); + const middle=target.map((n,axis)=>Math.floor((n+prior[axis])/2));h.state.peer.naws_history.push(middle,target);h.tick(time+650);h.tick(time+700); + assert.equal(h.file('resize-ready-'+index),undefined);h.tick(time+1250);assert(h.file('resize-ready-'+index)); + } +}); +test('post-input document changes restart the full 400ms stability interval',()=>{ + const h=fixture();h.phase(0);h.tick(1700);h.tick(2000);assert.equal(h.file('ack-0'),undefined); + h.state.terminal.top=1;h.tick(2050);h.tick(2100);assert.equal(h.file('ack-0'),undefined); + h.tick(2449);assert.equal(h.file('ack-0'),undefined);h.tick(2450);assert(h.file('ack-0')); +}); +test('initial sentinels are checked after bounded observation stability',()=>{ + const h=fixture();h.phase(0);h.state.terminal.lines[1]='';h.tick(1700);assert.equal(h.file('resize-combined-result.json'),undefined); + h.state.terminal.lines[1]='HTOP';h.tick(1900);h.tick(2299);assert.equal(h.file('ack-0'),undefined); + h.tick(2300);assert(h.file('ack-0')); +}); +test('fixed deadline rejects missing input without extending the schedule',()=>{ + const h=fixture();h.tick(41001);assert.equal(h.file('ack-0'),undefined); + const result=JSON.parse(h.file('resize-combined-result.json').data);assert.equal(result.status,'failed');assert.match(result.error,/bounded scenario deadline/); +}); +test('wrong actual endpoint is saved before prediction failure without calibration',()=>{ + for(const bad of ['grid','capacity','geometry']){ + const h=fixture(),time=h.through(1);h.state.peer.resize_armed=2;h.tick(time); + h.state.peer.rows=bad==='grid'?13:14;h.state.terminal.capacity=bad==='capacity'?20:h.state.peer.rows; + h.state.peer.naws_history.push([61,h.state.peer.rows]);if(bad==='geometry')h.state.window[1]++; + h.tick(time+100);h.tick(time+700);assert.equal(h.file('resize-ready-2'),undefined); + if(bad==='capacity')h.tick(41001); + const result=JSON.parse(h.file('resize-combined-result.json').data);assert.equal(result.status,'failed'); + assert.deepEqual(result.failure_geometry.requested_window,[1000,569]); + assert.deepEqual(result.failure_geometry.window_size,[1000,bad==='geometry'?570:569]); + assert.deepEqual(result.failure_geometry.observed_grid,[61,bad==='grid'?13:14]); + assert.equal(result.failure_geometry.terminal_capacity,bad==='capacity'?20:h.state.peer.rows); + if(bad!=='capacity'){ + assert.equal(result.geometry_checks.length,3); + assert.deepEqual(result.geometry_checks[2].observed_grid,result.failure_geometry.observed_grid); + assert.equal(result.geometry_checks[2].terminal_capacity,result.failure_geometry.terminal_capacity); + } + assert.equal(h.resizes.length,3); + } +}); +test('peer or phase errors retain bounded actual geometry and do not advance',()=>{ + for(const bad of ['peer','case','label']){ + const h=fixture();h.phase(0);if(bad==='case')h.state.peer.case_id='wrong';else if(bad==='label')h.state.peer.phase_label='wrong';else h.state.peer.error='synthetic';h.tick(1700); + assert.equal(h.file('ack-0'),undefined);const result=JSON.parse(h.file('resize-combined-result.json').data);assert.equal(result.status,'failed'); + assert.deepEqual(result.failure_geometry.window_size,[1360,749]);assert.deepEqual(result.failure_geometry.observed_grid,[91,20]); + } +}); +test('failed terminal binding cannot erase independent window and NAWS evidence',()=>{ + const h=fixture(),time=h.through(1);h.state.peer.resize_armed=2;h.tick(time); + h.state.peer.rows=14;h.state.peer.naws_history.push([61,14]);h.state.failTextRead=true;h.tick(time+100); + const result=JSON.parse(h.file('resize-combined-result.json').data);assert.equal(result.status,'failed'); + assert.deepEqual(result.failure_geometry.window_size,[1000,569]); + assert.deepEqual(result.failure_geometry.observed_grid,[61,14]);assert.equal(result.failure_geometry.terminal_capacity,null); + assert.equal(h.file('resize-ready-2'),undefined);assert.equal(h.resizes.length,3); +}); +test('missing final observation cannot become completion',()=>{ + const h=fixture(),time=h.through(3);h.state.peer.complete=true;h.tick(time); + assert.equal(JSON.parse(h.file('resize-combined-result.json').data).status,'failed');assert.equal(h.file('ack-4'),undefined); +}); +test('actual text sampler records ASCII API offsets separately from pixels',()=>{ + const h=fixture(),lines=initialLines(); + h.state.textAPI={isConnected:true,protocolName:'Telnet',visibleLineCount:20,screenLineCapacity:20,topLine:null,scrollTop:0, + caretLine:18,caretColumn:6,caretPosition:1806,lineText:i=>lines[i],lineStart:i=>i*100, + visibleRectFromPosition:p=>({x:(p%100)*8,y:Math.floor(p/100)*18,width:8,height:18})}; + h.through(0);const markers=h.messages.find(x=>x.kind==='resize_combined_phase').terminal.markers; + const u=markers.find(x=>x.label==='u');assert.equal(u.column,89);assert.equal(u.line_start_api,1100);assert.equal(u.position,1189); + assert.deepEqual(JSON.parse(JSON.stringify(u.rect)),{x:712,y:198,width:8,height:18}); +}); +const results=[];for(const entry of cases){try{entry.action();results.push({case:entry.name,status:'passed'});} + catch(error){results.push({case:entry.name,status:'failed',error:String(error.message)});}} +const failed=results.filter(v=>v.status==='failed').length; +process.stdout.write(JSON.stringify({schema:'termforge-resize-combined-controller-test-v1',node_version:process.version, + actual_source_sha256:crypto.createHash('sha256').update(original).digest('hex'),case_count:results.length,failed, + child_processes_spawned:0,sockets_opened:0,target_runs:0,results})+'\n');process.exitCode=failed?1:0; From 57cb70b220f7d9129358f876821049a4a9955509 Mon Sep 17 00:00:00 2001 From: JavaZeroo-Bot <133958205+JavaZeroo-Bot@users.noreply.github.com> Date: Tue, 3 Nov 2026 00:53:31 +0800 Subject: [PATCH 2/8] docs(research): pin ConPTY resize and VT output semantics Co-authored-by: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> --- docs/EVIDENCE.md | 1 + .../conpty-resize-output-2026-11-02.md | 42 +++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 docs/research/conpty-resize-output-2026-11-02.md diff --git a/docs/EVIDENCE.md b/docs/EVIDENCE.md index 17f1ba9a..389393a9 100644 --- a/docs/EVIDENCE.md +++ b/docs/EVIDENCE.md @@ -13,6 +13,7 @@ | 原版窗口高度分配及纯高度行为 | [独立两状态几何](../research/ui/results/2026-11-02-resize-geometry-reference/README.md)、[三阶段原版正文/DSR](../research/ui/results/2026-11-02-resize-height-measured-reference/README.md)和[核心12阶段回放](../experiments/core/results/2026-11-02-height-measured/README.md):window749→569实际改变180px,终端区域少120px;20→14→20行时正文不变,DSR19→14→14、scroll0→5→5 | 原10行预测失败保留,不回填缺失窗口测量;不把两状态比例推广。原top=null且非完整grid,scroll不证明内部物理历史。核心首轮夹具source_end越界保留,修夹具后1/1;没有生产修复或宽高组合/Qt验收 | | 自主 C++ 区域续接、历史结构通知与每屏 origin | [模式交叉与红绿](../experiments/core/results/2026-11-02-r27-mode-cross/README.md)、[每屏 origin 修正](../experiments/core/results/2026-11-02-origin-frame/README.md)、[原版固定控制对照](../research/ui/results/2026-11-02-mode-state-reference/README.md) | 区域自动换行的跨行复制/Find、RIS/ED3 结构通知、Unicode/分块/重复/异常输入与独立主屏 origin 恢复;当前物理 resize 策略测试不冒充原版匹配,完整 #27 继续开放 | | Qt / 固定 ConPTY 模式与正文 resize 差异 | [独立字面预期及失败运输证据](../experiments/shell/results/2026-11-02-qt-terminal-modes/README.md) | 模式阶段经公开 Qt 按键和原生网格核对;缩窄后原生 reflow 与核心物理裁剪不同步,独立运输捕获未见重排字节。失败诊断与模式回归分列,#29 的正文重排和完整 #27 不由基础尺寸通过代替 | +| 固定 ConPTY resize 与输出机制 | [1.25.260930003 包身份及 pinned 源码核查](research/conpty-resize-output-2026-11-02.md) | 当前 0x18 是字宽,旧 resize quirk 已删除;signal 成功非完成屏障,内部 reflow 不保证重发整屏,DSR 光标同步不证明正文一致。静态解释不替代同次 Qt 动态证据或原版 oracle;本次未新增运行或改 flags | | 原版 resize 后寻址与自主核心字面诊断 | [独立直接协议样本](../research/ui/results/2026-11-02-resize-address-reference/README.md)、[默认裁剪路径的失败诊断](../experiments/core/results/2026-11-02-resize-address/README.md) | 四例自然追加、CR、LF、CUP 保留动作前观测,文档、DSR、显示坐标分别核对;第五例 EL1 保留原审计失败。默认 `Screen::resize` 历史诊断仅 16/64 阶段通过、退出 1;当时 `EXCLUDE_FROM_ALL`,现参与默认构建但 CTest 只注册另行验证的显式保留入口。旧红不设 `WILL_FAIL`,常规 CI 或另一入口通过不能豁免默认路径差异 | | 自主 C++ 同 owner 保留列宽事务与有限编辑(#27/#29 独立入口) | [源码、定向红绿与限制](../experiments/core/results/2026-11-02-retained-columns/README.md):`a35be70` 的四个寻址 case 为 64/64 阶段;`fdeaed6` 使打印/ICH/DCH 按协议宽更新目标行,内容长度与 `written` 分离,三例编辑为 64/64 阶段。该固定源码重新构建后核心 22/22、Qt/固定 ConPTY 4/4,各配置身份独立记录 | 默认 resize / Qt 未切换;Qt 回归不执行新保留入口。Q/R 与 ICH/DCH 红绿分别保留,owner 存储及空白长度工程检查没有独立先行 RED。后续光标与隐藏帧修正另列,不继承本轮结果;默认裁剪与 B/CD 失败不豁免 | | 原版活动/隐藏帧与缩放后下一字符 | [6次原版、29阶段](../research/ui/results/2026-11-02-resize-frame-reference/README.md):5次原审计通过、1次隐藏主屏原审计失败后独立离线补充通过;无补跑。文档、DSR与显示分别记录 | 活动备用屏保留正文;隐藏主屏返回的正文和边界不同;4个ON/OFF缩放末列样本支持下一字符覆盖当前位置。原版Document索引差异、私有模式位及完整网格不据此判定 | diff --git a/docs/research/conpty-resize-output-2026-11-02.md b/docs/research/conpty-resize-output-2026-11-02.md new file mode 100644 index 00000000..e7cd6366 --- /dev/null +++ b/docs/research/conpty-resize-output-2026-11-02.md @@ -0,0 +1,42 @@ +# 固定 ConPTY 的 resize 与输出机制 + +2026-11-02 只读核查,服务于 #27/#29 的正式 Qt 接线。**固定版本没有可用的 `PSEUDOCONSOLE_RESIZE_QUIRK` 开关;内部 reflow 不意味着向宿主重发重排后的整屏。** 本文没有新增运行、修改 flags、依赖或产品路线,也不将源码解释记为动态验收。 + +## 固定身份 + +本次重新核对[包清单](../../third_party/conpty/manifest.json)、本地包原字节及 zip 成员。微软 [v1.25.2733.0 release](https://github.com/microsoft/terminal/releases/tag/v1.25.2733.0) 的同名 asset 大小及 digest 与本地一致;[annotated tag](https://api.github.com/repos/microsoft/terminal/git/tags/643609c3f9e022c9fb44366a43f5ebb8567f9722) 指向 **`dc8ae365847d41f62d1ebca93815bb00959bc12f`**。以下源码链接均固定在该 commit,历史链接另行标明。 + +| 对象 | SHA-256 | +|---|---| +| `Microsoft.Windows.Console.ConPTY.1.25.260930003.nupkg`,1,750,533 B | `6d6f8b008c655d814c498d8205d4012dcd4d0b6dfc1096461d0a185d1e3caf03` | +| win-x64 `conpty.dll` | `feeef341d891643c62d30b6b07800bc70f0bc148f44cb8c3bee84aa557ae805a` | +| 同包 `OpenConsole.exe` | `3d66b23d0a71bb8eed2b77edc8b9df9bf54ce6c8fb74c863a30e760997f80586` | +| 包内 `inc/conpty.h` | `976ccae27ccd70a4710275d05bbedbc7139a8e04b2537bfca0a827f70414f53d` | + +包内 header 与固定源码 [`src/inc/conpty-static.h`](https://github.com/microsoft/terminal/blob/dc8ae365847d41f62d1ebca93815bb00959bc12f/src/inc/conpty-static.h) 逐字节相同。以上建立发布包、pair、tag 与公开接口的来源关联,**不等于可重现构建或二进制控制流的动态证明**;不公开原始包或私有运行材料。 + +## flags 与实际机制 + +当前 [Session](../../experiments/conpty/session.hpp) 创建实参为 0,[正式后端配置](../../experiments/conpty/backend.hpp) 叠加 `0x18`,含义是 `PSEUDOCONSOLE_GLYPH_WIDTH_CONSOLE`。固定 header 的其他公开位是 inherit-cursor `0x1`、字宽选择 `0x08/0x10/0x18`、ambiguous-wide `0x20`。固定 [Create 路径](https://github.com/microsoft/terminal/blob/dc8ae365847d41f62d1ebca93815bb00959bc12f/src/winconpty/winconpty.cpp#L117-L200) 没有消费旧 `0x2` 的路径;按源码增加该位不会改变启动参数,本次未运行 A/B 验证。 + +历史 `PSEUDOCONSOLE_RESIZE_QUIRK=0x2` 控制的是 resize 后是否额外整屏 invalidation,**不是关闭内部 reflow**。删除前默认 false;开启后由终端负责自身布局,避免额外整屏重绘。[旧实现](https://github.com/microsoft/terminal/blob/a8582978afa50ece88edc7eda9e182ced64876e2/src/renderer/vt/state.cpp#L291-L342) 与[旧默认值](https://github.com/microsoft/terminal/blob/a8582978afa50ece88edc7eda9e182ced64876e2/src/host/ConsoleArguments.hpp#L130-L138)仅说明历史行为。微软在 2024-08-01 的 [`450eec48…` / #17510](https://github.com/microsoft/terminal/commit/450eec48de252a3a8d270bade847755ecbeb5a75) 删除该 flag 和 VtEngine,改为 API 调用期间生成 VT;不能把旧开关套到当前包。 + +固定源码支持以下区分: + +- **resize 请求与完成不同。** [`ConptyResizePseudoConsole`](https://github.com/microsoft/terminal/blob/dc8ae365847d41f62d1ebca93815bb00959bc12f/src/winconpty/winconpty.cpp#L280-L302) 写入独立 signal pipe;返回成功表示消息写入成功,不是布局完成或重绘屏障。[接收线程](https://github.com/microsoft/terminal/blob/dc8ae365847d41f62d1ebca93815bb00959bc12f/src/host/PtySignalInputThread.cpp#L165-L187)随后执行内部 resize。 +- **原生布局与输出流不同。** wrap-text [默认 true](https://github.com/microsoft/terminal/blob/dc8ae365847d41f62d1ebca93815bb00959bc12f/src/host/settings.cpp#L17-L45),[主屏走 reflow、备用屏走 traditional resize](https://github.com/microsoft/terminal/blob/dc8ae365847d41f62d1ebca93815bb00959bc12f/src/host/screenInfo.cpp#L1352-L1407);ConPTY [不创建旧 Renderer](https://github.com/microsoft/terminal/blob/dc8ae365847d41f62d1ebca93815bb00959bc12f/src/host/srvinit.cpp#L826-L865)。[`WriteCharsVT`](https://github.com/microsoft/terminal/blob/dc8ae365847d41f62d1ebca93815bb00959bc12f/src/host/_stream.cpp#L378-L435)先更新内部 state machine,再输出输入文本,保留 LF 转换及少量模式注入;它不是原生网格的持续镜像流。 +- **查询光标可能产生协议交互。** resize 会标记 cursor 可能失配;外部 GetConsoleScreenBufferInfo(Ex) [必要时等待 DSR 同步](https://github.com/microsoft/terminal/blob/dc8ae365847d41f62d1ebca93815bb00959bc12f/src/server/ApiDispatchers.cpp#L594-L606)。源码明确允许内部 buffer 因 VT/reflow 与宿主不同,[同步只处理光标](https://github.com/microsoft/terminal/blob/dc8ae365847d41f62d1ebca93815bb00959bc12f/src/host/screenInfo.cpp#L1412-L1515)。查询及回复可能干预 cursor,位置一致不能证明正文一致。 + +## 对已有 B/CD 红例的解释与限制 + +[既有 Qt / 固定 ConPTY 记录](../../experiments/shell/results/2026-11-02-qt-terminal-modes/README.md)中,80×24→60×12 后原生网格重排 B/CD,物理裁剪核心丢失右端正文并留下旧 D。另一条同源 transport 会话捕获 265 B,resize 边界后没有搬移 B/CD、清旧 D 或整屏重绘字节。这与固定源码的“内部 reflow + 按写入生成 VT”一致,不能据此归因于 parser 忽略已有重绘,也不能通过添加旧 flag 修复。 + +该运输捕获**不是同一次 Qt 会话**,源码不能替代其动态顺序证据。原生 grid 也不能替代 [WindTerm 原版分层观察](../../research/ui/results/2026-11-02-resize-state-reference/README.md)的显示 oracle;同时,原生缓冲会影响 legacy Console API 读写,不能整体忽略。保留旧 B/CD 失败,不把独立模块、纯 VT 或尺寸通过扩写为完整本地 shell 验收。 + +## 最小下一实验(尚未执行) + +复用原固定 pair、`0x18`、B/CD 合成输入、阶段门和回收预算,在**同一个正式 Qt SessionModel 会话**中: + +1. 于实际运输消费边界记录最多 64 KiB 字节、offset、resize 请求/worker 调用、native 尺寸 ACK 和 DSR 往返;API 返回仅记信号成功,以阶段和尺寸 ACK 确认落地。 +2. 分别固定 native grid/cursor、核心源正文及软硬边界、协议 cursor、ready 投影与源映射、Qt 实际消费的 page/layout revision;不以 native 行号替代显示行号。 +3. 检查是否真的出现重排/擦除字节,以及源内容和显示 page 是否符合原版合同。若有重绘则转查 parser/调度;若无重绘但源或 page 丢字,则仍是未完成的保留/投影闭环。记录查询光标的副作用,不能用光标一致掩盖正文差异。 From a73ee757970decfb9b8b29cc6036e4cc9ac77656 Mon Sep 17 00:00:00 2001 From: JavaZeroo-Bot <133958205+JavaZeroo-Bot@users.noreply.github.com> Date: Tue, 3 Nov 2026 00:53:31 +0800 Subject: [PATCH 3/8] docs(research): record width-height retention and restored layout Co-authored-by: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> --- .../README.md | 61 + .../audit.json | 102 + .../strict-export.json | 1896 +++++++++++++++++ 3 files changed, 2059 insertions(+) create mode 100644 research/ui/results/2026-11-02-resize-combined-reference/README.md create mode 100644 research/ui/results/2026-11-02-resize-combined-reference/audit.json create mode 100644 research/ui/results/2026-11-02-resize-combined-reference/strict-export.json diff --git a/research/ui/results/2026-11-02-resize-combined-reference/README.md b/research/ui/results/2026-11-02-resize-combined-reference/README.md new file mode 100644 index 00000000..77272a57 --- /dev/null +++ b/research/ui/results/2026-11-02-resize-combined-reference/README.md @@ -0,0 +1,61 @@ +# WindTerm 2.7.0:缩宽后缩高,再分别恢复高度与宽度 + +一次新进程完成 `91×20 → 61×20 → 61×14 → 61×20 → 91×20`。五阶段的完整 19 行合成文档完全相同,包括缩宽后仍位于源 offset 89/90/91 的 `uvw`;随后缩高没有裁掉这段右尾。DSR 为 **`19;7 → 19;7 → 14;7 → 14;7 → 14;7`**。恢复高度后 marker 矩形不变;最后恢复宽度只恢复宽度布局,保留缩高造成的纵向偏移。 + +这是固定顺序、主屏、ASCII、后续仅 DSR 的直接 Telnet 观察。`status=observation`、`semantic_acceptance=false`,不声明原版内部数据结构或 TermForge 组合尺寸实现已通过。 + +## 身份与固定输入 + +[strict-export.json](strict-export.json) 是 VM 严格导出的原字节,SHA256 `6a1561f3e6f6bc480c6a2f086adf7ed4798d5007b04a7bc71c7ae9deba90cfe6`;独立本地核对见 [audit.json](audit.json)。 + +- [探针合同](../../resize_combined_reference.md):唯一 case `width-height-retention`,1 次原版运行、5 个阶段;执行源码 `2c7c5929e11d911ad69a7e2b7c420120591862ed`。 +- 源 ZIP SHA256:`fcde0a8233b65fc4f2f786a357d956b26ac24bbcd9838ec8654ff9b190a400de`;执行源 manifest SHA256:`ee1b034d15c88279be9ca4a94b7e5869a064766310aae753a9367840bcb12a96`。 +- 单次计划 SHA256:`97e42ad2f90d4fd44a096f3843d32e329fc182748febae6d3099ef674a347c4b`。初始同纯高度样本的 75 B,SHA256 `ff48cd41220a72e5da2350dc97c3c8c8c884d48cc4f7fa2328609bf8bc0cb09e`;之后没有 CUP、CR/LF、补写、清屏或重绘。五次 DSR 共 20 B,应用回复共 35 B。 +- 逐阶段保存实际窗口、NAWS、capacity 与事件数后,再检查固定预测。`1000×569 / 61×14` 在本次得到实际核验;计划中的 `combined_grid_previously_observed=false` 仍保留其冻结前事实,不回填修改旧合同。 +- 本地核对 10 个执行源和 3 个出口源的 ZIP、manifest、当前冻结文件及指定提交;逐项重建 5 份完整文档、35 个 marker 和 5 组实际几何。完整原版包、私有 raw/provider 与计划校验由 VM 严格出口完成,本地没有重开那些原件。 + +这次使用独立 namespace、归档和新计划。[旧 10 行预测失败](../2026-11-02-resize-height-reference/README.md)仍保留,旧 `width-then-height` 仍为 0 次运行,不与本次五阶段样本合并。 + +## 五阶段结果 + +| 阶段 | 请求/实际窗口,px | NAWS,列×行 | capacity | DSR | API scroll | +| --- | --- | --- | ---: | --- | ---: | +| initial-body | 1360×749 | 91×20 | 20 | 19;7 | 0 | +| width-shrunk-unmodified | 1000×749 | 61×20 | 20 | 19;7 | 0 | +| height-shrunk-unmodified | 1000×569 | 61×14 | 14 | 14;7 | 5 | +| height-restored-unmodified | 1000×749 | 61×20 | 20 | 14;7 | 5 | +| width-restored-unmodified | 1360×749 | 91×20 | 20 | 14;7 | 5 | + +NAWS 事件仅为表中五组,冻结事件数 `1,2,3,4,5`;四次 resize 各有前后边界。API `top` 五次均为 null,不能补成 5;caret API `(line,column,position)` 五次均为 `(17,0,131)`,不能直接换算为协议 DSR。 + +五份完整逻辑文档数组逐项相同:零基行 1 为 `HTOP`,行 5 为 `HARD_A`,行 6 为 `HARD_B`,行 11 为 **89 个空格加 `uvw`**(92 个 ASCII 字符),行 17 为 `LOWEND`;其余 14 行为空字符串,包括末尾行 18。完整数组按 `json.dumps(lines,separators=(",", ":"))` 编码的 SHA256 均为 `ac5cc0c46768d14945e8c63103f3d7d56f9a5fc2f5631dd4fe8f485e9ba19efc`。 + +19 个逻辑字符串不是 19 条活动物理行,也不是完整活动网格。公开件逐行保存受限合成正文的 UTF-8 hex、目录索引及长度;初始目录 13 项,后续目录 19 项,目录身份不同不影响实际正文数组相等。 + +## 源位置与显示位置分列 + +七个 marker 在每阶段各一次,源位置全程不变。下表中源行/offset 为零基,DSR 为一基;矩形是 API 查询起点的 `(x,y)` 像素值。 + +| marker | 源行 / 行内 offset | API position | 初始 91×20 | 缩宽 61×20 | 缩高 61×14 | 恢复高 61×20 | 恢复宽 91×20 | +| --- | --- | ---: | --- | --- | --- | --- | --- | +| HTOP | 1 / 0 | 1 | (147,18) | (147,18) | (147,-72) | (147,-72) | (147,-72) | +| HARD_A | 5 / 0 | 9 | (147,90) | (147,90) | (147,0) | (147,0) | (147,0) | +| HARD_B | 6 / 0 | 16 | (147,108) | (147,108) | (147,18) | (147,18) | (147,18) | +| u | 11 / 89 | 116 | (859,198) | (371,216) | (371,126) | (371,126) | (859,108) | +| v | 11 / 90 | 117 | (867,198) | (379,216) | (379,126) | (379,126) | (867,108) | +| w | 11 / 91 | 118 | (147,216) | (387,216) | (387,126) | (387,126) | (147,126) | +| LOWEND | 17 / 0 | 125 | (147,324) | (147,324) | (147,234) | (147,234) | (147,234) | + +所有查询矩形宽、高均为 `8×18 px`,不是整个 marker 的包围框。`HTOP` 后三阶段为负 y,不能写成仍在可见区域内;正坐标同样不代表实际 glyph 呈现验收。本文没有用矩形或 `scroll=5` 推断原版内部恰有 5 条 history 物理行,也不把 API caret 与协议光标合为一层。 + +## 与已有观察的比较及实现约束 + +1. 本次阶段 0→1 是同一份 75 B 正文的单独缩宽对照:逻辑正文和 DSR 保持,`uvw` 按 61 列重新显示。这与[早先纯宽度样本](../2026-11-02-resize-state-reference/README.md)的“源位置保留、显示重排”现象一致;早先输入是另一份 78 B 正文与不同光标,不能拼作同一个样本或直接比较全部坐标。 +2. 阶段 1→2 只改变高度,正文右尾仍完整,所有 marker 比阶段 1 上移 90 px。高度恢复后仍保持这个位置。因此后续 owner 高度操作不能把先前保留的宽行重新裁到当前 61 列,造成 `u/v` 或软续接丢失。 +3. 最后恢复宽度后,解码正文、文档/API 标量、marker 和 DSR 与[纯高度样本](../2026-11-02-resize-height-measured-reference/README.md)的恢复阶段相同;初始观测也相同。两次公开件的目录来源/索引元数据仍独立。这只是两条固定轨迹的已观测端点相同,不证明任意宽高操作可交换。 + +当前 C++ 物理 `Screen::resize(height,currentWidth)` 会按当前列数重新适配已有行,可能丢弃前一宽度保留事务的右尾;这是源码对照,本文没有执行核心回放或声明修复。下一步应以本次精确源、五组 DSR、完整正文和显示位置建立独立字面回放,在同一 owner 内完成高度与宽度保留的组合职责。旧默认路径或已有测试成功不自动满足这条新合同。 + +仍未知:备用/隐藏帧、非默认 margin/origin、其他光标与底部正文、resize 后写入、历史上限/淘汰、其他尺寸或同时改变双轴、Unicode、选择/Find、真实 Qt/ConPTY 与实际呈现。完整网格、样式、性能及 R2 均未验收。 + +拥有进程的请求 exit 0、RootReaped/CleanupVerified 为 true、JobAfterActive 为 0;目标 Job 同样无活动进程。原版仍由受控启动器结束,清理成功不表示自然 GUI 退出。 diff --git a/research/ui/results/2026-11-02-resize-combined-reference/audit.json b/research/ui/results/2026-11-02-resize-combined-reference/audit.json new file mode 100644 index 00000000..b943a3c5 --- /dev/null +++ b/research/ui/results/2026-11-02-resize-combined-reference/audit.json @@ -0,0 +1,102 @@ +{ + "schema": "termforge-resize-combined-public-audit-v1", + "strict_public_sha256": "6a1561f3e6f6bc480c6a2f086adf7ed4798d5007b04a7bc71c7ae9deba90cfe6", + "strict_public_bytes": 59588, + "execution_source_base_commit": "2c7c5929e11d911ad69a7e2b7c420120591862ed", + "source_zip_sha256": "fcde0a8233b65fc4f2f786a357d956b26ac24bbcd9838ec8654ff9b190a400de", + "source_manifest_sha256": "ee1b034d15c88279be9ca4a94b7e5869a064766310aae753a9367840bcb12a96", + "execution_source_count": 10, + "offline_export_source_count": 3, + "source_zip_and_current_and_commit_bytes_match_public": true, + "original_runs": 1, + "phases_checked": 5, + "complete_literal_document_arrays_checked": 5, + "complete_document_line_count": 19, + "complete_document_sha256": "ac5cc0c46768d14945e8c63103f3d7d56f9a5fc2f5631dd4fe8f485e9ba19efc", + "literal_marker_records_checked": 35, + "saved_actual_geometry_records_checked": 5, + "status": "observation", + "semantic_acceptance": false, + "geometry_grids": [ + [ + 91, + 20 + ], + [ + 61, + 20 + ], + [ + 61, + 14 + ], + [ + 61, + 20 + ], + [ + 91, + 20 + ] + ], + "protocol_cursor_replies": [ + [ + 19, + 7 + ], + [ + 19, + 7 + ], + [ + 14, + 7 + ], + [ + 14, + 7 + ], + [ + 14, + 7 + ] + ], + "api_scroll_values": [ + 0, + 0, + 5, + 5, + 5 + ], + "api_top_values": [ + null, + null, + null, + null, + null + ], + "api_caret": { + "caretLine": 17, + "caretColumn": 0, + "caretPosition": 131 + }, + "all_marker_source_positions_unchanged": true, + "height_restore_marker_rectangles_equal_height_shrink": true, + "initial_and_final_semantic_fields_equal_pure_height_initial_and_final": true, + "cross_run_comparison_excludes_catalog_identity_metadata": true, + "internal_history_policy_inferred": false, + "full_active_grid_verified": false, + "post_resize_write_verified": false, + "alternate_frame_verified": false, + "raw_run_records_reopened_here": false, + "audit_boundary": "Local audit checked strict public bytes, five complete literal ASCII arrays, thirty-five literal marker records, saved actual geometry/NAWS/DSR, source archive/current/commit bytes, manifest and owned cleanup summary. Full original package/raw/provider checks were performed by the VM strict exporter and not repeated here.", + "owner_cleanup": { + "RootExitCode": 0, + "JobAfterActive": 0, + "RootReaped": true, + "CleanupVerified": true + }, + "old_height_10_row_failure_preserved": true, + "old_width_then_height_original_runs": 0, + "performance_gate": "not_evaluated" +} diff --git a/research/ui/results/2026-11-02-resize-combined-reference/strict-export.json b/research/ui/results/2026-11-02-resize-combined-reference/strict-export.json new file mode 100644 index 00000000..1274d044 --- /dev/null +++ b/research/ui/results/2026-11-02-resize-combined-reference/strict-export.json @@ -0,0 +1,1896 @@ +{ + "schema": "termforge-original-resize-combined-reference-v1", + "issue": 29, + "related_issue": 27, + "case_id": "width-height-retention", + "authorized_plan_sha256": "97e42ad2f90d4fd44a096f3843d32e329fc182748febae6d3099ef674a347c4b", + "geometry_basis": { + "combined_grid_previously_observed": false, + "geometry": { + "reference": "research/ui/results/2026-11-02-resize-geometry-reference/strict-export.json", + "strict_export_sha256": "f18a2320c073ed477f773b42076c9968c0c8af8530af19548d5695e66e8cf9b5", + "execution_source_base_commit": "3d4c8c2d6222caf0fc024729d9e0b34c020a69cf" + }, + "pure_height": { + "reference": "research/ui/results/2026-11-02-resize-height-measured-reference/strict-export.json", + "strict_export_sha256": "32d3f6cf2d1c6746ff517b684ecd8c631c86ecc87874209ba5f831ea244730e0", + "execution_source_base_commit": "50a560739e45d00ee31177d84b7c54a144f5273e" + } + }, + "offline_export_source_files_sha256": { + "research/ui/export_resize_combined_reference.py": "ccac4ba6f46c534c0ce7574cd346c48ca99486894a62cd38da4b33f3cd1ff442", + "research/ui/export_resize_state_reference.py": "473e901305354d4ba8b22c08d22fa506c4d40da0e73bed3bf3122e4ff0e3c9b7", + "research/ui/export_mode_state_reference.py": "7e0a1e0e49f149791464c0dec357d3b5260bc2c34ca9f9c1be4edab35626cdf3" + }, + "status": "observation", + "observation_kind": "bounded-direct-protocol-and-combined-resize", + "actual_original_runs": 1, + "maximum_original_runs": 1, + "semantic_acceptance": false, + "combined_policy_classification": null, + "full_active_grid_verified": false, + "terminal_styles_verified": false, + "local_shell_transport_verified": false, + "presented_done": null, + "performance_gate": "not_evaluated", + "r2_complete": false, + "rea_capture_seconds": 45, + "cli_wait_timeout_seconds": 60, + "whole_lifecycle_bound_seconds": null, + "deliberate_original_process_termination": true, + "natural_gui_exit_verified": false, + "capture": { + "phases": [ + { + "phase": 0, + "label": "initial-body", + "source_hex": "1b5b3f366c1b5b3f37681b5b721b5b306d1b5b324a1b5b323b314848544f501b5b363b3148484152445f410d0a484152445f421b5b31323b3930487576771b5b31393b31484c4f57454e44", + "source_bytes": 75, + "source_sha256": "ff48cd41220a72e5da2350dc97c3c8c8c884d48cc4f7fa2328609bf8bc0cb09e", + "dsr_suffix_hex": "1b5b366e", + "cursor_reply": [ + 19, + 7 + ], + "response_hex": "1b5b31393b3752", + "private_dsr_prefix": false, + "frozen_grid": [ + 91, + 20 + ], + "window_rect": [ + 102, + 102, + 1461, + 850 + ], + "window_size": [ + 1360, + 749 + ], + "document_projection": { + "line_count": 19, + "capacity": 20, + "top": null, + "scroll": 0, + "caret_api": { + "caretLine": 17, + "caretColumn": 0, + "caretPosition": 131 + }, + "markers": [ + { + "label": "HTOP", + "document_line": 1, + "line_text_offset_utf16": 0, + "line_text_offset_scalar": 0, + "line_text_prefix_utf8_bytes": 0, + "line_start_api_value": 1, + "requested_position_api_value": 1, + "rect": { + "x": 147, + "y": 18, + "width": 8, + "height": 18 + } + }, + { + "label": "HARD_A", + "document_line": 5, + "line_text_offset_utf16": 0, + "line_text_offset_scalar": 0, + "line_text_prefix_utf8_bytes": 0, + "line_start_api_value": 9, + "requested_position_api_value": 9, + "rect": { + "x": 147, + "y": 90, + "width": 8, + "height": 18 + } + }, + { + "label": "HARD_B", + "document_line": 6, + "line_text_offset_utf16": 0, + "line_text_offset_scalar": 0, + "line_text_prefix_utf8_bytes": 0, + "line_start_api_value": 16, + "requested_position_api_value": 16, + "rect": { + "x": 147, + "y": 108, + "width": 8, + "height": 18 + } + }, + { + "label": "u", + "document_line": 11, + "line_text_offset_utf16": 89, + "line_text_offset_scalar": 89, + "line_text_prefix_utf8_bytes": 89, + "line_start_api_value": 27, + "requested_position_api_value": 116, + "rect": { + "x": 859, + "y": 198, + "width": 8, + "height": 18 + } + }, + { + "label": "v", + "document_line": 11, + "line_text_offset_utf16": 90, + "line_text_offset_scalar": 90, + "line_text_prefix_utf8_bytes": 90, + "line_start_api_value": 27, + "requested_position_api_value": 117, + "rect": { + "x": 867, + "y": 198, + "width": 8, + "height": 18 + } + }, + { + "label": "w", + "document_line": 11, + "line_text_offset_utf16": 91, + "line_text_offset_scalar": 91, + "line_text_prefix_utf8_bytes": 91, + "line_start_api_value": 27, + "requested_position_api_value": 118, + "rect": { + "x": 147, + "y": 216, + "width": 8, + "height": 18 + } + }, + { + "label": "LOWEND", + "document_line": 17, + "line_text_offset_utf16": 0, + "line_text_offset_scalar": 0, + "line_text_prefix_utf8_bytes": 0, + "line_start_api_value": 125, + "requested_position_api_value": 125, + "rect": { + "x": 147, + "y": 324, + "width": 8, + "height": 18 + } + } + ], + "synthetic_lines_sha256": "ac5cc0c46768d14945e8c63103f3d7d56f9a5fc2f5631dd4fe8f485e9ba19efc", + "synthetic_text": { + "encoding": "UTF-8", + "catalog_source": "resize_combined_reference.synthetic_lines(case_id, phase); sorted ASCII scalar order", + "catalog_count": 13, + "catalog_sha256": "c420a7173a140e0651f3704d8e12996e4b4ff448809df62a104ba8505d68f5f5", + "lines": [ + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 8, + "utf8_hex": "48544f50", + "utf8_bytes": 4, + "unicode_scalars": 4, + "utf16_code_units": 4 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 4, + "utf8_hex": "484152445f41", + "utf8_bytes": 6, + "unicode_scalars": 6, + "utf16_code_units": 6 + }, + { + "catalog_index": 6, + "utf8_hex": "484152445f42", + "utf8_bytes": 6, + "unicode_scalars": 6, + "utf16_code_units": 6 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 3, + "utf8_hex": "2020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020757677", + "utf8_bytes": 92, + "unicode_scalars": 92, + "utf16_code_units": 92 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 10, + "utf8_hex": "4c4f57454e44", + "utf8_bytes": 6, + "unicode_scalars": 6, + "utf16_code_units": 6 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + } + ] + } + }, + "stage_role": "initial-body" + }, + { + "phase": 1, + "label": "width-shrunk-unmodified", + "source_hex": "", + "source_bytes": 0, + "source_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "dsr_suffix_hex": "1b5b366e", + "cursor_reply": [ + 19, + 7 + ], + "response_hex": "1b5b31393b3752", + "private_dsr_prefix": false, + "frozen_grid": [ + 61, + 20 + ], + "window_rect": [ + 102, + 102, + 1101, + 850 + ], + "window_size": [ + 1000, + 749 + ], + "document_projection": { + "line_count": 19, + "capacity": 20, + "top": null, + "scroll": 0, + "caret_api": { + "caretLine": 17, + "caretColumn": 0, + "caretPosition": 131 + }, + "markers": [ + { + "label": "HTOP", + "document_line": 1, + "line_text_offset_utf16": 0, + "line_text_offset_scalar": 0, + "line_text_prefix_utf8_bytes": 0, + "line_start_api_value": 1, + "requested_position_api_value": 1, + "rect": { + "x": 147, + "y": 18, + "width": 8, + "height": 18 + } + }, + { + "label": "HARD_A", + "document_line": 5, + "line_text_offset_utf16": 0, + "line_text_offset_scalar": 0, + "line_text_prefix_utf8_bytes": 0, + "line_start_api_value": 9, + "requested_position_api_value": 9, + "rect": { + "x": 147, + "y": 90, + "width": 8, + "height": 18 + } + }, + { + "label": "HARD_B", + "document_line": 6, + "line_text_offset_utf16": 0, + "line_text_offset_scalar": 0, + "line_text_prefix_utf8_bytes": 0, + "line_start_api_value": 16, + "requested_position_api_value": 16, + "rect": { + "x": 147, + "y": 108, + "width": 8, + "height": 18 + } + }, + { + "label": "u", + "document_line": 11, + "line_text_offset_utf16": 89, + "line_text_offset_scalar": 89, + "line_text_prefix_utf8_bytes": 89, + "line_start_api_value": 27, + "requested_position_api_value": 116, + "rect": { + "x": 371, + "y": 216, + "width": 8, + "height": 18 + } + }, + { + "label": "v", + "document_line": 11, + "line_text_offset_utf16": 90, + "line_text_offset_scalar": 90, + "line_text_prefix_utf8_bytes": 90, + "line_start_api_value": 27, + "requested_position_api_value": 117, + "rect": { + "x": 379, + "y": 216, + "width": 8, + "height": 18 + } + }, + { + "label": "w", + "document_line": 11, + "line_text_offset_utf16": 91, + "line_text_offset_scalar": 91, + "line_text_prefix_utf8_bytes": 91, + "line_start_api_value": 27, + "requested_position_api_value": 118, + "rect": { + "x": 387, + "y": 216, + "width": 8, + "height": 18 + } + }, + { + "label": "LOWEND", + "document_line": 17, + "line_text_offset_utf16": 0, + "line_text_offset_scalar": 0, + "line_text_prefix_utf8_bytes": 0, + "line_start_api_value": 125, + "requested_position_api_value": 125, + "rect": { + "x": 147, + "y": 324, + "width": 8, + "height": 18 + } + } + ], + "synthetic_lines_sha256": "ac5cc0c46768d14945e8c63103f3d7d56f9a5fc2f5631dd4fe8f485e9ba19efc", + "synthetic_text": { + "encoding": "UTF-8", + "catalog_source": "resize_combined_reference.synthetic_lines(case_id, phase); sorted ASCII scalar order", + "catalog_count": 19, + "catalog_sha256": "0b70999b5513a2dc9b22820cdea8ef8509adc6f52a1759c9417209fac376f5f2", + "lines": [ + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 12, + "utf8_hex": "48544f50", + "utf8_bytes": 4, + "unicode_scalars": 4, + "utf16_code_units": 4 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 6, + "utf8_hex": "484152445f41", + "utf8_bytes": 6, + "unicode_scalars": 6, + "utf16_code_units": 6 + }, + { + "catalog_index": 9, + "utf8_hex": "484152445f42", + "utf8_bytes": 6, + "unicode_scalars": 6, + "utf16_code_units": 6 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 4, + "utf8_hex": "2020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020757677", + "utf8_bytes": 92, + "unicode_scalars": 92, + "utf16_code_units": 92 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 15, + "utf8_hex": "4c4f57454e44", + "utf8_bytes": 6, + "unicode_scalars": 6, + "utf16_code_units": 6 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + } + ] + } + }, + "stage_role": "width-shrunk-unmodified" + }, + { + "phase": 2, + "label": "height-shrunk-unmodified", + "source_hex": "", + "source_bytes": 0, + "source_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "dsr_suffix_hex": "1b5b366e", + "cursor_reply": [ + 14, + 7 + ], + "response_hex": "1b5b31343b3752", + "private_dsr_prefix": false, + "frozen_grid": [ + 61, + 14 + ], + "window_rect": [ + 102, + 102, + 1101, + 670 + ], + "window_size": [ + 1000, + 569 + ], + "document_projection": { + "line_count": 19, + "capacity": 14, + "top": null, + "scroll": 5, + "caret_api": { + "caretLine": 17, + "caretColumn": 0, + "caretPosition": 131 + }, + "markers": [ + { + "label": "HTOP", + "document_line": 1, + "line_text_offset_utf16": 0, + "line_text_offset_scalar": 0, + "line_text_prefix_utf8_bytes": 0, + "line_start_api_value": 1, + "requested_position_api_value": 1, + "rect": { + "x": 147, + "y": -72, + "width": 8, + "height": 18 + } + }, + { + "label": "HARD_A", + "document_line": 5, + "line_text_offset_utf16": 0, + "line_text_offset_scalar": 0, + "line_text_prefix_utf8_bytes": 0, + "line_start_api_value": 9, + "requested_position_api_value": 9, + "rect": { + "x": 147, + "y": 0, + "width": 8, + "height": 18 + } + }, + { + "label": "HARD_B", + "document_line": 6, + "line_text_offset_utf16": 0, + "line_text_offset_scalar": 0, + "line_text_prefix_utf8_bytes": 0, + "line_start_api_value": 16, + "requested_position_api_value": 16, + "rect": { + "x": 147, + "y": 18, + "width": 8, + "height": 18 + } + }, + { + "label": "u", + "document_line": 11, + "line_text_offset_utf16": 89, + "line_text_offset_scalar": 89, + "line_text_prefix_utf8_bytes": 89, + "line_start_api_value": 27, + "requested_position_api_value": 116, + "rect": { + "x": 371, + "y": 126, + "width": 8, + "height": 18 + } + }, + { + "label": "v", + "document_line": 11, + "line_text_offset_utf16": 90, + "line_text_offset_scalar": 90, + "line_text_prefix_utf8_bytes": 90, + "line_start_api_value": 27, + "requested_position_api_value": 117, + "rect": { + "x": 379, + "y": 126, + "width": 8, + "height": 18 + } + }, + { + "label": "w", + "document_line": 11, + "line_text_offset_utf16": 91, + "line_text_offset_scalar": 91, + "line_text_prefix_utf8_bytes": 91, + "line_start_api_value": 27, + "requested_position_api_value": 118, + "rect": { + "x": 387, + "y": 126, + "width": 8, + "height": 18 + } + }, + { + "label": "LOWEND", + "document_line": 17, + "line_text_offset_utf16": 0, + "line_text_offset_scalar": 0, + "line_text_prefix_utf8_bytes": 0, + "line_start_api_value": 125, + "requested_position_api_value": 125, + "rect": { + "x": 147, + "y": 234, + "width": 8, + "height": 18 + } + } + ], + "synthetic_lines_sha256": "ac5cc0c46768d14945e8c63103f3d7d56f9a5fc2f5631dd4fe8f485e9ba19efc", + "synthetic_text": { + "encoding": "UTF-8", + "catalog_source": "resize_combined_reference.synthetic_lines(case_id, phase); sorted ASCII scalar order", + "catalog_count": 19, + "catalog_sha256": "0b70999b5513a2dc9b22820cdea8ef8509adc6f52a1759c9417209fac376f5f2", + "lines": [ + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 12, + "utf8_hex": "48544f50", + "utf8_bytes": 4, + "unicode_scalars": 4, + "utf16_code_units": 4 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 6, + "utf8_hex": "484152445f41", + "utf8_bytes": 6, + "unicode_scalars": 6, + "utf16_code_units": 6 + }, + { + "catalog_index": 9, + "utf8_hex": "484152445f42", + "utf8_bytes": 6, + "unicode_scalars": 6, + "utf16_code_units": 6 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 4, + "utf8_hex": "2020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020757677", + "utf8_bytes": 92, + "unicode_scalars": 92, + "utf16_code_units": 92 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 15, + "utf8_hex": "4c4f57454e44", + "utf8_bytes": 6, + "unicode_scalars": 6, + "utf16_code_units": 6 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + } + ] + } + }, + "stage_role": "height-shrunk-unmodified" + }, + { + "phase": 3, + "label": "height-restored-unmodified", + "source_hex": "", + "source_bytes": 0, + "source_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "dsr_suffix_hex": "1b5b366e", + "cursor_reply": [ + 14, + 7 + ], + "response_hex": "1b5b31343b3752", + "private_dsr_prefix": false, + "frozen_grid": [ + 61, + 20 + ], + "window_rect": [ + 102, + 102, + 1101, + 850 + ], + "window_size": [ + 1000, + 749 + ], + "document_projection": { + "line_count": 19, + "capacity": 20, + "top": null, + "scroll": 5, + "caret_api": { + "caretLine": 17, + "caretColumn": 0, + "caretPosition": 131 + }, + "markers": [ + { + "label": "HTOP", + "document_line": 1, + "line_text_offset_utf16": 0, + "line_text_offset_scalar": 0, + "line_text_prefix_utf8_bytes": 0, + "line_start_api_value": 1, + "requested_position_api_value": 1, + "rect": { + "x": 147, + "y": -72, + "width": 8, + "height": 18 + } + }, + { + "label": "HARD_A", + "document_line": 5, + "line_text_offset_utf16": 0, + "line_text_offset_scalar": 0, + "line_text_prefix_utf8_bytes": 0, + "line_start_api_value": 9, + "requested_position_api_value": 9, + "rect": { + "x": 147, + "y": 0, + "width": 8, + "height": 18 + } + }, + { + "label": "HARD_B", + "document_line": 6, + "line_text_offset_utf16": 0, + "line_text_offset_scalar": 0, + "line_text_prefix_utf8_bytes": 0, + "line_start_api_value": 16, + "requested_position_api_value": 16, + "rect": { + "x": 147, + "y": 18, + "width": 8, + "height": 18 + } + }, + { + "label": "u", + "document_line": 11, + "line_text_offset_utf16": 89, + "line_text_offset_scalar": 89, + "line_text_prefix_utf8_bytes": 89, + "line_start_api_value": 27, + "requested_position_api_value": 116, + "rect": { + "x": 371, + "y": 126, + "width": 8, + "height": 18 + } + }, + { + "label": "v", + "document_line": 11, + "line_text_offset_utf16": 90, + "line_text_offset_scalar": 90, + "line_text_prefix_utf8_bytes": 90, + "line_start_api_value": 27, + "requested_position_api_value": 117, + "rect": { + "x": 379, + "y": 126, + "width": 8, + "height": 18 + } + }, + { + "label": "w", + "document_line": 11, + "line_text_offset_utf16": 91, + "line_text_offset_scalar": 91, + "line_text_prefix_utf8_bytes": 91, + "line_start_api_value": 27, + "requested_position_api_value": 118, + "rect": { + "x": 387, + "y": 126, + "width": 8, + "height": 18 + } + }, + { + "label": "LOWEND", + "document_line": 17, + "line_text_offset_utf16": 0, + "line_text_offset_scalar": 0, + "line_text_prefix_utf8_bytes": 0, + "line_start_api_value": 125, + "requested_position_api_value": 125, + "rect": { + "x": 147, + "y": 234, + "width": 8, + "height": 18 + } + } + ], + "synthetic_lines_sha256": "ac5cc0c46768d14945e8c63103f3d7d56f9a5fc2f5631dd4fe8f485e9ba19efc", + "synthetic_text": { + "encoding": "UTF-8", + "catalog_source": "resize_combined_reference.synthetic_lines(case_id, phase); sorted ASCII scalar order", + "catalog_count": 19, + "catalog_sha256": "0b70999b5513a2dc9b22820cdea8ef8509adc6f52a1759c9417209fac376f5f2", + "lines": [ + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 12, + "utf8_hex": "48544f50", + "utf8_bytes": 4, + "unicode_scalars": 4, + "utf16_code_units": 4 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 6, + "utf8_hex": "484152445f41", + "utf8_bytes": 6, + "unicode_scalars": 6, + "utf16_code_units": 6 + }, + { + "catalog_index": 9, + "utf8_hex": "484152445f42", + "utf8_bytes": 6, + "unicode_scalars": 6, + "utf16_code_units": 6 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 4, + "utf8_hex": "2020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020757677", + "utf8_bytes": 92, + "unicode_scalars": 92, + "utf16_code_units": 92 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 15, + "utf8_hex": "4c4f57454e44", + "utf8_bytes": 6, + "unicode_scalars": 6, + "utf16_code_units": 6 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + } + ] + } + }, + "stage_role": "height-restored-unmodified" + }, + { + "phase": 4, + "label": "width-restored-unmodified", + "source_hex": "", + "source_bytes": 0, + "source_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "dsr_suffix_hex": "1b5b366e", + "cursor_reply": [ + 14, + 7 + ], + "response_hex": "1b5b31343b3752", + "private_dsr_prefix": false, + "frozen_grid": [ + 91, + 20 + ], + "window_rect": [ + 102, + 102, + 1461, + 850 + ], + "window_size": [ + 1360, + 749 + ], + "document_projection": { + "line_count": 19, + "capacity": 20, + "top": null, + "scroll": 5, + "caret_api": { + "caretLine": 17, + "caretColumn": 0, + "caretPosition": 131 + }, + "markers": [ + { + "label": "HTOP", + "document_line": 1, + "line_text_offset_utf16": 0, + "line_text_offset_scalar": 0, + "line_text_prefix_utf8_bytes": 0, + "line_start_api_value": 1, + "requested_position_api_value": 1, + "rect": { + "x": 147, + "y": -72, + "width": 8, + "height": 18 + } + }, + { + "label": "HARD_A", + "document_line": 5, + "line_text_offset_utf16": 0, + "line_text_offset_scalar": 0, + "line_text_prefix_utf8_bytes": 0, + "line_start_api_value": 9, + "requested_position_api_value": 9, + "rect": { + "x": 147, + "y": 0, + "width": 8, + "height": 18 + } + }, + { + "label": "HARD_B", + "document_line": 6, + "line_text_offset_utf16": 0, + "line_text_offset_scalar": 0, + "line_text_prefix_utf8_bytes": 0, + "line_start_api_value": 16, + "requested_position_api_value": 16, + "rect": { + "x": 147, + "y": 18, + "width": 8, + "height": 18 + } + }, + { + "label": "u", + "document_line": 11, + "line_text_offset_utf16": 89, + "line_text_offset_scalar": 89, + "line_text_prefix_utf8_bytes": 89, + "line_start_api_value": 27, + "requested_position_api_value": 116, + "rect": { + "x": 859, + "y": 108, + "width": 8, + "height": 18 + } + }, + { + "label": "v", + "document_line": 11, + "line_text_offset_utf16": 90, + "line_text_offset_scalar": 90, + "line_text_prefix_utf8_bytes": 90, + "line_start_api_value": 27, + "requested_position_api_value": 117, + "rect": { + "x": 867, + "y": 108, + "width": 8, + "height": 18 + } + }, + { + "label": "w", + "document_line": 11, + "line_text_offset_utf16": 91, + "line_text_offset_scalar": 91, + "line_text_prefix_utf8_bytes": 91, + "line_start_api_value": 27, + "requested_position_api_value": 118, + "rect": { + "x": 147, + "y": 126, + "width": 8, + "height": 18 + } + }, + { + "label": "LOWEND", + "document_line": 17, + "line_text_offset_utf16": 0, + "line_text_offset_scalar": 0, + "line_text_prefix_utf8_bytes": 0, + "line_start_api_value": 125, + "requested_position_api_value": 125, + "rect": { + "x": 147, + "y": 234, + "width": 8, + "height": 18 + } + } + ], + "synthetic_lines_sha256": "ac5cc0c46768d14945e8c63103f3d7d56f9a5fc2f5631dd4fe8f485e9ba19efc", + "synthetic_text": { + "encoding": "UTF-8", + "catalog_source": "resize_combined_reference.synthetic_lines(case_id, phase); sorted ASCII scalar order", + "catalog_count": 19, + "catalog_sha256": "0b70999b5513a2dc9b22820cdea8ef8509adc6f52a1759c9417209fac376f5f2", + "lines": [ + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 12, + "utf8_hex": "48544f50", + "utf8_bytes": 4, + "unicode_scalars": 4, + "utf16_code_units": 4 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 6, + "utf8_hex": "484152445f41", + "utf8_bytes": 6, + "unicode_scalars": 6, + "utf16_code_units": 6 + }, + { + "catalog_index": 9, + "utf8_hex": "484152445f42", + "utf8_bytes": 6, + "unicode_scalars": 6, + "utf16_code_units": 6 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 4, + "utf8_hex": "2020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020757677", + "utf8_bytes": 92, + "unicode_scalars": 92, + "utf16_code_units": 92 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + }, + { + "catalog_index": 15, + "utf8_hex": "4c4f57454e44", + "utf8_bytes": 6, + "unicode_scalars": 6, + "utf16_code_units": 6 + }, + { + "catalog_index": 0, + "utf8_hex": "", + "utf8_bytes": 0, + "unicode_scalars": 0, + "utf16_code_units": 0 + } + ] + } + }, + "stage_role": "width-restored-unmodified" + } + ], + "source_bytes": 75, + "dsr_request_bytes": 20, + "application_reply_bytes": 35, + "window_rect_format": "qt-qrect-inclusive-ltrb", + "geometry_checks": [ + { + "phase": 0, + "expected_size": [ + 1360, + 749 + ], + "window_rect": [ + 102, + 102, + 1461, + 850 + ], + "window_size": [ + 1360, + 749 + ], + "observed_grid": [ + 91, + 20 + ], + "terminal_capacity": 20, + "event_count": 1 + }, + { + "phase": 1, + "expected_size": [ + 1000, + 749 + ], + "window_rect": [ + 102, + 102, + 1101, + 850 + ], + "window_size": [ + 1000, + 749 + ], + "observed_grid": [ + 61, + 20 + ], + "terminal_capacity": 20, + "event_count": 2 + }, + { + "phase": 2, + "expected_size": [ + 1000, + 569 + ], + "window_rect": [ + 102, + 102, + 1101, + 670 + ], + "window_size": [ + 1000, + 569 + ], + "observed_grid": [ + 61, + 14 + ], + "terminal_capacity": 14, + "event_count": 3 + }, + { + "phase": 3, + "expected_size": [ + 1000, + 749 + ], + "window_rect": [ + 102, + 102, + 1101, + 850 + ], + "window_size": [ + 1000, + 749 + ], + "observed_grid": [ + 61, + 20 + ], + "terminal_capacity": 20, + "event_count": 4 + }, + { + "phase": 4, + "expected_size": [ + 1360, + 749 + ], + "window_rect": [ + 102, + 102, + 1461, + 850 + ], + "window_size": [ + 1360, + 749 + ], + "observed_grid": [ + 91, + 20 + ], + "terminal_capacity": 20, + "event_count": 5 + } + ], + "frozen_grids": [ + [ + 91, + 20 + ], + [ + 61, + 20 + ], + [ + 61, + 14 + ], + [ + 61, + 20 + ], + [ + 91, + 20 + ] + ], + "naws_history": [ + [ + 91, + 20 + ], + [ + 61, + 20 + ], + [ + 61, + 14 + ], + [ + 61, + 20 + ], + [ + 91, + 20 + ] + ], + "freeze_event_counts": [ + 1, + 2, + 3, + 4, + 5 + ], + "resize_boundaries": [ + { + "phase": 1, + "before_event_count": 1, + "after_event_count": 2, + "requested_window": [ + 1000, + 749 + ] + }, + { + "phase": 2, + "before_event_count": 2, + "after_event_count": 3, + "requested_window": [ + 1000, + 569 + ] + }, + { + "phase": 3, + "before_event_count": 3, + "after_event_count": 4, + "requested_window": [ + 1000, + 749 + ] + }, + { + "phase": 4, + "before_event_count": 4, + "after_event_count": 5, + "requested_window": [ + 1360, + 749 + ] + } + ], + "job_after": { + "active": 0, + "total": 3, + "terminated": 0 + }, + "provider": { + "id": "frida", + "version": null, + "confidence": "observed", + "evidence_id": "ev_c26798abe1e62ab2806e6d70cf55a9bf1d8c90f802255277ddb8180f2b5ba892", + "messages": 10, + "readiness_messages": 3, + "phase_messages": 5, + "messages_truncated": false, + "cleanup_error": null + }, + "stateful_observation_count": 5, + "post_initial_terminal_input": "DSR only", + "independent_dsr_bounds": { + "row": [ + 1, + 200 + ], + "column": [ + 1, + 500 + ] + }, + "text_units": { + "source": "UTF-8 bytes", + "document_line": "original public API index", + "marker_text_offsets": "JS UTF-16 code units; scalar count coincides only for this ASCII source alphabet", + "position_api": "recorded lineStart API value plus JS UTF-16 offset, passed unchanged to visibleRectFromPosition", + "caret_api": "original API scalar values; numbering policy not inferred", + "rectangles": "actual public API pixel rectangles, kept separate from protocol cells", + "protocol_cursor": "DSR values kept independent of text offsets and pixel rectangles", + "cpp_cell_column_mapping": null, + "document_position_unit_classification": null, + "ascii_only": true + } + }, + "provenance": { + "source_identity": { + "kind": "source-archive", + "manifest_sha256": "ee1b034d15c88279be9ca4a94b7e5869a064766310aae753a9367840bcb12a96" + }, + "execution_source_base_commit": "2c7c5929e11d911ad69a7e2b7c420120591862ed", + "execution_source_files_sha256": { + "research/ui/resize_combined_reference.py": "1af3f437d966d6da608bf12bdccb77308dd6a1429f2602c16a527a5fa426e967", + "research/ui/resize_combined_reference.js": "dac2668b054eea5c015539d7c0c0ca06d175a9d0436ef8a5a59af7cf6d28631f", + "research/ui/resize_state_reference.py": "628f9e3a5e3ecea6c34829810df2cdf0e93dc221bd3d19b31b3f5028759f07fd", + "research/ui/mode_state_reference.py": "bac745e74a2653412ea7a701e3a1f3defa1fb97fdd7841b73fad483120496ec4", + "research/ui/alternate_reference.py": "94b85d49347e0e9813f7acddf0244f58520a7d1fce14084677c91cf4ddafffea", + "research/ui/r1_launch.py": "54fb41872c6abcc48242a0938044184854b016785cc13659c66b1bf404da0d8a", + "research/ui/launch.py": "6970e0d4f2fe5e7ec4827df7aa1026cd0e74aeb828cdc9b7a0b42b3c887eb4e6", + "research/ui/qt_r1_text.js": "f5820d413fdd72c13dffcefbf4d0ade615abfe15754c6b0b52e1e098814eea6f", + "research/ui/r1_vt_fixture.py": "5853f684d5b8a4c17a30f22a536f04f0eb212813b8d6b3cc3c197df392eb732a", + "experiments/conpty/run.py": "3766879cafe38aabf53cfac8ab2f62985b1976b6fa24c8b6031258bdc914258f" + }, + "launcher_namespace_sha256": "fac0387952aba91510e94a9e7158a3a88cb8b9b955f6cb7e18ea58652ebb2312", + "reference_identity": { + "exe_sha256": "7b10531dccc7cd614ae559f36f1ef518e0c96335637431b4646354823b2804d0", + "manifest_sha256": "e459adf2f12adb6a1b7f55e66a9da1c22794185e66e848057d21e0c60d669203", + "files_verified": 4047, + "module_files_sha256": { + "WindTerm.exe": "7b10531dccc7cd614ae559f36f1ef518e0c96335637431b4646354823b2804d0", + "Qt5Core.dll": "8d2ff4ce9096ddccc4f4cd62c2e41fc854cfd1b0d6e8d296645a7f5fd4ae565a", + "Qt5Gui.dll": "5e7d2d41b8b92a880e83b8cc0ca173f5da61218604186196787ee1600956be1e", + "Qt5Widgets.dll": "3788c669d4b645e5a576de9fc77fca776bf516d43c89143dc2ca28291ba14358", + "Qt5Qml.dll": "30035484c81590976627f8face9507caa8581a7dc7630cccf6a8d6de65cab707" + } + }, + "launch_host": { + "windows_version": "10.0.26300", + "python_version_number": "3.12.10", + "python_exe_sha256": "4d6f5f81a4bca11191c4c7c6b43632694d0a4ce74e068619d8fdc161d469859a", + "node_exe_sha256": "1c331340fa6cac5e4294725959070a143fa0507195a78ad77eed7943f719aac9", + "rea_package_version": "6.7.0", + "rea_shim_sha256": "d092352a4088e7df223e9fe39039d703411dc5cbd39990f69756ade448ef1de4", + "rea_package_json_sha256": "3f6c47fb12c8388f291a21e7f99a876495d03aee671b3b6a0708dd12d6ebb570", + "rea_entry_sha256": "ed06cee49505b95a3cc5fceb1b4b279167e26df86227c644bb366ffffdbaf738", + "runtime_identity_source": "recorded execution plan; exporter does not execute the captured runtimes" + }, + "raw_files": { + "evidence.json": { + "bytes": 21663, + "sha256": "a85a33337b3abc04fd11a6c572548e673651c7610027b6ba87970873f644a20f" + }, + "run.json": { + "bytes": 617, + "sha256": "34569b6ef80280235373e1be96930d503c34dcb32952a7d4812b497fc3cfe548" + }, + "probe.js": { + "bytes": 26164, + "sha256": "c72562ce2467e7c6c30c27224a7456b9911a2dc3b0ae651695aaa276ddaa9b7e" + }, + "peer.json": { + "bytes": 2655, + "sha256": "7593fcd4a26c06f081ae3add74e1590d249e49128b25002a1b6a996ffbdc7a1a" + }, + "resize-combined-result.json": { + "bytes": 9963, + "sha256": "c0f489462c62332da5e7a547289ccb61ee7ccff9dbcaf2922dca6f46ff12b2d6" + }, + "files.json": { + "bytes": 637666, + "sha256": "a25c87cba935294d5bfd3bbc76c650d31a8473809564d798ad5a889e4aea00e8" + }, + "mechanics-check.json": { + "bytes": 458, + "sha256": "dab09bb21ee335d8159f6d5aa6009f6b0c3ed9365bab1305fa4b6edbbf70ab39" + }, + "stderr.txt": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "profile-check.json": { + "bytes": 52, + "sha256": "5e522a64e98197722ccd084639b1ce50258b238e03be2f016e81c8af023b5ee1" + }, + "cleanup.json": { + "bytes": 42, + "sha256": "8400f93b6a4a4a50cde35b5d474660590ed215732f55772a9d8afac138da4f1d" + }, + "fixture-ready": { + "bytes": 32, + "sha256": "1f0b8ece17746ce41c81df65e45a6f8480747d9b69858dc905a29ae8d4c449cb" + }, + "ack-0": { + "bytes": 8, + "sha256": "604cee807f644af47487bf2bbab442b94212ac5119f36f995f78e9e4694dae8c" + }, + "ack-1": { + "bytes": 8, + "sha256": "604cee807f644af47487bf2bbab442b94212ac5119f36f995f78e9e4694dae8c" + }, + "ack-2": { + "bytes": 8, + "sha256": "604cee807f644af47487bf2bbab442b94212ac5119f36f995f78e9e4694dae8c" + }, + "ack-3": { + "bytes": 8, + "sha256": "604cee807f644af47487bf2bbab442b94212ac5119f36f995f78e9e4694dae8c" + }, + "ack-4": { + "bytes": 8, + "sha256": "604cee807f644af47487bf2bbab442b94212ac5119f36f995f78e9e4694dae8c" + }, + "resize-request-1": { + "bytes": 31, + "sha256": "38caa2237388a5f1b45a7e96077c0f8424d279f6c34ca97d177e16cc47b99fe9" + }, + "resize-ready-1": { + "bytes": 42, + "sha256": "311f5ead4d5362d9c2e384404d7d2b293d6cbd037cefa590190e8b208c00a89c" + }, + "resize-request-2": { + "bytes": 31, + "sha256": "188ccc5f6bf7968c1ff8ff30784e061d2cf5c53d034281ca09718e19c1213ef0" + }, + "resize-ready-2": { + "bytes": 42, + "sha256": "42fb20666bbbdbf80eee431d292e5893a580b9c1199c1fb96ed32cc748da3c8c" + }, + "resize-request-3": { + "bytes": 31, + "sha256": "d807d031f7f18ae634ece071eb06c4018202bb502d83fb005628dbe6b5670e61" + }, + "resize-ready-3": { + "bytes": 42, + "sha256": "3401b378ce7514c087a38a59f6ff9c211df1a3165859244d629b9c520f105114" + }, + "resize-request-4": { + "bytes": 31, + "sha256": "8e3406b4230c141bd9ad2bf9e0216aa52c512930fafe79623425c3429e1036ae" + }, + "resize-ready-4": { + "bytes": 42, + "sha256": "ca1d387d0d766ed6c443f9e8dc614d9a72515ad10432b9dd1b1734a3b59324f4" + } + }, + "authorized_plan": { + "bytes": 7469, + "sha256": "97e42ad2f90d4fd44a096f3843d32e329fc182748febae6d3099ef674a347c4b" + } + }, + "limitations": [ + "One fresh direct Telnet process executes the immutable width-height-retention schedule 91x20 -> 61x20 -> 61x14 -> 61x20 -> 91x20; every stage is observed before the next resize.", + "After stage zero, terminal input is DSR only: no CUP, CR/LF, redraw, replacement text or post-resize write.", + "Stage zero requires every preloaded sentinel and DSR19;7; later missing markers are observations, never semantic acceptance.", + "The separate geometry and pure-height observations identify prior evidence only. The combined 61x14 endpoint and subsequent restorations are verified in this run, not inherited from those observations. Exact geometry and NAWS remain mandatory, with no calibration or retries.", + "Document indices, API positions, ASCII byte/UTF16 offsets, DSR and pixel rectangles are recorded separately; no general original position-unit or C++ Cell mapping is inferred.", + "DSR safety bounds remain row1..200/column1..500 independently of the fourteen-row viewport.", + "Only finite ASCII source-derived catalog strings may be published, with complete reconstructible document arrays. Unrestricted recording stays private.", + "This single schedule does not establish alternate-frame, Unicode, arbitrary dimensions or resize order, post-resize editing, ConPTY, performance or R2 acceptance." + ] +} From 13186bff79bb522276364c2f397aab6398fd02f0 Mon Sep 17 00:00:00 2001 From: JavaZeroo-Bot <133958205+JavaZeroo-Bot@users.noreply.github.com> Date: Tue, 3 Nov 2026 00:53:31 +0800 Subject: [PATCH 4/8] test(core): freeze observed width-height retention comparison Co-authored-by: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> --- CMakeLists.txt | 4 + experiments/core/resize_combined_test.cpp | 270 ++++++++++++++++++++++ 2 files changed, 274 insertions(+) create mode 100644 experiments/core/resize_combined_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 1216ac67..1984539a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -157,6 +157,10 @@ add_executable(resize_height_measured_test experiments/core/resize_height_measur target_link_libraries(resize_height_measured_test PRIVATE termforge_core) termforge_warnings(resize_height_measured_test) +add_executable(resize_combined_test experiments/core/resize_combined_test.cpp) +target_link_libraries(resize_combined_test PRIVATE termforge_core) +termforge_warnings(resize_combined_test) + add_executable(resize_unicode_test experiments/core/resize_unicode_test.cpp) target_link_libraries(resize_unicode_test PRIVATE termforge_core) termforge_warnings(resize_unicode_test) diff --git a/experiments/core/resize_combined_test.cpp b/experiments/core/resize_combined_test.cpp new file mode 100644 index 00000000..e2d792f1 --- /dev/null +++ b/experiments/core/resize_combined_test.cpp @@ -0,0 +1,270 @@ +// Literal WindTerm 2.7.0 width-height-retention observation: one run, five stages. +// Strict public JSON SHA256: +// 6a1561f3e6f6bc480c6a2f086adf7ed4798d5007b04a7bc71c7ae9deba90cfe6. +// Authorized plan SHA256: +// 97e42ad2f90d4fd44a096f3843d32e329fc182748febae6d3099ef674a347c4b. +// Original logical text, DSR and ASCII marker rectangles are separate oracles. +// Source mapping and full-text selection are TermForge engineering checks. +// Original scroll=5 does not establish an internal physical history layout. +// This does not cover full original grids, styles, Unicode, ConPTY or Qt. +#include "terminal.hpp" +#include "history_view.hpp" +#include "text_selection.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace termforge; + +struct Checks { + std::string context; + size_t literal_assertions = 0, literal_failures = 0; + size_t engineering_assertions = 0, engineering_failures = 0; + size_t phases = 0, literal_passed_phases = 0, engineering_passed_phases = 0; + + void expect(bool condition, const std::string& message, bool engineering = false) { + if (engineering) ++engineering_assertions; + else ++literal_assertions; + if (condition) return; + if (engineering) ++engineering_failures; + else ++literal_failures; + std::cerr << context << (engineering ? " [engineering]: " : " [literal]: ") << message << '\n'; + } +}; + +using DisplayPosition = std::pair; +using LogicalPosition = std::pair; + +struct Stage { + const char* label; + int columns, rows; + const char* reply; + std::array markers; +}; + +static constexpr std::array marker_names = { + "HTOP", "HARD_A", "HARD_B", "u", "v", "w", "LOWEND" +}; +static constexpr std::array original_logical_positions = {{ + {1, 0}, {5, 0}, {6, 0}, {11, 89}, {11, 90}, {11, 91}, {17, 0} +}}; + +// Pixel origin x=147 and ASCII pitch 8x18. These literal row/column values +// come from the fixed rectangles, not from original scroll or caretPosition. +// Initial x=147/147/147/859/867/147/147, y=18/90/108/198/198/216/324. +// Narrow u/v/w x=371/379/387, y=216; after height change their y=126. +// The last stage restores u/v to x=859/867,y=108 and w to x=147,y=126. +// HTOP y=-72 after shrinking height remains off-viewport, never clamped to 0. +static const std::array stages = {{ + {"initial-body", 91, 20, "\x1b[19;7R", {{{1, 0}, {5, 0}, {6, 0}, {11, 89}, {11, 90}, {12, 0}, {18, 0}}}}, + {"width-shrunk-unmodified", 61, 20, "\x1b[19;7R", {{{1, 0}, {5, 0}, {6, 0}, {12, 28}, {12, 29}, {12, 30}, {18, 0}}}}, + {"height-shrunk-unmodified", 61, 14, "\x1b[14;7R", {{{-4, 0}, {0, 0}, {1, 0}, {7, 28}, {7, 29}, {7, 30}, {13, 0}}}}, + {"height-restored-unmodified", 61, 20, "\x1b[14;7R", {{{-4, 0}, {0, 0}, {1, 0}, {7, 28}, {7, 29}, {7, 30}, {13, 0}}}}, + {"width-restored-unmodified", 91, 20, "\x1b[14;7R", {{{-4, 0}, {0, 0}, {1, 0}, {6, 89}, {6, 90}, {7, 0}, {13, 0}}}} +}}; + +static void feed(Parser& parser, std::string_view bytes, bool bytewise) { + if (bytewise) { + for (const char byte : bytes) parser.feed(std::string_view(&byte, 1)); + } else parser.feed(bytes); +} + +static void omit_empty_tail(std::vector& lines) { + // Only terminal empty strings from allocation capacity are excluded. + // Interior empty lines and every source-significant space remain exact. + while (!lines.empty() && lines.back().empty()) lines.pop_back(); +} + +static std::vector source_lines(const TextDocument& document) { + if (document.size() > 200) throw std::runtime_error("source exceeds the bounded fixture"); + std::vector result; + std::string logical; + for (size_t rank = 0; rank < document.size(); ++rank) { + const auto line = document.line(rank); + logical += line.projection.text; + if (!line.joins_next) { + result.push_back(std::move(logical)); + logical.clear(); + } + } + if (!logical.empty()) result.push_back(std::move(logical)); + omit_empty_tail(result); + return result; +} + +static std::vector marker_positions(const ViewSnapshot& page, std::string_view marker) { + std::vector result; + for (size_t row = 0; row < page.lines.size(); ++row) { + const auto& line = page.lines[row]; + for (size_t column = 0; column + marker.size() <= line.size(); ++column) { + bool matches = true; + for (size_t offset = 0; offset < marker.size(); ++offset) { + const auto& cell = line[column + offset]; + if (cell.width != 1 || cell.text != std::string(1, marker[offset])) { + matches = false; + break; + } + } + if (matches) result.emplace_back(static_cast(row), static_cast(column)); + } + } + return result; +} + +static std::optional logical_position(const TextDocument& document, TextPosition position) { + if (!document.valid(position)) return std::nullopt; + size_t row = 0, prefix = 0; + for (size_t rank = 0; rank < document.size(); ++rank) { + const auto line = document.line(rank); + if (line.projection.id == position.line_id) return LogicalPosition{row, prefix + position.column}; + // This fixture is ASCII; Cell columns and text offsets agree within + // each source fragment. This is not a UTF-8 or original API offset rule. + if (line.joins_next) prefix += line.projection.text.size(); + else { ++row; prefix = 0; } + } + return std::nullopt; +} + +static std::optional complete_text_range(const TextDocument& document) { + const auto first = document.position(0, 0); + if (!first) return std::nullopt; + for (size_t rank = document.size(); rank > 0; --rank) { + const auto line = document.line(rank - 1); + if (line.projection.spans.empty()) continue; + const auto last = document.position(rank - 1, line.projection.spans.back().column_end); + if (last) return TextRange{*first, *last}; + } + return std::nullopt; +} + +static void resize_existing_path(Screen& screen, const Stage& stage) { + // Exercise the existing real APIs. Width and height change in separate + // stages; the height path must not be replaced by an invented stub. + if (screen.width() != stage.columns) { + if (screen.height() != stage.rows) throw std::runtime_error("fixture changes two axes at once"); + screen.resize_columns_preserving(stage.columns); + } else if (screen.height() != stage.rows) { + screen.resize(stage.rows, stage.columns); + } +} + +static void replay(Checks& checks, ParseMode mode, bool bytewise) { + // Original 75 B input SHA256: + // ff48cd41220a72e5da2350dc97c3c8c8c884d48cc4f7fa2328609bf8bc0cb09e. + constexpr std::string_view source = "\x1b[?6l\x1b[?7h\x1b[r\x1b[0m\x1b[2J" + "\x1b[2;1HHTOP\x1b[6;1HHARD_A\r\nHARD_B\x1b[12;90Huvw\x1b[19;1HLOWEND"; + static_assert(source.size() == 75); + // Independently reconstructed original array: 19 entries in every stage. + // Canonical JSON SHA256: + // ac5cc0c46768d14945e8c63103f3d7d56f9a5fc2f5631dd4fe8f485e9ba19efc. + std::vector expected_body = { + "", "HTOP", "", "", "", "HARD_A", "HARD_B", "", "", "", "", + std::string(89, ' ') + "uvw", "", "", "", "", "", "LOWEND", "" + }; + omit_empty_tail(expected_body); + std::string expected_copy; + for (size_t row = 0; row < expected_body.size(); ++row) { + if (row) expected_copy += "\r\n"; + expected_copy += expected_body[row]; + } + + Parser parser(20, 91, 1000, mode); + HistoryViewport live_viewport, projection_viewport; + TextSelection full_selection; + const auto prefix = std::string("combined/") + + (mode == ParseMode::strict ? "strict/" : "recovery/") + (bytewise ? "bytewise/" : "whole/"); + + for (size_t phase = 0; phase < stages.size(); ++phase) { + const auto& stage = stages[phase]; + checks.context = prefix + stage.label; + const auto literal_before = checks.literal_failures; + const auto engineering_before = checks.engineering_failures; + if (phase == 0) feed(parser, source, bytewise); + else resize_existing_path(parser.screen, stage); + checks.expect(parser.screen.width() == stage.columns && parser.screen.height() == stage.rows, + "protocol grid differs"); + feed(parser, "\x1b[6n", bytewise); + checks.expect(parser.take_replies() == std::vector{stage.reply}, "DSR differs"); + + TextDocument document(parser.screen); + const auto body = source_lines(document); + checks.expect(body.size() == expected_body.size(), "logical body extent differs"); + for (size_t row = 0; row < expected_body.size(); ++row) + checks.expect(row < body.size() && body[row] == expected_body[row], + "logical body row " + std::to_string(row) + " differs"); + + if (phase == 0) { + const auto range = complete_text_range(document); + checks.expect(range && full_selection.set(document, *range), "cannot select complete source text", true); + } + const auto copied = full_selection.copy(document, 4096); + checks.expect(copied.status == SelectionCopyStatus::ok && copied.text == expected_copy, + "full-text selection lost or changed retained content", true); + + // Source traversal uses the real retained size, not an out-of-range + // source_end=200. The 200-row output cap bounds this fixed small case. + const auto page = projection_viewport.project(parser.screen, static_cast(stage.columns), + 200, {0, 0}, parser.screen.retained_size()); + const auto revision = projection_viewport.layout_revision(); + checks.expect(page.stop == ProjectionStop::source_end, "projection did not cover the complete source", true); + const auto live = live_viewport.reconcile(parser.screen); + // The live source anchor is an implementation value, not original + // scroll. In this fixture every line above it is a short hard line, + // so its source rank also denotes the projection's display origin. + for (size_t marker = 0; marker < marker_names.size(); ++marker) { + const auto positions = marker_positions(page.snapshot, marker_names[marker]); + auto relative = positions; + for (auto& position : relative) position.first -= static_cast(live.top); + checks.expect(relative == std::vector{stage.markers[marker]}, + std::string("projected marker ") + marker_names[marker] + " differs"); + + const auto name = std::string(marker_names[marker]); + checks.expect(positions.size() == 1, "mapping requires exactly one " + name, true); + if (positions.size() != 1) continue; + const auto row = static_cast(positions.front().first); + const auto column = static_cast(positions.front().second); + const auto begin = page.snapshot.mapping.hit(document, row, column, ViewAffinity::leading, revision); + const auto end = page.snapshot.mapping.hit(document, row, column + name.size(), ViewAffinity::trailing, revision); + checks.expect(begin && logical_position(document, *begin) == original_logical_positions[marker], + name + " maps to a different logical source position", true); + TextSelection marker_selection; + const bool selected = begin && end && *begin != *end && + marker_selection.set(document, {*begin, *end}) && marker_selection.range().has_value(); + checks.expect(selected, "cannot select mapped " + name, true); + if (selected) { + const auto text = marker_selection.copy(document, 64); + checks.expect(text.status == SelectionCopyStatus::ok && text.text == name, + "mapped selection does not copy " + name, true); + const auto highlight = page.snapshot.mapping.project(document, *marker_selection.range(), revision); + checks.expect(highlight.size() == 1 && highlight.front().row == row && + highlight.front().column_begin == column && highlight.front().column_end == column + name.size(), + "mapped highlight differs for " + name, true); + } + } + ++checks.phases; + if (checks.literal_failures == literal_before) ++checks.literal_passed_phases; + if (checks.engineering_failures == engineering_before) ++checks.engineering_passed_phases; + } + parser.finish(); +} + +int main() { + Checks checks; + for (const auto mode : {ParseMode::strict, ParseMode::recovery}) + for (const bool bytewise : {false, true}) { + try { replay(checks, mode, bytewise); } + catch (const std::exception& error) { checks.expect(false, std::string("replay exception: ") + error.what()); } + } + std::cout << "combined width/height literal replay: " << checks.literal_passed_phases << '/' << checks.phases + << " phases, " << checks.literal_failures << " failed assertions of " << checks.literal_assertions << '\n'; + std::cout << "separate mapping/selection engineering checks: " << checks.engineering_passed_phases << '/' << checks.phases + << " phases, " << checks.engineering_failures << " failed assertions of " << checks.engineering_assertions + << " (1 original case x 5 stages x strict/recovery x whole/bytewise; " + "existing width-preserving + legacy-height route; no original internal-history or Qt claim)\n"; + return checks.literal_failures || checks.engineering_failures ? 1 : 0; +} From eb09421cd87fa738d6ed9d6c5be97a567ca37375 Mon Sep 17 00:00:00 2001 From: JavaZeroo-Bot <133958205+JavaZeroo-Bot@users.noreply.github.com> Date: Tue, 3 Nov 2026 00:55:44 +0800 Subject: [PATCH 5/8] feat(core): retain bodies across height changes in one owner transaction Screen::resize_preserving prepares both frames before publishing: the height prefix moves into the primary history, surviving live rows keep their source cells and identities, and hidden live rows fit the protocol footprint. Any allocation failure publishes nothing. The transaction sweep and history-budget checks are engineering invariants, not original, Qt or performance acceptance. Co-authored-by: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> --- CMakeLists.txt | 6 + .../resize_preserving_transaction_test.cpp | 294 ++++++++++++++++++ experiments/core/screen.hpp | 92 ++++++ 3 files changed, 392 insertions(+) create mode 100644 experiments/core/resize_preserving_transaction_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 1984539a..f132dace 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -161,6 +161,10 @@ add_executable(resize_combined_test experiments/core/resize_combined_test.cpp) target_link_libraries(resize_combined_test PRIVATE termforge_core) termforge_warnings(resize_combined_test) +add_executable(resize_preserving_transaction_test experiments/core/resize_preserving_transaction_test.cpp) +target_link_libraries(resize_preserving_transaction_test PRIVATE termforge_core) +termforge_warnings(resize_preserving_transaction_test) + add_executable(resize_unicode_test experiments/core/resize_unicode_test.cpp) target_link_libraries(resize_unicode_test PRIVATE termforge_core) termforge_warnings(resize_unicode_test) @@ -430,6 +434,8 @@ add_test(NAME projection_job_core COMMAND projection_job_test) set_tests_properties(projection_job_core PROPERTIES TIMEOUT 10) add_test(NAME resize_height_measured_core COMMAND resize_height_measured_test) set_tests_properties(resize_height_measured_core PROPERTIES TIMEOUT 10) +add_test(NAME resize_preserving_transaction_core COMMAND resize_preserving_transaction_test) +set_tests_properties(resize_preserving_transaction_core PROPERTIES TIMEOUT 60) add_test(NAME retained_resize_unicode_core COMMAND resize_unicode_test) set_tests_properties(retained_resize_unicode_core PROPERTIES TIMEOUT 10) add_test(NAME retained_resize_combining_core COMMAND resize_combining_test) diff --git a/experiments/core/resize_preserving_transaction_test.cpp b/experiments/core/resize_preserving_transaction_test.cpp new file mode 100644 index 00000000..43f3d8d0 --- /dev/null +++ b/experiments/core/resize_preserving_transaction_test.cpp @@ -0,0 +1,294 @@ +// Failure-atomic owner engineering contract for the new geometry transaction. +// No original behavior oracle, no preimplementation RED claim. Allocation +// injection is confined to resize_preserving in this single-threaded process; +// fixture construction, inspection, exception reporting and retries run normally. +#include "screen.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace allocation_fault { +bool enabled = false; +size_t fail_at = 0, calls = 0, bytes = 0; +void start(size_t index) noexcept { fail_at = index; calls = bytes = 0; enabled = true; } +void stop() noexcept { enabled = false; } +void* allocate(size_t size) { + if (enabled) { + const auto index = calls++; + bytes += size; + if (index == fail_at) throw std::bad_alloc(); + } + if (void* value = std::malloc(size ? size : 1)) return value; + throw std::bad_alloc(); +} +} +void* operator new(size_t size) { return allocation_fault::allocate(size); } +void* operator new[](size_t size) { return allocation_fault::allocate(size); } +void operator delete(void* value) noexcept { std::free(value); } +void operator delete[](void* value) noexcept { std::free(value); } +void operator delete(void* value, size_t) noexcept { std::free(value); } +void operator delete[](void* value, size_t) noexcept { std::free(value); } + +using namespace termforge; +static_assert(std::is_nothrow_move_constructible_v); +static_assert(std::is_nothrow_move_assignable_v); +static_assert(alignof(Line) <= __STDCPP_DEFAULT_NEW_ALIGNMENT__); +static_assert(alignof(Cell) <= __STDCPP_DEFAULT_NEW_ALIGNMENT__); + +static void check(bool value, const char* message) { + if (!value) throw std::runtime_error(message); +} + +static std::string line_image(const Line& line) { + std::ostringstream out; + out << line.id << ':' << line.version << ':' << static_cast(line.ending) << ':' + << line.next_id << ':' << line.wrap_columns << ':' << line.content_columns << ':' << line.cropped << ':'; + for (const auto& cell : line) { + Screen::quoted(out, cell.text); + out << ':' << cell.width << ':' << cell.marks << ':' << cell.written << ':' << cell.clipped + << ':' << cell.grapheme_fragment << ':' << cell.leading_spacer; + cell.style.json(out); + } + return out.str(); +} + +static std::string frame_image(const Screen& screen) { + std::ostringstream out; + out << screen.width() << ',' << screen.height() << ',' << screen.row << ',' << screen.column << ',' + << screen.wrap_pending << ',' << screen.discarded << ',' << screen.origin_mode() << ',' + << screen.region_top() << ',' << screen.region_bottom() << ',' << screen.order_known() << ';'; + screen.style.json(out); + out << "history["; + for (const auto& line : screen.history_lines()) out << line_image(line) << ';'; + out << "]live["; + for (const auto& line : screen.visible_lines()) out << line_image(line) << ';'; + return out.str() + ']'; +} + +// All callers create both frames before this observation. Switching a value +// copy exposes hidden state without mutating the original owner's epoch/guards. +static std::string owner_image(const Screen& screen) { + std::ostringstream out; + out << static_cast(screen.active_buffer()) << ',' << screen.buffer_epoch() << ',' + << screen.order_revision() << ',' << screen.content_revision() << ':' << frame_image(screen); + Screen copy(screen); + copy.activate_buffer(screen.active_buffer() == ScreenBuffer::primary ? ScreenBuffer::alternate : ScreenBuffer::primary); + return out.str() + "|hidden|" + frame_image(copy); +} + +struct HistoryAddress { uint64_t id; const Cell* data; }; +static std::vector history_addresses(const Screen& screen) { + std::vector result; + for (const auto& line : screen.history_lines()) result.push_back({line.id, line.data()}); + return result; +} +static void surviving_addresses(const Screen& screen, const std::vector& before) { + for (const auto& address : before) { + const auto* line = screen.find_line(address.id); + if (line) check(line->data() == address.data, "surviving old history was copied or replaced"); + } +} + +static void fill_row(Screen& screen, int row) { + screen.move(row, 0); + screen.put('e', "e"); + // Seventeen UTF-8 bytes force a real string allocation on common STL + // implementations; the test does not assume a particular SSO capacity. + for (int i = 0; i < 8; ++i) screen.put(0x0301, "\xcc\x81"); + screen.put(0x754c, "\xe7\x95\x8c"); + screen.put('Z', "Z"); +} +struct Fixture { + Screen screen; + std::vector primary_history; + Fixture(int rows, int columns, size_t limit, size_t history_rows, bool alternate) + : screen(rows, columns, limit) { + screen.style.fg = 2; screen.style.bg = 4; screen.style.underline = true; + for (int row = 0; row < rows; ++row) fill_row(screen, row); + for (size_t i = 0; i < history_rows; ++i) { + screen.move(rows - 1, 0); screen.newline(); fill_row(screen, rows - 1); + } + screen.set_scroll_region(0, rows - 1); screen.set_origin_mode(true); + fill_row(screen, rows - 1); + screen.activate_buffer(ScreenBuffer::alternate); + screen.style.fg = 1; screen.style.reverse = true; + for (int row = 0; row < rows; ++row) fill_row(screen, row); + screen.activate_buffer(ScreenBuffer::primary); + primary_history = history_addresses(screen); + if (alternate) screen.activate_buffer(ScreenBuffer::alternate); + } +}; + +static uint64_t next_identity_probe(const Screen& original) { + Screen copy(original); + copy.set_origin_mode(false); copy.set_scroll_region(0, copy.height() - 1); + copy.move(copy.height() - 1, 0); copy.newline(); + return copy.visible_lines().back().id; +} +static std::string attachment_probe(const Screen& original) { + Screen copy(original); + std::string accepted = "mark accepted:"; + try { copy.put(0x0302, "\xcc\x82"); } + catch (const InputError&) { accepted = "mark rejected:"; } + copy.put('Q', "Q"); + return accepted + owner_image(copy); +} + +static bool injected_resize(Screen& screen, int rows, int columns, size_t failure) { + bool failed = false; + allocation_fault::start(failure); + try { screen.resize_preserving(rows, columns); } + catch (const std::bad_alloc&) { failed = true; } + catch (...) { allocation_fault::stop(); throw; } + allocation_fault::stop(); + return failed; +} + +static void every_allocation_rolls_back(bool alternate, bool grow) { + size_t failures = 0; + bool reached_success = false; + for (size_t failure = 0; failure < 4096; ++failure) { + // Shrinking 48 rows to one appends a bounded 47-row prefix to primary + // history, exercising deque block/map allocations as well as live copies. + Fixture fixture(grow ? 4 : 48, 6, 64, 5, alternate); + auto& screen = fixture.screen; + Screen unchanged(screen), success(screen); + const auto before = owner_image(screen); + const auto addresses = history_addresses(screen); + const int rows = grow ? 12 : 1, columns = grow ? 12 : 6; + success.resize_preserving(rows, columns); + const bool failed = injected_resize(screen, rows, columns, failure); + if (failed) { + ++failures; + check(owner_image(screen) == before, "allocation failure partly published frame/storage/guard state"); + surviving_addresses(screen, addresses); + check(next_identity_probe(screen) == next_identity_probe(unchanged), "failed resize spent a future identity"); + check(attachment_probe(screen) == attachment_probe(unchanged), "failed resize changed pending/attachment behavior"); + screen.resize_preserving(rows, columns); // Retry outside injection. + } + check(owner_image(screen) == owner_image(success), "retry differs from uninterrupted transaction"); + check(next_identity_probe(screen) == next_identity_probe(success), "retry changed the next fresh identity"); + if (screen.active_buffer() == ScreenBuffer::alternate) screen.activate_buffer(ScreenBuffer::primary); + surviving_addresses(screen, fixture.primary_history); + if (!failed) { reached_success = true; break; } + } + check(reached_success && failures > 0, "allocation sweep did not exercise failure and eventual success"); + std::cout << "allocation sweep " << (alternate ? "alternate" : "primary") << '/' << (grow ? "grow" : "shrink") + << ": " << failures << " injected failures, then success (no timing claim)\n"; +} + +static void history_budgets_and_soft_links() { + for (const size_t limit : {size_t{0}, size_t{3}, size_t{10}}) { + Screen screen(10, 6, limit); + for (int row = 0; row < 10; ++row) fill_row(screen, row); + for (size_t i = 0; i < limit + 2; ++i) { + screen.move(9, 0); screen.newline(); fill_row(screen, 9); + } + screen.move(7, 0); screen.newline(true, 6); // Retained history/live soft boundary after shrink. + screen.move(9, 4); + std::vector expected_history(screen.history_lines().begin(), screen.history_lines().end()); + const auto live = screen.visible_lines(); + expected_history.insert(expected_history.end(), live.begin(), live.begin() + 8); + const auto evicted = expected_history.size() > limit ? expected_history.size() - limit : 0; + expected_history.erase(expected_history.begin(), expected_history.begin() + evicted); + const auto addresses = history_addresses(screen); + const auto discarded = screen.discarded, epoch = screen.buffer_epoch(); + const auto content = screen.content_revision(), order = screen.order_revision(); + screen.resize_preserving(2, 6); + check(screen.history_size() == expected_history.size() && screen.discarded == discarded + evicted, + "history capacity or discarded count differs"); + for (size_t row = 0; row < expected_history.size(); ++row) + check(line_image(screen.history_lines()[row]) == line_image(expected_history[row]), "retained history content/link changed"); + for (size_t row = 0; row < 2; ++row) + check(line_image(screen.visible_lines()[row]) == line_image(live[row + 8]), "surviving live content/link changed"); + surviving_addresses(screen, addresses); + check(screen.buffer_epoch() == epoch && screen.content_revision() > content && screen.order_revision() > order, + "successful geometry transaction published wrong reader guards"); + } +} + +static void noops_and_invalid_dimensions() { + Fixture fixture(4, 6, 8, 5, false); + auto& screen = fixture.screen; + screen.move(3, 5); screen.put('P', "P"); + const auto before = owner_image(screen); + check(!injected_resize(screen, 4, 6, 0) && allocation_fault::calls == 0, "same-size request allocated"); + check(owner_image(screen) == before, "same-size request changed pending content or guards"); + for (const auto [rows, columns] : {std::pair{0, 6}, {-1, 6}, {201, 6}, {4, 0}, {4, -1}, {4, 501}}) { + bool rejected = false; + try { screen.resize_preserving(rows, columns); } + catch (const std::invalid_argument&) { rejected = true; } + check(rejected && owner_image(screen) == before, "invalid geometry partly published state"); + } + Screen old_width_path(screen); + screen.resize_preserving(4, 3); old_width_path.resize_columns_preserving(3); + check(owner_image(screen) == owner_image(old_width_path), "same-height request changed the existing width-only contract"); +} + +static void reachable_overflow_failures() { + for (const bool identity : {false, true}) { + Fixture fixture(2, 6, 8, 0, false); + auto& screen = fixture.screen; + auto imported = screen.visible_lines(); + if (identity) { + imported[0].id = (std::numeric_limits::max)() - 3; + imported[1].id = (std::numeric_limits::max)() - 2; + } else imported[0].version = (std::numeric_limits::max)(); + screen.publish({}, std::move(imported)); + if (!identity) screen.activate_buffer(ScreenBuffer::alternate); // Hidden primary must be cropped. + const auto before = owner_image(screen); + const auto next = next_identity_probe(screen); + bool rejected = false; + try { screen.resize_preserving(3, identity ? 6 : 3); } + catch (const std::overflow_error&) { rejected = true; } + check(rejected && owner_image(screen) == before && next_identity_probe(screen) == next, + "ID/version overflow published state or spent an identity"); + } + // This counter is already public; private content/order guards get no test + // escape hatch. Their prechecks remain part of the implementation audit. + Fixture fixture(4, 6, 0, 0, false); + fixture.screen.discarded = (std::numeric_limits::max)(); + const auto before = owner_image(fixture.screen); + bool rejected = false; + try { fixture.screen.resize_preserving(1, 6); } + catch (const std::overflow_error&) { rejected = true; } + check(rejected && owner_image(fixture.screen) == before, "discard count overflow partly published state"); +} + +static void growth_allocations_do_not_copy_history() { + std::pair measured[2]; + for (size_t sample = 0; sample < 2; ++sample) { + Fixture fixture(4, 6, 5000, sample ? 4000 : 8, false); + const auto addresses = history_addresses(fixture.screen); + check(!injected_resize(fixture.screen, 7, 6, (std::numeric_limits::max)()), "unlimited allocation recording failed"); + measured[sample] = {allocation_fault::calls, allocation_fault::bytes}; + surviving_addresses(fixture.screen, addresses); + } + // Growth has no outgoing prefix or deque append. Equal live fixtures must + // allocate the same preparation regardless of 8 versus 4000 history rows. + // This is an allocation invariant, not a time or deque-capacity benchmark. + check(measured[0] == measured[1], "height growth allocation depends on history length"); +} + +int main() { + try { + for (const bool alternate : {false, true}) + for (const bool grow : {false, true}) every_allocation_rolls_back(alternate, grow); + history_budgets_and_soft_links(); + noops_and_invalid_dimensions(); + reachable_overflow_failures(); + growth_allocations_do_not_copy_history(); + std::cout << "resize preservation transaction engineering checks passed; no original/Qt/performance acceptance claim\n"; + return 0; + } catch (const std::exception& error) { + allocation_fault::stop(); + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/experiments/core/screen.hpp b/experiments/core/screen.hpp index 117e7a95..d018fd36 100644 --- a/experiments/core/screen.hpp +++ b/experiments/core/screen.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -506,6 +507,97 @@ class Screen { inactive_->last_row = inactive_->last_column = -1; } } + // Resize retained source within the existing owner. A height transaction + // prepares only live rows and their outgoing prefix; old history bodies are + // never traversed or copied. Active fragments retain their right tails. + // Hidden live fragments fit the protocol footprint, including on a height + // change. Neither frame pulls old history back into newly added bottom rows. + void resize_preserving(int rows, int columns) { + if (rows < 1 || rows > 200 || columns < 1 || columns > 500) + throw std::invalid_argument("invalid screen dimensions"); + if (rows == height()) { resize_columns_preserving(columns); return; } + require_content_revision(); + if (order_revision_ == (std::numeric_limits::max)()) + throw std::overflow_error("screen order revision exhausted"); + struct Prepared { + std::vector live, outgoing; + size_t trim = 0, discarded = 0; + int row = 0, column = 0; + }; + auto identity = next_id_; + const auto prepare = [&](const auto& state, size_t limit, bool hidden) { + Prepared next; + const auto shift = static_cast((std::max)(0, state.row - rows + 1)); + const auto evicted = limit ? shift - (std::min)(shift, limit - state.history.size()) : shift; + if (evicted > (std::numeric_limits::max)() - state.discarded) + throw std::overflow_error("screen discarded count exhausted"); + next.discarded = state.discarded + evicted; + next.trim = limit ? evicted : 0; + next.row = state.row - static_cast(shift); + next.column = (std::min)(state.column, columns - 1); + const auto fit = [&](Line& line, bool live) { + if (hidden && line.size() > static_cast(columns)) { + stamp_line(line); + materialize_columns(line, columns, Cell{}); + line.wrap_columns = (std::min)(line.wrap_columns, static_cast(columns)); + } + if (live && line.size() < static_cast(columns)) line.resize(columns); + }; + if (limit) { + next.outgoing.reserve(shift); + for (size_t index = 0; index < shift; ++index) { + next.outgoing.push_back(state.lines[index]); + fit(next.outgoing.back(), false); + } + } + next.live.reserve(rows); + const auto end = (std::min)(state.lines.size(), shift + static_cast(rows)); + for (size_t index = shift; index < end; ++index) { + next.live.push_back(state.lines[index]); + fit(next.live.back(), true); + } + while (next.live.size() < static_cast(rows)) + next.live.push_back(make_line(columns, Cell{}, identity)); + return next; + }; + auto next = prepare(*this, active_history_limit(), false); + std::optional hidden; + if (inactive_) + hidden = prepare(*inactive_, active_buffer_ == ScreenBuffer::primary ? 0 : history_limit, true); + + // Only primary owns history. Deque growth can allocate blocks or its + // pointer map. Append before publishing, and undo every appended row on + // failure; existing history nodes and all live state remain untouched. + // This bounds copied bodies, not deque map work or elapsed time. + static_assert(std::is_nothrow_move_constructible_v); + auto& primary_history = active_buffer_ == ScreenBuffer::primary ? history : inactive_->history; + auto& primary_next = active_buffer_ == ScreenBuffer::primary ? next : *hidden; + size_t appended = 0; + try { + for (auto& line : primary_next.outgoing) { + primary_history.push_back(std::move(line)); + ++appended; + } + } catch (...) { + while (appended) { primary_history.pop_back(); --appended; } + throw; + } + // Every operation from here is nonthrowing. Trimming cannot invalidate + // the source of a later allocation, and both frames publish together. + const auto commit = [](auto& state, Prepared& prepared) noexcept { + for (size_t index = 0; index < prepared.trim; ++index) state.history.pop_front(); + state.lines.swap(prepared.live); + state.row = prepared.row; state.column = prepared.column; + state.discarded = prepared.discarded; + state.wrap_pending = false; + state.last_row = state.last_column = -1; + state.scroll_top = 0; state.scroll_bottom = -1; + }; + commit(*this, next); + if (hidden) commit(*inactive_, *hidden); + columns_ = columns; next_id_ = identity; + ++content_revision_; ++order_revision_; + } // Physical-row policy: no reflow, no pulling old history into the viewport. // Both frames share geometry. Prepare both before committing so invalid // dimensions or allocation failure cannot leave one frame half-resized. From 4d24c792cf8721167df3bbdeb4c0f768babfc81c Mon Sep 17 00:00:00 2001 From: JavaZeroo-Bot <133958205+JavaZeroo-Bot@users.noreply.github.com> Date: Tue, 3 Nov 2026 00:55:55 +0800 Subject: [PATCH 6/8] test(core): replay the combined original resize on the retained owner The literal five-stage WindTerm 2.7.0 width/height observation previously passed 8/20 phases on the existing width-preserving plus legacy height route. Selecting the retained owner now passes 20/20 with 744 mapping and selection assertions, so both this replay and the transaction sweep are registered as CTest gates. Co-authored-by: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> --- CMakeLists.txt | 2 ++ experiments/core/resize_combined_test.cpp | 17 ++++++----------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f132dace..4ab61248 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -434,6 +434,8 @@ add_test(NAME projection_job_core COMMAND projection_job_test) set_tests_properties(projection_job_core PROPERTIES TIMEOUT 10) add_test(NAME resize_height_measured_core COMMAND resize_height_measured_test) set_tests_properties(resize_height_measured_core PROPERTIES TIMEOUT 10) +add_test(NAME resize_combined_core COMMAND resize_combined_test) +set_tests_properties(resize_combined_core PROPERTIES TIMEOUT 10) add_test(NAME resize_preserving_transaction_core COMMAND resize_preserving_transaction_test) set_tests_properties(resize_preserving_transaction_core PROPERTIES TIMEOUT 60) add_test(NAME retained_resize_unicode_core COMMAND resize_unicode_test) diff --git a/experiments/core/resize_combined_test.cpp b/experiments/core/resize_combined_test.cpp index e2d792f1..f6a8c7cf 100644 --- a/experiments/core/resize_combined_test.cpp +++ b/experiments/core/resize_combined_test.cpp @@ -142,15 +142,10 @@ static std::optional complete_text_range(const TextDocument& document return std::nullopt; } -static void resize_existing_path(Screen& screen, const Stage& stage) { - // Exercise the existing real APIs. Width and height change in separate - // stages; the height path must not be replaced by an invented stub. - if (screen.width() != stage.columns) { - if (screen.height() != stage.rows) throw std::runtime_error("fixture changes two axes at once"); - screen.resize_columns_preserving(stage.columns); - } else if (screen.height() != stage.rows) { - screen.resize(stage.rows, stage.columns); - } +static void resize_retained_path(Screen& screen, const Stage& stage) { + // The literal inputs and expected observations are unchanged from the + // archived existing-path failure; only the owner transaction is selected. + screen.resize_preserving(stage.rows, stage.columns); } static void replay(Checks& checks, ParseMode mode, bool bytewise) { @@ -185,7 +180,7 @@ static void replay(Checks& checks, ParseMode mode, bool bytewise) { const auto literal_before = checks.literal_failures; const auto engineering_before = checks.engineering_failures; if (phase == 0) feed(parser, source, bytewise); - else resize_existing_path(parser.screen, stage); + else resize_retained_path(parser.screen, stage); checks.expect(parser.screen.width() == stage.columns && parser.screen.height() == stage.rows, "protocol grid differs"); feed(parser, "\x1b[6n", bytewise); @@ -265,6 +260,6 @@ int main() { std::cout << "separate mapping/selection engineering checks: " << checks.engineering_passed_phases << '/' << checks.phases << " phases, " << checks.engineering_failures << " failed assertions of " << checks.engineering_assertions << " (1 original case x 5 stages x strict/recovery x whole/bytewise; " - "existing width-preserving + legacy-height route; no original internal-history or Qt claim)\n"; + "retained-owner geometry transaction; no original internal-history or Qt claim)\n"; return checks.literal_failures || checks.engineering_failures ? 1 : 0; } From 1eb4efcf31569fcc06a72f4a8502e4f10f4323bf Mon Sep 17 00:00:00 2001 From: JavaZeroo-Bot <133958205+JavaZeroo-Bot@users.noreply.github.com> Date: Tue, 3 Nov 2026 00:59:04 +0800 Subject: [PATCH 7/8] docs(core): record the retained combined resize replay and its limits Record the reproduced 8/20 legacy failure, the 20/20 retained-owner replay and the allocation-failure sweeps. The report states that the default resize and Qt remain on the physical path and makes no performance or full #27/#29 acceptance claim. Co-authored-by: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> --- docs/EVIDENCE.md | 1 + docs/FEATURES.md | 2 +- .../2026-11-02-retained-combined/README.md | 50 +++++++++++ .../validation.json | 82 +++++++++++++++++++ 4 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 experiments/core/results/2026-11-02-retained-combined/README.md create mode 100644 experiments/core/results/2026-11-02-retained-combined/validation.json diff --git a/docs/EVIDENCE.md b/docs/EVIDENCE.md index 389393a9..361ad496 100644 --- a/docs/EVIDENCE.md +++ b/docs/EVIDENCE.md @@ -11,6 +11,7 @@ | 保留正文、Unicode投影与编辑的分配置整合回归 | [当前固定源码与前后身份](../experiments/core/results/2026-11-02-projection-job/README.md):源码`ef1da6e`,核心31/31;Qt/固定ConPTY默认路径4/4。[旧30/4](../experiments/core/results/2026-11-02-resize-caret/README.md)、[旧28/4](../experiments/core/results/2026-11-02-display-projection-unicode/README.md)及其历史记录保持原身份 | 两配置独立。66份选定源码先冻结,35个实际测试EXE及产品/辅助程序/部署依赖运行后固定,非完整构建输入manifest。后续纯高度单项1/1单列;Qt仍未接新保留/投影,B/CD不豁免,不是完整R2或性能验收 | | 增量显示投影与跨步骤源身份 | [旧停滞诊断、13工程用例及身份](../experiments/core/results/2026-11-02-projection-job/README.md):500单列片段按200/200/100步完成一行,跨200边界反向选择/复制/Find一致,取消/同身份导入/旧owner等拒绝 | 新API无先行RED;旧诊断exit0表示复现停滞,归档于运行后。每步仍限200,不发布半页;源锚点非全局视觉行号。持续输出可能反复取消,任意定位/Qt调度未交付 | | 原版窗口高度分配及纯高度行为 | [独立两状态几何](../research/ui/results/2026-11-02-resize-geometry-reference/README.md)、[三阶段原版正文/DSR](../research/ui/results/2026-11-02-resize-height-measured-reference/README.md)和[核心12阶段回放](../experiments/core/results/2026-11-02-height-measured/README.md):window749→569实际改变180px,终端区域少120px;20→14→20行时正文不变,DSR19→14→14、scroll0→5→5 | 原10行预测失败保留,不回填缺失窗口测量;不把两状态比例推广。原top=null且非完整grid,scroll不证明内部物理历史。核心首轮夹具source_end越界保留,修夹具后1/1;没有生产修复或宽高组合/Qt验收 | +| 自主 C++ 共 owner 宽高组合保留事务(#27/#29 显式入口) | [原版五阶段记录](../research/ui/results/2026-11-02-resize-combined-reference/README.md)、[字面回放、分配失败 sweep 与身份](../experiments/core/results/2026-11-02-retained-combined/README.md):`13186bf` 的旧"宽度保留 + 旧高度"路径 8/20 阶段、80/560 断言失败;`resize_preserving` 后 20/20、0/560,映射/选择 20/20、744 断言零失败;活动主屏/备用屏缩增各 151/42 次注入失败后重试一致 | 固定 75 B ASCII 主屏五阶段、后续仅 DSR;只走显式入口。默认 `Screen::resize` 与 Qt 仍物理裁剪,隐藏主屏、Unicode、resize 后写入、历史淘汰、Qt/固定 ConPTY 与性能未验收;工程不变量不是原版 oracle | | 自主 C++ 区域续接、历史结构通知与每屏 origin | [模式交叉与红绿](../experiments/core/results/2026-11-02-r27-mode-cross/README.md)、[每屏 origin 修正](../experiments/core/results/2026-11-02-origin-frame/README.md)、[原版固定控制对照](../research/ui/results/2026-11-02-mode-state-reference/README.md) | 区域自动换行的跨行复制/Find、RIS/ED3 结构通知、Unicode/分块/重复/异常输入与独立主屏 origin 恢复;当前物理 resize 策略测试不冒充原版匹配,完整 #27 继续开放 | | Qt / 固定 ConPTY 模式与正文 resize 差异 | [独立字面预期及失败运输证据](../experiments/shell/results/2026-11-02-qt-terminal-modes/README.md) | 模式阶段经公开 Qt 按键和原生网格核对;缩窄后原生 reflow 与核心物理裁剪不同步,独立运输捕获未见重排字节。失败诊断与模式回归分列,#29 的正文重排和完整 #27 不由基础尺寸通过代替 | | 固定 ConPTY resize 与输出机制 | [1.25.260930003 包身份及 pinned 源码核查](research/conpty-resize-output-2026-11-02.md) | 当前 0x18 是字宽,旧 resize quirk 已删除;signal 成功非完成屏障,内部 reflow 不保证重发整屏,DSR 光标同步不证明正文一致。静态解释不替代同次 Qt 动态证据或原版 oracle;本次未新增运行或改 flags | diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 85824179..a21020de 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -56,7 +56,7 @@ Qt 构建显式准备并部署[固定微软 CRT 14.51.36247.0](../third_party/ms | 中文 IME 与键盘 | **限定验收**:基础按键/修饰键/UTF-16 与实际接收字节;[固定 ZWJ/keycap 输入](../experiments/shell/results/2026-10-26-unicode-keyboard/validation.json)在默认/候选各两轮精确接收。固定 Microsoft 拼音[产品场景](../research/ime/results/2026-10-24-product-ime-contexts/validation.json)覆盖两区域组合/提交/取消、候选、输出/resize/zoom/受控 DPI、隐藏/关闭、历史阅读和原生控件 | 限定 Windows Server 2025 固定拼音及已有场景;同次组合由八会话工作流另验收。固定字素绘制/选择已由 #39 限定完成,但所有输入法、应用键盘模式、完整 IME 和真实不同 DPI 跨屏仍未完成 | R1 / R2 | | VT 核心 | **实现中,未验收**:默认自研支持基础光标/擦除(含 ECH)、CR/LF/BS/TAB、有限 SGR(粗体/暗色/下划线/反显/删除线/隐藏/上划线)和有界恢复;新增编辑类 `ICH`/`DCH`(`CSI @`/`CSI P`)、`IL`/`DL`(`CSI L`/`CSI M`)、滚动区域 `DECSTBM`(`CSI Pt;Pb r`,归位与非法区域行为按参考实现)、`ED3`(`CSI 3J` 只清回滚)与 `RIS`(`ESC c`),`DSR 6n` 回复、`DECCKM`(`?1`)与 `DECTCEM`(`?25`)已接入 Win32 与 Qt 两个入口,均有[核心契约测试](../experiments/core/edit_csi_test.cpp)与[回复测试](../experiments/core/reply_test.cpp)。显式候选 `.4` 接入 `Term/Processor`、C++ 拥有值适配、稳定行元数据、每缓冲区 margin 与流式字素;[共同轨迹](../experiments/differential/results/2026-10-26-candidate-common-v6/summary.json)及[源 margin 合同](../experiments/shell/results/2026-10-26-margin-state/validation.json)为各自固定版本结果 | [#27](https://github.com/JavaZeroo/termforge/issues/27) 的 origin/wrap、模式/resize、插删与滚动区域交叉未闭合;[#41](https://github.com/JavaZeroo/termforge/issues/41) 系统 ConPTY 已丢失的 margin 不能由末端补回。ECH pending-wrap 差异见[限定合同](research/more-capture-ech.md);`DA1` 仅 strict 回放回复 VT102,recovery 产品路径不回复;`DA2` 与 `OSC 10/11` 回复未实现,原版未知仍不算匹配;`SGR 58` 等下划线颜色属性亦未实现;#36/#38、完整呈现和平台支持独立验收 | R2 / R4 | | Unicode 与文字显示 | **实现中,未验收**:默认自研保留 Unicode 15。显式候选 `.4` 的 [#39 Unicode 17 流式字素合同](research/streaming-graphemes.md)已限定验收:固定 ZWJ/keycap 的单 lead/两列、光标、SGR/查询、原子选择复制、单列 resize 与有界无损分片;实际 ConPTY/DWrite 核对固定样本的单 glyph、字体和网格 | [#30](https://github.com/JavaZeroo/termforge/issues/30) 仍需完整输入/IME 交叉;所有脚本/整形、通用 fallback、所有原生重放、真彩色/更多属性、Powerline 和真实跨屏未验收。分段 helper 的规范数据通过不代表完整终端 Unicode 正确性;[#54](https://github.com/JavaZeroo/termforge/issues/54) 保留已观察的性能成本 | R2 / R4 | -| resize 与 reflow | **实现中,未验收**:默认仍按 ADR-0005 物理行裁剪;[显式保留列宽事务与显示投影](../experiments/core/results/2026-11-02-retained-columns/README.md)通过四例有限寻址诊断,尚未替换默认 resize 或接入 Qt。历史候选在[固定 ConPTY 重放](../experiments/core/results/2026-10-26-resize-replay/validation.json)与连续同步中保持行身份、Find/选区和有界历史,淘汰/清空不复活;候选 `.4` 另覆盖主/备用屏缩到单列 | [ADR-0010](adr/0010-bounded-resize-retention.md)已确认正文保留与显示投影方向,#27/#29 联合交叉仍未完成;[正式路径 B/CD 缩放诊断](../experiments/shell/results/2026-11-02-qt-terminal-modes/README.md)仍红,固定事务不等于完整 reflow 或 WindTerm 等价。[#156固定交错诊断](../experiments/shell/results/2026-10-29-resize-repaint-witness/README.md)见证默认核心在两种时序下选区失效,历史几何错配已有字节/消费网格证据;[#194](https://github.com/JavaZeroo/termforge/issues/194) 后续按用户接受未复现收口,未证明产品修复,重现时按该项条件重开。系统/实验 ConPTY、备用屏/滚动区、真实 DPI、呈现、性能及 #36/#38 分别验收;默认缩窄不可恢复 | R2 | +| resize 与 reflow | **实现中,未验收**:默认仍按 ADR-0005 物理行裁剪;[显式保留列宽事务与显示投影](../experiments/core/results/2026-11-02-retained-columns/README.md)通过四例有限寻址诊断;[同 owner 宽高组合保留事务](../experiments/core/results/2026-11-02-retained-combined/README.md)把固定五阶段原版回放从 8/20 阶段修至 20/20,并注册核心门禁,尚未替换默认 resize 或接入 Qt。历史候选在[固定 ConPTY 重放](../experiments/core/results/2026-10-26-resize-replay/validation.json)与连续同步中保持行身份、Find/选区和有界历史,淘汰/清空不复活;候选 `.4` 另覆盖主/备用屏缩到单列 | [ADR-0010](adr/0010-bounded-resize-retention.md)已确认正文保留与显示投影方向,#27/#29 联合交叉仍未完成;[正式路径 B/CD 缩放诊断](../experiments/shell/results/2026-11-02-qt-terminal-modes/README.md)仍红,固定事务不等于完整 reflow 或 WindTerm 等价。[#156固定交错诊断](../experiments/shell/results/2026-10-29-resize-repaint-witness/README.md)见证默认核心在两种时序下选区失效,历史几何错配已有字节/消费网格证据;[#194](https://github.com/JavaZeroo/termforge/issues/194) 后续按用户接受未复现收口,未证明产品修复,重现时按该项条件重开。系统/实验 ConPTY、备用屏/滚动区、真实 DPI、呈现、性能及 #36/#38 分别验收;默认缩窄不可恢复 | R2 | | 本地 shell 选择 | **实现中,未验收**:`ShellLaunch` 分离可执行文件、参数、工作目录及 shell 命令差异;cmd、Windows PowerShell 5.1、PowerShell 7、显式 Ubuntu WSL 各有[两轮真实启动/目录/尺寸/正常退出证据](../experiments/shell/results/2026-10-24-r2-shell-lifecycle/validation.json),无效程序启动错误可见且可关闭后恢复 cmd。四 shell 测试已补 [WSL 原生尺寸程序返回后的新提示符同步](research/wsl-size-return.md),正常退出证据只在实际全部回收后写入;默认/system 的事件反例、原两轮及完整回归分别留证 | 仅声明上述固定入口和隔离目录;#80 原 hosted stage6 是否同因仍未知,#81 hosted 准备超时另行定位;任意 profile/参数、通用分屏/排序、全部运行时错误提示及管理员 shell 未完成 | R2;权限场景 R4 | | 常用 TUI | **实现中,未验收**:显式候选 + 系统 ConPTY 有固定 Vim 进入、编辑、60×12 resize、退出写盘、主屏恢复及光标回复的[产品证据](../experiments/core/CANDIDATE.md);More 尾部 ECH 与捕获端点有[限定修复](research/more-capture-ech.md) | [#28](https://github.com/JavaZeroo/termforge/issues/28) 的更多应用及完整输入/输出模式未完成;PR #50 实验后端的 GUI Vim [#52](https://github.com/JavaZeroo/termforge/issues/52) 仍有失败;[native 捕获目录复用](research/vim-fixture-isolation.md)已独立定位并修正,不能代替 GUI 或完整 TUI 验收。headless 比较与自有 DIB 不等于真实呈现,`presented_done=null` | R2 | diff --git a/experiments/core/results/2026-11-02-retained-combined/README.md b/experiments/core/results/2026-11-02-retained-combined/README.md new file mode 100644 index 00000000..a45878ab --- /dev/null +++ b/experiments/core/results/2026-11-02-retained-combined/README.md @@ -0,0 +1,50 @@ +# 同 owner 宽高组合保留事务与五阶段字面回放 + +[原版五阶段样本](../../../../research/ui/results/2026-11-02-resize-combined-reference/README.md)在 `91×20 → 61×20 → 61×14 → 61×20 → 91×20` 中保持完整正文,DSR 为 `19;7 → 19;7 → 14;7 → 14;7 → 14;7`。本切片把该观察建立为独立字面回放,并在现有 Screen owner 内新增 `resize_preserving(rows, columns)`:高度变化时把移出视口的前缀行交给主屏历史,存活行保留源 Cell、身份与软续接;两帧在准备完成后一起发布,分配失败不发布任何一帧。 + +同一份字面预期下,旧"宽度保留事务 + 旧高度物理裁剪"路径为 **8/20 阶段、80/560 条断言失败、退出 1**;改为新事务后为 **20/20 阶段、零失败**,其中映射/选择工程检查从 8/20、60/648 变为 **20/20、744 条断言零失败**。断言总数增加不是放宽分母,而是失败消失后执行了后续映射/选择检查。 + +**默认 `Screen::resize` 与 Qt 仍走原物理裁剪路径,本切片只增加显式入口与核心回归门禁。** 原版观察、核心回放、Qt/固定 ConPTY 与 CI 仍是不同配置;本页不声明 #27/#29、完整 reflow 或性能验收。 + +## 源码与验证身份 + +- 基线 `8eec75d0b47570681850747e070e2d3e1d8a8ae3`(本次任务开始时 `origin/main`)。 +- 旧路径失败复现源码 `13186bff79bb522276364c2f397aab6398fd02f0`:回放调用既有 `resize_columns_preserving` + `Screen::resize`;实际构建并运行于本工作区。 +- 实现与回放源码 `eb09421`(事务,含分配失败检查)与 `4d24c792cf8721167df3bbdeb4c0f768babfc81c`(切换回放并注册两个 CTest 门禁)。本报告提交只增加文档,不改代码。 + +构建/运行环境:Windows 11 x64、Visual Studio 17 2022 17.12.6、MSVC 19.42.34433.0、Windows SDK 10.0.26100.0、Release、Qt OFF、候选适配器 OFF、Python 3.12.10、Node 24.22.2。定向构建后收集源码与 EXE SHA-256,见 [validation.json](validation.json)。 + +## 结果 + +| 验证 | 实际结果 | 口径 | +|---|---|---| +| 旧路径组合回放(`13186bf`) | **8/20 阶段、80/560 失败**,映射/选择 8/20、60/648 失败,退出 1 | 在本次工作区重新构建并运行;失败阶段为缩高、恢复高、恢复宽 | +| 新事务组合回放(`4d24c79`) | **20/20 阶段、0/560 失败**;映射/选择 **20/20、0/744 失败**,退出 0 | 同一字面预期、同一 75 B 源;仅替换尺寸事务调用 | +| 事务分配失败 sweep | 活动主屏缩/增各 **151/42** 次注入失败后重试成功;活动备用屏(隐藏主屏)同 | 每次失败后检查两帧、历史、身份、守卫与后续附着;重试结果与无故障一致 | +| 历史预算/空操作/越界 | 限额 0/3/10 的内容、淘汰与 `discarded` 一致;同尺寸不分配;非法尺寸与可达计数溢出不发布状态 | 工程不变量,不是时延或分配性能测量 | +| 增长分配与历史长度无关 | 8 与 4000 行历史下,增高事务的分配次数与字节相同,旧历史地址不变 | 只证明准备路径不遍历/复制历史 | +| 核心 CTest | **34/34 通过,4.52 秒** | 含 `resize_combined_core` 与 `resize_preserving_transaction_core`;时间不是性能结果 | +| 组合探针离线 CTest | **3/3 通过,44.63 秒** | `windterm_resize_combined_reference`、`_optimized`、`_controller`;不启动原版 | + +回放覆盖 strict/recovery × 整块/逐字节四次执行,每次五阶段,共 20 阶段。每阶段分别检查协议宽高、DSR、完整 19 行逻辑文档、选择复制全文、源位置/显示位置映射和高亮;没有前缀匹配或跳过失败阶段。 + +## 复现命令 + +```powershell +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build --config Release --target resize_combined_test resize_preserving_transaction_test --parallel 4 +ctest --test-dir build -C Release -R "_core$" --output-on-failure +ctest --test-dir build -C Release -R "windterm_resize_combined" --output-on-failure +& build/bin/Release/resize_combined_test.exe +& build/bin/Release/resize_preserving_transaction_test.exe +``` + +旧路径失败可用 `13186bf` 的 `resize_existing_path` 版本重建 `resize_combined_test`;该调用已被后续提交替换,本报告不把旧 EXE 当作新验证。 + +## 边界与未完成 + +- 只覆盖固定 75 B ASCII 主屏样本与五组固定尺寸、后续仅 DSR;不覆盖 resize 后继续输出、双轴同时变化、备用屏活动、隐藏主屏原版对照、Unicode、历史淘汰或保存光标交叉。 +- 新事务是显式入口;默认 `Screen::resize`、Qt 呈现、选择/Find UI 与固定 ConPTY 运输尚未切换。Qt 的 B/CD 缩放失败未被本切片修复或豁免。 +- 分配失败检查与历史预算检查是工程不变量;原版内部历史行数、分配器和时间成本均未测量。 +- `resize_preserving` 的隐藏帧拟合沿用早先"隐藏 live 仅保留协议足迹"策略,不是本次新证实的原版行为;隐藏主屏原始审计仍为失败,见 #27 记录。 +- #27/#29 保持 OPEN;本切片不以核心回放成功代替原版、Qt 或 R2 联合验收。 diff --git a/experiments/core/results/2026-11-02-retained-combined/validation.json b/experiments/core/results/2026-11-02-retained-combined/validation.json new file mode 100644 index 00000000..1fa6d5f0 --- /dev/null +++ b/experiments/core/results/2026-11-02-retained-combined/validation.json @@ -0,0 +1,82 @@ +{ + "schema": "termforge-retained-combined-validation-v1", + "date": "2026-11-02", + "status": "explicit-owner-transaction-and-combined-replay-passed", + "default_resize_replaced": false, + "qt_projection_integrated": false, + "r27_complete": false, + "r29_complete": false, + "implementation_commits": [ + "eb09421", + "4d24c792cf8721167df3bbdeb4c0f768babfc81c" + ], + "failed_baseline_commit": "13186bff79bb522276364c2f397aab6398fd02f0", + "main_base_commit": "8eec75d0b47570681850747e070e2d3e1d8a8ae3", + "hashes_collected_after_runs": true, + "build": { + "platform": "Windows 11 x64", + "generator": "Visual Studio 17 2022 17.12.6", + "compiler": "MSVC 19.42.34433.0", + "windows_sdk": "10.0.26100.0", + "configuration": "Release", + "qt": false, + "candidate_adapter": false, + "python": "3.12.10", + "node": "24.22.2" + }, + "source_files_sha256": { + "CMakeLists.txt": "36b3358fe01cc06b909550eecb99666b8a60a7966bf3a9c200ca1af65e9dc2fd", + "experiments/core/screen.hpp": "910735615a453246e499f5fea07296b7e4b6451bc727bcd28324e3590b956769", + "experiments/core/resize_combined_test.cpp": "18f926588e8d4986e84f9f640f337da04df1c34a21768a07a99a3fb4df7a8d32", + "experiments/core/resize_preserving_transaction_test.cpp": "ece5da0a72676ee489f0524152bfbbcbe1f938f928570f4f0390e17cdbd410fb" + }, + "binary_sha256": { + "resize_combined_test.exe": "56213732e8bff541ec9f1a82693ac719f0b4ece01ca2ebcbac5eaf783b8100d5", + "resize_preserving_transaction_test.exe": "13ccd207ca33f80db63fecec96b0b7c049d05d263c8e2e65a566891b677c71eb" + }, + "original_runs_this_slice": 0, + "original_observation": "research/ui/results/2026-11-02-resize-combined-reference/README.md", + "combined_literal_replay": { + "configurations": 4, + "phases": 20, + "baseline": { + "phases_passed": 8, + "literal_failures": 80, + "literal_assertions": 560, + "engineering_failures": 60, + "engineering_assertions": 648, + "exit_code": 1 + }, + "after": { + "phases_passed": 20, + "literal_failures": 0, + "literal_assertions": 560, + "engineering_failures": 0, + "engineering_assertions": 744, + "exit_code": 0 + } + }, + "transaction_checks": { + "allocation_sweeps": { + "primary_shrink": 151, + "primary_grow": 42, + "alternate_shrink": 151, + "alternate_grow": 42 + }, + "history_limits": [0, 3, 10], + "growth_allocation_history_rows": [8, 4000], + "retry_matches_uninterrupted": true, + "no_timing_claim": true + }, + "ctest": { + "core": {"passed": 34, "failed": 0, "seconds": 4.52}, + "combined_probes": {"passed": 3, "failed": 0, "seconds": 44.63, "launches_original": false} + }, + "limitations": [ + "Fixed 75 B ASCII main-primary sample and five fixed geometries only; no writes after resize, dual-axis change, active alternate frame, Unicode, history eviction or saved-cursor cross.", + "Explicit entry only: default Screen::resize and Qt still use physical clipping; Qt B/CD resize failures remain.", + "Hidden-frame fitting follows the earlier engineering footprint policy, not a newly confirmed original behavior; the hidden-primary original audit still failed.", + "Allocation and history checks are engineering invariants, not allocator, latency or memory acceptance.", + "No Qt/fixed-ConPTY transport, user path, or performance measurement in this slice." + ] +} From 890c2c59a20aa06c0b9d76c0d4e98e5f51d0b693 Mon Sep 17 00:00:00 2001 From: JavaZeroo-Bot <133958205+JavaZeroo-Bot@users.noreply.github.com> Date: Tue, 3 Nov 2026 01:00:41 +0800 Subject: [PATCH 8/8] docs(core): point the core README at the retained resize entries Co-authored-by: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> --- experiments/core/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/experiments/core/README.md b/experiments/core/README.md index ffc5a568..377c56d9 100644 --- a/experiments/core/README.md +++ b/experiments/core/README.md @@ -4,7 +4,7 @@ 解析器提供默认 `strict` 和显式 `ParseMode::recovery` 两种模式。replay/receive、trace 默认值和共同子集差分使用严格模式,本地 shell 选择恢复模式;测试 trace 可用第七个参数 `--recovery` 比较恢复行为,或 `--resize=RxC` 在输入结束点 resize 并追加检查点。[恢复策略](RECOVERY.md)定义未知控制序列、非法 UTF-8、超长输入、取消及 EOF 的有界处理;下表未支持输入的失败规则适用于严格模式。 -`Screen::resize` 采用[物理行裁剪契约](../../docs/adr/0005-physical-row-resize.md)。`resize_test` 通过根 CTest 在 Windows/Linux 检查内容、历史、宽字符与字节流边界;本地 shell 将它与窗口/ConPTY 尺寸同步,replay CLI 不执行隐式 resize。 +`Screen::resize` 采用[物理行裁剪契约](../../docs/adr/0005-physical-row-resize.md)。`resize_test` 通过根 CTest 在 Windows/Linux 检查内容、历史、宽字符与字节流边界;本地 shell 将它与窗口/ConPTY 尺寸同步,replay CLI 不执行隐式 resize。显式保留入口 `resize_columns_preserving` 与 `resize_preserving` 不替换该默认契约:前者由 `retained_columns_core` 检查,后者的高度保留及五阶段组合回放见[组合保留结果](results/2026-11-02-retained-combined/README.md)。 ## 支持范围和约定