From 4ef16a40192149d353831695fd7dcaeb594fdf01 Mon Sep 17 00:00:00 2001 From: fy59 Date: Tue, 13 Oct 2026 22:28:14 +0200 Subject: [PATCH] test(mosaic): qualify static nested standard return and integration readiness E2 is FEASIBLE for the measured matrix. The initial private integration candidate is BUILDABLE; its accepted comment delta is NOT REBUILT. STEP20C remains NEXT and production multi-output remains NOT YET SUPPORTED. --- CHANGELOG.md | 21 + docs/ARCHITECTURE.md | 28 ++ docs/KNOWN_LIMITATIONS.md | 31 ++ docs/README.md | 16 + docs/ROADMAP.md | 26 ++ ...20C_STATIC_E2_TRANSITION_AND_COMPLETION.md | 338 ++++++++++++++++ .../step20c-static-e2-completion/README.md | 105 +++++ .../README_RESULTS.md | 60 +++ .../step20c-static-e2-completion/audit.py | 52 +++ .../authorization.py | 10 + .../client-contract.json | 13 + .../derivation.json | 113 ++++++ .../step20c-static-e2-completion/gate.py | 74 ++++ .../step20c-static-e2-completion/handoff.py | 194 +++++++++ .../handoff_tests.py | 108 +++++ .../ipc_predicates.py | 111 ++++++ .../step20c-static-e2-completion/launch.py | 22 + .../step20c-static-e2-completion/ledger.py | 65 +++ .../step20c-static-e2-completion/lifecycle.py | 64 +++ .../step20c-static-e2-completion/model.py | 194 +++++++++ .../observations.py | 59 +++ .../step20c-static-e2-completion/pixels.py | 21 + .../placement.patch | 16 + .../predicates.py | 199 +++++++++ .../step20c-static-e2-completion/replay.py | 75 ++++ .../reusable_tests.py | 171 ++++++++ .../step20c-static-e2-completion/run.py | 376 ++++++++++++++++++ .../step20c-static-e2-completion/support.py | 70 ++++ .../tests_offline.py | 220 ++++++++++ .../transition.py | 173 ++++++++ 30 files changed, 3025 insertions(+) create mode 100644 docs/design/STEP20C_STATIC_E2_TRANSITION_AND_COMPLETION.md create mode 100644 tooling/probes/step20c-static-e2-completion/README.md create mode 100644 tooling/probes/step20c-static-e2-completion/README_RESULTS.md create mode 100644 tooling/probes/step20c-static-e2-completion/audit.py create mode 100644 tooling/probes/step20c-static-e2-completion/authorization.py create mode 100644 tooling/probes/step20c-static-e2-completion/client-contract.json create mode 100644 tooling/probes/step20c-static-e2-completion/derivation.json create mode 100644 tooling/probes/step20c-static-e2-completion/gate.py create mode 100644 tooling/probes/step20c-static-e2-completion/handoff.py create mode 100644 tooling/probes/step20c-static-e2-completion/handoff_tests.py create mode 100644 tooling/probes/step20c-static-e2-completion/ipc_predicates.py create mode 100644 tooling/probes/step20c-static-e2-completion/launch.py create mode 100644 tooling/probes/step20c-static-e2-completion/ledger.py create mode 100644 tooling/probes/step20c-static-e2-completion/lifecycle.py create mode 100644 tooling/probes/step20c-static-e2-completion/model.py create mode 100644 tooling/probes/step20c-static-e2-completion/observations.py create mode 100644 tooling/probes/step20c-static-e2-completion/pixels.py create mode 100644 tooling/probes/step20c-static-e2-completion/placement.patch create mode 100644 tooling/probes/step20c-static-e2-completion/predicates.py create mode 100644 tooling/probes/step20c-static-e2-completion/replay.py create mode 100644 tooling/probes/step20c-static-e2-completion/reusable_tests.py create mode 100644 tooling/probes/step20c-static-e2-completion/run.py create mode 100644 tooling/probes/step20c-static-e2-completion/support.py create mode 100644 tooling/probes/step20c-static-e2-completion/tests_offline.py create mode 100644 tooling/probes/step20c-static-e2-completion/transition.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 51877fd..93940ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -106,3 +106,24 @@ uncommitted and unpublished, and readiness only permits requesting a separate future authorization. No compositor/runtime authorization, production/UX/package change or STEP20D work follows. Architecture UNDECIDED; STEP20C NEXT; production multi-output NOT YET SUPPORTED. + + +## Static E2 completion and private integration evidence — 2026-10-13 + +- Recorded the independently accepted shared applied-standard-transition gate: + 104 targeted offline tests PASS, historical replay and 277 classified assertion + sites, with the separate older-suite 145 PASS/61 setup errors preserved. +- Recorded the sole unchanged-static-binary E2 run as EXPERIMENTAL / FEASIBLE + for two genuine grabs, Mosaic pixels, applied splith return, standard B/A + input, explicit B close/A survival and normal owner close/full-H2 restoration. + Raw terminal IPC1, startup entries and historical INCONCLUSIVE findings remain. +- Recorded a private initial integration candidate BUILDABLE from one SceneFX + build, one Mosaic build/model/ELF check and one exact five-file DESTDIR stage. + Its distinct binary is graphically NOT TESTED. Independent P2 comment repair + is accepted as COMMENT-ONLY / NOT REBUILT; initial recipe/build evidence stays + unchanged, recipe execution NOT RUN and legal compliance NOT APPROVED. +- Added the [completion report](docs/design/STEP20C_STATIC_E2_TRANSITION_AND_COMPLETION.md) + and [current fixture results](tooling/probes/step20c-static-e2-completion/README_RESULTS.md), + preserving the original README as a sealed pre-execution protocol. Final batch + audit is PASS / ACCEPTED with no BLOCKING finding. No production source, UX, package, public ABI + or cumulative patch change; STEP20C NEXT, multi-output NOT YET SUPPORTED. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 988ac6a..db70ebb 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -488,3 +488,31 @@ compositor/client exits0 remain intact. No rerun or algorithm change; historical is preserved for publication by the E2 completion batch. Architecture UNDECIDED, STEP20C NEXT, production multi-output NOT YET SUPPORTED; broader damage/generation/output/ effects/ABI/distribution gates remain open. + + +## Static E2 completion and private integration evidence — 2026-10-13 + +The [E2 completion report](design/STEP20C_STATIC_E2_TRANSITION_AND_COMPLETION.md) +records independent B/C/D acceptance. The shared applied-transition predicate +passes 104 targeted offline tests; its 277-site source audit is classification, +not graphical execution proof. The sole unchanged-static-binary C run is +**EXPERIMENTAL / FEASIBLE for the tested matrix**: two real grabbed popups, +owner-derived Mosaic pixels, ordered current/applied splith return with clips +absent, standard B input, explicit B close/A survival, standard A input and normal +owner close with exact whole-H2 restoration. This correlates one live root and +owner/workspace/output; it does not establish an atomic transaction generation. +The historical runtime remains INCONCLUSIVE with its missing gates unchanged. + +The conditional private D candidate is **BUILDABLE for its initial build**, +model/ELF and five-file DESTDIR stage. Its distinct binary is graphically +**NOT TESTED**. The accepted four-file comment correction is +**COMMENT-ONLY / NOT REBUILT**; the initial recipe/sources/manifests/binary remain +unchanged. Characterized85 overlap symbols and 17 local structs/six offsets do +not establish maintained semantic ABI. Legal notice closure and named release/ +security roles remain open. No production source, public ABI, library dependency, +packaging or popup UX contract changes. Final batch audit is PASS / ACCEPTED; documentation and fixtures are approved +for normal publication. +[Current fixture results](../tooling/probes/step20c-static-e2-completion/README_RESULTS.md) +supersede the original README's sealed pre-execution status only. +Architecture remains UNDECIDED for production integration; STEP20C NEXT, +production multi-output NOT YET SUPPORTED. diff --git a/docs/KNOWN_LIMITATIONS.md b/docs/KNOWN_LIMITATIONS.md index ddd2a21..60b4a4e 100644 --- a/docs/KNOWN_LIMITATIONS.md +++ b/docs/KNOWN_LIMITATIONS.md @@ -429,3 +429,34 @@ compositor/client exits0 remain intact. No rerun or algorithm change; historical is preserved for publication by the E2 completion batch. Architecture UNDECIDED, STEP20C NEXT, production multi-output NOT YET SUPPORTED; broader damage/generation/output/ effects/ABI/distribution gates remain open. + + +## Static E2 completion support boundary — 2026-10-13 + +The [completion report](design/STEP20C_STATIC_E2_TRANSITION_AND_COMPLETION.md) +and [current fixture results](../tooling/probes/step20c-static-e2-completion/README_RESULTS.md) +record B offline PASS, C **FEASIBLE for the tested E2 matrix** and D initial +**BUILDABLE** with an accepted **COMMENT-ONLY / NOT REBUILT** delta. C closes +its own sampled standard-return B/A input, explicit B-close/A-survival and normal +owner-close/full-H2 restoration gates. It does not rewrite the historical +INCONCLUSIVE campaign, whose corresponding gates remain NOT TESTED. + +C ran once on normal scale1 outputs at x=-800/0 with the unchanged static binary. +New static hidden-owner, saved-buffer/effects, vertical/scaled/rotated and +migration/resize matrices remain unqualified; historical private evidence retains +its own limits. Settled full-H2 equality and 256 sampled damage traces do not +qualify every intermediate frame, complete damage/stale pixels or atomic +transaction/render/input generation. Universal click-through and final offscreen +popup UX remain UNDECIDED. + +D's new binary has **NOT TESTED** graphics. Initial builds/model/ELF/stage pass; +annotated sources are NOT REBUILT and the future recipe is syntax-only, NOT RUN. +The exact five-file private stage includes the binary and four license notices; +semantic ABI maintenance is NOT ESTABLISHED, legal compliance NOT APPROVED, +and release/security names remain required. Raw terminal IPC1 and startup +EGL_BAD_ALLOC entries remain preserved despite runner/compositor/client0 and +completed C observations. A fresh old 206-suite setup retained 145 PASS/61 setup +errors, separate from B's accepted104 tests and the historical206 result. +Final batch audit is PASS / ACCEPTED. Publication identities are recorded in +Git history and the private closeout receipt. STEP20C remains NEXT, production +multi-output NOT YET SUPPORTED; no daily-driver qualification follows. diff --git a/docs/README.md b/docs/README.md index e5a7321..e7599b2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -347,3 +347,19 @@ compositor/client exits0 remain intact. No rerun or algorithm change; historical is preserved for publication by the E2 completion batch. Architecture UNDECIDED, STEP20C NEXT, production multi-output NOT YET SUPPORTED; broader damage/generation/output/ effects/ABI/distribution gates remain open. + + +[Static E2 completion and private integration results](design/STEP20C_STATIC_E2_TRANSITION_AND_COMPLETION.md) +and [current fixture results](../tooling/probes/step20c-static-e2-completion/README_RESULTS.md): +EXPERIMENTAL. B 104 targeted offline tests PASS with independent acceptance; +C FEASIBLE for the sole static nested-popup standard-return/input/normal-close +matrix; D initial candidate BUILDABLE, accepted comment delta COMMENT-ONLY / +NOT REBUILT, distinct candidate graphics NOT TESTED. The original fixture +[README](../tooling/probes/step20c-static-e2-completion/README.md) is the sealed +pre-execution protocol, preserved byte-for-byte; current status is in +README_RESULTS. Historical INCONCLUSIVE verdicts remain unchanged. Final batch +audit is PASS / ACCEPTED, with publication identities recorded in Git history +and the private closeout receipt. Complete damage, +atomic generation, wider static graphics/output coverage, maintained ABI and +legal/maintenance gates remain open. STEP20C NEXT, production multi-output +NOT YET SUPPORTED; no production/UX/packaging contract change. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index efa2fc1..2844dcf 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -433,3 +433,29 @@ compositor/client exits0 remain intact. No rerun or algorithm change; historical is preserved for publication by the E2 completion batch. Architecture UNDECIDED, STEP20C NEXT, production multi-output NOT YET SUPPORTED; broader damage/generation/output/ effects/ABI/distribution gates remain open. + + +## Static E2 completion and conditional private integration — 2026-10-13 + +The [completion report](design/STEP20C_STATIC_E2_TRANSITION_AND_COMPLETION.md) +records B offline PASS with accepted final handoff, C independent ACCEPTED / +**FEASIBLE for the tested matrix**, and conditional D initial **BUILDABLE**. +C measures two genuine grabs, applied standard return, B then A standard input, +explicit child close/A survival and normal owner close with exact whole-H2 RGB. +Historical INCONCLUSIVE evidence is unchanged. D passes its one SceneFX build, +one Mosaic build/model/ELF check and one exact five-file stage with zero graphics +or installations. The accepted comment delta is **COMMENT-ONLY / NOT REBUILT**; +C does not qualify the different D binary. The original fixture README remains +a sealed pre-execution protocol; use +[README_RESULTS](../tooling/probes/step20c-static-e2-completion/README_RESULTS.md). + +Final batch audit is PASS / ACCEPTED with no BLOCKING finding. The accompanying +documentation/fixture publication and its verified remote identities are recorded +in Git history and the private closeout receipt. +A separate bounded authorization is required for an annotated rebuild or D's +exact-binary graphical qualification. Complete damage, atomic generation, static +hidden-owner/effects/saved-buffer continuity, wider output/migration matrices, +maintained ABI, notice/legal closure and named maintenance roles remain gates. +This is STEP20C evidence, not implementation of roadmap STEP20D. STEP20A DONE; +STEP20B EXPERIMENTAL; STEP20C NEXT; STEP20D–G PLANNED. Production multi-output +remains NOT YET SUPPORTED. No production/UX/packaging contract changes. diff --git a/docs/design/STEP20C_STATIC_E2_TRANSITION_AND_COMPLETION.md b/docs/design/STEP20C_STATIC_E2_TRANSITION_AND_COMPLETION.md new file mode 100644 index 0000000..56fd3ee --- /dev/null +++ b/docs/design/STEP20C_STATIC_E2_TRANSITION_AND_COMPLETION.md @@ -0,0 +1,338 @@ +# STEP20C static E2 transition and completion + +**EXPERIMENTAL. Phase B PASS; Phase C FEASIBLE for the tested matrix; +Phase D initial candidate BUILDABLE, comment delta ACCEPTED / NOT REBUILT.** +Independent B/C/D reviews accept these bounded findings. Phase E final deep audit is PASS / ACCEPTED, with no BLOCKING finding. +Publication identities are recorded in Git history and the private closeout +receipt; this report does not self-reference its publication commit. +Ticket: `SWAYFX_MOSAIC_STEP20C_E2_COMPLETION_AND_PRIVATE_INTEGRATION_BATCH_V1`. +Historical freeze: `c5f96a34e3924275179365938d02f3a318c0c4bf`, verified on +Forgejo and GitHub. The historical runtime remains INCONCLUSIVE. + +## Accepted bounded experiment contract + +Canonical [architecture](../ARCHITECTURE.md), [UX](../UX.md), +[limitations](../KNOWN_LIMITATIONS.md), [roadmap](../ROADMAP.md), +[development](../DEVELOPMENT.md), [packaging](../PACKAGING.md) and +[upstream](../UPSTREAM.md) govern. This experiment changes no production or UX +contract. All historical LAB bytes remain unchanged. Historical fixture scripts are +unchanged, and the repository fixtures remain byte-identical to the Phase A +publication baseline `c5f96a34` during B–E. The Phase A README status update +and its earlier runtime-inventory binding are explicitly documented below. + +The derived fixture under `tooling/probes/step20c-static-e2-completion/` must +share `validate_applied_standard_transition(...)` between offline replay and +runtime. It verifies ordered applied Mosaic, targeted accepted splith IPC, +applied standard and stabilized standard observations, with exact owner/root/ +workspace/output attribution, absent tiled/root clips and live mapped popups. +Pending state is insufficient. Multiple roots or ambiguous epochs are +INCONCLUSIVE. Independent observations do not establish atomic generation. +Negative tests cover all seventeen specified temporal/provenance failures, +legitimate omissions and empty PASS records. The entire E2 assertion path and +handoff must be independently accepted before any runtime reservation. + +## Sole authorized runtime, conditional on offline gates + +One fresh private LAB, ticket-specific durable owner and exclusive consumed +launch token. Unchanged static binary SHA256 +`f82557c1a9978a5e9665b9b4291b74880253725359e3df1c5b8d554921f7603b`, +build ID `942db64269640778ee433e5970ba1100bc60f94c`; actual mapped wlroots +SHA256 `7edca9a7d65855addda68e29ba5db94d57c803eb5fa7d0f86db3136b90ca9837`. +No SceneFX dynamic mapping, compositor rebuild or pristine control. +Private headless H1(-800,0), H2(0,0), each800x600, scale1/normal; runtime0700, +clean environment and explicit private IPC socket on every command. +Limits: run1, chain1, grabs2, complete clicks4, camera1, transition1, +PNG12, positions16, maximum150 seconds. Failed runs are never refunded/retried. + +Unique static H2 reference #303038, border none by actual con_id, full content. +Two stabilized full-RGB references must be identical without mask/tolerance. +A and B open from distinct real button serials with TOPLEVEL->A->B hierarchy, +configure/ACK/map/protocol corroboration. Applied camera/owner geometry and +root/tiled clips must explain measured Mosaic H1 pixels and whole-H2 equality. +Targeted splith preserves workspace and mapped owner/A/B; shared temporal +predicate qualifies observed clip removal. Standard H2 overflow is expected. +B right press/release comes from a measured solid green interior. Explicit +client close of B must be distinguished from popup_done; A retains its XDG +object, map, pixels and input. Recalculate A right-click point from a new +capture after B disappears. Normal owner close destroys popups; final whole +H2 RGB must exactly equal the initial reference, with no A/B residual color. +Preserve raw runner, terminal IPC, compositor/client codes and protocol logs, +including cleanup/rescue. Terminal IPC1 alone does not undo completed science. + +## Conditional private integration and publication + +Only runtime FEASIBLE plus accepted review permits a private integration +candidate outside production Git. Pinned source/patch provenance is mandatory; +limits are one SceneFX static build, one Mosaic build, one DESTDIR stage, zero +graphical runs/installations. Stage only `/usr/bin/swayfx-mosaic` and justified +notices under `/usr/share/licenses/swayfx-mosaic/`. BUILDABLE is not production +readiness, and the candidate binary has no graphical qualification from E2. +Reviewed documentation/fixture source may be committed and pushed normally; +no production source, real PKGBUILD, cumulative patch, binary, PNG or build file. + +STEP20A DONE; STEP20B EXPERIMENTAL; STEP20C NEXT; STEP20D–G PLANNED. +Production multi-output NOT YET SUPPORTED. + + +## Recorded completion — 2026-10-13 + +This is the current results record for the ticket. The original fixture +[README](../../tooling/probes/step20c-static-e2-completion/README.md) is the +**sealed pre-execution protocol**, preserved byte-for-byte in the accepted +runtime manifest; its PREPARATION/NOT RUN wording describes that earlier state. +Use [README_RESULTS](../../tooling/probes/step20c-static-e2-completion/README_RESULTS.md) +for the synchronized current status. Historical reports and fixtures retain +their original INCONCLUSIVE verdicts. This report adds evidence, not production +architecture, final popup UX, a packaging change or STEP20D implementation. + +### Phase A — historical preservation + +Initial batch HEAD was `54392cc0`; historical publication was verified at +`c5f96a34e3924275179365938d02f3a318c0c4bf`, equal to origin/main and github/main +at that gate with a clean tracked tree. This identifies the historical +publication only; the completion batch has no recorded publication SHA yet. +The 60-entry historical source manifest SHA256 is +`263f5dbee5e96f2c49619802d063115f792718f6aeb71fa41c58c73b83eb8c91`; +the 178-entry historical evidence manifest is +`e8b2307612ce2402ff9b31e68b50a47d666ca38cd8cce1d67096297f8129034a`. +All entries, thirteen PNGs and raw terminal codes were verified. Historical +runner/dispatch1, IPC1 and compositor/clients0 remain unchanged. Historical +standard input, explicit B close/A survival and final H2 restoration remain +NOT TESTED in that campaign. + +The Phase A publication updated only historical repository documentation +status, including the runtime fixture README. Its old 92-entry pre-execution +inventory still records that README at SHA256 +`12d324b8cc971565c1e1f59b370cc7ada5df2567f6c6573f8b4f6cf60b8e56d8`; +the published README is now +`9574f2ab2cd538d78f9bf57bf98484dd5fdd66203013658709ddeb02ba5332ed`. +The inventory and LAB copies remain intact. No new PASS92 is claimed: the +final source60/evidence178 seals used here and by replay verify every entry. +The final reviewer classified this documentation provenance note NON_BLOCKING. + +### Phase B — shared applied-transition validation, offline only + +`validate_applied_standard_transition(...)` is shared by replay and runner. +It requires source-backed applied/current Mosaic, a targeted accepted splith +request, applied standard and at least two coherent stabilized standard +observations. Exact owner/root/workspace/output, live XDG hierarchy, protocol +and capture identity are correlated, and observed clip reactivation is rejected. +Pending state or ambiguous roots/epochs cannot qualify the gate. This bounded +correlation does not establish atomic transaction generation. + +The old request-start timestamp was absent. Offline replay uses the explicit +actual interval `[181922658327071,181923034516066]`, reply +`181923035203192`, retained old root `181923034904618` and cleared standard roots +`181923236944243` and `181923848675032`. No old row is discarded. The historical +standard PNG has content origin `(-398,25)`, A `[-298,105,350,170]`, +B `[-198,165,300,130]`, and H2 A3120/B13260 pixels. This is **OFFLINE replay**, +not a new runtime or retroactive qualification of missing historical input. + +The final derived fixture corrects old-configure mixing and the pre-root +opening window. All 104 targeted tests pass, including seventeen temporal/ +provenance negatives and fifteen explicitly synthetic handoff tests. Nonempty +executed receipts are required; the 277 static assertion inventory classifies +source sites and is not graphical execution proof. The independent tester +reproduced 104 PASS, C17 syntax for four clients and nineteen Python AST checks. +The independent reviewer reran 88 pure tests and verified all 104 actual +receipts, accepting B. All B gate markers pass before dispatch. + +The attempted fresh setup of the older 206-test suite retained 145 PASS and +61 errors from missing preflight native-SHM evidence and dependent source-audit +setup. Its 145 preflight checks and replay passed. This setup limitation is +separate from the new 104-test gate and does not rewrite the historical qualified +206 result. No synthetic execution receipts were substituted. Early failed +bounded tester attempts are retained privately; the final C validation has +28 PASS. + +### Phase C — sole new runtime, FEASIBLE for this E2 matrix + +Private LAB: `/home/fy59/swayfx-mosaic-e2-completion-dk1hq9an`. +The exclusive launch token was consumed once; post-run refusal was verified. +There was no E1/pristine-control build or runtime retry. Binary SHA256 remains +`f82557c1a9978a5e9665b9b4291b74880253725359e3df1c5b8d554921f7603b`, +build ID `942db64269640778ee433e5970ba1100bc60f94c`. Actual mapped wlroots is +`7edca9a7d65855addda68e29ba5db94d57c803eb5fa7d0f86db3136b90ca9837`. +No dynamic SceneFX mapping was observed. Actual rendering used FX/GLES; +an environment renderer request is not evidence of Pixman execution. +Outputs were H1 `(-800,0)` and H2 `(0,0)`, 800×600, scale1, normal transform. + +Reference container7/PID2130252 was solid `#303038` with empty decoration and +full client size. Stored border width2 is valid under the observed border-none +state. Owner9/PID2130263 stayed in workspace6. One camera scroll -320 changed +480→160; applied scene was `(-320,0,640,600)`, content origin `(-318,25)`. +Native SHM420×320 remains distinct from assigned content636×573. Root +`0x55b8d482ea30` carried local clip `[-482,-25,800,600]`, global +`[-800,0,800,600]`. + +Real left serial33/ACK34 opened A; real left serial42/ACK43 opened B. +The wire chain was TOPLEVEL XDG9 → A XDG12 → B XDG15. Configure/ACK/map and +wire/client observations corroborate both valid grabs. Mosaic A was +`[-218,105,350,170]`, B `[-118,165,300,130]`. + +| Settled capture | H1 A | H1 B | H2 A | H2 B | Whole H2 reference equality | +| --- | ---: | ---: | ---: | ---: | --- | +| Mosaic | 24080 | 15340 | 0 | 0 | Exact | +| Standard A+B | 28880 | 25740 | 3120 | 13260 | Ordinary overflow expected | +| Standard after explicit B close | 50660 | 0 | 8840 | 0 | Ordinary A overflow expected | +| Final owner close | 0 | 0 | 0 | 0 | Exact | + +Targeted `[con_id=9] layout splith` request `191234782728967` received accepted +reply `191234785087227`. First applied standard root was `191234987463354`, +stabilized root `191235601727206`; four more standard callbacks through B input +retained absent tiled/root clips. Owner/root/workspace/output and mapped popups +were preserved. Standard content origin became `(-398,25)` with the same A/B +rectangles as the old standard replay. Standard H2 popup pixels are expected +ordinary overflow, not a reference-equality failure. + +Measured B point `(10,175)` delivered right pair53/54 at local `(208,10)`. +Explicit `close_b` produced exact wire destruction and `COMMAND close_b DONE`, +with no `popup_done`. A retained the same mapped objects and pixels. A new +post-close capture selected point `(10,115)` and right pair59/60 at local +`(308,10)`. Normal `close_owner` destroyed A before TOPLEVEL and exited0. +Final H1/H2 contain zero A/B colors. Reference1, reference2, Mosaic H2 and final +H2 all have PNG SHA256 +`4471e93ccb6cf3ff4970f61fce405431a228af598d2f1f030a3b7586962ecd7a`; +all 480000 RGB pixels match without mask or tolerance. Twelve captures and +six exact A/B geometry/occlusion masks were independently verified. + +Runtime was 13.05365723601426 seconds of 150: run1, chain1, grabs2, complete +clicks4, captures12, camera1, transition1, positions5/16. Runner and dispatch0; +terminal IPC1 retained stderr `Unable to receive IPC response`; compositor, +E2, filler, reference and pointer0. No rescue/watchdog was needed; private +runtime `/tmp/msng-znx8zrw4` disappeared and owned PIDs were reaped. Response +loss during normal shutdown is compatible with these observations, not causally +proven. Two startup EGL_BAD_ALLOC enumeration entries are retained; they did +not prevent the measured matrix or normal exits. The 256 damage traces are +sampled coverage, not complete damage or atomic-generation proof. +Independent C review ACCEPTED the FEASIBLE finding; final tester C has 28 PASS. + +### Phase D — private initial candidate BUILDABLE, annotated delta NOT REBUILT + +C's accepted FEASIBLE gate enabled the conditional private integration dossier +at `LAB/integration-candidate/`. Pinned sources: SwayFX +`fd71a6bdc061bd633b488ae7b83e8a6981d22f86`, Mosaic +`cd550382b311efdbfe3ed892f4e1d281d852e754`, SceneFX +`37ccd723bef49e6891156ffafce8f549f01446cc`, wlroots +`d783533489e1f75d6886c2ab5c5960090ef268f8`. Archive reconstruction and eight +historical patches passed applicability and source-equivalence checks. +One SceneFX static build, one Mosaic build, one model test1/1 and one DESTDIR +stage passed. Zero graphical runs and zero installations occurred. +The private initial binary is +`616aa504334d88aada5b1596df624957f39b0f1ae975a1a04105fbf73aea6dd0`, +build ID `a0a46860f3b0f1b9dcc7f9044acca7f8489cceec`; it differs from C and +is graphically **NOT TESTED**. Private cumulative SwayFX patch SHA256 is +`8acde9a9755e0db16fee4c49c479649414bd23f0dab399e4e6a4aa2e1053b1d3`, +SceneFX `768372773f8755083aa02be5547863b1269ef9facb2131b0a7acc5574c822856`. +These private patches are not published in the fixture or production patch. +No additional Meson delta was needed beyond the existing private static +pkg-config selection. `PKGBUILD.future` syntax passed; recipe execution is +NOT RUN and the production PKGBUILD remains unchanged. + +The complete stage has exactly five regular owned files: + +- `/usr/bin/swayfx-mosaic`; +- `/usr/share/licenses/swayfx-mosaic/INCORPORATED-NOTICES`; +- `/usr/share/licenses/swayfx-mosaic/SceneFX-LICENSE`; +- `/usr/share/licenses/swayfx-mosaic/SwayFX-LICENSE`; +- `/usr/share/licenses/swayfx-mosaic/wlroots-LICENSE`. + +The one-binary Meson manifest is the intermediate stage before notice addition; +the final five-file manifest is complete. An initial tester concern about absent +notices was corrected against that final manifest. Relocation checks pass; +85 overlapping symbols are characterized, 17 structs/six offsets were locally +checked with C17 and 71 transitive libraries inventoried. Semantic ABI maintenance +is **NOT ESTABLISHED**. Conservative notice inventory covers 895 inputs, +599 headers,12 shaders,7 XML,16 generated inputs and282 full notice blocks. +Exact extraction/header obligations and legal release clearance remain open: +legal compliance **NOT APPROVED**. Named release/security roles remain required. + +The initial independent D review was CORRECTION_REQUIRED on P2 source comments. +One bounded `comment-repair/` derivation annotated four files in two private +patches: SceneFX +`21f057ff4180bb5dd35af35070aa4a95923be83f81abb864793e5bfb93643b01`, +Mosaic `7fee137101abc0cd5024cea172c5904268ddf995bc5855c570e4dfabd643c970`. +Lexical checks preserve all active C tokens; exact patch application and parent/ +annotated hashes pass. Delta review ACCEPTED / P2 RESOLVED. This is +**COMMENT-ONLY / NOT REBUILT**: the original sources/builds/binary/manifests are +unchanged; inserted lines may affect `__LINE__` diagnostics. The initial recipe +was neither amended nor executed for these annotated copies. A future annotated +recipe must add both patches after the eight historical patches and requires +separate rebuild authorization. D BUILDABLE means initial build evidence plus +resolved comment obligation, never a compiled annotated candidate. +The sealed `results/final-gate.json` retains its original review-PENDING +snapshot. The authoritative appended `/batch/phase-d-gate.json` records the +accepted final D gate without rewriting that snapshot. + +### Provenance and independent review bindings + +`/batch` below means `/home/fy59/swayfx-mosaic-e2-batch-hyrd8qm1`; `LAB` means +the sole C laboratory above. All referenced private manifests and entries were +verified; artifacts, raw logs, PNGs, builds and integration patches remain +outside Git. The 23 accepted fixture source files, including the original README, +remain byte-identical. The current results README is a documentation append, +not a modification of the reviewed runtime inputs. + +| Record | Entries / status | SHA256 | +| --- | --- | --- | +| B/C source handoff | 73 verified entries | `b90bd1564acfdd78276843a5fe5ddb2ba1b5fb75fdd5f097fa64f073d0a89cbc` | +| C evidence | 66 verified entries | `1f3723db89a6a5dca05f094d8e7c370feebcc640c922838b7abecb99167b6067` | +| D initial source | 1003 verified entries | `fca097b0376694fd563b33fb49923086a19d1a5a9cfe5ba9fc045707b07bc61f` | +| D initial evidence | 172 verified entries | `4be20af63e0ee4f4b5c2187584bbb7087ece1be5cd9d5576246ae5a62eda2957` | +| `/batch/review-b-report.md` | ACCEPTED | `eddefb3dfbde5410c32b1addf940977d0b2a176c91009597bf4130ba4929ef82` | +| `/batch/review-c-report.md` | ACCEPTED / FEASIBLE | `e506b42e64a18fd237cea5199f67d49745436ee9a10f01798f07eb2072050f68` | +| `/batch/review-d-delta.md` | ACCEPTED / P2 RESOLVED | `6a7299e530ce3abde51997572b0bccccc61c5d88180c15925dcecc6cb76a32d7` | + +The original D review `/batch/review-d-initial.md`, earlier tester failures, +raw terminal failures and historical fixtures remain available unchanged. + +### Consolidated boundary and next gate + +| Domain | Evidence now recorded | Remaining qualification | +| --- | --- | --- | +| Static tiled clip | Prior static RGB/input/standard retest | Wider graphics/daily use NOT QUALIFIED | +| Static grabbed popups in Mosaic | Historical E1 and new C E2 sampled pixels/hierarchy/grabs | Other popup families/geometries NOT TESTED by C | +| Static standard return/input/lifecycle | C E2 FEASIBLE, B then A input and explicit normal close | Universal click-through/final popup UX UNDECIDED | +| Hidden-owner guard | Historical private evidence | New static C hidden-owner matrix NOT TESTED | +| Saved-buffer continuity | Separate historical saved-target evidence | Static C continuity NOT TESTED; lifetime gate open | +| Blur/shadow/effects | Historical PARTIALLY_FEASIBLE with retained limits | New static C effects NOT TESTED | +| Scale/rotation/vertical geometry | Historical bounded private probes | Generalized static pair NOT QUALIFIED | +| Output migration/resize | Historical bounded ownership evidence | Static C/D migration NOT QUALIFIED | +| Damage/stale pixels | Settled restoration and 256 sampled C traces | Complete/intermediate-frame coverage NOT QUALIFIED | +| Transaction generation | Ordered applied observations | Atomic transaction/render/input identity NOT ESTABLISHED | +| ABI and maintenance | D local header/symbol/relocation characterization | Maintained semantic ABI, upgrades/security gates open | +| Private static packaging | D initial BUILDABLE, exact five-file stage | Annotated source NOT REBUILT; recipe NOT RUN; legal NOT APPROVED | +| Production integration/distribution | No source/package/UX change | STEP20C NEXT, production multi-output NOT YET SUPPORTED | + +Phase E documentation prepares the authorized second publication with intended +message `test(mosaic): qualify static nested standard return and integration readiness`. +The final deep batch audit is PASS / ACCEPTED, with no BLOCKING finding. +Normal publication identities are recorded by the accompanying Git commit and +`/batch/closeout.json`; no self-referential commit SHA is embedded here. Any graphical qualification of the D binary or annotated rebuild +requires a separate bounded authorization; C's result cannot qualify D. +STEP20A DONE; STEP20B EXPERIMENTAL; STEP20C NEXT; STEP20D–G PLANNED. + +## Final audit and gate record + +The independent Astra final review is preserved at `/batch/final-review.md`, +SHA256 `088f1b0e9d32095d20c842400d865a3e880cf122ce51b952cbf342c99ee3fe3f`. +It reran 88 pure tests, recalculated all claimed A/C/D manifest entries and +C's actual geometry/RGB/input/lifecycle evidence, and accepted publication. +No additional runtime, build or staging was executed during closeout. + +```text +STATIC_NESTED_RUNTIME_FREEZE=PASS +E2_TEMPORAL_REPLAY=PASS +E2_TEMPORAL_NEGATIVES=PASS +E2_ASSERTION_AUDIT=PASS +E2_OFFLINE_TESTS=PASS +E2_INDEPENDENT_REVIEW=ACCEPTED +STATIC_E2_RUNTIME_PREPARATION=PASS +STATIC_E2_RUNTIME_HANDOFF=PASS +SWAYFX_MOSAIC_STATIC_E2_COMPLETION=FEASIBLE +STEP20C_PRIVATE_INTEGRATION_CANDIDATE=BUILDABLE +``` + +BUILDABLE refers to the initial D build; the accepted annotated source delta +remains COMMENT-ONLY / NOT REBUILT. Final remote identities and publication +outcome are recorded separately in `/batch/closeout.json` after direct checks. diff --git a/tooling/probes/step20c-static-e2-completion/README.md b/tooling/probes/step20c-static-e2-completion/README.md new file mode 100644 index 0000000..b0f9f60 --- /dev/null +++ b/tooling/probes/step20c-static-e2-completion/README.md @@ -0,0 +1,105 @@ +# Static E2 completion fixture + +**EXPERIMENTAL / PREPARATION; runtime NOT RUN, review PENDING.** +Ticket `SWAYFX_MOSAIC_STEP20C_E2_COMPLETION_AND_PRIVATE_INTEGRATION_BATCH_V1`. +No production source, UX, packaging or cumulative patch change. STEP20C stays +NEXT; production multi-output is NOT YET SUPPORTED. + +`derivation.json` identifies initial exact copies from the frozen +`step20c-static-nested-geometry-replay` fixture at +`c5f96a34e3924275179365938d02f3a318c0c4bf`. Model, IPC adapters, observations, +pixels, predicates and core geometry/protocol/terminal tests are retained. +`reusable_tests.py` preserves the identified AST method bodies. The historical +readiness aggregate and its ACCEPTED review grant no acceptance here. + +`transition.py` is shared by raw historical replay and the new E2-only runner. +It examines the entire selected H1 callback window: old applied Mosaic, +targeted accepted splith, first coherent applied standard, then at least two +coherent standard observations. It verifies source-backed `current` layout, +actual owner scene, exact workspace/output/XDG identities, unique enabled root, +clip removal, capture intervals, and no reactivation through standard B input. +PI_POPUP lacks owner IDs and reports the global popup root on both outputs. +Unique live XDG hierarchy plus exact IPC ancestry supplies bounded correlation; +independent observations do **not** establish an atomic transaction generation. + +Historical request-start time was not recorded. `replay.py` uses the previous +actual capture event and first monotonic trace after actual command handling as +an explicit request interval. It preserves the old applied root after handling +and before reply, followed by two cleared roots; no row is discarded. The +historical cleanup was compositor shutdown, corroborated by seat capabilities0, +then destruction. It does not qualify normal owner close. Replay cuts only at +that explicit shutdown boundary, retaining any earlier dismissal/destruction. + +The new runner records exact IPC request start and actual reply separately. +Its transition window starts at a complete source-backed Mosaic commit after +A/B are fully mapped and before the request; earlier pre-root callbacks stay +in separate opening evidence. Absence of an eligible callback is INCONCLUSIVE. +Only E2 runs: real left TOPLEVEL opens A, real left A opens B, right B in standard, +explicit client close B, then a fresh measured A right click. Each point comes +from a measured solid interior with an8px margin. Every click records fresh +client protocol deltas, full button pairs and wire/callback coordinates. +Configured/applied destinations remain distinct from native420x320 SHM; an old +Mosaic observation is not reparsed with a later standard configure. Client +close requests are recorded before flushing stdin. B destruction and COMMAND +close_b DONE must match exact objects, without popup_done. A preserves map/IDs, +exact pixels on both outputs, zero B colors and input. Normal owner close must +be corroborated by command, child-first destruction and wire destroy requests. +Final whole H2 RGB equals the initial #303038 reference without mask/tolerance; +final H1 has no popup colors. Raw terminal IPC/compositor/client codes and rescue +are retained separately. Terminal IPC1 alone does not undo completed science. + +Budget: run1, chain1, grabs2, complete clicks4, camera1, transition1, PNG12, +positions16. Maximum150s; active work watchdog cuts at143s to leave cleanup +margin. Two full H2 references are identical. Runtime is0700, private two-output +headless H1(-800,0)/H2(0,0), 800x600 scale1 normal, with explicit private swaymsg +socket. No pristine control, compositor/SceneFX build or desktop operation. + +## Offline preparation + +All outputs below are in a **new** directory outside Git. Commands never reserve +a real runtime budget, launch a client/compositor or change historical LAB bytes. + +```sh +F=tooling/probes/step20c-static-e2-completion +O=/home/fy59/swayfx-mosaic-e2-batch-hyrd8qm1/implementation-final-offline +mkdir "$O" +PYTHONDONTWRITEBYTECODE=1 python "$F/tests_offline.py" --output "$O/test-receipts.json" > "$O/tests.log" 2>&1 +PYTHONDONTWRITEBYTECODE=1 python "$F/replay.py" --output "$O/replay.json" +PYTHONDONTWRITEBYTECODE=1 python "$F/audit.py" "$O/source-audit.json" +PYTHONDONTWRITEBYTECODE=1 python "$F/gate.py" "$O" +``` + +The gate requires nonempty exact test inventory, actual executed PASS receipts, +every temporal negative rejection, final source hashes, raw replay equality and +an exhaustive current static assertion inventory. Audit PASS means classified +source inventory; graphical execution coverage remains NOT TESTED. +Negative handoff tests simulate records only in temporary isolated directories; +these are explicitly synthetic and never mint a real independent review. + +`handoff.py prepare "$O"` creates the sole fresh registered LAB, exact source +manifest and prospective handoff. It reuses the historical exact C17 clients +and their actual adapted static_cross/protocol sources byte-for-byte; no build +or client execution is required. Static binary identity remains +f82557c1a9978a5e9665b9b4291b74880253725359e3df1c5b8d554921f7603b, +build ID942db64269640778ee433e5970ba1100bc60f94c, mapped wlroots identity +7edca9a7d65855addda68e29ba5db94d57c803eb5fa7d0f86db3136b90ca9837. +The original source directory is part of the frozen runtime manifest, alongside +installed fixture, binaries/clients, actual private trace source and receipts. + +## Independent review before dispatch + +The implementation author does not generate acceptance. A genuine independent +read-only reviewer must produce a report and `results/handoff-review.json` with: +status ACCEPTED, independent=true, read_only=true, +scope STATIC_E2_COMPLETION_FINAL_BYTES, blockers=[], fixture_sha256, +source_sha256 (all final source files), runtime_handoff_sha256, +authorization_sha256, registration_sha256, gate_sha256, report_file and its +report_sha256. These bind the actual final files, not a historical gate. + +After parent approval, `handoff.py preflight LAB` publishes an exact preflight. +Only then may the parent run `launch.py LAB` once. Exclusive O_EXCL consumed token +precedes dispatch. Runner repeats reviewed source preflight and checks the exact +consumed token before its lock or run debit. Wrong/old LAB, token, binary, +wlroots, stale review, changed manifest/runner, existing run/socket and prior +consumption fail closed. Failure never refunds the ticket; no automatic retry. +No runtime dispatch has been performed during implementation. diff --git a/tooling/probes/step20c-static-e2-completion/README_RESULTS.md b/tooling/probes/step20c-static-e2-completion/README_RESULTS.md new file mode 100644 index 0000000..513d47f --- /dev/null +++ b/tooling/probes/step20c-static-e2-completion/README_RESULTS.md @@ -0,0 +1,60 @@ +# Static E2 completion — current results + +**EXPERIMENTAL. B PASS; C FEASIBLE for the tested E2 matrix; D initial candidate +BUILDABLE with accepted COMMENT-ONLY / NOT REBUILT delta.** Final batch audit +is PASS / ACCEPTED, with no BLOCKING finding. Publication identities are +recorded in Git history and the private closeout receipt. + +The [original README](README.md) is the **sealed pre-execution protocol**. +Its PREPARATION / runtime NOT RUN / review PENDING statements record the state +before execution. It remains byte-identical, together with the 23 accepted +fixture sources, to the 73-entry runtime handoff manifest SHA256 +`b90bd1564acfdd78276843a5fe5ddb2ba1b5fb75fdd5f097fa64f073d0a89cbc`. +Use this results README and the +[completion report](../../../docs/design/STEP20C_STATIC_E2_TRANSITION_AND_COMPLETION.md) +for current evidence, provenance, exact hashes, failed attempts and open gates. +Do not rerun the runtime commands: the sole launch token and run budget are +consumed, and post-run refusal was verified. Historical fixtures/LABs remain +unchanged and historical INCONCLUSIVE results are not retroactively upgraded. + +The shared transition validator passes 104 targeted offline tests, including 17 +negative temporal/provenance cases and 15 synthetic handoff tests. Independent B +review ACCEPTED; the 277-site static audit is not runtime execution proof. An +attempted fresh setup of the old 206 suite retained 145 PASS/61 setup errors; +that limitation is separate from the accepted104 gate and historical206 result. + +C used the unchanged static binary +`f82557c1a9978a5e9665b9b4291b74880253725359e3df1c5b8d554921f7603b` +once in `/home/fy59/swayfx-mosaic-e2-completion-dk1hq9an`, on 800×600 scale1 +normal outputs at x=-800/0. Two genuine grabs formed TOPLEVEL→A→B. Mosaic +retained H1 popup pixels and exact full H2 RGB. Ordered applied splith observations +removed tiled/root clips while preserving live popups. Measured standard B input, +explicit B destruction, A survival and measured A input passed; normal owner close +restored all 480000 H2 RGB pixels. Standard H2 overflow is expected. Independent +C review ACCEPTED and tester final28 PASS. Runner/dispatch0, terminal IPC1 +(`Unable to receive IPC response`), compositor/clients0 and two startup +EGL_BAD_ALLOC enumeration entries remain recorded. Shutdown response-loss +causality is compatible but NOT PROVEN. Runtime 13.053657s/150; run1, chain1, +grabs2, clicks4, captures12, camera1, transition1, positions5/16; no rescue. + +D is entirely private under that LAB's `integration-candidate/`. One SceneFX +build, one Mosaic build, model1/1, relocation/header checks and one DESTDIR stage +pass, with zero graphical runs/installations. Initial binary +`616aa504334d88aada5b1596df624957f39b0f1ae975a1a04105fbf73aea6dd0` +is distinct from C and graphically NOT TESTED. Stage contains exactly the binary +and four full notices under `/usr/share/licenses/swayfx-mosaic/`. The initial +future recipe has syntax PASS and execution NOT RUN; production PKGBUILD is +unchanged. P2 comments were corrected in four private annotated copies with two +patches, accepted independently as COMMENT-ONLY / NOT REBUILT. Initial builds, +recipe, sources and manifests remain unchanged. A future annotated recipe must +apply both comment patches after the eight historical patches, then undergo a +separately authorized rebuild. The original review-PENDING D final-gate snapshot +is sealed; `/home/fy59/swayfx-mosaic-e2-batch-hyrd8qm1/phase-d-gate.json` is the +appended accepted gate. Integration patches are not copied into this fixture. + +Complete damage/intermediate frames, atomic generation, broader output geometry, +migration, hidden-owner/effects/saved-buffer static coverage, maintained semantic +ABI and legal notices remain open. Legal compliance NOT APPROVED; release/security +names remain required. No production/UX/packaging contract changes. STEP20A DONE; +STEP20B EXPERIMENTAL; STEP20C NEXT; STEP20D–G PLANNED. Production multi-output +NOT YET SUPPORTED. Raw logs, PNGs, binaries and builds remain outside Git. diff --git a/tooling/probes/step20c-static-e2-completion/audit.py b/tooling/probes/step20c-static-e2-completion/audit.py new file mode 100644 index 0000000..3311519 --- /dev/null +++ b/tooling/probes/step20c-static-e2-completion/audit.py @@ -0,0 +1,52 @@ +"""Exhaustive assertion inventory for the derived E2 runtime, offline only. + +PASS means classified source inventory, not executed runtime assertion coverage. +Independent review must inspect these final bytes before dispatch. +""" +import ast +import hashlib +from pathlib import Path +from model import require +from support import sha +HERE=Path(__file__).resolve().parent +RUNTIME=('run.py','model.py','observations.py','ipc_predicates.py','predicates.py','pixels.py','transition.py','lifecycle.py','authorization.py','ledger.py','support.py','handoff.py','launch.py','gate.py') +DOMAINS={'run.py':'runtime orchestration and raw outcomes','model.py':'typed coordinate and provenance contract', + 'observations.py':'native SHM vs applied destination geometry','ipc_predicates.py':'IPC identity and applied trace adapters', + 'predicates.py':'RGB/input/XDG protocol/unique-root/terminal observations','pixels.py':'exact RGB geometry', + 'transition.py':'current applied temporal transition','lifecycle.py':'explicit close and input lifecycle', + 'authorization.py':'reviewed dispatch before runtime','ledger.py':'nonrefundable action accounting', + 'support.py':'private environment/source integrity','handoff.py':'exclusive prospective source/review binding', + 'launch.py':'exclusive single dispatch','gate.py':'semantic offline receipts'} + +def audit(): + records=[] + for name in RUNTIME: + source=HERE/name;tree=ast.parse(source.read_text()) + for n in ast.walk(tree): + if isinstance(n,ast.Assert) or (isinstance(n,ast.Call) and isinstance(n.func,ast.Name) and n.func.id=='require'): + records.append(dict(file=name,line=n.lineno,domain=DOMAINS[name], + predicate=ast.unparse(n),ast_sha256=hashlib.sha256(ast.dump(n,include_attributes=False).encode()).hexdigest(), + runtime_execution='NOT TESTED',review='PENDING_INDEPENDENT_REVIEW')) + run=(HERE/'run.py').read_text();tree=ast.parse(run) + require('E1' not in run,'E1-runtime-entirely-removed') + calls=[n for n in ast.walk(tree) if isinstance(n,ast.Call) and isinstance(n.func,ast.Name)] + require(sum(n.func.id=='open_chain' for n in calls)==1,'single-runtime-chain') + require(sum(n.func.id=='click' for n in calls)==4,'four-complete-click-sites') + require(sum(n.func.id=='validate_applied_standard_transition' for n in calls)==2,'transition-through-B-input') + require(all(name in run for name in ('opening_pair','explicit_child_close','a_survival','normal_owner_close','final_restoration')),'E2-full-path-shared-predicates') + require("global baseline" in run and "if name=='E2':" in run,'reference-relocated-to-E2') + require('applied_model(owners' not in run,'old-configure-not-reparsed-as-current') + require(bool(records),'nonempty-source-audit') + return dict(status='PASS',scope='EXHAUSTIVE_STATIC_ASSERTION_INVENTORY; runtime NOT TESTED', + sources={n:sha(HERE/n) for n in RUNTIME},records=records, + reviewed_hazards=['native SHM420x320 vs configured/applied content; no painted-height=320 predicate', + 'IPC y is not content origin; scene+titlebar+window offset is explicit', + 'current PI_SCENE and actual clip/owner trees; pending and IPC reply insufficient', + 'old Mosaic geometry reused directly, not reparsed using later standard configure', + 'root traces have no owner ID; only bounded unique-root XDG/IPC correlation', + 'right input pairs and left opening serials are different predicates', + 'actual terminal IPC code and each owned process code stay separate']) + +if __name__=='__main__': + import json,sys + with Path(sys.argv[1]).open('x') as out:json.dump(audit(),out,indent=2);out.write('\n') diff --git a/tooling/probes/step20c-static-e2-completion/authorization.py b/tooling/probes/step20c-static-e2-completion/authorization.py new file mode 100644 index 0000000..1dd8b6a --- /dev/null +++ b/tooling/probes/step20c-static-e2-completion/authorization.py @@ -0,0 +1,10 @@ +"""Fail closed before any lock, ledger debit or subprocess.""" +import json +from pathlib import Path +from model import require + +def authorize(lab,authorization): + require(authorization is not None,'explicit-authorization-required') + from handoff import preflight + preflight(lab,authorization,review=True,consumed=True) + return json.loads(Path(authorization).read_text()) diff --git a/tooling/probes/step20c-static-e2-completion/client-contract.json b/tooling/probes/step20c-static-e2-completion/client-contract.json new file mode 100644 index 0000000..f295d2d --- /dev/null +++ b/tooling/probes/step20c-static-e2-completion/client-contract.json @@ -0,0 +1,13 @@ +{ + "status": "EXPERIMENTAL exact retained historical client/protocol byte contract", + "source_receipt": "client-provenance.json in offline LAB; scanner must reproduce exact hashes", + "adapted_nested_sha256": "6eafb8ad63b0b06b8297e7e72711056a6d42bdf33ccf795793eeda77f98221c7", + "placement_sha256": "e0e87fb0cb3d6e7dafb1cd59ccb50f323f2934e65247526199da1695487d3e4a", + "protocols": { + "xdg-shell-client.h": "a09726137ea9fe1a23d58c95f1d33ab77a3fc692c63726bc82fbc91ab8e5c6fc", + "xdg-shell-protocol.c": "9e12fcd5676d8c6b6cfd233c2eba563163c0ea7e586cb6e27f202a328323c7f0", + "virtual-pointer-client.h": "0dcb6107cf948a007dcef0a6b2192e4f7b9166c0f57b499ca49b7aaf48adc56c", + "virtual-pointer-protocol.c": "a72e9b38dd5ed0c49fae22aa2f01883aea76075ad33f6399d9b2033df4330675" + }, + "native_nested_sha256": "2e40c98af72a0df8b316810589a0be8f4425a763db1de448b49a7813956b6f7d" +} diff --git a/tooling/probes/step20c-static-e2-completion/derivation.json b/tooling/probes/step20c-static-e2-completion/derivation.json new file mode 100644 index 0000000..fc42c90 --- /dev/null +++ b/tooling/probes/step20c-static-e2-completion/derivation.json @@ -0,0 +1,113 @@ +{ + "status": "EXPERIMENTAL; historical acceptance does not apply", + "base_commit": "c5f96a34e3924275179365938d02f3a318c0c4bf", + "copies": [ + { + "file": "model.py", + "source": "tooling/probes/step20c-static-nested-geometry-replay/model.py", + "sha256": "a578fc589fb092dae3ccdcf8c4eec12a97e515f721decc4c968078dedcb2367a", + "kind": "identifiable initial copy; see final diff" + }, + { + "file": "observations.py", + "source": "tooling/probes/step20c-static-nested-geometry-replay/observations.py", + "sha256": "3a9193bb4221c2da00c3231d60bf90b51403833d06e0178b87f86dd44c9251db", + "kind": "identifiable initial copy; see final diff" + }, + { + "file": "ipc_predicates.py", + "source": "tooling/probes/step20c-static-nested-geometry-replay/ipc_predicates.py", + "sha256": "4e0615d155183d841543131bf540056a2c2d49eb23f64997a6867d796633548c", + "kind": "identifiable initial copy; see final diff" + }, + { + "file": "pixels.py", + "source": "tooling/probes/step20c-static-nested-geometry-replay/pixels.py", + "sha256": "d315ddad978238ffbf1c8e4d712d4608ba23564703332d814f627f57f7b6e7f5", + "kind": "identifiable initial copy; see final diff" + }, + { + "file": "predicates.py", + "source": "tooling/probes/step20c-static-nested-geometry-replay/predicates.py", + "sha256": "1435832e61d9694fc80d429281d6b6a18b11f56a97d7ea4fee8d117e6953ba72", + "kind": "identifiable initial copy; see final diff" + }, + { + "file": "support.py", + "source": "tooling/probes/step20c-static-nested-geometry-replay/support.py", + "sha256": "84687f5421ad8ef5d2e6bf1eb334fa5377ed9c59741a2cd593c043cf63c5fd76", + "kind": "identifiable initial copy; see final diff" + }, + { + "file": "ledger.py", + "source": "tooling/probes/step20c-static-nested-geometry-replay/ledger.py", + "sha256": "3492ed714780d94fd72f9bbc0a7dc27586c62ee4955a2570ad15ec3809c5e12d", + "kind": "identifiable initial copy; see final diff" + }, + { + "file": "run.py", + "source": "tooling/probes/step20c-static-nested-geometry-replay/run.py", + "sha256": "4382f2c0ffc8a41a6a65aecfca72dff762ded528f667288d179acbb354a9fcc9", + "kind": "identifiable initial copy; see final diff" + }, + { + "file": "placement.patch", + "source": "tooling/probes/step20c-static-nested-geometry-replay/placement.patch", + "sha256": "e0e87fb0cb3d6e7dafb1cd59ccb50f323f2934e65247526199da1695487d3e4a", + "kind": "identifiable initial copy; see final diff" + }, + { + "file": "client-contract.json", + "source": "tooling/probes/step20c-static-nested-geometry-replay/client-contract.json", + "sha256": "f330328c9f7965aed06cf73693054631847eceef39de18f2978b27120a645613", + "kind": "identifiable initial copy; see final diff" + } + ], + "exact_reusable_test_methods": { + "setUp": "a14a5a3abba8b66055da064bdfc63ae75653c3ca35aed826f981a801b85f72e8", + "tearDown": "f311acf2dab71c6461a030efaec2062641116bb51e32fc49f8ee0b4643d21557", + "reject": "32eb998c52f371a172b64e21ef149c0e9df16efec884244d4e7ce80961f249ce", + "test_positive_geometry": "de5050007c1ae5833c57cfd8fa292f4b9e7b7e91ac63547ba6789c0c70bd7a69", + "test_negative_native_height": "cb1b1da00973603eebc33464cfcb31dc0385263af45b54c876952d8e6f990625", + "test_negative_scene_window_y": "124a051ce6e7e9d44efb10536c99fcedc9122255fe74f63fcda56483135866dd", + "test_negative_titlebar": "59d2e6723e6aae2500f6ac290eeea0bdfca78da8ec9b23b66aa0678d44f90217", + "test_negative_output_origin": "88d8c9c9a497f35f4db87085e15a32ea7c8c91e6ea5106b3a1f20058c518d84b", + "test_negative_png_space": "52bfb9216cab05c0b75db3aba426610986ec0618d4146b9049023c8c303c01fa", + "test_negative_png_bounds": "7096414bdf77bcf91a341cc2ed4884356090aa29f5ba5bffb6c9e1d6705bb981", + "test_negative_assignment": "bdf5b8441835124a327b8108cd5d6c25a5d91de5106672d02d17ec33ca9605aa", + "test_negative_workspace": "28a098ea5b234dbba4ca93dff5599e94bbff355893cca4d1074f87d71ff6b102", + "test_negative_owner": "a06eff220c9af175bb7278bca28c8cd874305f846a673402f157304dc05b4719", + "test_negative_old_camera": "2bdf861835107873f2a6ed9d9417da1213139c29ad502c09a6964ed6efe558db", + "test_negative_local_global_clip": "187bab95f02f45a6de3b9dd0415c950690e3e947c04319a068a39997b7387ffa", + "test_negative_buffer_identity": "81699b15d784ce23f1bbc4565a1b669afb49d98580ebb722949e0dc7c74b955c", + "test_negative_pending": "602a0ec9d828adf7351b8012d958b84be5395eb754c01b9513753f28aa39f8f7", + "test_negative_scale": "8833c6bc39c9b460ad1e9cd00fb0da1a02f2edbba1631081f2df98cedd1c107c", + "test_negative_transform": "392ef3eb03a7938b038e1b2bf5db405950a7a0f4c6829e63d80dddc34ed111fc", + "test_positive_popup": "c622548815bf95fac5b28f56d490e0f9c68955ef86c1009f3d3dc9940a2b92c3", + "test_negative_parent": "62a71f2976d9e32acf870c03dd031fad8a3427366304662d78be19ee686f22f9", + "test_negative_ack": "502d89e4e302adb14d3a52bfbdf6eca59cf46f75ef7825ad73bde90dc2d0b23a", + "test_negative_map": "e73c12f816d8832d00d8498bcbb777f8d8a910a1fad1b9c72750180516ff9bb7", + "test_negative_constraints": "d723171a9897f3dc316b4e4691a1ac99a3b3e47fac2d7f518f3b0f640e018830", + "test_negative_pointer": "c8b574c028e4cac0ec7e4148c5a8fc52c97138e5bcce3d18be1b4bae486ceada", + "test_negative_occlusion": "7bcbcf61ebb34e2196f296c4ca0000aaaf8070a7bdb68afb43473d4e0e19a956", + "test_negative_standard_stale_origin": "0dc996c2c9684e446383637aa0b4c6eec4a5ee2c98deaf672a4b0df19a326e2c", + "test_positive_standard_fresh": "2803ac768aad04f0af424876c04147e852b9351255ca4b92c40945475ac17d39", + "test_negative_E2_identity": "2f9ce9a4afdd72e7e7c4cba0b7cdb9ecb64c98ef84aae863e7db8632806e9a0a", + "test_negative_E2_serials": "c9950830e1c4244aec26a4fc3768c9db4e0d0b17fc24c3317c7f28e23f24ac65", + "test_positive_E2": "e20673fbed4498af2b0698d32fb51b57e56137de37bddba70af5e5d6ddc3f655", + "test_positive_terminal": "b37653a245fab18cc21af8a32c76827ba3ec8bda1e503822d6100274b52d9916", + "test_negative_terminal_incomplete": "68f75b8803fea6d62cb28baef6de8a5a80ec9dd0d91386f0c2bb93b957c335e5", + "test_negative_terminal_unhandled": "f7348d3486097e8ef162b7fe70931af3a668724de97de59fb1c106f0ec449547", + "test_negative_terminal_compositor": "1d58c53f4dd048068e747d77174ccb27cc12791bf05f29909f1f939923cb5be0", + "test_negative_terminal_protocol": "aa0fcdbe99b83282f6c7571e9d29b15be8140fc887bc35873905dd93cb9db878", + "test_negative_terminal_rescue": "2de8c7b755bc94167a8d4de40d721030cc41a6aa75fb46f2d761cd747b79f4af", + "test_negative_dismissal_hit": "d5ca0ee4a496a30072ea873683731b1e9a2ff6c3ed7a16259d43cd244ac0d942", + "test_negative_dismissal_incomplete": "559d5ac3b5d46a93b7cbf9c2f4d9f42d801d8da0a87e12102aaf4173da49980d", + "test_positive_dismissal": "8bc890b903eef26d0cab7a2c3c3248eed33f5ff58b64a5bd25bc79abd33a4bd1", + "test_positive_button": "7a320d9fab94dfc9ab483fcb1ba5f584bf9e39d2ec99bb21e03f5e247e8fa6b5", + "test_negative_button": "9511648c2dce6549036e1fa87f5d9b14cd920af0d3b7cf316bbcdbaa5e703726", + "test_negative_root_ambiguous": "6aaf85c8ab2f4e40ecc99db1e5601372313d9fbbc64d27676881337211f2d7f5", + "test_positive_root": "af8106e6e5264e21c9257f1af7fd591c2e5f2849033f9317ac6e6e35c4619312" + }, + "test_source": "tooling/probes/step20c-static-nested-geometry-replay/tests_offline.py" +} diff --git a/tooling/probes/step20c-static-e2-completion/gate.py b/tooling/probes/step20c-static-e2-completion/gate.py new file mode 100644 index 0000000..9df8432 --- /dev/null +++ b/tooling/probes/step20c-static-e2-completion/gate.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +"""Generate a prospective gate only from actual nonempty executed receipts. + +WHY: empty PASS and historical acceptance must not grant new readiness. +CONTRACT: final source hashes + complete tests + actual raw replay + AST audit. +INVARIANT: this gate is READY_FOR_REVIEW; independent acceptance is separate. +""" +import json +from pathlib import Path +from model import require +from support import sha +HERE=Path(__file__).resolve().parent + + +def normalized(value):return json.loads(json.dumps(value)) + + +def sources():return {p.name:sha(p) for p in HERE.iterdir() if p.is_file()} + + +def validate_test_receipts(receipt): + from tests_offline import expected_tests + expected=expected_tests() + rows=receipt.get('tests') + require(receipt.get('status')=='PASS' and isinstance(rows,list) and len(rows)==len(expected) + and receipt.get('executed')==len(expected) and receipt.get('failures')==receipt.get('errors')==receipt.get('skips')==0, + 'complete-executed-test-receipts') + require({r.get('id') for r in rows}==expected and all(r.get('status')=='PASS' for r in rows),'exact-test-inventory') + require(receipt.get('sources')=={p.name:sha(p) for p in HERE.glob('*.py')},'tested-final-source-bytes') + negative=['retained_clip','no_standard_epoch','wrong_layout','wrong_owner','wrong_root','wrong_output', + 'reversed_times','standard_before_application','clip_reactivation','missing_root_traces','multiple_roots', + 'wrong_capture_epoch','different_workspace','popup_disappears_early','ipc_failure','no_root_clip_removal','pending_presented_applied'] + indexed={r['id']:r for r in rows} + for name in negative: + row=indexed['tests_offline.Transition.test_'+name] + require(bool(row.get('rejected_contracts') or row.get('rejected_exceptions')),'executed-negative-rejection:'+name) + return receipt + + +def validate_gate(path): + from replay import replay + from audit import audit + path=Path(path);gate=json.loads(path.read_text()) + require(gate.get('status')=='READY_FOR_REVIEW' and gate.get('marker')=='E2_COMPLETION_OFFLINE=READY_FOR_REVIEW' + and gate.get('source_sha256')==sources() and gate.get('blockers')==[], 'prospective-gate-final-source') + receipts={} + for name in ('test-receipts.json','replay.json','source-audit.json'): + p=path.parent/name + require(sha(p)==gate['receipts_sha256'][name],'actual-file-receipt:'+name) + receipts[name]=json.loads(p.read_text()) + validate_test_receipts(receipts['test-receipts.json']) + require(receipts['replay.json']==normalized(replay()),'real-raw-replay-receipt') + require(receipts['source-audit.json']==normalized(audit()),'exhaustive-current-source-audit') + require(gate.get('test_count')==len(receipts['test-receipts.json']['tests']),'gate-test-count') + return gate + + +def write(directory): + from audit import audit + from replay import replay + directory=Path(directory) + tests=json.loads((directory/'test-receipts.json').read_text());validate_test_receipts(tests) + require(json.loads((directory/'source-audit.json').read_text())==normalized(audit()),'audit-current') + require(json.loads((directory/'replay.json').read_text())==normalized(replay()),'real-raw-replay-before-writing') + result=dict(status='READY_FOR_REVIEW',marker='E2_COMPLETION_OFFLINE=READY_FOR_REVIEW',source_sha256=sources(), + receipts_sha256={n:sha(directory/n) for n in ('test-receipts.json','replay.json','source-audit.json')}, + test_count=len(tests['tests']),blockers=[],runtime='NOT RUN',independent_review='PENDING') + path=directory/'offline-gate.json' + with path.open('x') as out:json.dump(result,out,indent=2);out.write('\n') + validate_gate(path);print(result['marker']) + +if __name__=='__main__': + import argparse + p=argparse.ArgumentParser();p.add_argument('directory',type=Path);a=p.parse_args();write(a.directory) diff --git a/tooling/probes/step20c-static-e2-completion/handoff.py b/tooling/probes/step20c-static-e2-completion/handoff.py new file mode 100644 index 0000000..e9a0ec1 --- /dev/null +++ b/tooling/probes/step20c-static-e2-completion/handoff.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +"""Prospective E2 handoff, derived from static-nested-runtime/handoff.py. + +WHY: historical ACCEPTED cannot sign changed runner bytes. +CONTRACT: genuine independent review binds final sources, receipts and owner. +INVARIANT: preflight reads only; launch consumption is exclusive and durable. +""" +import ast +import json +import os +from pathlib import Path +import secrets +import shutil +import stat +import subprocess +import sys +import tempfile +from support import sha,verify,environment +from model import require +from ledger import LIMITS,valid +from transition import source_bindings,SOURCE_FILES + +HERE=Path(__file__).resolve().parent +SOURCE_ROOT=Path('/home/fy59/Documents/Development/swayfx-mosaic/tooling/probes/step20c-static-e2-completion') +TICKET='SWAYFX_MOSAIC_STEP20C_E2_COMPLETION_AND_PRIVATE_INTEGRATION_BATCH_V1' +REGISTRY=Path('/home/fy59/swayfx-mosaic-e2-completion-v1.owner') +REGISTRATION=REGISTRY.with_suffix('.registration.json') +SPENT=REGISTRY.with_suffix('.launch-consumed.json') +OLD_LAB=Path('/home/fy59/swayfx-mosaic-static-nested-runtime-pvn0osxm') +BINARY=Path('/home/fy59/swayfx-mosaic-static-runtime-hm5kbdkt/stage-static/usr/bin/swayfx-mosaic') +BINARY_SHA='f82557c1a9978a5e9665b9b4291b74880253725359e3df1c5b8d554921f7603b' +BUILD_ID='942db64269640778ee433e5970ba1100bc60f94c' +WLROOTS=Path('/usr/lib/libwlroots-0.20.so') +WLROOTS_SHA='7edca9a7d65855addda68e29ba5db94d57c803eb5fa7d0f86db3136b90ca9837' +MARKER='E2_COMPLETION_OFFLINE=READY_FOR_REVIEW' + + +def read(path):return json.loads(Path(path).read_text()) + + +def exclusive(path,data): + path=Path(path) + fd=os.open(path,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600) + with os.fdopen(fd,'w') as out: + out.write(data if isinstance(data,str) else json.dumps(data,indent=2)+'\n');out.flush();os.fsync(out.fileno()) + fd=os.open(path.parent,os.O_RDONLY|os.O_DIRECTORY) + try:os.fsync(fd) + finally:os.close(fd) + + +def regular(path,mode=None): + p=Path(path);s=p.lstat() + require(stat.S_ISREG(s.st_mode) and s.st_uid==os.getuid(),'owned-regular-file:'+str(p)) + if mode is not None:require(stat.S_IMODE(s.st_mode)==mode,'file-mode:'+str(p)) + + +def copy_exact(source,dest): + regular(source) + with Path(source).open('rb') as src,Path(dest).open('xb') as out: + shutil.copyfileobj(src,out);out.flush();os.fsync(out.fileno()) + Path(dest).chmod(stat.S_IMODE(Path(source).stat().st_mode)) + require(sha(source)==sha(dest),'copy-exact') + return dict(source=str(source),destination=str(dest),sha256=sha(source)) + + +def source_map():return {p.name:sha(p) for p in SOURCE_ROOT.iterdir() if p.is_file()} + + +def immutable_paths(lab): + paths=[p for root in ('fixture','client-source','offline') for p in (lab/root).iterdir() if p.is_file()] + paths += [lab/n for n in ('swayfx-mosaic','nested-client','pointer-daemon','static-reference','campaign-owner')] + paths += [lab/'results/binary-identity.json',*[lab/'raw'/(name+'.json') for name in ('ELF','symbols','relocations')],BINARY,WLROOTS.resolve(),REGISTRY] + paths += [Path(p) for p in SOURCE_FILES.values()] + paths += [p for p in SOURCE_ROOT.iterdir() if p.is_file()] + require(len(paths)==len(set(paths)),'unique-manifest-paths') + return sorted(paths) + + +def prepare(offline): + from gate import validate_gate + offline=Path(offline).resolve();gate=validate_gate(offline/'offline-gate.json') + require(not any(p.exists() for p in (REGISTRY,REGISTRATION,SPENT)),'ticket-already-registered') + require(sha(BINARY)==BINARY_SHA and sha(WLROOTS)==WLROOTS_SHA,'binary-wlroots-identities') + lab=Path(tempfile.mkdtemp(prefix='swayfx-mosaic-e2-completion-',dir='/home/fy59'));lab.chmod(0o700) + for n in ('results','raw','fixture','client-source','offline'):(lab/n).mkdir() + token=secrets.token_hex(32) + exclusive(REGISTRY,str(lab)+'\n');exclusive(lab/'campaign-owner',str(REGISTRY)+'\n') + transfers=[] + for p in sorted(HERE.iterdir()): + if p.is_file():transfers.append(copy_exact(p,lab/'fixture'/p.name)) + for name in ('offline-gate.json','test-receipts.json','replay.json','source-audit.json'): + transfers.append(copy_exact(offline/name,lab/'offline'/name)) + for n in ('swayfx-mosaic','nested-client','pointer-daemon','static-reference'): + transfers.append(copy_exact(BINARY if n=='swayfx-mosaic' else OLD_LAB/n,lab/n));regular(lab/n,0o755) + # Actual prepared static_cross sources and generated protocols, not rebuilt. + for p in sorted((OLD_LAB/'client-build').iterdir()): + if p.suffix in ('.c','.h'):transfers.append(copy_exact(p,lab/'client-source'/p.name)) + contract=read(HERE/'client-contract.json') + require(sha(lab/'client-source/nested-client.c')==contract['adapted_nested_sha256'],'actual-static-cross-source') + for name,digest in contract['protocols'].items():require(sha(lab/'client-source'/name)==digest,'actual-protocol-source') + identity={} + for label,argv in [('ELF',['readelf','-hW','-nW','-dW',str(lab/'swayfx-mosaic')]), + ('symbols',['nm',str(lab/'swayfx-mosaic')]),('relocations',['ldd','-r',str(lab/'swayfx-mosaic')])]: + r=subprocess.run(argv,env=environment(),capture_output=True,text=True,timeout=30) + exclusive(lab/'raw'/(label+'.json'),dict(argv=argv,exit=r.returncode,stdout=r.stdout,stderr=r.stderr)) + require(r.returncode==0,'identity-command');identity[label]=r.stdout + require(BUILD_ID in identity['ELF'] and 'ELF64' in identity['ELF'] and + all(s not in identity['ELF'] for s in ('libscenefx','(RPATH)','(RUNPATH)')),'ELF-static-contract') + require(all(' T '+n in identity['symbols'] for n in ('wlr_scene_tree_set_clip','wlr_scene_tree_get_clip')),'clip-symbols') + require(str(WLROOTS.resolve()) in identity['relocations'] and + all(s not in identity['relocations'] for s in ('libscenefx','undefined symbol','not found')),'resolved-libraries') + exclusive(lab/'results/binary-identity.json',dict(status='PASS',build_id=BUILD_ID,binary_sha256=BINARY_SHA, + wlroots_sha256=WLROOTS_SHA,raw={n:sha(lab/'raw'/(n+'.json')) for n in identity})) + exclusive(lab/'results/fixture-freeze-sha256.txt',''.join(sha(p)+' '+str(p)+'\n' for p in immutable_paths(lab))) + frozen=verify(lab/'results/fixture-freeze-sha256.txt') + registration=dict(ticket=TICKET,lab=str(lab),owner_registry=str(REGISTRY),uid=os.getuid(),token=token, + fixture_sha256=frozen['sha256'],binary=str(BINARY),binary_sha256=BINARY_SHA,wlroots_sha256=WLROOTS_SHA) + exclusive(REGISTRATION,registration);exclusive(lab/'results/registration.json',registration) + auth={**registration,'registered_lab':str(lab),'scope':'one-new-static-e2-completion-headless-run','explicit_user_authorization':True} + exclusive(lab/'results/authorization.json',auth) + exclusive(lab/'results/budget.json',dict(limits=LIMITS,used=dict.fromkeys(LIMITS,0),events=[])) + exclusive(lab/'results/offline-gate.json',dict(marker=MARKER,status='READY_FOR_REVIEW',fixture_sha256=frozen['sha256'], + offline_gate_sha256=sha(lab/'offline/offline-gate.json'))) + exclusive(lab/'results/runtime-handoff.json',dict(status='PREPARED_FOR_INDEPENDENT_REVIEW',ticket=TICKET, + lab=str(lab),fixture=frozen,transfers=transfers,source_sha256=source_map(),source_contract=source_bindings(), + gate_sha256=sha(lab/'offline/offline-gate.json'),authorization_sha256=sha(lab/'results/authorization.json'), + registration_sha256=sha(REGISTRATION),historical_review='does not accept changed runner',run_reserved=False)) + print('LAB='+str(lab));return lab + + +def preflight(lab,authorization,review=True,consumed=False): + """Read-only. review=False is prospective preparation, never dispatch.""" + from gate import validate_gate + lab=Path(lab).resolve();authorization=Path(authorization) + require(not sys.flags.optimize,'assertions-required') + require(lab!=OLD_LAB and lab.name.startswith('swayfx-mosaic-e2-completion-'),'fresh-ticket-lab') + require(lab.stat().st_uid==os.getuid() and stat.S_IMODE(lab.stat().st_mode)==0o700,'lab-owner-mode') + for p in (REGISTRY,REGISTRATION,lab/'campaign-owner',authorization):regular(p,0o600) + require(REGISTRY.read_text().strip()==str(lab) and (lab/'campaign-owner').read_text().strip()==str(REGISTRY),'exclusive-owner') + auth=read(authorization);registration=read(REGISTRATION) + require(registration==read(lab/'results/registration.json'),'durable-registration-copy') + require(auth=={**registration,'registered_lab':str(lab),'scope':'one-new-static-e2-completion-headless-run', + 'explicit_user_authorization':True},'exact-authorization') + require(auth['ticket']==TICKET and auth['lab']==str(lab) and auth['owner_registry']==str(REGISTRY) + and auth['uid']==os.getuid() and len(auth['token'])==64,'ticket-lab-token') + require(auth['binary']==str(BINARY) and auth['binary_sha256']==BINARY_SHA and auth['wlroots_sha256']==WLROOTS_SHA, + 'authorized-identities') + require(sha(BINARY)==sha(lab/'swayfx-mosaic')==BINARY_SHA and sha(WLROOTS)==WLROOTS_SHA,'actual-identities') + require(not (lab/'run').exists() and not (lab/'run.lock').exists(),'unconsumed-runtime') + require(not any(stat.S_ISSOCK(p.lstat().st_mode) for p in lab.rglob('*')),'no-existing-sockets') + manifest=lab/'results/fixture-freeze-sha256.txt' + frozen=verify(manifest) + require(manifest.read_text()==''.join(sha(p)+' '+str(p)+'\n' for p in immutable_paths(lab)),'complete-exact-manifest') + require({p.name:sha(p) for p in (lab/'fixture').iterdir() if p.is_file()}==source_map(),'installed-final-fixture') + require(auth['fixture_sha256']==frozen['sha256'],'authorization-manifest') + handoff=read(lab/'results/runtime-handoff.json') + require(handoff['ticket']==TICKET and handoff['lab']==str(lab) and handoff['fixture']==frozen and + handoff['source_sha256']==source_map() and handoff['source_contract']==source_bindings() and + handoff['authorization_sha256']==sha(authorization) and handoff['registration_sha256']==sha(REGISTRATION), + 'handoff-final-bindings') + require(bool(handoff['transfers']) and len({row['destination'] for row in handoff['transfers']})==len(handoff['transfers']),'nonempty-unique-transfers') + for row in handoff['transfers']:require(sha(row['source'])==sha(row['destination'])==row['sha256'],'transfer-changed') + gate=validate_gate(lab/'offline/offline-gate.json') + require(handoff['gate_sha256']==sha(lab/'offline/offline-gate.json'),'handoff-gate-binding') + require(read(lab/'results/offline-gate.json')==dict(marker=MARKER,status='READY_FOR_REVIEW',fixture_sha256=frozen['sha256'], + offline_gate_sha256=sha(lab/'offline/offline-gate.json')),'derived-gate') + budget=valid(read(lab/'results/budget.json'));require(not any(budget['used'].values()) and not budget['events'],'ledger-unreserved') + identity=read(lab/'results/binary-identity.json') + require(identity['status']=='PASS' and identity['build_id']==BUILD_ID and identity['binary_sha256']==BINARY_SHA + and identity['wlroots_sha256']==WLROOTS_SHA,'identity-receipt') + for n,digest in identity['raw'].items():require(sha(lab/'raw'/(n+'.json'))==digest,'raw-identity-binding') + expected_spent=dict(ticket=TICKET,lab=str(lab),token=auth['token'],fixture_sha256=frozen['sha256'],authorization_sha256=sha(authorization)) + if consumed:require(SPENT.exists() and read(SPENT)==expected_spent,'runner-exact-consumption') + else:require(not SPENT.exists(),'ticket-launch-already-consumed') + if review: + reviewed=read(lab/'results/handoff-review.json') + require(reviewed.get('status')=='ACCEPTED' and reviewed.get('independent') is True and reviewed.get('read_only') is True + and reviewed.get('scope')=='STATIC_E2_COMPLETION_FINAL_BYTES' and reviewed.get('blockers')==[], + 'genuine-independent-review-required') + require(reviewed['fixture_sha256']==frozen['sha256'] and reviewed['source_sha256']==source_map() + and reviewed['runtime_handoff_sha256']==sha(lab/'results/runtime-handoff.json') + and reviewed['authorization_sha256']==sha(authorization) and reviewed['registration_sha256']==sha(REGISTRATION) + and reviewed['gate_sha256']==sha(lab/'offline/offline-gate.json'),'review-final-bindings') + regular(reviewed['report_file']);require(sha(reviewed['report_file'])==reviewed['report_sha256'],'review-report-bytes') + return dict(status='PASS',ticket=TICKET,lab=str(lab),fixture=frozen,token=auth['token'], + review='ACCEPTED' if review else 'PENDING_INDEPENDENT_REVIEW',run_reserved=False) + +if __name__=='__main__': + import argparse + p=argparse.ArgumentParser();p.add_argument('action',choices=['prepare','preflight']);p.add_argument('path',type=Path);a=p.parse_args() + if a.action=='prepare':prepare(a.path) + else: + result=preflight(a.path,a.path/'results/authorization.json');exclusive(a.path/'results/preflight.json',result) + print('E2_COMPLETION_PREFLIGHT=PASS') diff --git a/tooling/probes/step20c-static-e2-completion/handoff_tests.py b/tooling/probes/step20c-static-e2-completion/handoff_tests.py new file mode 100644 index 0000000..198cf48 --- /dev/null +++ b/tooling/probes/step20c-static-e2-completion/handoff_tests.py @@ -0,0 +1,108 @@ +"""Isolated preflight mutations. No real owner/review/token is created. + +All simulated accepted records are confined to TemporaryDirectory; mocked gate +validation avoids claiming historical or prospective runtime acceptance. +""" +import copy +import json +import os +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch +from contextlib import ExitStack +import handoff as h +from model import Invalid +from ledger import LIMITS +from support import sha + +class Handoff(unittest.TestCase): + def setUp(self): + self.tmp=tempfile.TemporaryDirectory(prefix='e2-handoff-tests-');self.root=Path(self.tmp.name) + self.lab=self.root/'swayfx-mosaic-e2-completion-synthetic';self.lab.mkdir(mode=0o700) + for n in ('results','raw','fixture','client-source','offline'):(self.lab/n).mkdir() + self.stack=ExitStack() + for name,value in [('REGISTRY',self.root/'owner'),('REGISTRATION',self.root/'registration.json'),('SPENT',self.root/'spent.json'), + ('BINARY',self.root/'binary'),('WLROOTS',self.root/'wlroots'),('SOURCE_FILES',{})]: + self.stack.enter_context(patch.object(h,name,value)) + h.BINARY.write_bytes(b'synthetic binary');h.WLROOTS.write_bytes(b'synthetic wlroots') + self.stack.enter_context(patch.object(h,'BINARY_SHA',sha(h.BINARY)));self.stack.enter_context(patch.object(h,'WLROOTS_SHA',sha(h.WLROOTS))) + self.stack.enter_context(patch('gate.validate_gate',return_value={'status':'SYNTHETIC_TEST_ONLY'})) + for p in h.HERE.iterdir(): + if p.is_file():(self.lab/'fixture'/p.name).write_bytes(p.read_bytes()) + for n in ('nested-client','pointer-daemon','static-reference'):(self.lab/n).write_bytes(b'synthetic client') + (self.lab/'swayfx-mosaic').write_bytes(h.BINARY.read_bytes()) + for n in ('offline-gate.json','test-receipts.json','replay.json','source-audit.json'):(self.lab/'offline'/n).write_text('{}') + h.exclusive(h.REGISTRY,str(self.lab)+'\n');h.exclusive(self.lab/'campaign-owner',str(h.REGISTRY)+'\n') + for n in ('ELF','symbols','relocations'):(self.lab/'raw'/(n+'.json')).write_text('{}') + self.write('binary-identity',dict(status='PASS',build_id=h.BUILD_ID,binary_sha256=h.BINARY_SHA,wlroots_sha256=h.WLROOTS_SHA,raw={})) + manifest=self.lab/'results/fixture-freeze-sha256.txt' + manifest.write_text(''.join(sha(p)+' '+str(p)+'\n' for p in h.immutable_paths(self.lab))) + frozen=h.verify(manifest) + reg=dict(ticket=h.TICKET,lab=str(self.lab),owner_registry=str(h.REGISTRY),uid=os.getuid(),token='a'*64, + fixture_sha256=frozen['sha256'],binary=str(h.BINARY),binary_sha256=h.BINARY_SHA,wlroots_sha256=h.WLROOTS_SHA) + h.exclusive(h.REGISTRATION,reg);self.write('registration',reg) + self.auth={**reg,'registered_lab':str(self.lab),'scope':'one-new-static-e2-completion-headless-run','explicit_user_authorization':True} + h.exclusive(self.lab/'results/authorization.json',self.auth) + self.write('budget',dict(limits=LIMITS,used=dict.fromkeys(LIMITS,0),events=[])) + self.write('offline-gate',dict(marker=h.MARKER,status='READY_FOR_REVIEW',fixture_sha256=frozen['sha256'],offline_gate_sha256=sha(self.lab/'offline/offline-gate.json'))) + self.write('runtime-handoff',dict(status='PREPARED_FOR_INDEPENDENT_REVIEW',ticket=h.TICKET,lab=str(self.lab),fixture=frozen, + source_sha256=h.source_map(),source_contract=h.source_bindings(),authorization_sha256=sha(self.lab/'results/authorization.json'), + registration_sha256=sha(h.REGISTRATION),gate_sha256=sha(self.lab/'offline/offline-gate.json'), + transfers=[dict(source=str(h.BINARY),destination=str(self.lab/'swayfx-mosaic'),sha256=h.BINARY_SHA)])) + self.report=self.root/'synthetic-review.md';self.report.write_text('SYNTHETIC TEST ONLY, not a genuine independent review') + self.review=dict(status='ACCEPTED',independent=True,read_only=True,scope='STATIC_E2_COMPLETION_FINAL_BYTES',blockers=[], + fixture_sha256=frozen['sha256'],source_sha256=h.source_map(),runtime_handoff_sha256=sha(self.lab/'results/runtime-handoff.json'), + authorization_sha256=sha(self.lab/'results/authorization.json'),registration_sha256=sha(h.REGISTRATION), + gate_sha256=sha(self.lab/'offline/offline-gate.json'),report_file=str(self.report),report_sha256=sha(self.report)) + self.write('handoff-review',self.review) + def tearDown(self):self.stack.close();self.tmp.cleanup() + def write(self,name,data): + p=self.lab/'results'/(name+'.json');p.write_text(json.dumps(data));p.chmod(0o600) + def mutate_auth(self,**updates):self.write('authorization',{**self.auth,**updates}) + def call(self,review=True):return h.preflight(self.lab,self.lab/'results/authorization.json',review=review) + def test_prospective_no_review_pure(self): + before={str(p):p.read_bytes() for p in self.root.rglob('*') if p.is_file()} + self.assertEqual(self.call(review=False)['review'],'PENDING_INDEPENDENT_REVIEW') + self.assertEqual(before,{str(p):p.read_bytes() for p in self.root.rglob('*') if p.is_file()}) + def test_wrong_lab(self): + with self.assertRaises(Invalid):h.preflight(h.OLD_LAB,self.lab/'results/authorization.json') + def test_wrong_token(self): + self.mutate_auth(token='b'*64) + with self.assertRaisesRegex(Invalid,'exact-authorization'):self.call() + def test_wrong_ticket(self): + self.mutate_auth(ticket='old-ticket') + with self.assertRaises(Invalid):self.call() + def test_wrong_binary(self): + (self.lab/'swayfx-mosaic').write_bytes(b'changed') + with self.assertRaisesRegex(Invalid,'actual-identities'):self.call() + def test_wrong_wlroots(self): + h.WLROOTS.write_bytes(b'changed') + with self.assertRaisesRegex(Invalid,'actual-identities'):self.call() + def test_missing_review(self): + (self.lab/'results/handoff-review.json').unlink() + with self.assertRaises(FileNotFoundError):self.call() + def test_historical_review_rejected(self): + self.write('handoff-review',{**self.review,'scope':'STATIC_NESTED_RUNTIME_HANDOFF'}) + with self.assertRaisesRegex(Invalid,'genuine-independent-review-required'):self.call() + def test_stale_review(self): + self.write('handoff-review',{**self.review,'fixture_sha256':'0'*64}) + with self.assertRaisesRegex(Invalid,'review-final-bindings'):self.call() + def test_changed_manifest(self): + (self.lab/'results/fixture-freeze-sha256.txt').write_text(sha(h.BINARY)+' '+str(h.BINARY)+'\n') + with self.assertRaisesRegex(Invalid,'complete-exact-manifest'):self.call() + def test_changed_runner(self): + (self.lab/'fixture/run.py').write_text('changed runner') + with self.assertRaises(AssertionError):self.call() + def test_consumed_run(self): + h.exclusive(h.SPENT,{'synthetic':'spent'}) + with self.assertRaisesRegex(Invalid,'ticket-launch-already-consumed'):self.call() + def test_existing_run(self): + (self.lab/'run').mkdir() + with self.assertRaisesRegex(Invalid,'unconsumed-runtime'):self.call() + def test_review_report_changed(self): + self.report.write_text('changed synthetic report') + with self.assertRaisesRegex(Invalid,'review-report-bytes'):self.call() + def test_consumed_runner_wrong_token(self): + h.exclusive(h.SPENT,dict(ticket=h.TICKET,lab=str(self.lab),token='wrong',fixture_sha256=self.auth['fixture_sha256'],authorization_sha256=sha(self.lab/'results/authorization.json'))) + with self.assertRaisesRegex(Invalid,'runner-exact-consumption'):h.preflight(self.lab,self.lab/'results/authorization.json',consumed=True) diff --git a/tooling/probes/step20c-static-e2-completion/ipc_predicates.py b/tooling/probes/step20c-static-e2-completion/ipc_predicates.py new file mode 100644 index 0000000..9c29a74 --- /dev/null +++ b/tooling/probes/step20c-static-e2-completion/ipc_predicates.py @@ -0,0 +1,111 @@ +"""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_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-e2-completion/launch.py b/tooling/probes/step20c-static-e2-completion/launch.py new file mode 100644 index 0000000..674b53c --- /dev/null +++ b/tooling/probes/step20c-static-e2-completion/launch.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +"""Exclusive consumed ticket; full reviewed preflight before dispatch. + +Derived from static-nested-runtime/launch.py. No retries, refunds or builds. +""" +import sys +from pathlib import Path +import subprocess +from handoff import preflight,read,exclusive,SPENT,TICKET,sha,environment + +def launch(lab): + lab=Path(lab).resolve();auth=lab/'results/authorization.json' + result=preflight(lab,auth) + if read(lab/'results/preflight.json')!=result:raise ValueError('published-preflight-mismatch') + exclusive(SPENT,dict(ticket=TICKET,lab=str(lab),token=result['token'],fixture_sha256=result['fixture']['sha256'],authorization_sha256=sha(auth))) + argv=[sys.executable,str(lab/'fixture/run.py'),str(lab),'--future-authorization',str(auth)] + with (lab/'raw/runner.stdout').open('x') as out,(lab/'raw/runner.stderr').open('x') as err: + proc=subprocess.run(argv,env=environment(),stdout=out,stderr=err) + exclusive(lab/'results/dispatch.json',dict(argv=argv,exit_code=proc.returncode,launch_consumed=True,runner_sha256=sha(lab/'fixture/run.py'))) + return proc.returncode + +if __name__=='__main__':raise SystemExit(launch(Path(sys.argv[1]))) diff --git a/tooling/probes/step20c-static-e2-completion/ledger.py b/tooling/probes/step20c-static-e2-completion/ledger.py new file mode 100644 index 0000000..115d0e4 --- /dev/null +++ b/tooling/probes/step20c-static-e2-completion/ledger.py @@ -0,0 +1,65 @@ +"""Durable debits. Offline tests initialize temporary ledgers only. + +The offline LAB never has a runtime ledger. Future registration is a separate +explicitly authorized ticket; failed reservations/actions are never refunded. +""" +import fcntl +import json +import os +from pathlib import Path +import tempfile +import time +from model import require + +LIMITS = dict(run=1, chain=1, grab=2, click=4, capture=12, camera=1, transition=1, move=16) + + +def durable(path, state): + fd, tmp = tempfile.mkstemp(prefix='.debit-', dir=path.parent) + try: + with os.fdopen(fd, 'w') as out: + json.dump(state, out); out.flush(); os.fsync(out.fileno()) + os.replace(tmp, path) + fd = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY) + try: + os.fsync(fd) + finally: + os.close(fd) + finally: + if os.path.exists(tmp): + os.unlink(tmp) + + +def initial_for_offline_test(path): + """No production initialization API: the caller proves isolated test storage.""" + path = Path(path) + require(path.parent.name.startswith('geometry-ledger-test-'), 'test-ledger-only') + with path.open('x') as out: + json.dump(dict(limits=LIMITS, used=dict.fromkeys(LIMITS, 0), events=[]), out) + out.flush(); os.fsync(out.fileno()) + + +def valid(state): + require(state.get('limits') == LIMITS and set(state.get('used', {})) == set(LIMITS), 'ledger-schema') + require(all(type(state['used'][k]) is int and 0 <= state['used'][k] <= LIMITS[k] for k in LIMITS), 'ledger-counts') + events = state.get('events') + require(isinstance(events, list) and len(events) == sum(state['used'].values()), 'ledger-events') + require(all(isinstance(e, dict) and e.get('seq') == i and e.get('kind') in LIMITS and + type(e.get('ns')) is int and e['ns'] > 0 for i, e in enumerate(events)), 'ledger-sequences') + require(all(sum(e['kind'] == k for e in events) == state['used'][k] for k in LIMITS), 'ledger-accounting') + return state + + +def reserve(path, *kinds): + path = Path(path) + require(path.is_file() and not path.is_symlink(), 'existing-ledger-required') + with path.with_suffix('.lock').open('a+') as lock: + fcntl.flock(lock, fcntl.LOCK_EX) + state = valid(json.loads(path.read_text())) + require(bool(kinds) and all(k in LIMITS for k in kinds), 'ledger-debit-kinds') + require(all(state['used'][k] + kinds.count(k) <= LIMITS[k] for k in kinds), 'ledger-exhausted') + for k in kinds: + state['used'][k] += 1 + state['events'].append(dict(seq=len(state['events']), kind=k, ns=time.monotonic_ns())) + durable(path, state) + return state diff --git a/tooling/probes/step20c-static-e2-completion/lifecycle.py b/tooling/probes/step20c-static-e2-completion/lifecycle.py new file mode 100644 index 0000000..89885b1 --- /dev/null +++ b/tooling/probes/step20c-static-e2-completion/lifecycle.py @@ -0,0 +1,64 @@ +"""Pure E2 input/lifecycle predicates shared with offline negative tests.""" +import re +from model import require +from predicates import COLORS, paint, equal, pair, point + + +def opening_pair(lines,target,serial): + actual=[] + for line in lines: + m=re.fullmatch(r'POINTER_BUTTON target=(\w+) serial=(\d+) button=(\d+) state=(\d+)',line) + require(m is not None,'opening-button-format');actual.append([m[1],*map(int,m.groups()[1:])]) + require(len(actual)==2 and all(e[0]==target for e in actual) and + [e[2:] for e in actual]==[[272,1],[272,0]] and actual[0][1]==serial and + 00,'child-zero-residual-A-painted') + require(before.startswith('') and after.startswith(before),'A-input-protocol-appended') + require('POPUP_A DESTROY ' not in after and 'POPUP_A POPUP_DONE ' not in after and + after.count(f'POPUP_A MAP wl={a[0]} xdg={a[1]}')==1,'same-A-live-after-input') + require(rect[0]<=global_point[0]= 0 and self.height >= 0, 'nonnegative-size') + require(bool(self.provenance.evidence), 'evidence-required') + + @property + def end(self): + return (self.x + self.width, self.y + self.height) + + def contains(self, xy): + return self.x <= xy[0] < self.end[0] and self.y <= xy[1] < self.end[1] + + def values(self): + return [self.x, self.y, self.width, self.height] + +@dataclass(frozen=True) +class Native: + rectangle: Rect + buffer_id: int + stride: int + format: int + commit_serial: int + +@dataclass(frozen=True) +class Applied: + scene: Rect + window: Rect + configured: Rect + native: Native + border_left: int + border_top: int + titlebar: int + configure_ack: int + view_offset: tuple = (0, 0) + +@dataclass(frozen=True) +class Output: + name: str + rectangle: Rect + scale: float + transform: str + + +def same(a, b): + require(a.owner == b.owner, 'owner-identity') + require(a.workspace == b.workspace, 'workspace-identity') + require(a.generation == b.generation, 'applied-generation') + require(a.state == b.state == 'applied', 'pending-not-applied') + + +def content(applied): + s, w, c, n = applied.scene, applied.window, applied.configured, applied.native + require(s.space is Space.GLOBAL and w.space is Space.CONTENT and c.space is Space.CONFIGURE, + 'assignment-spaces') + require(n.rectangle.space is Space.NATIVE, 'native-space') + for r in (w, c, n.rectangle): + same(s.provenance, r.provenance) + require(n.buffer_id > 0 and n.stride == n.rectangle.width * 4 and n.format in (0, 1), 'native-buffer-identity') + require(0 < n.commit_serial <= 0xffffffff, 'commit-serial') + require(0 < applied.configure_ack <= 0xffffffff, 'assigned-configure-ack') + require((w.width, w.height) == (c.width, c.height), 'configure-assignment') + require(applied.border_left == w.x and applied.border_top == applied.titlebar + w.y, + 'titlebar-content-offset') + require(w.width + 2 * applied.border_left == s.width and + w.height + applied.border_top + applied.border_left == s.height, 'assigned-destination') + return Rect(Space.GLOBAL, s.x + applied.border_left + applied.view_offset[0], + s.y + applied.border_top + applied.view_offset[1], w.width, w.height, s.provenance) + + +def intersect(rect, boundary): + require(rect.space is Space.GLOBAL and boundary.space in (Space.GLOBAL, Space.VIEWPORT), 'global-intersection') + same(rect.provenance, boundary.provenance) + x, y = max(rect.x, boundary.x), max(rect.y, boundary.y) + return Rect(Space.GLOBAL, x, y, max(0, min(rect.end[0], boundary.end[0]) - x), + max(0, min(rect.end[1], boundary.end[1]) - y), rect.provenance) + + +def to_png(rect, output): + require(rect.space is Space.GLOBAL and output.rectangle.space is Space.GLOBAL, 'global-to-png-spaces') + require(output.scale == 1.0, 'unmeasured-output-scale') + require(output.transform == 'normal', 'unmeasured-output-transform') + same(rect.provenance, output.rectangle.provenance) + visible = intersect(rect, output.rectangle) + return Rect(Space.PNG, min(max(visible.x - output.rectangle.x,0),output.rectangle.width), min(max(visible.y - output.rectangle.y,0),output.rectangle.height), + visible.width, visible.height, rect.provenance) + + +def from_png(rect, output): + require(rect.space is Space.PNG and output.rectangle.space is Space.GLOBAL, 'png-to-global-spaces') + require(output.scale == 1.0 and output.transform == 'normal', 'unmeasured-png-transform') + same(rect.provenance, output.rectangle.provenance) + require(0 <= rect.x <= rect.end[0] <= output.rectangle.width and + 0 <= rect.y <= rect.end[1] <= output.rectangle.height, 'png-bounds') + return Rect(Space.GLOBAL, rect.x + output.rectangle.x, rect.y + output.rectangle.y, + rect.width, rect.height, rect.provenance) + + +def verify_paint(rect, output, bbox, count, clip=None): + expected = to_png(intersect(rect, clip) if clip is not None else rect, output) + actual = [] if bbox is None else list(bbox) + box=[expected.x, expected.y, *expected.end] if expected.width and expected.height else [] + require(actual == box, 'paint-origin-intersection') + require(count == expected.width * expected.height, 'paint-area') + return expected + + +def popup(parent, configured, parent_xdg, expected_parent_xdg, ack, mapped): + require(parent.space is Space.GLOBAL and configured.space is Space.POPUP, 'popup-spaces') + same(parent.provenance, configured.provenance) + require(parent_xdg == expected_parent_xdg and expected_parent_xdg > 0, 'xdg-parent') + require(0 < ack <= 0xffffffff, 'popup-ack') + require(mapped is True, 'popup-map') + return Rect(Space.GLOBAL, parent.x + configured.x, parent.y + configured.y, + configured.width, configured.height, parent.provenance) + + +def crossing(rect, boundary_x): + require(rect.space is Space.GLOBAL, 'crossing-space') + return 'EXERCISED' if rect.x < boundary_x < rect.end[0] else 'NOT EXERCISED' + + +def local_clip_to_global(local, origin, provenance): + require(local.space is Space.CONTENT, 'clip-local-space') + same(local.provenance, provenance) + return Rect(Space.VIEWPORT, local.x + origin[0], local.y + origin[1], + local.width, local.height, provenance) + + +def pointer_local(xy, rect, actual): + require(rect.space is Space.GLOBAL and rect.contains(xy), 'pointer-global-bounds') + require(list(actual) == [xy[0] - rect.x, xy[1] - rect.y], 'pointer-local-correlation') + return list(actual) + + +def fresh_chain(old, new): + require(old['owner'] != new['owner'] and old['pid'] != new['pid'], 'fresh-E2-identity') + require(len(set(old['serials'] + new['serials'])) == 4 and + max(old['serials']) < min(new['serials']), 'fresh-E2-serials') + + +def fresh_standard(mosaic, standard): + require(mosaic.provenance.owner == standard.provenance.owner and + mosaic.provenance.workspace == standard.provenance.workspace, 'standard-owner') + require(mosaic.provenance.generation != standard.provenance.generation, 'fresh-standard-generation') + return standard + + +def constraints(flags, requested, configured): + require(type(flags) is int and 0 <= flags <= 63, 'positioner-flags') + require(flags != 0 or requested == configured, 'zero-flags-no-adjustment') + return {'flags': flags, 'request': requested, 'actual': configured, 'prediction_only': True} diff --git a/tooling/probes/step20c-static-e2-completion/observations.py b/tooling/probes/step20c-static-e2-completion/observations.py new file mode 100644 index 0000000..a2f7074 --- /dev/null +++ b/tooling/probes/step20c-static-e2-completion/observations.py @@ -0,0 +1,59 @@ +"""Adapters from independently recorded protocol/IPC/applied trace observations. + +No native-buffer/frame identity is attributed to a screenshot. Generation is +an observation-window label; atomic transaction-generation is NOT TESTED. +""" +import re +from model import * + + +def applied_model(measured, owner, text, generation): + require(measured['id'] == owner['id'] and measured['IPC'] == owner, 'measured-owner-identity') + workspace = measured.get('workspace') + require(workspace is not None, 'measured-workspace-required') + p = Provenance(owner['id'], workspace, generation, measured['raw']) + buffers = re.findall(r'create_buffer\(new id wl_buffer#(\d+), \d+, (\d+), (\d+), (\d+), (\d+)\)', text) + require(bool(buffers), 'native-shm-trace-required') + # Top's first buffer is recorded before TOPLEVEL MAP; never choose popup buffer. + first = re.search(r'create_buffer\(new id wl_buffer#(\d+), \d+, (\d+), (\d+), (\d+), (\d+)\)', text) + require(first.start() < text.index('TOPLEVEL MAP '), 'toplevel-buffer-before-map') + bid,w,h,stride,fmt = map(int, first.groups()) + top_id=int(re.search(r'TOPLEVEL CREATE wl=(\d+)',text)[1]) + require(re.search(rf'wl_surface#{top_id}\.attach\(wl_buffer#{bid}, 0, 0\)',text) is not None,'native-toplevel-attach') + cfgs = re.findall(r'TOPLEVEL ROLE_CONFIGURE w=(\d+) h=(\d+)', text) + configured = next((list(map(int,c)) for c in reversed(cfgs) if int(c[0]) > 0 and int(c[1]) > 0), None) + require(configured is not None, 'toplevel-configure-required') + assigned_ack = int(re.findall(r'TOPLEVEL XDG_CONFIGURE serial=(\d+) ack', text)[-1]) + require(f'ack_configure({assigned_ack})' in text, 'assigned-ack-trace') + serial = int(re.search(r'TOPLEVEL XDG_CONFIGURE serial=(\d+) ack', text)[1]) + natural=owner['geometry'] + require(natural==dict(x=0,y=0,width=w,height=h),'native-natural-geometry') + win = owner['window_rect'] + return Applied(Rect(Space.GLOBAL,*measured['scene'],p), + Rect(Space.CONTENT,*[win[k] for k in ('x','y','width','height')],p), + Rect(Space.CONFIGURE,0,0,*configured,p), + Native(Rect(Space.NATIVE,0,0,w,h,p),bid,stride,fmt,serial), + win['x'], owner['deco_rect']['height'] + win['y'], owner['deco_rect']['height'],assigned_ack) + + +def output_model(snapshot, name, provenance): + outputs = [o for o in snapshot['outputs'] if o['name'] == name and o['active']] + require(len(outputs) == 1, 'unique-active-output') + o = outputs[0] + return Output(name,Rect(Space.GLOBAL,*[o['rect'][k] for k in ('x','y','width','height')],provenance),o['scale'],o['transform']) + + +def viewport_model(scene, provenance): + require(scene['workspace'] == provenance.workspace, 'viewport-workspace') + require(scene['clip_valid'] is True, 'viewport-applied-valid') + return Rect(Space.VIEWPORT,*map(int,scene['global_clip']),provenance) + + +def verify_owner_occlusion(left,right,rect,g,mosaic): + from pixels import color_geometry + p=rect.provenance + occluders=tuple(Rect(Space.GLOBAL,*g[k],p) for k in ('A','B')) + for im,ox in ((left,-800),(right,0)): + out=Output(str(ox),Rect(Space.GLOBAL,ox,0,800,600,p),1.0,'normal') + clip=Rect(Space.VIEWPORT,-800,0,800,600,p) if mosaic else None + color_geometry(im,'TOPLEVEL',rect,out,clip,occluders) diff --git a/tooling/probes/step20c-static-e2-completion/pixels.py b/tooling/probes/step20c-static-e2-completion/pixels.py new file mode 100644 index 0000000..e5660b6 --- /dev/null +++ b/tooling/probes/step20c-static-e2-completion/pixels.py @@ -0,0 +1,21 @@ +"""Exact binary RGB masks with explicit output origin, clip and occlusion. + +Screenshots prove color/visible geometry only, never native-buffer identity. +""" +from PIL import Image,ImageChops,ImageDraw +from model import * +COLORS=dict(TOPLEVEL=(35,68,204),POPUP_A=(224,192,32),POPUP_B=(32,208,160)) + + +def color_geometry(im,target,rect,output,clip,occluders=()): + require(im.size==(output.rectangle.width,output.rectangle.height),'replay-output-size') + visible=intersect(rect,clip) if clip else rect + expected=Image.new('L',im.size,0);draw=ImageDraw.Draw(expected) + box=to_png(visible,output) + if box.width and box.height:draw.rectangle((box.x,box.y,box.end[0]-1,box.end[1]-1),fill=255) + for occ in occluders: + ob=to_png(occ,output) + if ob.width and ob.height:draw.rectangle((ob.x,ob.y,ob.end[0]-1,ob.end[1]-1),fill=0) + actual=Image.new('L',im.size);actual.putdata([255 if px==COLORS[target] else 0 for px in im.get_flattened_data()]) + require(ImageChops.difference(expected,actual).getbbox() is None,'replay-paint-'+target) + return dict(count=sum(actual.histogram()[1:]),bbox=actual.getbbox()) diff --git a/tooling/probes/step20c-static-e2-completion/placement.patch b/tooling/probes/step20c-static-e2-completion/placement.patch new file mode 100644 index 0000000..404ddd1 --- /dev/null +++ b/tooling/probes/step20c-static-e2-completion/placement.patch @@ -0,0 +1,16 @@ +--- a/nested-client.c ++++ b/nested-client.c +@@ -205,7 +205,12 @@ + int main(int argc,char **argv) { + signal(SIGTERM,stop); + const char *placement=argc>1?argv[1]:"b_cross"; +- if (!strcmp(placement,"visible")) { ++ /* EXPERIMENTAL: only placement changes; serial/grab/lifecycle stay historical. ++ * Both 1x1 anchors lie inside their parent SHM window geometry. */ ++ if (!strcmp(placement,"static_cross")) { ++ a.anchor_x=100; a.anchor_y=80; a.width=350; ++ b.anchor_x=100; b.anchor_y=60; b.width=300; ++ } else if (!strcmp(placement,"visible")) { + a.anchor_x=50; a.anchor_y=80; a.width=250; + b.anchor_x=20; b.anchor_y=60; b.width=170; + } else if (!strcmp(placement,"both_cross")) { diff --git a/tooling/probes/step20c-static-e2-completion/predicates.py b/tooling/probes/step20c-static-e2-completion/predicates.py new file mode 100644 index 0000000..f252a0d --- /dev/null +++ b/tooling/probes/step20c-static-e2-completion/predicates.py @@ -0,0 +1,199 @@ +"""Shared offline/live predicates: facts precede scientific classification. + +CONTRACT: full RGB comparisons have no mask/tolerance; grabs require real logs. +INVARIANT: dismissal, client destruction, clipping and layout remain distinct. +""" +import re +from model import Rect,Space,Provenance,Output,popup,crossing,local_clip_to_global,require +from pixels import color_geometry,COLORS +from PIL import Image,ImageChops + +class Defect(RuntimeError): + """A directly measured scientific contract failure, distinct from fixture ambiguity.""" + +def paint(im,target): + im=im.convert('RGB');assert im.size==(800,600) + mask=Image.new('L',im.size);mask.putdata([255 if p==COLORS[target] else 0 for p in im.get_flattened_data()]) + return dict(count=sum(mask.histogram()[1:]),bbox=mask.getbbox()) + +def point(im,target,origin): + box=paint(im,target)['bbox'];assert box,('no measured paint',target) + # WHY: A may be occluded by B; bbox center alone is not a valid hit point. + x1,y1,x2,y2=box + for y in range(y1+10,y2-10,5): + for x in range(x1+10,x2-10,5): + if all(im.getpixel((xx,yy))==COLORS[target] for yy in range(y-8,y+9) for xx in range(x-8,x+9)): + return [x+origin,y] + raise AssertionError('no solid measured interior '+target) + +def equal(a,b): + assert a.size==b.size==(800,600) + rows=[(i%800,i//800,x,y) for i,(x,y) in enumerate(zip(a.get_flattened_data(),b.get_flattened_data())) if x!=y] + return dict(equal=not rows,changed=len(rows),bbox=ImageChops.difference(a,b).getbbox(),pixels=rows) + +def reference(a,b): + result=equal(a,b) + assert result['equal'] and all(c==(48,48,56) for c in a.get_flattened_data()),'unstable reference' + return result + +def configured(text,name): + rows=re.findall(rf'{name} ROLE_CONFIGURE x=(-?\d+) y=(-?\d+) w=(\d+) h=(\d+)',text) + assert rows,(name,'no configure') + return list(map(int,rows[-1])) + +def geometry(text,origin,provenance=None): + a=configured(text,'POPUP_A');b=configured(text,'POPUP_B') + require(provenance is not None,'popup-owner-provenance-required') + p=provenance + top=Rect(Space.GLOBAL,*origin,0,0,p) + ids={n:list(map(int,re.search(rf'{n} CREATE wl=(\d+) xdg=(\d+) role=(\d+)',text).groups())) for n in ('TOPLEVEL','POPUP_A','POPUP_B')} + result={} + parent=top + for key,name,parent_name,c in [('A','POPUP_A','TOPLEVEL',a),('B','POPUP_B','POPUP_A',b)]: + parent_id=int(re.search(rf'{name} CREATE .*parent_xdg=(\d+)',text)[1]) + ack=int(re.findall(rf'{name} XDG_CONFIGURE serial=(\d+) ack',text)[-1]) + parent=popup(parent,Rect(Space.POPUP,*c,p),parent_id,ids[parent_name][1],ack,name+' MAP ' in text) + result[key]=parent.values() + result.update(configured_A=a,configured_B=b,origin=origin,provenance=dict(owner=p.owner,workspace=p.workspace,generation=p.generation,evidence=p.evidence,state=p.state), + crossing={key:crossing(Rect(Space.GLOBAL,*result[key],p),0) for key in ('A','B')}) + return result + +def mapped(text): + for name in ['POPUP_A','POPUP_B']: + assert text.count(name+' MAP ')==1 and name+' POPUP_DONE ' not in text and name+' DESTROY ' not in text,(name,'not mapped') + +def protocol(text): + assert not re.search(r'wl_display[^\n]*\.error\(|protocol error|invalid_popup_parent|not_the_topmost_popup',text,re.I),'protocol error' + mapped(text) + ids={} + for name in ['TOPLEVEL','POPUP_A','POPUP_B']: + m=re.search(rf'{name} CREATE wl=(\d+) xdg=(\d+) role=(\d+)',text);assert m + ids[name]=list(map(int,m.groups())) + assert len({n for v in ids.values() for n in v})==9,'object identities reused' + grabs=[] + for name,parent in [('POPUP_A','TOPLEVEL'),('POPUP_B','POPUP_A')]: + create=re.search(rf'{name} CREATE .*parent={parent} parent_xdg=(\d+)',text);assert create + assert int(create[1])==ids[parent][1], 'wrong XDG parent' + debug_parent=re.search(rf'xdg_surface#{ids[name][1]}\.get_popup\(new id xdg_popup#{ids[name][2]}, xdg_surface#{ids[parent][1]}, xdg_positioner#\d+\)',text) + require(debug_parent is not None,'debug-parent-request') + m=re.search(rf'{name} GRAB_REQUEST origin=wl_pointer.button target={parent} serial=(\d+) used=(\d+)',text);assert m + serial,used=map(int,m.groups());assert serial==used and 0=2 + assert request.start() POPUP_A -> POPUP_B',status='PASS') + +def pair(lines,target): + events=[] + for line in lines: + m=re.fullmatch(r'POINTER_BUTTON target=(\w+) serial=(\d+) button=(\d+) state=(\d+)',line);assert m + events.append([m[1],*map(int,m.groups()[1:])]) + if len(events)!=2 or any(e[0]!=target for e in events) or [e[2:] for e in events]!=[[273,1],[273,0]]: + raise Defect('wrong actual button recipient/pair: '+str(events)) + assert 00 for name in ('POPUP_A','POPUP_B')),'permitted-popup-color') + if mosaic: + result['whole_H2']=equal(baseline,right) + if not result['whole_H2']['equal']:raise Defect('whole-H2 changed under Mosaic') + else: + require(all(result['H2'][name]['count']>0 for name in ('POPUP_A','POPUP_B')),'standard-popup-continuation') + return result + +def terminal(measures,ipc_code,compositor_code,log,protocol_error,rescue,stdout='',stderr='',reaped=True): + ok=bool(measures and compositor_code==0 and "Handling command 'exit'" in log + and 'Shutting down sway' in log and not protocol_error and not rescue) + # Reaping is completed by the runner after recording actual terminal IPC. + require(type(ipc_code) is int and type(compositor_code) is int,'actual-terminal-codes') + return dict(scientific_cleanup_pass=ok,ipc_exit=ipc_code,compositor_exit=compositor_code, + stdout=stdout,stderr=stderr,all_owned_reaped=reaped,ipc_success=ipc_code==0,transport_incident=bool(ok and ipc_code!=0), + interpretation=('compatible queued-response loss, not proven packet causality' if ok and ipc_code!=0 else 'actual recorded IPC outcome')) + + +def terminal_exchange(execute, wait, argv, env, measures, compositor_log, + protocol_error, rescue, record): + """CONTRACT: preserve an obtained IPC reply before fallible compositor wait.""" + data=dict(scientific_cleanup_pass=False,ipc_exit=None,stdout='',stderr='', + compositor_exit=None,measurements_completed=measures) + try: + reply=execute(argv,env=env,capture_output=True,text=True,timeout=3) + data.update(ipc_exit=reply.returncode,stdout=reply.stdout,stderr=reply.stderr) + record(reply.stdout,reply.stderr) + except Exception as exc: + data['ipc_error']=repr(exc) + return data + try: + data['compositor_exit']=wait(timeout=3) + except Exception as exc: + data['compositor_error']=repr(exc) + return data + try: + data.update(terminal(measures,data['ipc_exit'],data['compositor_exit'], + compositor_log(),protocol_error(),rescue(),stdout=data['stdout'], + stderr=data['stderr'],reaped=False)) + except Exception as exc: + data['terminal_error']=repr(exc) + return data diff --git a/tooling/probes/step20c-static-e2-completion/replay.py b/tooling/probes/step20c-static-e2-completion/replay.py new file mode 100644 index 0000000..d6bcf42 --- /dev/null +++ b/tooling/probes/step20c-static-e2-completion/replay.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Read-only real E2 transition replay; raw PNGs/logs remain outside Git.""" +import json +from pathlib import Path +import re +from PIL import Image +from transition import validate_applied_standard_transition, source_bindings +from predicates import protocol, geometry, popup_pixels, reference +from model import Provenance,require +from support import sha,verify + +HISTORICAL=Path('/home/fy59/swayfx-mosaic-static-nested-runtime-pvn0osxm') +SEALS={'source-sha256.txt':(60,'263f5dbee5e96f2c49619802d063115f792718f6aeb71fa41c58c73b83eb8c91'), + 'evidence-sha256.txt':(178,'e8b2307612ce2402ff9b31e68b50a47d666ca38cd8cce1d67096297f8129034a')} + +def historical_inputs(): + for name,(count,digest) in SEALS.items(): + p=HISTORICAL/'results'/name + require(sha(p)==digest and verify(p)['entries']==count,'historical-seal') + events=[json.loads(l) for l in (HISTORICAL/'run/events.jsonl').read_text().splitlines()] + replies=[e for e in events if e.get('args')==['[con_id=10] layout splith']] + require(len(replies)==1,'single-historical-transition');reply=replies[0] + text=(HISTORICAL/'run/compositor.log').read_text() + handling="Handling command 'layout splith'" + require(text.count(handling)==1,'historical-command-handling') + after=text.split(handling,1)[1] + # Lower bound is previous actual runner event; upper bound is first actual + # monotonic compositor record after handling. Exact request time was omitted. + previous=max(e['ns'] for e in events if e['ns']143 or any(p.stat().st_size>32*1024*1024 for p in run.glob('*.log')): + expired.set();stop_owned();return + +thread=threading.Thread(target=watchdog,daemon=True);thread.start() + +def log(name):return (run/(name+'.log')).read_text(errors='replace') + +def wait(fn,seconds=6): + end=min(time.monotonic()+seconds,started+143) + while time.monotonic()before);time.sleep(.18) + event('injection',action=action,global_point=xy,sent=[x+800,y],extent=[1600,600],grab=grab) + +def buttons(name): + return [s for s in log(name).splitlines() if s.startswith(('POINTER_BUTTON ','BUTTON '))] + +def click(tag,name,xy,grab=False,action='right'): + before={n:len(buttons(n)) for n in [name,'reference','filler']} + cuts={n:len(log(n)) for n in before} + t0=time.monotonic_ns();inject(action,xy,grab) + row=dict(tag=tag,global_point=xy,action=action,start_ns=t0,end_ns=time.monotonic_ns(),events={n:buttons(n)[before[n]:] for n in before},pointer_protocol={n:log(n)[cuts[n]:] for n in before}) + clicks.append(row);save(lab,'clicks',clicks);return row + +def delivery(row,name,target): + assert not row['events']['reference'] and not row['events']['filler'],'other client received visible popup button' + result=pair(row['events'][name],target) + for _,serial,button,state in result: + require(re.search(rf'wl_pointer#\d+\.button\({serial}, \d+, {button}, {state}\)',row['pointer_protocol'][name]) is not None,'button-wire-corroboration') + return result + +def correlate_pointer(name,xy,rect,target,evidence): + # WHY: an injection request alone does not identify applied popup origin. + rows=re.findall(rf'POINTER_(?:ENTER|MOTION) target={target} .*?x=([\d.-]+) y=([\d.-]+)',evidence) + assert rows,'missing pointer local coordinates' + local=list(map(float,rows[-1]));expected=[xy[0]-rect[0],xy[1]-rect[1]] + prov=Provenance(1,1,'pointer-sample','actual pointer log') + pointer_local(xy,Rect(Space.GLOBAL,*rect,prov),local) + wl=int(re.search(rf'{target} CREATE wl=(\d+)',log(name))[1]) + wire=re.findall(rf'wl_pointer#\d+\.(?:enter\(\d+, wl_surface#{wl},|motion\(\d+,) ([\d.-]+), ([\d.-]+)\)',evidence) + require(any(list(map(float,v))==local for v in wire),'fresh-pointer-wire-coordinates') + return dict(global_point=xy,local=local,rectangle=rect) + +def close_client(proc,name,value): + proc.stdin.write(value+'\n');proc.stdin.flush() + code=proc.wait(timeout=3);assert code==0,(name,code) + assert 'TOPLEVEL DESTROY' in log(name) + wait(lambda:find('org.mosaic.NestedGrabProbe',proc.pid) is None) + +# CONTRACT: applied owner, paint and XDG surface size are distinct measurements. +def owner_state(name,proc,tag): + snap=snapshot(tag);ws=workspace(snap['tree'],'1');owner=next(n for n in nodes(ws) if n.get('pid')==proc.pid) + assert owner['app_id']=='org.mosaic.NestedGrabProbe' and owner['shell']=='xdg_shell' and owner['visible'] + assert ws['output']=='HEADLESS-1' and owner['focused'] and snap['focused']==[owner['id']] + measured=measure_owner(log('compositor'),owner);scene=scene_from_log(log('compositor'),'HEADLESS-1') + assert scene['workspace']==ws['id'] + measured['workspace']=ws['id'] + return snap,ws,owner,measured,scene + +def open_chain(name): + global stage + reserve(lab,'chain');cmd('workspace 1');proc=spawn(name,[lab/'nested-client','static_cross'],debug=True) + wait(lambda:find('org.mosaic.NestedGrabProbe',proc.pid) and 'TOPLEVEL MAP ' in log(name));time.sleep(.6) + snap,ws,owner,before,sc=owner_state(name,proc,name+'-before-camera') + assert ws['layout']=='mosaic' + current=ws['mosaic']['rows'][0]['scroll_x'];x,w=before['horizontal_content'];desired=-w//2;delta=int(x-desired) + if delta: + reserve(lab,'camera');cmd(f'[con_id={owner["id"]}] layout mosaic scroll {delta}');time.sleep(.6) + snap,ws,owner,after,scene=owner_state(name,proc,name+'-applied') + validate_camera(current,ws['mosaic']['rows'][0]['scroll_x'],delta,scene) + assert scene['clip_valid'] and scene['global_clip']==[-800,0,800,600] + assert not scene_from_log(log('compositor'),'HEADLESS-2')['clip_valid'] + refstate=validate_reference(snap,refid,ref.pid) + camera_rows.append(dict(chain=name,before=current,delta=delta,after=scene['scroll'],before_owner=before,after_owner=after,scene=scene,reference=refstate)) + save(lab,'camera-calibration',camera_rows) + if name=='E2': + stage='reference';inject('move',[-790,590]);time.sleep(.5) + first=capture('H2-reference-1','HEADLESS-2');time.sleep(.6) + second=capture('H2-reference-2','HEADLESS-2');cmp=reference(first,second) + save(lab,'reference-stability',dict(status='PASS',comparison=cmp,reference=refstate,interval='>=600ms',focus=snap['focused'])) + global baseline + baseline=first + root_boundary[name]=time.monotonic_ns() + stage=name+'-open-A';topim=capture(name+'-top','HEADLESS-1') + topbox=paint(topim,'TOPLEVEL')['bbox'];assert topbox + # Tiled scene+window offset is checked against actual painted blue origin. + applied=applied_model(after,owner,log(name),name+'-mosaic') + content_rect=content(applied);origin=[content_rect.x,content_rect.y] + out=output_model(snap,'HEADLESS-1',content_rect.provenance) + verify_paint(content_rect,out,topbox,paint(topim,'TOPLEVEL')['count'],viewport_model(scene,content_rect.provenance)) + assert topbox[0]-800==origin[0] + assert origin[1]==after['scene'][1]+owner['deco_rect']['height']+owner['window_rect']['y'] + owners[name]=dict(id=owner['id'],pid=proc.pid,workspace=ws['id'],logical_rect=owner['rect'],applied=after,drawn_size=[420,320],paint=paint(topim,'TOPLEVEL'),content_origin=origin) + save(lab,'applied-owner-geometry',owners) + top_point=point(topim,'TOPLEVEL',-800) + require(0 <= top_point[0]-origin[0] < applied.native.rectangle.width and 0 <= top_point[1]-origin[1] < applied.native.rectangle.height, 'opening-point-native-input-bounds') + cp=click(name+'-open-A',name,top_point,True,'left') + wait(lambda:'POPUP_A MAP ' in log(name));time.sleep(.25) + correlate_pointer(name,top_point,content_rect.values(),'TOPLEVEL',cp['pointer_protocol'][name]) + aim=capture(name+'-A','HEADLESS-1');ap=point(aim,'POPUP_A',-800) + stage=name+'-open-B';cpB=click(name+'-open-B',name,ap,True,'left') + wait(lambda:'POPUP_B MAP ' in log(name));time.sleep(.3) + prot=protocol(log(name)) + require(not cp['events']['reference'] and not cp['events']['filler'] and not cpB['events']['reference'] and not cpB['events']['filler'],'opening-other-recipient') + opening_pair(cp['events'][name],'TOPLEVEL',prot['grabs'][0]['serial']) + opening_pair(cpB['events'][name],'POPUP_A',prot['grabs'][1]['serial']) + g=geometry(log(name),origin,content_rect.provenance) + # Actual configure/ACK/map drives geometry; requests are predictions only. + g['positioner_prediction']={'flags':0,'A':[100,80,350,170],'B':[100,60,300,130],'evidence':'source-backed prediction only'} + require(all(value=='EXERCISED' for value in g['crossing'].values()),'popup crossing NOT EXERCISED') + correlate_pointer(name,ap,g['A'],'POPUP_A',cpB['pointer_protocol'][name]) + fully_mapped_ns[name]=time.monotonic_ns();event('fully-mapped-chain',name=name,ids=prot['ids']) + left=capture(name+'-mosaic-H1','HEADLESS-1');right=capture(name+'-mosaic-H2','HEADLESS-2') + rgb=popup_pixels(left,right,g,True,baseline) + clip=root_clip(log('compositor'),origin,True,owner['id'],root_boundary[name],time.monotonic_ns()) + chains[name]=dict(protocol=prot,geometry=g,pixels=rgb,owner=owners[name]) + clips[name]=dict(tile=scene,root=clip,child=clip['child']) + save(lab,'grab-serials',{n:v['protocol']['grabs'] for n,v in chains.items()}) + save(lab,'xdg-protocol-order',{n:v['protocol'] for n,v in chains.items()}) + save(lab,'popup-root-geometry',{n:v['geometry']['A'] for n,v in chains.items()}) + save(lab,'popup-nested-geometry',{n:v['geometry']['B'] for n,v in chains.items()}) + save(lab,'applied-clips',clips);save(lab,'mosaic-pixels',{n:v['pixels'] for n,v in chains.items()}) + return proc,owner,g,left + +try: + comp=spawn('compositor',[lab/'swayfx-mosaic','-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(WAYLAND_DISPLAY=display.name,SWAYSOCK=str(sock)) + assert Path(f'/proc/{comp.pid}/exe').resolve()==lab/'swayfx-mosaic' + maps=Path(f'/proc/{comp.pid}/maps').read_text();(run/'proc-maps.txt').write_text(maps) + paths=sorted({s.split()[-1] for s in maps.splitlines() if len(s.split())>=6 and s.split()[-1].startswith('/')}) + wl=Path('/usr/lib/libwlroots-0.20.so').resolve() + assert [Path(x).resolve() for x in paths if 'libwlroots' in x]==[wl] and sha(wl)==WLROOTS_SHA + assert not any('libscenefx' in x for x in paths) + save(lab,'dependency-mappings',dict(status='PASS',paths=paths,wlroots=str(wl),wlroots_sha256=sha(wl),SceneFX_shared=False)) + outputs=ipc('-t','get_outputs');validate_outputs(outputs);save(lab,'output-geometry',outputs) + pointer=spawn('pointer',[lab/'pointer-daemon']);wait(lambda:'POINTER_READY' in log('pointer')) + cmd('workspace 2');ref=spawn('reference',[lab/'static-reference'],debug=True) + rn=wait(lambda:find('org.mosaic.StaticReference',ref.pid));refid=rn['id'];cmd(f'[con_id={refid}] border none') + cmd('workspace 1');filler=spawn('filler',[lab/'static-reference'],debug=True) + wait(lambda:find('org.mosaic.StaticReference',filler.pid));cmd('layout mosaic') + E2,owner2,g2,im2=open_chain('E2') + stage='E2-standard';mapped(log('E2'));standard_boundary=time.monotonic_ns();reserve(lab,'transition');cmd(f'[con_id={owner2["id"]}] layout splith');time.sleep(.6) + snap,ws,std_owner,std_measure,std_scene=owner_state('E2',E2,'standard') + assert ws['id']==owners['E2']['workspace'] and ws['layout']=='splith' + if std_scene['clip_valid'] or std_scene['layout_enum']!=1:raise Defect('standard tiled clip retained') + mapped(log('E2')) + stdleft=capture('standard-H1','HEADLESS-1');stdright=capture('standard-H2','HEADLESS-2') + blue=paint(stdleft,'TOPLEVEL')['bbox'];assert blue + stdapplied=applied_model(std_measure,std_owner,log('E2'),'E2-standard') + stdcontent=content(stdapplied);stdorigin=[stdcontent.x,stdcontent.y] + fresh_standard(Rect(Space.GLOBAL,*owners['E2']['applied']['scene'],Provenance(owner2['id'],ws['id'],'E2-mosaic',owners['E2']['applied']['raw'])),stdapplied.scene) + # Blue is occluded by A/B; full pixel mask checked against actual geometry below. + assert blue[0]-800==stdorigin[0] and blue[1]==stdorigin[1] + stdg=geometry(log('E2'),stdorigin,stdcontent.provenance) + transition_end=time.monotonic_ns() + events=[json.loads(l) for l in (run/'events.jsonl').read_text().splitlines()] + request=[e for e in events if e['kind']=='ipc-request-start' and e.get('args')==[f'[con_id={owner2["id"]}] layout splith']] + replies=[e for e in events if e['kind']=='ipc' and e.get('args')==[f'[con_id={owner2["id"]}] layout splith']] + require(len(request)==len(replies)==1,'one-transition-request-reply') + applied_transition_start=transition_window_start(log('compositor'),'HEADLESS-1',clips['E2']['root']['tree'], + owner2['id'],ws['id'],fully_mapped_ns['E2'],request[0]['ns']) + transition=validate_applied_standard_transition(request[0]['ns'],replies[0],log('compositor'), + owner2['id'],clips['E2']['root']['tree'],ws['id'],'HEADLESS-1',ws['layout'],captures[-2:], + log('E2'),snap,chains['E2']['protocol']['ids'],[applied_transition_start,transition_end],source_bindings()) + stdrgb=popup_pixels(stdleft,stdright,stdg,False,baseline) + save(lab,'standard-return',dict(status='PASS',transition=transition,geometry=stdg, + mapped_before_and_after=True,no_prior_popup_done=True)) + verify_owner_occlusion(stdleft,stdright,stdcontent,stdg,False) + save(lab,'standard-popup-pixels',stdrgb) + stage='E2-standard-B';xy=point(stdright,'POPUP_B',0);row=click(stage,'E2',xy) + Bpair=delivery(row,'E2','POPUP_B');Bcoords=correlate_pointer('E2',xy,stdg['B'],'POPUP_B',row['pointer_protocol']['E2']);mapped(log('E2')) + # Re-examine every trace through the B input, including any late clip reactivation. + transition=validate_applied_standard_transition(request[0]['ns'],replies[0],log('compositor'), + owner2['id'],clips['E2']['root']['tree'],ws['id'],'HEADLESS-1',ws['layout'],captures[-2:], + log('E2'),snap,chains['E2']['protocol']['ids'],[applied_transition_start,time.monotonic_ns()],source_bindings()) + save(lab,'standard-transition-through-B-input',transition) + before_close=log('E2');request_close_ns=time.monotonic_ns() + event('client-close-request',name='E2',command='close_b') + E2.stdin.write('close_b\n');E2.stdin.flush();wait(lambda:'COMMAND close_b DONE' in log('E2')) + close_proof=explicit_child_close(before_close,log('E2'),chains['E2']['protocol']['ids'],request_close_ns,time.monotonic_ns()) + save(lab,'explicit-child-close',close_proof) + stage='E2-standard-A';Aleft=capture('standard-A-H1','HEADLESS-1');Aimage=capture('standard-A-H2','HEADLESS-2');assert paint(Aimage,'POPUP_B')['count']==0 + from pixels import color_geometry + for im,ox in ((Aleft,-800),(Aimage,0)): + require(paint(im,'POPUP_B')['count']==0,'no-child-residual-both-outputs') + color_geometry(im,'POPUP_A',Rect(Space.GLOBAL,*stdg['A'],stdcontent.provenance), + Output(str(ox),Rect(Space.GLOBAL,ox,0,800,600,stdcontent.provenance),1.,'normal'),None) + xy=point(Aimage,'POPUP_A',0);before_A=log('E2');rowA=click(stage,'E2',xy);Apair=delivery(rowA,'E2','POPUP_A') + Aproof=a_survival(Aimage,before_A,log('E2'),chains['E2']['protocol']['ids'],rowA['events']['E2'],xy,stdg['A']) + Acoords=correlate_pointer('E2',xy,stdg['A'],'POPUP_A',rowA['pointer_protocol']['E2']) + save(lab,'standard-popup-input',dict(status='PASS',B=Bpair,A=Apair,B_coordinates=Bcoords,A_coordinates=Acoords,explicit_B_close=close_proof,A_survived=Aproof)) + stage='E2-cleanup';before_owner_close=log('E2') + event('client-close-request',name='E2',command='close_owner') + close_client(E2,'E2','close_owner') + ownerclose=normal_owner_close(before_owner_close,log('E2'),chains['E2']['protocol']['ids'],E2.returncode) + save(lab,'normal-owner-close',ownerclose);time.sleep(.35) + final=capture('final-H2','HEADLESS-2');save(lab,'final-restoration',final_restoration(baseline,final)) + assert not any(paint(final,n)['count'] for n in ['POPUP_A','POPUP_B']) + finalleft=capture('final-H1','HEADLESS-1') + require(not any(paint(finalleft,n)['count'] for n in ('POPUP_A','POPUP_B')),'final-H1-popup-residual') + snap=snapshot('final');validate_reference(snap,refid,ref.pid) + assert filler.poll() is None and ref.poll() is None + measures=True;event('measurements-completed',status='PASS');stage='completed' +except Exception as exc: + failure=dict(type=type(exc).__name__,message=str(exc),stage=stage,verdict='BLOCKED' if isinstance(exc,Defect) else 'INCONCLUSIVE') + event('failure',**failure) +finally: + # WHY: shutdown transport outcome is not folded into check=True scientific gates. + exit_data=None + if comp is not None and comp.poll() is None and 'sock' in globals() and sock.is_socket(): + # WHY: record transport evidence before wait can raise or time out. + def record_terminal(stdout,stderr): + (run/'terminal-exit.stdout').write_text(stdout) + (run/'terminal-exit.stderr').write_text(stderr) + exit_data=terminal_exchange(subprocess.run,comp.wait, + ['swaymsg','-s',str(sock),'exit'],env,measures, + lambda:log('compositor'), + lambda:any(re.search(r'wl_display[^\n]*\.error\(|protocol error',log(n),re.I) + for n,_ in processes if n not in ['compositor','pointer']), + expired.is_set,record_terminal) + if exit_data is None:exit_data=dict(scientific_cleanup_pass=False,compositor_exit=comp.poll() if comp else None,measurements_completed=measures) + codes=[] + for name,proc in reversed(processes): + if proc.poll() is None: + try:proc.wait(timeout=.7) + except subprocess.TimeoutExpired: + if name=='pointer':proc.stdin.close() # normal EOF destroys virtual pointer + else: + rescue=True + try:os.killpg(proc.pid,signal.SIGTERM) + except ProcessLookupError:pass + try:proc.wait(timeout=.7) + except subprocess.TimeoutExpired: + rescue=True + try:os.killpg(proc.pid,signal.SIGKILL) + except ProcessLookupError:pass + proc.wait(timeout=.7) + codes.append(dict(name=name,pid=proc.pid,exit=proc.returncode)) + if proc.stdin and not proc.stdin.closed:proc.stdin.close() + finished.set();thread.join(timeout=1) + for out in files:out.close() + for name,proc in processes: + try:os.killpg(proc.pid,0) + except ProcessLookupError:pass + else:rescue=True;os.killpg(proc.pid,signal.SIGKILL) + assert all(not Path('/proc',str(row['pid'])).exists() for row in codes),'owned process residual' + private_sockets=[str(p) for p in runtime.iterdir() if p.is_socket()] + shutil.rmtree(runtime) + exit_data.update(rescue_cleanup=rescue,owned_process_codes=codes) + exit_data['scientific_cleanup_pass']=bool(exit_data['scientific_cleanup_pass'] and not rescue and all(row['exit']==0 for row in codes)) + exit_data['all_owned_reaped']=all(not Path('/proc',str(row['pid'])).exists() for row in codes) + if rescue:exit_data['scientific_cleanup_pass']=False + if measures and (not exit_data['scientific_cleanup_pass'] or any(r['exit']!=0 for r in codes)): + failure=dict(type='TerminalCleanupFailure',message='terminal/process evidence failed',stage='completed',verdict='INCONCLUSIVE') + save(lab,'terminal-exit',exit_data) + save(lab,'cleanup',dict(status='PASS' if not rescue else 'RESCUE',runtime=str(runtime),runtime_removed=not runtime.exists(),owned_processes=codes,sockets_before_owned_directory_removal=private_sockets,private_socket_residual=False)) + save(lab,'runtime',dict(status='PASS' if failure is None else failure['verdict'],failure=failure,stage=stage,measurements_completed=measures,duration_seconds=time.monotonic()-started,watchdog=expired.is_set(),rescue_cleanup=rescue,runner_exit=0 if failure is None else 1)) + verify(lab/'results/fixture-freeze-sha256.txt') +print('RUNTIME', 'PASS' if failure is None else failure) +raise SystemExit(0 if failure is None else 1) diff --git a/tooling/probes/step20c-static-e2-completion/support.py b/tooling/probes/step20c-static-e2-completion/support.py new file mode 100644 index 0000000..654276e --- /dev/null +++ b/tooling/probes/step20c-static-e2-completion/support.py @@ -0,0 +1,70 @@ +"""Private probe ownership, durable budgets and bounded argv execution. + +WHY: an exhausted experiment must not be restarted by replacing its LAB. +CONTRACT: stable exclusive owner and lock files; fsync precedes every action. +INVARIANT: failures consume all reservations, never refund or reset counters. +""" +import fcntl +import hashlib +import json +import os +from pathlib import Path +import subprocess +import tempfile +import time + +REPO = Path(__file__).resolve().parents[3] +# No immutable compositor is built/copied and no runtime budget is initialized. + +def sha(path): + return hashlib.sha256(Path(path).read_bytes()).hexdigest() + +def save(lab, name, data): + (Path(lab)/'results'/(name+'.json')).write_text(json.dumps(data,indent=2)+'\n') + +def durable(path, data): + fd,tmp=tempfile.mkstemp(prefix='.ledger-',dir=path.parent) + with os.fdopen(fd,'w') as out: + json.dump(data,out,indent=2);out.write('\n');out.flush();os.fsync(out.fileno()) + os.replace(tmp,path) + fd=os.open(path.parent,os.O_RDONLY|os.O_DIRECTORY) + try:os.fsync(fd) + finally:os.close(fd) + +def reserve(lab, *kinds): + from ledger import reserve as debit + return debit(Path(lab)/'results/budget.json', *kinds) + +def environment(): + # WHY: inherited desktop/loader settings must not select a daily socket. + excluded={'DISPLAY','WAYLAND_DISPLAY','SWAYSOCK','I3SOCK','XDG_RUNTIME_DIR', + 'WAYLAND_DEBUG','DBUS_SESSION_BUS_ADDRESS','XDG_SESSION_ID','XDG_SEAT', + 'XDG_VTNR','XDG_SESSION_TYPE','XDG_CURRENT_DESKTOP','XDG_SESSION_DESKTOP', + 'LD_LIBRARY_PATH','LD_PRELOAD','LD_AUDIT','PKG_CONFIG_PATH','WLR_BACKENDS', + 'WLR_RENDERER','WLR_HEADLESS_OUTPUTS','WLR_DRM_DEVICES','WLR_LIBINPUT_NO_DEVICES'} + env={k:v for k,v in os.environ.items() if k not in excluded and not k.startswith('WLR_')} + env.update(LC_ALL='C',PYTHONDONTWRITEBYTECODE='1') + return env + +def command(lab,label,argv,cwd=None): + result=subprocess.run(list(map(str,argv)),cwd=cwd,env=environment(),capture_output=True,text=True,timeout=30) + (lab/'raw'/(label+'.stdout')).write_text(result.stdout) + (lab/'raw'/(label+'.stderr')).write_text(result.stderr) + with (lab/'raw/commands.jsonl').open('a') as out: + out.write(json.dumps(dict(label=label,argv=list(map(str,argv)),exit=result.returncode,cwd=str(cwd)))+'\n') + assert result.returncode==0,(label,result.returncode,result.stderr) + return result.stdout + +def verify(manifest): + seen=set() + for row in Path(manifest).read_text().splitlines(): + digest,p=row.split(' ',1) + assert p not in seen and sha(p)==digest,(manifest,p) + seen.add(p) + assert seen + return dict(entries=len(seen),sha256=sha(manifest)) + +def inventory(path, files): + with Path(path).open('x') as out: + for p in sorted(files):out.write(f'{sha(p)} {p}\n') + return verify(path) diff --git a/tooling/probes/step20c-static-e2-completion/tests_offline.py b/tooling/probes/step20c-static-e2-completion/tests_offline.py new file mode 100644 index 0000000..ccb5cef --- /dev/null +++ b/tooling/probes/step20c-static-e2-completion/tests_offline.py @@ -0,0 +1,220 @@ +#!/usr/bin/env python3 +"""Independent single mutations and executable receipts; never run compositor.""" +import ast +import copy +import json +from pathlib import Path +import re +import tempfile +from types import SimpleNamespace +import unittest +from unittest.mock import patch +import sys +from PIL import Image,ImageDraw +from model import Invalid,require +from transition import validate_applied_standard_transition,transition_window_start +from replay import historical_inputs,replay +from predicates import COLORS,point,terminal_exchange,Defect +from lifecycle import * +from support import sha +from ledger import initial_for_offline_test,reserve,valid,LIMITS +from reusable_tests import Case as ReusableGeometry +from handoff_tests import Handoff + +HERE=Path(__file__).resolve().parent +ACTIVE=None;OBSERVED={} + +def trace(frame,event,arg): + if ACTIVE and event=='return' and frame.f_code.co_name=='require' and not frame.f_locals.get('value'): + OBSERVED.setdefault(ACTIVE,set()).add(frame.f_locals['contract']) + if ACTIVE and event=='exception' and arg[0] is AssertionError and Path(frame.f_code.co_filename).parent==HERE: + OBSERVED.setdefault(ACTIVE,set()).add('AssertionError:'+frame.f_code.co_name) + return trace + +class Transition(unittest.TestCase): + @classmethod + def setUpClass(cls):cls.original=historical_inputs() + def setUp(self):self.data=copy.deepcopy(self.original) + def reject(self,expected): + with self.assertRaisesRegex(Invalid,'^'+re.escape(expected)+'$'): + validate_applied_standard_transition(**self.data) + def replace(self,old,new): + self.assertIn(old,self.data['trace']);self.data['trace']=self.data['trace'].replace(old,new) + def test_real_raw_replay(self):self.assertEqual(replay()['status'],'PASS') + def test_retained_clip(self): + self.replace('layout=1 scroll=-1 tile_scene=0,0 clip_valid=0','layout=1 scroll=-1 tile_scene=0,0 clip_valid=1');self.reject('standard-clips-absent') + def test_no_standard_epoch(self): + self.replace('layout=1 scroll=-1 tile_scene=0,0 clip_valid=0 clip=0,0,0,0','layout=5 scroll=-1 tile_scene=0,0 clip_valid=1 clip=0,0,800,600') + self.data['trace']=re.sub(r'(PI_POPUP[^\n]*?)clip_valid=0 clip=0,0,0,0',r'\1clip_valid=1 clip=-402,-25,800,600',self.data['trace']);self.reject('two-standard-observations-required') + def test_wrong_layout(self): + self.replace('workspace=6 layout=1','workspace=6 layout=2');self.reject('unexpected-applied-layout') + def test_wrong_owner(self):self.data['owner_id']=8;self.reject('targeted-accepted-splith') + def test_wrong_root(self):self.data['root_id']='0x123';self.reject('unique-enabled-root') + def test_wrong_output(self):self.data['output']='HEADLESS-2';self.reject('exact-workspace-output') + def test_reversed_times(self): + self.replace('ns=181923236944243','ns=181923034904617');self.reject('trace-monotonic-order') + def test_standard_before_application(self): + self.data['request_ns']=[181923236944244,181923236944244];self.data['ipc_response']['ns']=181923236944245 + self.reject('standard-before-request-application') + def test_clip_reactivation(self): + t=self.data['trace'];lines=t.splitlines();start=next(i for i,l in enumerate(lines) if 'PI_OUTPUT_COMMIT output=HEADLESS-1 commit_seq=56 ' in l) + for i in range(start,len(lines)): + if 'PI_SCENE output=HEADLESS-1 ' in lines[i]:lines[i]=lines[i].replace('layout=1','layout=5').replace('clip_valid=0 clip=0,0,0,0','clip_valid=1 clip=0,0,800,600') + self.data['trace']='\n'.join(lines);self.reject('clip-reactivated-after-standard') + def test_missing_root_traces(self): + self.data['trace']='\n'.join(l for l in self.data['trace'].splitlines() if 'PI_POPUP output=HEADLESS-1 ' not in l);self.reject('unique-enabled-root') + def test_multiple_roots(self): + line=next(l for l in self.data['trace'].splitlines() if 'ns=181923236944243 ' in l) + self.replace(line,line+'\n'+line.replace('181923236944243','181923236944244').replace('tree=0x56292fbcc170','tree=0x999').replace('local_index=1','local_index=2'));self.reject('unique-enabled-root') + def test_wrong_capture_epoch(self): + self.data['captures'][0]['start_ns']=181923035203193;self.reject('capture-applied-standard-epoch') + def test_different_workspace(self): + self.replace('workspace=6 layout=1','workspace=99 layout=1');self.reject('applied-current-scene-workspace') + def test_popup_disappears_early(self): + self.data['client_protocol']+='\nPOPUP_B DESTROY role=17 xdg=15 wl=13\n' + with self.assertRaisesRegex(Invalid,'malformed-transition-evidence'):validate_applied_standard_transition(**self.data) + def test_ipc_failure(self):self.data['ipc_response']['exit']=1;self.reject('targeted-accepted-splith') + def test_no_root_clip_removal(self): + self.replace('tree=0x56292fbcc170 enabled=1 clip_valid=0 clip=0,0,0,0','tree=0x56292fbcc170 enabled=1 clip_valid=1 clip=-482,-25,800,600');self.reject('standard-clips-absent') + def test_pending_presented_applied(self): + self.replace('scene_valid=1 scene=-400,0','scene_valid=0 scene=-400,0');self.reject('exact-applied-owner') + def test_missing_second_observation(self): + lines=self.data['trace'].splitlines();start=next(i for i,l in enumerate(lines) if 'PI_OUTPUT_COMMIT output=HEADLESS-1 commit_seq=56 ' in l) + end=next((i for i in range(start+1,len(lines)) if 'PI_OUTPUT_COMMIT ' in lines[i]),len(lines)) + self.data['trace']='\n'.join(lines[:start]+lines[end:]);self.reject('two-standard-observations-required') + def test_historical_request_omission_interval(self): + proof=validate_applied_standard_transition(**self.data) + self.assertEqual(proof['request_bounds'],[181922658327071,181923034516066]) + self.assertLess(proof['request_bounds'][1],181923034904618) + def test_request_ambiguity(self):self.data['request_ns']=[181923035203193,181923035203194];self.reject('request-reply-window') + def test_empty_captures(self):self.data['captures']=[];self.reject('two-standard-captures') + def test_changed_xdg_identity(self):self.data['expected_ids']['POPUP_A'][1]=999;self.reject('exact-xdg-object-identities') + def test_runtime_pre_root_epochs_window(self): + # Real earlier callbacks contain no root before A maps. The transition + # starts at a complete observed callback after A/B fully mapped. + start=transition_window_start(self.data['trace'],'HEADLESS-1',self.data['root_id'],10,6, + 181922000000000,self.data['request_ns'][1]) + self.assertLess(start,self.data['request_ns'][0]) + self.data['window'][0]=start + self.assertEqual(validate_applied_standard_transition(**self.data)['status'],'PASS') + def test_runtime_mapped_window_missing_commit(self): + with self.assertRaisesRegex(Invalid,'fully-mapped-mosaic-commit-before-request'): + transition_window_start(self.data['trace'],'HEADLESS-1',self.data['root_id'],10,6, + self.data['ipc_response']['ns'],self.data['request_ns'][1]) + def test_legitimate_missing_protocol_map_invalid(self): + self.data['client_protocol']=self.data['client_protocol'].replace('POPUP_B MAP ','POPUP_B OMITTED ') + with self.assertRaisesRegex(Invalid,'malformed-transition-evidence'):validate_applied_standard_transition(**self.data) + def test_missing_reply_timestamp_invalid(self): + del self.data['ipc_response']['ns'] + with self.assertRaisesRegex(Invalid,'malformed-transition-evidence'):validate_applied_standard_transition(**self.data) + +class Lifecycle(unittest.TestCase): + def setUp(self): + self.data=historical_inputs();self.before=self.data['client_protocol'];self.ids=self.data['expected_ids'] + self.b=self.ids['POPUP_B'];self.a=self.ids['POPUP_A'];self.top=self.ids['TOPLEVEL'] + b=self.b + self.after=self.before+f'\nPOPUP_B DESTROY role={b[2]} xdg={b[1]} wl={b[0]}\nxdg_popup#{b[2]}.destroy()\nxdg_surface#{b[1]}.destroy()\nwl_surface#{b[0]}.destroy()\nCOMMAND close_b DONE\n' + self.image=Image.new('RGB',(800,600),(48,48,56));ImageDraw.Draw(self.image).rectangle((0,105,51,274),fill=COLORS['POPUP_A']) + self.buttons=['POINTER_BUTTON target=POPUP_A serial=110 button=273 state=1','POINTER_BUTTON target=POPUP_A serial=111 button=273 state=0'] + def test_explicit_close(self):self.assertEqual(explicit_child_close(self.before,self.after,self.ids,1,2)['status'],'PASS') + def test_popup_done_distinct(self): + with self.assertRaisesRegex(Invalid,'close-must-not-be-dismissal'):explicit_child_close(self.before,self.after+'POPUP_B POPUP_DONE role=17\n',self.ids,1,2) + def test_missing_close_command(self): + with self.assertRaisesRegex(Invalid,'explicit-child-destroy-DONE'):explicit_child_close(self.before,self.after.replace('COMMAND close_b DONE',''),self.ids,1,2) + def test_destroy_wrong_object(self): + with self.assertRaisesRegex(Invalid,'child-protocol-destroy'):explicit_child_close(self.before,self.after.replace(f'xdg_popup#{self.b[2]}.destroy()','xdg_popup#999.destroy()'),self.ids,1,2) + def test_A_destroyed(self): + with self.assertRaisesRegex(Invalid,'same-A-remains-mapped'):explicit_child_close(self.before,self.after+'POPUP_A DESTROY role=14\n',self.ids,1,2) + def test_A_input_and_recomputed_point(self): + xy=point(self.image,'POPUP_A',0);self.assertEqual(a_survival(self.image,self.after,self.after,self.ids,self.buttons,xy,[-298,105,350,170])['status'],'PASS') + def test_B_residual(self): + self.image.putpixel((1,1),COLORS['POPUP_B']) + with self.assertRaisesRegex(Invalid,'child-zero-residual-A-painted'):a_survival(self.image,self.after,self.after,self.ids,self.buttons,[10,120],[-298,105,350,170]) + def test_wrong_A_button(self): + with self.assertRaises(Defect):a_survival(self.image,self.after,self.after,self.ids,[s.replace('POPUP_A','POPUP_B') for s in self.buttons],[10,120],[-298,105,350,170]) + def test_final_exact(self): + im=Image.new('RGB',(800,600),(48,48,56));self.assertEqual(final_restoration(im,im.copy())['status'],'PASS') + def test_final_no_tolerance(self): + im=Image.new('RGB',(800,600),(48,48,56));b=im.copy();b.putpixel((799,599),(49,48,56)) + with self.assertRaisesRegex(Invalid,'whole-H2-final-exact'):final_restoration(im,b) + def owner_tail(self): + a,t=self.a,self.top + return f'COMMAND close_owner\nPOPUP_A DESTROY role={a[2]} xdg={a[1]} wl={a[0]}\nxdg_popup#{a[2]}.destroy()\nxdg_surface#{a[1]}.destroy()\nwl_surface#{a[0]}.destroy()\nTOPLEVEL DESTROY\nxdg_toplevel#{t[2]}.destroy()\nxdg_surface#{t[1]}.destroy()\nwl_surface#{t[0]}.destroy()\n' + def test_normal_owner_close(self):self.assertEqual(normal_owner_close(self.after,self.after+self.owner_tail(),self.ids,0)['status'],'PASS') + def test_owner_close_without_command(self): + with self.assertRaisesRegex(Invalid,'normal-owner-close-command'):normal_owner_close(self.after,self.after+self.owner_tail().replace('COMMAND close_owner',''),self.ids,0) + def test_open_left_pair_real_serial(self): + self.assertEqual(opening_pair(['POINTER_BUTTON target=TOPLEVEL serial=98 button=272 state=1','POINTER_BUTTON target=TOPLEVEL serial=99 button=272 state=0'],'TOPLEVEL',98)[0][1],98) + def test_open_wrong_serial(self): + with self.assertRaisesRegex(Invalid,'real-left-opening-pair'):opening_pair(['POINTER_BUTTON target=TOPLEVEL serial=98 button=272 state=1','POINTER_BUTTON target=TOPLEVEL serial=99 button=272 state=0'],'TOPLEVEL',97) + +class TerminalLedger(unittest.TestCase): + def test_actual_terminal_ipc_one(self): + captured=[] + r=terminal_exchange(lambda *a,**k:SimpleNamespace(returncode=1,stdout='raw out',stderr='raw err'),lambda **k:0,[],{},True, + lambda:"Handling command 'exit'\nShutting down sway",lambda:False,lambda:False,lambda *a:captured.append(a)) + self.assertTrue(r['scientific_cleanup_pass']);self.assertEqual(captured,[('raw out','raw err')]);self.assertEqual(r['ipc_exit'],1) + def test_terminal_wait_failure_retains_codes(self): + captured=[] + def fail(**k):raise TimeoutError('raw wait') + r=terminal_exchange(lambda *a,**k:SimpleNamespace(returncode=1,stdout='out',stderr='err'),fail,[],{},True,lambda:'',lambda:False,lambda:False,lambda *a:captured.append(a)) + self.assertEqual(r['ipc_exit'],1);self.assertEqual(r['stdout'],'out');self.assertFalse(r['scientific_cleanup_pass']) + def test_exact_budget_exhaustion(self): + self.assertEqual(LIMITS,dict(run=1,chain=1,grab=2,click=4,capture=12,camera=1,transition=1,move=16)) + with tempfile.TemporaryDirectory(prefix='geometry-ledger-test-') as temp: + p=Path(temp)/'budget.json';initial_for_offline_test(p) + for kind,count in LIMITS.items():reserve(p,*([kind]*count)) + before=p.read_bytes() + with self.assertRaisesRegex(Invalid,'ledger-exhausted'):reserve(p,'run') + self.assertEqual(p.read_bytes(),before) + def test_runner_single_chain_and_source_audit(self): + from audit import audit + result=audit();self.assertEqual(result['status'],'PASS');self.assertTrue(result['records']) + +class Receipt(unittest.TestCase): + def test_empty_PASS_rejected(self): + from gate import validate_test_receipts + with self.assertRaises(Invalid):validate_test_receipts({'status':'PASS','tests':[]}) + def test_skipped_rejected(self): + from gate import validate_test_receipts + with self.assertRaises(Invalid):validate_test_receipts({'status':'PASS','tests':[{'id':'test','status':'SKIP'}]}) + def test_incomplete_temporal_PASS_records_rejected(self): + from gate import validate_test_receipts + receipt=dict(status='PASS',tests=[dict(id=n,status='PASS',rejected_contracts=[]) for n in expected_tests()], + executed=len(expected_tests()),failures=0,errors=0,skips=0,sources={p.name:sha(p) for p in HERE.glob('*.py')}) + with self.assertRaisesRegex(Invalid,'executed-negative-rejection'): + validate_test_receipts(receipt) + +class Result(unittest.TextTestResult): + def startTest(self,test): + global ACTIVE + ACTIVE=test.id();super().startTest(test) + def stopTest(self,test): + global ACTIVE + ACTIVE=None;super().stopTest(test) + def addSuccess(self,test): + super().addSuccess(test);self.receipts.append(dict(id=test.id(),status='PASS',rejected_contracts=sorted(OBSERVED.get(test.id(),[])))) + def __init__(self,*a,**k):super().__init__(*a,**k);self.receipts=[] + + +def expected_tests(): + suite=unittest.defaultTestLoader.loadTestsFromModule(sys.modules[__name__]) + def flatten(s): + for item in s: + if isinstance(item,unittest.TestSuite):yield from flatten(item) + else:yield item + return {test.id().replace('__main__.','tests_offline.') for test in flatten(suite)} + +if __name__=='__main__': + import argparse + p=argparse.ArgumentParser();p.add_argument('--output',type=Path);a=p.parse_args() + sys.settrace(trace) + result=unittest.TextTestRunner(verbosity=2,resultclass=Result).run(unittest.defaultTestLoader.loadTestsFromModule(sys.modules[__name__])) + sys.settrace(None) + # Module identity is normalized so direct CLI and imported inventory agree. + receipts=[{**r,'id':r['id'].replace('__main__.','tests_offline.')} for r in result.receipts] + if a.output: + with a.output.open('x') as out:json.dump(dict(status='PASS' if result.wasSuccessful() else 'FAIL',tests=receipts, + executed=result.testsRun,failures=len(result.failures),errors=len(result.errors),skips=len(result.skipped), + sources={p.name:sha(p) for p in HERE.glob('*.py')}),out,indent=2);out.write('\n') + raise SystemExit(0 if result.wasSuccessful() else 1) diff --git a/tooling/probes/step20c-static-e2-completion/transition.py b/tooling/probes/step20c-static-e2-completion/transition.py new file mode 100644 index 0000000..f9e32d5 --- /dev/null +++ b/tooling/probes/step20c-static-e2-completion/transition.py @@ -0,0 +1,173 @@ +"""Observed applied transition; EXPERIMENTAL, no atomic generation claim. + +WHY: an accepted layout request can precede the transaction applying it. +CONTRACT: keep the old state, then require two coherent standard observations. +INVARIANT: no first-row discard, last-row shortcut or pending-as-applied proof. +PI_POPUP has no owner field: uniqueness + exact live XDG hierarchy + IPC ancestry +provide bounded correlation only. The actual private output.c reads current. +""" +import json +import re +from model import require +from predicates import protocol +from ipc_predicates import ancestry,nodes + +SOURCE_FILES = { + 'output.c': '/home/fy59/swayfx-mosaic-static-runtime-hm5kbdkt/mosaic-source/sway/desktop/output.c', + 'transaction.c': '/home/fy59/swayfx-mosaic-static-runtime-hm5kbdkt/mosaic-source/sway/desktop/transaction.c', + 'xdg_shell.c': '/home/fy59/swayfx-mosaic-static-runtime-hm5kbdkt/mosaic-source/sway/desktop/xdg_shell.c', +} + +def fields(line): + return dict(re.findall(r'(\w+)=([^\s]+)', line)) + + +def observed_epochs(text, output, start_ns, end_ns): + """Group the exact commit callback's ordered SCENE/OWNER/POPUP records. + + Other output callbacks may contain the same global popup tree. We examine + all callbacks for the selected output and never filter by desired owner. + """ + epochs=[]; active=None; last=0 + for line in text.splitlines(): + if not re.search(r'\bPI_(?:OUTPUT_COMMIT|SCENE|OWNER|POPUP)\b',line):continue + f=fields(line) + if f.get('output')!=output:continue + ns=int(f['ns']) + if not start_ns<=ns<=end_ns:continue + require(ns>last,'trace-monotonic-order');last=ns + if 'PI_OUTPUT_COMMIT ' in line: + active={'commit':f,'scene':None,'owners':[],'roots':[],'raw':[line]};epochs.append(active) + else: + require(active is not None,'trace-without-commit') + active['raw'].append(line) + if 'PI_SCENE ' in line: + require(active['scene'] is None,'duplicate-scene');active['scene']=f + elif 'PI_OWNER ' in line:active['owners'].append(f) + else:active['roots'].append(f) + require(bool(epochs),'applied-observations-missing') + return epochs + + +def transition_window_start(trace,output,root_id,owner_id,workspace_id,mapped_after_ns,request_ns): + """Select an explicitly coherent mapped A/B Mosaic callback before request. + + Earlier no-popup callbacks belong to opening evidence, not the transition. + We require a complete commit header + current scene + owner + unique root; + no unconditional trace-row deletion is allowed. + """ + epochs=observed_epochs(trace,output,0,request_ns) + candidates=[] + for epoch in epochs: + c,s,roots=epoch['commit'],epoch['scene'],epoch['roots'] + if int(c['ns'])bounds[1],'standard-before-request-application') + require(s['clip_valid']==r['clip_valid']=='0' and s['clip']==r['clip']=='0,0,0,0' + and s['tile_scene']=='0,0','standard-clips-absent') + state='applied-standard';standard.append(epoch) + else:require(False,'unexpected-applied-layout') + states.append({'ns':t,'state':state,'owner':o,'scene':s,'root':r,'commit':c}) + require(states[0]['state']=='old-applied-Mosaic' and states[0]['ns']<=ipc_response['ns'], + 'old-applied-mosaic-required') + require(len(standard)>=2,'two-standard-observations-required') + # Coherent stabilization retains actual owner geometry and root identity. + stable=[{k:e['owners'][next(i for i,o in enumerate(e['owners']) if o['id']==str(owner_id))][k] + for k in ('scene','applied_size','current')} for e in standard] + require(all(v==stable[0] for v in stable),'standard-geometry-not-stabilized') + first_ns=int(standard[0]['roots'][0]['ns']) + require(isinstance(captures,list) and len(captures)==2 and + {c.get('output') for c in captures}=={'HEADLESS-1','HEADLESS-2'},'two-standard-captures') + for capture in captures: + require(capture.get('exit')==0 and type(capture.get('start_ns')) is int and + first_nsfirst_ns and snapshot['ns']<=end,'snapshot-applied-epoch') + return dict(status='PASS',states=states,request_bounds=bounds,ipc_reply_ns=ipc_response['ns'], + first_standard_ns=first_ns,stabilized_ns=int(standard[-1]['roots'][0]['ns']), + owner=owner_id,root=root_id,workspace=workspace_id,output=output,ids=prot['ids'], + captures=captures,source_contract=source_contract, + limitation='bounded unique-root correlation; independent observations, not atomic transaction generation') + + +EXPECTED_SOURCE = {'output.c':'b517c07655c6796a74fba731ab42d0f96ef53e104f6a9a4c0af256f26a25ded1', + 'transaction.c':'b04483d05100f57867560289d2a7db346e3e8925cec9fa4be35b23b1f6928165', + 'xdg_shell.c':'2a57ba553cacd82220bf16459debb167a3db5889c172af28d9067951f2df3610'} + +def source_bindings(): + from support import sha + observed={name:sha(path) for name,path in SOURCE_FILES.items()} + require(observed==EXPECTED_SOURCE,'private-source-bytes-changed') + return observed + + +def validate_applied_standard_transition(*args,**kwargs): + """Malformed/omitted evidence is Invalid/INCONCLUSIVE, never accepted.""" + from model import Invalid + try:return _validate_applied_standard_transition(*args,**kwargs) + except Invalid:raise + except (KeyError,IndexError,TypeError,ValueError,AttributeError,AssertionError) as exc: + raise Invalid('malformed-transition-evidence:'+str(exc)) from exc