From d0fa8c5c9e6f878a0cf30c16c5ce006392838791 Mon Sep 17 00:00:00 2001 From: fy59 Date: Tue, 13 Oct 2026 08:24:56 +0200 Subject: [PATCH] docs(mosaic): preserve static RGB input and standard return evidence --- CHANGELOG.md | 2 + docs/ARCHITECTURE.md | 14 + docs/KNOWN_LIMITATIONS.md | 14 + docs/README.md | 14 + docs/ROADMAP.md | 14 + ...TEP20C_STATIC_RGB_INPUT_STANDARD_RETEST.md | 377 ++++++++++++++++++ .../step20c-static-rgb-input-retest/README.md | 95 +++++ .../collect.py | 73 ++++ .../ipc_predicates.py | 139 +++++++ .../step20c-static-rgb-input-retest/pixels.py | 71 ++++ .../prepare.py | 75 ++++ .../step20c-static-rgb-input-retest/replay.py | 154 +++++++ .../step20c-static-rgb-input-retest/run.py | 258 ++++++++++++ .../support.py | 90 +++++ .../test-budget.py | 31 ++ .../validate_results.py | 129 ++++++ .../step20c-static-rgb-input-retest/verify.py | 19 + 17 files changed, 1569 insertions(+) create mode 100644 docs/design/STEP20C_STATIC_RGB_INPUT_STANDARD_RETEST.md create mode 100644 tooling/probes/step20c-static-rgb-input-retest/README.md create mode 100644 tooling/probes/step20c-static-rgb-input-retest/collect.py create mode 100644 tooling/probes/step20c-static-rgb-input-retest/ipc_predicates.py create mode 100644 tooling/probes/step20c-static-rgb-input-retest/pixels.py create mode 100644 tooling/probes/step20c-static-rgb-input-retest/prepare.py create mode 100644 tooling/probes/step20c-static-rgb-input-retest/replay.py create mode 100644 tooling/probes/step20c-static-rgb-input-retest/run.py create mode 100644 tooling/probes/step20c-static-rgb-input-retest/support.py create mode 100644 tooling/probes/step20c-static-rgb-input-retest/test-budget.py create mode 100644 tooling/probes/step20c-static-rgb-input-retest/validate_results.py create mode 100644 tooling/probes/step20c-static-rgb-input-retest/verify.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 0998064..f8b5e81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### STEP20C experimental evidence +- Preserved independent static RGB/input/standard retest: corrected shared predicate and real replay pass; exact whole-H2 RGB, H1 retained/H2 forbidden pixels, real target/reference clicks and targeted splith clip removal/input pass. FEASIBLE for this matrix. Terminal exit IPC makes runner exit 1 after measured gates and compositor exit 0; raw failure retained and offline diagnosis passes. No new compositor/library build or production change. + - Preserved static focus/input/standard fixture failure: exact static binary and loaded pair verified; one run stopped before captures/input on an incorrect border-width fixture assertion. INCONCLUSIVE / UNDECIDED; camera and clip traces retained, RGB/clicks/splith NOT TESTED, no retry or production change. - Approved private static candidate decisions (2026-10-13): separate notices, project maintenance and exact-pair qualification. Current package unchanged; private real `/usr` static build/model/staging/library identity/headless startup pass. Whole-H2 reference fixture failed after a focus change; input and splith return NOT TESTED. PARTIALLY_FEASIBLE / UNDECIDED, preserved historical evidence; the original manifests and fixture bytes remain unchanged. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 571db5b..5854560 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -416,3 +416,17 @@ PNG, click or splith transition. Those gates are NOT TESTED; no clipping defect or need for an algorithm change is demonstrated. No retry/build/production change occurred. A separately authorized corrected-fixture gate is required. STEP20C stays NEXT; production multi-output remains NOT YET SUPPORTED. + +The [static RGB/input/standard retest](design/STEP20C_STATIC_RGB_INPUT_STANDARD_RETEST.md) +is **EXPERIMENTAL / FEASIBLE for the tested matrix**; its evidence is frozen +for publication. +The corrected shared IPC predicate/replay pass before the sole unchanged-static- +binary run. Two full H2 references and all later H2 RGB are exact; H1 retains +182,214 target pixels, H2 zero, actual visible/reference/standard button pairs +are delivered only to the right client. Targeted splith removes the native clip +and retains ordinary pixels/input. The runner exits 1 only on terminal exit IPC +after completed measurements and compositor exit 0; that raw failure is retained +and separately diagnosed/revalidated offline. No algorithm, build, production, +package or UX change follows. Wider static popup/effect/lifecycle, complete +damage/generation, ABI/maintenance/notices gates remain open. D1–D4 persist; +STEP20C stays NEXT and production multi-output remains NOT YET SUPPORTED. diff --git a/docs/KNOWN_LIMITATIONS.md b/docs/KNOWN_LIMITATIONS.md index 3234ef8..e3e7131 100644 --- a/docs/KNOWN_LIMITATIONS.md +++ b/docs/KNOWN_LIMITATIONS.md @@ -357,3 +357,17 @@ PNG, click or splith transition. Those gates are NOT TESTED; no clipping defect or need for an algorithm change is demonstrated. No retry/build/production change occurred. A separately authorized corrected-fixture gate is required. STEP20C stays NEXT; production multi-output remains NOT YET SUPPORTED. + +The [static RGB/input/standard retest](design/STEP20C_STATIC_RGB_INPUT_STANDARD_RETEST.md) +is **EXPERIMENTAL / FEASIBLE for the tested matrix**; its evidence is frozen +for publication. +The corrected shared IPC predicate/replay pass before the sole unchanged-static- +binary run. Two full H2 references and all later H2 RGB are exact; H1 retains +182,214 target pixels, H2 zero, actual visible/reference/standard button pairs +are delivered only to the right client. Targeted splith removes the native clip +and retains ordinary pixels/input. The runner exits 1 only on terminal exit IPC +after completed measurements and compositor exit 0; that raw failure is retained +and separately diagnosed/revalidated offline. No algorithm, build, production, +package or UX change follows. Wider static popup/effect/lifecycle, complete +damage/generation, ABI/maintenance/notices gates remain open. D1–D4 persist; +STEP20C stays NEXT and production multi-output remains NOT YET SUPPORTED. diff --git a/docs/README.md b/docs/README.md index 7a40490..1c04225 100644 --- a/docs/README.md +++ b/docs/README.md @@ -275,3 +275,17 @@ PNG, click or splith transition. Those gates are NOT TESTED; no clipping defect or need for an algorithm change is demonstrated. No retry/build/production change occurred. A separately authorized corrected-fixture gate is required. STEP20C stays NEXT; production multi-output remains NOT YET SUPPORTED. + +The [static RGB/input/standard retest](design/STEP20C_STATIC_RGB_INPUT_STANDARD_RETEST.md) +is **EXPERIMENTAL / FEASIBLE for the tested matrix**; its evidence is frozen +for publication. +The corrected shared IPC predicate/replay pass before the sole unchanged-static- +binary run. Two full H2 references and all later H2 RGB are exact; H1 retains +182,214 target pixels, H2 zero, actual visible/reference/standard button pairs +are delivered only to the right client. Targeted splith removes the native clip +and retains ordinary pixels/input. The runner exits 1 only on terminal exit IPC +after completed measurements and compositor exit 0; that raw failure is retained +and separately diagnosed/revalidated offline. No algorithm, build, production, +package or UX change follows. Wider static popup/effect/lifecycle, complete +damage/generation, ABI/maintenance/notices gates remain open. D1–D4 persist; +STEP20C stays NEXT and production multi-output remains NOT YET SUPPORTED. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 9254cde..634b0c9 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -361,3 +361,17 @@ PNG, click or splith transition. Those gates are NOT TESTED; no clipping defect or need for an algorithm change is demonstrated. No retry/build/production change occurred. A separately authorized corrected-fixture gate is required. STEP20C stays NEXT; production multi-output remains NOT YET SUPPORTED. + +The [static RGB/input/standard retest](design/STEP20C_STATIC_RGB_INPUT_STANDARD_RETEST.md) +is **EXPERIMENTAL / FEASIBLE for the tested matrix**; its evidence is frozen +for publication. +The corrected shared IPC predicate/replay pass before the sole unchanged-static- +binary run. Two full H2 references and all later H2 RGB are exact; H1 retains +182,214 target pixels, H2 zero, actual visible/reference/standard button pairs +are delivered only to the right client. Targeted splith removes the native clip +and retains ordinary pixels/input. The runner exits 1 only on terminal exit IPC +after completed measurements and compositor exit 0; that raw failure is retained +and separately diagnosed/revalidated offline. No algorithm, build, production, +package or UX change follows. Wider static popup/effect/lifecycle, complete +damage/generation, ABI/maintenance/notices gates remain open. D1–D4 persist; +STEP20C stays NEXT and production multi-output remains NOT YET SUPPORTED. diff --git a/docs/design/STEP20C_STATIC_RGB_INPUT_STANDARD_RETEST.md b/docs/design/STEP20C_STATIC_RGB_INPUT_STANDARD_RETEST.md new file mode 100644 index 0000000..e89fdc8 --- /dev/null +++ b/docs/design/STEP20C_STATIC_RGB_INPUT_STANDARD_RETEST.md @@ -0,0 +1,377 @@ +# STEP20C static RGB, input and standard-return retest + +**EXPERIMENTAL / LOCAL / UNCOMMITTED. Protocol before fixture development.** +Phase A froze the historical failure in +`fb0293cd855a2ec917c14df3ecf3504c064499fc`, published to canonical Forgejo +and GitHub with a clean worktree. This independent phase B is never committed +or published. [Architecture](../ARCHITECTURE.md), [UX](../UX.md), +[Roadmap](../ROADMAP.md) and [Packaging](../PACKAGING.md) remain authoritative. +D1–D4 persist. STEP20A DONE; STEP20B EXPERIMENTAL; STEP20C NEXT; STEP20D–G +PLANNED. Production multi-output remains NOT YET SUPPORTED. + +## Historical error and mandatory offline gates + +The [frozen follow-up](STEP20C_STATIC_RUNTIME_FOCUS_INPUT_STANDARD_PROBE.md) +stopped on an invalid `current_border_width == 0` assertion, before any PNG, +click or standard return. Its verdict remains INCONCLUSIVE / UNDECIDED and +its run/camera budget is exhausted. Neither its runner nor manifests change. + +Pinned SwayFX `fd71a6bdc061bd633b488ae7b83e8a6981d22f86` border.c sets +B_NONE and only changes thickness with an optional second argument. The exact +candidate's ipc-json.c reports stored current.border_thickness; transaction.c +sets effective B_NONE border width/top to zero. Therefore width 2 is accepted +as recorded metadata under border none. A shared pure predicate validates the +actual IPC object, identity, ancestors, visibility, border mode, empty deco_rect, +full rect/window_rect and exact H2 geometry. The runner imports the same function +as the offline tests. No parallel implementation is permitted. + +Read-only replay uses the sealed +`/home/fy59/swayfx-mosaic-static-followup-nhwejz6u/results/preflight-snapshot.json`, +scene traces, client logs and earlier real IPC observations. Historical IDs +7/8/9 and PIDs are used only in replay. Negative tests alter explicit copies: +wrong App ID, PID, ID, workspace, output, visibility, border, deco, absent view, +window/content/output geometry, focus, camera and clip must be rejected. +An actual earlier border-normal reference must also fail. + +`STATIC_REFERENCE_IPC_REPLAY=PASS` and `STATIC_FIXTURE_PREFLIGHT_REPLAY=PASS` +are mandatory before any run reservation. Historical PNGs from the older static +runtime may validate pixel/coordinate code, clearly labelled HISTORICAL; the +failed follow-up contains no PNG. Synthetic button fixtures are explicitly +SYNTHETIC, never new input evidence. Tests check exact BTN_RIGHT=273 press/release, +positive real-format serials, timestamps/order, event count, malformed lines, +wrong recipient and absence in both other clients. No events may be silently +discarded by the button parser. + +## Assertion authority and remaining measurement + +| Field / assertion | Source and meaning | Offline evidence / live requirement | +| --- | --- | --- | +| outputs rect/scale/transform | ipc-json output description, logical coordinates | actual snapshots, wrong-output negatives; reread live | +| App ID/PID/con_id/ancestors | real view/node and tree ownership | actual reference/target/filler; live IDs discovered by PID | +| visible/focused/workspaces | view_is_visible and default-seat focus; not pixel coverage | actual tree plus get_workspaces; sample live | +| border/width | current border enum and stored thickness | B_NONE width 2 accepted; normal rejected | +| rect/deco_rect/window_rect | node box with title removed; decoration and content-relative box | actual full H2, empty deco; pixels still require capture | +| scroll_x | Mosaic row state, may precede applied scene | compare IPC with native PI_SCENE after settling | +| PI_OWNER scene/applied_size | wlr_scene_node_coords, animation current size | parse actual target trace; never use IPC rect as buffer y | +| PI_SCENE local/global clip | applied tiling tree, private get_clip | output origin + tiling translation + local clip equals viewport | +| client COMMIT size/color | exact C17 SHM draw, configure/ACK loop | actual logs; target horizontal width compared with IPC content | +| SHM colors | XRGB8888 target 0x2344cc/0x20d0a0; reference 0x303038 | source and historical PNG; live captures required | +| capture coordinates | scale 1/normal, local x=global x-output origin | signed-coordinate tests and measured pixels; no approximate native y | +| click point | 41×41 actual-color interior, margin 20; H2 continuation | historical image and negative sparse fixtures; recompute live | +| BUTTON log | real pointer callback, ns/serial/button/state | positive/negative parser tests; live delivery mandatory | +| splith criteria owner | layout.c uses matched container workspace; Mosaic clears parent target | exact candidate source audited; standard trace/pixels/input live | + +The machine-readable assertion audit records each source location, historical +input, replay result and uncertainty. Assertions depending on new RGB/input or +standard-return observations remain explicitly live requirements; historical +or synthetic success cannot qualify them. No presentation hypothesis is an +invariant. True output/ownership/input/clip invariants remain required. + +## Exact candidate, isolation and immutable runner + +Reuse only +`/home/fy59/swayfx-mosaic-static-runtime-hm5kbdkt/stage-static/usr/bin/swayfx-mosaic`: +SHA-256 `f82557c1a9978a5e9665b9b4291b74880253725359e3df1c5b8d554921f7603b`, +build ID `942db64269640778ee433e5970ba1100bc60f94c`. +Check regular executable, ELF64 x86-64, private clip get/set ordinary ELF definitions (not necessarily .dynsym exports), DT_NEEDED, +no SceneFX shared dependency, ldd -r and the exact system wlroots SHA-256 +`7edca9a7d65855addda68e29ba5db94d57c803eb5fa7d0f86db3136b90ca9837`. +Copy unchanged into one new campaign-owned laboratory outside Git. Compile +only the three historical C17 clients from manifest-verified exact C/protocol +sources; retain sources, commands and binary hashes. No SceneFX/Mosaic build. + +The prepared laboratory has one exclusive owner file and persistent flock +ledger. Offline gates, source audit, AST, C17, library identity, ledger tests, +documentation and the exact runner inventory must all pass before reservation. +Freeze hashes of the executed fixture and verify them immediately before run. +Do not edit that copy afterward. Reports/collector receipts are separate. + +Private runtime mode 0700, headless backend, private WAYLAND_DISPLAY and +explicit private swaymsg -s only. Strip inherited desktop sockets/library +overrides. Own process groups, bounded logs and 150-second watchdog. Retain +logs/exit codes before removing only owned sockets/runtime. No daily IPC. + +## One-run ordered protocol + +HEADLESS-1=(-800,0,800,600), HEADLESS-2=(0,0,800,600), scale 1, normal. +No popup, rotation, fractional scaling, resize, migration, saved-buffer scenario +or added effect. Set focus_follows_mouse no. Create H2 static reference, discover +its real PID/con_id, and apply only `[con_id=ID] border none`. H1 filler shares +its App ID; never apply a global App ID rule. Create H1 target and establish +Mosaic with target/workspace 1 focused before any H2 reference PNG. + +Measure applied scene and client content width. Compute desired left=-width/2 +and signed camera delta from that actual state, reserve once and apply. Do not +hard-code +160. Confirm IPC/applied scroll, owner crossing x=0, workspace identity +and global clip [-800,0,800,600]. No second adjustment if later focus moves it. + +Park pointer on H1 (-790,590), stabilize and capture H2 twice at least 500 ms +apart. Require 800×600, fixed #303038 throughout and exact full RGB equality; +retain PNG hashes. No tolerance, crop or excluded ROI. Difference stops all +dependent tests and records every differing pixel. + +Capture H1/H2 Mosaic. Require positive measured target paint on H1, continuation +across x=0, zero target colors on H2 and full H2 RGB equality. Client COMMIT, +painted bbox and scene-derived horizontal continuation are separate observations. +Select visible click from actual target-colored H1 pixels with 20-pixel margins. +Select H2 point in geometric continuation, within the actual full reference +surface, at least 20 pixels from borders. One right press/release to visible +target; exact client traces, no delivery to other clients. One right click to H2; +zero new target/filler events, actual reference pair, record focus and camera. +Park pointer on H1, recapture whole H2 and require exact equality after focus. + +Send `[con_id=targetID] layout splith`, even when global focus is H2. Require +same H1 workspace, actual splith, mapped target, absent H1/H2 tiling clips, +ordinary target pixels and zero forbidden target pixels on H2. Capture both +outputs and compare whole H2. Recompute standard target interior; third right +click must deliver real target pair only. Park pointer, capture final H2 and +check focus-transition RGB. No reuse of Mosaic point without measurement. + +## Budget, classification and preservation + +Limits: static run 1, pristine 0, Mosaic build 0, SceneFX build 0, camera 1, +right clicks 3 (one of each named kind), movements 8, PNG 12, duration 150 s. +Every click also moves the pointer and consumes a movement reservation. Reserve +before run/capture/click/camera/move; failed work consumes reservations, no reset, +refund or second laboratory. Expected PNG 8, movement 6 including click motions. + +FEASIBLE requires all exact candidate/replay/reference/Mosaic/input/standard +gates and no crash/protocol blocker in this precise matrix. PARTIALLY_FEASIBLE +retains demonstrated properties with open paths. INCONCLUSIVE denotes fixture +measurement failure. BLOCKED requires measured clipping/input/ABI/standard +defect, not an invalid fixture assertion. Architecture is respectively +STATIC_MOSAIC_RGB_INPUT_STANDARD_FEASIBLE_FOR_TESTED_MATRIX, +STATIC_MOSAIC_RGB_INPUT_STANDARD_REQUIRES_REDESIGN, or UNDECIDED. + +Retain exact fixture/client/config, raw PNG/IPC/protocol/client/scene logs, +timestamps, serials, library mappings and exit codes outside Git. Produce all +requested result JSON and seal source/evidence inventories once, excluding +manifests themselves. Verify every entry read-only. Damage extents, client +damage requests and final PNGs do not prove intermediate frames or native +output-buffer identity. Wider geometry, popup/grabs, lifecycle/effects/buffers, +applied-generation consistency, complete ABI/maintenance/notices remain open. +No algorithm change or STEP20C integration follows automatically. + +The [source-only fixture](../../tooling/probes/step20c-static-rgb-input-retest/README.md) +contains replay, predicates, ledger, runner, pixel analysis and collector. + +## Preparation incident before run reservation + +The first preparation in the single new LAB stopped because the checker used +`nm -D` for private static clip functions. The exact historical symbol receipt +qualifies ordinary executable T definitions, and `nm` confirms both. This is +a checker error, not a missing function or ABI failure. Preserve the original +checker and dynamic-symbol log; continue only that same LAB's pre-run preparation, +with no budget reservation/reset and no compositor launch. The corrected gate +uses ordinary ELF symbols. The initial checker and incident are evidence files; +the subsequently frozen/executed runner is explicitly distinct. + +## Completed single campaign — 2026-10-13 + +`SWAYFX_MOSAIC_STATIC_RGB_INPUT_STANDARD_PROBE=FEASIBLE` + +Architecture: +**STATIC_MOSAIC_RGB_INPUT_STANDARD_FEASIBLE_FOR_TESTED_MATRIX**. +This is a scientific verdict on the requested static, tiled, two-output matrix; +it does not mean the runner command exited zero, nor close STEP20C production. +LAB `/home/fy59/swayfx-mosaic-static-rgb-input-retest-hn67lnhq`. +Run duration 9.142914 seconds, one run, no retry. No pristine run, no new +SceneFX/Mosaic build, no binary/shader/algorithm/production/package/UX change. + +### Candidate, clients and offline gates + +The original and copied executable both retain SHA-256 +`f82557c1a9978a5e9665b9b4291b74880253725359e3df1c5b8d554921f7603b` +and build ID `942db64269640778ee433e5970ba1100bc60f94c`, regular executable +mode 0755, ELF64 x86-64. Ordinary ELF clip get/set T definitions pass; these +private static definitions need not appear in .dynsym. No SceneFX DT_NEEDED, +RPATH/RUNPATH, unresolved relocation or loaded shared SceneFX exists. Actual +process maps identify the unchanged system wlroots with SHA-256 +`7edca9a7d65855addda68e29ba5db94d57c803eb5fa7d0f86db3136b90ca9837`. +The historical full source/evidence inventories and follow-up 24/46-entry +inventories were verified read-only. No historical manifest was rewritten. + +Three C17 client-only syntax checks and builds passed from exact verified +historical C/generated-protocol inputs; commands are in raw/commands.jsonl: + +| Client | Result executable SHA-256 | +| --- | --- | +| saved-client | `3d5d0c5e446b995c210006a5bafe0d86dd5f32a228e1b7137d12af6ec9e217da` | +| static-reference | `11166ea1bb7cb3bb08f10ab93e091c5c5ea8687649b5fb0705288c7ca91680e8` | +| pointer-daemon | `76ad0f093f2b7a97359920b7bd456cfa93a1581c73875547a86eb911f2ed2d7a` | + +The corrected shared predicate accepted the actual historical H2 node with +border none / stored width 2, full window_rect and zero deco_rect. Thirty-nine +negative cases rejected wrong identities/ownership/geometry/focus/clip, the +actual filler, an actual earlier normal-border reference, sparse/inaccessible +pixel fixtures and invalid synthetic button sequences/recipients. Historical +PNG and synthetic input observations remain explicitly labelled. Both required +replay markers passed, with AST, shell recipes, links and durable concurrent/ +exhausted/corrupt-ledger tests before reservation. + +The exact runner SHA-256 is +`07ce1f98c9cb35ddb5219aff3991f19321759ac4edb26ab6d354096a853979d6`. +The pre-run fixture inventory (10 source files plus protocol) has 11 entries, +SHA-256 `294c001b5e88d7759255afb59646a386a59e2273f21e19c5a29512a4230f56a2`. +The executed copy remains immutable. The live runner imports the same validators +as replay. No width-zero assertion was relaxed after launching the compositor. + +### Applied ownership, border, camera and clip + +Both real outputs match (-800,0,800,600) / (0,0,800,600), scale 1, normal. +Reference con_id 7/PID 1700453 belonged to workspace 2 (ID 5), H2. Filler ID 8/ +PID 1700459 kept normal border; target ID 9/PID 1700462 belonged to workspace 1 +(ID 6), H1. These IDs were discovered at runtime, never hard-coded in the runner. +Target/workspace 1 initially focused; reference unfocused. Only `[con_id=7] +border none` affected H2. IPC retained width 2, rect/window_rect 800×600 and +zero deco_rect; full RGB then independently established absent rendered decoration. + +Applied scroll initially 0. Actual target horizontal content left -158, width +636, desired left -318 gave the sole signed correction +160. This was computed +again from actual state, not borrowed as a constant. IPC and scene both read +160. Owner scene (-320,0,640,600), content continuation [-318,318), actual client +COMMIT 636×573. Tile-local translation (-160,0), local clip (160,0,800,600), H1 +origin (-800,0) yield global clip (-800,0,800,600). H2 tiling clip is absent. +Native buffer y was never inferred from window_rect.y. + +### Whole RGB and measured input + +The two 800×600 H2 references, captured 600 ms apart after H1 focus/setup and +pointer parking, are entirely #303038 and exact RGB equals. Every subsequent +H2 PNG is also entirely equal: Mosaic, after H2 click/focus, standard layout, +and after standard target click/focus. Six H2 PNGs share SHA-256 +`4471e93ccb6cf3ff4970f61fce405431a228af598d2f1f030a3b7586962ecd7a`. +Five independent whole-frame comparisons each have zero changed pixels; no +crop, excluded ROI, RGB tolerance or focus compensation. + +H1 Mosaic contains 182,214 target-colored pixels, solid painted box local +[482,25,800,598), global x [-318,0), against continuation [-318,318). +H2 has zero target-colored pixels plus independently exact whole RGB. +H1 Mosaic PNG SHA-256: +`1ca64ee4aac80dd8377609528598608a412c1169d75bd4bce93b394292919ab8`. + +| Actual injection | Recipient and real serials | Other client buttons | Focus afterward | +| --- | --- | --- | --- | +| Mosaic visible (-159,311) | target, BTN_RIGHT 273 press 35/release 36 | reference/filler none | target 9, workspace 1 | +| Mosaic continuation on H2 (100,311) | reference, press 48/release 49 | target/filler none | reference 7, workspace 2 | +| Standard recomputed (-200,311) | target, press 67/release 68 | reference/filler none | target 9, workspace 1 | + +Each point uses measured captured pixels and a 41×41 interior neighborhood; +H2 also lies within the un-clipped target's measured horizontal continuation. +Client logs retain ns timestamps, real serials, enter/motion and protocol traces. +All target logs contain exactly four button lines (visible + standard pairs), +reference exactly two, filler zero. Camera remains 160 after both Mosaic clicks. +Reference focus changes without any H2 RGB difference. + +After reference focus, `[con_id=9] layout splith` keeps H1 workspace ID 6, +changes its actual layout to splith and leaves the target mapped. commands.c +binds handler context to matched container.pending.workspace; layout.c clears +container targeting when that workspace is Mosaic. This source audit preceded +execution. Native PI_SCENE reports layout enum 1, tile translation (0,0), +clip_valid=0 on both H1 and H2. H1 standard has 226,908 target pixels, local +[402,25,798,598), configured 396×573, with zero target pixels on H2. H1 standard +PNG SHA-256 `195c4faea3020af31819214346772339f62a34c8a82386afb3176ddb9391ee1a`. +The standard click point was recalculated from this image. Actual standard input, +clip removal, layout, ownership and rendered pixels pass together. + +### Terminal fixture incident, preserved raw status + +The executed runner's final `swaymsg -s PRIVATE exit` returned 1 after all +measurements and `standard-return.json` had been written. It therefore exited +1 and wrote runtime.status=INCONCLUSIVE. Those bytes/statuses are preserved, +not changed to PASS. The compositor log shows Handling command exit, orderly +shutdown and compositor code 0. All three clients exited 0, logging errno 32 +(EPIPE) only after the final capture; pointer was owned and terminated -15. +No wl_display.error protocol event or pre-measurement transport failure occurs. + +Exact source is compatible with the terminal observation: cmd_exit invokes +sway_terminate(0), which terminates the display event loop; ipc_send_reply queues +its write for a later writable callback. Closing the compositor may remove the +socket before swaymsg reads that reply. This is an observed terminal fixture +incident, not a clipping/input/standard-layout failure. Loss of the queued IPC +response is a compatible explanation, not an independently observed packet +trace or a universal assertion about every exit command. Its exact response error +text was not retained separately because subprocess.check=True raised; the +CalledProcessError, command/code and compositor/client logs remain available. + +A separate post-run validator rechecks actual PNGs, IPC/scene, recipient event +lists, focus, clip absence, budget, library identity and cleanup without any +runtime invocation. Negative post-run cases reject abnormal compositor exit, +retained standard clip, invisible-target delivery and even one changed H2 pixel. +Only all measured gates plus this narrow terminal-source/log diagnosis permit +scientific FEASIBLE. The collector and validator are retained under postrun-fixture, +separate from immutable fixture. No new run, workaround or budget refund. + +### Budget, damage, integrity and validation + +| Resource | Consumed / maximum | +| --- | --- | +| Static runs | 1/1 | +| Pristine runs | 0/0 | +| SceneFX / Mosaic builds | 0/0 each | +| Camera correction | 1/1 | +| Right clicks | 3/3, one per named kind | +| Pointer motions, including click positioning | 6/8 | +| PNGs | 8/12 | +| Duration | 9.142914 / 150 seconds | + +Persistent ledger remains consumed. Unused moves/PNGs never authorize another +run. The single runtime `/tmp/msr-26spytfz` is removed; all owned process groups +are reaped and PIDs absent. No private socket remains. Protected system binary, +wlroots/SceneFX libraries, AGENTS/UX/package/patch hashes remain unchanged. +No daily IPC/session/QuickShell/dotfile/Codex change or STEP20D work. + +271 TEMPORAL_DAMAGE lines cover setup, transitions and cleanup. Actual clients +request full SHM damage on commits, and stabilized PNGs are consistent. This +is PARTIAL damage evidence: no complete intermediate-frame coverage, native +output-buffer/PNG identity or atomic transaction/input generation is proved. +Popups/grabs, scaling/rotation, resize/migration, extra effects and saved-buffer +scenarios are NOT TESTED in this new campaign. Exhaustive semantic ABI, other +library pairs, notice/legal closure and named release/security roles remain open. + +Final source inventory: 31 entries, SHA-256 +`cb812ae3a27c9715efce2e1b2cfd36f315ea897d003c77944f688bc931861f1e`. +Final evidence inventory: 93 entries, SHA-256 +`209196b2857381db7a856347dcce45cd3ee33c48c2db0c73777fc982d01ad568`. +Both sealed once and every entry verified. Final report/documentation/Git diff +receipts are outside those inventories to avoid circular digests. Repository +README result additions are later documentation, not the executed source copy. + +Validation: AST, shell, Markdown links, historical manifests, IPC reference/ +preflight replay, 39 negatives, ledger concurrency/exhaustion/corruption, C17 +sources/builds, exact binary/loader/maps, actual outputs/camera/clip/pixels/buttons/ +focus/splith, protected hashes and residual-process/socket checks PASS. Run +command FAIL at terminal exit IPC only; scientific gates PASS after independent +artifact revalidation. Meson compile/test, ordinary headless suite and pristine +run NOT RUN (outside this ticket and prohibited new builds/runs). Parent source, +contract and final-delta audit performed; no independent subagent review claimed. + +### Architectural answers and next bounded gate + +| Question | Evidence-backed answer | +| --- | --- | +| Q1 corrected none/width 2? | Yes, accepted in actual historical and live snapshots; mode/deco/content still checked. | +| Q2 other assertions replayed? | Yes for available IPC/scene/COMMIT; pixel selector uses historical PNG; buttons synthetic negatives; live-only states then actually measured. | +| Q3 two whole references identical? | Yes, 480,000 RGB pixels, zero differences and same PNG SHA. | +| Q4 H2 unchanged during Mosaic? | Yes, whole RGB exact. | +| Q5 permitted H1 pixels retained? | Yes, 182,214 actual target pixels. | +| Q6 forbidden H2 pixels absent? | Yes, zero target colors and full reference equality. | +| Q7 visible click correct? | Yes, actual target serials 35/36 only. | +| Q8 invisible portion excluded? | Yes, no new target/filler event on the H2 click. | +| Q9 H2 receives click? | Yes, reference serials 48/49. | +| Q10 focus graphical change? | No H2 change across both actual focus transitions; no claim about all compositor decorations. | +| Q11 splith removes clip? | Yes, actual H1 workspace splith and native H1/H2 clip_valid=0. | +| Q12 standard input? | Yes, recalculated target point, actual serials 67/68 only. | +| Q13 static retains shared-private behavior? | Tested tiled/render/input/standard continuity is supported; exhaustive static/shared equivalence and broader effects/popups are not proved. | +| Q14 algorithm change justified? | No reproduced clipping defect; both incidents belong to fixture checks/terminal handling. | +| Q15 remaining integration limits? | Static popup/grab/effects/buffer continuity, wider geometry/lifecycle, complete damage/generation, exhaustive ABI/maintenance/notices/release ownership. | + +Smallest next justified experiment: a separately authorized same-static-binary +root/nested valid-grab popup RGB/input/standard-return continuity gate. Do not +extend this exhausted campaign or integrate clipping in production. STEP20C +remains NEXT and production multi-output NOT YET SUPPORTED. At the start of the evidence-freeze ticket, local main, Forgejo and GitHub +all pointed to fb0293cd855a2ec917c14df3ecf3504c064499fc. This report and the +source-only fixture are frozen for a single evidence-publication commit. The +separate new static nested-grab campaign may start only after that commit is +verified on both remotes with a clean worktree, and remains uncommitted and +unpublished. The historical runtime and three sealed inventories are immutable. diff --git a/tooling/probes/step20c-static-rgb-input-retest/README.md b/tooling/probes/step20c-static-rgb-input-retest/README.md new file mode 100644 index 0000000..b3eda0a --- /dev/null +++ b/tooling/probes/step20c-static-rgb-input-retest/README.md @@ -0,0 +1,95 @@ +# Static RGB, input and standard-return retest fixture + +EXPERIMENTAL / FROZEN EVIDENCE. No production integration. +Read the [protocol](../../../docs/design/STEP20C_STATIC_RGB_INPUT_STANDARD_RETEST.md). +The historical failed campaign is frozen and must never be rerun. + +`ipc_predicates.py` supplies pure validators shared by replay and the real runner. +`replay.py` uses actual sealed historical IPC/scene/client observations and +clearly labelled historical PNGs or synthetic input fixtures. Negative tests +must reject wrong identities/ownership/geometry/focus/clip and malformed input. +`pixels.py` compares every RGB pixel, preserves differences, measures actual +paint and selects points with a 20-pixel interior margin. `support.py` and +`test-budget.py` implement/test durable exclusive no-refund reservations. + +Offline preparation, client-only C17 checks and gates: + +```sh +PYTHONDONTWRITEBYTECODE=1 python tooling/probes/step20c-static-rgb-input-retest/test-budget.py +PYTHONDONTWRITEBYTECODE=1 python tooling/probes/step20c-static-rgb-input-retest/prepare.py +# LAB is the one newly printed path, registered in the exclusive owner file. +PYTHONDONTWRITEBYTECODE=1 python "$LAB/fixture/replay.py" --lab "$LAB" +``` + +Preparation copies the exact qualified static binary, verifies the unchanged +historical manifests, builds only exact historical clients and freezes the +fixture inventory. No Mosaic or SceneFX build is allowed. `replay.py` verifies +source authority, syntax and ledger tests and records the two mandatory PASS +markers. `run.py` verifies the immutable inventory and all gates before its +first run reservation. Never replace the owner file, reset the ledger, modify +the executed fixture or create a second lab to retry. + +Only after every gate passes, execute the single retained runner: + +```sh +PYTHONDONTWRITEBYTECODE=1 python "$LAB/fixture/run.py" "$LAB" +PYTHONDONTWRITEBYTECODE=1 python "$LAB/fixture/collect.py" "$LAB" +PYTHONDONTWRITEBYTECODE=1 python "$LAB/fixture/verify.py" "$LAB" +``` + +One static run, zero pristine runs/builds, one camera correction, three named +right clicks, eight movements including click positioning, twelve PNGs and +150 seconds. Reservations precede work and remain consumed on failure. +Headless/private mode-0700 runtime only, explicit swaymsg -s, owned process +groups, bounded logs and cleanup. No inherited daily IPC or library override. +The collector seals source/evidence once, verification never rewrites them. +Generated C protocols, clients/binaries/builds/logs/PNG/sockets stay outside Git. +Actual observations and final verdict are recorded below after collection. + +## Post-run collection distinction + +The exact executed fixture remains immutable in LAB/fixture. The measurements +completed, but its final `swaymsg exit` returned 1 after normal compositor exit. +Raw runtime.json and runner code/status are preserved. `validate_results.py` +independently rechecks actual PNG, IPC/scene, real recipient events, focus, +layout/clip and cleanup, and rejects changed RGB, invisible delivery, retained +clip or abnormal compositor exit. It accepts terminal transport only after all +scientific measurements, explicit exit, compositor code 0 and terminal EPIPE. +The post-run collector is retained separately in LAB/postrun-fixture, with its +own source inventory. Run that collector/validator for this completed campaign; +do not substitute it for the historical executed fixture or rerun the compositor. + +## Measured result (one run, no retry) + +LAB `/home/fy59/swayfx-mosaic-static-rgb-input-retest-hn67lnhq`. +Scientific FEASIBLE for the exact matrix, architecture +STATIC_MOSAIC_RGB_INPUT_STANDARD_FEASIBLE_FOR_TESTED_MATRIX. The executed runner +exited 1 only on final exit IPC; compositor exited 0 after every measured gate. +Raw failure is retained, never rewritten as a successful runner execution. +271 damage traces are partial evidence, not every-frame qualification. + +H2 six PNGs are whole-RGB identical; 182,214 target pixels on H1, zero H2. +Actual target visible serials 35/36, reference 48/49, standard target 67/68; +wrong clients have no button delivery. Real H1 splith/clip removal measured, +standard H1 paint 226,908 pixels. Budget run 1, camera 1, PNG 8, clicks 3, +movements 6 including click positioning, duration 9.142914 seconds. No second run. + +Source manifest 31 entries SHA-256 +`cb812ae3a27c9715efce2e1b2cfd36f315ea897d003c77944f688bc931861f1e`; +evidence 93 entries SHA-256 +`209196b2857381db7a856347dcce45cd3ee33c48c2db0c73777fc982d01ad568`. +Every entry verifies; runner/pre-run inventory and post-run collector are +separate retained sources. Result additions here are later documentation. + +Read-only final validation (no recording into sealed results): + +```sh +PYTHONDONTWRITEBYTECODE=1 python "$LAB/postrun-fixture/validate_results.py" "$LAB" +PYTHONDONTWRITEBYTECODE=1 python "$LAB/postrun-fixture/verify.py" "$LAB" +``` + +No production readiness, broad static/shared equivalence, popup/effect/buffer +qualification, full damage/generation or exhaustive ABI approval. STEP20C NEXT, +production multi-output NOT YET SUPPORTED. The completed retest sources and report are preserved for publication. +Any later static nested-grab campaign is separate and remains local, uncommitted +and unpublished. No installation or system/session change. diff --git a/tooling/probes/step20c-static-rgb-input-retest/collect.py b/tooling/probes/step20c-static-rgb-input-retest/collect.py new file mode 100644 index 0000000..102d2f3 --- /dev/null +++ b/tooling/probes/step20c-static-rgb-input-retest/collect.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""Collect actual one-run evidence and seal inventories once. + +WHY: unexecuted cells remain NOT TESTED, never manufactured by collection. +CONTRACT: verdict uses real gate observations; a fixture failure is not a leak. +INVARIANT: source fixture and historical inventories remain untouched. +""" +import argparse +import json +from pathlib import Path +from support import save,sha +from verify import verify_inventory +from validate_results import check as check_actual_observations + +p=argparse.ArgumentParser();p.add_argument('lab',type=Path);a=p.parse_args();lab=a.lab.resolve() +assert (Path.home()/'swayfx-mosaic-static-rgb-input-retest-v1.owner').read_text().strip()==str(lab) +assert not (lab/'results/evidence-sha256.txt').exists(), 'already sealed' +verify_inventory(lab/'results/fixture-freeze-sha256.txt') +runtime=json.loads((lab/'results/runtime.json').read_text());budget=json.loads((lab/'results/budget.json').read_text()) +for row in runtime['process_exit_codes']:assert not Path('/proc',str(row['pid'])).exists(), 'owned PID residual' +assert not Path(runtime['runtime_removed']).exists(), 'private runtime residual' +protected=json.loads((lab/'results/protected-before.json').read_text()) +assert all(sha(p)==digest for p,digest in protected.items()), 'protected file changed' +save(lab,'protected-after',{'status':'PASS','hashes':protected}) +required=['binary-identity','source-provenance','ipc-replay','preflight-replay','assertion-audit', + 'output-geometry','focus-states','reference-state','reference-stability','camera-calibration', + 'scene-clips','painted-geometry','pixel-analysis','visible-input','forbidden-input','reference-input', + 'standard-return','damage-analysis'] +for name in required: + if not (lab/'results'/(name+'.json')).exists(): + save(lab,name,{'status':'NOT TESTED','reason':runtime['failure'],'stopping_stage':runtime['stopping_stage']}) +def read(name):return json.loads((lab/'results'/(name+'.json')).read_text()) +qualification=check_actual_observations(lab) +full=qualification['scientific_gates']=='ALL PASS' +save(lab,'scientific-qualification',qualification) +verdict='FEASIBLE' if full else 'INCONCLUSIVE' +# Classification does not turn a metadata/parser failure into a compositor defect. +for name in ['forbidden-pixels']: + if (lab/'results'/(name+'.json')).exists() and read(name).get('count',0)>0:verdict='BLOCKED' +if not full and verdict!='BLOCKED' and read('pixel-analysis').get('STATIC_MOSAIC_VISIBLE_PIXELS')=='PASS': + verdict='PARTIALLY_FEASIBLE' +architecture=('STATIC_MOSAIC_RGB_INPUT_STANDARD_FEASIBLE_FOR_TESTED_MATRIX' if full else + 'STATIC_MOSAIC_RGB_INPUT_STANDARD_REQUIRES_REDESIGN' if verdict=='BLOCKED' else 'UNDECIDED') +matrix={ + 'binary_identity':'PASS','source_provenance':'PASS exact historical C17 clients', + 'IPC_replay':'PASS','preflight_replay':'PASS','initial_focus':'PASS target/workspace 1', + 'effective_border':'PASS border none, stored width 2, empty deco and uniform full RGB', + 'outputs':'PASS negative-origin scale-1 normal', 'camera':'PASS calculated once and applied', + 'Mosaic_clip':'PASS applied local/global relationship plus RGB/input', + 'H2_reference':'PASS two full RGB images','H1_pixels':'PASS measured target paint', + 'H2_forbidden_pixels':0,'whole_H2_RGB':'PASS five complete comparisons', + 'visible_click':'PASS target real pair only','invisible_target':'PASS no event', + 'reference_click':'PASS reference real pair only','focus':'PASS H1 to H2 to H1', + 'standard_return':'PASS splith, clip absent, rendered pixels, real target pair', + 'damage':'PARTIAL client requests and output extents; intermediate frames NOT TESTED', + 'runner_exit':'FAIL exit 1 on terminal exit IPC; raw status preserved', + 'compositor_exit':'PASS code 0','terminal_transport':'observed EPIPE after completed measurements', + 'cleanup':'PASS','native_PNG_buffer_identity':'NOT PROVEN','popups':'NOT TESTED'} +save(lab,'matrix',matrix) +save(lab,'summary',{'verdict':verdict,'architecture':architecture,'runtime':runtime,'budget':budget, + 'scientific_qualification':qualification,'executed_runner_status_unchanged':True, + 'new_Mosaic_builds':0,'new_SceneFX_builds':0,'STEP20C':'NEXT','production_multi_output':'NOT YET SUPPORTED', + 'marker':'SWAYFX_MOSAIC_STATIC_RGB_INPUT_STANDARD_PROBE='+verdict}) +source=sorted(p for root in ['fixture','postrun-fixture','client-source'] for p in (lab/root).rglob('*') if p.is_file()) +source.append(lab/'protocol-before-run.md') +evidence=sorted(p for root in ['raw','run','results'] for p in (lab/root).rglob('*') if p.is_file()) +evidence += [lab/n for n in ['swayfx-mosaic','saved-client','static-reference','pointer-daemon','campaign-owner']] +# Capture both lists before writing either inventory, avoiding circular digests. +for name,files in [('source',source),('evidence',evidence)]: + with (lab/'results'/(name+'-sha256.txt')).open('x') as out: + for path in files:out.write(f'{sha(path)} {path}\n') + print(name,verify_inventory(lab/'results'/(name+'-sha256.txt'))) +print('SWAYFX_MOSAIC_STATIC_RGB_INPUT_STANDARD_PROBE='+verdict) diff --git a/tooling/probes/step20c-static-rgb-input-retest/ipc_predicates.py b/tooling/probes/step20c-static-rgb-input-retest/ipc_predicates.py new file mode 100644 index 0000000..df5a800 --- /dev/null +++ b/tooling/probes/step20c-static-rgb-input-retest/ipc_predicates.py @@ -0,0 +1,139 @@ +"""Pure validators for replay and live observations, never desktop IPC. + +WHY: stored border thickness and container IPC geometry are not rendered pixels. +CONTRACT: callers supply measured identities; validate ancestors and applied scene. +INVARIANT: no width-zero predicate, hard-coded runtime IDs, or native buffer y. +""" +import re + +OUTPUTS = {'HEADLESS-1': (-800, 0, 800, 600, 1.0, 'normal'), + 'HEADLESS-2': (0, 0, 800, 600, 1.0, 'normal')} +ZERO = dict(x=0, y=0, width=0, height=0) +FULL = dict(x=0, y=0, width=800, height=600) + + +def nodes(node): + yield node + for child in node.get('nodes', []) + node.get('floating_nodes', []): + yield from nodes(child) + + +def workspace(tree, name): + rows = [n for n in nodes(tree) if n.get('type') == 'workspace' and n.get('name') == name] + assert len(rows) == 1, 'workspace identity' + return rows[0] + + +def ancestry(tree, con_id, path=()): + if tree.get('id') == con_id: + return path + (tree,) + for child in tree.get('nodes', []) + tree.get('floating_nodes', []): + found = ancestry(child, con_id, path + (tree,)) + if found: + return found + return () + + +def validate_outputs(outputs): + actual = {o['name']: (*[o['rect'][k] for k in ('x', 'y', 'width', 'height')], + o['scale'], o['transform']) for o in outputs if o['active']} + assert actual == OUTPUTS, ('output geometry', actual) + assert len([o for o in outputs if o['active']]) == 2, 'duplicate output' + return actual + + +def validate_reference(snapshot, con_id, pid, workspace_name='2', output_name='HEADLESS-2'): + """B_NONE may retain width 2; require actual empty decoration/full content. + + IPC visibility means view_is_visible, not proof of SHM coverage. RGB is + independently required by the runner. Width is returned only as metadata. + """ + validate_outputs(snapshot['outputs']) + path = ancestry(snapshot['tree'], con_id) + assert path, 'reference surface absent' + ref = path[-1] + ws = next((n for n in path if n.get('type') == 'workspace'), None) + out = next((n for n in path if n.get('type') == 'output'), None) + assert ref.get('type') == 'con' and ref.get('shell') == 'xdg_shell', 'reference view absent' + assert ref.get('app_id') == 'org.mosaic.StaticReference', 'reference App ID' + assert ref.get('pid') == pid and pid > 0 and ref['id'] == con_id, 'reference PID/ID' + assert ws and ws['name'] == workspace_name and ws.get('output') == output_name, 'reference workspace' + assert out and out['name'] == output_name and out['rect'] == FULL, 'reference output' + assert ref.get('visible') is True, 'reference visibility' + assert ref.get('border') == 'none', 'reference border mode' + assert ref.get('rect') == FULL and ref.get('window_rect') == FULL, 'reference full content' + assert ref.get('deco_rect') == ZERO, 'effective reference decoration' + return {'status': 'PASS', 'con_id': con_id, 'pid': pid, 'workspace': ws['id'], + 'output': output_name, 'stored_border_width': ref.get('current_border_width'), + 'reference': ref} + + +def scene_from_log(text, output): + rows = [s for s in text.splitlines() if 'PI_SCENE output=' + output + ' ' in s + and 'workspace=none' not in s] + assert rows, 'applied scene trace absent' + line = rows[-1] + m = re.search(r'workspace=(\d+) layout=(\d+) scroll=(-?[\d.]+) tile_scene=(-?\d+),(-?\d+) clip_valid=(\d+) clip=(-?\d+),(-?\d+),(\d+),(\d+)', line) + assert m, 'scene trace format' + v = list(map(float, m.groups())) + origin = OUTPUTS[output][:2] + return {'workspace': int(v[0]), 'layout_enum': int(v[1]), 'scroll': v[2], + 'tile_local': v[3:5], 'clip_valid': bool(v[5]), 'clip_local': v[6:10], + 'global_clip': [origin[0] + v[3] + v[6], origin[1] + v[4] + v[7], v[8], v[9]], + 'raw': line} + + +def measure_owner(text, owner): + rows = [x for x in text.splitlines() if 'PI_OWNER output=HEADLESS-1 ' in x + and f' id={owner["id"]} ' in x] + assert rows, 'applied owner trace absent' + line = rows[-1] + m = re.search(r'scene_valid=1 scene=(-?\d+),(-?\d+) applied_size=(\d+),(\d+)', line) + assert m, 'owner trace format' + x, y, w, h = map(int, m.groups()) + win = owner['window_rect'] + return {'id': owner['id'], 'scene': [x, y, w, h], 'window_rect': win, + 'horizontal_content': [x + win['x'], win['width']], 'raw': line, 'IPC': owner} + + +def client_commit(text): + rows = [s for s in text.splitlines() if s.startswith('COMMIT ')] + assert rows, 'no real client commit' + m = re.fullmatch(r'COMMIT ns=(\d+) serial=(\d+) size=(\d+)x(\d+) color=([0-9a-f]{8})', rows[-1]) + assert m, 'client commit format' + ns, serial, w, h = map(int, m.groups()[:4]) + assert ns > 0 and 0 < serial <= 0xffffffff and 0 < w <= 4096 and 0 < h <= 4096 + return {'ns': ns, 'serial': serial, 'width': w, 'height': h, 'color': m[5], 'raw': rows[-1]} + + +def validate_preflight(snapshot, con_id, pid, target_id, target_pid, scene, h2_scene, measured, commit): + reference = validate_reference(snapshot, con_id, pid) + ws = workspace(snapshot['tree'], '1') + path = ancestry(snapshot['tree'], target_id) + assert path and ws in path, 'target workspace ownership' + owner = path[-1] + assert owner.get('app_id') == 'org.mosaic.SavedBufferProbe' and owner.get('pid') == target_pid + assert owner.get('shell') == 'xdg_shell' and owner.get('visible') is True, 'target mapped' + assert ws['output'] == 'HEADLESS-1' and ws['layout'] == 'mosaic', 'H1 Mosaic' + assert owner.get('focused') is True and reference['reference']['focused'] is False, 'initial focus' + assert [z['name'] for z in snapshot['workspaces'] if z['focused']] == ['1'], 'workspace focus' + scroll = ws['mosaic']['rows'][0]['scroll_x'] + assert scene['workspace'] == ws['id'] and scene['layout_enum'] == 5, 'applied Mosaic owner' + assert scene['scroll'] == scroll and scene['tile_local'] == [-scroll, 0], 'applied camera' + assert scene['clip_valid'] and scene['global_clip'] == [-800, 0, 800, 600], 'viewport clip' + assert scene['clip_local'] == [scroll, 0, 800, 600], 'local clip' + h2ws = workspace(snapshot['tree'], '2') + assert h2_scene['workspace'] == h2ws['id'] and not h2_scene['clip_valid'], 'H2 ordinary clip' + assert measured['id'] == target_id and measured['IPC'] == owner, 'measured owner identity' + x, w = measured['horizontal_content'] + assert -800 < x < 0 < x + w, 'target continuation crosses boundary' + assert w == commit['width'] == owner['window_rect']['width'], 'configured content width' + assert commit['height'] == owner['window_rect']['height'] and commit['color'] in ('002344cc', '0020d0a0') + return {'status': 'PASS', 'reference': reference, 'workspace': ws['id'], + 'target': target_id, 'scroll': scroll, 'scene': scene, 'H2_scene': h2_scene, + 'measured': measured, 'commit': commit} + + +def validate_camera(before_scroll, after_scroll, delta, scene): + assert type(delta) is int and -2147483647 <= delta <= 2147483647 + assert after_scroll == before_scroll + delta and scene['scroll'] == after_scroll, 'camera correction applied' diff --git a/tooling/probes/step20c-static-rgb-input-retest/pixels.py b/tooling/probes/step20c-static-rgb-input-retest/pixels.py new file mode 100644 index 0000000..d5d436f --- /dev/null +++ b/tooling/probes/step20c-static-rgb-input-retest/pixels.py @@ -0,0 +1,71 @@ +"""Exact whole-image comparisons and measured interior click selection. + +CONTRACT: no mask or tolerance may turn a whole-output difference into PASS. +INVARIANT: coordinates of every differing pixel are retained on failure. +""" +from PIL import Image, ImageChops +import re +COLORS = {(35,68,204),(32,208,160)} +def analyze(im): + mask=Image.new('L',im.size) + values=[255 if c in COLORS else 0 for c in im.get_flattened_data()] + mask.putdata(values) + return {'count':sum(v!=0 for v in values),'bbox':mask.getbbox()} +def compare(a,b): + assert a.size==b.size + points=[{'x':i%a.width,'y':i//a.width,'before':x,'after':y} + for i,(x,y) in enumerate(zip(a.get_flattened_data(),b.get_flattened_data())) if x!=y] + return {'equal':not points,'changed':len(points),'bbox':ImageChops.difference(a,b).getbbox(),'pixels':points} +def interior(im): + box=analyze(im)['bbox'];assert box + x1,y1,x2,y2=box;assert x2-x1>40 and y2-y1>40 + x,y=(x1+x2)//2,(y1+y2)//2 + assert all(im.getpixel((xx,yy)) in COLORS for xx in range(x-20,x+21) for yy in range(y-20,y+21)) + return x,y + +def button_pair(lines): + # CONTRACT: malformed or extra client button lines must never be filtered out. + events=[] + for line in lines: + m=re.fullmatch(r'BUTTON ns=(\d+) serial=(\d+) button=(\d+) state=(\d+)',line) + assert m, ('malformed button',line) + events.append(tuple(map(int,m.groups()))) + assert len(events)==2 and [x[2:] for x in events]==[(273,1),(273,0)],events + assert all(x[0]>0 and 0current.border_thickness)','c->pending.content_width','view_is_visible']), + ('sway/commands/layout.c',['workspace && workspace->layout == L_MOSAIC','container = NULL;','workspace->layout = new_layout']), + ('sway/desktop/transaction.c',['border_width = 0;','wlr_scene_tree_set_clip(child->layers.tiling, NULL)']), + ('sway/desktop/output.c',['wlr_scene_node_coords','PI_SCENE output=','PI_OWNER output='])] + sources=[] + for name,tokens in checks: + p=src/name;contents=p.read_text();assert all(t in contents for t in tokens),(p,tokens) + sources.append({'file':str(p),'sha256':sha(p),'semantics_checked':tokens}) + fields=[('outputs rect/scale/transform','sway/ipc-json.c','actual IPC','PASS','live reread'), + ('view App ID/PID/con_id/ancestors/visible','sway/ipc-json.c','actual IPC','PASS','visibility is not pixels'), + ('workspace/focus/layout','sway/ipc-json.c; sway/commands/layout.c','actual IPC','PASS','later focus live'), + ('border/stored width/deco/window_rect','border.c; ipc-json.c; transaction.c','actual IPC plus earlier normal','PASS','effective RGB live'), + ('camera scroll_x/scene tree/clip local/global','transaction.c; output.c; ipc-json.c','actual traces + calibration','PASS','no atomic generation asserted'), + ('content dimensions/SHM colors','client-source/saved-client.c','actual COMMIT + historical PNG','PASS','new pixels live'), + ('capture coordinates/painted click selection','pixels.py; virtual pointer source','HISTORICAL H1 + SYNTHETIC H2','PASS','new accessible surface verified live'), + ('buttons/recipient/count/order/serial','saved-client.c:pointer_button; pointer-daemon.c','SYNTHETIC labelled cases','PASS','new actual client events required'), + ('targeted splith return','sway/commands/layout.c','exact candidate source','PASS source audit','layout/clip/render/input live; NOT TESTED offline')] + audit=[dict(field=f,source=s,historical_data=h,replay=r,remaining_uncertainty=u, + invariant=f+' remains required live') for f,s,h,r,u in fields] + runtime_assertions=[] + runner=Path(__file__).with_name('run.py').read_text() + for node in ast.walk(ast.parse(runner)): + if isinstance(node,ast.Assert): + runtime_assertions.append({'line':node.lineno,'expression':ast.get_source_segment(runner,node.test), + 'authority':'see corresponding field/source table; operational assertions use owned files/processes', + 'offline':'shared predicates and historical/synthetic tests above; environment identity checked by prepare', + 'uncertainty':'live-only states require new observations; no retrospective PASS'}) + audit={'fields':audit,'runner_assertions':sorted(runtime_assertions,key=lambda x:x['line'])} + return {'reference':{'marker':'STATIC_REFERENCE_IPC_REPLAY=PASS','snapshot_sha256':sha(HIST/'results/preflight-snapshot.json'), + 'accepted_width':2,'reference':good['reference'],'negatives':results}, + 'preflight':{'marker':'STATIC_FIXTURE_PREFLIGHT_REPLAY=PASS','actual':good,'paint_HISTORICAL':paint, + 'points_HISTORICAL_SYNTHETIC':points,'synthetic_input_tests':'PASS'}, + 'audit':audit,'source_audit':sources} + +if __name__=='__main__': + p=argparse.ArgumentParser();p.add_argument('--lab',type=Path);a=p.parse_args() + result=check() + for path in Path(__file__).parent.glob('*.py'):ast.parse(path.read_text(),filename=str(path)) + subprocess.run(['python',str(Path(__file__).with_name('test-budget.py'))],check=True) + if a.lab: + from verify import verify_inventory + verify_inventory(a.lab/'results/fixture-freeze-sha256.txt') + save(a.lab,'ipc-replay',result['reference']);save(a.lab,'preflight-replay',result['preflight']) + save(a.lab,'assertion-audit',result['audit']);save(a.lab,'source-audit',result['source_audit']) + save(a.lab,'offline-gate',{'reference':'PASS','preflight':'PASS','AST':'PASS','ledger':'PASS','source_audit':'PASS'}) + print(result['reference']['marker']);print(result['preflight']['marker']) + print('PASS',len(result['reference']['negatives']),'negative cases; historical/synthetic evidence labelled') diff --git a/tooling/probes/step20c-static-rgb-input-retest/run.py b/tooling/probes/step20c-static-rgb-input-retest/run.py new file mode 100644 index 0000000..bf3334e --- /dev/null +++ b/tooling/probes/step20c-static-rgb-input-retest/run.py @@ -0,0 +1,258 @@ +#!/usr/bin/env python3 +"""One independent budgeted RGB/input/standard retest of the unchanged verified static compositor. + +WHY: resolved relocations do not demonstrate real compositor execution. +CONTRACT: verify /proc executable/library identity before mapping any client. +INVARIANT: owned sockets/groups only, one camera correction, durable captures. +""" +import argparse +import json +import os +from pathlib import Path +import re +import shutil +import signal +import subprocess +import tempfile +import threading +import time +from PIL import Image,ImageChops +from support import environment,reserve,save,sha +from pixels import analyze, compare, interior, painted_geometry, click_points, delivery +from ipc_predicates import (nodes, workspace, validate_outputs, validate_reference, + validate_preflight, measure_owner, scene_from_log, client_commit, validate_camera) +from verify import verify_inventory + +p=argparse.ArgumentParser();p.add_argument('lab',type=Path);a=p.parse_args();lab=a.lab.resolve() +assert not __import__('sys').flags.optimize, 'Python assertions must be enabled' +assert json.loads((lab/'results/binary-identity.json').read_text())['status']=='PASS' +assert sha(lab/'swayfx-mosaic')=='f82557c1a9978a5e9665b9b4291b74880253725359e3df1c5b8d554921f7603b' +assert (Path.home()/'swayfx-mosaic-static-rgb-input-retest-v1.owner').read_text().strip()==str(lab) +# CONTRACT: run reservation is impossible until the exact frozen fixture passes. +assert not __import__('sys').flags.optimize, 'Python assertions must be enabled' +verify_inventory(lab/'results/fixture-freeze-sha256.txt') +gate=json.loads((lab/'results/offline-gate.json').read_text()) +assert gate=={'reference':'PASS','preflight':'PASS','AST':'PASS','ledger':'PASS','source_audit':'PASS'} +assert json.loads((lab/'results/ipc-replay.json').read_text())['marker']=='STATIC_REFERENCE_IPC_REPLAY=PASS' +assert json.loads((lab/'results/preflight-replay.json').read_text())['marker']=='STATIC_FIXTURE_PREFLIGHT_REPLAY=PASS' +reserve(lab,'run');run=lab/'run';run.mkdir();runtime=Path(tempfile.mkdtemp(prefix='msr-',dir='/tmp')) +runtime.chmod(0o700);(run/'runtime-path').write_text(str(runtime)+'\n') +(run/'config').write_text('output HEADLESS-1 resolution 800x600 scale 1 transform normal position -800 0\noutput HEADLESS-2 resolution 800x600 scale 1 transform normal position 0 0\nworkspace 1 output HEADLESS-1\nworkspace 2 output HEADLESS-2\nfocus_follows_mouse no\nseat seat0 hide_cursor 100\n') +env=environment();env.update(XDG_RUNTIME_DIR=str(runtime),WLR_BACKENDS='headless',WLR_HEADLESS_OUTPUTS='2',WLR_RENDERER='pixman') +binary=lab/'swayfx-mosaic';processes=[];files=[];failure=None;compositor=None;events=[];stage='startup' +started=time.monotonic();finished=threading.Event();expired=threading.Event() + +def event(kind,**data): + row={'ns':time.monotonic_ns(),'kind':kind,**data} + path=run/'events.jsonl' + assert not path.exists() or path.stat().st_size<64*1024*1024, 'event output bound' + with path.open('a') as out:out.write(json.dumps(row)+'\n') + +def stop_owned(): + for proc in list(processes): + try:os.killpg(proc.pid,signal.SIGTERM) + except ProcessLookupError:pass + +def watchdog(): + while not finished.wait(.1): + if time.monotonic()-started>150 or any(x.stat().st_size>64*1024*1024 for x in run.glob('*.log')): + expired.set();stop_owned();return + +thread=threading.Thread(target=watchdog,daemon=True);thread.start() + +def spawn(name,argv,stdin=False,extra=None): + assert not expired.is_set() + out=(run/(name+'.log')).open('w');files.append(out) + proc=subprocess.Popen(list(map(str,argv)),env={**env,**(extra or {})},stdin=subprocess.PIPE if stdin else subprocess.DEVNULL,stdout=out,stderr=subprocess.STDOUT,text=True,start_new_session=True) + processes.append(proc);event('spawn',name=name,pid=proc.pid,argv=list(map(str,argv)));return proc + +def wait(predicate,seconds=8): + end=time.monotonic()+seconds + while time.monotonic()before) + time.sleep(.15);event('pointer',action=action,global_point=[x,y],target_tail=log('target').splitlines()[-6:] if (run/'target.log').exists() else []) + +def capture(tag,output): + reserve(lab,'capture');path=run/(tag+'.png') + subprocess.run(['grim','-s','1','-o',output,str(path)],env=env,check=True,timeout=5,stdout=subprocess.DEVNULL) + im=Image.open(path).convert('RGB');assert im.size==(800,600) + event('capture',file=path.name,output=output,sha256=sha(path));return im + +def measure(owner):return measure_owner(log('compositor'),owner) + +# WHY: global focus becomes H2 after the forbidden-region click. Workspace and +# owner IDs, rather than global command context, bind the standard-return test. +focus_states=[] +def snapshot(tag): + tree=ipc('-t','get_tree');outputs=ipc('-t','get_outputs');workspaces=ipc('-t','get_workspaces') + row={'tag':tag,'ns':time.monotonic_ns(),'tree':tree,'outputs':outputs,'workspaces':workspaces, + 'focused':[n['id'] for n in nodes(tree) if n.get('focused')]} + focus_states.append(row);save(lab,'focus-states',focus_states) + save(lab,tag+'-snapshot',row) + return tree + +def scene(output):return scene_from_log(log('compositor'),output) + +def camera(tree):return workspace(tree,'1')['mosaic']['rows'][0]['scroll_x'] +def buttons(name):return [x for x in log(name).splitlines() if x.startswith('BUTTON ')] +def measured_click(kind,point): + before={n:len(buttons(n)) for n in ['target','reference','filler']} + inject('right',*point,kind=kind) + row={'global_point':point,'events':{n:buttons(n)[before[n]:] for n in before}, + 'tails':{n:log(n).splitlines()[-15:] for n in before},'ns':time.monotonic_ns()} + save(lab,kind,row);return row + +def whole(tag,baseline,im): + value=compare(baseline,im);save(lab,tag,value);assert value['equal'],'whole H2 differs: '+tag + return value + +try: + compositor=spawn('compositor',[binary,'-d','-c',run/'config']) + sock=wait(lambda:next(iter(runtime.glob('sway-ipc.*.sock')),None)) + display=wait(lambda:next((x for x in runtime.glob('wayland-*') if x.is_socket()),None)) + env.update(SWAYSOCK=str(sock),WAYLAND_DISPLAY=display.name) + maps=Path(f'/proc/{compositor.pid}/maps').read_text();(run/'proc-maps.txt').write_text(maps) + exe=Path(f'/proc/{compositor.pid}/exe').resolve();assert exe==binary and sha(exe)==sha(binary) + paths=sorted({line.split()[-1] for line in maps.splitlines() if len(line.split())>=6 and line.split()[-1].startswith('/')}) + wl=Path('/usr/lib/libwlroots-0.20.so').resolve() + assert not any('libscenefx' in x for x in paths) + assert [Path(x).resolve() for x in paths if 'libwlroots' in x]==[wl] + assert sha(wl)=='7edca9a7d65855addda68e29ba5db94d57c803eb5fa7d0f86db3136b90ca9837' + assert not any('/swayfx-mosaic-' in x and Path(x)!=binary for x in paths) + save(lab,'dependency-mappings',{'status':'PASS','exe':str(exe),'paths':paths,'wlroots_sha256':sha(wl),'SceneFX_shared':False}) + outputs=ipc('-t','get_outputs');validate_outputs(outputs);actual={o['name']:(o['rect']['x'],o['rect']['y'],o['rect']['width'],o['rect']['height'],o['scale'],o['transform']) for o in outputs if o['active']} + assert actual=={'HEADLESS-1':(-800,0,800,600,1.0,'normal'),'HEADLESS-2':(0,0,800,600,1.0,'normal')},actual + save(lab,'output-geometry',outputs) + pointer=spawn('pointer',[lab/'pointer-daemon','1600','600'],stdin=True);wait(lambda:'POINTER_READY' in log('pointer')) + stage='clients' + cmd('workspace 2');ref=spawn('reference',[lab/'static-reference'],stdin=True,extra={'WAYLAND_DEBUG':'1'}) + reference=wait(lambda:find('org.mosaic.StaticReference'));refid=reference['id'] + cmd(f'[con_id={refid}] border none') + cmd('workspace 1');filler=spawn('filler',[lab/'static-reference'],stdin=True) + wait(lambda:len([n for n in nodes(ipc('-t','get_tree')) if n.get('app_id')=='org.mosaic.StaticReference'])==2) + target=spawn('target',[lab/'saved-client'],stdin=True,extra={'WAYLAND_DEBUG':'1'}) + wait(lambda:find('org.mosaic.SavedBufferProbe')) + stage='preflight' + cmd('layout mosaic');time.sleep(.6) + tree=snapshot('before-camera');owner=find('org.mosaic.SavedBufferProbe');targetid=owner['id'];wsid=workspace(tree,'1')['id'] + before=measure(owner);sc_before=scene('HEADLESS-1');scroll_before=camera(tree) + assert sc_before['scroll']==scroll_before + x,w=before['horizontal_content'];desired=-w//2;delta=x-desired + reserve(lab,'camera');cmd(f'layout mosaic scroll {delta}') + time.sleep(.6);owner=find('org.mosaic.SavedBufferProbe');after=measure(owner) + tree=snapshot('preflight');scroll_after=camera(tree);sc_after=scene('HEADLESS-1') + save(lab,'camera-calibration',{'before':scroll_before,'delta':delta,'desired_left':desired,'after':scroll_after,'before_scene':sc_before,'after_scene':sc_after}) + validate_camera(scroll_before,scroll_after,delta,sc_after) + x,w=after['horizontal_content'];assert x<00 and forbidden['count']==0 + whole('v1-H2-comparison',baseline,right) + paint=painted_geometry(left,after,commit) + points=click_points(left,right,after) + permitted=points['permitted'];forbidden_point=points['forbidden'] + save(lab,'painted-geometry',{'paint':paint,'measured':after,'commit':commit,'points':points}) + save(lab,'pixel-analysis',{'STATIC_MOSAIC_VISIBLE_PIXELS':'PASS', + 'STATIC_MOSAIC_FORBIDDEN_PIXELS':forbidden['count'],'STATIC_MOSAIC_H2_WHOLE_RGB':'PASS', + 'H1':visible,'H2':forbidden}) + save(lab,'click-points',{'permitted':permitted,'forbidden':forbidden_point,'paint_bbox':visible['bbox'],'horizontal_content':[x,w]}) + stage='visible-input' + click=measured_click('visible_click',permitted);events_visible=delivery(click['events'],'target') + assert not click['events']['reference'] and not click['events']['filler'] + save(lab,'visible-input',{'status':'PASS','point':permitted,'events':events_visible}) + tree=snapshot('after-visible');assert camera(tree)==scroll_after + assert measure(find('org.mosaic.SavedBufferProbe'))['horizontal_content']==[x,w] + stage='forbidden-input' + click=measured_click('forbidden_click',forbidden_point);events_reference=delivery(click['events'],'reference') + assert not click['events']['target'] and not click['events']['filler'],'invisible target or wrong client input' + save(lab,'forbidden-input',{'status':'PASS','target_received':False}) + save(lab,'reference-input',{'status':'PASS','point':forbidden_point,'events':events_reference,'invisible_target_events':click['events']['target']}) + tree=snapshot('after-reference');assert camera(tree)==scroll_after + assert next(n for n in nodes(tree) if n['id']==refid)['focused'] + save(lab,'reference-after-focus',validate_reference(focus_states[-1],refid,ref.pid)) + inject('move',-790,590);time.sleep(.6) + whole('post-click-H2-comparison',baseline,capture('h2-after-clicks','HEADLESS-2')) + stage='standard-return' + cmd(f'[con_id={targetid}] layout splith');time.sleep(.6) + tree=snapshot('standard');standard_ws=workspace(tree,'1');assert standard_ws['id']==wsid and standard_ws['layout']=='splith' + standard_owner=next(n for n in nodes(standard_ws) if n['id']==targetid);assert standard_owner['visible'] + standard_left=capture('h1-standard','HEADLESS-1');standard_right=capture('h2-standard','HEADLESS-2') + save(lab,'standard-pixels',{'H1':analyze(standard_left),'H2':analyze(standard_right)}) + assert analyze(standard_right)['count']==0, 'standard target leaked to H2' + whole('standard-H2-comparison',baseline,standard_right) + standard_scene=scene('HEADLESS-1');standard_h2=scene('HEADLESS-2') + assert standard_scene['workspace']==wsid and not standard_scene['clip_valid'] and not standard_h2['clip_valid'] + sx,sy=interior(standard_left);point=(sx-800,sy) + click=measured_click('standard_click',point);events_standard=delivery(click['events'],'target') + assert not click['events']['reference'] and not click['events']['filler'] + final_tree=snapshot('after-standard-click') + assert next(n for n in nodes(final_tree) if n['id']==targetid)['focused'] + assert [z['name'] for z in focus_states[-1]['workspaces'] if z['focused']]==['1'] + save(lab,'reference-after-standard',validate_reference(focus_states[-1],refid,ref.pid)) + inject('move',-790,590);time.sleep(.6) + whole('final-H2-comparison',baseline,capture('h2-standard-after-click','HEADLESS-2')) + save(lab,'standard-return',{'status':'STATIC_STANDARD_RETURN=PASS','workspace':standard_ws,'owner':measure(standard_owner),'scene':standard_scene,'H2_scene':standard_h2,'point':point,'events':events_standard,'pixels':analyze(standard_left),'H2_equal':True}) + assert all(p.poll() is None for p in [target,ref,filler,pointer]) + stage='completed' + cmd('exit');compositor.wait(timeout=5);assert compositor.returncode==0 +except Exception as exc: + failure=f'{type(exc).__name__}: {exc}';event('failure',message=failure) +finally: + finished.set();thread.join(timeout=1);stop_owned() + for proc in processes: + try:proc.wait(timeout=3) + except subprocess.TimeoutExpired: + os.killpg(proc.pid,signal.SIGKILL);proc.wait(timeout=3) + try:os.killpg(proc.pid,signal.SIGKILL) + except ProcessLookupError:pass + for out in files:out.close() + shutil.rmtree(runtime) + save(lab,'runtime',{'status':'PASS' if failure is None else 'INCONCLUSIVE','failure':failure,'stopping_stage':stage,'duration_seconds':time.monotonic()-started,'process_exit_codes':[{'pid':x.pid,'argv':x.args,'exit':x.returncode} for x in processes],'runtime_removed':str(runtime),'cleanup':'owned groups reaped, runtime removed'}) + damage=[s for s in log('compositor').splitlines() if 'TEMPORAL_DAMAGE ' in s] if (run/'compositor.log').exists() else [] + save(lab,'damage-analysis',{'extents':damage,'all_frames':'NOT TESTED','PNG_native_buffer_identity':'NOT PROVEN','client_damage':'wl_surface_damage_buffer full SHM at actual commits; see client source/protocol logs'}) +print('STATIC_RGB_INPUT_STANDARD',failure or 'PASS') +if failure:raise SystemExit(1) diff --git a/tooling/probes/step20c-static-rgb-input-retest/support.py b/tooling/probes/step20c-static-rgb-input-retest/support.py new file mode 100644 index 0000000..db6bd74 --- /dev/null +++ b/tooling/probes/step20c-static-rgb-input-retest/support.py @@ -0,0 +1,90 @@ +"""Private probe support; no desktop environment or global process ownership. + +WHY: failures must consume the same persistent experiment budget as successes. +CONTRACT: a stable flock file serializes durable atomic ledger updates. +INVARIANT: no reset/refund; only argv execution and owned process groups. +""" +import fcntl +import hashlib +import json +import os +from pathlib import Path +import signal +import subprocess +import tempfile +import time + +REPO = Path(__file__).resolve().parents[3] +LIMITS = {'run': 1, 'capture': 12, 'camera': 1, 'visible_click': 1, + 'forbidden_click': 1, 'standard_click': 1, 'move': 8} + +def sha(path): + return hashlib.sha256(Path(path).read_bytes()).hexdigest() + +def save(lab, name, value): + (Path(lab)/'results'/(name+'.json')).write_text(json.dumps(value, indent=2)+'\n') + +def reserve(lab, kind): + lab = Path(lab) + if kind not in LIMITS: + raise ValueError(kind) + with (lab/'budget.lock').open('a+') as lock: + fcntl.flock(lock, fcntl.LOCK_EX) + path = lab/'results/budget.json' + state = json.loads(path.read_text()) if path.exists() else { + 'limits': LIMITS, 'used': dict.fromkeys(LIMITS, 0), 'events': []} + # CONTRACT: corruption cannot create negative/extra capacity or erase history. + valid = (state.get('limits') == LIMITS and set(state.get('used', {})) == set(LIMITS) + and all(type(state['used'][k]) is int and 0 <= state['used'][k] <= LIMITS[k] for k in LIMITS) + and len(state.get('events', [])) == sum(state['used'].values()) + and all(e.get('sequence') == i and e.get('kind') in LIMITS for i,e in enumerate(state.get('events', []))) + and all(sum(e['kind'] == k for e in state['events']) == state['used'][k] for k in LIMITS)) + if not valid or state['used'][kind] >= LIMITS[kind]: + raise RuntimeError('budget exhausted or changed: '+kind) + state['used'][kind] += 1 + state['events'].append({'sequence': len(state['events']), 'kind': kind}) + fd, temporary = tempfile.mkstemp(prefix='.budget-', dir=path.parent) + with os.fdopen(fd, 'w') as out: + json.dump(state, out, indent=2); out.write('\n'); out.flush(); os.fsync(out.fileno()) + os.replace(temporary, path) + fd = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY) + try: + os.fsync(fd) + finally: + os.close(fd) + return state + +def environment(): + env = {k:v for k,v in os.environ.items() if k not in ( + 'SWAYSOCK','I3SOCK','WAYLAND_DISPLAY','DISPLAY','XDG_RUNTIME_DIR', + 'LD_LIBRARY_PATH','LD_PRELOAD','PKG_CONFIG_PATH')} + env.update(LC_ALL='C', PYTHONDONTWRITEBYTECODE='1', MESON_ROOT_CMD='/bin/false') + return env + +def command(lab, label, argv, cwd=None, extra=None, timeout=600, allow_failure=False): + lab=Path(lab); path=lab/'raw'/(label+'.log') + assert not path.exists(), 'diagnostic already recorded: '+label + with path.open('wb') as out: + proc=subprocess.Popen(list(map(str,argv)),cwd=cwd,env={**environment(),**(extra or {})}, + stdout=out,stderr=subprocess.STDOUT,start_new_session=True) + try: + deadline=time.monotonic()+timeout + while proc.poll() is None: + if time.monotonic()>=deadline or path.stat().st_size>64*1024*1024: + raise subprocess.TimeoutExpired(argv,timeout) + time.sleep(.1) + except subprocess.TimeoutExpired: + os.killpg(proc.pid,signal.SIGTERM) + try: proc.wait(timeout=3) + except subprocess.TimeoutExpired: + os.killpg(proc.pid,signal.SIGKILL);proc.wait() + try: os.killpg(proc.pid,signal.SIGKILL) + except ProcessLookupError: pass + raise + with (lab/'raw/commands.jsonl').open('a') as out: + out.write(json.dumps({'label':label,'argv':list(map(str,argv)), 'cwd':str(cwd), + 'env':extra or {},'exit':proc.returncode})+'\n') + assert path.stat().st_size < 64*1024*1024, 'output bound exceeded' + if not allow_failure and proc.returncode: + raise RuntimeError(f'{label} exit={proc.returncode}; inspect {path}') + return path.read_text(errors='replace') diff --git a/tooling/probes/step20c-static-rgb-input-retest/test-budget.py b/tooling/probes/step20c-static-rgb-input-retest/test-budget.py new file mode 100644 index 0000000..992bb2a --- /dev/null +++ b/tooling/probes/step20c-static-rgb-input-retest/test-budget.py @@ -0,0 +1,31 @@ +"""Offline exhaustion and cross-process reservation tests, outside campaign LAB.""" +import concurrent.futures +import json +from pathlib import Path +import tempfile +from support import LIMITS, reserve + +def attempt(args): + lab, kind = args + try: reserve(lab,kind); return True + except RuntimeError: return False + +if __name__ == '__main__': + with tempfile.TemporaryDirectory(prefix='static-budget-test-') as tmp: + lab=Path(tmp);(lab/'results').mkdir() + with concurrent.futures.ProcessPoolExecutor(max_workers=4) as pool: + assert sum(pool.map(attempt,[(lab,'capture')]*20))==12 + for kind,limit in LIMITS.items(): + if kind=='capture':continue + for _ in range(limit):assert attempt((lab,kind)) + assert not attempt((lab,kind)) + state=json.loads((lab/'results/budget.json').read_text()) + assert state['used']==LIMITS + assert not attempt((lab,'run')) + before=(lab/'results/budget.json').read_bytes() + assert not attempt((lab,'capture')) + assert (lab/'results/budget.json').read_bytes()==before, 'failed exhaustion rewrote budget' + corrupted=json.loads(before);corrupted['used']['run']=-1 + (lab/'results/budget.json').write_text(json.dumps(corrupted)) + assert not attempt((lab,'run')), 'negative capacity accepted' + print('PASS offline ledger: concurrent reservations, exhaustion, durable failures, no refund') diff --git a/tooling/probes/step20c-static-rgb-input-retest/validate_results.py b/tooling/probes/step20c-static-rgb-input-retest/validate_results.py new file mode 100644 index 0000000..72d2e35 --- /dev/null +++ b/tooling/probes/step20c-static-rgb-input-retest/validate_results.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +"""Offline requalification of actual RGB/input/return artifacts after the sole run. + +WHY: shutdown IPC may lose its reply after cmd_exit terminates the compositor. +CONTRACT: never rewrite runtime.json or claim the executed runner exited zero. +INVARIANT: all scientific gates must independently pass; terminal transport is +accepted only with completed measurements, explicit exit and compositor code 0. +""" +import argparse +import copy +import json +from pathlib import Path +import re +from PIL import Image +from support import sha,save +from ipc_predicates import (nodes,workspace,validate_preflight,measure_owner, + scene_from_log,client_commit,validate_reference,validate_outputs,validate_camera) +from pixels import compare,analyze,painted_geometry,click_points,delivery,button_pair,interior + + +def check(lab,overrides=None): + lab=Path(lab);overrides=overrides or {} + def read(name):return overrides.get(name,json.loads((lab/'results'/(name+'.json')).read_text())) + def image(name):return overrides.get(name,Image.open(lab/'run'/name).convert('RGB')) + text=(lab/'run/compositor.log').read_text() + snap=read('preflight-snapshot');cal=read('camera-calibration');measured=read('owner-geometry')['after'] + ref=read('reference-state');tid=measured['id'];rid=ref['con_id'] + owner=next(n for n in nodes(snap['tree']) if n['id']==tid) + assert measured['raw'] in text and cal['after_scene']['raw'] in text + measured=measure_owner(measured['raw'],owner) + scene=scene_from_log(cal['after_scene']['raw'],'HEADLESS-1') + clips=read('scene-clips');assert clips['H2']['raw'] in text + h2=scene_from_log(clips['H2']['raw'],'HEADLESS-2') + commit=read('live-preflight')['commit'];assert commit['raw'] in (lab/'run/target.log').read_text() + commit=client_commit(commit['raw']) + validate_preflight(snap,rid,ref['pid'],tid,owner['pid'],scene,h2,measured,commit) + validate_camera(cal['before'],cal['after'],cal['delta'],scene) + baseline=image('h2-reference-1.png');assert baseline.size==(800,600) + assert all(p==(48,48,56) for p in baseline.get_flattened_data()) + rgb={} + for name in ['h2-reference-2.png','h2-mosaic.png','h2-after-clicks.png','h2-standard.png','h2-standard-after-click.png']: + im=image(name);assert im.size==(800,600) + value=compare(baseline,im);assert value['equal'], ('whole H2 RGB',name) + assert analyze(im)['count']==0 + rgb[name]={'changed':0,'PNG_sha256':sha(lab/'run'/name)} + paint=painted_geometry(image('h1-mosaic.png'),measured,commit) + points=click_points(image('h1-mosaic.png'),image('h2-mosaic.png'),measured) + logs={n:(lab/'run'/(n+'.log')).read_text() for n in ['target','reference','filler']} + button_lines={n:[s for s in t.splitlines() if s.startswith('BUTTON ')] for n,t in logs.items()} + pairs={} + for name,recipient,point in [('visible_click','target',points['permitted']), + ('forbidden_click','reference',points['forbidden']), + ('standard_click','target',None)]: + click=read(name);pairs[name]=delivery(click['events'],recipient) + assert all(row in button_lines[recipient] for row in click['events'][recipient]), 'event not in actual log' + if point:assert tuple(click['global_point'])==point + assert button_lines['target']==read('visible_click')['events']['target']+read('standard_click')['events']['target'] + assert button_lines['reference']==read('forbidden_click')['events']['reference'] and not button_lines['filler'] + focus={} + for name,expected in [('after-visible',tid),('after-reference',rid),('standard',rid),('after-standard-click',tid)]: + s=read(name+'-snapshot');assert s['focused']==[expected] + validate_reference(s,rid,ref['pid']);validate_outputs(s['outputs']) + if name in ['after-visible','after-reference']: + assert workspace(s['tree'],'1')['mosaic']['rows'][0]['scroll_x']==cal['after'] + focus[name]=expected + standard=read('standard-return');assert standard['status']=='STATIC_STANDARD_RETURN=PASS' + stdsnap=read('standard-snapshot');ws=workspace(stdsnap['tree'],'1') + assert ws['id']==workspace(snap['tree'],'1')['id'] and ws['layout']=='splith' + stdowner=next(n for n in nodes(ws) if n['id']==tid) + assert stdowner['visible'] and stdowner['pid']==owner['pid'] + for output,key in [('HEADLESS-1','scene'),('HEADLESS-2','H2_scene')]: + sc=standard[key];assert sc['raw'] in text + parsed=scene_from_log(sc['raw'],output);assert not parsed['clip_valid'] and parsed['layout_enum']==1 + std=image('h1-standard.png');paint_std=analyze(std);assert paint_std['count']>0 + x1,y1,x2,y2=paint_std['bbox'];assert paint_std['count']==(x2-x1)*(y2-y1) + stdpoint=interior(std);assert read('standard_click')['global_point']==[stdpoint[0]-800,stdpoint[1]] + assert analyze(image('h2-standard.png'))['count']==0 + runtime=read('runtime');assert runtime['duration_seconds']<150 + assert [r['exit'] for r in runtime['process_exit_codes']]==[0,-15,0,0,0], 'unexpected owned process exit' + assert not Path(runtime['runtime_removed']).exists() + for row in runtime['process_exit_codes']: + assert not Path('/proc',str(row['pid'])).exists(), 'owned process remains' + budget=read('budget');assert budget['used']==dict(run=1,capture=8,camera=1,visible_click=1,forbidden_click=1,standard_click=1,move=6) + assert len(list((lab/'run').glob('*.png')))==8 + injections=[s for s in (lab/'run/pointer.log').read_text().splitlines() if s.startswith('INJECT ')] + assert len(injections)==6 and sum('INJECT right ' in s for s in injections)==3 + events=[json.loads(s) for s in (lab/'run/events.jsonl').read_text().splitlines()] + adjustments=[e for e in events if e['kind']=='ipc' and e['args']==['layout mosaic scroll '+str(cal['delta'])]] + assert len(adjustments)==1 + assert 'wl_display' not in '\n'.join(s for t in logs.values() for s in t.splitlines() if '.error(' in s) + assert not re.search(r'protocol error|INVALID_|MISSING_GLOBAL', '\n'.join(logs.values()), re.I) + shutdown_ns=min(int(re.search(r'EXIT ns=(\d+)',t)[1]) for t in logs.values()) + last_capture=max(e['ns'] for e in events if e['kind']=='capture') + assert shutdown_ns>last_capture, 'client transport failure before final measurements' + terminal=False + if runtime['status']!='PASS': + assert runtime['stopping_stage']=='completed' + assert runtime['failure'].startswith('CalledProcessError: Command') and "'exit']' returned non-zero exit status 1." in runtime['failure'] + assert "Handling command 'exit'" in text and 'Shutting down sway' in text + assert all('error=32' in t.splitlines()[-1] for t in logs.values()), 'different terminal transport error' + terminal=True + return {'status':'PASS','scientific_gates':'ALL PASS','verdict':'FEASIBLE', + 'architecture':'STATIC_MOSAIC_RGB_INPUT_STANDARD_FEASIBLE_FOR_TESTED_MATRIX', + 'RGB':rgb,'H1':paint,'standard_H1':paint_std,'pairs':pairs,'focus':focus, + 'terminal_transport_incident':terminal,'runner_exit':1 if terminal else 0, + 'original_runtime_status_preserved':runtime['status'], + 'terminal_diagnosis':'completed measurements precede explicit compositor exit 0; client errno 32 is terminal EPIPE, not a wl_display protocol error'} + +if __name__=='__main__': + p=argparse.ArgumentParser();p.add_argument('lab',type=Path);p.add_argument('--record',action='store_true');a=p.parse_args() + result=check(a.lab) + # Negative post-run classification tests do not write any original observation. + negatives=[] + for name in ['runtime','standard-return','forbidden_click']: + value=json.loads((a.lab/'results'/(name+'.json')).read_text());bad=copy.deepcopy(value) + if name=='runtime':bad['process_exit_codes'][0]['exit']=1 + elif name=='standard-return':bad['scene']['clip_valid']=True;bad['scene']['raw']=bad['scene']['raw'].replace('clip_valid=0','clip_valid=1') + else:bad['events']['target']=['BUTTON ns=1 serial=3 button=273 state=1'] + try:check(a.lab,{name:bad}) + except AssertionError:negatives.append(name+' REJECTED') + else:raise AssertionError('postrun bad state accepted: '+name) + changed=Image.open(a.lab/'run/h2-mosaic.png').convert('RGB');changed.putpixel((799,599),(48,48,57)) + try:check(a.lab,{'h2-mosaic.png':changed}) + except AssertionError:negatives.append('whole H2 RGB REJECTED') + else:raise AssertionError('postrun changed H2 accepted') + result['negative_tests']=negatives + if a.record:save(a.lab,'postrun-validation',result) + print('PASS actual RGB, scene, all real buttons/focus, standard return, budget and terminal diagnosis') + print('PASS negative postrun classification',negatives) diff --git a/tooling/probes/step20c-static-rgb-input-retest/verify.py b/tooling/probes/step20c-static-rgb-input-retest/verify.py new file mode 100644 index 0000000..159535a --- /dev/null +++ b/tooling/probes/step20c-static-rgb-input-retest/verify.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +"""Read-only integrity checks; no remapping, rewriting, or budget refund.""" +import argparse +from pathlib import Path +from support import sha + + +def verify_inventory(manifest): + rows=Path(manifest).read_text().splitlines();assert rows + seen=set() + for row in rows: + digest,p=row.split(' ',1);assert p not in seen;seen.add(p) + assert sha(p)==digest,('manifest mismatch',p) + return {'entries':len(rows),'sha256':sha(manifest)} + +if __name__=='__main__': + p=argparse.ArgumentParser();p.add_argument('lab',type=Path);a=p.parse_args() + for name in ['fixture-freeze','source','evidence']: + print(name,verify_inventory(a.lab/'results'/(name+'-sha256.txt')),'PASS')