docs(mosaic): record partial STEP20C overnight evidence
Record the annotated rebuild, bounded graphics matrices, private release-shape smoke, and remaining integration gates. Preserve STEP20C NEXT and production multi-output NOT YET SUPPORTED.
This commit is contained in:
parent
4ef16a4019
commit
30dc3182ab
38 changed files with 3147 additions and 0 deletions
16
CHANGELOG.md
16
CHANGELOG.md
|
|
@ -127,3 +127,19 @@ STEP20C NEXT; production multi-output NOT YET SUPPORTED.
|
|||
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.
|
||||
|
||||
## 2026-10-14 — preserved private STEP20C overnight qualification
|
||||
|
||||
- Record the fresh annotated private rebuild, exact local ABI and five-file
|
||||
DESTDIR stage, 18 unique graphics attempts/196 captures, and separately built
|
||||
release-shape horizontal smoke with its own binary identity.
|
||||
- Preserve positive sampled clip/input observations alongside D2 fixture
|
||||
ambiguity, saved-buffer NOT EXERCISED, failed whole-H1 effects restoration,
|
||||
stale-coordinate F assertion, terminal IPC incidents and unproven lifetime,
|
||||
atomic generation, full damage, legal and package qualification.
|
||||
- Add the 22-question production integration decision draft and compact
|
||||
source-only provenance. PRIVATE_CANDIDATE_NEEDS_TARGETED_EVIDENCE remains a
|
||||
recommendation; STEP20C NEXT and production multi-output NOT YET SUPPORTED.
|
||||
Final independent review is ACCEPTED / audit PASS for experimental evidence
|
||||
fidelity; documentary publication is prepared, not executed at this edition. No production
|
||||
source, current package, UX or historical evidence contract is changed.
|
||||
|
|
|
|||
|
|
@ -516,3 +516,28 @@ for normal publication.
|
|||
supersede the original README's sealed pre-execution status only.
|
||||
Architecture remains UNDECIDED for production integration; STEP20C NEXT,
|
||||
production multi-output NOT YET SUPPORTED.
|
||||
|
||||
## Preserved overnight STEP20C qualification — 2026-10-14
|
||||
|
||||
The [overnight dossier](design/STEP20C_OVERNIGHT_QUALIFICATION_AND_INTEGRATION_GATE_V1.md)
|
||||
records fresh private annotated and release-shape binaries with separate exact
|
||||
identities. Sampled owner-derived root popup clipping and fully hidden-owner
|
||||
empty clips agree with recorded pixels/input in the qualified windows;
|
||||
release-shape G has its own horizontal smoke only. These are experimental
|
||||
observations, not adopted runtime architecture or a production dependency.
|
||||
|
||||
Applied scene callbacks, client input-region commits and native output
|
||||
commit/present events remain separate observations. No atomic transaction,
|
||||
scene, hit or presented-buffer generation is established. Saved-buffer/GPU
|
||||
reference and release lifetime, complete frame damage and instrumentation
|
||||
neutrality remain NOT PROVEN. G retains PI listeners and framebuffer
|
||||
bind-before-failure ordering after removing reviewed logging statements.
|
||||
Local header/layout/symbol checks qualify only the exact recorded pair.
|
||||
|
||||
The compositor and its native tiling tree remain authoritative; Row 0 borrows
|
||||
workspace->tiling and no additional runtime row or UX policy is adopted.
|
||||
STEP20C remains NEXT, production multi-output NOT YET SUPPORTED. The decision
|
||||
recommendation is PRIVATE_CANDIDATE_NEEDS_TARGETED_EVIDENCE, not integration.
|
||||
Final independent review is ACCEPTED / audit PASS for experimental evidence
|
||||
fidelity. Documentary publication is prepared, not executed at this edition;
|
||||
final publication identities belong to the private receipt and final handoff.
|
||||
|
|
|
|||
|
|
@ -144,3 +144,18 @@ SwayFX remains the fallback. All other pairs are NOT QUALIFIED.
|
|||
These decisions do not settle popup UX or close STEP20C production gates.
|
||||
STEP20A DONE; STEP20B EXPERIMENTAL; STEP20C NEXT; STEP20D–G PLANNED.
|
||||
Production multi-output remains NOT YET SUPPORTED.
|
||||
|
||||
## Preserved overnight experimental reproduction
|
||||
|
||||
The [annotated rebuild/private-stage report](design/STEP20C_ANNOTATED_REBUILD_AND_PRIVATE_STAGE.md)
|
||||
and [source-only qualification index](../tooling/probes/step20c-overnight/qualification/README.md)
|
||||
record exact pins, ordered patches, private Meson options, local ABI checks,
|
||||
staging manifests and an immutable 18-run budget outcome. Reproduce only in a
|
||||
new disposable LAB with verified retained inputs and a separately authorized
|
||||
budget; the original budget is exhausted and no retry is authorized. Private
|
||||
patches, source archives and future recipe remain local references, not a
|
||||
self-contained publicly distributed build kit. Build/model/ELF/stage passed
|
||||
locally; makepkg metadata/source verification/full build was NOT RUN. No binary
|
||||
bit-reproducibility or distribution readiness is claimed. Experimental helper
|
||||
fixtures do not add a production dependency. See the dossier for transport,
|
||||
cleanup and unexercised-case limits before interpreting runtime results.
|
||||
|
|
|
|||
|
|
@ -460,3 +460,29 @@ 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.
|
||||
|
||||
## Preserved overnight STEP20C qualification — 2026-10-14
|
||||
|
||||
The [overnight qualification dossier](design/STEP20C_OVERNIGHT_QUALIFICATION_AND_INTEGRATION_GATE_V1.md)
|
||||
records a fresh annotated private rebuild and its own bounded graphics evidence,
|
||||
plus a separately built release-shape candidate with its own horizontal smoke.
|
||||
The [exact matrix](design/STEP20C_ANNOTATED_GRAPHICS_MATRIX.md) contains 18 unique
|
||||
runs and 196 captures, without retries. Horizontal C is FEASIBLE for sampled
|
||||
cases; the overall result is PARTIALLY_ADVANCED and the integration recommendation
|
||||
is PRIVATE_CANDIDATE_NEEDS_TARGETED_EVIDENCE, a draft decision, not adoption.
|
||||
|
||||
Vertical, scaled, R90 and resize/migration observations remain bounded. D2's
|
||||
fixture root association stopped before input/final qualification; saved-buffer
|
||||
E2 was NOT EXERCISED; whole-H1 effects restoration failed in both controls;
|
||||
F's stale-coordinate assertion stopped its remaining mutations. Atomic
|
||||
transaction/input/present identity, complete damage and GPU buffer lifetime
|
||||
remain NOT PROVEN. G is SMOKE_FEASIBLE horizontally, without inheriting A's
|
||||
matrix or establishing instrumentation neutrality. Exact local ABI/staging
|
||||
checks and future recipe syntax/input checks do not qualify future pairs,
|
||||
complete makepkg reproduction, actual notice incorporation or legal distribution.
|
||||
|
||||
STEP20A DONE; STEP20B EXPERIMENTAL; STEP20C NEXT; STEP20D–G PLANNED.
|
||||
Production multi-output remains NOT YET SUPPORTED. No production, UX or current
|
||||
package contract is changed. Final independent review is ACCEPTED / audit PASS for experimental evidence
|
||||
fidelity. Documentary publication is prepared, not executed at this edition;
|
||||
production integration remains undecided.
|
||||
|
|
|
|||
|
|
@ -363,3 +363,14 @@ 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.
|
||||
|
||||
Preserved [overnight qualification dossier](design/STEP20C_OVERNIGHT_QUALIFICATION_AND_INTEGRATION_GATE_V1.md):
|
||||
EXPERIMENTAL / PARTIALLY_ADVANCED, with a [fresh annotated rebuild and private stage](design/STEP20C_ANNOTATED_REBUILD_AND_PRIVATE_STAGE.md),
|
||||
[18-run graphics matrix](design/STEP20C_ANNOTATED_GRAPHICS_MATRIX.md),
|
||||
[lifecycle/effects/native-ordering limits](design/STEP20C_LIFECYCLE_EFFECTS_AND_NATIVE_ORDERING.md),
|
||||
and [22-question production integration decision draft](design/STEP20C_PRODUCTION_INTEGRATION_DECISION_DRAFT.md).
|
||||
The [compact source-only qualification index](../tooling/probes/step20c-overnight/qualification/README.md)
|
||||
preserves exact identities, budget and private provenance references. Recommendation:
|
||||
PRIVATE_CANDIDATE_NEEDS_TARGETED_EVIDENCE; no production decision applied.
|
||||
STEP20C NEXT; production multi-output NOT YET SUPPORTED. Final independent review is ACCEPTED / audit PASS for experimental evidence
|
||||
fidelity. Documentary publication is prepared, not executed at this edition.
|
||||
|
|
|
|||
|
|
@ -459,3 +459,27 @@ 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.
|
||||
|
||||
## Preserved overnight STEP20C qualification — 2026-10-14
|
||||
|
||||
The [overnight qualification dossier](design/STEP20C_OVERNIGHT_QUALIFICATION_AND_INTEGRATION_GATE_V1.md)
|
||||
advances private evidence through a fresh annotated rebuild, 18 unique graphics
|
||||
attempts/196 captures and a separately built release-shape horizontal smoke.
|
||||
C horizontal is FEASIBLE for sampled cases; the overall scientific result is
|
||||
PARTIALLY_ADVANCED. PRIVATE_CANDIDATE_NEEDS_TARGETED_EVIDENCE is the proposed
|
||||
integration decision, not an applied decision or READY status.
|
||||
|
||||
The next justified gate is targeted evidence with a new authorized scope and
|
||||
budget: trigger an actual saved configure, diagnose whole-H1 effects restoration
|
||||
against pristine, tie input assertions to applied geometry, and establish
|
||||
relevant native lifetime/transaction/damage and instrumentation-neutrality
|
||||
proof. Exact-pair requalification, complete package reproduction, actual notice
|
||||
incorporation/legal review and release/security nominations remain open.
|
||||
Popup hide/dismiss/reposition and grab click-through policy remain undecided.
|
||||
Relevant canonical contracts must precede any future implementation.
|
||||
|
||||
STEP20A DONE; STEP20B EXPERIMENTAL; STEP20C NEXT; STEP20D–G PLANNED.
|
||||
Production multi-output remains NOT YET SUPPORTED. No STEP20D work or production
|
||||
integration follows automatically from documentary publication. Final independent
|
||||
review is ACCEPTED / audit PASS for experimental evidence fidelity. Documentary
|
||||
publication is prepared, not executed at this edition.
|
||||
|
|
|
|||
|
|
@ -61,3 +61,17 @@ SwayFX remains the fallback. All other pairs are NOT QUALIFIED.
|
|||
These decisions do not settle popup UX or close STEP20C production gates.
|
||||
STEP20A DONE; STEP20B EXPERIMENTAL; STEP20C NEXT; STEP20D–G PLANNED.
|
||||
Production multi-output remains NOT YET SUPPORTED.
|
||||
|
||||
## Overnight private maintenance evidence
|
||||
|
||||
The [annotated reconstruction report](design/STEP20C_ANNOTATED_REBUILD_AND_PRIVATE_STAGE.md)
|
||||
qualifies only the exact private SceneFX 0.5 source and shared wlroots 0.20.2
|
||||
build recorded there. Local header/layout/symbol/relocation checks establish no
|
||||
future semantic ABI guarantee. Any relevant source or dependency build change
|
||||
requires impact review, rebuild and fresh qualification of the new binary;
|
||||
no previous SHA's graphics results are inherited. Release and security roles
|
||||
remain unnamed, and no SLA is established. Future recipe and notices remain
|
||||
PARTIALLY_FEASIBLE: syntax/input correspondence passed, complete makepkg and
|
||||
human legal/distribution review remain open. The [decision draft](design/STEP20C_PRODUCTION_INTEGRATION_DECISION_DRAFT.md)
|
||||
records proposed monitoring, requalification and rollback without adopting a
|
||||
production integration or changing normal SwayFX upgrade independence.
|
||||
|
|
|
|||
72
docs/design/STEP20C_ANNOTATED_GRAPHICS_MATRIX.md
Normal file
72
docs/design/STEP20C_ANNOTATED_GRAPHICS_MATRIX.md
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
# STEP20C — matrice graphique du binaire annoté
|
||||
|
||||
> EXPERIMENTAL — édition documentaire du 2026-10-14, après revue finale ACCEPTED / audit PASS du dossier expérimental ; publication préparée, non exécutée à cette édition. Les résultats concernent des laboratoires privés ; aucune intégration production, installation ou décision UX. STEP20A DONE ; STEP20B EXPERIMENTAL ; STEP20C NEXT ; STEP20D–G PLANNED. Multi-output de production : NOT YET SUPPORTED. Les anciens rapports restent historiques et leurs résultats ne se transfèrent pas à un nouveau SHA.
|
||||
|
||||
L’autorité brute est le LAB privé `/home/fy59/swayfx-mosaic-overnight-mcolx0gq`. Les chemins LAB ci-dessous sont des références locales de preuve, pas des fichiers distribués. Les sources de fixture préservées se trouvent dans [tooling/probes/step20c-overnight](../../tooling/probes/step20c-overnight/qualification/README.md). La [provenance compacte](../../tooling/probes/step20c-overnight/qualification/provenance.json) lie les identités et reçus sans publier logs, images, binaires, archives ou patches candidats.
|
||||
|
||||
## Matrice réellement exécutée
|
||||
|
||||
18 tentatives uniques, aucun retry, 196 captures compositeur. Chaque run utilise un runtime/socket privé. Le renderer observé dans les startups est SceneFX FX, OpenGL ES 3.2 Mesa 26.2.4-arch1.1, AMD Radeon 780M ; `WLR_RENDERER=pixman` demandé ne constitue pas une preuve pixman. Identités de spawn et proc maps contrôlées.
|
||||
|
||||
| Identité | SHA256 | Build ID |
|
||||
| --- | --- | --- |
|
||||
| A annoté, utilisé C1/C2 et Mosaic D/E/F | `979dcd95ab43fa1e465d6e9d262485b73d186cedacd0f957059dd24f482ce6f4` | `fabfeb823754a0f5107f83eb077323ad17ddedd1` |
|
||||
| G release-shape, smoke propre | `8063478b2d5948da3eb8b1a329978882dc1d045cf36932cc2145a78af20ba76a` | `83896fdb36f3fd21522becec0e8c5219e15f4b8e` |
|
||||
| Contrôle pristine retenu | `8ac865481bc0487344210cd0e16d8e6c7655b29db887cd58ddc11b0ca22fe494` | `298a9626b331139ff2f081ade9c4efdde2d4fb19` |
|
||||
|
||||
Paire locale : SceneFX 0.5.0 statique privé, wlroots 0.20.2 partagé, bibliothèque SHA256 `7edca9a7d65855addda68e29ba5db94d57c803eb5fa7d0f86db3136b90ca9837`. Tous les autres builds/paires sont NOT QUALIFIED.
|
||||
|
||||
Géométries : C, D4, E, F et G ont H1 `[-800,0,800,600]`, H2 `[0,0,800,600]`, modes 800×600, scale1, normal ; D1 H1 `[0,0,800,600]`, H2 `[0,600,800,600]`. D2 H1 logique `[-800,0,800,600]`, mode1000×750, scale1,25/normal ; D3 même logique, mode750×1000, scale1,25/R90. H2 reste 800×600 scale1/normal. Les cinq marqueurs asymétriques réellement capturés établissent la frame PNG1000×750 avant les conversions D2/D3 : une conversion logique→PNG est appliquée une seule fois. La migration/resize D4 est une mutation mesurée, pas un autre run. Les rectangles utilisent des bornes half-open. Native SHM buffer, configure XDG, destination tiling/IPC assigned, scène applied et origine client globale, viewport logique, mode physique et pixels PNG restent des espaces distincts ; les mesures de destination ne sont pas une identité de buffer. Les adaptations de scale/rotation sont corroborées par marqueurs et wire, sans mélanger l’origine current et applied.
|
||||
|
||||
| Run | Binaire | PNG | Mesure / runtime / cleanup | Portée |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| C0 | pristine | 10 | PASS / PASS / PASS | contrôle pristine horizontal |
|
||||
| C1 | A | 10 | PASS / PASS / PASS | contrôle annoté splith |
|
||||
| C2 | A | 17 | PASS / PASS / PASS | Mosaic, deux chaînes et retour standard |
|
||||
| D1-pristine | pristine | 8 | PASS / INCONCLUSIVE / PASS | overflow vertical standard |
|
||||
| D1-mosaic | A | 8 | PASS / INCONCLUSIVE / PASS | clip vertical + input/dismissal |
|
||||
| D2-pristine | pristine | 9 | PASS / INCONCLUSIVE / PASS | scale1,25 standard + input |
|
||||
| D2-mosaic | A | 7 | INCONCLUSIVE / INCONCLUSIVE / PASS | pixels/grabs ; association root fixture ambiguë |
|
||||
| D3-pristine | pristine | 9 | PASS / INCONCLUSIVE / PASS | R90 standard + input |
|
||||
| D4-pristine | pristine | 17 | PASS / INCONCLUSIVE / PASS | resize/migration standard |
|
||||
| D4-mosaic | A | 17 | PASS / INCONCLUSIVE / PASS | resize/migration Mosaic ; input pendant migration non échantillonné |
|
||||
| E1-mosaic | A | 12 | PASS / INCONCLUSIVE / PASS | hide/return même chaîne |
|
||||
| E2-mosaic | A | 2 | INCONCLUSIVE / INCONCLUSIVE / PASS | saved configure non déclenché |
|
||||
| E3-pristine | pristine | 16 | INCONCLUSIVE / INCONCLUSIVE / FAIL clients | ROI cache A/B/A ; full restoration échoue |
|
||||
| E3-mosaic | A | 16 | INCONCLUSIVE / INCONCLUSIVE / FAIL clients | ROI/cache USE ; full restoration échoue |
|
||||
| F-pristine | pristine | 8 | INCONCLUSIVE / INCONCLUSIVE / PASS | input regions ; sans trace privée native |
|
||||
| F-mosaic | A | 4 | INCONCLUSIVE / INCONCLUSIVE / PASS | input regions ; assertion de géométrie périmée |
|
||||
| G-smoke | G | 17 | PASS / PASS / PASS | preuve propre G horizontal seulement |
|
||||
| D3-mosaic | A | 9 | PASS / INCONCLUSIVE / PASS | R90 v3 première exécution ; revue runtime acceptée |
|
||||
|
||||
Les trois runs C sont FEASIBLE dans leur scope ; D est PARTIALLY_FEASIBLE globalement. Les receipts structurés préservent serials réels, actions et géométries de chaque run dans la provenance compacte. Les mesures absentes ne sont pas inférées.
|
||||
|
||||
## Pixels, protocoles et input
|
||||
|
||||
C0/C1 : TOPLEVEL→A→B réel, deux grabs serial33/42, configure/ACK/SHM. A[-298,105,350,170], B[-198,165,300,130] traversent x0 ; entrée B puis A réelle sur H2. Huit images homologues C0/C1 entières exactement égales en RGB, sans masque ni tolérance. Fermeture B conservant A et fermeture owner normales, référence H2 finale exacte.
|
||||
|
||||
C2 : deux owners distincts, grabs33/42 puis91/100. A[-218,105,350,170], B[-118,165,300,130], owner appliqué[-318,25,636,573]. H1 conserve les pixels A/B ; H2 complet exact. B visible reçoit51/52 ; B-H2[91,195] ne reçoit aucun bouton, puis done/destroy B→A ; référence62/63 observée. Le retour splith du même owner/root/hierarchie est reconstitué sur deux callbacks old-Mosaic puis six applied-standard sans réactivation du clip, avec B111/112 puis A117/118. Aucun click-through universel.
|
||||
|
||||
D1 : A/B traversent y600 de75/115. Pristine H1 A21200/B2550 et H2 A6000/B19550 ; Mosaic H1 mêmes nombres, H2 A0/B0 et RGB exact. Grabs31/40 pristine,33/42 Mosaic. B H2 réel standard, absent côté Mosaic ; dismissal et référence62/63 observés. Le champ helper historique `crossing` x0 NOT EXERCISED n’est pas le prédicat vertical : revue y600 indépendante.
|
||||
|
||||
D2 : pristine H1 A45150/B40261, H2 A3120/B13260 ; Mosaic H1 A37650/B23961, H2 A0/B0, grabs35/44 réels. Le layer wallpaper de marqueurs crée avant l’owner un host descriptor POPUP ; le fixture exigeait à tort un root global unique/index1 alors que le root XDG est index2. Association source+temps+cardinalité, sans owner-ID natif direct. Arrêt consommé avec7 PNG et2 clics d’ouverture ; input B visible/interdit, dismissal et finals NOT EXERCISED. Aucun rerun D2 ni défaut compositor démontré.
|
||||
|
||||
D3 : pristine contrôle réel avec grabs35/44 et marqueurs. Mosaic v3 a été réparé/revu avant sa première exécution, freeze `7ae5f3ad8cc8cb738740c6a32fe7e1f336d20226b7813a03e22be3030d1507fb`, 1172 entrées ; archive v2 exécutée578 SHA conservée. Mesures runner PASS,9 PNG/4 clics/2 grabs35/44,1 caméra ; H1 A37650/B23961, H2 A0/B0 et480000 pixels exacts ; B visible, interdit puis dismissal final observés. La revue runtime indépendante `reviews/rotation-v3-runtime-review.md` est ACCEPTED / FEASIBLE_SAMPLED_R90 : 11 callbacks baseline avant target owner9 (les trois derniers concernent filler8), puis un callback mapped complet. Neuf négatifs sont refusés ; owner/root association bornée sans IDs natifs. Aucun retour splith testé dans D3.
|
||||
|
||||
D4 : resize H1→960 puis800, workspace/owner H1→H2→H1, IDs et ordre natif préservés, ancienne sortie restaurée exactement. Trois cellules, largeur minimale640/gap0, caméra demandée relative1920 clampée à960. Input durant migration NOT TESTED ; aucune identité de génération atomique.
|
||||
|
||||
G :17 PNG/8 clics/4 grabs,2 chaînes,2 caméras,1 retour splith. Maps G exact et absence SceneFX dynamique. Clips root/owner, H2 Mosaic/final exact, standard H2 A3120/B13260 et input wire corroborés ; cleanup propre. Verdict SMOKE_FEASIBLE horizontal, aucune matrice G complète ni neutralité héritée de A.
|
||||
|
||||
## Transport, limites et budgets
|
||||
|
||||
Toutes les IPC terminales ont code1 « Unable to receive IPC response », stdout vide, compositor0. Les politiques C/G permettent runtime PASS lorsque mesures/fermetures complètes ; matrix conserve runtime INCONCLUSIVE. Cette différence explicite n’est pas harmonisée et la cause de la perte de réponse n’est pas prouvée. E3 : deux clients par run code1/error32 après interruption, cleanup scientifique FAIL ; tous processus sont pourtant reaped et runtimes supprimés, sans watchdog/rescue. Les autres cleanups sont PASS.
|
||||
|
||||
Registre durable flock + remplacement atomique + fsync :2/2 SceneFX,2/2 Mosaic,2/2 stages,18/18 runs,196/220 captures, maximum20/run et12 pour F,150 s/run. Local C3,D1–D4 chacun2,E1=1,E2=1,E3=2,F=2,G=1. Quatre PNG de marqueurs générés sont des inputs séparés :200 nouvelles images ;78 copies d’assets upstream ne sont pas des captures. Aucun remboursement, reset ou relance. Hash final ledger `6ebedc65f30bf2c81f45b1487adf974a390a0049f632cfbb18bbbb00e3bfae3f`.
|
||||
|
||||
Revue indépendante C, premiers D et lifecycle/G acceptées pour ces faits seulement ; preuves/négatifs conservés même lorsqu’un premier script d’analyse a échoué puis été corrigé hors ligne. Revue R90 runtime acceptée ; revue finale du dossier expérimental ACCEPTED / audit PASS, distincte des gates production. Damage de toutes frames, lifetime GPU, génération atomique, sortie réelle non headless et UX popup universelle NOT PROVEN / NOT TESTED.
|
||||
|
||||
## Revue finale observée et publication préparée
|
||||
|
||||
Revue indépendante finale ACCEPTED / audit PASS pour la fidélité du dossier expérimental : 0 BLOCKING, 4 NON_BLOCKING, 3 FUTURE_SCOPE ; 180 checks hors ligne et 15 mutations de preuves réelles rejetées. Rapport privé `reviews/overnight-final-review.md`, SHA256 `bb4000dffb51b07b59e86f91e8dd76bc2c4f0b9513b48a52c9f6bd6c8c3f45af` ; reçu JSON SHA256 `72ba252092ca3375d344faae1c2026c8d5626fd190e87771d5c0c1ad8212c9b0`. Index scientifique post-exécution `seals/index.json`, SHA256 `cdf034e1f8466a9ae8a0e0a33aa8d0d0195273f0d1bc782667b50c3bcf682505`, contexte de preuve courant, pas un binding pré-exécution.
|
||||
|
||||
NB-01 est complété dans l’index compact depuis les grabs wire réels de chacun des huit runs concernés, avec lignes et hashes sources dans `parent/metadata-serial-completion.json`. Les stops fixtures, échecs E3 et incident IPC restent documentés. Les gates lifetime/génération/damage, couverture G, maintenance ABI/paquet/juridique et robustesse/coût des sidecars privés restent ouverts. L’acceptation documentaire ne qualifie pas la production et n’applique aucune décision. Publication préparée, pas encore exécutée à cette édition ; les refs/SHAs finaux et le résultat de publication feront foi dans le reçu privé et le handoff final.
|
||||
90
docs/design/STEP20C_ANNOTATED_REBUILD_AND_PRIVATE_STAGE.md
Normal file
90
docs/design/STEP20C_ANNOTATED_REBUILD_AND_PRIVATE_STAGE.md
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
# STEP20C — reconstruction annotée et staging privé
|
||||
|
||||
> EXPERIMENTAL — édition documentaire du 2026-10-14, après revue finale ACCEPTED / audit PASS du dossier expérimental ; publication préparée, non exécutée à cette édition. Les résultats concernent des laboratoires privés ; aucune intégration production, installation ou décision UX. STEP20A DONE ; STEP20B EXPERIMENTAL ; STEP20C NEXT ; STEP20D–G PLANNED. Multi-output de production : NOT YET SUPPORTED. Les anciens rapports restent historiques et leurs résultats ne se transfèrent pas à un nouveau SHA.
|
||||
|
||||
L’autorité brute est le LAB privé `/home/fy59/swayfx-mosaic-overnight-mcolx0gq`. Les chemins LAB ci-dessous sont des références locales de preuve, pas des fichiers distribués. Les sources de fixture préservées se trouvent dans [tooling/probes/step20c-overnight](../../tooling/probes/step20c-overnight/qualification/README.md). La [provenance compacte](../../tooling/probes/step20c-overnight/qualification/provenance.json) lie les identités et reçus sans publier logs, images, binaires, archives ou patches candidats.
|
||||
|
||||
## Reconstruction exacte A et portée H
|
||||
|
||||
A : PASS pour rejeu source, annotations, build, modèle, ELF, ABI locale et stage ; H : PARTIALLY_FEASIBLE pour préparation du paquet et maintenance. La compilation ne prouve pas un comportement graphique. Le nouveau A a ensuite ses propres preuves C/D/E/F ; G possède une identité et un smoke séparés.
|
||||
|
||||
| Source épinglée | Commit |
|
||||
| --- | --- |
|
||||
| SwayFX 0.6 | `fd71a6bdc061bd633b488ae7b83e8a6981d22f86` |
|
||||
| Mosaic source historique | `cd550382b311efdbfe3ed892f4e1d281d852e754` |
|
||||
| SceneFX 0.5 | `37ccd723bef49e6891156ffafce8f549f01446cc` |
|
||||
| wlroots 0.20.2 | `d783533489e1f75d6886c2ab5c5960090ef268f8` |
|
||||
|
||||
La revue `reviews/phase-a-review.md` a vérifié les labels commit pax des quatre archives, leurs hashes et le rejeu strict. L’absence de `.git` dans une extraction n’invalide pas cette provenance ; l’avertissement initial d’inventaire est résolu par cette preuve. Huit patches fonctionnels/instrumentation sont rejoués dans l’ordre, puis deux patches de commentaires. `git apply --check` précède chaque application normale ; chaque arbre parent/résultat et les quatre fichiers annotés sont vérifiés. Une comparaison lexicale tenant compte des littéraux conserve tous les tokens C actifs. Les insertions peuvent décaler `__LINE__`. Le NOT REBUILT historique décrivait l’ancien D, pas A.
|
||||
|
||||
| Arbre / ordre | Patch | SHA256 |
|
||||
| --- | --- | --- |
|
||||
| scene-source / 0 | scenefx-popup-plus-base.patch | `c74c515b98da8f5f637e260e6a3755c7a75bd78d1c39e54a67850f1c383e30e7` |
|
||||
| scene-source / 1 | scenefx-timing.patch | `7a7ef1875b559f65a3240a0016592da1da6a8537929d0e96b8aaa2d20ecd46fa` |
|
||||
| scene-source / 2 | scenefx-cache-trace.patch | `a0fd5a64156a79b1605ff3b52bd9accb20390199b2b36e1970389f5086b68f39` |
|
||||
| mosaic-source / 0 | mosaic-popup-plus-base.patch | `d416f6413ea097a48a44d231e50ffba363914f46c8687f0ea2e4dcacd5f31fe9` |
|
||||
| mosaic-source / 1 | hidden-guard.patch | `35785a8f3e863b3c1fd87932d8ff0c2949b03cb04382cc34e1126170968f802b` |
|
||||
| mosaic-source / 2 | mosaic-output-timing.patch | `4419059231e038675f50a6321889f27ba981c5d8df2779ccbc4a61e02f1d6668` |
|
||||
| mosaic-source / 3 | presentation-events.patch | `dbf1f1d11c9c741b641916d25ac22bcb13c3455ee3002e1f534aa6794cd8a538` |
|
||||
| mosaic-source / 4 | sway-source-trace.patch | `b83ff12bf24ee879cc713a134b0c312e9b8bd1b4034ecc2aea124d55a743ebb7` |
|
||||
| annotations | scenefx-comments.patch | `21f057ff4180bb5dd35af35070aa4a95923be83f81abb864793e5bfb93643b01` |
|
||||
| annotations | mosaic-comments.patch | `7fee137101abc0cd5024cea172c5904268ddf995bc5855c570e4dfabd643c970` |
|
||||
|
||||
| Identité | SHA256 | Build ID |
|
||||
| --- | --- | --- |
|
||||
| A annoté, utilisé C1/C2 et Mosaic D/E/F | `979dcd95ab43fa1e465d6e9d262485b73d186cedacd0f957059dd24f482ce6f4` | `fabfeb823754a0f5107f83eb077323ad17ddedd1` |
|
||||
| G release-shape, smoke propre | `8063478b2d5948da3eb8b1a329978882dc1d045cf36932cc2145a78af20ba76a` | `83896fdb36f3fd21522becec0e8c5219e15f4b8e` |
|
||||
| Contrôle pristine retenu | `8ac865481bc0487344210cd0e16d8e6c7655b29db887cd58ddc11b0ca22fe494` | `298a9626b331139ff2f081ade9c4efdde2d4fb19` |
|
||||
|
||||
Paire locale : SceneFX 0.5.0 statique privé, wlroots 0.20.2 partagé, bibliothèque SHA256 `7edca9a7d65855addda68e29ba5db94d57c803eb5fa7d0f86db3136b90ca9837`. Tous les autres builds/paires sont NOT QUALIFIED.
|
||||
|
||||
L’ancien D `616aa504334d88aada5b1596df624957f39b0f1ae975a1a04105fbf73aea6dd0` et l’ancien candidat C `f82557…` sont historiques. Aucun résultat de ces identités ne qualifie A ou G. Les patches cumulatifs annotés privés sont `candidate-cumulative.patch` (`8fea59b98907f5c57f841e624c53437d0fe886b56ccf80f472be8d08389b8ca0`) et `scenefx-cumulative.patch` (`7e1549d68d33945620690d26ee6b1a6cc2f3942e4000c9ac89d79a317f136302`) ; leurs hashes diffèrent volontairement des cumulatifs historiques non annotés.
|
||||
|
||||
## Reproduction bornée
|
||||
|
||||
Reconstituer les inputs vérifiés dans un nouveau LAB ; ne pas utiliser le checkout production comme pristine. Le point d’entrée source est `tooling/probes/step20c-overnight/replay_annotated.py LAB --historical HISTORICAL`, où HISTORICAL est le dossier privé `integration-candidate` retenu avec ses archives, patches et scripts (`/home/fy59/swayfx-mosaic-e2-completion-dk1hq9an/integration-candidate` dans cette campagne). Ce script initialise un nouveau registre, vérifie le rejeu et exécute les builds/stage ; son lancement n’est pas autorisé par ce rapport documentaire et ne doit jamais viser le LAB consommé. Les dépendances privées doivent être conservées et vérifiées avant une reproduction. Les commandes Meson ci-dessous décrivent celles du script, pas une voie permettant de contourner les réservations. Les scripts de fixture et leurs manifests décrivent le rejeu. Le registre réserve avant setup/build/stage, sans reset ni remboursement. Les commandes A effectivement enregistrées dans `raw/commands.jsonl` sont reproduites ci-dessous avec `LAB` désignant ce nouveau répertoire ; `PKG_CONFIG_PATH` reste limité à chaque processus.
|
||||
|
||||
```sh
|
||||
meson setup "$LAB/scene-build" "$LAB/scene-source" --prefix=/usr \
|
||||
-Ddefault_library=static -Dexamples=false -Dwerror=false
|
||||
meson compile -C "$LAB/scene-build" -j 8
|
||||
PKG_CONFIG_PATH="$LAB/scene-build/meson-uninstalled" meson setup \
|
||||
"$LAB/mosaic-build" "$LAB/mosaic-source" --prefix=/usr --bindir=bin \
|
||||
-Dwerror=false -Dsd-bus-provider=libsystemd -Dmosaic-package=true \
|
||||
-Dswaybar=false -Dswaynag=false -Dman-pages=disabled -Ddefault-wallpaper=false
|
||||
PKG_CONFIG_PATH="$LAB/scene-build/meson-uninstalled" meson compile -C "$LAB/mosaic-build" -j 8
|
||||
meson test -C "$LAB/mosaic-build" --no-rebuild mosaic-model --print-errorlogs
|
||||
meson install -C "$LAB/mosaic-build" --no-rebuild --destdir "$LAB/stage-static"
|
||||
```
|
||||
|
||||
Les notices sont ajoutées par simulation privée du futur paquet, puis le manifeste complet est audité. A : SceneFX C23, Mosaic C11 ; audit de layouts C17 `-Wall -Wextra -Werror`. Les 132 commandes A, leurs dates Unix, durées et codes sont conservées ; les dates UTC exactes des setups/compilations figurent dans la provenance compacte. Compilation SceneFX environ 0,90 s, Mosaic environ 7,81 s ; setup séparé. Les deux `git diff --no-index` code 1 produisent les patches privés, sans échec de compilation. Les commandes ultérieures `g-*` dans le même journal ne font pas partie des 132 commandes A.
|
||||
|
||||
G : second build/stage autorisé, exécuté le 2026-10-14T00:35:52+02:00 avec les mêmes options et chemins `release-shape/`. Modèle 1/1, audit C17 et stage PASS. Sortie intégrale Meson/Ninja non conservée par l’adaptateur : les journaux/résultats/extraits existent, sans prétendre à un stdout complet. Les spans `.ninja_log` sont des timings internes et ne remplacent pas une durée murale. Un avertissement `LIBINPUT_CONFIG_SCROLL_CIRCULAR` dans `sway/ipc-json.c:1175` est conservé.
|
||||
|
||||
## ELF et ABI locale
|
||||
|
||||
A et G : ELF64 x86-64 PIE, aucune dépendance SceneFX DT_NEEDED, wlroots partagé attendu, RPATH/RUNPATH vides, `ldd -r` sans symbole indéfini, fonctions clip définies et appels directs inspectés. Comparaison locale de 17 structures et 6 offsets identique ; 121 headers wlroots épinglés ; 85 noms communs caractérisés, distincts des 34 bindings réellement observés par le loader diagnostique. Ni les 85 fournisseurs runtime universels ni l’ABI sémantique future, l’interposition générale ou la neutralité graphique ne sont démontrés.
|
||||
|
||||
## Stage et notices
|
||||
|
||||
Exactement cinq fichiers réguliers dans chacun des deux DESTDIR, binaire 0755 et notices 0644. Le manifeste A est ci-dessous ; G garde le même périmètre mais ses propres hashes, liés dans la provenance. Aucun système installé.
|
||||
|
||||
| Chemin | Octets A | SHA256 A |
|
||||
| --- | --- | --- |
|
||||
| `/usr/bin/swayfx-mosaic` | 6760288 | `979dcd95ab43fa1e465d6e9d262485b73d186cedacd0f957059dd24f482ce6f4` |
|
||||
| `/usr/share/licenses/swayfx-mosaic/INCORPORATED-NOTICES` | 255102 | `cf69048aad394c4d3e3dc875c1b9aae1922946af861fec18e337f0b45aacd7fb` |
|
||||
| `/usr/share/licenses/swayfx-mosaic/SceneFX-LICENSE` | 1096 | `ffd3737a478b83a8b51b42757d3bf909ef36694508355879722e11fc1fa6736b` |
|
||||
| `/usr/share/licenses/swayfx-mosaic/SwayFX-LICENSE` | 1061 | `bd35722bff46327f5eded1d211207277bbe043d2a22048b282c42ef801593b8b` |
|
||||
| `/usr/share/licenses/swayfx-mosaic/wlroots-LICENSE` | 1140 | `35d427c043dcafe8893b9e7247348f599847c81d9a067703587c80707f3d58df` |
|
||||
|
||||
L’inventaire A couvre conservativement 895 inputs, 599 headers, 12 shaders, 7 XML et 282 blocs de notice ; G 894 inputs, 598 headers, 12 shaders, 7 XML, 16 entrées générées et 282 blocs. `parent/package-input-verification.json` a vérifié les 895 hashes A et les cinq fichiers du stage sans mismatch. Ce sont des contributions potentielles de compilation, pas une preuve d’extraction exacte de chaque membre d’archive ni des branches préprocesseur réellement incorporées. Validation humaine juridique et distribution : PARTIAL / NOT QUALIFIED.
|
||||
|
||||
`PKGBUILD.future` reste privé, SHA256 `50b098411d1234bc42f1abc748b312ce1be7ada8efe24be7d9ddb29481cba43d`. Syntaxe shell et Python du heredoc, sept inputs sources et allowlist vérifiés PASS. `makepkg --printsrcinfo`, `--verifysource`, build complet et installation : NOT RUN. Recette reproductible au sens d’un dossier d’inputs local contrôlé seulement ; aucune reproductibilité binaire publique ou distribution Arch validée. Le PKGBUILD réel, le patch production et la dépendance système restent inchangés.
|
||||
|
||||
H propose surveillance, revue d’impact, rebuild/requalification de toute paire modifiée, notices, stage, décision séparée et rollback. Release maintainer et security reviewer non nommés ; aucun SLA. Voir [décision draft](STEP20C_PRODUCTION_INTEGRATION_DECISION_DRAFT.md).
|
||||
|
||||
## Revue finale observée et publication préparée
|
||||
|
||||
Revue indépendante finale ACCEPTED / audit PASS pour la fidélité du dossier expérimental : 0 BLOCKING, 4 NON_BLOCKING, 3 FUTURE_SCOPE ; 180 checks hors ligne et 15 mutations de preuves réelles rejetées. Rapport privé `reviews/overnight-final-review.md`, SHA256 `bb4000dffb51b07b59e86f91e8dd76bc2c4f0b9513b48a52c9f6bd6c8c3f45af` ; reçu JSON SHA256 `72ba252092ca3375d344faae1c2026c8d5626fd190e87771d5c0c1ad8212c9b0`. Index scientifique post-exécution `seals/index.json`, SHA256 `cdf034e1f8466a9ae8a0e0a33aa8d0d0195273f0d1bc782667b50c3bcf682505`, contexte de preuve courant, pas un binding pré-exécution.
|
||||
|
||||
NB-01 est complété dans l’index compact depuis les grabs wire réels de chacun des huit runs concernés, avec lignes et hashes sources dans `parent/metadata-serial-completion.json`. Les stops fixtures, échecs E3 et incident IPC restent documentés. Les gates lifetime/génération/damage, couverture G, maintenance ABI/paquet/juridique et robustesse/coût des sidecars privés restent ouverts. L’acceptation documentaire ne qualifie pas la production et n’applique aucune décision. Publication préparée, pas encore exécutée à cette édition ; les refs/SHAs finaux et le résultat de publication feront foi dans le reçu privé et le handoff final.
|
||||
63
docs/design/STEP20C_LIFECYCLE_EFFECTS_AND_NATIVE_ORDERING.md
Normal file
63
docs/design/STEP20C_LIFECYCLE_EFFECTS_AND_NATIVE_ORDERING.md
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
# STEP20C — lifecycle, effets et ordre natif
|
||||
|
||||
> EXPERIMENTAL — édition documentaire du 2026-10-14, après revue finale ACCEPTED / audit PASS du dossier expérimental ; publication préparée, non exécutée à cette édition. Les résultats concernent des laboratoires privés ; aucune intégration production, installation ou décision UX. STEP20A DONE ; STEP20B EXPERIMENTAL ; STEP20C NEXT ; STEP20D–G PLANNED. Multi-output de production : NOT YET SUPPORTED. Les anciens rapports restent historiques et leurs résultats ne se transfèrent pas à un nouveau SHA.
|
||||
|
||||
L’autorité brute est le LAB privé `/home/fy59/swayfx-mosaic-overnight-mcolx0gq`. Les chemins LAB ci-dessous sont des références locales de preuve, pas des fichiers distribués. Les sources de fixture préservées se trouvent dans [tooling/probes/step20c-overnight](../../tooling/probes/step20c-overnight/qualification/README.md). La [provenance compacte](../../tooling/probes/step20c-overnight/qualification/provenance.json) lie les identités et reçus sans publier logs, images, binaires, archives ou patches candidats.
|
||||
|
||||
## E1 — owner caché et retour
|
||||
|
||||
Même chaîne TOPLEVEL→A→B, mêmes identités et grabs ; hide donne clip root vide et pixels A/B nuls, puis le retour conserve la hiérarchie sans recreation et restaure l’entrée B réelle. Les fenêtres hide/return sont rejouées, H2 RGB exact. Une motion sans enter pendant hide est observée ; elle ne prouve pas tous les hits possibles. Mesures PASS, runtime INCONCLUSIVE, cleanup PASS. Aucun UX hide/dismiss/reposition final ni click-through universel décidé.
|
||||
|
||||
## E2 — saved buffers non exercés
|
||||
|
||||
Configure29 puis30 ACKés et `hold_next_configure` armé sont réels ; aucun configure ultérieur ni HELD, deux PNG de référence seulement. Source A `sway/commands/gaps.c` : `gaps inner 30` avec argc2 appelle `gaps_set_defaults`, modifie la config sans arranger le workspace courant ; la branche runtime demande quatre arguments. Le succès IPC n’est pas un configure mesuré. Saved-node identity/render, timeout effectif et lifetime : NOT EXERCISED / NOT PROVEN ; runtime INCONCLUSIVE. Aucun timeout artificiellement prolongé ni rerun.
|
||||
|
||||
L’audit source décrit `view_save_buffer` : arbre saved frère de content_tree, destination/source/transform copiés, buffer courant locké via SceneFX set_buffer ; suppression détruit l’arbre et réactive le contenu. Cela reste SOURCE AUDIT ONLY sur A, pas une observation runtime des identités saved/locks/releases. Les anciens SB/BI/ASI/HIT instruments et résultats appartiennent à d’autres binaires et ne sont pas incorporés ici.
|
||||
|
||||
## E3 — effets et cache : résultat partiel
|
||||
|
||||
Deux tentatives,16 PNG chacune. A/B modifie226908 pixels pristine et182214 Mosaic. Whole-H1 A/A-return reste différent de33883 pixels pristine (bbox402,25–798,598) et32132 Mosaic (bbox482,25–800,598). Le critère de restauration entière échoue réellement dans les deux contrôles ; aucune causalité propre à Mosaic n’est établie. La ROI centrale64×64 change ses4096 pixels A→B puis retrouve exactement A ; quatre ROI adjacentes retrouvent A. Fraîcheur RGB échantillonnée positive, restauration full/cache/damage complet NON démontrée.
|
||||
|
||||
Native Mosaic observe régénération naturelle réussie et optimized=1 USE d’un buffer non nul. Sept états H2 Mosaic restent identiques à la référence. Shadow-on ne change aucun pixel H1, mais déborde sur21598 pixels H2 pristine (bbox0,0–36,600) ; H2 Mosaic reste exact. Les captures classic-floating off/on sont entièrement égales, ROI268,268–332,332 et métrique adjacente0→0 : filtration classic NOT EXERCISED. Classic tiled Mosaic NOT TESTED et optimized tiled spill standard non exercé par ce glass intérieur.
|
||||
|
||||
Les deux runs s’arrêtent avant effects-evidence et fermeture normale/final capture ; glass/background code1/error32 après teardown. Cleanup scientifique FAIL malgré tous les PID reapés et runtime supprimé sans rescue. Ne pas transformer l’absence de résidu en fermeture normale réussie. Verdict PARTIAL_RGB_FRESHNESS_WHOLE_RESTORATION_FAIL_INCONCLUSIVE.
|
||||
|
||||
## F — état appliqué, entrée et ordre natif
|
||||
|
||||
Pristine : trois clics/commits region réels, owner ne reçoit pas de bouton lorsque region vide, restauration mesurée ;8 PNG, verdict INCONCLUSIVE et aucune PI privée native dans ce contrôle non modifié.
|
||||
|
||||
Mosaic : input full→empty→full observé, trois clics,4 PNG, deux livraisons owner autour de l’état vide. Assertion échoue parce qu’elle conserve l’origine initiale(-318,25) pour le clic(-314,30), attend(4,5), alors que l’origine appliquée après restauration est(-638,25), cohérente avec motion wire(324,5) et bouton45/46. Aucun défaut de routing n’en découle ; focus/resize/layout/close suivants NOT EXERCISED. Runtime INCONCLUSIVE, cleanup PASS.
|
||||
|
||||
Les12 premiers couples output/commit_seq natifs analysés sur57 commits bruts ont10 PAIRED et2 MISSING_PRESENT : seq2 H1 et H2. Les45 autres commits restent dans le brut sans être transformés en preuve de pairing. Un event headless present `presented=1` ne démontre pas les pixels du buffer ni une présentation physique. Il serait incorrect de dire12/12 presents ou lifetime qualifié.
|
||||
|
||||
Source wlroots : incrément commit_seq et signal commit synchrone, present headless différé idle, idle annulé au destroy. Source instrumentation : PI_SCENE/OWNER/POPUP échantillonne la scène appliquée dans le callback output commit, PI_POPUP sans owner-ID natif. Les flux séparés ne forment pas une génération atomique.
|
||||
|
||||
| Contrat demandé | État réel |
|
||||
| --- | --- |
|
||||
| Géométrie/clip/input observés dans les fenêtres documentées | Échantillonnés, cohérents pour les mesures citées |
|
||||
| Mutation naturelle commit→input change→present discriminante | NOT EXERCISED |
|
||||
| Epoch/génération atomique transactions/scène/hit/present | NOT PROVEN |
|
||||
| Buffer exact présenté et identité native hit | NOT PROVEN |
|
||||
| Locks/références/releases GPU, saved, teardown exhaustifs | NOT PROVEN |
|
||||
| Damage complet de toutes frames et stale pixels intermédiaires | NOT PROVEN |
|
||||
| Damage minimal | NOT PROVEN |
|
||||
|
||||
TEMPORAL_DAMAGE donne temps monotoniques/extents, distincts des captures. Des extents échantillonnés ne prouvent ni toutes frames ni tous pixels ; pas de nouvelle conclusion d’atomicité ou damage-to-present. Aucun GPU readback ajouté à A/G.
|
||||
|
||||
## G et neutralité de l’instrumentation
|
||||
|
||||
Le patch privé `release-shape/release-removal.patch` (`6e8a5b50149732ac70ecd79ab8db9258956f59036aba1bf17a477866bc44a5c1`) retire26 statements de log dans7 fichiers et les helpers démontrés orphelins de trace ; hashes parents/résultats et bindings de revue vérifiés. Les patches fonctionnels clip/owner guard restent. PI listeners et ordre FBO bind avant le chemin d’échec restent : cet ordre peut avoir un effet non passif et sa neutralité n’est pas prouvée. « Sans instrumentation » serait faux.
|
||||
|
||||
G a un build/stage et un smoke horizontal propres, SMOKE_FEASIBLE ; aucune qualification complète de son lifecycle/effets ni équivalence générale trace-off/on héritée. Un nouveau SHA nécessitera contrôles et matrice appropriés.
|
||||
|
||||
## Validation et limites de provenance
|
||||
|
||||
Revue lifecycle/G :32 checks hors ligne sur vrais logs/PNG plus négatifs parent/grab/root/owner/input stale/present/RGB ; archive exécutée v2 exacte578 entrées. Revue premiers D :14 négatifs ; C :7 mutations de preuves ; R90 v3 :9 négatifs. Les échecs initiaux d’extraction/analyse et corrections hors ligne sont conservés, sans réexécution graphique. Les preuves sont `reviews/matrix-lifecycle-runtime-review.{md,json}`, `matrix-lifecycle-analysis.json` et `parent/native-source-audit.json` dans le LAB.
|
||||
|
||||
Tous les runtimes matrix ont terminal IPC1 conservé ; le chemin individuel de runtime matrix n’a pas été journalisé. `runtime_removed=true`, tous PID absents et audit parent de résidus sont observés ; suppression de chaque ancien chemin nommé n’est pas indépendamment revérifiable. La clôture parent vérifie absence de PID/socket privés. Le hash SceneFX système initial n’a pas été enregistré ; hash final seul et ctime antérieure ne doivent pas être décrits comme une comparaison initial/final. Aucun usage IPC de session desktop ni installation système.
|
||||
|
||||
## Revue finale observée et publication préparée
|
||||
|
||||
Revue indépendante finale ACCEPTED / audit PASS pour la fidélité du dossier expérimental : 0 BLOCKING, 4 NON_BLOCKING, 3 FUTURE_SCOPE ; 180 checks hors ligne et 15 mutations de preuves réelles rejetées. Rapport privé `reviews/overnight-final-review.md`, SHA256 `bb4000dffb51b07b59e86f91e8dd76bc2c4f0b9513b48a52c9f6bd6c8c3f45af` ; reçu JSON SHA256 `72ba252092ca3375d344faae1c2026c8d5626fd190e87771d5c0c1ad8212c9b0`. Index scientifique post-exécution `seals/index.json`, SHA256 `cdf034e1f8466a9ae8a0e0a33aa8d0d0195273f0d1bc782667b50c3bcf682505`, contexte de preuve courant, pas un binding pré-exécution.
|
||||
|
||||
NB-01 est complété dans l’index compact depuis les grabs wire réels de chacun des huit runs concernés, avec lignes et hashes sources dans `parent/metadata-serial-completion.json`. Les stops fixtures, échecs E3 et incident IPC restent documentés. Les gates lifetime/génération/damage, couverture G, maintenance ABI/paquet/juridique et robustesse/coût des sidecars privés restent ouverts. L’acceptation documentaire ne qualifie pas la production et n’applique aucune décision. Publication préparée, pas encore exécutée à cette édition ; les refs/SHAs finaux et le résultat de publication feront foi dans le reçu privé et le handoff final.
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
# SWAYFX_MOSAIC_STEP20C_OVERNIGHT_QUALIFICATION_AND_INTEGRATION_GATE_V1
|
||||
|
||||
> EXPERIMENTAL — édition documentaire du 2026-10-14, après revue finale ACCEPTED / audit PASS du dossier expérimental ; publication préparée, non exécutée à cette édition. Les résultats concernent des laboratoires privés ; aucune intégration production, installation ou décision UX. STEP20A DONE ; STEP20B EXPERIMENTAL ; STEP20C NEXT ; STEP20D–G PLANNED. Multi-output de production : NOT YET SUPPORTED. Les anciens rapports restent historiques et leurs résultats ne se transfèrent pas à un nouveau SHA.
|
||||
|
||||
L’autorité brute est le LAB privé `/home/fy59/swayfx-mosaic-overnight-mcolx0gq`. Les chemins LAB ci-dessous sont des références locales de preuve, pas des fichiers distribués. Les sources de fixture préservées se trouvent dans [tooling/probes/step20c-overnight](../../tooling/probes/step20c-overnight/qualification/README.md). La [provenance compacte](../../tooling/probes/step20c-overnight/qualification/provenance.json) lie les identités et reçus sans publier logs, images, binaires, archives ou patches candidats.
|
||||
|
||||
## Bilan proposé
|
||||
|
||||
Ticket exact : **SWAYFX_MOSAIC_STEP20C_OVERNIGHT_QUALIFICATION_AND_INTEGRATION_GATE_V1**. État global proposé **PARTIALLY_ADVANCED**, sous réserve de publication observée, recommandation **PRIVATE_CANDIDATE_NEEDS_TARGETED_EVIDENCE** ; jamais PRODUCTION_READY. Aucun gate production fermé par ce ticket. Le budget est clos après18 tentatives uniques et196 captures, sans retry. A et G ont chacun build/stage propres ; les mesures positives et les limites ci-dessous restent simultanément vraies.
|
||||
|
||||
## Phases A–J et verdicts structurés
|
||||
|
||||
| Phase | Verdict enregistré | Preuve / limite |
|
||||
| --- | --- | --- |
|
||||
| A replay annoté | PASS | Pins archives pax,8 patches +2 commentaires,4 fichiers tokens équivalents ; nouveau SHA propre |
|
||||
| A build/model/ELF | PASS local | SceneFX statique/Mosaic, modèle1/1, ELF/relocations/layouts ; ABI future non établie |
|
||||
| A stage | STATIC_STAGE_WITH_LICENSES=PASS |5 fichiers exacts, DESTDIR privé ; legal PARTIAL |
|
||||
| B préflight | PASS | Contrôles fixture/identité/budget prélaunch revus ; erreurs préflight corrigées hors ligne avant les runs concernés |
|
||||
| C horizontal | FEASIBLE sampled |3 runs pristine/annotated-standard/Mosaic→standard ; preuve A propre |
|
||||
| D géométries | PARTIALLY_FEASIBLE |D1/D3/D4 mesures positives, D2-Mosaic INCONCLUSIVE association fixture ; runtime tous INCONCLUSIVE |
|
||||
| E1 hide/return | SAMPLED_MEASUREMENTS_PASS_RUNTIME_INCONCLUSIVE |Même hiérarchie, clips/pixels/input échantillonnés |
|
||||
| E2 saved | NOT_EXERCISED_INCONCLUSIVE |Trigger defaults n’arrange pas courant, aucun configure HELD |
|
||||
| E3 effets/cache | PARTIAL_RGB_FRESHNESS_WHOLE_RESTORATION_FAIL_INCONCLUSIVE |ROI A/B/A fraîche ; full-H1 return échoue dans les deux contrôles ; cleanup clients FAIL |
|
||||
| F ordre/input | SAMPLED_INPUT_WITH_CEILINGS_INCONCLUSIVE |Assertion geometry stale, atomic/ref/release NOT PROVEN,10/12 pairs initiaux présents |
|
||||
| G retrait/build | PASS local / BUILDABLE |26 logs/7 fichiers, revue delta exact ; PI/FBO ordering restent |
|
||||
| G smoke | SMOKE_FEASIBLE |17 PNG, preuve G propre horizontale ; runtime PASS selon politique C/G avec IPC1 conservé |
|
||||
| H package/maintenance | PARTIALLY_FEASIBLE |Recette syntaxe/inputs PASS, makepkg NOT RUN, notices/legal PARTIAL, nominations absentes |
|
||||
| I décision intégration | DRAFT / PRIVATE_CANDIDATE_NEEDS_TARGETED_EVIDENCE |22 réponses ; aucune décision appliquée |
|
||||
| J docs/revue/publication | Revue finale ACCEPTED / audit PASS ; publication préparée, non exécutée |0 BLOCKING, 4 NON_BLOCKING, 3 FUTURE_SCOPE ; aucun commit/push revendiqué à cette édition |
|
||||
|
||||
## Évidence et discipline
|
||||
|
||||
[Reconstruction annotée](STEP20C_ANNOTATED_REBUILD_AND_PRIVATE_STAGE.md), [matrice graphique](STEP20C_ANNOTATED_GRAPHICS_MATRIX.md), [lifecycle/effets/native ordering](STEP20C_LIFECYCLE_EFFECTS_AND_NATIVE_ORDERING.md) et [22 réponses de décision](STEP20C_PRODUCTION_INTEGRATION_DECISION_DRAFT.md) sont les rapports de cette tranche. Ils ne réécrivent pas les freezes historiques. Provenance compacte liée dans leur en-tête ; source fixtures seulement, sans artefacts générés.
|
||||
|
||||
Préflight corrigé avant lancement : C inverse déco16 tests ; matrix association owner/root/temps et préservation transport33 tests ; R90 v3 association host38 tests ; G-SMOKE01/02 compteurs offline9 tests. Ledger5 tests avec rejets/concurrence ; revue runtime C7 mutations, premiers D14 négatifs, lifecycle32 checks, R90 runtime9 négatifs. Tous diagnostics initiaux et refus sont conservés. Correction offline de validateurs ne constitue pas un retry compositor. La revue source G et le smoke ont leur propre acceptation/gel.
|
||||
|
||||
Budget final : SceneFX2/2, Mosaic2/2, stages2/2, runs18/18, captures196/220 ; C3,D1–D4 chacun2,E1=1,E2=1,E3=2,F=2,G=1. Registre flock/atomique/fsync,18 tokens consommés une fois, aucun remboursement/reset.4 PNG marqueurs inputs séparés donnent200 nouvelles images ;78 assets upstream copiés ne sont pas des captures. Hash ledger `6ebedc65f30bf2c81f45b1487adf974a390a0049f632cfbb18bbbb00e3bfae3f`.
|
||||
|
||||
## Risques et critères de sortie proposés
|
||||
|
||||
Ce tableau est un registre de risques, pas une politique adoptée. P0 : aucun défaut bloquant confirmé dans l’échantillon. P1 : preuve essentielle avant intégration selon périmètre retenu ; P2 : maintenance/instrumentation/notice et décisions humaines ; P3 : couverture future hors tranche. Factuel ou hypothétique est indiqué. Tous environnements mesurés sont privés headless FX/GLES AMD780M et paire exacte A/G ; extrapolation interdite.
|
||||
|
||||
| ID / sévérité | Fait ou hypothèse / source / reproduction | Contrat affecté et travail minimal | Acceptation proposée / dépendance / humain |
|
||||
| --- | --- | --- | --- |
|
||||
| R-P0-01 / P0 |Aucun clip/input bloquant confirmé C/D1/D3/E1/G ; hypothèse de défaut ailleurs non testée ; replay PNG/wire |Ne pas conclure absence universelle ; conserver scope exact |Aucun P0 ouvert confirmé ; revue scientifique requise si nouvelle preuve |
|
||||
| R-LIFE-01 / P1 |Refs/releases GPU/saved non observées ; source audit F-SOURCE-02/07 et E2 non exercé |Ownership/lifetime : fixture configure runtime attesté puis identités natives locks/releases/teardown |Protocole et critères lifetime avant nouveau budget ; dépend revue source/ABI ; reviewer humain |
|
||||
| R-TXN-01 / P1 |F full→empty→full ;12 couples initiaux10 PAIRED/2 missing,57 commits ; fenêtre discriminante absente |État appliqué render/input/present : mesure native discriminante, sans forcer calendrier pour fabriquer PASS |Preuve d’identité et cohérence de génération selon contrat accepté ; dépend instrumentation/lifetime ; décision architecture |
|
||||
| R-DAMAGE-01 / P1 |E3 full-H1 return33883 pristine/32132 Mosaic différences, ROI exact ; causalité ouverte |Damage/stale pixels : contrôle comparable et diagnostic fenêtres/effets, all-frame scope explicite |Critères full-frame/frames intermédiaires pertinents satisfaits, sans masque opportuniste ; dépend effets/txn ; revue scientifique |
|
||||
| R-FIX-01 / P1 |D2 root ambiguity, E2 defaults, F rect stale ; sources/review raw reproductibles offline |Fixtures : owner-derived association, configure runtime attesté, geometry de la fenêtre appliquée |Nouveaux freezes/négatifs acceptés avant première exécution autorisée ; pas de retry du ticket ; reviewer |
|
||||
| R-G-01 / P1 |G horizontal smoke seul, matrice étendue NOT TESTED |Qualification exact SHA G : relevant matrix complète et contrôles propres |Aucun héritage A ; dépend risque FBO/fixtures ; gate humain |
|
||||
| R-ABI-01 / P2 |17 structs/6 offsets/121 headers/85 overlaps/34 bindings locaux, aucune ABI future |Paire épinglée ; audit headers/layout/providers/rebuild à chaque build pertinent |Nouvelle paire entièrement requalifiée ; dépend mainteneur/reviewer sécurité nommés |
|
||||
| R-INST-01 / P2 |G retire26 logs mais PI listeners/FBO bind-before-failed conservés ; neutralité hypothétique |Auditer effets de callbacks/binds, comparer contrôles adéquats |Neutralité pertinente démontrée sans comparer des états non homologues ; dépend R-G/ABI ; reviewer |
|
||||
| R-LIC-01 / P2 |895/894 inputs conservateurs, exact incorporation/legal PARTIAL |Textes/shaders/protocols/contributions conditionnelles et extraction archive |Inventaire incorporation et notices suffisantes examinés humainement avant distribution ; dépend package complet/release |
|
||||
| R-PKG-01 / P2 |PKGBUILD.future syntaxe/inputs PASS, makepkg NOT RUN |Build paquet isolé, manifeste exact5 fichiers notices justifiées, coexistence |Validation intégrale source/package/stage et choix publication distinct ; dépend R-LIC/ABI ; release maintainer |
|
||||
| R-ROLE-01 / P2 |Aucun release/security nominee/SLA ; H proposition privée |Veille/impact/rebuild/requalification/notices et décision release |Nominations humaines avant public distribution ; aucun SLA inventé |
|
||||
| R-UX-01 / P2 |Hide/dismiss/reposition/click-through ouverts ; routing mesuré seulement |Décision UX explicite avant comportement shipped ; contracts first |Acceptation utilisateur puis docs UX/architecture pertinentes ; pas de changement ici |
|
||||
| R-TRANSPORT-01 / P2 |IPC exit1 tous runs ; C/G runtime PASS policy, matrix INCONCLUSIVE |Conserver incident et distinguer fermeture process/réponse transport |Cause NOT PROVEN ; protocole futur doit documenter ce critère, sans silent upgrade ; reviewer |
|
||||
| R-FUTURE-01 / P3 |Sorties physiques/backends autres, matrices exhaustives/future pairs NOT TESTED |Qualification au besoin, pas extension générale du fork |Scope et budget séparés ; STEP20D–G restent PLANNED ; décision utilisateur |
|
||||
|
||||
## État du dépôt et publication
|
||||
|
||||
HEAD initial `4ef16a40192149d353831695fd7dcaeb594fdf01`, branche main ; le répertoire source-only overnight était non suivi avant cette mise à jour. Sept fichiers protégés comparés avant/après identiques dans `parent/protected-integrity-before-publication.json`. Production, UX, Packaging, AGENTS, PKGBUILD, patch cumulatif et fixtures historiques préservés. Aucun stage Git/commit/push par l’auteur documentaire.
|
||||
|
||||
Audit parent `cleanup-after-graphs.json` : aucun PID/socket privé restant ; runtimes supprimés selon cleanup exécuté, chemins matrix individuels non consignés. Aucun live IPC, installation ou remplacement système. SceneFX système initial SHA non enregistré ; le SHA final ne permet pas une égalité initial/final inventée.
|
||||
|
||||
Revue finale indépendante ACCEPTED / audit PASS pour le dossier expérimental. Publication origin/github préparée, non exécutée à cette édition ; le reçu privé et le handoff final porteront le résultat réel et les SHAs finaux, sans autoréférence au futur hash de commit. La publication documentaire ne transforme pas la recommandation en décision production. Prochaine petite étape justifiée : validation du delta documentaire final et publication autorisée, puis gates ciblés dans une nouvelle tranche avant toute intégration.
|
||||
|
||||
## Revue finale observée et publication préparée
|
||||
|
||||
Revue indépendante finale ACCEPTED / audit PASS pour la fidélité du dossier expérimental : 0 BLOCKING, 4 NON_BLOCKING, 3 FUTURE_SCOPE ; 180 checks hors ligne et 15 mutations de preuves réelles rejetées. Rapport privé `reviews/overnight-final-review.md`, SHA256 `bb4000dffb51b07b59e86f91e8dd76bc2c4f0b9513b48a52c9f6bd6c8c3f45af` ; reçu JSON SHA256 `72ba252092ca3375d344faae1c2026c8d5626fd190e87771d5c0c1ad8212c9b0`. Index scientifique post-exécution `seals/index.json`, SHA256 `cdf034e1f8466a9ae8a0e0a33aa8d0d0195273f0d1bc782667b50c3bcf682505`, contexte de preuve courant, pas un binding pré-exécution.
|
||||
|
||||
NB-01 est complété dans l’index compact depuis les grabs wire réels de chacun des huit runs concernés, avec lignes et hashes sources dans `parent/metadata-serial-completion.json`. Les stops fixtures, échecs E3 et incident IPC restent documentés. Les gates lifetime/génération/damage, couverture G, maintenance ABI/paquet/juridique et robustesse/coût des sidecars privés restent ouverts. L’acceptation documentaire ne qualifie pas la production et n’applique aucune décision. Publication préparée, pas encore exécutée à cette édition ; les refs/SHAs finaux et le résultat de publication feront foi dans le reçu privé et le handoff final.
|
||||
109
docs/design/STEP20C_PRODUCTION_INTEGRATION_DECISION_DRAFT.md
Normal file
109
docs/design/STEP20C_PRODUCTION_INTEGRATION_DECISION_DRAFT.md
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
# STEP20C — décision d’intégration production, draft
|
||||
|
||||
> EXPERIMENTAL — édition documentaire du 2026-10-14, après revue finale ACCEPTED / audit PASS du dossier expérimental ; publication préparée, non exécutée à cette édition. Les résultats concernent des laboratoires privés ; aucune intégration production, installation ou décision UX. STEP20A DONE ; STEP20B EXPERIMENTAL ; STEP20C NEXT ; STEP20D–G PLANNED. Multi-output de production : NOT YET SUPPORTED. Les anciens rapports restent historiques et leurs résultats ne se transfèrent pas à un nouveau SHA.
|
||||
|
||||
L’autorité brute est le LAB privé `/home/fy59/swayfx-mosaic-overnight-mcolx0gq`. Les chemins LAB ci-dessous sont des références locales de preuve, pas des fichiers distribués. Les sources de fixture préservées se trouvent dans [tooling/probes/step20c-overnight](../../tooling/probes/step20c-overnight/qualification/README.md). La [provenance compacte](../../tooling/probes/step20c-overnight/qualification/provenance.json) lie les identités et reçus sans publier logs, images, binaires, archives ou patches candidats.
|
||||
|
||||
## Recommandation soumise, non appliquée
|
||||
|
||||
**PRIVATE_CANDIDATE_NEEDS_TARGETED_EVIDENCE**. L’état global proposé est PARTIALLY_ADVANCED, sous réserve de publication observée, jamais PRODUCTION_READY. Aucun défaut P0 clip/input confirmé dans le scope mesuré, mais les gates lifetime, damage/transactions, effets, ABI-maintenance, packaging/notices et politique humaine restent ouverts. L’issue globale scientifique est PARTIALLY_ADVANCED ; ce draft ne décide pas une intégration et n’autorise ni distribution ni STEP20D.
|
||||
|
||||
Les réponses sont liées aux [matrices exactes](STEP20C_ANNOTATED_GRAPHICS_MATRIX.md), au [build/stage](STEP20C_ANNOTATED_REBUILD_AND_PRIVATE_STAGE.md) et aux [limites lifecycle](STEP20C_LIFECYCLE_EFFECTS_AND_NATIVE_ORDERING.md). Elles distinguent preuve locale et contrat futur.
|
||||
|
||||
## 1. Le nouveau binaire annoté est-il réellement qualifié?
|
||||
|
||||
A est qualifié localement pour source/build/model/ELF/stage et possède ses propres mesures graphiques C/D/E/F. FEASIBLE pour C horizontal ; PARTIALLY_FEASIBLE globalement. Ni qualification production complète ni READY.
|
||||
|
||||
## 2. Quelles matrices ont été exercées?
|
||||
|
||||
18 runs uniques : C0/C1/C2 horizontal négatif ; D1 vertical pair ; D2 scale1,25 pair ; D3 R90/1,25 pair ; D4 resize/migration pair ; E1 hide/return ; E2 saved trigger ; E3 effets/cache pair ; F regions/ordering pair ; G smoke. Les limites et actions non exercées restent explicites dans la matrice.
|
||||
|
||||
## 3. Quels contrôles pristine sont disponibles?
|
||||
|
||||
C0 et D1/D2/D3/D4/E3/F pristine réellement exécutés sur le SHA retenu. C0/C1 full RGB égal aux huit états homologues ; D2/D3 markers et spill/input ordinary effectifs. Pristine F non modifié ne porte aucune PI privée. Ces contrôles ne qualifient pas un autre SHA.
|
||||
|
||||
## 4. Quels résultats restent partiels?
|
||||
|
||||
D2-Mosaic association root ambiguë avant input/final ; E2 saved NOT EXERCISED ; E3 full-H1 return FAIL dans les deux contrôles, classic NOT EXERCISED ; F géométrie fixture périmée et suite non exercée ; G horizontal smoke seul ; ABI future/notices/legal/package complets non qualifiés.
|
||||
|
||||
## 5. Existe-t-il un défaut bloquant de clipping ou d’input?
|
||||
|
||||
Aucun défaut bloquant démontré dans les fenêtres échantillonnées C/D1/D3/D4/E1/G. D2/F stops sont expliqués par le fixture, sans PASS rétroactif. Cela ne prouve pas l’absence universelle de défaut.
|
||||
|
||||
## 6. Existe-t-il un risque de lifetime non résolu?
|
||||
|
||||
Oui : identité native buffers, locks/références/releases GPU/saved, teardown exhaustif et scénarios de commit rejeté ne sont pas prouvés. Cleanup PID propre est une preuve de processus, pas de lifetime GPU. G garde FBO bind-before-failed potentiellement non passif.
|
||||
|
||||
## 7. Quel est l’état réel du damage?
|
||||
|
||||
Échantillons et traces extents conservés, H2 exact dans les états citées ; whole-H1 A-return effets échoue dans les deux contrôles. Damage exhaustif, minimal, stale pixels intermédiaires et contenu exact présenté NOT PROVEN.
|
||||
|
||||
## 8. Quelle est la portée des preuves de transactions?
|
||||
|
||||
Source applied clip et callbacks échantillonnés, regions full/empty/full observées. Sur12 couples initiaux F,10 PAIRED/2 MISSING_PRESENT ;57 commits bruts. Pas d’epoch atomique ni de fenêtre naturelle discriminante ni d’identité pixels/hit/present complète.
|
||||
|
||||
## 9. Le candidat release-shape existe-t-il?
|
||||
|
||||
Oui, G privé source/build/stage exact existe, SHA8063478b2d5948da3eb8b1a329978882dc1d045cf36932cc2145a78af20ba76a, build ID83896fdb36f3fd21522becec0e8c5219e15f4b8e. Aucun candidat binaire/package public distribué.
|
||||
|
||||
## 10. Son comportement est-il testé ou seulement compilé?
|
||||
|
||||
G est buildable et possède un smoke propre17 PNG/8 clics/4 grabs/retour splith, indépendamment revu. SMOKE_FEASIBLE horizontal seulement ; matrice G étendue et neutralité complète NOT TESTED/NOT PROVEN.
|
||||
|
||||
## 11. Quels patches fonctionnels doivent être conservés?
|
||||
|
||||
Dans le candidat privé : subtree clip SceneFX, clip owner-derived popup, guard owner totalement caché et delta natif Mosaic. Les commentaires token-equivalent sont acceptés. Une extraction de production exige encore revue exacte source/ownership/lifetime et décision, sans appliquer ces patches maintenant.
|
||||
|
||||
## 12. Quels patches de trace peuvent être retirés?
|
||||
|
||||
Seulement le retrait privé G déjà revu :26 logs/7 fichiers et helpers réellement orphelins. Timing/cache/PI ne se retirent pas en bloc sans audit ; PI listeners et FBO bind ordering restent. Aucun patch de production réécrit.
|
||||
|
||||
## 13. Quelle ABI SceneFX est réellement qualifiée?
|
||||
|
||||
SceneFX0.5 statique exact local, ELF appels clip directs,17 structs/6 offsets/85 overlaps/34 diagnostic bindings. Pas une ABI externe stable, ni tous providers runtime, ni une ABI future ou full semantic safety.
|
||||
|
||||
## 14. Quelle paire wlroots est réellement qualifiée?
|
||||
|
||||
Paire locale SceneFX37ccd723bef49e6891156ffafce8f549f01446cc + wlroots0.20.2 d783533489e1f75d6886c2ab5c5960090ef268f8, libSHA7edca9a7d65855addda68e29ba5db94d57c803eb5fa7d0f86db3136b90ca9837 et121 headers épinglés. Tous autres builds/paires NOT QUALIFIED ; changement implique rebuild/requalification.
|
||||
|
||||
## 15. La future recette Arch est-elle reproductible?
|
||||
|
||||
Inputs privés hashés, syntaxe shell/Python et sources PASS ; makepkg metadata/source verification/build NOT RUN. Pas de reproductibilité binaire bit à bit ni de paquet distribution publique validé. PKGBUILD.future ne modifie pas PKGBUILD réel.
|
||||
|
||||
## 16. Quels textes de licence restent à valider?
|
||||
|
||||
Les quatre fichiers stage incluent LICENSE SceneFX/SwayFX/wlroots et INCORPORATED-NOTICES. Inventaires895 A/894 G conservateurs ; incorporation exacte, extraction archives et contributions conditionnelles puis suffisance juridique nécessitent validation humaine. Aucun legal PASS.
|
||||
|
||||
## 17. Qui devra assurer les releases et la sécurité?
|
||||
|
||||
Responsabilité projet pour SceneFX embarqué déjà approuvée dans le périmètre privé ; release maintainer et security reviewer doivent être désignés avant distribution. Aucun nom ni SLA inventé. Veille, impact, rebuild, tests, notices et publication documentés comme procédure proposée.
|
||||
|
||||
## 18. Quelle politique popup UX reste à décider?
|
||||
|
||||
Hide/dismiss/reposition de popup offscreen et comportement sous grab/click-through restent ouverts. Les observations de routing ne fixent pas une politique universelle. Aucun changement UX dans cette tranche.
|
||||
|
||||
## 19. Quelles corrections sont nécessaires avant une intégration dans main?
|
||||
|
||||
Nouveau fixture ciblé saved provoquant configure runtime attesté, diagnostic full-H1 effets avec contrôles, assertion input liée à géométrie appliquée, mesure lifetime/native refs/releases/temporal discriminante et neutralité G/FBO. Chaque gate pertinent doit avoir critères acceptés et nouvelle autorisation/budget ; aucun rerun dans le budget clos. Puis politiques humaines ABI/notices/release/security/UX, contrats canoniques avant code.
|
||||
|
||||
## 20. Quelle est la plus petite tranche de production envisageable après décision utilisateur?
|
||||
|
||||
Après décision humaine et gates pertinents clos seulement : clip natif optionnel dans workspace Sway ordinaire, Row0 empruntant workspace->tiling, owner popup clip et hidden-owner guard revus, restauration standard locale préservée. Contrats Architecture/UX/Packaging/Upstream/Development d’abord, puis petit delta revu/testé. Aucun STEP20D, ligne runtime additionnelle, animation ou taille finale ; aucune implementation ici.
|
||||
|
||||
## 21. Quels tests doivent impérativement être refaits après une intégration?
|
||||
|
||||
Nouveau exact SHA : compile/model/headless IPC et standard pristine, export cumulatif forward apply/source-equivalence/checksum, ELF/header/layout/providers/relocations, manifests stage/notices et package complet, toute matrice graphique C/D/E/F pertinente et smoke propre G, input/popups/grabs/retour standard, geometry scale/rotation/migration, saved/effects/damage/lifetime. Pas d’héritage automatique du LAB.
|
||||
|
||||
## 22. Quel plan de rollback protège SwayFX standard?
|
||||
|
||||
Proposition future : conserver package SwayFX et /usr/bin/sway indépendants, sélectionner sa session existante après quitter la candidate ; retirer seulement fichiers appartenant à Mosaic vérifiés. Aucun provides/conflicts/replaces, système lib/config/dotfiles remplacé. Déploiement et rollback nécessitent décision distincte, jamais exécutés ici.
|
||||
|
||||
## Conditions de décision
|
||||
|
||||
Ce dossier apporte un candidat et des preuves reviewables. La revue finale indépendante est ACCEPTED / audit PASS pour la fidélité de l’évidence expérimentale, sans adoption production. Publication documentaire préparée, non exécutée à cette édition. Publication des docs/fixtures et adoption en production sont des actes distincts ; le résultat réel et les SHAs finaux origin/github feront foi dans le reçu privé et le handoff final, sans faux hash futur dans ce draft.
|
||||
|
||||
## Revue finale observée et publication préparée
|
||||
|
||||
Revue indépendante finale ACCEPTED / audit PASS pour la fidélité du dossier expérimental : 0 BLOCKING, 4 NON_BLOCKING, 3 FUTURE_SCOPE ; 180 checks hors ligne et 15 mutations de preuves réelles rejetées. Rapport privé `reviews/overnight-final-review.md`, SHA256 `bb4000dffb51b07b59e86f91e8dd76bc2c4f0b9513b48a52c9f6bd6c8c3f45af` ; reçu JSON SHA256 `72ba252092ca3375d344faae1c2026c8d5626fd190e87771d5c0c1ad8212c9b0`. Index scientifique post-exécution `seals/index.json`, SHA256 `cdf034e1f8466a9ae8a0e0a33aa8d0d0195273f0d1bc782667b50c3bcf682505`, contexte de preuve courant, pas un binding pré-exécution.
|
||||
|
||||
NB-01 est complété dans l’index compact depuis les grabs wire réels de chacun des huit runs concernés, avec lignes et hashes sources dans `parent/metadata-serial-completion.json`. Les stops fixtures, échecs E3 et incident IPC restent documentés. Les gates lifetime/génération/damage, couverture G, maintenance ABI/paquet/juridique et robustesse/coût des sidecars privés restent ouverts. L’acceptation documentaire ne qualifie pas la production et n’applique aucune décision. Publication préparée, pas encore exécutée à cette édition ; les refs/SHAs finaux et le résultat de publication feront foi dans le reçu privé et le handoff final.
|
||||
104
tooling/probes/step20c-overnight/HORIZONTAL.md
Normal file
104
tooling/probes/step20c-overnight/HORIZONTAL.md
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
# Exact annotated horizontal fixture
|
||||
|
||||
EXPERIMENTAL / PREPARATION. Runtime **NOT RUN**, independent review **PENDING**.
|
||||
No production, UX, PKGBUILD, cumulative patch, installation or desktop action.
|
||||
STEP20C stays NEXT; production multi-output NOT YET SUPPORTED.
|
||||
|
||||
Only the explicitly owned overnight LAB is accepted. `phase-a.json` supplies the
|
||||
new annotated staged binary; the previous D binary cannot satisfy the frozen
|
||||
identity. The retained pristine control is separately bound to its fd71 source
|
||||
archive, SceneFX37ccd source archive, original build receipt, exact binary and
|
||||
mapped SceneFX library. Both use the current exact wlroots0.20 library. Native
|
||||
startup log lines establish the actual FX/GL renderer; requested environment
|
||||
`WLR_RENDERER=pixman` is metadata, never renderer proof.
|
||||
|
||||
C0 runs pristine splith, C1 annotated splith, C2 annotated Mosaic. Every case has
|
||||
H1=(-800,0,800,600), H2=(0,0,800,600), scale1, transform normal. H2 is a static
|
||||
opaque SHM XDG client #303038; two separated initial whole RGB images must be
|
||||
identical and uniform. No mask, channel tolerance or excluded pixels.
|
||||
|
||||
The clients are copied privately from the actual retained static_cross sources
|
||||
and compiled with C17. Scanner regeneration must reproduce every retained XDG
|
||||
and virtual-pointer generated file byte for byte; XML and source hashes are
|
||||
frozen. TOPLEVEL→A→B hierarchy is checked against actual object IDs, get_popup
|
||||
wire requests, genuine left-button serials, grab-before-initial-commit,
|
||||
configure/ACK, buffer dimensions and mapping order. Each serial is consumed by
|
||||
one distinct grab; C2's second chain uses new owner/PID and four distinct ordered
|
||||
serials across both chains.
|
||||
|
||||
NativeSHM, XDG configure, IPC assigned destination, applied owner scene,
|
||||
global logical coordinates, output logical rectangles and physical PNG pixels
|
||||
remain separate. At scale1 normal the logical-to-physical transform is identity
|
||||
apart from output origin; all rectangles use half-open bounds. C0 has no private
|
||||
scene trace: its current IPC assignment is explicitly labelled, then compared
|
||||
to measured paint and fresh pointer wire coordinates. C1 also corroborates the
|
||||
assignment against the actual applied PI_OWNER and ordinary unclipped PI_SCENE.
|
||||
Neither establishes an atomic transaction generation or screenshot/native
|
||||
buffer identity.
|
||||
|
||||
C0/C1 open a genuine crossing A/B chain, measure full standard popup pixels on
|
||||
both outputs, click B inside its actual H2 paint, explicitly close B, verify A
|
||||
survives with no B residual on either output, click A inside its fresh H2 paint,
|
||||
then close the owner normally. Final whole H2 equals the initial RGB image;
|
||||
final H1 has no A/B colors. Four complete clicks, two grabs, one chain,
|
||||
10 captures, five pointer placements; no camera or transition.
|
||||
|
||||
C2 opens E1 after a reserved camera correction derived from measured applied
|
||||
owner geometry. Owner, A and B must actually cross x=0. H1 retains A/B pixels;
|
||||
whole H2 stays equal. A real right click inside H1 B must reach B. A real right
|
||||
click in B's hypothetical H2 extent must reach neither popup; actual popup_done
|
||||
and destruction order must be B then A. Any reference delivery is recorded as
|
||||
outside-grab routing, not promised universal click-through. E1 H2 must be clean.
|
||||
The same compositor then opens fresh E2. A con_id-targeted splith preserves live
|
||||
A/B; all selected callback records preserve old Mosaic then show two coherent
|
||||
standard observations and clip removal through B input. Actual standard H2 A/B
|
||||
pixels, B input, explicit child close/A survival, A input and normal owner close
|
||||
are checked as in controls. Final whole H2 equality is exact. Eight clicks,
|
||||
four grabs, two chains, up to two camera corrections, one transition,
|
||||
17 captures and ten pointer placements. No retry or calibration loop.
|
||||
|
||||
`horizontal_adapter.py` applies explicit unique-occurrence transformations to
|
||||
the sealed historical E2 runner. Generated runners exist only in LAB, are AST
|
||||
compiled, source-hashed and reviewed with every reused dependency. The original
|
||||
historical fixture and its source/receipts remain read only. The transition
|
||||
validator is rebound to the exact annotated output/transaction/XDG sources;
|
||||
old binary/source acceptance is never transferred.
|
||||
|
||||
Global ledger:18 runs,220 PNG and exactly three C runs, durable reservation before
|
||||
compositor creation/capture. Local action ledgers additionally reserve every
|
||||
click, move, grab, chain, camera and transition before the corresponding action.
|
||||
Reserved and observed consumed counts remain separate; failures never refund.
|
||||
Per-run maximum150s, active watchdog143s and bounded logs/process waits. Private
|
||||
runtime0700, inherited desktop/loader sockets removed, explicit `swaymsg -s`
|
||||
on every IPC command. Only owned process groups are stopped/reaped; runtime and
|
||||
sockets are removed. Actual terminal IPC/client/compositor codes are preserved.
|
||||
|
||||
The C controls are comparison evidence, not automatic dispatch vetoes. A shared
|
||||
fixture defect requires review before continuing; otherwise each case can pass
|
||||
its own preflight. Missing/failed control prevents a full comparative verdict
|
||||
and must be reported explicitly; no inferred pristine PASS.
|
||||
|
||||
## Preparation and review
|
||||
|
||||
`horizontal_gate.py prepare LAB` compiles only C clients; it creates no socket
|
||||
and reserves no graphical run. `horizontal_tests.py --output RECEIPT` executes
|
||||
pure/isolated negative tests. Failed first-pass receipts are preserved separately.
|
||||
`horizontal_gate.py seal LAB` freezes final fixture/dependency/client/generated
|
||||
runner/binary/library/source/provenance/test bytes; it cannot create acceptance.
|
||||
|
||||
A genuine independent read-only reviewer writes LAB/horizontal/review.json:
|
||||
status ACCEPTED, independent=true, read_only=true,
|
||||
scope OVERNIGHT_HORIZONTAL_FINAL_BYTES, blockers=[],
|
||||
freeze_sha256=SHA256(freeze.json), files=the exact freeze files mapping,
|
||||
report_file=absolute report path, report_sha256=its exact SHA256.
|
||||
No historical or synthetic acceptance is valid. Final byte changes invalidate
|
||||
acceptance. `horizontal_gate.py preflight LAB --case C0` is read only and requires
|
||||
that review. `run.py LAB C0` then consumes a unique O_EXCL case token before the
|
||||
run debit; C1/C2 have separate tokens. Existing runs, sockets, bad identity,
|
||||
changed reviewed bytes and exhausted budgets fail closed. No actual run has been
|
||||
performed during fixture preparation.
|
||||
|
||||
Remaining gates include wider outputs/migration, complete damage/intermediate
|
||||
frames, atomic generation, static effects/hidden-owner/saved buffers and
|
||||
ABI/distribution/legal/maintenance. Successful C alone is bounded horizontal
|
||||
feasibility, never STEP20C production readiness.
|
||||
131
tooling/probes/step20c-overnight/horizontal_adapter.py
Normal file
131
tooling/probes/step20c-overnight/horizontal_adapter.py
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
"""Bounded explicit transformations of the sealed E2 runner, never a broad copy.
|
||||
|
||||
Generated runners are private frozen review artifacts. Reused validators retain
|
||||
native/configured/assigned/applied distinctions and half-open coordinate rules.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import ast
|
||||
import hashlib
|
||||
|
||||
BASE = Path(__file__).resolve().parent.parent / 'step20c-static-e2-completion'
|
||||
BASE_SHA = '1bd67d5269b5492f5d9ac6e7b670a4db0345b0e2cd3f89930dcbd154c4473504'
|
||||
REUSE = ('model.py','observations.py','ipc_predicates.py','pixels.py','predicates.py','lifecycle.py','transition.py','support.py')
|
||||
|
||||
def replace_one(text, old, new):
|
||||
if text.count(old) != 1:
|
||||
raise ValueError('sealed transformation occurrence mismatch: '+old[:80])
|
||||
return text.replace(old,new,1)
|
||||
|
||||
STANDARD_EXTERIOR = "def splith_exterior(owner, layout):\n # Pinned sway/ipc-json.c:899-906: count=1 unless parent is stacked.\n # This inverse is deliberately scoped to the ordinary splith controls.\n require(layout == 'splith', 'standard-splith-only')\n assigned = owner['rect']; deco = owner['deco_rect']['height']\n require(type(deco) is int and deco >= 0, 'standard-decoration-height')\n return [assigned['x'], assigned['y'] - deco,\n assigned['width'], assigned['height'] + deco]\n\n"
|
||||
|
||||
STANDARD_OPEN = '''def open_standard(name):
|
||||
global stage,baseline
|
||||
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=snapshot(name+'-assigned');ws=workspace(snap['tree'],'1')
|
||||
owner=next(n for n in nodes(ws) if n.get('pid')==proc.pid)
|
||||
require(ws['layout']=='splith' and owner['visible'] and owner['focused'],'standard-owner')
|
||||
require(owner['app_id']=='org.mosaic.NestedGrabProbe' and owner['shell']=='xdg_shell','standard-owner-identity')
|
||||
refstate=validate_reference(snap,refid,ref.pid)
|
||||
inject('move',[-790,590]);first=capture('H2-reference-1','HEADLESS-2');time.sleep(.6)
|
||||
second=capture('H2-reference-2','HEADLESS-2');baseline=first
|
||||
save(lab,'reference-stability',dict(status='PASS',comparison=reference(first,second),reference=refstate))
|
||||
topim=capture(name+'-top','HEADLESS-1');bbox=paint(topim,'TOPLEVEL')['bbox'];require(bbox is not None,'standard-owner-paint')
|
||||
# C0 has no private scene trace. Current IPC assignment is explicitly named;
|
||||
# the actual painted origin and fresh pointer wire coordinates corroborate it.
|
||||
assigned=dict(owner['rect']);exterior=splith_exterior(owner,ws['layout'])
|
||||
measured=dict(id=owner['id'],workspace=ws['id'],scene=exterior,IPC=owner,
|
||||
raw='CURRENT IPC raw assignment; splith exterior inverse ipc-json.c:899-906; actual painting/input checked separately')
|
||||
applied=applied_model(measured,owner,log(name),name+'-standard-assigned-observation')
|
||||
content_rect=content(applied);origin=[content_rect.x,content_rect.y]
|
||||
require([bbox[0]-800,bbox[1]]==origin,'standard-painted-assigned-origin')
|
||||
out=output_model(snap,'HEADLESS-1',content_rect.provenance)
|
||||
verify_paint(content_rect,out,bbox,paint(topim,'TOPLEVEL')['count'])
|
||||
if CASE=='C1':
|
||||
scene=scene_from_log(log('compositor'),'HEADLESS-1');real=measure_owner(log('compositor'),owner)
|
||||
require(not scene['clip_valid'] and scene['layout_enum']==1,'standard-no-native-clip')
|
||||
require(real['scene']==measured['scene'],'standard-applied-scene-matches-assignment')
|
||||
owners[name]=dict(id=owner['id'],pid=proc.pid,workspace=ws['id'],assigned=assigned,exterior=exterior,content_origin=origin,
|
||||
native=applied.native.rectangle.values(),configured=applied.configured.values(),paint=paint(topim,'TOPLEVEL'))
|
||||
save(lab,'applied-owner-geometry',owners)
|
||||
xy=point(topim,'TOPLEVEL',-800);require(applied.native.rectangle.contains([xy[0]-origin[0],xy[1]-origin[1]]),'opening-native-bounds')
|
||||
cp=click(name+'-open-A',name,xy,True,'left');wait(lambda:'POPUP_A MAP ' in log(name));time.sleep(.25)
|
||||
correlate_pointer(name,xy,content_rect.values(),'TOPLEVEL',cp['pointer_protocol'][name])
|
||||
aim=capture(name+'-A','HEADLESS-1');xyA=point(aim,'POPUP_A',-800)
|
||||
cpB=click(name+'-open-B',name,xyA,True,'left');wait(lambda:'POPUP_B MAP ' in log(name));time.sleep(.3)
|
||||
prot=protocol(log(name));opening_pair(cp['events'][name],'TOPLEVEL',prot['grabs'][0]['serial'])
|
||||
opening_pair(cpB['events'][name],'POPUP_A',prot['grabs'][1]['serial'])
|
||||
require(not any(cp['events'][n] or cpB['events'][n] for n in ('reference','filler')),'opening-other-client')
|
||||
g=geometry(log(name),origin,content_rect.provenance);require(all(v=='EXERCISED' for v in g['crossing'].values()),'standard-root-child-crossing')
|
||||
correlate_pointer(name,xyA,g['A'],'POPUP_A',cpB['pointer_protocol'][name])
|
||||
left=capture('standard-H1','HEADLESS-1');right=capture('standard-H2','HEADLESS-2')
|
||||
rgb=popup_pixels(left,right,g,False,baseline);verify_owner_occlusion(left,right,content_rect,g,False)
|
||||
chains[name]=dict(protocol=prot,geometry=g,pixels=rgb,owner=owners[name])
|
||||
save(lab,'xdg-protocol-order',{name:prot});save(lab,'standard-popup-pixels',rgb)
|
||||
return proc,owner,g,left,right,content_rect
|
||||
'''
|
||||
|
||||
E1 = ''' E1,owner1,g1,im1=open_chain('E1')
|
||||
stage='E1-visible-B';xy=point(im1,'POPUP_B',-800);row=click(stage,'E1',xy)
|
||||
result=delivery(row,'E1','POPUP_B');coords=correlate_pointer('E1',xy,g1['B'],'POPUP_B',row['pointer_protocol']['E1']);mapped(log('E1'))
|
||||
save(lab,'mosaic-input',dict(status='PASS',pair=result,coordinates=coords,click=row))
|
||||
stage='E1-forbidden-B';B=g1['B'];xy=[int((max(0,B[0])+min(800,B[0]+B[2]))//2),B[1]+30]
|
||||
require(B[0]<=xy[0]<B[0]+B[2] and 8<=xy[0]<792 and B[1]<=xy[1]<B[1]+B[3],'forbidden-point-real-child-H2')
|
||||
cut=len(log('E1'));row=click(stage,'E1',xy);time.sleep(.3)
|
||||
ds=dismissal(log('E1')[cut:],row['events']['E1'])
|
||||
require(not row['events']['filler'],'forbidden-wrong-filler-recipient')
|
||||
refs=row['events']['reference'];require(len(refs) in (0,2),'outside-reference-pair')
|
||||
if refs:
|
||||
require(all('button=273 ' in s for s in refs) and ['state=1' in refs[0],'state=0' in refs[1]]==[True,True],'outside-reference-button')
|
||||
save(lab,'forbidden-input',dict(status='PASS',click=row,dismissal=ds,reference_events=refs,
|
||||
interpretation='outside dismissal; reference routing recorded, no universal click-through claim'))
|
||||
inject('move',[-790,590]);clean=capture('E1-dismissed-H2','HEADLESS-2');require(equal(baseline,clean)['equal'],'dismissal-whole-H2-residue')
|
||||
close_client(E1,'E1','close_owner')
|
||||
E2,owner2,g2,im2=open_chain('E2')
|
||||
fresh_chain(dict(owner=owner1['id'],pid=E1.pid,serials=[g['serial'] for g in chains['E1']['protocol']['grabs']]),
|
||||
dict(owner=owner2['id'],pid=E2.pid,serials=[g['serial'] for g in chains['E2']['protocol']['grabs']]))
|
||||
'''
|
||||
|
||||
def generate(case):
|
||||
if case not in ('C0','C1','C2'):raise ValueError('unknown C case')
|
||||
text=(BASE/'run.py').read_text()
|
||||
if hashlib.sha256(text.encode()).hexdigest()!=BASE_SHA:raise ValueError('sealed base changed')
|
||||
text=replace_one(text,'from authorization import authorize\n','')
|
||||
begin=text.index('p=argparse.ArgumentParser()');end=text.index("run=lab/'run';run.mkdir()")
|
||||
text=text[:begin]+'''# Gate, unique consumption and run reservation precede even runtime directory creation.
|
||||
from horizontal_gate import begin_run, debit, binding_sources, finish_run
|
||||
LAB,CASE,lab,auth=begin_run()
|
||||
BINARY=Path(auth['binary']);BINARY_SHA=auth['binary_sha256'];WLROOTS_SHA=auth['wlroots_sha256']
|
||||
def reserve(unused,*kinds):return debit(LAB,CASE,lab,*kinds)
|
||||
def source_bindings():return binding_sources(auth)
|
||||
import transition
|
||||
transition.source_bindings=source_bindings
|
||||
'''+text[end:]
|
||||
text=replace_one(text,"reserve(lab,'capture');time.sleep(.20)","reserve(lab,'capture');time.sleep(.20)")
|
||||
text=replace_one(text,"assert Path(f'/proc/{comp.pid}/exe').resolve()==lab/'swayfx-mosaic'","assert Path(f'/proc/{comp.pid}/exe').resolve()==BINARY.resolve() and sha(BINARY)==BINARY_SHA")
|
||||
text=replace_one(text,"assert not any('libscenefx' in x for x in paths)","assert CASE=='C0' or not any('libscenefx' in x for x in paths)")
|
||||
text=replace_one(text," save(lab,'dependency-mappings',"," if CASE=='C0':\n scene_paths=[Path(x).resolve() for x in paths if 'libscenefx' in x]\n require(scene_paths==[Path(auth['pristine']['scene_path'])] and sha(scene_paths[0])==auth['pristine']['scene_sha256'],'pristine mapped SceneFX identity')\n save(lab,'dependency-mappings',")
|
||||
text=replace_one(text,"SceneFX_shared=False","SceneFX_shared=any('libscenefx' in x for x in paths)")
|
||||
marker=" outputs=ipc('-t','get_outputs');validate_outputs(outputs);save(lab,'output-geometry',outputs)"
|
||||
text=replace_one(text,marker,""" actual=[s for s in log('compositor').splitlines() if any(k in s for k in ('Creating scenefx FX renderer','GL renderer:','Using OpenGL ES','GL vendor:'))]
|
||||
require(any('Creating scenefx FX renderer' in s for s in actual) and any('GL renderer:' in s for s in actual),'actual-renderer-log-required')
|
||||
save(lab,'actual-renderer',dict(status='PASS',source='actual compositor startup log',lines=actual,requested_env_renderer=env.get('WLR_RENDERER')))
|
||||
"""+marker)
|
||||
text=replace_one(text," comp=spawn('compositor',[lab/'swayfx-mosaic','-d','-c',run/'config'])"," comp=spawn('compositor',[BINARY,'-d','-c',run/'config'])")
|
||||
text=replace_one(text," verify(lab/'results/fixture-freeze-sha256.txt')"," finish_run(LAB,CASE,lab)")
|
||||
if case=='C2':
|
||||
text=replace_one(text," if name=='E2':"," if name=='E1':")
|
||||
text=replace_one(text," content_rect=content(applied);origin=[content_rect.x,content_rect.y]"," content_rect=content(applied);origin=[content_rect.x,content_rect.y]\n require(content_rect.x < 0 < content_rect.end[0],'real-owner-crossing')")
|
||||
text=replace_one(text," E2,owner2,g2,im2=open_chain('E2')\n",E1)
|
||||
else:
|
||||
text=text[:text.index('\ndef open_chain(name):')]+ '\n'+STANDARD_EXTERIOR+STANDARD_OPEN+text[text.index('\ntry:\n comp='):]
|
||||
text=replace_one(text,";cmd('layout mosaic')",";cmd('layout splith')")
|
||||
start=text.index(" E2,owner2,g2,im2=open_chain('E2')")
|
||||
end=text.index(" stage='E2-standard-B';")
|
||||
text=text[:start]+" E2,owner2,stdg,stdleft,stdright,stdcontent=open_standard('E2')\n"+text[end:]
|
||||
start=text.index(' # Re-examine every trace through the B input')
|
||||
end=text.index(" before_close=log('E2');",start)
|
||||
text=text[:start]+text[end:]
|
||||
ast.parse(text)
|
||||
return text
|
||||
168
tooling/probes/step20c-overnight/horizontal_gate.py
Normal file
168
tooling/probes/step20c-overnight/horizontal_gate.py
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
"""Offline preparation and fail-closed independent review / exact-byte runtime gate."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import fcntl
|
||||
from horizontal_adapter import BASE,REUSE,generate
|
||||
import ledger
|
||||
|
||||
HERE=Path(__file__).resolve().parent
|
||||
DEFAULT_LAB=Path('/home/fy59/swayfx-mosaic-overnight-mcolx0gq')
|
||||
OLD_CLIENTS=Path('/home/fy59/swayfx-mosaic-static-nested-runtime-pvn0osxm/client-build')
|
||||
PRISTINE=Path('/home/fy59/swayfx-mosaic-optimized-edge-a55yjb8n/pristine-build/sway/sway')
|
||||
PRISTINE_SHA='8ac865481bc0487344210cd0e16d8e6c7655b29db887cd58ddc11b0ca22fe494'
|
||||
WLROOTS=Path('/usr/lib/libwlroots-0.20.so').resolve()
|
||||
PLANS={'C0':dict(chain=1,grab=2,click=4,capture=10,camera=0,transition=0,move=5),
|
||||
'C1':dict(chain=1,grab=2,click=4,capture=10,camera=0,transition=0,move=5),
|
||||
'C2':dict(chain=2,grab=4,click=8,capture=17,camera=2,transition=1,move=10)}
|
||||
|
||||
def sha(p):return hashlib.sha256(Path(p).read_bytes()).hexdigest()
|
||||
def read(p):return json.loads(Path(p).read_text())
|
||||
def require(ok,msg):
|
||||
if not ok:raise ValueError(msg)
|
||||
def exclusive(p,data):
|
||||
fd=os.open(p,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
|
||||
with os.fdopen(fd,'w') as f:
|
||||
json.dump(data,f,indent=2);f.write('\n');f.flush();os.fsync(f.fileno())
|
||||
def durable(p,data):
|
||||
from importlib.util import spec_from_file_location,module_from_spec
|
||||
spec=spec_from_file_location('horizontal_support',BASE/'support.py');m=module_from_spec(spec);spec.loader.exec_module(m)
|
||||
m.durable(p,data)
|
||||
def env():
|
||||
spec=__import__('importlib.util',fromlist=['spec_from_file_location']).spec_from_file_location('horizontal_support',BASE/'support.py')
|
||||
m=__import__('importlib.util',fromlist=['module_from_spec']).module_from_spec(spec);spec.loader.exec_module(m)
|
||||
return m.environment()
|
||||
|
||||
def binding_sources(auth):
|
||||
rows=auth['trace_sources'];require(all(sha(p)==h for p,h in rows.items()),'annotated trace source changed')
|
||||
return {Path(p).name:h for p,h in rows.items()}
|
||||
|
||||
def prepare(lab):
|
||||
lab=Path(lab).resolve();require(lab==DEFAULT_LAB,'only registered overnight lab')
|
||||
a=read(lab/'phase-a.json');require(a['status']=='PASS' and sha(a['binary']['path'])==a['binary']['sha256'],'phase A exact binary')
|
||||
require(sha(PRISTINE)==PRISTINE_SHA,'pristine binary identity')
|
||||
work=lab/'horizontal';work.mkdir();(work/'clients').mkdir()
|
||||
rows=[]
|
||||
names=['nested-client.c','pointer-daemon.c','static-reference.c','saved-client.c','xdg-shell-client.h','xdg-shell-protocol.c','virtual-pointer-client.h','virtual-pointer-protocol.c']
|
||||
for n in names:
|
||||
target=work/'clients'/n;shutil.copyfile(OLD_CLIENTS/n,target);rows.append(dict(source=str(OLD_CLIENTS/n),path=str(target),sha256=sha(target)))
|
||||
contract=read(BASE/'client-contract.json');require(sha(work/'clients/nested-client.c')==contract['adapted_nested_sha256'],'actual static_cross')
|
||||
for n,h in contract['protocols'].items():require(sha(work/'clients'/n)==h,'actual generated protocol')
|
||||
receipts=[]
|
||||
flags=subprocess.run(['pkg-config','--cflags','--libs','wayland-client'],capture_output=True,text=True,check=True).stdout.split()
|
||||
for name,sources in [('nested-client',['nested-client.c','xdg-shell-protocol.c']),('pointer-daemon',['pointer-daemon.c','virtual-pointer-protocol.c']),('static-reference',['static-reference.c','xdg-shell-protocol.c'])]:
|
||||
argv=['cc','-std=c17','-O2','-Wall','-Wextra',*[str(work/'clients'/s) for s in sources],'-o',str(work/'clients'/name),*flags]
|
||||
p=subprocess.run(argv,env=env(),capture_output=True,text=True,timeout=30)
|
||||
receipts.append(dict(argv=argv,exit=p.returncode,stdout=p.stdout,stderr=p.stderr,binary_sha256=sha(work/'clients'/name) if p.returncode==0 else None))
|
||||
require(p.returncode==0,'C17 client compilation')
|
||||
exclusive(work/'compile.json',dict(status='PASS',transfers=rows,commands=receipts,protocol_provenance=str(BASE/'client-contract.json'),client_execution='NOT RUN'))
|
||||
for case in PLANS:
|
||||
d=work/case;d.mkdir();(d/'results').mkdir()
|
||||
for name in ('nested-client','pointer-daemon','static-reference'):(d/name).symlink_to(work/'clients'/name)
|
||||
(d/'runner.py').write_text(generate(case))
|
||||
trace={str(lab/'mosaic-source/sway/desktop'/n):sha(lab/'mosaic-source/sway/desktop'/n) for n in ('output.c','transaction.c','xdg_shell.c')}
|
||||
scene=Path('/home/fy59/swayfx-mosaic-optimized-edge-a55yjb8n/scene0-build/libscenefx-0.5.so').resolve()
|
||||
exclusive(work/'identity.json',dict(status='PREPARED',lab=str(lab),phase_a_sha256=sha(lab/'phase-a.json'),
|
||||
annotated_binary=a['binary'],pristine=dict(path=str(PRISTINE),sha256=PRISTINE_SHA,scene_path=str(scene),scene_sha256=sha(scene)),
|
||||
wlroots=dict(path=str(WLROOTS),sha256=sha(WLROOTS)),trace_sources=trace,plans=PLANS,review='PENDING',runtime='NOT RUN'))
|
||||
print('HORIZONTAL=PREPARED_CLIENTS_COMPILED_RUNTIME_NOT_RUN')
|
||||
|
||||
def paths(lab):
|
||||
w=lab/'horizontal';ident=read(w/'identity.json')
|
||||
out=[p for p in HERE.iterdir() if p.is_file()]+[BASE/n for n in REUSE]+[BASE/'run.py',BASE/'client-contract.json']
|
||||
out+=[p for p in (w/'clients').iterdir() if p.is_file()]
|
||||
out+=[w/'compile.json',w/'identity.json',w/'offline-tests-repair-v1.json',w/'pristine-provenance.json',lab/'phase-a.json']
|
||||
out+=[w/c/'runner.py' for c in PLANS]+[Path(p) for p in ident['trace_sources']]
|
||||
out+=[Path(ident['annotated_binary']['path']),PRISTINE,WLROOTS,Path(ident['pristine']['scene_path'])]
|
||||
provenance=read(w/'pristine-provenance.json')
|
||||
out+=[Path(provenance['historical_build_receipt']),w/'provenance_check.py']
|
||||
out+=[Path(row['path']) for check in provenance['source_equivalence'] for row in check['files']]
|
||||
out+=[Path(row['source']) for row in read(w/'compile.json')['transfers']]
|
||||
out+=[w/'protocol-scanner.json',w/'offline-tests.json']
|
||||
out+=[Path(row[k]) for row in read(w/'protocol-scanner.json')['commands'] for k in ('protocol_xml','generated')]
|
||||
retained=Path('/home/fy59/swayfx-mosaic-static-nested-runtime-pvn0osxm')
|
||||
out+=[retained/p for p in ('results/standard-snapshot.json','results/standard-offline-observation.json',
|
||||
'run/compositor.log','run/E2.log','run/standard-H1.png','run/standard-H2.png')]
|
||||
out+=[lab/'swayfx-source/sway/ipc-json.c',w/'offline-tests-repair-attempt1.json']
|
||||
out+=[Path(shutil.which(name)).resolve() for name in ('swaymsg','grim')]
|
||||
return sorted(set(out))
|
||||
|
||||
def seal(lab):
|
||||
require(read(lab/'horizontal/offline-tests-repair-v1.json')['status']=='PASS','executed offline tests required')
|
||||
exclusive(lab/'horizontal/freeze.json',dict(status='READY_FOR_REVIEW',files={str(p):sha(p) for p in paths(lab)},plans=PLANS))
|
||||
|
||||
def verify_review(lab,review=True):
|
||||
w=lab/'horizontal';f=read(w/'freeze.json')
|
||||
require(f['status']=='READY_FOR_REVIEW' and f['files']=={str(p):sha(p) for p in paths(lab)} and f['plans']==PLANS,'complete final frozen bytes')
|
||||
ident=read(w/'identity.json');a=read(lab/'phase-a.json')
|
||||
require(a['status']=='PASS' and ident['phase_a_sha256']==sha(lab/'phase-a.json') and ident['annotated_binary']==a['binary'],'exact phase A')
|
||||
require(ident['pristine']['sha256']==PRISTINE_SHA and sha(PRISTINE)==PRISTINE_SHA,'pristine bytes')
|
||||
provenance=read(w/'pristine-provenance.json');require(provenance['status']=='PASS','pristine source equivalence')
|
||||
require(all(sha(row['path'])==row['sha256'] for check in provenance['source_equivalence'] for row in check['files']),'pristine sources actual frozen equality')
|
||||
require(read(w/'offline-tests-repair-v1.json')['status']=='PASS','executed tests')
|
||||
require(read(w/'protocol-scanner.json')['status']=='PASS','scanner provenance')
|
||||
if review:
|
||||
r=read(w/'review.json')
|
||||
require(r.get('status')=='ACCEPTED' and r.get('independent') is True and r.get('read_only') is True and r.get('blockers')==[] and r.get('scope')=='OVERNIGHT_HORIZONTAL_FINAL_BYTES','genuine independent acceptance')
|
||||
require(r.get('freeze_sha256')==sha(w/'freeze.json') and r.get('files')==f['files'],'review final bindings')
|
||||
require(sha(r['report_file'])==r['report_sha256'],'review report bytes')
|
||||
return ident
|
||||
|
||||
def preflight(lab,case,review=True):
|
||||
require(lab==DEFAULT_LAB and case in PLANS and not sys.flags.optimize,'registered lab/case assertions')
|
||||
require(lab.stat().st_uid==os.getuid() and stat.S_IMODE(lab.stat().st_mode)==0o700,'owned lab 0700')
|
||||
ident=verify_review(lab,review)
|
||||
w=lab/'horizontal'/case
|
||||
require(not (w/'run').exists() and not (w/'consumed.json').exists(),'no retry or existing runtime')
|
||||
require(not any(stat.S_ISSOCK(p.lstat().st_mode) for p in (lab/'horizontal').rglob('*')),'no stale sockets')
|
||||
budget=read(lab/'budget.json');require(case not in budget['runs'] and budget['used']['run']<18 and budget['campaign_used']['C']<3,'unspent global run')
|
||||
require(budget['used']['capture']+PLANS[case]['capture']<=220,'available planned PNG budget')
|
||||
# Controls are scientific comparison prerequisites, not invented dispatch vetoes.
|
||||
# Shared-fixture defects must be reviewed before proceeding; no status is transferred.
|
||||
# No compositor/client process exists in this fresh per-case directory.
|
||||
for proc in Path('/proc').iterdir():
|
||||
if proc.name.isdigit():
|
||||
try:exe=(proc/'exe').resolve(strict=True)
|
||||
except (FileNotFoundError,PermissionError,ProcessLookupError):continue
|
||||
require(exe!=Path(ident['annotated_binary']['path']),'annotated binary already running')
|
||||
binary=ident['pristine'] if case=='C0' else ident['annotated_binary']
|
||||
require(sha(binary['path'])==binary['sha256'] and sha(ident['wlroots']['path'])==ident['wlroots']['sha256'],'binary/wlroots preflight')
|
||||
return dict(binary=binary['path'],binary_sha256=binary['sha256'],wlroots_sha256=ident['wlroots']['sha256'],trace_sources=ident['trace_sources'],pristine=ident['pristine'])
|
||||
|
||||
def begin_run():
|
||||
p=argparse.ArgumentParser();p.add_argument('lab',type=Path);p.add_argument('case',choices=PLANS);a=p.parse_args()
|
||||
lab=a.lab.resolve();case=a.case;w=lab/'horizontal'/case
|
||||
auth=preflight(lab,case)
|
||||
exclusive(w/'consumed.json',dict(case=case,freeze_sha256=sha(lab/'horizontal/freeze.json'),at_ns=time.monotonic_ns()))
|
||||
ledger.reserve(lab/'budget.json','run',campaign='C',run_id=case,duration_s=150)
|
||||
exclusive(w/'actions.json',dict(planned=PLANS[case],reserved=dict.fromkeys(PLANS[case],0),events=[]))
|
||||
return lab,case,w,auth
|
||||
|
||||
def debit(lab,case,w,*kinds):
|
||||
with (w/'actions.lock').open('a+') as lock:
|
||||
fcntl.flock(lock,fcntl.LOCK_EX);state=read(w/'actions.json')
|
||||
require(bool(kinds) and all(k in PLANS[case] for k in kinds),'known actions')
|
||||
require(all(state['reserved'][k]+kinds.count(k)<=state['planned'][k] for k in kinds),'local action budget exhausted')
|
||||
for k in kinds:
|
||||
if k=='capture':ledger.reserve(lab/'budget.json','capture',run_id=case)
|
||||
state['reserved'][k]+=1;state['events'].append(dict(kind=k,ns=time.monotonic_ns(),sequence=len(state['events'])))
|
||||
durable(w/'actions.json',state)
|
||||
|
||||
def finish_run(lab,case,w):
|
||||
verify_review(lab);state=read(w/'actions.json');events=[json.loads(s) for s in (w/'run/events.jsonl').read_text().splitlines()]
|
||||
consumed=dict(capture=sum(e['kind']=='capture' for e in events),click=sum(e['kind']=='injection' and e['action']!='move' for e in events),
|
||||
move=sum(e['kind']=='injection' for e in events),grab=sum(e['kind']=='injection' and e.get('grab',False) for e in events))
|
||||
state['consumed_observed']=consumed;durable(w/'actions.json',state)
|
||||
|
||||
if __name__=='__main__':
|
||||
p=argparse.ArgumentParser();p.add_argument('action',choices=['prepare','seal','preflight']);p.add_argument('lab',type=Path);p.add_argument('--case',choices=PLANS,default='C0');a=p.parse_args()
|
||||
if a.action=='prepare':prepare(a.lab)
|
||||
elif a.action=='seal':seal(a.lab)
|
||||
else:print(json.dumps(preflight(a.lab.resolve(),a.case),indent=2))
|
||||
139
tooling/probes/step20c-overnight/horizontal_tests.py
Normal file
139
tooling/probes/step20c-overnight/horizontal_tests.py
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
"""Executed offline negative tests; never mint an independent acceptance."""
|
||||
import argparse
|
||||
import ast
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
from horizontal_adapter import BASE,STANDARD_EXTERIOR,generate,replace_one
|
||||
import horizontal_gate as gate
|
||||
sys.path.insert(1,str(BASE))
|
||||
from model import Rect,Space,Provenance,Invalid,pointer_local,popup,require
|
||||
from predicates import dismissal,reference,protocol
|
||||
from PIL import Image
|
||||
from observations import applied_model,verify_owner_occlusion
|
||||
from ipc_predicates import nodes,workspace,measure_owner
|
||||
from model import content
|
||||
from dataclasses import replace
|
||||
import copy
|
||||
|
||||
class Offline(unittest.TestCase):
|
||||
def test_generated_all_compile(self):
|
||||
for c in gate.PLANS:
|
||||
code=generate(c);compile(code,c,'exec')
|
||||
self.assertIn('begin_run()',code);self.assertNotIn("reserve(lab,'run')",code)
|
||||
self.assertLess(code.index('begin_run()'),code.index('tempfile.mkdtemp'))
|
||||
self.assertIn("['swaymsg','-s',str(sock)",code)
|
||||
def test_repository_import_closure_matches_frozen_paths(self):
|
||||
frozen = set(gate.paths(gate.DEFAULT_LAB))
|
||||
roots = (gate.HERE, BASE)
|
||||
pending = [ast.parse(generate(c)) for c in gate.PLANS]
|
||||
seen = set()
|
||||
while pending:
|
||||
tree = pending.pop()
|
||||
for node in ast.walk(tree):
|
||||
names = [a.name for a in node.names] if isinstance(node, ast.Import) else ([node.module] if isinstance(node, ast.ImportFrom) and node.module else [])
|
||||
for name in names:
|
||||
matches = [root / (name.split('.')[0] + '.py') for root in roots]
|
||||
local = next((p for p in matches if p.is_file()), None)
|
||||
if local is None:
|
||||
continue
|
||||
self.assertIn(local, frozen, 'executed repository import absent: ' + name)
|
||||
if local not in seen:
|
||||
seen.add(local);pending.append(ast.parse(local.read_text()))
|
||||
self.assertNotIn(BASE / 'authorization.py', seen)
|
||||
|
||||
def retained_standard(self):
|
||||
old = Path('/home/fy59/swayfx-mosaic-static-nested-runtime-pvn0osxm')
|
||||
snap = json.loads((old/'results/standard-snapshot.json').read_text())
|
||||
ws = workspace(snap['tree'], '1')
|
||||
owner = next(n for n in nodes(ws) if n['id'] == 10)
|
||||
env = {'require': require};exec(STANDARD_EXTERIOR, env)
|
||||
scene = env['splith_exterior'](owner, ws['layout'])
|
||||
measured = measure_owner((old/'run/compositor.log').read_text(), owner)
|
||||
self.assertEqual(scene, measured['scene'])
|
||||
measured['workspace'] = ws['id']
|
||||
applied = applied_model(measured, owner, (old/'run/E2.log').read_text(), 'repair-retained-offline')
|
||||
return old, owner, applied, env['splith_exterior']
|
||||
|
||||
def test_retained_standard_exterior_content_and_actual_png(self):
|
||||
old, owner, applied, inverse = self.retained_standard()
|
||||
self.assertEqual(owner['rect'], dict(x=-400,y=25,width=400,height=575))
|
||||
self.assertEqual(inverse(owner,'splith'), [-400,0,400,600])
|
||||
rect = content(applied);self.assertEqual(rect.values(), [-398,25,396,573])
|
||||
obs = json.loads((old/'results/standard-offline-observation.json').read_text())
|
||||
verify_owner_occlusion(Image.open(old/'run/standard-H1.png').convert('RGB'),
|
||||
Image.open(old/'run/standard-H2.png').convert('RGB'), rect, obs['geometry'], False)
|
||||
|
||||
def test_retained_standard_invalid_decoration_model_destination(self):
|
||||
old, owner, applied, inverse = self.retained_standard()
|
||||
with self.assertRaises(Invalid):inverse(owner,'stacked')
|
||||
altered=copy.deepcopy(owner);altered['deco_rect']['height']=-1
|
||||
with self.assertRaises(Invalid):inverse(altered,'splith')
|
||||
for bad in (replace(applied, titlebar=26),
|
||||
replace(applied, scene=replace(applied.scene,y=25,height=575)),
|
||||
replace(applied, window=replace(applied.window,height=572))):
|
||||
with self.assertRaises(Invalid):content(bad)
|
||||
altered=copy.deepcopy(owner);altered['deco_rect']['height']=26
|
||||
with self.assertRaises(Invalid):
|
||||
require(inverse(altered,'splith') == applied.scene.values(), 'standard-applied-scene-matches-assignment')
|
||||
|
||||
def test_unique_transform_rejects(self):
|
||||
for s in ('missing','x x'):
|
||||
with self.assertRaises(ValueError):replace_one(s,'x','y')
|
||||
def test_standard_has_no_camera_or_mosaic_command(self):
|
||||
for c in ('C0','C1'):
|
||||
code=generate(c);self.assertNotIn('open_chain(',code);self.assertNotIn('layout mosaic scroll',code)
|
||||
self.assertNotIn('layout mosaic\')',code)
|
||||
self.assertNotIn('validate_applied_standard_transition(request',code)
|
||||
def test_C2_contract_actions(self):
|
||||
code=generate('C2');self.assertIn("open_chain('E1')",code);self.assertIn('fresh_chain(',code)
|
||||
self.assertIn('dismissal(log',code);self.assertIn('validate_applied_standard_transition(request',code)
|
||||
self.assertIn("layout splith",code);self.assertIn('final_restoration(baseline,final)',code)
|
||||
def test_half_open_and_wrong_pointer_reject(self):
|
||||
p=Provenance(1,2,'window','actual');r=Rect(Space.GLOBAL,-200,20,300,130,p)
|
||||
self.assertTrue(r.contains((99,149)));self.assertFalse(r.contains((100,149)))
|
||||
for xy,local in [((100,20),(300,0)),((0,20),(0,0))]:
|
||||
with self.assertRaises(Invalid):pointer_local(xy,r,local)
|
||||
def test_parent_identity_and_ack_reject(self):
|
||||
p=Provenance(1,2,'window','actual');r=Rect(Space.GLOBAL,-200,20,300,130,p);c=Rect(Space.POPUP,10,10,30,30,p)
|
||||
for parent,ack,mapped in [(22,3,True),(11,0,True),(11,3,False)]:
|
||||
with self.assertRaises(Invalid):popup(r,c,parent,11,ack,mapped)
|
||||
def test_reference_unmasked_single_pixel_reject(self):
|
||||
a=Image.new('RGB',(800,600),(48,48,56));b=a.copy();b.putpixel((799,599),(48,48,57))
|
||||
with self.assertRaises(AssertionError):reference(a,b)
|
||||
def test_forbidden_button_rejects(self):
|
||||
with self.assertRaises(RuntimeError):dismissal('POPUP_B POPUP_DONE \nPOPUP_A POPUP_DONE \nPOPUP_B DESTROY \nPOPUP_A DESTROY \n',['POINTER_BUTTON target=POPUP_B serial=2 button=273 state=1'])
|
||||
def test_dismissal_missing_wrong_order_reject(self):
|
||||
for text in ('','POPUP_A POPUP_DONE \nPOPUP_B POPUP_DONE \nPOPUP_B DESTROY \nPOPUP_A DESTROY \n'):
|
||||
with self.assertRaises(AssertionError):dismissal(text,[])
|
||||
def test_protocol_missing_real_wire_reject(self):
|
||||
text=Path('/home/fy59/swayfx-mosaic-e2-completion-dk1hq9an/run/E2.log').read_text()
|
||||
# Retained full log ends with explicit destruction; cut before actual close.
|
||||
text=text[:text.index('POPUP_B DESTROY ')];protocol(text)
|
||||
with self.assertRaises((AssertionError,Invalid)):protocol(text.replace('.grab(wl_seat#','.fake_grab(wl_seat#'))
|
||||
def test_review_pending_rejected_before_effect(self):
|
||||
with patch.object(gate,'verify_review',side_effect=ValueError('review pending')),patch.object(gate.ledger,'reserve') as debit:
|
||||
with self.assertRaises(ValueError):gate.preflight(gate.DEFAULT_LAB,'C0')
|
||||
debit.assert_not_called()
|
||||
def test_local_budget_reject_no_global_capture(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
w=Path(d);gate.exclusive(w/'actions.json',dict(planned=gate.PLANS['C0'],reserved={**dict.fromkeys(gate.PLANS['C0'],0),'capture':10},events=[]))
|
||||
with patch.object(gate.ledger,'reserve') as debit:
|
||||
with self.assertRaises(ValueError):gate.debit(w,'C0',w,'capture')
|
||||
debit.assert_not_called()
|
||||
def test_unknown_action_rejected(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
w=Path(d);gate.exclusive(w/'actions.json',dict(planned=gate.PLANS['C0'],reserved=dict.fromkeys(gate.PLANS['C0'],0),events=[]))
|
||||
with self.assertRaises(ValueError):gate.debit(w,'C0',w,'retry')
|
||||
|
||||
if __name__=='__main__':
|
||||
p=argparse.ArgumentParser();p.add_argument('--output',type=Path,required=True);a=p.parse_args()
|
||||
suite=unittest.defaultTestLoader.loadTestsFromTestCase(Offline);names=[t.id() for t in suite]
|
||||
result=unittest.TextTestRunner(verbosity=2).run(suite)
|
||||
gate.exclusive(a.output,dict(status='PASS' if result.wasSuccessful() else 'FAIL',executed=result.testsRun,
|
||||
tests=names,failures=[(str(t),s) for t,s in result.failures],errors=[(str(t),s) for t,s in result.errors],
|
||||
runtime='NOT RUN',review_acceptance_created=False))
|
||||
raise SystemExit(not result.wasSuccessful())
|
||||
122
tooling/probes/step20c-overnight/ledger.py
Normal file
122
tooling/probes/step20c-overnight/ledger.py
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
"""EXPERIMENTAL persistent overnight budget.
|
||||
WHY: failures consume the same budget as successes.
|
||||
CONTRACT: reserve before action; no refunds or reset operation.
|
||||
INVARIANT: exclusive flock covers validation and durable atomic replacement.
|
||||
"""
|
||||
import fcntl
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
LIMITS = {'scene_build': 2, 'mosaic_build': 2, 'stage': 2, 'run': 18, 'capture': 220}
|
||||
CAMPAIGNS = {'C': 3, 'D1': 2, 'D2': 2, 'D3': 2, 'D4': 2, 'E1': 1, 'E2': 1, 'E3': 2, 'F': 2, 'G': 1}
|
||||
|
||||
def _sync_dir(parent):
|
||||
fd = os.open(parent, os.O_RDONLY | os.O_DIRECTORY)
|
||||
try:
|
||||
os.fsync(fd)
|
||||
finally:
|
||||
os.close(fd)
|
||||
|
||||
def initialize(path):
|
||||
path = Path(path)
|
||||
with path.with_suffix('.lock').open('a') as lock:
|
||||
fcntl.flock(lock, fcntl.LOCK_EX)
|
||||
state = {'limits': LIMITS, 'campaign_limits': CAMPAIGNS,
|
||||
'used': dict.fromkeys(LIMITS, 0), 'campaign_used': dict.fromkeys(CAMPAIGNS, 0),
|
||||
'runs': {}, 'events': []}
|
||||
# O_EXCL makes reinitialization an error, never an implicit reset.
|
||||
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
||||
with os.fdopen(fd, 'w') as out:
|
||||
json.dump(state, out, indent=2); out.write('\n'); out.flush(); os.fsync(out.fileno())
|
||||
_sync_dir(path.parent)
|
||||
return state
|
||||
|
||||
def _validate(state):
|
||||
"""WHY: corrupted persisted counters must fail closed before any action."""
|
||||
if not isinstance(state, dict) or state.get('limits') != LIMITS or state.get('campaign_limits') != CAMPAIGNS:
|
||||
raise RuntimeError('budget contract changed or corrupt')
|
||||
for key, limits in [('used', LIMITS), ('campaign_used', CAMPAIGNS)]:
|
||||
values = state.get(key)
|
||||
if not isinstance(values, dict) or set(values) != set(limits):
|
||||
raise RuntimeError('corrupt counter keys')
|
||||
if any(type(values[k]) is not int or not 0 <= values[k] <= limits[k] for k in limits):
|
||||
raise RuntimeError('corrupt counter values')
|
||||
events, runs = state.get('events'), state.get('runs')
|
||||
if not isinstance(events, list) or not isinstance(runs, dict):
|
||||
raise RuntimeError('corrupt event/run data')
|
||||
counted = dict.fromkeys(LIMITS, 0)
|
||||
campaigns = dict.fromkeys(CAMPAIGNS, 0)
|
||||
replay = {}
|
||||
for i, event in enumerate(events):
|
||||
if not isinstance(event, dict) or type(event.get('sequence')) is not int or event['sequence'] != i:
|
||||
raise RuntimeError('corrupt event sequence')
|
||||
kind, amount = event.get('kind'), event.get('amount')
|
||||
if kind not in LIMITS or type(amount) is not int or amount <= 0 or type(event.get('reserved_at_ns')) is not int or event['reserved_at_ns'] <= 0:
|
||||
raise RuntimeError('corrupt reservation event')
|
||||
counted[kind] += amount
|
||||
campaign, run_id = event.get('campaign'), event.get('run_id')
|
||||
if kind == 'run':
|
||||
if amount != 1 or campaign not in CAMPAIGNS or not isinstance(run_id, str) or not run_id or run_id in replay:
|
||||
raise RuntimeError('corrupt run event')
|
||||
replay[run_id] = {'campaign': campaign, 'captures': 0}
|
||||
campaigns[campaign] += 1
|
||||
elif kind == 'capture':
|
||||
if run_id not in replay or campaign is not None:
|
||||
raise RuntimeError('corrupt capture event')
|
||||
replay[run_id]['captures'] += amount
|
||||
elif campaign is not None or run_id is not None:
|
||||
raise RuntimeError('unexpected build event association')
|
||||
if counted != state['used'] or campaigns != state['campaign_used'] or set(replay) != set(runs):
|
||||
raise RuntimeError('events do not reconcile counters')
|
||||
for run_id, expected in replay.items():
|
||||
run = runs[run_id]
|
||||
if not isinstance(run, dict) or set(run) != {'campaign', 'captures', 'duration_s'} or run.get('campaign') != expected['campaign'] or type(run.get('captures')) is not int or run['captures'] != expected['captures']:
|
||||
raise RuntimeError('corrupt run record')
|
||||
if not 0 <= run['captures'] <= (12 if run['campaign'] == 'F' else 20) or type(run.get('duration_s')) not in (int, float) or not 0 < run['duration_s'] <= 150:
|
||||
raise RuntimeError('corrupt run bounds')
|
||||
|
||||
def reserve(path, kind, *, campaign=None, run_id=None, amount=1, duration_s=150):
|
||||
path = Path(path)
|
||||
if kind not in LIMITS or type(amount) is not int or amount <= 0:
|
||||
raise ValueError('invalid reservation')
|
||||
with path.with_suffix('.lock').open('a') as lock:
|
||||
fcntl.flock(lock, fcntl.LOCK_EX)
|
||||
state = json.loads(path.read_text())
|
||||
_validate(state)
|
||||
if state['used'][kind] + amount > LIMITS[kind]:
|
||||
raise RuntimeError('global budget exhausted: ' + kind)
|
||||
if kind == 'run':
|
||||
if amount != 1 or campaign not in CAMPAIGNS or not isinstance(run_id, str) or not run_id or run_id in state['runs']:
|
||||
raise ValueError('unique run and known campaign required')
|
||||
if type(duration_s) not in (int, float) or not 0 < duration_s <= 150:
|
||||
raise ValueError('run duration exceeds 150 seconds')
|
||||
if state['campaign_used'][campaign] >= CAMPAIGNS[campaign]:
|
||||
raise RuntimeError('campaign budget exhausted')
|
||||
state['campaign_used'][campaign] += 1
|
||||
state['runs'][run_id] = {'campaign': campaign, 'captures': 0, 'duration_s': duration_s}
|
||||
elif kind == 'capture':
|
||||
if campaign is not None or run_id not in state['runs']:
|
||||
raise ValueError('capture requires reserved run')
|
||||
run = state['runs'][run_id]
|
||||
if run['captures'] + amount > (12 if run['campaign'] == 'F' else 20):
|
||||
raise RuntimeError('run capture budget exhausted')
|
||||
run['captures'] += amount
|
||||
elif campaign is not None or run_id is not None:
|
||||
raise ValueError('build cannot associate run')
|
||||
state['used'][kind] += amount
|
||||
event = {'sequence': len(state['events']), 'kind': kind, 'amount': amount,
|
||||
'campaign': campaign, 'run_id': run_id, 'reserved_at_ns': time.time_ns()}
|
||||
state['events'].append(event)
|
||||
fd, temporary = tempfile.mkstemp(prefix='.budget-', dir=path.parent)
|
||||
try:
|
||||
with os.fdopen(fd, 'w') as out:
|
||||
json.dump(state, out, indent=2); out.write('\n'); out.flush(); os.fsync(out.fileno())
|
||||
os.replace(temporary, path)
|
||||
_sync_dir(path.parent)
|
||||
finally:
|
||||
if os.path.exists(temporary):
|
||||
os.unlink(temporary)
|
||||
return event
|
||||
173
tooling/probes/step20c-overnight/matrix/README.md
Normal file
173
tooling/probes/step20c-overnight/matrix/README.md
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
# Prospective v3: D3-mosaic only
|
||||
|
||||
**EXPERIMENTAL / Runtime NOT RUN; independent review PENDING.**
|
||||
This revision is usable only for the unused `D3-mosaic` case. Every other case
|
||||
is refused by the entry gate. Consumed v2 results, tokens and receipts are
|
||||
unchanged. D2-mosaic remains INCONCLUSIVE and cannot be retried.
|
||||
|
||||
Before editing, all 578 files of accepted v2, its freeze/review and all matrix
|
||||
modules were copied with exact SHA256 verification into
|
||||
`LAB/matrix/executed-v2-archive/`. `archive-map.json` explicitly binds original
|
||||
paths to archival paths. The original freeze hash remains
|
||||
`a20ccb39095ca35363119b5cfe4b5c839c1a75863bd72a097686aeac76dd7d7f`.
|
||||
Original source paths now contain this prospective revision; verification of
|
||||
executed v2 uses the archival bytes. The v3 freeze binds and verifies that
|
||||
complete archive, current import closure, binaries and clients, reports and
|
||||
executed offline receipt. Its separate scope is
|
||||
`OVERNIGHT_MATRIX_D3_PROSPECTIVE_V3`; it grants no author acceptance.
|
||||
|
||||
The actual pinned layer-shell constructor creates a descriptor popup host for
|
||||
wallpaper even without an XDG popup. Before launching the owner, D3 records the
|
||||
one live marker process, exactly one wallpaper layer creation, no IPC popup
|
||||
owner, and at least two complete callbacks containing the same pre-existing
|
||||
host. All baseline callbacks are retained. Opening then requires exactly this
|
||||
unchanged known host and one new stable root; unknown or additional hosts,
|
||||
missing known host, reordered indexes, missing owner/workspace, wrong geometry
|
||||
and every erroneous comparable root clip are refused. Raw callbacks retain
|
||||
both hosts and their original indexes. Identity selection uses only membership
|
||||
in the pre-owner set, never the latest box or clipped index. The existing live
|
||||
XDG parent/grab hierarchy and exact IPC ancestry bind the new root to the owner.
|
||||
|
||||
Constructor + timing + cardinality give bounded association. Native layer/root
|
||||
owner IDs, host child count and atomic transaction generation are not observed.
|
||||
The D2 replay tests validate only this fixture algorithm and never upgrade D2's
|
||||
result. There is no compositor, client, package or production change.
|
||||
|
||||
The v2 preparation description below is historical context. Its general
|
||||
fourteen-case dispatch is superseded by the single prospective v3 gate.
|
||||
|
||||
# Overnight output/lifetime/effects/ordering matrix
|
||||
|
||||
**EXPERIMENTAL / PREPARATION. Runtime NOT RUN; independent review PENDING.**
|
||||
This fixture changes no compositor, UX, package, cumulative patch, desktop or
|
||||
historical LAB. It recompiles only private C17 clients, preserving every source,
|
||||
protocol XML/scanner input and output, command and resulting binary hash.
|
||||
STEP20C remains NEXT; production multi-output NOT YET SUPPORTED.
|
||||
|
||||
`protocols.py` defines fourteen unique cases. The private `protocols.json` is
|
||||
compared with that exact mapping by the gate. Every case specifies the scientific
|
||||
question, output physical/logical geometry, preconditions, client placement,
|
||||
ordered actions, point computation, ceilings, stop criteria and raw retention.
|
||||
`engine.py` provides one small shared Session; `cases.py` supplies bounded
|
||||
scenarios. No massive historical runner is copied. Existing model/protocol
|
||||
functions are imported with their exact dependency hashes in a separate matrix
|
||||
freeze. C's reviewer cannot grant matrix acceptance.
|
||||
|
||||
| Cases | Actual planned captures per run | Scientific ceiling |
|
||||
| --- | ---: | --- |
|
||||
| D1 pristine/Mosaic | 8 | sampled vertical true A/B grabs/crossing/input/dismissal |
|
||||
| D2 pristine/Mosaic | 9 | sampled scale1.25 geometry after actual asymmetric markers |
|
||||
| D3 pristine/Mosaic | 9 | sampled R90 scale1.25 geometry after actual asymmetric markers |
|
||||
| D4 pristine/Mosaic | 17 | resize/clamp/migration sampled geometry; no atomic generation |
|
||||
| E1 Mosaic | 12 | sampled fully hidden owner and same live chain return |
|
||||
| E2 Mosaic | 8 | always INCONCLUSIVE for saved identity/render: native traces absent |
|
||||
| E3 pristine/Mosaic | 18 | sampled effects/cache RGB, never complete damage/cache equivalence |
|
||||
| F pristine/Mosaic | 8 | INCONCLUSIVE for native buffer identity/atomic input epoch |
|
||||
|
||||
Matrix158 + C37 + optional G17 =212/220 captures. Matrix14 + C3 + optional G1
|
||||
=18/18 runs. These are fixed planned allocations below the ticket ceilings;
|
||||
failures consume reservations and are never refunded. Each case independently
|
||||
checks remaining global budget. Failure in another independent case does not
|
||||
invent a dispatch veto, transfer a PASS or authorize a retry.
|
||||
|
||||
D1 uses real vertical outputs(0,0)/(0,600), native575-height vertical client and
|
||||
actual y600 root/child crossing. D2 uses logical H1(-800,0,800,600) physical
|
||||
1000x750 scale1.25, H2 normal. D3 uses the same logical boxes and H1 physical
|
||||
750x1000 transform string90. Five distinct marker colors, positions and actual
|
||||
PNG dimensions establish the frame before any coordinate conversion. The
|
||||
marker source PNG is generated client content, recorded separately from actual
|
||||
captures. Every injection uses the observed logical output union, never applies
|
||||
scale twice, and recomputes a solid measured interior and fresh wire/local
|
||||
coordinates. Rectangles remain half open.
|
||||
|
||||
The pristine standard origin uses IPC rect+window offset: exported rect.y already
|
||||
includes decoration. Its exterior is the source-backed inverse(rect.y-deco,
|
||||
rect.height+deco). Candidate applied origins come from PI_OWNER exterior scene
|
||||
plus actual decoration/window offsets. Assignment, applied scene, native SHM,
|
||||
configure, output logical, physical mode and PNG frame are distinct observations.
|
||||
Root association requires the unique live XDG hierarchy and exact owner ancestry;
|
||||
PI_POPUP has no owner field. No private generation is invented.
|
||||
|
||||
D4 opens clients before camera calibration, records startup blank H1, resizes H1
|
||||
to960 atx-960 then back800 atx-800, and migrates workspace1 H1→H2→H1 once.
|
||||
The Mosaic request derives maximum from frozen minimum640 policy, known gaps0
|
||||
and exact row child count; it requests maximum+960 once and checks actual clamp.
|
||||
Pristine clamp is NOT EXERCISED. Captures preserve all migration states, logical
|
||||
order/IDs and legitimate popup dismissal. Input is never sent to an old invisible
|
||||
surface. Closing owner/fillers restores blank H1 and exact full H2.
|
||||
|
||||
E1 preopens two prefix and two trailing fillers around the owner, then calibrates
|
||||
once, opens two genuine grabs, clicks visible B, computes a hide camera from the
|
||||
applied owner's endpoint, requires zero owner intersection and empty root clip,
|
||||
checks no popup pixels on either output and no popup enter, returns by the exact
|
||||
inverse, requires unchanged object IDs/no popup_done/recreation, clicks B again
|
||||
and closes normally. The optional second invisible click is omitted.
|
||||
|
||||
E2 keeps the compiled default200ms timeout. It receives a genuine configure,
|
||||
records HELD serial and independent wire configure with no ACK, captures only
|
||||
what the real scheduling permits, and waits for the natural transaction timeout.
|
||||
A capture named held may already follow timeout; the receipt explicitly says
|
||||
NOT EXERCISED for native saved content. The responsive client continues dispatch;
|
||||
natural ping responses are retained if present. No forced ACK, timer override,
|
||||
code-level save or invented saved-buffer node is used. H2 sampled equality cannot
|
||||
upgrade native saved identity or rendering from INCONCLUSIVE.
|
||||
|
||||
E3 uses the retained duplicate-configure-safe checker layer and translucent XDG
|
||||
client. Shadow/no-shadow comparisons precede a classic floating glass wholly
|
||||
inside H1 (source selects classic for floating). This samples retained ordinary floating
|
||||
classic filtering, not Mosaic-tiled classic clipping. That same client is then retiled
|
||||
and recalibrated once for settled tiled optimized blur. Explicit background
|
||||
A/B/A commands drive real cache reconstruction. Every state has whole H2 capture.
|
||||
An absent visible shadow or classic ROI is recorded NOT EXERCISED and does not
|
||||
abort independent optimized measurements. Pristine lacks OBC trace. Native cache
|
||||
observations, final drawing and input remain distinct; historical maxRGB6 is no
|
||||
new threshold. No shader/cache/readback edit or compositor rebuild is used.
|
||||
|
||||
F uses a nongrab toplevel with real wl_surface input-region commits. Full/empty/
|
||||
full right-button recipients, focus, gaps geometry, candidate camera forward/
|
||||
inverse and standard/Mosaic return are sampled within bounds. Native pairs are
|
||||
selected by exact(output,commit_seq), at most12, with missing presents explicit.
|
||||
Independent client wire input, PI scene/owner/root, TEMPORAL_DAMAGE and commit/
|
||||
present remain separate. Native buffer references/releases and hit epochs are
|
||||
absent from the candidate. No scheduler manipulation, interval-seeking flood,
|
||||
forced timing or GPU readback is used. Unobserved natural interleaving remains
|
||||
NOT EXERCISED; buffer identity/atomic generation remain NOT PROVEN.
|
||||
|
||||
Global run reservation precedes runtime/socket/process creation; local durable
|
||||
reservations precede every camera, chain, grab, click/move, configuration,
|
||||
mutation and capture. Typed local counters reconcile exact event sequences and
|
||||
immutable limits. A unique O_EXCL token makes each failed attempt unretryable.
|
||||
Private runtime0700 and cleared inherited desktop/loader environment are required;
|
||||
every swaymsg has explicit -s. Actual renderer startup lines and proc maps bind
|
||||
candidate/pristine, wlroots and static/private SceneFX. Active watchdog143s,
|
||||
run150s, per-log32MiB and shared cleanup deadlines bound all owned process groups.
|
||||
Actual terminal IPC codes, process exit codes, rescue and runtime removal survive
|
||||
failures. Cleanup failure downgrades the outcome and keeps measurement status.
|
||||
|
||||
## Separate independent review
|
||||
|
||||
Preparation entry: `PYTHONDONTWRITEBYTECODE=1 python matrix/prepare.py`.
|
||||
This compiles clients only and is O_EXCL for its new private directory. Two
|
||||
failed offline preparations on absent historical close_a context are retained
|
||||
under matrix-client-preparation-failure01/02; the final region client reuses only
|
||||
the exact command branch, with unique-line matching. No graphical attempt followed.
|
||||
Initial offline tests23PASS/1FAIL asserted205 instead of actual203 planned PNG;
|
||||
G was subsequently assigned17 captures, so final whole-batch planning is212.
|
||||
that failed receipt stays preserved. Final negative tests and AST checks are
|
||||
actual execution, not an acceptance generator or proof of graphics.
|
||||
|
||||
`gate.freeze()` writes LAB/matrix/freeze.json with every final source, reused
|
||||
module, client source/protocol/binary, source audit, exact binary/library, pristine
|
||||
provenance and executed receipt. No author-generated acceptance exists.
|
||||
An independent read-only reviewer writes LAB/matrix/review.json with:
|
||||
status ACCEPTED, independent=true, read_only=true, blockers=[],
|
||||
scope OVERNIGHT_MATRIX_FINAL_BYTES, freeze_sha256=SHA256(freeze.json),
|
||||
files=the exact frozen files mapping, report_file=absolute report,
|
||||
report_sha256=its exact SHA256. Changed bytes invalidate acceptance.
|
||||
|
||||
Only after acceptance: `PYTHONDONTWRITEBYTECODE=1 python matrix/run.py D1-pristine`
|
||||
(or another exact case key). The gate fails before any token/run reservation if
|
||||
review, source/client/binary identity, ownership, socket/process state or budget
|
||||
is invalid. The author performs no run and grants no acceptance. Independent
|
||||
review may refuse technically incomplete predicates before any case consumes
|
||||
its budget; raw results never upgrade unsupported scientific claims.
|
||||
9
tooling/probes/step20c-overnight/matrix/analyze.py
Normal file
9
tooling/probes/step20c-overnight/matrix/analyze.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
"""Offline per-case bounded evidence summary; never invent missing native identity."""
|
||||
import argparse,json
|
||||
from pathlib import Path
|
||||
from gate import LAB
|
||||
from protocols import RUNS
|
||||
p=argparse.ArgumentParser();p.add_argument('run_id',choices=RUNS);a=p.parse_args();w=LAB/'matrix'/a.run_id/'run'
|
||||
raw=json.loads((w/'outcome.json').read_text());spec=RUNS[a.run_id]
|
||||
print(json.dumps(dict(case=spec['id'],runtime=raw,status=raw['status'],source_observability_ceilings=spec.get('ceilings',[]),
|
||||
complete_damage='NOT PROVEN',atomic_generation='NOT PROVEN',production='NOT YET SUPPORTED',historical_acceptance_transferred=False),indent=2))
|
||||
266
tooling/probes/step20c-overnight/matrix/cases.py
Normal file
266
tooling/probes/step20c-overnight/matrix/cases.py
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
"""Small campaign scenarios over the same reviewed Session; no historical runner copy."""
|
||||
import re,time
|
||||
from PIL import ImageChops
|
||||
from engine import *
|
||||
from geometry import *
|
||||
|
||||
def init_reference(s,prefix_fillers=1):
|
||||
s.cmd('workspace 2');s.launch_client('reference','static-reference');rid=s.find('reference')['id'];s.cmd(f'[con_id={rid}] border none')
|
||||
s.cmd('workspace 1')
|
||||
for i in range(prefix_fillers):s.launch_client('filler'+str(i),'static-reference')
|
||||
s.cmd('layout mosaic' if s.spec['mode']=='mosaic' else 'layout splith')
|
||||
return rid
|
||||
|
||||
def stable_reference(s):
|
||||
s.inject('move',[s.outputs[0]['logical'][0]+10,s.outputs[0]['logical'][1]+590]);time.sleep(.15)
|
||||
a=s.capture('H2-reference1','HEADLESS-2');time.sleep(.6);b=s.capture('H2-reference2','HEADLESS-2');exact(a,b)
|
||||
need(all(v==(48,48,56) for v in a.get_flattened_data()),'static uniform H2');s.save('reference-stability',dict(status='PASS',exact=exact(a,b)));return a
|
||||
|
||||
def marker_host_baseline(s):
|
||||
"""D3-only pre-XDG association: source + creation timing + cardinality.
|
||||
|
||||
This does not observe native owner IDs or host child count. No root is
|
||||
selected by clip/index after the owner exists.
|
||||
"""
|
||||
from transition import observed_epochs
|
||||
need(s.spec['id']=='D3-mosaic' and 'owner' not in s.proc,'prospective pre-owner marker baseline')
|
||||
marker=s.proc.get('marker-background');need(marker is not None and marker.poll() is None,'live marker layer client')
|
||||
start=next(e['ns'] for e in s.events if e['kind']=='spawn' and e['name']=='marker-background')
|
||||
end=time.monotonic_ns();trace=s.log('compositor')
|
||||
creations=[l for l in trace.splitlines() if 'new layer surface: namespace ' in l]
|
||||
need(len(creations)==1 and 'namespace wallpaper layer 0 ' in creations[0],'one wallpaper layer creation')
|
||||
snapshot=s.snapshot('pre-XDG-marker-host')
|
||||
need(not any(n.get('app_id')=='org.mosaic.NestedGrabProbe' for n in nodes(snapshot['tree'])),'no popup owner before baseline')
|
||||
epochs=observed_epochs(trace,'HEADLESS-1',start,end);tree=None;observed=[]
|
||||
for e in epochs:
|
||||
need(e['scene'] is not None,'complete baseline callback')
|
||||
roots=e['roots']
|
||||
if not roots:
|
||||
need(tree is None,'marker host disappeared before owner');continue
|
||||
need(len(roots)==1,'unique pre-XDG host')
|
||||
r=roots[0]
|
||||
need(r.get('local_index')=='1' and r.get('enabled')=='1' and r.get('clip_valid')=='0' and r.get('clip')=='0,0,0,0','source-backed ordinary layer host')
|
||||
if tree is None:tree=r['tree']
|
||||
need(r['tree']==tree,'stable pre-XDG host')
|
||||
observed.append(e)
|
||||
need(len(observed)>=2,'two complete pre-XDG host observations')
|
||||
baseline=dict(tree=tree,window=[start,end],epochs=epochs,layer_creation=creations[0],marker_pid=marker.pid,snapshot=snapshot,
|
||||
correlation='single layer constructor + pre-owner timing + stable cardinality; no native layer ID or child count')
|
||||
s.save('marker-host-baseline',baseline);s.marker_host=baseline
|
||||
return baseline
|
||||
|
||||
def associated_root(roots,baseline=None,commit_ns=None):
|
||||
if baseline is None:
|
||||
need(len(roots)==1 and roots[0].get('local_index')=='1','unique first popup root');return roots[0]
|
||||
need(commit_ns is not None and commit_ns>baseline['window'][1],'root callback after pre-XDG baseline')
|
||||
need(len(roots)==2 and len({r.get('tree') for r in roots})==2,'one known layer host plus one new XDG root')
|
||||
need([r.get('local_index') for r in roots]==['1','2'],'native host order retained')
|
||||
known=[r for r in roots if r.get('tree')==baseline['tree']]
|
||||
need(len(known)==1 and known[0].get('enabled')=='1' and known[0].get('clip_valid')=='0' and known[0].get('clip')=='0,0,0,0','same unchanged known layer host')
|
||||
return next(r for r in roots if r['tree']!=baseline['tree'])
|
||||
|
||||
def root_state(s,state,origin,window_start,expect_hidden=False,previous=None,expected_tree=None):
|
||||
from transition import observed_epochs
|
||||
from ipc_predicates import ancestry
|
||||
end=time.monotonic_ns();output=state['workspace']['output']
|
||||
out=next(o['logical'] for o in s.outputs if o['name']==output)
|
||||
need(state['workspace']['layout']=='mosaic' and origin==state['origin'],'current Mosaic origin')
|
||||
path=ancestry(state['snapshot']['tree'],state['owner']['id'])
|
||||
need(path and path[-1]['id']==state['owner']['id'],'exact IPC owner ancestry')
|
||||
need([n['id'] for n in path if n.get('type')=='workspace']==[state['workspace']['id']] and
|
||||
[n['name'] for n in path if n.get('type')=='output']==[output],'exact workspace output ancestry')
|
||||
live=[n for n in nodes(state['snapshot']['tree']) if n.get('app_id')==state['owner']['app_id'] and n.get('visible')]
|
||||
need(len(live)==1 and live[0]['id']==state['owner']['id'],'unique live XDG owner')
|
||||
prot=protocol(s.log('owner'));need(len(prot['grabs'])==2,'two real grabs')
|
||||
epochs=observed_epochs(s.log('compositor'),output,window_start,end)
|
||||
rows=[];trees=set();applied=False
|
||||
for e in epochs:
|
||||
c,sc,roots=e['commit'],e['scene'],e['roots']
|
||||
need(c.get('has_buffer') in ('0','1') and sc is not None and sc.get('workspace')==str(state['workspace']['id']) and sc.get('layout')=='5','complete current callback workspace/layout')
|
||||
owners=[o for o in e['owners'] if o.get('id')==str(state['owner']['id'])]
|
||||
need(len(owners)==1 and owners[0].get('scene_valid')=='1','exact applied owner')
|
||||
r=associated_root(roots,getattr(s,'marker_host',None),int(c['ns']))
|
||||
o=owners[0];trees.add(r['tree'])
|
||||
need(len(trees)==1 and (expected_tree is None or r['tree']==expected_tree),'same live root')
|
||||
need(int(c['ns'])<int(sc['ns'])<int(o['ns'])<int(r['ns']),'callback record order')
|
||||
scene=list(map(int,o['scene'].split(',')))+list(map(int,o['applied_size'].split(',')))
|
||||
target=scene==state['scene']
|
||||
old=previous is not None and scene==previous['scene']
|
||||
need(target or (old and not applied),'unexpected or stale applied owner geometry')
|
||||
chosen=state if target else previous
|
||||
need(o['current']==','.join(str(chosen['owner']['rect'][k] if k!='y' else chosen['owner']['rect'][k]-chosen['owner']['deco_rect']['height']) for k in ('x','y','width','height')) or
|
||||
list(map(int,o['current'].split(',')))==exterior_ipc(chosen['owner']),'current owner geometry')
|
||||
win=chosen['owner']['window_rect'];actual=[scene[0]+win['x'],scene[1]+chosen['owner']['deco_rect']['height']+win['y']]
|
||||
need(actual==chosen['origin'],'applied content origin')
|
||||
box=list(map(int,r['clip'].split(',')));global_box=[box[0]+actual[0],box[1]+actual[1],*box[2:]]
|
||||
hidden=intersection(scene,out)[2]==0 or intersection(scene,out)[3]==0
|
||||
need(r['enabled']=='1' and r['clip_valid']=='1','enabled clipped root')
|
||||
need((box[2]==0 or box[3]==0) if hidden else global_box==out,'every comparable root clip')
|
||||
tile=list(map(int,sc['tile_scene'].split(',')));clip=list(map(int,sc['clip'].split(',')))
|
||||
need(sc['clip_valid']=='1' and [out[0]+tile[0]+clip[0],out[1]+tile[1]+clip[1],*clip[2:]]==out,'applied tiling viewport')
|
||||
if target:
|
||||
need(hidden==expect_hidden,'target visibility');applied=True
|
||||
rows.append(dict(state='applied-target' if target else 'old-applied',owner=o,scene=sc,root=r,commit=c,local_clip=box,global_clip=global_box,raw=e['raw'],all_roots=roots,known_layer_host=getattr(s,'marker_host',{}).get('tree')))
|
||||
need(applied,'target applied callback missing')
|
||||
return dict(tree=next(iter(trees)),states=rows,window=[window_start,end],ids=prot['ids'],marker_baseline=getattr(s,'marker_host',None),correlation='bounded unique live XDG hierarchy and exact IPC ancestry; no native parent ID or atomic generation')
|
||||
|
||||
def open_chain(s,tail_fillers=0):
|
||||
if s.spec['id']=='D3-mosaic':marker_host_baseline(s)
|
||||
s.debit('chain');s.launch_client('owner',s.spec['client'],s.spec['placement'])
|
||||
for i in range(tail_fillers):s.launch_client('tail'+str(i),'static-reference')
|
||||
s.cmd(f'[con_id={s.find("owner")["id"]}] focus');state=s.calibrate('owner');baseline=stable_reference(s)
|
||||
im=s.capture('owner-top','HEADLESS-1');rect=[*state['origin'],state['owner']['window_rect']['width'],state['owner']['window_rect']['height']]
|
||||
xy=solid_point(im,COLORS['TOPLEVEL'],s.outputs[0],s.frames['HEADLESS-1'],rect)
|
||||
need(contains([*state['origin'],420,575 if s.spec['campaign']=='D1' else 320],xy),'opening native bounds')
|
||||
ta,ba=s.click('owner',xy,'left',True);s.wait(lambda:'POPUP_A MAP ' in s.log('owner'));time.sleep(.2)
|
||||
aim=s.capture('popup-A','HEADLESS-1');prov=Provenance(state['owner']['id'],state['workspace']['id'],'bounded-opening-window',state['evidence'])
|
||||
xyA=solid_point(aim,COLORS['POPUP_A'],s.outputs[0],s.frames['HEADLESS-1'])
|
||||
tb,bb=s.click('owner',xyA,'left',True);s.wait(lambda:'POPUP_B MAP ' in s.log('owner'));time.sleep(.2)
|
||||
window=time.monotonic_ns();time.sleep(.15);p=protocol(s.log('owner'));g=geometry(s.log('owner'),state['origin'],prov)
|
||||
s.delivery('owner',rect,xy,ta,ba,'TOPLEVEL',p['grabs'][0]['serial']);s.delivery('owner',g['A'],xyA,tb,bb,'POPUP_A',p['grabs'][1]['serial'])
|
||||
need(all(crossing(g[k],s.spec['axis'],s.spec['boundary']) for k in ('A','B')),'actual root and child crossing NOT EXERCISED')
|
||||
left,right=s.pair_capture('mapped')
|
||||
for name in ('POPUP_A','POPUP_B'):
|
||||
need(count(left,COLORS[name])>0,'permitted popup pixels missing')
|
||||
if s.spec['mode']=='pristine':need(count(right,COLORS[name])>0,'standard popup continuation missing')
|
||||
if s.spec['mode']=='mosaic':exact(baseline,right);root=root_state(s,state,state['origin'],window)
|
||||
else:root=dict(status='NOT OBSERVABLE',meaning='unmodified control has no private clip instrumentation')
|
||||
s.save('chain',dict(protocol=p,geometry=g,state=state,root=root,H2_exact=s.spec['mode']=='mosaic',color_counts={n:[count(left,c),count(right,c)] for n,c in COLORS.items()}))
|
||||
return state,baseline,p,g,left,right
|
||||
|
||||
def popup_campaign(s):
|
||||
init_reference(s);state,baseline,p,g,left,right=open_chain(s)
|
||||
xy=solid_point(left,COLORS['POPUP_B'],s.outputs[0],s.frames['HEADLESS-1'],g['B']);tails,buttons=s.click('owner',xy);s.delivery('owner',g['B'],xy,tails,buttons,'POPUP_B')
|
||||
forbidden=intersection(g['B'],s.outputs[1]['logical']);need(forbidden[2]>6 and forbidden[3]>6,'H2 crossing interior missing')
|
||||
xy=[forbidden[0]+forbidden[2]//2,forbidden[1]+forbidden[3]//2]
|
||||
if s.spec['mode']=='pristine':
|
||||
# The point must be real standard paint, not just a requested extent.
|
||||
xy=solid_point(right,COLORS['POPUP_B'],s.outputs[1],s.frames['HEADLESS-2'],g['B']);tails,buttons=s.click('owner',xy);s.delivery('owner',g['B'],xy,tails,buttons,'POPUP_B')
|
||||
else:
|
||||
tails,buttons=s.click('owner',xy);ds=dismissal(tails['owner'],buttons['owner']);s.save('dismissal',dict(proof=ds,reference_buttons=buttons.get('reference'),interpretation='outside grab dismissal; no universal click-through claim'))
|
||||
need(not any('target=POPUP_A ' in l or 'target=POPUP_B ' in l for v in buttons.values() for l in v),'forbidden popup delivery')
|
||||
s.close_client('owner');time.sleep(.2);final=s.pair_capture('final');exact(baseline,final[1]);need(not any(count(im,COLORS[n]) for im in final for n in ('POPUP_A','POPUP_B')),'popup residual')
|
||||
return 'PASS'
|
||||
|
||||
def hidden_campaign(s):
|
||||
init_reference(s,2);state,baseline,p,g,left,right=open_chain(s,2)
|
||||
xy=solid_point(left,COLORS['POPUP_B'],s.outputs[0],'logical',g['B']);tails,buttons=s.click('owner',xy);s.delivery('owner',g['B'],xy,tails,buttons,'POPUP_B')
|
||||
delta=state['scene'][0]+state['scene'][2]-s.outputs[0]['logical'][0]+16;start=time.monotonic_ns();s.scroll('owner',delta);hidden=s.owner('owner')
|
||||
need(intersection(hidden['scene'],s.outputs[0]['logical'])[2]==0,'owner not completely hidden')
|
||||
images=s.pair_capture('fully-hidden');need(not any(count(im,COLORS[n]) for im in images for n in ('POPUP_A','POPUP_B')),'hidden popup residual');exact(baseline,images[1])
|
||||
root=root_state(s,hidden,hidden['origin'],start,True,previous=state);cut=len(s.log('owner'));s.inject('move',[-790,590]);need('target=POPUP_A ' not in s.log('owner')[cut:] and 'target=POPUP_B ' not in s.log('owner')[cut:],'hidden popup pointer enter')
|
||||
return_start=time.monotonic_ns();s.scroll('owner',-delta);restored=s.owner('owner');need(restored['origin']==state['origin'],'same owner origin restored')
|
||||
images=s.pair_capture('returned');exact(baseline,images[1]);returned_root=root_state(s,restored,restored['origin'],return_start,previous=hidden,expected_tree=root['tree']);fresh=protocol(s.log('owner'));need(fresh['ids']==p['ids'] and fresh['grabs']==p['grabs'],'unchanged live chain')
|
||||
xy=solid_point(images[0],COLORS['POPUP_B'],s.outputs[0],'logical',g['B']);tails,buttons=s.click('owner',xy);s.delivery('owner',g['B'],xy,tails,buttons,'POPUP_B')
|
||||
s.save('hidden-return',dict(status='PASS',hidden=hidden,empty_root=root,returned=restored,returned_root=returned_root,ids=p['ids']))
|
||||
s.close_client('owner');final=s.pair_capture('final');exact(baseline,final[1]);return 'PASS'
|
||||
|
||||
def dynamic_campaign(s):
|
||||
blank=s.capture('startup-vacated-H1','HEADLESS-1');init_reference(s,2);s.cmd('gaps inner 0');state,baseline,p,g,left,right=open_chain(s)
|
||||
owner_id=state['owner']['id'];ws_id=state['workspace']['id'];order=state['workspace'].get('mosaic',{}).get('rows',[{}])[0].get('children')
|
||||
s.cmd('output HEADLESS-1 resolution 960x600 position -960 0',True);s.outputs[0]={**s.outputs[0],'logical':[-960,0,960,600],'mode':[960,600]};time.sleep(.35)
|
||||
snap=s.snapshot('resized960');s.pair_capture('resized960')
|
||||
clamp=dict(status='NOT EXERCISED',reason='standard control has no Mosaic camera')
|
||||
if s.spec['mode']=='mosaic':
|
||||
ws=workspace(snap['tree'],'1');row=ws['mosaic']['rows'][0];n=len(row['children']);gap=max(0,ws.get('gaps',{}).get('inner',0));cell=max(640,(960-(n-1)*gap)//n);maximum=max(0,n*cell+(n-1)*gap-960)
|
||||
request=maximum+960;before=row['scroll_x'];s.scroll('owner',request);settled=s.snapshot('clamped960');actual=workspace(settled['tree'],'1')['mosaic']['rows'][0]['scroll_x']
|
||||
need(actual==maximum,'source-derived clamp mismatch');clamp=dict(status='PASS',count=n,gap=gap,minimum_cell_width=640,source='frozen mosaic.c125-152',maximum=maximum,before=before,requested_relative=request,observed=actual)
|
||||
s.cmd('output HEADLESS-1 resolution 800x600 position -800 0',True);s.outputs[0]=dict(s.spec['outputs'][0]);time.sleep(.35);s.snapshot('resized-back');s.pair_capture('resized-back')
|
||||
stages=[]
|
||||
for out,tag in [('HEADLESS-2','migrated-H2'),('HEADLESS-1','migrated-back-H1')]:
|
||||
s.cmd('workspace 1');s.cmd(f'move workspace to output {out}',True);time.sleep(.35);snap=s.snapshot(tag);ws=workspace(snap['tree'],'1');owner=s.find('owner');need(ws['id']==ws_id and ws['output']==out and owner['id']==owner_id,'migrated exact owner/workspace')
|
||||
if order is not None:need(ws['mosaic']['rows'][0]['children']==order,'logical row order changed')
|
||||
pictures=s.pair_capture(tag);stages.append(dict(tag=tag,workspace=ws,owner=owner,popup_done=re.findall(r'(POPUP_[AB]) POPUP_DONE',s.log('owner'))))
|
||||
if out=='HEADLESS-2':need(not any(count(pictures[0],COLORS[n]) for n in ('POPUP_A','POPUP_B')),'old output retained popup pixels')
|
||||
s.save('dynamic-ownership',dict(status='PASS',clamp=clamp,stages=stages,ids_before=p['ids'],ids_after=protocol(s.log('owner'))['ids'] if 'POPUP_DONE ' not in s.log('owner') else 'legitimate dismissal recorded',atomic_generation='NOT PROVEN'))
|
||||
s.close_client('owner')
|
||||
for name in list(s.proc):
|
||||
if name.startswith('filler'):s.close_client(name,'close')
|
||||
time.sleep(.2);final=s.pair_capture('final');exact(baseline,final[1]);exact(blank,final[0]);return 'PASS'
|
||||
|
||||
def saved_campaign(s):
|
||||
init_reference(s,2);s.launch_client('owner','saved-client');s.cmd(f'[con_id={s.find("owner")["id"]}] focus');state=s.calibrate('owner');baseline=stable_reference(s)
|
||||
s.client_cmd('owner','hold_next_configure','COMMAND hold_next_configure DONE');cut=len(s.log('owner'));cut_comp=len(s.log('compositor'));request=time.monotonic_ns();s.cmd('gaps inner 30',True)
|
||||
s.wait(lambda:'HELD ' in s.log('owner')[cut:]);held=s.log('owner')[cut:];m=re.search(r'HELD ns=(\d+) serial=(\d+)',held);need(m is not None,'real held configure serial')
|
||||
held_serial=int(m[2]);need(re.search(rf'xdg_surface#\d+\.configure\({held_serial}\)',held) is not None,'held actual wire configure');need(f'ack_configure({held_serial})' not in held,'held serial was ACKed')
|
||||
held_images=s.pair_capture('held');held_capture_observation='NOT EXERCISED: native saved identity absent; capture may follow natural200ms timeout'
|
||||
s.wait(lambda:'timed out (' in s.log('compositor')[cut_comp:],seconds=3);need(s.proc['owner'].poll() is None,'held client alive')
|
||||
after=s.pair_capture('natural-timeout');exact(baseline,after[1]);s.save('saved-observability',dict(status='INCONCLUSIVE',request_ns=request,held_wire=held,natural_timeout=[l for l in s.log('compositor')[cut_comp:].splitlines() if 'timed out (' in l],default_timeout_ms=200,held_serial=held_serial,
|
||||
saved_node_identity='NOT OBSERVABLE',saved_render_identity='NOT PROVEN',held_capture_observation=held_capture_observation,meaning='source-backed saved path and held configure; sampled SHM/pixels do not identify a saved scene node',ping_causality='wire responses recorded if natural ping occurs; no forced ping'))
|
||||
s.close_client('owner','close');final=s.pair_capture('final');exact(baseline,final[1]);return 'INCONCLUSIVE'
|
||||
|
||||
def interior_roi(s,name):
|
||||
owner=s.find(name)
|
||||
if s.spec['mode']=='mosaic' and not owner.get('floating','auto_off').endswith('on'):
|
||||
origin,scene,raw=actual_origin(owner,s.log('compositor'))
|
||||
else:origin=assigned_origin(owner)
|
||||
win=owner['window_rect'];r=intersection([*origin,win['width'],win['height']],s.outputs[0]['logical'])
|
||||
need(r[2]>=80 and r[3]>=80,'effect central ROI not visible')
|
||||
center=[r[0]+r[2]//2,r[1]+r[3]//2];p=global_png(center,s.outputs[0],s.frames['HEADLESS-1'])
|
||||
return [p[0]-32,p[1]-32,p[0]+32,p[1]+32]
|
||||
|
||||
def adjacent_metric(image,roi):
|
||||
im=image.crop(roi);total=0
|
||||
for y in range(im.height):
|
||||
for x in range(im.width-1):total+=sum(abs(a-b) for a,b in zip(im.getpixel((x,y)),im.getpixel((x+1,y))))
|
||||
return total/(im.height*(im.width-1)*3)
|
||||
|
||||
def effects_campaign(s):
|
||||
init_reference(s,1);baseline=stable_reference(s);s.launch_client('glass','fx-clients','glass');s.cmd(f'[con_id={s.find("glass")["id"]}] focus');s.calibrate('glass')
|
||||
bg=s.spawn('background',[LAB/'matrix/clients/fx-clients','background'],True);s.wait(lambda:'BACKGROUND_CONFIGURED' in s.log('background'));s.client_cmd('background','A','COMMAND A DONE');time.sleep(.2)
|
||||
s.cmd('gaps inner 24');s.cmd('blur_xray disable');s.cmd('blur_radius 5');s.cmd('blur_passes 3');s.cmd('blur_noise 0');s.cmd('blur_brightness 1');s.cmd('blur_contrast 1');s.cmd('blur_saturation 1')
|
||||
off=s.pair_capture('FX-off');gid=s.find('glass')['id']
|
||||
s.cmd('shadow_offset 20 0');s.cmd('shadow_blur_radius 16');s.cmd('shadow_color #000000A0');s.cmd(f'[con_id={gid}] shadows enable',True);time.sleep(.25);shadow=s.pair_capture('shadow-on')
|
||||
shadow_exercised=ImageChops.difference(off[0],shadow[0]).getbbox() is not None
|
||||
s.cmd(f'[con_id={gid}] shadows disable',True)
|
||||
# Native source selects classic for a floating owner, optimized for settled
|
||||
# tiled owners. Keep this classic glass wholly inside H1, then retile.
|
||||
s.cmd(f'[con_id={gid}] floating enable',True);s.cmd(f'[con_id={gid}] resize set width 300 px height 200 px');s.cmd(f'[con_id={gid}] move absolute position -650 200');time.sleep(.25)
|
||||
classic_off=s.pair_capture('classic-off');classic_roi=interior_roi(s,'glass');s.cmd(f'[con_id={gid}] blur enable',True);time.sleep(.25);classic=s.pair_capture('classic-blur')
|
||||
classic_adjacent_off=adjacent_metric(classic_off[0],classic_roi);classic_adjacent_on=adjacent_metric(classic[0],classic_roi)
|
||||
classic_exercised=classic_off[0].crop(classic_roi).tobytes()!=classic[0].crop(classic_roi).tobytes() and classic_adjacent_on<classic_adjacent_off
|
||||
floating=s.find('glass');need(floating['rect']['x']>=-800 and floating['rect']['x']+floating['rect']['width']<=0,'classic floating ROI wholly H1')
|
||||
s.cmd(f'[con_id={gid}] floating disable',True);time.sleep(.25);s.calibrate('glass')
|
||||
# A real background layer below blur_tree drives the native optimized path;
|
||||
# no shader/cache call is made by the runner.
|
||||
s.cmd(f'[con_id={gid}] blur disable',True);s.cmd(f'[con_id={gid}] blur enable',True);time.sleep(.25);optA=s.pair_capture('optimized-A');optimized_roi=interior_roi(s,'glass')
|
||||
s.client_cmd('background','B','COMMAND B DONE');time.sleep(.25);optB=s.pair_capture('optimized-B')
|
||||
s.client_cmd('background','A','COMMAND A DONE');time.sleep(.25);optA2=s.pair_capture('optimized-A-return')
|
||||
need(optA[0].crop(optimized_roi).tobytes()!=optB[0].crop(optimized_roi).tobytes(),'fresh central glass A-B RGB difference');exact(optA[0],optA2[0])
|
||||
if s.spec['mode']=='mosaic':
|
||||
for imgs in (off,shadow,classic_off,classic,optA,optB,optA2):exact(baseline,imgs[1])
|
||||
log=s.log('compositor');native_optimized='OPT_REGEN_RESULT' in log and 'OPT_BLUR_USE' in log and any('optimized=1' in l and 'chosen=(nil)' not in l for l in log.splitlines() if 'OPT_BLUR_USE' in l)
|
||||
else:native_optimized='NOT OBSERVABLE on pristine'
|
||||
s.save('effects-evidence',dict(status='PARTIALLY_FEASIBLE',standard_shadow_spill='EXERCISED' if s.spec['mode']=='pristine' and ImageChops.difference(off[1],shadow[1]).getbbox() else 'NOT EXERCISED',shadow_actual_RGB='EXERCISED' if shadow_exercised else 'NOT EXERCISED',classic_blur_actual_RGB='EXERCISED' if classic_exercised else 'NOT EXERCISED',native_optimized=native_optimized,classic_filtering=dict(roi=classic_roi,adjacent_off=classic_adjacent_off,adjacent_on=classic_adjacent_on),optimized_center_roi=optimized_roi,A_B_A_actual=True,
|
||||
classic_vs_optimized_identity='source-selected classic floating wholly H1 versus settled tiled optimized; native chosen/cache observations retained, no whole-cache equivalence claim; Mosaic-tiled classic clipping NOT TESTED',edges='recorded full PNG; historical max6 not presumed',complete_damage='NOT PROVEN'))
|
||||
s.close_client('glass','close');s.close_client('background','close');time.sleep(.2);final=s.pair_capture('final');exact(baseline,final[1]);return 'PARTIALLY_FEASIBLE'
|
||||
|
||||
def parse_pairs(text,limit=12):
|
||||
commits=[];presents={}
|
||||
for line in text.splitlines():
|
||||
if 'PI_OUTPUT_COMMIT ' in line or 'PI_OUTPUT_PRESENT ' in line:
|
||||
fields=dict(re.findall(r'(\w+)=([^\s]+)',line));need(all(k in fields for k in ('output','commit_seq','ns')),'native pair format');key=(fields['output'],int(fields['commit_seq']))
|
||||
if 'PI_OUTPUT_COMMIT ' in line:commits.append((key,fields,line))
|
||||
else:need(key not in presents,'duplicate native present');presents[key]=(fields,line)
|
||||
rows=[]
|
||||
for key,fields,raw in commits[:limit]:
|
||||
pr=presents.get(key);rows.append(dict(output=key[0],seq=key[1],commit=raw,present=pr[1] if pr else None,status='PAIRED' if pr else 'MISSING_PRESENT'))
|
||||
return dict(analyzed=rows,limit=limit,total_commits=len(commits),unselected_retained_raw=True,buffer_identity='NOT PROVEN')
|
||||
|
||||
def ordering_campaign(s):
|
||||
init_reference(s);s.launch_client('owner','region-client',s.spec['placement']);s.cmd(f'[con_id={s.find("owner")["id"]}] focus');state=s.calibrate('owner');baseline=stable_reference(s)
|
||||
im=s.capture('input-full-before','HEADLESS-1');rect=[*state['origin'],state['owner']['window_rect']['width'],state['owner']['window_rect']['height']];xy=solid_point(im,COLORS['TOPLEVEL'],s.outputs[0],'logical',rect)
|
||||
tails,buttons=s.click('owner',xy);s.delivery('owner',rect,xy,tails,buttons,'TOPLEVEL')
|
||||
s.client_cmd('owner','region_empty_top','COMMAND region_empty_top DONE');tails,buttons=s.click('owner',xy);need(not buttons['owner'],'empty input region delivered owner button');s.capture('region-empty','HEADLESS-1')
|
||||
s.client_cmd('owner','region_full_top','COMMAND region_full_top DONE');tails,buttons=s.click('owner',xy);s.delivery('owner',rect,xy,tails,buttons,'TOPLEVEL')
|
||||
s.cmd(f'[con_id={s.find("filler0")["id"]}] focus',True);s.cmd(f'[con_id={s.find("owner")["id"]}] focus',True)
|
||||
s.cmd('gaps inner 30',True);s.cmd('gaps inner 0',True);time.sleep(.25);s.capture('resize-settled','HEADLESS-1')
|
||||
if s.spec['mode']=='mosaic':
|
||||
s.scroll('owner',20);s.scroll('owner',-20);s.cmd(f'[con_id={s.find("owner")["id"]}] layout splith',True);s.cmd(f'[con_id={s.find("owner")["id"]}] layout mosaic',True)
|
||||
s.capture('layout-settled','HEADLESS-1');s.close_client('owner');final=s.pair_capture('final');exact(baseline,final[1])
|
||||
pairs=parse_pairs(s.log('compositor'));s.save('ordering',dict(status='INCONCLUSIVE',pairs=pairs,
|
||||
native_atomic_epoch='NOT PROVEN',native_hit_path='NOT OBSERVABLE',buffer_reference_release='NOT PROVEN',natural_commit_mutation_present='NOT EXERCISED unless independent timestamp analysis finds a real window',
|
||||
damage=[l for l in s.log('compositor').splitlines() if 'TEMPORAL_DAMAGE' in l],client_input='actual wire/button/region commits retained separately'))
|
||||
return 'INCONCLUSIVE'
|
||||
|
||||
CAMPAIGN={'D1':popup_campaign,'D2':popup_campaign,'D3':popup_campaign,'D4':dynamic_campaign,'E1':hidden_campaign,'E2':saved_campaign,'E3':effects_campaign,'F':ordering_campaign}
|
||||
188
tooling/probes/step20c-overnight/matrix/engine.py
Normal file
188
tooling/probes/step20c-overnight/matrix/engine.py
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
"""Bounded owned headless session, usable only after the separate matrix review gate."""
|
||||
import json,os,re,shutil,signal,subprocess,tempfile,threading,time
|
||||
from pathlib import Path
|
||||
from PIL import Image,ImageDraw
|
||||
from geometry import *
|
||||
from gate import ROOT,BASE,LAB,sha,clean_environment,action_debit
|
||||
import sys
|
||||
sys.path.insert(1,str(BASE))
|
||||
from ipc_predicates import nodes,workspace
|
||||
from predicates import protocol,geometry,pair,dismissal
|
||||
from lifecycle import opening_pair
|
||||
from model import Provenance
|
||||
COLORS={'TOPLEVEL':(35,68,204),'POPUP_A':(224,192,32),'POPUP_B':(32,208,160)}
|
||||
|
||||
class Session:
|
||||
def __init__(self,spec,auth):
|
||||
self.spec=spec;self.auth=auth;self.folder=LAB/'matrix'/spec['id'];self.run=self.folder/'run';self.run.mkdir()
|
||||
self.runtime=Path(tempfile.mkdtemp(prefix='m20-',dir='/tmp'));self.runtime.chmod(0o700)
|
||||
self.env=clean_environment();self.env.update(XDG_RUNTIME_DIR=str(self.runtime),WLR_BACKENDS='headless',WLR_HEADLESS_OUTPUTS='2',OPT_TRACE='1')
|
||||
self.outputs=[dict(o) for o in spec['outputs']];self.frames={o['name']:'logical' for o in self.outputs}
|
||||
self.proc={};self.handles=[];self.started=time.monotonic();self.finished=threading.Event();self.expired=False;self.events=[];self.pointer=None
|
||||
def start(self):
|
||||
spec=self.spec;auth=self.auth
|
||||
cfg='\n'.join(f"output {o['name']} resolution {o['mode'][0]}x{o['mode'][1]} scale {o['scale']} transform {o['transform']} position {o['logical'][0]} {o['logical'][1]}" for o in self.outputs)
|
||||
cfg+='\nworkspace 1 output HEADLESS-1\nworkspace 2 output HEADLESS-2\nfocus_follows_mouse no\nseat seat0 hide_cursor 100\nshadows disable\nblur disable\n'
|
||||
if spec['campaign'] in ('D2','D3'):
|
||||
marker=Image.new('RGB',(800,600),(30,30,46));draw=ImageDraw.Draw(marker)
|
||||
for k,box in [('TL',(15,15,44,44)),('TR',(755,15,784,44)),('BL',(15,555,44,584)),('BR',(755,555,784,584)),('C',(385,285,414,314))]:draw.rectangle(box,fill=MARKERS[k])
|
||||
marker.save(self.run/'marker-source.png');self.marker_source=self.run/'marker-source.png';self.record('generated-content-PNG',path=str(self.marker_source),counts_as_capture=False)
|
||||
(self.run/'config').write_text(cfg);self.watch=threading.Thread(target=self.watchdog,daemon=True);self.watch.start()
|
||||
self.comp=self.spawn('compositor',[auth['binary'],'-d','-c',self.run/'config'])
|
||||
self.sock=self.wait(lambda:next(iter(self.runtime.glob('sway-ipc.*.sock')),None));display=self.wait(lambda:next((p for p in self.runtime.glob('wayland-*') if p.is_socket()),None))
|
||||
self.env.update(WAYLAND_DISPLAY=display.name,SWAYSOCK=str(self.sock));need(Path(f'/proc/{self.comp.pid}/exe').resolve()==Path(auth['binary']).resolve(),'actual executable')
|
||||
maps=Path(f'/proc/{self.comp.pid}/maps').read_text();(self.run/'proc-maps.txt').write_text(maps)
|
||||
paths={s.split()[-1] for s in maps.splitlines() if len(s.split())>=6 and s.split()[-1].startswith('/')}
|
||||
wl=[Path(p).resolve() for p in paths if 'libwlroots' in p];need(wl==[Path(auth['wlroots'])] and sha(wl[0])==auth['wlroots_sha256'],'mapped wlroots')
|
||||
scene=[Path(p).resolve() for p in paths if 'libscenefx' in p]
|
||||
if spec['mode']=='mosaic':need(not scene,'static SceneFX')
|
||||
else:need(scene==[Path(auth['pristine_scene'])] and sha(scene[0])==auth['pristine_scene_sha256'],'mapped pristine SceneFX')
|
||||
render=[s for s in self.log('compositor').splitlines() if any(x in s for x in ('Creating scenefx FX renderer','GL renderer:','GL vendor:','Using OpenGL ES'))]
|
||||
need(any('Creating scenefx FX renderer' in s for s in render) and any('GL renderer:' in s for s in render),'actual renderer')
|
||||
self.record('actual-renderer',lines=render);self.snapshot('startup')
|
||||
if spec['campaign'] in ('D2','D3'):
|
||||
self.spawn('marker-background',['swaybg','-o','HEADLESS-1','-i',self.marker_source,'-m','fill']);time.sleep(.35)
|
||||
im=self.capture('marker-H1','HEADLESS-1',orientation_pending=True);m=marker_frame(im,self.outputs[0]['scale'],self.outputs[0]['transform']=='90');self.frames['HEADLESS-1']=m['frame'];self.record('marker-frame',**m)
|
||||
self.pointer=self.spawn('pointer',[LAB/'matrix/clients/pointer-daemon']);self.wait(lambda:'POINTER_READY' in self.log('pointer'))
|
||||
def record(self,kind,**fields):
|
||||
e=dict(kind=kind,ns=time.monotonic_ns(),**fields);self.events.append(e)
|
||||
with (self.run/'events.jsonl').open('a') as f:f.write(json.dumps(e)+'\n')
|
||||
def save(self,name,value):
|
||||
(self.run/(name+'.json')).write_text(json.dumps(value,indent=2)+'\n')
|
||||
def debit(self,*kinds):action_debit(self.spec['id'],*kinds)
|
||||
def spawn(self,name,argv,debug=False):
|
||||
need(not self.expired,'expired before spawn');f=(self.run/(name+'.log')).open('w');self.handles.append(f)
|
||||
p=subprocess.Popen(list(map(str,argv)),stdin=subprocess.PIPE,stdout=f,stderr=subprocess.STDOUT,text=True,start_new_session=True,env={**self.env,**({'WAYLAND_DEBUG':'1'} if debug else {})})
|
||||
self.proc[name]=p;self.record('spawn',name=name,pid=p.pid,argv=list(map(str,argv)));return p
|
||||
def log(self,name):return (self.run/(name+'.log')).read_text(errors='replace')
|
||||
def wait(self,fn,seconds=5):
|
||||
end=min(self.started+143,time.monotonic()+seconds)
|
||||
while time.monotonic()<end:
|
||||
need(not self.expired,'watchdog')
|
||||
if hasattr(self,'comp'):need(self.comp.poll() is None,'unexpected compositor exit')
|
||||
v=fn()
|
||||
if v:return v
|
||||
time.sleep(.02)
|
||||
raise Ambiguous('bounded wait timeout')
|
||||
def watchdog(self):
|
||||
while not self.finished.wait(.1):
|
||||
if time.monotonic()-self.started>143 or any(p.stat().st_size>32*1024*1024 for p in self.run.glob('*.log')):
|
||||
self.expired=True
|
||||
for p in list(self.proc.values()):
|
||||
try:os.killpg(p.pid,signal.SIGTERM)
|
||||
except ProcessLookupError:pass
|
||||
return
|
||||
def ipc(self,*args,strict=True):
|
||||
need(self.sock.is_relative_to(self.runtime) and self.sock.is_socket(),'private socket')
|
||||
self.record('ipc-request',args=args)
|
||||
p=subprocess.run(['swaymsg','-s',str(self.sock),*args],env=self.env,capture_output=True,text=True,timeout=4)
|
||||
self.record('ipc-reply',args=args,exit=p.returncode,stdout=p.stdout,stderr=p.stderr)
|
||||
if strict:need(p.returncode==0,'IPC command failure')
|
||||
return json.loads(p.stdout) if p.returncode==0 else None
|
||||
def cmd(self,command,business=False):
|
||||
self.debit('mutation' if business else 'configuration');reply=self.ipc(command);need(all(r['success'] for r in reply),'command rejected')
|
||||
def snapshot(self,tag,validate=True):
|
||||
snap=dict(tree=self.ipc('-t','get_tree'),outputs=self.ipc('-t','get_outputs'),workspaces=self.ipc('-t','get_workspaces'),ns=time.monotonic_ns())
|
||||
if validate:expected_outputs(snap['outputs'],self.outputs)
|
||||
self.save(tag+'-snapshot',snap);return snap
|
||||
def find(self,name):
|
||||
pid=self.proc[name].pid;rows=[n for n in nodes(self.ipc('-t','get_tree')) if n.get('pid')==pid];need(len(rows)<=1,'client owner ambiguity');return rows[0] if rows else None
|
||||
def launch_client(self,name,client,*args):
|
||||
p=self.spawn(name,[LAB/'matrix/clients'/client,*args],True);self.wait(lambda:self.find(name));time.sleep(.25);return p
|
||||
def capture(self,tag,out,orientation_pending=False):
|
||||
self.debit('capture');spec=next(o for o in self.outputs if o['name']==out);p=self.run/(tag+'.png');need(not p.exists(),'capture unique')
|
||||
t=time.monotonic_ns();r=subprocess.run(['grim','-s',str(spec['scale']),'-o',out,str(p)],env=self.env,capture_output=True,text=True,timeout=4)
|
||||
self.record('capture',tag=tag,output=out,start_ns=t,end_ns=time.monotonic_ns(),exit=r.returncode,path=str(p),sha256=sha(p) if p.exists() else None)
|
||||
need(r.returncode==0,'capture failure');im=Image.open(p).convert('RGB')
|
||||
logical=tuple(round(v*spec['scale']) for v in spec['logical'][2:]);physical=tuple(spec['mode'])
|
||||
need(im.size==logical or (orientation_pending and im.size==physical) or (self.frames[out]=='physical90' and im.size==physical),'PNG size')
|
||||
return im
|
||||
def pair_capture(self,tag):return [self.capture(tag+'-H1','HEADLESS-1'),self.capture(tag+'-H2','HEADLESS-2')]
|
||||
def inject(self,action,point,grab=False):
|
||||
need(action in ('move','left','right'),'injection action');outs=self.ipc('-t','get_outputs');active=[o for o in outs if o['active']]
|
||||
rects=[[o['rect'][k] for k in ('x','y','width','height')] for o in active];need(any(contains(r,point) for r in rects),'logical point outside output')
|
||||
minx=min(r[0] for r in rects);miny=min(r[1] for r in rects);w=max(r[0]+r[2] for r in rects)-minx;h=max(r[1]+r[3] for r in rects)-miny
|
||||
self.debit('move',*(['click'] if action!='move' else []),*(['grab'] if grab else []))
|
||||
sent=[point[0]-minx,point[1]-miny];marker=f'INJECT_DONE {action} {sent[0]},{sent[1]}';before=self.log('pointer').count(marker)
|
||||
self.pointer.stdin.write(f'{action} {sent[0]} {sent[1]} {w} {h}\n');self.pointer.stdin.flush();self.wait(lambda:self.log('pointer').count(marker)>before)
|
||||
self.record('injection',action=action,global_logical=point,sent=sent,logical_extent=[w,h],grab=grab);time.sleep(.12)
|
||||
def click(self,name,point,action='right',grab=False):
|
||||
before={n:len(self.log(n)) for n in self.proc if n not in ('compositor','pointer','background')};t=time.monotonic_ns();self.inject(action,point,grab)
|
||||
tails={n:self.log(n)[i:] for n,i in before.items()};lines={n:[s for s in v.splitlines() if s.startswith(('POINTER_BUTTON ','BUTTON '))] for n,v in tails.items()}
|
||||
self.record('click-observation',name=name,point=point,start_ns=t,tails=tails,buttons=lines)
|
||||
return tails,lines
|
||||
def delivery(self,name,rect,point,tails,lines,target,opening_serial=None):
|
||||
need(not any(v for n,v in lines.items() if n!=name),'wrong client received popup button')
|
||||
proof=opening_pair(lines[name],target,opening_serial) if opening_serial else pair(lines[name],target)
|
||||
rows=re.findall(rf'POINTER_(?:ENTER|MOTION) target={target} .*?x=([\d.-]+) y=([\d.-]+)',tails[name]);need(rows,'fresh pointer local coordinates')
|
||||
local=list(map(float,rows[-1]));need(local==[point[0]-rect[0],point[1]-rect[1]],'logical pointer correlation')
|
||||
for e in proof:need(re.search(rf'wl_pointer#\d+\.button\({e[1]}, \d+, {e[2]}, {e[3]}\)',tails[name]) is not None,'button actual wire')
|
||||
return proof
|
||||
def owner(self,name):
|
||||
snap=self.snapshot(name+'-owner');owner=self.find(name);need(owner is not None and owner.get('visible'),'owner visibility')
|
||||
ws=workspace(snap['tree'],'1');need(any(n['id']==owner['id'] for n in nodes(ws)),'owner workspace ancestry')
|
||||
if self.spec['mode']=='mosaic':origin,scene,raw=actual_origin(owner,self.log('compositor'))
|
||||
else:origin=assigned_origin(owner);scene=exterior_ipc(owner);raw='current IPC assigned exterior inverse; paint/pointer corroboration required'
|
||||
data=dict(owner=owner,workspace=ws,origin=origin,scene=scene,evidence=raw,snapshot=snap);self.save(name+'-geometry',data);return data
|
||||
def calibrate(self,name):
|
||||
state=self.owner(name)
|
||||
if self.spec['mode']=='mosaic' and self.spec['axis']==0:
|
||||
delta=int(state['origin'][0]+state['owner']['window_rect']['width']//2)
|
||||
if delta:self.scroll(name,delta)
|
||||
state=self.owner(name);need(crossing([*state['origin'],state['owner']['window_rect']['width'],state['owner']['window_rect']['height']],0,0),'owner real x0 crossing')
|
||||
return state
|
||||
def scroll(self,name,delta):
|
||||
self.debit('camera');self.cmd(f'[con_id={self.find(name)["id"]}] layout mosaic scroll {int(delta)}');time.sleep(.35)
|
||||
self.record('camera',owner=self.find(name)['id'],delta=int(delta))
|
||||
def client_cmd(self,name,cmd,marker=None):
|
||||
self.debit('configuration' if cmd.startswith('close') else 'mutation');before=len(self.log(name));self.record('client-request',name=name,command=cmd)
|
||||
self.proc[name].stdin.write(cmd+'\n');self.proc[name].stdin.flush()
|
||||
if marker:self.wait(lambda:marker in self.log(name)[before:])
|
||||
def close_client(self,name,command='close_owner'):
|
||||
self.client_cmd(name,command);code=self.proc[name].wait(timeout=3);need(code==0,'normal client exit');self.wait(lambda:self.find(name) is None)
|
||||
def cleanup(self,measurement_status,failure=None):
|
||||
terminal=None;rescue=False
|
||||
if hasattr(self,'comp') and self.comp.poll() is None and hasattr(self,'sock') and self.sock.is_socket():
|
||||
try:
|
||||
p=subprocess.run(['swaymsg','-s',str(self.sock),'exit'],env=self.env,capture_output=True,text=True,timeout=2)
|
||||
terminal=dict(ipc_exit=p.returncode,stdout=p.stdout,stderr=p.stderr)
|
||||
self.save('terminal-ipc',terminal)
|
||||
except Exception as e:
|
||||
terminal=dict(ipc_error=str(e));self.save('terminal-ipc',terminal)
|
||||
if terminal is not None and 'ipc_exit' in terminal:
|
||||
try:self.comp.wait(timeout=1)
|
||||
except Exception as e:
|
||||
terminal['wait_error']=str(e);self.save('terminal-ipc',terminal)
|
||||
codes=[]
|
||||
# One shared cleanup deadline prevents N owned clients multiplying waits.
|
||||
cleanup_end=time.monotonic()+2
|
||||
if self.pointer and self.pointer.poll() is None and self.pointer.stdin and not self.pointer.stdin.closed:self.pointer.stdin.close()
|
||||
for name,p in reversed(list(self.proc.items())):
|
||||
if p.poll() is None:
|
||||
try:p.wait(timeout=max(.01,cleanup_end-time.monotonic()))
|
||||
except subprocess.TimeoutExpired:pass
|
||||
residual=[p for p in self.proc.values() if p.poll() is None]
|
||||
if residual:
|
||||
rescue=True
|
||||
for p in residual:
|
||||
try:os.killpg(p.pid,signal.SIGKILL)
|
||||
except ProcessLookupError:pass
|
||||
kill_end=time.monotonic()+1
|
||||
for p in residual:p.wait(timeout=max(.01,kill_end-time.monotonic()))
|
||||
for name,p in self.proc.items():
|
||||
codes.append(dict(name=name,pid=p.pid,exit=p.returncode))
|
||||
if p.stdin and not p.stdin.closed:p.stdin.close()
|
||||
self.finished.set();self.watch.join(timeout=1)
|
||||
for h in self.handles:h.close()
|
||||
for p in self.proc.values():
|
||||
try:os.killpg(p.pid,0)
|
||||
except ProcessLookupError:pass
|
||||
else:rescue=True;os.killpg(p.pid,signal.SIGKILL)
|
||||
reaped=all(not Path('/proc',str(p.pid)).exists() for p in self.proc.values());need(reaped,'owned process residual');shutil.rmtree(self.runtime)
|
||||
seconds=time.monotonic()-self.started
|
||||
protocol_errors=[n for n in self.proc if n not in ('compositor','pointer') and re.search(r'wl_display[^\n]*\.error\(|protocol error|invalid_popup_parent|not_the_topmost_popup',self.log(n),re.I)]
|
||||
cleanup_pass=bool(not rescue and reaped and seconds<=150 and not self.expired and not protocol_errors and all(p['exit']==0 for p in codes))
|
||||
transport_pass=terminal is not None and terminal.get('ipc_exit')==0 and 'wait_error' not in terminal and 'ipc_error' not in terminal
|
||||
final_status=measurement_status if cleanup_pass and transport_pass else 'INCONCLUSIVE'
|
||||
self.save('outcome',dict(status=final_status,measurements_status=measurement_status,failure=failure,terminal=terminal,processes=codes,rescue=rescue,reaped=reaped,runtime_removed=not self.runtime.exists(),seconds=seconds,watchdog=self.expired,protocol_errors=protocol_errors,cleanup_pass=cleanup_pass,transport_status='PASS' if transport_pass else 'INCONCLUSIVE'))
|
||||
return final_status
|
||||
109
tooling/probes/step20c-overnight/matrix/gate.py
Normal file
109
tooling/probes/step20c-overnight/matrix/gate.py
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
"""Separate exact-byte gate: no case can borrow C's review or runtime budget."""
|
||||
import fcntl,json,os,stat,sys,time
|
||||
from pathlib import Path
|
||||
HERE=Path(__file__).resolve().parent;ROOT=HERE.parent;BASE=ROOT.parent/'step20c-static-e2-completion'
|
||||
sys.path.insert(1,str(ROOT))
|
||||
from horizontal_gate import DEFAULT_LAB as LAB,sha,env as clean_environment,exclusive,durable,PRISTINE,PRISTINE_SHA,WLROOTS
|
||||
import ledger
|
||||
from protocols import RUNS
|
||||
PROSPECTIVE_CASES=['D3-mosaic']
|
||||
REVIEW_SCOPE='OVERNIGHT_MATRIX_D3_PROSPECTIVE_V3'
|
||||
PLANNED_PNG={'D1':8,'D2':9,'D3':9,'D4':17,'E1':12,'E2':8,'E3':18,'F':8}
|
||||
|
||||
def read(p):return json.loads(Path(p).read_text())
|
||||
def need(ok,msg):
|
||||
if not ok:raise ValueError(msg)
|
||||
def verify_executed_archive():
|
||||
archive=LAB/'matrix/executed-v2-archive';f=read(archive/'freeze.json');mapping=read(archive/'archive-map.json')
|
||||
need(sha(archive/'freeze.json')=='a20ccb39095ca35363119b5cfe4b5c839c1a75863bd72a097686aeac76dd7d7f','original executed v2 freeze retained')
|
||||
r=read(archive/'review.json')
|
||||
need(r.get('status')=='ACCEPTED' and r.get('independent') is True and r.get('freeze_sha256')==sha(archive/'freeze.json') and r.get('files')==f['files'],'original executed v2 acceptance retained')
|
||||
need(set(mapping)==set(f['files']),'complete original path archive binding')
|
||||
for original,digest in f['files'].items():
|
||||
p=Path(mapping[original]);need(p.is_relative_to(archive/'frozen-files') and sha(p)==digest,'archived executed bytes:'+original)
|
||||
return mapping
|
||||
|
||||
def frozen_paths():
|
||||
w=LAB/'matrix';archive=verify_executed_archive();out=[p for p in HERE.iterdir() if p.is_file()]
|
||||
out+=[Path(p) for p in archive.values()]+[w/'executed-v2-archive/archive-map.json']
|
||||
out+=[ROOT/'ledger.py',ROOT/'horizontal_gate.py',ROOT/'horizontal_adapter.py']
|
||||
out+=[BASE/n for n in ('model.py','predicates.py','pixels.py','lifecycle.py','ipc_predicates.py','support.py','transition.py')]
|
||||
out+=[LAB/'phase-a.json',LAB/'parent/native-source-audit.json',w/'protocols.json',w/'compile.json',w/'offline-tests-rotation-v3-frozen.json',w/'readiness.json']
|
||||
out+=[p for p in (w/'clients').iterdir() if p.is_file()]
|
||||
out+=[w/'offline-tests-rotation-v3-complete-deps.json',w/'rotation-v3-readonly-validation-attempt1.json']
|
||||
out+=[LAB/'matrix/rotation-v3-repair-report.json',LAB/'matrix/rotation-v3-source.patch',LAB/'matrix/offline-tests-rotation-v3-attempt1.json',LAB/'mosaic-source/sway/desktop/layer_shell.c']
|
||||
out+=[LAB/'reviews/matrix-first-runtime-review.md',LAB/'reviews/matrix-first-runtime-review.json',LAB/'matrix/executed-v2-archive/freeze.json',LAB/'matrix/executed-v2-archive/review.json']
|
||||
out+=[LAB/'matrix/D2-mosaic/run'/n for n in ('compositor.log','owner.log','owner-geometry.json','events.jsonl','startup-snapshot.json')]
|
||||
out+=[w/'offline-tests-initial.json',w/'offline-tests.json',w/'actual-review-refusal.json',LAB/'horizontal/C2/run/compositor.log',LAB/'horizontal/C2/run/E2.log']
|
||||
out+=[Path(r['source']) for r in read(w/'compile.json')['sources']]
|
||||
out+=[Path(read(LAB/'phase-a.json')['binary']['path']),WLROOTS,PRISTINE]
|
||||
provenance=read(LAB/'horizontal/pristine-provenance.json');out+=[LAB/'horizontal/pristine-provenance.json',Path(provenance['historical_build_receipt'])]
|
||||
out+=[Path(r['path']) for c in provenance['source_equivalence'] for r in c['files']]
|
||||
out+=[LAB/r['path'] for r in read(LAB/'parent/native-source-audit.json')['sources']]
|
||||
out+=[Path('/home/fy59/swayfx-mosaic-optimized-edge-a55yjb8n/scene0-build/libscenefx-0.5.so').resolve()]
|
||||
import shutil
|
||||
out+=[Path(shutil.which(t)).resolve() for t in ('swaymsg','grim','swaybg')]
|
||||
return sorted(set(out))
|
||||
def freeze():
|
||||
need(read(LAB/'matrix/protocols.json')==RUNS,'private protocol exact source mapping')
|
||||
need(read(LAB/'matrix/offline-tests-rotation-v3-frozen.json')['status']=='PASS','executed negative tests')
|
||||
need(sum(PLANNED_PNG[g]*sum(r['campaign']==g for r in RUNS.values()) for g in PLANNED_PNG)+37+17<=220,'whole batch planned PNG allocation')
|
||||
exclusive(LAB/'matrix/freeze.json',dict(scope=REVIEW_SCOPE,status='READY_FOR_REVIEW',prospective_cases=PROSPECTIVE_CASES,previous_freeze_sha256='a20ccb39095ca35363119b5cfe4b5c839c1a75863bd72a097686aeac76dd7d7f',files={str(p):sha(p) for p in frozen_paths()},protocols=RUNS,planned_png=PLANNED_PNG,
|
||||
total_matrix_png=sum(PLANNED_PNG[r['campaign']] for r in RUNS.values()),C_allocation=37,G_allocation=17))
|
||||
def verify(review=True):
|
||||
w=LAB/'matrix';f=read(w/'freeze.json');need(f['files']=={str(p):sha(p) for p in frozen_paths()} and f['protocols']==RUNS and f.get('scope')==REVIEW_SCOPE and f.get('prospective_cases')==PROSPECTIVE_CASES,'final frozen bytes/schema')
|
||||
need(read(w/'readiness.json')['status']=='PREPARED_FOR_REVIEW','readiness')
|
||||
if review:
|
||||
r=read(w/'review.json');need(r.get('status')=='ACCEPTED' and r.get('independent') is True and r.get('read_only') is True and r.get('scope')==REVIEW_SCOPE and r.get('blockers')==[],'genuine independent matrix review')
|
||||
need(r.get('freeze_sha256')==sha(w/'freeze.json') and r.get('files')==f['files'],'review binding')
|
||||
need(sha(r['report_file'])==r['report_sha256'],'review report bytes')
|
||||
return f
|
||||
|
||||
def preflight(run_id,review=True):
|
||||
need(run_id in RUNS and not sys.flags.optimize,'known immutable case and assertions')
|
||||
verify(review);need(run_id in PROSPECTIVE_CASES,'only unused D3-mosaic prospective v3');w=LAB/'matrix'/run_id
|
||||
need(not (w/'consumed.json').exists() and not (w/'run').exists(),'no retry')
|
||||
need(stat.S_IMODE(LAB.stat().st_mode)==0o700 and LAB.stat().st_uid==os.getuid(),'owned0700 LAB')
|
||||
need(not any(stat.S_ISSOCK(p.lstat().st_mode) for p in w.rglob('*')),'stale case socket')
|
||||
a=read(LAB/'phase-a.json');need(a['status']=='PASS','phase A')
|
||||
need(read(LAB/'matrix/compile.json')['status']=='PASS','C17 clients')
|
||||
budget=read(LAB/'budget.json');ledger._validate(budget);spec=RUNS[run_id]
|
||||
need(run_id not in budget['runs'] and budget['used']['run']<18 and budget['campaign_used'][spec['campaign']]<ledger.CAMPAIGNS[spec['campaign']],'global run budget')
|
||||
need(budget['used']['capture']+PLANNED_PNG[spec['campaign']]<=220,'planned PNG availability')
|
||||
mode=spec['mode'];binary=a['binary']['path'] if mode=='mosaic' else str(PRISTINE);digest=a['binary']['sha256'] if mode=='mosaic' else PRISTINE_SHA
|
||||
need(sha(binary)==digest and sha(WLROOTS)=='7edca9a7d65855addda68e29ba5db94d57c803eb5fa7d0f86db3136b90ca9837','exact candidate/pristine/wlroots')
|
||||
for p in Path('/proc').iterdir():
|
||||
if p.name.isdigit():
|
||||
try:exe=(p/'exe').resolve(strict=True)
|
||||
except (FileNotFoundError,PermissionError,ProcessLookupError):continue
|
||||
need(exe!=Path(binary).resolve(),'same binary already running')
|
||||
scene=Path('/home/fy59/swayfx-mosaic-optimized-edge-a55yjb8n/scene0-build/libscenefx-0.5.so').resolve()
|
||||
return dict(binary=binary,binary_sha256=digest,wlroots=str(WLROOTS),wlroots_sha256=sha(WLROOTS),pristine_scene=str(scene),pristine_scene_sha256=sha(scene))
|
||||
def begin(run_id):
|
||||
auth=preflight(run_id);w=LAB/'matrix'/run_id;exclusive(w/'consumed.json',dict(id=run_id,freeze_sha256=sha(LAB/'matrix/freeze.json'),ns=time.monotonic_ns()))
|
||||
ledger.reserve(LAB/'budget.json','run',campaign=RUNS[run_id]['campaign'],run_id=run_id,duration_s=150)
|
||||
limits=action_limits(run_id)
|
||||
exclusive(w/'actions.json',dict(planned=limits,reserved=dict.fromkeys(limits,0),events=[]));return auth
|
||||
def action_limits(run_id):
|
||||
limits={k:v for k,v in RUNS[run_id]['limits'].items() if k not in ('duration_s','active_watchdog_s')}
|
||||
limits['capture']=PLANNED_PNG[RUNS[run_id]['campaign']];limits['configuration']=48;return limits
|
||||
|
||||
def local_valid(run_id,state):
|
||||
limits=action_limits(run_id);need(state.get('planned')==limits,'immutable planned action limits')
|
||||
values=state.get('reserved');events=state.get('events');need(isinstance(values,dict) and set(values)==set(limits) and isinstance(events,list),'local schema')
|
||||
need(all(type(values[k]) is int and 0<=values[k]<=limits[k] for k in limits),'local typed counters')
|
||||
need(len(events)==sum(values.values()),'local event total')
|
||||
need(all(isinstance(e,dict) and e.get('sequence')==i and e.get('kind') in limits and type(e.get('ns')) is int and e['ns']>0 for i,e in enumerate(events)),'local event sequence')
|
||||
need(all(sum(e['kind']==k for e in events)==values[k] for k in limits),'local reconciliation')
|
||||
return state
|
||||
|
||||
def action_debit(run_id,*kinds):
|
||||
w=LAB/'matrix'/run_id
|
||||
with (w/'actions.lock').open('a+') as lock:
|
||||
fcntl.flock(lock,fcntl.LOCK_EX);state=local_valid(run_id,read(w/'actions.json'))
|
||||
need(kinds and all(k in state['planned'] for k in kinds),'known actions')
|
||||
need(all(state['reserved'][k]+kinds.count(k)<=state['planned'][k] for k in kinds),'planned local action exhausted')
|
||||
for k in kinds:
|
||||
if k=='capture':ledger.reserve(LAB/'budget.json','capture',run_id=run_id)
|
||||
state['reserved'][k]+=1;state['events'].append(dict(kind=k,ns=time.monotonic_ns(),sequence=len(state['events'])))
|
||||
durable(w/'actions.json',state)
|
||||
66
tooling/probes/step20c-overnight/matrix/geometry.py
Normal file
66
tooling/probes/step20c-overnight/matrix/geometry.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
"""Explicit logical global, IPC assignment, physical mode and PNG-frame adapters."""
|
||||
from dataclasses import dataclass
|
||||
import re
|
||||
from PIL import Image
|
||||
|
||||
class Ambiguous(ValueError):pass
|
||||
def need(ok,msg):
|
||||
if not ok:raise Ambiguous(msg)
|
||||
def box_end(r):return [r[0]+r[2],r[1]+r[3]]
|
||||
def contains(r,p):return r[0]<=p[0]<r[0]+r[2] and r[1]<=p[1]<r[1]+r[3]
|
||||
def crossing(r,axis,boundary):return r[axis]<boundary<r[axis]+r[axis+2]
|
||||
def intersection(a,b):
|
||||
x=max(a[0],b[0]);y=max(a[1],b[1]);return [x,y,max(0,min(a[0]+a[2],b[0]+b[2])-x),max(0,min(a[1]+a[3],b[1]+b[3])-y)]
|
||||
def assigned_origin(owner):
|
||||
# IPC rect already excludes top decoration (ipc-json.c899–906).
|
||||
return [owner['rect']['x']+owner['window_rect']['x'],owner['rect']['y']+owner['window_rect']['y']]
|
||||
def exterior_ipc(owner):
|
||||
r=owner['rect'];d=owner['deco_rect']['height'];return [r['x'],r['y']-d,r['width'],r['height']+d]
|
||||
def actual_origin(owner,trace):
|
||||
rows=[s for s in trace.splitlines() if 'PI_OWNER output=' in s and f' id={owner["id"]} ' in s]
|
||||
need(rows,'applied owner trace absent');m=re.search(r'scene_valid=1 scene=(-?\d+),(-?\d+) applied_size=(\d+),(\d+)',rows[-1]);need(m is not None,'applied owner malformed')
|
||||
r=list(map(int,m.groups()));w=owner['window_rect'];return [r[0]+w['x'],r[1]+owner['deco_rect']['height']+w['y']],r,rows[-1]
|
||||
MARKERS={'TL':(255,32,32),'TR':(32,255,32),'BL':(32,32,255),'BR':(255,255,32),'C':(192,32,255)}
|
||||
def marker_frame(image,scale,rotated):
|
||||
positions={k:[] for k in MARKERS};lookup={v:k for k,v in MARKERS.items()}
|
||||
for y in range(image.height):
|
||||
for x in range(image.width):
|
||||
k=lookup.get(image.getpixel((x,y)))
|
||||
if k:positions[k].append((x,y))
|
||||
need(all(len(v)>=100 for v in positions.values()),'marker color missing')
|
||||
c={k:[sum(p[i] for p in v)/len(v) for i in (0,1)] for k,v in positions.items()}
|
||||
logical=image.size==(round(800*scale),round(600*scale)) and c['TL'][0]<c['TR'][0] and c['TL'][1]<c['BL'][1]
|
||||
physical=rotated and image.size==(round(600*scale),round(800*scale)) and c['TL'][0]>c['BL'][0] and c['TL'][1]<c['TR'][1]
|
||||
need(logical or physical,'PNG orientation unestablished')
|
||||
return dict(frame='logical' if logical else 'physical90',centers=c,counts={k:len(v) for k,v in positions.items()},size=list(image.size))
|
||||
def png_global(px,output,frame):
|
||||
scale=output['scale'];x,y=px;ox,oy,w,h=output['logical']
|
||||
if frame=='physical90':x,y=y,round(h*scale)-1-x
|
||||
need(frame in ('logical','physical90'),'unknown PNG frame')
|
||||
# Sample integer logical centers only; resample at the chosen center later.
|
||||
return [ox+int(x/scale),oy+int(y/scale)]
|
||||
def global_png(p,output,frame):
|
||||
x=(p[0]-output['logical'][0])*output['scale'];y=(p[1]-output['logical'][1])*output['scale']
|
||||
if frame=='physical90':x,y=output['logical'][3]*output['scale']-1-y,x
|
||||
return [round(x),round(y)]
|
||||
def solid_point(image,color,output,frame,rectangle=None,margin=3):
|
||||
for y in range(margin,image.height-margin,3):
|
||||
for x in range(margin,image.width-margin,3):
|
||||
if image.getpixel((x,y))!=color:continue
|
||||
p=png_global((x,y),output,frame)
|
||||
if rectangle is not None and not contains(rectangle,p):continue
|
||||
pp=global_png(p,output,frame)
|
||||
if not (margin<=pp[0]<image.width-margin and margin<=pp[1]<image.height-margin):continue
|
||||
if all(image.getpixel((xx,yy))==color for yy in range(pp[1]-margin,pp[1]+margin+1) for xx in range(pp[0]-margin,pp[0]+margin+1)):
|
||||
return p
|
||||
raise Ambiguous('no measured solid logical interior')
|
||||
def count(image,color):return sum(v==color for v in image.get_flattened_data())
|
||||
def exact(a,b):need(a.size==b.size and a.tobytes()==b.tobytes(),'whole RGB differs');return dict(equal=True,pixels=a.width*a.height)
|
||||
def expected_outputs(outputs,spec):
|
||||
active=[o for o in outputs if o['active']];need(len(active)==2,'active output count')
|
||||
for want in spec:
|
||||
rows=[o for o in active if o['name']==want['name']];need(len(rows)==1,'output identity')
|
||||
o=rows[0];need([o['rect'][k] for k in ('x','y','width','height')]==want['logical'],'logical output geometry')
|
||||
need(o['scale']==want['scale'] and o['transform']==want['transform'],'scale/IPC transform string')
|
||||
need([o['current_mode'][k] for k in ('width','height')]==want['mode'],'physical mode')
|
||||
return active
|
||||
48
tooling/probes/step20c-overnight/matrix/prepare.py
Normal file
48
tooling/probes/step20c-overnight/matrix/prepare.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Private C17 client preparation; never reserves or launches a compositor."""
|
||||
from pathlib import Path
|
||||
import hashlib,json,os,shutil,subprocess,sys
|
||||
HERE=Path(__file__).resolve().parent;ROOT=HERE.parent;REPO=ROOT.parents[2]
|
||||
sys.path.insert(0,str(ROOT));from horizontal_gate import DEFAULT_LAB,BASE,sha,env,exclusive,OLD_CLIENTS
|
||||
from protocols import RUNS
|
||||
lab=DEFAULT_LAB/'matrix';lab.mkdir();dest=lab/'clients';dest.mkdir();commands=[];sources=[]
|
||||
def command(argv,cwd=None):
|
||||
r=subprocess.run(list(map(str,argv)),cwd=cwd,env=env(),capture_output=True,text=True,timeout=30)
|
||||
commands.append(dict(argv=list(map(str,argv)),cwd=str(cwd),exit=r.returncode,stdout=r.stdout,stderr=r.stderr))
|
||||
if r.returncode:raise RuntimeError('private client preparation failed')
|
||||
return r.stdout
|
||||
def copy(src,name):
|
||||
p=dest/name;shutil.copyfile(src,p);sources.append(dict(source=str(src),sha256=sha(src),destination=str(p)));return p
|
||||
for n in ('nested-client.c','pointer-daemon.c','saved-client.c','static-reference.c','xdg-shell-client.h','xdg-shell-protocol.c','virtual-pointer-client.h','virtual-pointer-protocol.c'):copy(OLD_CLIENTS/n,n)
|
||||
# An independent base is patched privately; original repository/historical bytes stay read only.
|
||||
copy(REPO/'tooling/probes/step20c-nested-grab/nested-client.c','vertical-client.c')
|
||||
patch=REPO/'tooling/probes/step20c-vertical-grab/client-vertical.patch'
|
||||
p=dest/'vertical-patch';p.write_text(patch.read_text().replace('nested-client.c','vertical-client.c'))
|
||||
command(['patch','--batch','--forward','--fuzz=0','-p1','-i',p],cwd=dest);sources.append(dict(source=str(patch),sha256=sha(patch),destination=str(p)))
|
||||
# Extents are supplied for every injection so resized output unions remain explicit.
|
||||
pointer=dest/'pointer-daemon.c';old='if (sscanf(line,"%15s %d %d",action,&x,&y)!=3) continue;'
|
||||
new='unsigned width,height;\n if (sscanf(line,"%15s %d %d %u %u",action,&x,&y,&width,&height)!=5 || !width || !height || x<0 || y<0 || (unsigned)x>=width || (unsigned)y>=height || (strcmp(action,"move") && strcmp(action,"left") && strcmp(action,"right"))) return 3;'
|
||||
s=pointer.read_text();assert s.count(old)==1;s=s.replace(old,new).replace('x,y,1600,600','x,y,width,height');pointer.write_text(s)
|
||||
copy(REPO/'tooling/probes/step20c-nested-grab/nested-client.c','region-client.c')
|
||||
patch=REPO/'tooling/probes/step20c-mutation-coverage/mutation-client.patch';p=dest/'region-patch';p.write_text(patch.read_text().replace('nested-client.c','region-client.c'))
|
||||
# Reuse only the exact added command branch; historical close_a context is absent.
|
||||
minus=[line[1:] for line in patch.read_text().splitlines() if line.startswith('-') and not line.startswith('---')]
|
||||
plus=[line[1:] for line in patch.read_text().splitlines() if line.startswith('+') and not line.startswith('+++')]
|
||||
r=dest/'region-client.c';text=r.read_text();assert len(minus)==1 and text.count(minus[0])==1
|
||||
r.write_text(text.replace(minus[0],'\n'.join(plus),1));sources.append(dict(source=str(patch),sha256=sha(patch),destination=str(p)))
|
||||
copy(REPO/'tooling/probes/step20c-optimized-blur-edge/clients.c','fx-clients.c')
|
||||
xml=REPO/'tooling/probes/step20c-optimized-blur-cache/wlr-layer-shell-unstable-v1.xml'
|
||||
copy(xml,'wlr-layer-shell-unstable-v1.xml')
|
||||
for mode,suffix in [('client-header','-client.h'),('private-code','-protocol.c')]:command(['wayland-scanner',mode,xml,dest/('layer-shell'+suffix)])
|
||||
# XDG/virtual-pointer regeneration must equal the retained compiled client's wire API.
|
||||
for stem,xml in [('xdg-shell',Path('/usr/share/wayland-protocols/stable/xdg-shell/xdg-shell.xml')),('virtual-pointer',REPO/'tooling/probes/step20c-valid-grab/wlr-virtual-pointer-unstable-v1.xml')]:
|
||||
for mode,suffix in [('client-header','-client.h'),('private-code','-protocol.c')]:
|
||||
p=dest/(stem+suffix+'.regenerated');command(['wayland-scanner',mode,xml,p]);assert p.read_bytes()==(dest/(stem+suffix)).read_bytes()
|
||||
sources.append(dict(source=str(xml),sha256=sha(xml),destination=None))
|
||||
flags=command(['pkg-config','--cflags','--libs','wayland-client']).split()
|
||||
for name,protocol in [('nested-client','xdg-shell'),('vertical-client','xdg-shell'),('region-client','xdg-shell'),('saved-client','xdg-shell'),('static-reference','xdg-shell'),('pointer-daemon','virtual-pointer'),('fx-clients','xdg-shell')]:
|
||||
command(['cc','-std=c17','-O2','-Wall','-Wextra',dest/(name+'.c'),dest/(protocol+'-protocol.c'),*([dest/'layer-shell-protocol.c'] if name=='fx-clients' else []),'-o',dest/name,*flags])
|
||||
exclusive(lab/'compile.json',dict(status='PASS',sources=sources,commands=commands,files={str(p):sha(p) for p in sorted(dest.iterdir()) if p.is_file()},runtime='NOT RUN'))
|
||||
exclusive(lab/'protocols.json',RUNS)
|
||||
for run_id in RUNS:(lab/run_id).mkdir()
|
||||
print('MATRIX_CLIENTS_C17=PASS RUNS=NOT_RUN')
|
||||
44
tooling/probes/step20c-overnight/matrix/protocols.py
Normal file
44
tooling/probes/step20c-overnight/matrix/protocols.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
"""Frozen bounded questions/actions; source absence limits the scientific verdict."""
|
||||
C=[dict(name='HEADLESS-1',logical=[-800,0,800,600],mode=[800,600],scale=1.,transform='normal'),dict(name='HEADLESS-2',logical=[0,0,800,600],mode=[800,600],scale=1.,transform='normal')]
|
||||
D1=[dict(name='HEADLESS-1',logical=[0,0,800,600],mode=[800,600],scale=1.,transform='normal'),dict(name='HEADLESS-2',logical=[0,600,800,600],mode=[800,600],scale=1.,transform='normal')]
|
||||
D2=[{**C[0],'mode':[1000,750],'scale':1.25},C[1]]
|
||||
D3=[{**C[0],'mode':[750,1000],'scale':1.25,'transform':'90'},C[1]]
|
||||
LIMITS=dict(duration_s=150,active_watchdog_s=143,capture=20,click=8,grab=2,camera=2,mutation=24,move=16,chain=1)
|
||||
SPECS={
|
||||
'D1':dict(outputs=D1,client='vertical-client',placement='v_both_cross',axis=1,boundary=600,
|
||||
question='Do genuine root/child vertical crossings preserve upper pixels/input and exclude lower popup pixels/input under Mosaic?',
|
||||
limits={**LIMITS,'capture':16,'camera':1},steps=['stable-H2-reference-pair','open-owner','real-left-TOPLEVEL-open-A','real-left-A-open-B','actual-y600-crossing-A-B','upper-B-right-input','lower-B-right-standard-or-forbidden-Mosaic','record-dismissal','normal-close','whole-H2-restoration']),
|
||||
'D2':dict(outputs=D2,client='nested-client',placement='static_cross',axis=0,boundary=0,
|
||||
question='Does scale1.25 use logical pointer coordinates once and preserve sampled crossing popup clipping?',
|
||||
limits={**LIMITS,'capture':16,'camera':1},steps=['five-asymmetric-output-markers','observe-logical-PNG-frame-and-physical-mode','stable-H2-reference-pair','owner-once-calibration','two-real-grabs','actual-A-B-x0-crossing','visible-B-right','H2-standard-or-forbidden-B-right','record-dismissal','normal-close','whole-H2-restoration']),
|
||||
'D3':dict(outputs=D3,client='nested-client',placement='static_cross',axis=0,boundary=0,
|
||||
question='Does R90 scale1.25 preserve measured logical popup and pointer geometry?',
|
||||
limits={**LIMITS,'capture':16,'camera':1},steps=['five-asymmetric-output-markers','observe-IPC-transform-string90-and-PNG-frame','stable-H2-reference-pair','owner-once-calibration','two-real-grabs','actual-A-B-x0-crossing','visible-B-right','H2-standard-or-forbidden-B-right','record-dismissal','normal-close','whole-H2-restoration']),
|
||||
'D4':dict(outputs=C,client='nested-client',placement='static_cross',axis=0,boundary=0,
|
||||
question='Do output resize/clamp and workspace migration preserve observed owner/order and clear old output pixels?',
|
||||
limits=LIMITS,steps=['startup-vacated-H1-reference','all-clients-open-before-camera','H2-reference-pair','owner-calibration','two-real-grabs','initial-identities-order-camera-clip','H1-mode960x600-position-960','single-known-extent-large-scroll-source-clamp','H1-mode800x600-position-800','workspace1-move-to-H2','workspace1-move-back-H1','record-live-identities-or-legitimate-dismissal','normal-close','final-vacated-H1-and-whole-H2']),
|
||||
'E1':dict(outputs=C,client='nested-client',placement='static_cross',axis=0,boundary=0,
|
||||
question='Does a fully hidden applied owner hide the same root/child and allow an unchanged live chain to return?',
|
||||
limits={**LIMITS,'capture':16,'grab':4,'chain':2,'camera':4},steps=['two-prefix-fillers-owner-two-tail-fillers-preopened','calibrate-once','H2-reference-pair','two-real-grabs-crossing','visible-B-right','hide-owner-camera-computed-from-applied-endpoint','empty-root-clip-and-no-popup-colors-H1-H2','motion-no-popup-enter','return-exact-inverse-camera','same-object-IDs-no-popup_done-or-recreation','visible-B-right','normal-close','whole-H2-restoration']),
|
||||
'E2':dict(outputs=C,client='saved-client',placement=None,axis=0,boundary=0,
|
||||
question='What is actually observed during one natural200ms held XDG transaction, without saved-node traces?',
|
||||
limits={**LIMITS,'capture':12,'click':2,'move':2,'camera':1,'grab':0,'chain':0},steps=['all-clients-preopened','one-crossing-camera','H2-reference-pair','arm-hold_next_configure','normal-gaps-inner30-configure-request','received-serial-HOLD-wire','held-capture-if-before-timeout','natural-default200ms-timeout-no-forced-ACK','client-alive-wire-ping-dispatch','post-timeout-capture','normal-close','whole-H2-restoration'],
|
||||
ceilings=['saved-node identity NOT OBSERVABLE','native saved render NOT PROVEN','saved clipping verdict INCONCLUSIVE even if sampled H2 RGB passes']),
|
||||
'E3':dict(outputs=C,client='fx-clients',placement=None,axis=0,boundary=0,
|
||||
question='Are shadow/classic/optimized blur actual RGB effects and fresh A/B/A pixels sampled with protected whole H2?',
|
||||
limits={**LIMITS,'mutation':10,'capture':20,'grab':0,'chain':0},steps=['static-H2-reference-pair','real-H1-checker-layer-explicit-A','translucent-XDG-and-fillers','single-crossing-camera-or-standard-spill','FX-off-pair','shadow-on-pair-visible-ROI-change','shadow-off-classic-floating-glass-wholly-H1-off-pair','classic-blur-on-pair-actual-filtered-RGB','retile-and-once-recalibrate-for-optimized','optimized-on-natural-cache-A-pair','explicit-background-B-pair','explicit-background-A-pair','central-A-B-A-fresh-RGB','Mosaic-whole-H2-protected-every-state','normal-close'],
|
||||
ceilings=['pristine has no OBC trace','sampling/cache-source and final-draw clip are distinct','classic floating wholly H1 does not qualify Mosaic-tiled classic clipping','standard glass tiled at output edge does not establish optimized glass crossing; H2 shadow spill recorded separately','common edge delta measured anew, historical max6 not a threshold']),
|
||||
'F':dict(outputs=C,client='region-client',placement='both_cross',axis=0,boundary=0,
|
||||
question='What natural applied scene/client input/damage/native commit-present ordering is actually sampled?',
|
||||
limits={**LIMITS,'capture':12,'click':12,'move':12,'mutation':24,'camera':3,'grab':0,'chain':0},steps=['H2-reference-pair','owner-current-assignment-and-applied-scene','baseline-right-input','region-empty-real-client-commit','same-point-right-no-owner-delivery','region-full-real-client-commit','same-point-right-owner-delivery','focus-filler-and-owner','gaps-inner30-and0','one-camera-forward-and-inverse-if-Mosaic','layout-splith-and-Mosaic-if-candidate','sample-settled-pixels-and-whole-H2','analyze-at-most12-exact-output-seq-commit-present-pairs','record-missing-present-and-natural-window-NOT_EXERCISED','normal-close'],
|
||||
ceilings=['native buffer reference/release identity NOT PROVEN','atomic applied epoch NOT PROVEN','no GPU readback, forced timing or interval-seeking loop'])}
|
||||
RUNS={}
|
||||
for group,spec in SPECS.items():
|
||||
modes=('mosaic',) if group in ('E1','E2') else ('pristine','mosaic')
|
||||
for mode in modes:
|
||||
run_id=group+'-'+mode
|
||||
RUNS[run_id]={**spec,'id':run_id,'campaign':group,'mode':mode,
|
||||
'preconditions':['independent final-byte review','exact annotated979dcd95 or pinned pristine8ac86548','exact wlroots7edca9','owned0700 runtime and explicit sockets','unused unique run token','global/local budget','complete client/protocol source provenance'],
|
||||
'click_computation':'measured solid color interior converted via observed PNG frame/scale/output origin; recompute against latest live configured/applied geometry before each injection; half-open logical bounds; no double scale',
|
||||
'stop_criteria':['deadline143s','log32MiB','ambiguous owner/root/popup ancestry','output mismatch','missing actual marker frame for scale/rotation','crossing not actually exercised','protocol error','unobservable mandatory predicate','local/global budget exhausted'],
|
||||
'retention':['all raw stdout/stderr/process and terminal IPC codes','all IPC snapshots/request intervals','client WAYLAND_DEBUG with object/serial/ACK/buttons','renderer and proc maps','original full RGB PNGs','every reservation and observed consumption','independent source windows without atomic generation claim']}
|
||||
24
tooling/probes/step20c-overnight/matrix/run.py
Normal file
24
tooling/probes/step20c-overnight/matrix/run.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Exactly one reviewed matrix case, with preserved failures and owned cleanup."""
|
||||
import argparse,json,time
|
||||
from pathlib import Path
|
||||
from gate import begin,LAB,verify,read,durable
|
||||
from protocols import RUNS
|
||||
from engine import Session
|
||||
from cases import CAMPAIGN
|
||||
p=argparse.ArgumentParser();p.add_argument('run_id',choices=RUNS);a=p.parse_args()
|
||||
auth=begin(a.run_id);s=None;status='INCONCLUSIVE';failure=None
|
||||
try:
|
||||
s=Session(RUNS[a.run_id],auth);s.start();status=CAMPAIGN[RUNS[a.run_id]['campaign']](s)
|
||||
except Exception as e:
|
||||
failure=dict(type=type(e).__name__,message=str(e));status='INCONCLUSIVE'
|
||||
finally:
|
||||
if s is not None:status=s.cleanup(status,failure)
|
||||
state=read(LAB/'matrix'/a.run_id/'actions.json')
|
||||
if s is not None:
|
||||
state['consumed_observed']=dict(capture=sum(e['kind']=='capture' for e in s.events),
|
||||
click=sum(e['kind']=='injection' and e['action']!='move' for e in s.events),
|
||||
move=sum(e['kind']=='injection' for e in s.events),grab=sum(e['kind']=='injection' and e.get('grab',False) for e in s.events))
|
||||
durable(LAB/'matrix'/a.run_id/'actions.json',state);verify()
|
||||
print(json.dumps(dict(id=a.run_id,status=status,failure=failure)))
|
||||
raise SystemExit(1 if failure else 0)
|
||||
248
tooling/probes/step20c-overnight/matrix/tests.py
Normal file
248
tooling/probes/step20c-overnight/matrix/tests.py
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
"""Executed pure/isolated negative tests, no synthetic independent review."""
|
||||
import argparse,ast,copy,json,os,sys,tempfile,unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch,Mock
|
||||
from PIL import Image,ImageDraw
|
||||
import gate
|
||||
from protocols import RUNS
|
||||
from geometry import *
|
||||
from cases import parse_pairs,root_state,marker_host_baseline,associated_root
|
||||
from transition import observed_epochs
|
||||
from types import SimpleNamespace
|
||||
|
||||
class Tests(unittest.TestCase):
|
||||
def test_all_source_compile(self):
|
||||
for p in Path(__file__).parent.glob('*.py'):compile(p.read_text(),str(p),'exec')
|
||||
def test_all_campaigns_exact_unique(self):
|
||||
self.assertEqual(len(RUNS),14);self.assertEqual(set(r['campaign'] for r in RUNS.values()),{'D1','D2','D3','D4','E1','E2','E3','F'})
|
||||
for key,r in RUNS.items():
|
||||
self.assertEqual(key,r['id']);self.assertTrue(r['steps'] and r['preconditions'] and r['retention'] and r['stop_criteria'])
|
||||
def test_global_planned_png_fits(self):
|
||||
total=sum(gate.PLANNED_PNG[r['campaign']] for r in RUNS.values())+37+17
|
||||
self.assertLessEqual(total,220);self.assertEqual(total,212)
|
||||
def test_limits(self):
|
||||
for r in RUNS.values():
|
||||
self.assertLessEqual(r['limits']['duration_s'],150);self.assertLessEqual(gate.PLANNED_PNG[r['campaign']],12 if r['campaign']=='F' else 20)
|
||||
def test_exact_binary_review_pending_refusal_no_action(self):
|
||||
with patch.object(gate,'verify',side_effect=ValueError('missing independent review')),patch.object(gate.ledger,'reserve') as reserve:
|
||||
for key in RUNS:
|
||||
with self.assertRaises(ValueError):gate.preflight(key)
|
||||
reserve.assert_not_called()
|
||||
def test_half_open_negative_origin(self):
|
||||
self.assertTrue(contains([-800,0,800,600],[-1,599]));self.assertFalse(contains([-800,0,800,600],[0,599]));self.assertFalse(contains([-800,0,800,600],[-1,600]))
|
||||
def test_real_crossing_axis(self):
|
||||
self.assertTrue(crossing([70,585,170,130],1,600));self.assertFalse(crossing([70,600,170,130],1,600));self.assertFalse(crossing([70,470,170,130],1,600))
|
||||
def test_splith_ipc_decoration_not_double_added(self):
|
||||
o=dict(rect=dict(x=-400,y=25,width=400,height=575),window_rect=dict(x=2,y=0,width=396,height=573),deco_rect=dict(height=25))
|
||||
self.assertEqual(assigned_origin(o),[-398,25]);self.assertEqual(exterior_ipc(o),[-400,0,400,600])
|
||||
def test_applied_origin_source_identity(self):
|
||||
o=dict(id=42,window_rect=dict(x=2,y=0),deco_rect=dict(height=25))
|
||||
origin,r,_=actual_origin(o,'PI_OWNER output=HEADLESS-1 ns=1 id=42 scene_valid=1 scene=-400,0 applied_size=400,600')
|
||||
self.assertEqual(origin,[-398,25])
|
||||
with self.assertRaises(Ambiguous):actual_origin(o,'PI_OWNER output=HEADLESS-1 ns=1 id=99 scene_valid=1 scene=-400,0 applied_size=400,600')
|
||||
def test_scale_once_pointer_roundtrip(self):
|
||||
o=RUNS['D2-mosaic']['outputs'][0]
|
||||
for p in ([-700,100],[-500,300],[-200,400]):
|
||||
self.assertEqual(png_global(global_png(p,o,'logical'),o,'logical'),p)
|
||||
def test_rotated_pointer_roundtrip(self):
|
||||
o=RUNS['D3-mosaic']['outputs'][0]
|
||||
for p in ([-700,100],[-500,300],[-200,400]):self.assertEqual(png_global(global_png(p,o,'physical90'),o,'physical90'),p)
|
||||
def test_unknown_frame_reject(self):
|
||||
with self.assertRaises(Ambiguous):png_global([3,5],RUNS['D2-mosaic']['outputs'][0],'guessed')
|
||||
def test_marker_actual_orientation_and_missing_reject(self):
|
||||
im=Image.new('RGB',(1000,750));d=ImageDraw.Draw(im)
|
||||
for k,b in [('TL',(10,10,40,40)),('TR',(950,10,980,40)),('BL',(10,700,40,730)),('BR',(950,700,980,730)),('C',(480,350,510,380))]:d.rectangle(b,fill=MARKERS[k])
|
||||
self.assertEqual(marker_frame(im,1.25,False)['frame'],'logical')
|
||||
self.assertEqual(marker_frame(im.transpose(Image.Transpose.ROTATE_270),1.25,True)['frame'],'physical90')
|
||||
im.putpixel((10,10),(0,0,0));blank=Image.new('RGB',im.size)
|
||||
with self.assertRaises(Ambiguous):marker_frame(blank,1.25,True)
|
||||
def test_exact_rgb_one_pixel_reject(self):
|
||||
a=Image.new('RGB',(800,600),(48,48,56));b=a.copy();b.putpixel((0,599),(48,48,55))
|
||||
with self.assertRaises(Ambiguous):exact(a,b)
|
||||
def test_output_physical_logical_scale_mismatch_reject(self):
|
||||
spec=RUNS['D3-mosaic']['outputs'];outputs=[]
|
||||
for o in spec:outputs.append(dict(active=True,name=o['name'],rect=dict(zip(('x','y','width','height'),o['logical'])),scale=o['scale'],transform=o['transform'],current_mode=dict(zip(('width','height'),o['mode']))))
|
||||
expected_outputs(outputs,spec)
|
||||
for key in ('scale','transform'):
|
||||
bad=copy.deepcopy(outputs);bad[0][key]=1 if key=='scale' else 90
|
||||
with self.assertRaises(Ambiguous):expected_outputs(bad,spec)
|
||||
def test_no_synthetic_saved_identity(self):
|
||||
r=RUNS['E2-mosaic'];self.assertTrue(any('saved-node' in x for x in r['ceilings']))
|
||||
code=Path(__file__).with_name('cases.py').read_text();self.assertIn("saved_node_identity='NOT OBSERVABLE'",code);self.assertNotIn('release_configure',code)
|
||||
def test_pairs_same_output_seq_only_and_missing(self):
|
||||
text='PI_OUTPUT_COMMIT output=H1 commit_seq=7 ns=1\nPI_OUTPUT_PRESENT output=H2 commit_seq=7 ns=2\n'
|
||||
a=parse_pairs(text);self.assertEqual(a['analyzed'][0]['status'],'MISSING_PRESENT')
|
||||
a=parse_pairs(text+'PI_OUTPUT_PRESENT output=H1 commit_seq=7 ns=3\n');self.assertEqual(a['analyzed'][0]['status'],'PAIRED')
|
||||
def test_duplicate_present_rejected(self):
|
||||
line='PI_OUTPUT_PRESENT output=H1 commit_seq=7 ns=2\n'
|
||||
with self.assertRaises(Ambiguous):parse_pairs(line+line)
|
||||
def test_pair_limit12(self):
|
||||
text=''.join(f'PI_OUTPUT_COMMIT output=H1 commit_seq={i} ns={i+1}\n' for i in range(30));r=parse_pairs(text)
|
||||
self.assertEqual(len(r['analyzed']),12);self.assertEqual(r['total_commits'],30)
|
||||
def test_owned_cleanup_and_explicit_IPC_source(self):
|
||||
tree=ast.parse(Path(__file__).with_name('engine.py').read_text());code=ast.unparse(tree)
|
||||
self.assertIn("['swaymsg', '-s', str(self.sock)",code);self.assertIn('start_new_session=True',code);self.assertIn('0o700',Path(__file__).with_name('engine.py').read_text())
|
||||
self.assertNotIn('shell=True',code);self.assertNotIn('os.killpg(0',code)
|
||||
def test_every_campaign_callable(self):
|
||||
from cases import CAMPAIGN
|
||||
self.assertEqual(set(CAMPAIGN),set(r['campaign'] for r in RUNS.values()))
|
||||
def test_cleanup_available_before_start(self):
|
||||
from engine import Session
|
||||
self.assertTrue(hasattr(Session,'start') and hasattr(Session,'cleanup'))
|
||||
def test_local_exhaustion_before_global(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
lab=Path(d);w=lab/'matrix/D1-mosaic';w.mkdir(parents=True)
|
||||
gate.exclusive(w/'actions.json',dict(planned={'capture':1},reserved={'capture':1},events=[]))
|
||||
with patch.object(gate,'LAB',lab),patch.object(gate.ledger,'reserve') as reserve:
|
||||
with self.assertRaises(ValueError):gate.action_debit('D1-mosaic','capture')
|
||||
reserve.assert_not_called()
|
||||
def test_corrupt_local_ledger_rejected(self):
|
||||
limits=gate.action_limits('D1-mosaic');state=dict(planned=limits,reserved=dict.fromkeys(limits,0),events=[])
|
||||
gate.local_valid('D1-mosaic',state)
|
||||
for kind in ('typed','reconciliation','limits'):
|
||||
bad=copy.deepcopy(state)
|
||||
if kind=='typed':bad['reserved']['capture']=True
|
||||
elif kind=='reconciliation':bad['reserved']['capture']=1
|
||||
else:bad['planned']['capture']+=1
|
||||
with self.assertRaises(ValueError):gate.local_valid('D1-mosaic',bad)
|
||||
def test_unknown_action_before_global(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
lab=Path(d);w=lab/'matrix/D1-mosaic';w.mkdir(parents=True)
|
||||
gate.exclusive(w/'actions.json',dict(planned={'capture':1},reserved={'capture':0},events=[]))
|
||||
with patch.object(gate,'LAB',lab):
|
||||
with self.assertRaises(ValueError):gate.action_debit('D1-mosaic','force_timer')
|
||||
|
||||
class RepairTests(unittest.TestCase):
|
||||
def fixture(self):
|
||||
run=gate.LAB/'horizontal/C2/run'
|
||||
trace=(run/'compositor.log').read_text();client=(run/'E2.log').read_text().split('POPUP_B DESTROY')[0]
|
||||
epoch=next(e for e in observed_epochs(trace,'HEADLESS-1',0,999999999999999999)
|
||||
if e['scene'] and e['scene'].get('layout')=='5' and len(e['roots'])==1)
|
||||
o=next(o for o in epoch['owners'] if o['id']=='9');scene=list(map(int,o['scene'].split(',')))+list(map(int,o['applied_size'].split(',')))
|
||||
x,y,w,h=map(int,o['current'].split(','));owner=dict(id=9,type='con',app_id='org.mosaic.NestedGrabProbe',visible=True,rect=dict(x=x,y=y+25,width=w,height=h-25),window_rect=dict(x=2,y=0,width=w-4,height=h-27),deco_rect=dict(height=25))
|
||||
ws=dict(id=6,type='workspace',layout='mosaic',output='HEADLESS-1',nodes=[owner],floating_nodes=[])
|
||||
tree=dict(id=1,type='root',nodes=[dict(id=2,type='output',name='HEADLESS-1',nodes=[ws],floating_nodes=[])],floating_nodes=[])
|
||||
state=dict(owner=owner,workspace=ws,scene=scene,origin=[scene[0]+2,scene[1]+25],snapshot=dict(tree=tree))
|
||||
raw='\n'.join(epoch['raw']);session=SimpleNamespace(outputs=[dict(name='HEADLESS-1',logical=[-800,0,800,600])],log=lambda n:raw if n=='compositor' else client)
|
||||
return session,state,raw
|
||||
def test_real_C_callback_positive(self):
|
||||
s,state,raw=self.fixture();self.assertEqual(root_state(s,state,state['origin'],0)['states'][0]['state'],'applied-target')
|
||||
def test_malformed_callbacks_rejected(self):
|
||||
s,state,raw=self.fixture()
|
||||
mutations=[raw.replace(' id=9 ',' id=99 '), '\n'.join(l for l in raw.splitlines() if 'PI_OWNER ' not in l),raw.replace('local_index=1','local_index=2'),raw.replace('workspace=6','workspace=99'),raw.replace('output=HEADLESS-1','output=HEADLESS-2'),raw+'\n'+next(l for l in raw.splitlines() if 'PI_POPUP ' in l),raw.replace('clip=-482,-25,800,600','clip=-481,-25,800,600')]
|
||||
# Mutate the observed root x regardless of this retained callback's origin.
|
||||
import re
|
||||
mutations.append(re.sub(r'(PI_POPUP .*clip=)(-?\d+)',lambda m:m[1]+str(int(m[2])+1),raw))
|
||||
for bad in mutations:
|
||||
with self.subTest(bad=bad[-120:]),self.assertRaises((ValueError,AssertionError)):
|
||||
s.log=lambda n:bad if n=='compositor' else (gate.LAB/'horizontal/C2/run/E2.log').read_text().split('POPUP_B DESTROY')[0];root_state(s,state,state['origin'],0)
|
||||
def test_missing_ancestry_and_origin_rejected(self):
|
||||
s,state,raw=self.fixture();bad=copy.deepcopy(state);bad['origin'][0]+=1
|
||||
with self.assertRaises(ValueError):root_state(s,bad,bad['origin'],0)
|
||||
bad=copy.deepcopy(state);bad['workspace']['id']=99
|
||||
with self.assertRaises(ValueError):root_state(s,bad,bad['origin'],0)
|
||||
def test_real_hierarchy_parent_and_missing_grab_rejected(self):
|
||||
s,state,raw=self.fixture();client=(gate.LAB/'horizontal/C2/run/E2.log').read_text().split('POPUP_B DESTROY')[0]
|
||||
for bad in (client.replace('parent_xdg=12','parent_xdg=999'),client.replace('POPUP_B GRAB_REQUEST','POPUP_B OMITTED_GRAB_REQUEST')):
|
||||
s.log=lambda n:raw if n=='compositor' else bad
|
||||
with self.assertRaises((ValueError,AssertionError)):root_state(s,state,state['origin'],0)
|
||||
def test_complete_nonbuffer_callback_preserved(self):
|
||||
s,state,raw=self.fixture();client=(gate.LAB/'horizontal/C2/run/E2.log').read_text().split('POPUP_B DESTROY')[0]
|
||||
s.log=lambda n:raw.replace('has_buffer=1','has_buffer=0') if n=='compositor' else client
|
||||
self.assertEqual(len(root_state(s,state,state['origin'],0)['states']),1)
|
||||
|
||||
def test_old_applied_hidden_return_and_stale_rejection(self):
|
||||
import re
|
||||
s,state,raw=self.fixture();hidden=copy.deepcopy(state)
|
||||
hidden['scene'][0]-=1600;hidden['origin'][0]-=1600;hidden['owner']['rect']['x']-=1600
|
||||
def shifted(text):
|
||||
text=re.sub(r'ns=(\d+)',lambda m:'ns='+str(int(m[1])+100000),text)
|
||||
text=re.sub(r'(PI_OWNER .*id=9 current=)(-?\d+)',lambda m:m[1]+str(int(m[2])-1600),text)
|
||||
text=re.sub(r'(PI_OWNER .*id=9 .*scene=)(-?\d+)',lambda m:m[1]+str(int(m[2])-1600),text)
|
||||
return re.sub(r'(PI_POPUP .*clip=)[^ ]+',r'\g<1>0,0,0,0',text)
|
||||
hidden_raw=shifted(raw);client=(gate.LAB/'horizontal/C2/run/E2.log').read_text().split('POPUP_B DESTROY')[0]
|
||||
s.log=lambda n:raw+'\n'+hidden_raw if n=='compositor' else client
|
||||
result=root_state(s,hidden,hidden['origin'],0,True,previous=state)
|
||||
self.assertEqual([r['state'] for r in result['states']],['old-applied','applied-target'])
|
||||
returned=re.sub(r'ns=(\d+)',lambda m:'ns='+str(int(m[1])+200000),raw)
|
||||
s.log=lambda n:hidden_raw+'\n'+returned if n=='compositor' else client
|
||||
self.assertEqual(len(root_state(s,state,state['origin'],0,previous=hidden)['states']),2)
|
||||
stale=re.sub(r'ns=(\d+)',lambda m:'ns='+str(int(m[1])+300000),hidden_raw)
|
||||
s.log=lambda n:hidden_raw+'\n'+returned+'\n'+stale if n=='compositor' else client
|
||||
with self.assertRaises(ValueError):root_state(s,state,state['origin'],0,previous=hidden)
|
||||
def test_earlier_bad_clip_then_good_rejected(self):
|
||||
import re
|
||||
s,state,raw=self.fixture();client=(gate.LAB/'horizontal/C2/run/E2.log').read_text().split('POPUP_B DESTROY')[0]
|
||||
bad=re.sub(r'(PI_POPUP .*clip=)(-?\d+)',lambda m:m[1]+str(int(m[2])+1),raw)
|
||||
good=re.sub(r'ns=(\d+)',lambda m:'ns='+str(int(m[1])+100000),raw)
|
||||
s.log=lambda n:bad+'\n'+good if n=='compositor' else client
|
||||
with self.assertRaises(ValueError):root_state(s,state,state['origin'],0)
|
||||
|
||||
def test_cleanup_preserves_actual_ipc_on_wait_timeout(self):
|
||||
from engine import Session
|
||||
import subprocess,time,threading
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
s=Session.__new__(Session);s.run=Path(d);s.runtime=s.run/'runtime';s.runtime.mkdir();s.sock=Mock();s.sock.is_socket.return_value=True;s.env={};s.pointer=None;s.handles=[];s.started=time.monotonic();s.expired=False;s.finished=threading.Event();s.watch=Mock()
|
||||
comp=Mock(pid=2147483647,returncode=0,stdin=None);comp.poll.side_effect=[None,0,0];comp.wait.side_effect=subprocess.TimeoutExpired('compositor',1);s.comp=comp;s.proc={'compositor':comp}
|
||||
with patch('engine.subprocess.run',return_value=SimpleNamespace(returncode=1,stdout='actual IPC stdout',stderr='actual IPC stderr')),patch('engine.os.killpg',side_effect=ProcessLookupError):
|
||||
self.assertEqual(s.cleanup('PASS'),'INCONCLUSIVE')
|
||||
terminal=json.loads((s.run/'terminal-ipc.json').read_text());out=json.loads((s.run/'outcome.json').read_text())
|
||||
self.assertEqual(terminal['ipc_exit'],1);self.assertEqual(terminal['stdout'],'actual IPC stdout');self.assertEqual(terminal['stderr'],'actual IPC stderr');self.assertIn('wait_error',terminal);self.assertEqual(out['measurements_status'],'PASS');self.assertEqual(out['processes'][0]['exit'],0)
|
||||
|
||||
class RotationTests(unittest.TestCase):
|
||||
def fixture(self):
|
||||
run=gate.LAB/'matrix/D2-mosaic/run';events=[json.loads(l) for l in (run/'events.jsonl').read_text().splitlines()]
|
||||
owner_start=next(e['ns'] for e in events if e['kind']=='spawn' and e['name']=='owner')
|
||||
trace=(run/'compositor.log').read_text()
|
||||
# Retain only the complete pre-owner native prefix for baseline. The
|
||||
# selected mapped epoch is from actual D2; it does not reclassify D2.
|
||||
# Later textual lines without ns could include cleanup: preserve the
|
||||
# prefix only, as live observation would do before owner creation.
|
||||
first_owner_line=next(i for i,l in enumerate(trace.splitlines()) if 'PI_OWNER ' in l and ' id=9 ' in l)
|
||||
before='\n'.join(trace.splitlines()[:first_owner_line])
|
||||
first_after=next(i for i,l in enumerate(before.splitlines()) if 'ns=' in l and int(__import__('re').search(r'ns=(\d+)',l)[1])>=owner_start)
|
||||
before='\n'.join(before.splitlines()[:first_after])
|
||||
session=SimpleNamespace(spec={'id':'D3-mosaic'},proc={'marker-background':Mock(pid=77)},events=[e for e in events if e['ns']<owner_start],log=lambda n:before,
|
||||
snapshot=lambda tag:json.loads((run/'startup-snapshot.json').read_text()),save=Mock())
|
||||
session.proc['marker-background'].poll.return_value=None
|
||||
with patch('cases.time.monotonic_ns',return_value=owner_start-1):baseline=marker_host_baseline(session)
|
||||
state=json.loads((run/'owner-geometry.json').read_text())
|
||||
epoch=next(e for e in observed_epochs(trace,'HEADLESS-1',0,999999999999999999) if e['scene'] and e['scene'].get('layout')=='5' and len(e['roots'])==2 and any(o.get('id')=='9' and list(map(int,o['scene'].split(',')))==state['scene'][:2] for o in e['owners']))
|
||||
raw='\n'.join(epoch['raw']);client=(run/'owner.log').read_text().split('POPUP_B DESTROY')[0].split('POPUP_B POPUP_DONE')[0]
|
||||
session.outputs=[dict(name='HEADLESS-1',logical=[-800,0,800,600])];session.log=lambda n:raw if n=='compositor' else client
|
||||
return session,state,raw,client,baseline
|
||||
def test_actual_D2_known_preowner_host_and_new_root(self):
|
||||
s,state,raw,client,baseline=self.fixture();result=root_state(s,state,state['origin'],baseline['window'][1])
|
||||
self.assertNotEqual(result['tree'],baseline['tree']);self.assertEqual(len(result['states'][0]['all_roots']),2)
|
||||
self.assertEqual(result['states'][0]['root']['local_index'],'2')
|
||||
def test_unknown_host_multiple_new_and_missing_known_rejected(self):
|
||||
s,state,raw,client,baseline=self.fixture();roots=observed_epochs(raw,'HEADLESS-1',0,999999999999999999)[0]['roots']
|
||||
for bad in ([roots[1]],roots+[dict(roots[1],tree='0xunknown',local_index='3')], [dict(roots[0],tree='0xunknown'),roots[1]], [dict(roots[0],clip_valid='1'),roots[1]], [roots[0],dict(roots[1],tree=roots[0]['tree'])]):
|
||||
with self.assertRaises(ValueError):associated_root(bad,baseline,baseline['window'][1]+1)
|
||||
with self.assertRaises(ValueError):associated_root(roots,baseline,baseline['window'][1])
|
||||
def test_wrong_owner_workspace_and_earlier_clip_rejected(self):
|
||||
import re
|
||||
s,state,raw,client,baseline=self.fixture()
|
||||
root=observed_epochs(raw,'HEADLESS-1',0,999999999999999999)[0]['roots'][1]
|
||||
badclip=raw.replace('clip='+root['clip'],'clip='+str(int(root['clip'].split(',')[0])+1)+','+','.join(root['clip'].split(',')[1:]))
|
||||
later=re.sub(r'ns=(\d+)',lambda m:'ns='+str(int(m[1])+100000),raw)
|
||||
for bad in (raw.replace(' id=9 ',' id=99 '),raw.replace('workspace=6','workspace=99'),badclip+'\n'+later):
|
||||
s.log=lambda n:bad if n=='compositor' else client
|
||||
with self.assertRaises(ValueError):root_state(s,state,state['origin'],baseline['window'][1])
|
||||
def test_v3_gate_disallows_every_old_case_before_debit(self):
|
||||
with patch.object(gate,'verify',return_value={}),patch.object(gate.ledger,'reserve') as reserve:
|
||||
for key in gate.RUNS:
|
||||
if key!='D3-mosaic':
|
||||
with self.assertRaisesRegex(ValueError,'only unused D3'):gate.preflight(key)
|
||||
reserve.assert_not_called()
|
||||
def test_executed_v2_archive_exact(self):
|
||||
archive=gate.LAB/'matrix/executed-v2-archive';f=gate.read(archive/'freeze.json');mapping=gate.read(archive/'archive-map.json')
|
||||
self.assertEqual(gate.sha(archive/'freeze.json'),'a20ccb39095ca35363119b5cfe4b5c839c1a75863bd72a097686aeac76dd7d7f')
|
||||
self.assertEqual(set(mapping),set(f['files']))
|
||||
for original,digest in f['files'].items():self.assertEqual(gate.sha(mapping[original]),digest,original)
|
||||
|
||||
if __name__=='__main__':
|
||||
p=argparse.ArgumentParser();p.add_argument('--output',type=Path,required=True);a=p.parse_args()
|
||||
suite=unittest.TestSuite([unittest.defaultTestLoader.loadTestsFromTestCase(c) for c in (Tests,RepairTests,RotationTests)]);names=[t.id() for group in suite for t in group];r=unittest.TextTestRunner(verbosity=2).run(suite)
|
||||
gate.exclusive(a.output,dict(status='PASS' if r.wasSuccessful() else 'FAIL',executed=r.testsRun,tests=names,failures=[(str(t),e) for t,e in r.failures],errors=[(str(t),e) for t,e in r.errors],runtime='NOT RUN',acceptance_created=False))
|
||||
raise SystemExit(not r.wasSuccessful())
|
||||
18
tooling/probes/step20c-overnight/qualification/README.md
Normal file
18
tooling/probes/step20c-overnight/qualification/README.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# STEP20C overnight — qualification documentaire
|
||||
|
||||
EXPERIMENTAL. Ce sous-répertoire contient seulement un index et des métadonnées compactes ; les modules et fixtures gelés du répertoire parent ne sont pas réécrits.
|
||||
|
||||
- [Provenance exacte](provenance.json) : pins, binaires, builds/stages, runs, budgets, freezes et références privées.
|
||||
- [Bilan et gate](../../../../docs/design/STEP20C_OVERNIGHT_QUALIFICATION_AND_INTEGRATION_GATE_V1.md).
|
||||
- [Reconstruction et stage](../../../../docs/design/STEP20C_ANNOTATED_REBUILD_AND_PRIVATE_STAGE.md).
|
||||
- [Matrice graphique](../../../../docs/design/STEP20C_ANNOTATED_GRAPHICS_MATRIX.md).
|
||||
- [Lifecycle, effets et ordre natif](../../../../docs/design/STEP20C_LIFECYCLE_EFFECTS_AND_NATIVE_ORDERING.md).
|
||||
- [Décision d’intégration draft](../../../../docs/design/STEP20C_PRODUCTION_INTEGRATION_DECISION_DRAFT.md).
|
||||
|
||||
Les sources de fixture restent dans le répertoire parent ; leur lecture et les manifestes gelés sont requis avant une reproduction en LAB neuf. Les références locales de provenance ne fournissent pas les archives, bibliothèques ou patches privés. Reproduction source contrôlée possible avec ces inputs retenus, archive autonome publique et reproductibilité binaire NOT QUALIFIED. Aucun raw log, PNG, binaire, archive, recette future ou fork complet n’est publié ici.
|
||||
|
||||
STEP20C NEXT ; production multi-output NOT YET SUPPORTED. Résultat proposé PRIVATE_CANDIDATE_NEEDS_TARGETED_EVIDENCE ; aucune décision appliquée. Revue finale ACCEPTED / audit PASS pour la fidélité du dossier expérimental, sans adoption production. Publication préparée, non exécutée à cette édition ; le reçu privé et le handoff final porteront les résultats/SHAs réels, sans futur hash inventé.
|
||||
|
||||
Les statuts NOT RUN/PENDING des documents de fixture gelés décrivent leur préparation avant lancement. Ils sont conservés sans réécriture ; les rapports de qualification ci-dessus et leurs reçus indépendants décrivent le résultat exécuté ultérieur. Les gates sont liés au LAB et aux bytes exacts de cette campagne : une nouvelle campagne exige préparation/identités/budget et revue distincts, sans copier une acceptation ni réutiliser les tokens consommés.
|
||||
|
||||
La revue finale compte 0 BLOCKING, 4 NON_BLOCKING et 3 FUTURE_SCOPE, avec 180 checks hors ligne et 15 négatifs sur preuves réelles. Ses hashes et le contexte de l’index scientifique post-exécution sont dans la provenance compacte. NB-01 complète huit listes de serials par lecture de leurs propres logs wire ; les lignes/hashes sources restent dans le reçu privé référencé. Les anciens manifestes/scellés sont préservés ; le delta documentaire après revue est enregistré séparément dans `parent/docs-post-final-delta.json`. Publication et gates production restent distincts.
|
||||
File diff suppressed because one or more lines are too long
11
tooling/probes/step20c-overnight/release_shape/README.md
Normal file
11
tooling/probes/step20c-overnight/release_shape/README.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# Fixture G privé — release-shape
|
||||
|
||||
Statut : **EXPERIMENTAL, prêt pour revue indépendante des octets gelés**. Le fixture reprend exactement le scénario C2 horizontal déjà préparé et remplace uniquement l’import du gate. Il cible le binaire G en stage statique, lié à son SHA256 `8063478b2d5948da3eb8b1a329978882dc1d045cf36932cc2145a78af20ba76a`, Build ID `83896fdb36f3fd21522becec0e8c5219e15f4b8e`, aux trois sources Mosaic de traces et à wlroots 0.20.2 SHA256 `7edca9a7d65855addda68e29ba5db94d57c803eb5fa7d0f86db3136b90ca9837`.
|
||||
|
||||
Le plan d’action reste celui de C2 : 2 chaînes, 4 grabs, 8 clics, 17 captures 800×600, 2 mouvements de caméra, une transition vers `splith`, et 10 mouvements du pointeur. Les prédicats C2 réutilisés vérifient le propriétaire et les popups root/enfant dans H2, la limitation visuelle et d’entrée, la fermeture/dismissal, le retour à la disposition standard, puis la restauration. Le fixture ne revendique aucun résultat graphique; `generic_author_generator_negatives` reste **NOT_PROVEN** puisque les preuves de ces négatifs n’ont pas été conservées.
|
||||
|
||||
Préparation hors ligne : depuis le dépôt, exécuter `python tooling/probes/step20c-overnight/release_shape/dispatch.py prepare`, puis `.../dispatch.py tests`, puis `.../dispatch.py freeze`. Le freeze couvre les entrées C2 réutilisées, le code et les imports G, les deux arbres source G, le patch et sa revue ACCEPTED, les reçus de build/stage/ELF, les clients/protocoles et les dépendances de référence.
|
||||
|
||||
Le préflight refuse de lancer tant que `release-shape/reviews/release-smoke-review.json` n’existe pas et ne lie pas exactement `smoke/freeze.json`, tous ses fichiers, le rapport de revue et le scope `OVERNIGHT_G_SMOKE_FINAL_BYTES`. Un refus préflight ne crée ni jeton one-shot, ni runtime, ni réservation. Après revue acceptée, la commande sera `python tooling/probes/step20c-overnight/release_shape/dispatch.py preflight`, puis `python tooling/probes/step20c-overnight/release_shape/dispatch.py run` depuis le répertoire du fixture. Le runner dure au plus 143 s; le reçu global réserve au plus 150 s. Chaque action est durable avant mutation et chaque capture réserve d’abord le compteur global.
|
||||
|
||||
Aucun build, stage ou smoke n’a été exécuté pour préparer ce fixture. Neutralité **NOT PROVEN**; le résultat ne transfère aucun résultat graphique de A, C, D ni de la matrice séparée.
|
||||
|
|
@ -0,0 +1 @@
|
|||
"""Private experimental fixture for the reviewed G headless smoke."""
|
||||
24
tooling/probes/step20c-overnight/release_shape/adapter.py
Normal file
24
tooling/probes/step20c-overnight/release_shape/adapter.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
"""Bounded G smoke derivation from the frozen accepted C2 fixture."""
|
||||
from pathlib import Path
|
||||
import ast
|
||||
import hashlib
|
||||
import sys
|
||||
|
||||
REPO = Path(__file__).resolve().parents[4]
|
||||
OVERNIGHT = REPO / 'tooling/probes/step20c-overnight'
|
||||
BASE = REPO / 'tooling/probes/step20c-static-e2-completion'
|
||||
sys.path.insert(0, str(OVERNIGHT))
|
||||
from horizontal_adapter import generate, replace_one
|
||||
|
||||
IMPORT = 'from horizontal_gate import begin_run, debit, binding_sources, finish_run'
|
||||
G_IMPORT = 'from gate import begin_run, debit, binding_sources, finish_run'
|
||||
|
||||
def generate_g():
|
||||
text = generate('C2')
|
||||
text = replace_one(text, IMPORT, G_IMPORT)
|
||||
ast.parse(text, filename='G-smoke/runner.py')
|
||||
return text
|
||||
|
||||
if __name__ == '__main__':
|
||||
runner = generate_g()
|
||||
print('G_RUNNER_SHA256', hashlib.sha256(runner.encode()).hexdigest())
|
||||
91
tooling/probes/step20c-overnight/release_shape/dispatch.py
Normal file
91
tooling/probes/step20c-overnight/release_shape/dispatch.py
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
"""Prepare, freeze, check, or explicitly preflight G; RUN needs review acceptance."""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
HERE=Path(__file__).resolve().parent
|
||||
OVERNIGHT=HERE.parent
|
||||
sys.path[:0]=[str(HERE),str(OVERNIGHT)]
|
||||
from adapter import generate_g
|
||||
import gate
|
||||
|
||||
def prepare():
|
||||
w=gate.WORK
|
||||
os.chmod(w,0o700);os.chmod(w/'results',0o700)
|
||||
if (w/'run').exists() or (w/'consumed.json').exists() or (w/'freeze.json').exists():
|
||||
raise RuntimeError('G workspace already consumed/frozen; no replacement or retry')
|
||||
for name in ('nested-client','pointer-daemon','static-reference'):
|
||||
p=w/name
|
||||
if not p.is_symlink() or p.resolve()!=(gate.LAB/'horizontal/clients'/name).resolve():
|
||||
raise RuntimeError('C2 client symlink must reference exact compiled fixture: '+name)
|
||||
# Bind complete reused C2 runner source and its accepted input before writing the generated G copy.
|
||||
c2=gate.LAB/'horizontal/C2/runner.py'
|
||||
from horizontal_adapter import generate
|
||||
if c2.read_text()!=generate('C2'):
|
||||
raise RuntimeError('frozen C2 fixture source mismatch')
|
||||
runner=generate_g()
|
||||
fd=os.open(w/'runner.py',os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
|
||||
with os.fdopen(fd,'w') as stream:
|
||||
stream.write(runner);stream.flush();os.fsync(stream.fileno())
|
||||
dfd=os.open(w,os.O_RDONLY|os.O_DIRECTORY)
|
||||
try:os.fsync(dfd)
|
||||
finally:os.close(dfd)
|
||||
print('G_SMOKE_RUNNER_PREPARED runtime NOT RUN')
|
||||
|
||||
def freeze():
|
||||
w=gate.WORK
|
||||
if (w/'freeze.json').exists(): raise RuntimeError('freeze already exists; no replacement')
|
||||
tests=json.loads((w/'results/prepare-tests.json').read_text())
|
||||
if tests.get('status')!='PASS': raise RuntimeError('offline fixture tests not PASS')
|
||||
files=gate.expected_files()
|
||||
# Verify all entries still exist and bind the current persistent budget without making it a post-run immutable input.
|
||||
budget_sha=gate.sha(gate.LAB/'budget.json')
|
||||
data={'status':'READY_FOR_REVIEW','scope':'OVERNIGHT_G_SMOKE_FINAL_BYTES',
|
||||
'files':files,'budget_before_run_sha256':budget_sha,
|
||||
'binary':{'path':str(gate.BINARY),'sha256':gate.BINARY_SHA,'build_id':gate.BUILD_ID},
|
||||
'source_bindings':gate.TRACE_SOURCES,
|
||||
'plan':gate.PLANS,'graphics':'NOT TESTED','neutrality':'NOT PROVEN',
|
||||
'generic_author_generator_negatives':'NOT_PROVEN: mechanism and execution receipt not retained',
|
||||
'runtime':'NOT RUN'}
|
||||
gate.exclusive(w/'freeze.json',data)
|
||||
# The persisted freeze is read back and checked before report.
|
||||
gate._frozen_files()
|
||||
print('G_SMOKE_FREEZE_READY_FOR_INDEPENDENT_REVIEW')
|
||||
|
||||
def missing_review_preflight():
|
||||
before=(gate.LAB/'budget.json').read_bytes()
|
||||
token=gate.WORK/'consumed.json';run=gate.WORK/'run'
|
||||
try:gate.preflight(require_review=True)
|
||||
except Exception as exc:
|
||||
refusal={'status':'REFUSED','reason':str(exc),'review_missing':not (gate.GROOT/'reviews/release-smoke-review.json').exists(),
|
||||
'budget_unchanged':(gate.LAB/'budget.json').read_bytes()==before,
|
||||
'consumed_token_created':token.exists(),'runtime_created':run.exists()}
|
||||
if not refusal['review_missing'] or not refusal['budget_unchanged'] or refusal['consumed_token_created'] or refusal['runtime_created']:
|
||||
raise RuntimeError('missing-review refusal side-effect invariant failed') from exc
|
||||
out=gate.WORK/'results/missing-review-preflight.json'
|
||||
fd=os.open(out,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
|
||||
with os.fdopen(fd,'w') as stream:json.dump(refusal,stream,indent=2);stream.write('\n');stream.flush();os.fsync(stream.fileno())
|
||||
print(json.dumps(refusal,indent=2));return 2
|
||||
print(json.dumps({'status':'PASS','scope':'reviewed G bytes only','binary':str(gate.BINARY),
|
||||
'binary_sha256':gate.BINARY_SHA,'build_id':gate.BUILD_ID,
|
||||
'runtime':'NOT RUN','token_created':False},indent=2))
|
||||
return 0
|
||||
|
||||
def main():
|
||||
p=argparse.ArgumentParser();p.add_argument('action',choices=('prepare','tests','freeze','preflight','run'));a=p.parse_args()
|
||||
if a.action=='prepare':prepare()
|
||||
elif a.action=='tests':
|
||||
import subprocess
|
||||
return subprocess.run([sys.executable,str(HERE/'tests.py'),'--output',str(gate.WORK/'results/prepare-tests.json')],check=False).returncode
|
||||
elif a.action=='freeze':freeze()
|
||||
elif a.action=='preflight':return missing_review_preflight()
|
||||
else:
|
||||
gate.preflight(require_review=True)
|
||||
runner=gate.WORK/'runner.py'
|
||||
if runner.read_text()!=generate_g():raise RuntimeError('G runner differs from freeze generator')
|
||||
sys.path[:0]=[str(HERE),str(OVERNIGHT),str(gate.BASE)]
|
||||
exec(compile(runner.read_text(),str(runner),'exec'),{'__name__':'__main__','__file__':str(runner)})
|
||||
return 0
|
||||
if __name__=='__main__':raise SystemExit(main())
|
||||
333
tooling/probes/step20c-overnight/release_shape/gate.py
Normal file
333
tooling/probes/step20c-overnight/release_shape/gate.py
Normal file
|
|
@ -0,0 +1,333 @@
|
|||
"""Fail-closed, one-shot gate for the private G release-shape smoke."""
|
||||
import fcntl
|
||||
from collections import Counter
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
REPO = HERE.parents[3]
|
||||
OVERNIGHT = REPO / 'tooling/probes/step20c-overnight'
|
||||
BASE = REPO / 'tooling/probes/step20c-static-e2-completion'
|
||||
LAB = Path('/home/fy59/swayfx-mosaic-overnight-mcolx0gq')
|
||||
GROOT = LAB / 'release-shape'
|
||||
WORK = GROOT / 'smoke'
|
||||
CASE = 'G-smoke'
|
||||
BINARY = GROOT / 'stage-static/usr/bin/swayfx-mosaic'
|
||||
BINARY_SHA = '8063478b2d5948da3eb8b1a329978882dc1d045cf36932cc2145a78af20ba76a'
|
||||
BUILD_ID = '83896fdb36f3fd21522becec0e8c5219e15f4b8e'
|
||||
WLROOTS = Path('/usr/lib/libwlroots-0.20.so').resolve()
|
||||
WLROOTS_SHA = '7edca9a7d65855addda68e29ba5db94d57c803eb5fa7d0f86db3136b90ca9837'
|
||||
PATCH_SHA = '6e8a5b50149732ac70ecd79ab8db9258956f59036aba1bf17a477866bc44a5c1'
|
||||
REVIEW = LAB / 'reviews/release-patch-review.json'
|
||||
REVIEW_SHA = '29e7ea8b9124fae59a231ff6abcab3981e1ac9c697da51c05c334e888970897a'
|
||||
BUILD_JSON = GROOT / 'results/phase-g-build.json'
|
||||
PLANS = dict(chain=2, grab=4, click=8, capture=17, camera=2, transition=1, move=10)
|
||||
# Contract: the move budget counts pointer relocations, including the eight
|
||||
# click injections; the event trace therefore has only two standalone `move`
|
||||
# actions plus eight `left`/`right` click actions.
|
||||
|
||||
# The three Mosaic source files whose G runtime traces bind the smoke.
|
||||
TRACE_SOURCES = {
|
||||
str(GROOT / 'mosaic-source/sway/desktop/output.c'): 'f703fab0828ee886ce690acfa727a1f70d0dad5f9ec1f5c153db646c73d10485',
|
||||
str(GROOT / 'mosaic-source/sway/desktop/transaction.c'): '1bb30b3f171f680f8b2e6ed28aa7aad0a6854d8d77360ac8b8e314918c0982f2',
|
||||
str(GROOT / 'mosaic-source/sway/desktop/xdg_shell.c'): '123313e80fbf5d55a8e9bf2edca5a0f15a7601643a5af2904da6ba9b3914d29f',
|
||||
}
|
||||
|
||||
def sha(path):
|
||||
return hashlib.sha256(Path(path).read_bytes()).hexdigest()
|
||||
|
||||
def read(path):
|
||||
return json.loads(Path(path).read_text())
|
||||
|
||||
def require(ok, message):
|
||||
if not ok:
|
||||
raise ValueError(message)
|
||||
|
||||
def validate_binary_identity(path, digest):
|
||||
require(Path(path).resolve() == BINARY.resolve() and digest == BINARY_SHA and sha(path) == BINARY_SHA, 'only exact G ELF accepted')
|
||||
|
||||
def validate_budget_room(state):
|
||||
sys.path.insert(0, str(OVERNIGHT))
|
||||
import ledger
|
||||
ledger._validate(state)
|
||||
require(CASE not in state['runs'] and state['used']['run'] < ledger.LIMITS['run'], 'run quota/unique id')
|
||||
require(state['campaign_used']['G'] < ledger.CAMPAIGNS['G'], 'G campaign quota')
|
||||
require(state['used']['capture'] + PLANS['capture'] <= ledger.LIMITS['capture'], 'global PNG quota')
|
||||
require(all(state['used'][k] == ledger.LIMITS[k] for k in ('scene_build','mosaic_build','stage')), 'G smoke must not extend exhausted build/stage budget')
|
||||
|
||||
def validate_wlroots(path, digest):
|
||||
require(Path(path).resolve()==WLROOTS and digest==WLROOTS_SHA and sha(path)==WLROOTS_SHA, 'wlroots exact bytes')
|
||||
|
||||
def validate_file_map(files):
|
||||
require(isinstance(files,dict) and bool(files), 'frozen file map missing')
|
||||
for path,digest in files.items():
|
||||
require(Path(path).is_file() and sha(path)==digest, 'frozen input changed: '+path)
|
||||
|
||||
def validate_one_shot(work):
|
||||
work=Path(work)
|
||||
require(not (work/'run').exists() and not (work/'consumed.json').exists(), 'G smoke is one-shot; no retry')
|
||||
|
||||
def validate_capture_batch(work, captures):
|
||||
from PIL import Image
|
||||
require(len(captures)==17 and len({row.get('file') for row in captures})==17, 'incomplete/duplicate G capture batch')
|
||||
expected={'H2-reference-1.png','H2-reference-2.png','E1-top.png','E1-A.png','E1-mosaic-H1.png','E1-mosaic-H2.png','E1-dismissed-H2.png','E2-top.png','E2-A.png','E2-mosaic-H1.png','E2-mosaic-H2.png','standard-H1.png','standard-H2.png','standard-A-H1.png','standard-A-H2.png','final-H2.png','final-H1.png'}
|
||||
require({row.get('file') for row in captures}==expected, 'G capture frame set incomplete')
|
||||
for row in captures:
|
||||
path=Path(work)/'run'/row['file']
|
||||
require(path.is_file() and sha(path)==row['sha256'] and row.get('exit')==0, 'G capture bytes changed')
|
||||
with Image.open(path) as image: require(image.size==(800,600), 'G capture frame dimensions')
|
||||
|
||||
def validate_window_results(work):
|
||||
names=('grab-serials.json','xdg-protocol-order.json','popup-root-geometry.json','popup-nested-geometry.json','applied-clips.json','mosaic-pixels.json','standard-return.json','standard-popup-input.json','final-restoration.json','cleanup.json','terminal-exit.json')
|
||||
missing=[name for name in names if not (Path(work)/'results'/name).is_file()]
|
||||
require(not missing, 'G window/lifecycle evidence incomplete: '+','.join(missing))
|
||||
|
||||
def durable(path, data):
|
||||
path = Path(path)
|
||||
tmp = path.with_name('.' + path.name + '.tmp-' + str(os.getpid()))
|
||||
fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
|
||||
try:
|
||||
with os.fdopen(fd, 'w') as stream:
|
||||
json.dump(data, stream, indent=2); stream.write('\n'); stream.flush(); os.fsync(stream.fileno())
|
||||
os.replace(tmp, path)
|
||||
dfd = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY)
|
||||
try: os.fsync(dfd)
|
||||
finally: os.close(dfd)
|
||||
finally:
|
||||
if tmp.exists(): tmp.unlink()
|
||||
|
||||
def exclusive(path, data):
|
||||
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
|
||||
with os.fdopen(fd, 'w') as stream:
|
||||
json.dump(data, stream, indent=2); stream.write('\n'); stream.flush(); os.fsync(stream.fileno())
|
||||
dfd = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY)
|
||||
try: os.fsync(dfd)
|
||||
finally: os.close(dfd)
|
||||
|
||||
def source_bindings(auth):
|
||||
rows = auth['trace_sources']
|
||||
require(rows == TRACE_SOURCES, 'G source binding set changed')
|
||||
require(all(sha(path) == digest for path, digest in rows.items()), 'G source hash changed')
|
||||
return {Path(path).name: digest for path, digest in rows.items()}
|
||||
|
||||
def _check_build_identity():
|
||||
build = read(BUILD_JSON)
|
||||
require(build['status'] == 'BUILDABLE', 'G build receipt status')
|
||||
require(build['source']['accepted_review'] == 'ACCEPTED', 'G accepted patch review')
|
||||
require(build['source']['patch_sha256'] == PATCH_SHA, 'G patch identity')
|
||||
require(build['source']['review_json_sha256'] == REVIEW_SHA, 'G review receipt binding')
|
||||
require(build['source']['release_manifest_sha256'] == sha(GROOT/'release-manifest.json'), 'G release manifest receipt binding')
|
||||
manifest=read(GROOT/'release-manifest.json')
|
||||
require(manifest['patch_sha256']==PATCH_SHA and manifest['result_source_hashes'], 'G release manifest identity')
|
||||
for rel,digest in manifest['result_source_hashes'].items(): require(sha(GROOT/rel)==digest, 'G release source changed: '+rel)
|
||||
require(build['elf']['sha256'] == BINARY_SHA and build['elf']['build_id'] == BUILD_ID, 'G ELF receipt identity')
|
||||
require(Path(build['elf']['path']).resolve() == BINARY.resolve(), 'G stage path identity')
|
||||
require(build['headless_runtime'] == 'NOT RUN' and build['graphics'] == 'NOT TESTED', 'no inherited G runtime result')
|
||||
validate_binary_identity(BINARY, BINARY_SHA)
|
||||
readelf = subprocess.run(['readelf', '-n', str(BINARY)], capture_output=True, text=True, timeout=5)
|
||||
require(readelf.returncode == 0 and BUILD_ID in readelf.stdout, 'G ELF build ID bytes')
|
||||
validate_wlroots(WLROOTS, WLROOTS_SHA)
|
||||
review = read(REVIEW)
|
||||
require(sha(REVIEW) == REVIEW_SHA and review['status'] == 'ACCEPTED' and review['independent'] is True, 'accepted exact G patch review')
|
||||
require(review['patch_sha256'] == PATCH_SHA, 'reviewed G patch digest')
|
||||
require(sha(review['report_file']) == review['report_sha256'] == '52e0db6963c44d6e17eab92404c9a882ca03a0bbae9d59ff15c43bdfb0aab0b8', 'accepted patch review report bytes')
|
||||
plan=read(LAB/'reviews/release-plan-review.json')
|
||||
require(plan.get('status')=='ACCEPTED_PLAN_ONLY' and plan.get('plan_sha256')=='b64201aa3f6a51c385ca8f6934ad20c07ea3853b5a08766e2ae174f5da229547', 'accepted plan binding')
|
||||
return build
|
||||
|
||||
def _frozen_files():
|
||||
freeze = read(WORK / 'freeze.json')
|
||||
require(freeze.get('status') == 'READY_FOR_REVIEW' and isinstance(freeze.get('files'), dict), 'G smoke freeze missing')
|
||||
files = freeze['files']
|
||||
require(files == expected_files(), 'G freeze file set changed')
|
||||
validate_file_map(files)
|
||||
return freeze
|
||||
|
||||
def expected_files():
|
||||
"""Complete inputs: C2 closure plus G sources/build/patch and fixture code."""
|
||||
c2 = read(LAB / 'horizontal/freeze.json')
|
||||
rows = dict(c2['files'])
|
||||
# Reused runtime modules and their sealed generator/input closure.
|
||||
for path in [HERE/'adapter.py', HERE/'gate.py', HERE/'dispatch.py', HERE/'tests.py',
|
||||
HERE/'README.md', HERE/'__init__.py',
|
||||
OVERNIGHT/'horizontal_adapter.py', OVERNIGHT/'horizontal_gate.py',
|
||||
OVERNIGHT/'ledger.py', BASE/'run.py', BASE/'client-contract.json',
|
||||
*(BASE/n for n in ('model.py','observations.py','ipc_predicates.py','pixels.py','predicates.py','lifecycle.py','transition.py','support.py')),
|
||||
WORK/'runner.py', WORK/'results/prepare-tests.json']:
|
||||
rows[str(path)] = sha(path)
|
||||
# Bind every source input of the G SceneFX and Mosaic trees.
|
||||
for root in (GROOT/'scene-source', GROOT/'mosaic-source'):
|
||||
for path in root.rglob('*'):
|
||||
if path.is_file() and '.git' not in path.parts:
|
||||
rows[str(path)] = sha(path)
|
||||
fixed = [
|
||||
REVIEW, LAB/'reviews/release-patch-review.md',
|
||||
GROOT/'release-removal.patch', GROOT/'release-manifest.json',
|
||||
LAB/'parent/release-plan.json', LAB/'reviews/release-plan-review.json',
|
||||
LAB/'reviews/release-plan-review.md', BUILD_JSON, GROOT/'REPORT-BUILD.md',
|
||||
GROOT/'results/static-stage-manifest.json', GROOT/'results/binary-elf.json',
|
||||
GROOT/'raw/phase-g-commands.json',
|
||||
GROOT/'scene-build/meson-info/intro-buildoptions.json',
|
||||
GROOT/'mosaic-build/meson-info/intro-buildoptions.json',
|
||||
GROOT/'mosaic-build/meson-info/intro-targets.json',
|
||||
LAB/'horizontal/identity.json', LAB/'horizontal/compile.json',
|
||||
LAB/'horizontal/protocol-scanner.json', LAB/'horizontal/offline-tests-repair-v1.json',
|
||||
LAB/'horizontal/C2/runner.py',
|
||||
Path('/usr/lib/libwlroots-0.20.so').resolve(), BINARY,
|
||||
]
|
||||
# The offline completion-predicate test consumes this immutable, completed
|
||||
# C2 evidence. Freeze the actual events and the records that validate them.
|
||||
c2work=LAB/'horizontal/C2'
|
||||
c2_evidence=[c2work/'actions.json',c2work/'run/events.jsonl',
|
||||
c2work/'results/captures.json',c2work/'results/clicks.json',c2work/'results/runtime.json']
|
||||
c2_evidence.extend(c2work/'results'/name for name in (
|
||||
'grab-serials.json','xdg-protocol-order.json','popup-root-geometry.json',
|
||||
'popup-nested-geometry.json','applied-clips.json','mosaic-pixels.json',
|
||||
'standard-return.json','standard-popup-input.json','final-restoration.json',
|
||||
'cleanup.json','terminal-exit.json'))
|
||||
c2_evidence.extend(c2work/'run'/name for name in (
|
||||
'H2-reference-1.png','H2-reference-2.png','E1-top.png','E1-A.png',
|
||||
'E1-mosaic-H1.png','E1-mosaic-H2.png','E1-dismissed-H2.png','E2-top.png',
|
||||
'E2-A.png','E2-mosaic-H1.png','E2-mosaic-H2.png','standard-H1.png',
|
||||
'standard-H2.png','standard-A-H1.png','standard-A-H2.png','final-H2.png','final-H1.png'))
|
||||
fixed.extend(c2_evidence)
|
||||
fixed.extend(path for info in (GROOT/'scene-build/meson-info',GROOT/'mosaic-build/meson-info') for path in info.iterdir() if path.is_file())
|
||||
for path in fixed:
|
||||
require(path.is_file(), 'required G freeze input missing: ' + str(path))
|
||||
rows[str(path)] = sha(path)
|
||||
# Complete DESTDIR identity, including the four separate future notices.
|
||||
for path in (GROOT/'stage-static').rglob('*'):
|
||||
if path.is_file(): rows[str(path)] = sha(path)
|
||||
# Exact C17 fixture binaries and generated protocol sources are also present through C2 closure.
|
||||
return dict(sorted(rows.items()))
|
||||
|
||||
def verify_review():
|
||||
freeze = _frozen_files()
|
||||
freeze_sha = sha(WORK/'freeze.json')
|
||||
review_path = GROOT/'reviews/release-smoke-review.json'
|
||||
require(review_path.is_file(), 'independent G smoke byte review missing')
|
||||
review = read(review_path)
|
||||
require(review.get('status') == 'ACCEPTED' and review.get('scope') == 'OVERNIGHT_G_SMOKE_FINAL_BYTES', 'G smoke review scope/status')
|
||||
require(review.get('independent') is True and review.get('read_only') is True and review.get('blockers') == [], 'G smoke review independence/blockers')
|
||||
require(review.get('freeze_sha256') == freeze_sha and review.get('files') == freeze['files'], 'G smoke review bindings')
|
||||
require(Path(review['report_file']).is_file() and sha(review['report_file']) == review['report_sha256'], 'G smoke report bytes')
|
||||
return review
|
||||
|
||||
def preflight(require_review=True):
|
||||
require(sys.flags.optimize == 0, 'assertions disabled')
|
||||
require(LAB.is_dir() and LAB.stat().st_uid == os.getuid() and stat.S_IMODE(LAB.stat().st_mode) == 0o700, 'registered owned LAB mode')
|
||||
require(WORK.is_dir() and WORK.stat().st_uid == os.getuid(), 'G smoke workspace owner')
|
||||
validate_one_shot(WORK)
|
||||
require((WORK/'results').is_dir(), 'G results directory')
|
||||
require((WORK/'results/clients').is_symlink() and (WORK/'results/clients').resolve()==(LAB/'horizontal/clients').resolve(), 'results client link changed')
|
||||
for name in ('nested-client','pointer-daemon','static-reference'):
|
||||
p=WORK/name
|
||||
require(p.is_symlink() and p.resolve()==(LAB/'horizontal/clients'/name).resolve(), 'C2 client symlink changed: '+name)
|
||||
for proc in Path('/proc').iterdir():
|
||||
if proc.name.isdigit():
|
||||
try: exe=(proc/'exe').resolve(strict=True)
|
||||
except (FileNotFoundError, PermissionError, ProcessLookupError): continue
|
||||
require(exe != BINARY.resolve(), 'G binary already running')
|
||||
build = _check_build_identity()
|
||||
sys.path.insert(0, str(OVERNIGHT)); import ledger
|
||||
validate_budget_room(read(LAB/'budget.json'))
|
||||
freeze = _frozen_files()
|
||||
# Refuse inherited A bytes and any other ELF even if it is executable.
|
||||
require(freeze['files'].get(str(BINARY)) == BINARY_SHA, 'G ELF absent from freeze')
|
||||
require(all(sha(path)==digest for path,digest in TRACE_SOURCES.items()), 'G runtime sources changed')
|
||||
from adapter import generate_g
|
||||
require((WORK/'runner.py').read_text() == generate_g(), 'generated G C2 runner changed')
|
||||
if require_review: verify_review()
|
||||
return dict(binary=str(BINARY), binary_sha256=BINARY_SHA, build_id=BUILD_ID,
|
||||
wlroots_sha256=WLROOTS_SHA, trace_sources=TRACE_SOURCES,
|
||||
pristine=dict(path=str(LAB/'stage-static/usr/bin/swayfx-mosaic'), sha256='979dcd95ab43fa1e465d6e9d262485b73d186cedacd0f957059dd24f482ce6f4'))
|
||||
|
||||
def begin_run():
|
||||
auth = preflight(require_review=True)
|
||||
token = WORK/'consumed.json'
|
||||
exclusive(token, dict(case=CASE, freeze_sha256=sha(WORK/'freeze.json'), consumed_at_ns=time.time_ns()))
|
||||
# The durable unique token comes before the locked global ledger reservation.
|
||||
sys.path.insert(0, str(OVERNIGHT))
|
||||
import ledger
|
||||
ledger.reserve(LAB/'budget.json', 'run', campaign='G', run_id=CASE, duration_s=150)
|
||||
exclusive(WORK/'actions.json', dict(planned=PLANS, reserved=dict.fromkeys(PLANS,0), events=[]))
|
||||
return LAB, CASE, WORK, auth
|
||||
|
||||
def debit(lab, case, work, *kinds):
|
||||
require(Path(lab).resolve()==LAB and case==CASE and Path(work).resolve()==WORK, 'G action binding')
|
||||
require(kinds and all(k in PLANS for k in kinds), 'unknown G action')
|
||||
lockpath=WORK/'actions.lock'
|
||||
with lockpath.open('a+') as lock:
|
||||
fcntl.flock(lock, fcntl.LOCK_EX)
|
||||
state=read(WORK/'actions.json')
|
||||
require(state.get('planned')==PLANS and set(state.get('reserved',{}))==set(PLANS), 'G action ledger schema')
|
||||
require(len(state.get('events',[]))==sum(state['reserved'].values()), 'G action ledger count mismatch')
|
||||
require(dict(Counter(e.get('kind') for e in state['events']))=={k:v for k,v in state['reserved'].items() if v}, 'G action event/count mismatch')
|
||||
for i,event in enumerate(state['events']):
|
||||
require(event.get('sequence')==i and event.get('kind') in PLANS and type(event.get('ns')) is int, 'G action event schema')
|
||||
require(all(state['reserved'][k]+kinds.count(k)<=PLANS[k] for k in kinds), 'G local action quota')
|
||||
for kind in kinds:
|
||||
if kind=='capture':
|
||||
sys.path.insert(0,str(OVERNIGHT));import ledger
|
||||
ledger.reserve(LAB/'budget.json','capture',run_id=CASE)
|
||||
state['reserved'][kind]+=1
|
||||
state['events'].append(dict(sequence=len(state['events']),kind=kind,ns=time.monotonic_ns()))
|
||||
durable(WORK/'actions.json',state)
|
||||
|
||||
def binding_sources(auth):
|
||||
require(auth['binary']==str(BINARY) and auth['binary_sha256']==BINARY_SHA, 'G binary source binding')
|
||||
return source_bindings(auth)
|
||||
|
||||
def finish_predicate(state, events, captures, clicks, runtime, capture_ok, window_ok):
|
||||
"""Evaluate the real completion gate over recorded action and runtime evidence."""
|
||||
counts=dict(Counter(e.get('kind') for e in state.get('events',[])))
|
||||
ledger_ok=(state.get('planned')==PLANS and set(state.get('reserved',{}))==set(PLANS)
|
||||
and counts=={k:v for k,v in state.get('reserved',{}).items() if v}
|
||||
and all(type(v) is int and 0<=v<=PLANS.get(k,-1) for k,v in state.get('reserved',{}).items()))
|
||||
event_capture_ok=sum(e.get('kind')=='capture' for e in events)==PLANS['capture']
|
||||
injections=[e for e in events if e.get('kind')=='injection']
|
||||
click_actions=[e for e in injections if e.get('action') in ('left','right')]
|
||||
standalone_moves=[e for e in injections if e.get('action')=='move']
|
||||
injection_ok=(len(injections)==PLANS['click']+2
|
||||
and len(click_actions)==PLANS['click']
|
||||
and len(standalone_moves)==2
|
||||
and len(click_actions)+len(standalone_moves)==len(injections))
|
||||
grabs=sum(e.get('grab') is True for e in injections)
|
||||
complete=(ledger_ok and state.get('reserved')==PLANS and capture_ok and window_ok
|
||||
and event_capture_ok and injection_ok and grabs==PLANS['grab']
|
||||
and len(clicks)==PLANS['click'] and runtime.get('status')=='PASS')
|
||||
return dict(complete=complete, action_ledger_valid=ledger_ok,
|
||||
event_capture_count_valid=event_capture_ok, injections=len(injections),
|
||||
click_injections=len(click_actions), standalone_move_injections=len(standalone_moves),
|
||||
pointer_counts_valid=injection_ok, grabs=grabs)
|
||||
|
||||
def finish_run(lab, case, work):
|
||||
verify_review()
|
||||
require(Path(lab).resolve()==LAB and case==CASE and Path(work).resolve()==WORK, 'G finish binding')
|
||||
state=read(WORK/'actions.json')
|
||||
events_path=WORK/'run/events.jsonl'
|
||||
events=[json.loads(line) for line in events_path.read_text().splitlines()] if events_path.is_file() else []
|
||||
captures=read(WORK/'results/captures.json') if (WORK/'results/captures.json').is_file() else []
|
||||
clicks=read(WORK/'results/clicks.json') if (WORK/'results/clicks.json').is_file() else []
|
||||
runtime=read(WORK/'results/runtime.json') if (WORK/'results/runtime.json').is_file() else {'status':'INCONCLUSIVE','failure':'runtime result missing'}
|
||||
try: validate_capture_batch(WORK,captures);capture_ok=True
|
||||
except Exception: capture_ok=False
|
||||
try: validate_window_results(WORK);window_ok=True
|
||||
except Exception: window_ok=False
|
||||
predicate=finish_predicate(state,events,captures,clicks,runtime,capture_ok,window_ok)
|
||||
observed=dict(status='PASS' if predicate['complete'] else 'INCONCLUSIVE', runtime=runtime,
|
||||
planned=PLANS, reserved=state.get('reserved'), action_ledger_valid=predicate['action_ledger_valid'],
|
||||
captures=len(captures), capture_bytes_valid=capture_ok, event_capture_count_valid=predicate['event_capture_count_valid'],
|
||||
clicks=len(clicks), injections=predicate['injections'], click_injections=predicate['click_injections'],
|
||||
standalone_move_injections=predicate['standalone_move_injections'], grabs=predicate['grabs'],
|
||||
pointer_counts_valid=predicate['pointer_counts_valid'],
|
||||
window_evidence_complete=window_ok,
|
||||
measurement_event=any(e.get('kind')=='measurements-completed' and e.get('status')=='PASS' for e in events))
|
||||
durable(WORK/'finish.json',observed)
|
||||
return observed
|
||||
127
tooling/probes/step20c-overnight/release_shape/tests.py
Normal file
127
tooling/probes/step20c-overnight/release_shape/tests.py
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
"""Offline fail-closed tests for the G smoke fixture; never launch compositor."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
HERE=Path(__file__).resolve().parent
|
||||
sys.path[:0]=[str(HERE),str(HERE.parent)]
|
||||
import adapter,gate
|
||||
from horizontal_adapter import replace_one
|
||||
|
||||
class GOffline(unittest.TestCase):
|
||||
def test_c2_transform_unique_and_compiles(self):
|
||||
text=adapter.generate_g();compile(text,'G-smoke/runner.py','exec')
|
||||
self.assertEqual(text.count(adapter.G_IMPORT),1)
|
||||
self.assertIn("open_chain('E1')",text);self.assertIn('fresh_chain(',text)
|
||||
self.assertIn('dismissal(log',text);self.assertIn('final_restoration(baseline,final)',text)
|
||||
self.assertIn('BINARY.resolve() and sha(BINARY)==BINARY_SHA',text)
|
||||
def test_transform_rejects_missing_and_duplicate(self):
|
||||
for source in ('missing','x x'):
|
||||
with self.assertRaises(ValueError):replace_one(source,'x','y')
|
||||
def test_old_A_and_non_G_ELF_rejected(self):
|
||||
old_a=gate.LAB/'stage-static/usr/bin/swayfx-mosaic'
|
||||
with self.assertRaises(ValueError):gate.validate_binary_identity(old_a,'979dcd95ab43fa1e465d6e9d262485b73d186cedacd0f957059dd24f482ce6f4')
|
||||
with self.assertRaises(ValueError):gate.validate_binary_identity('/home/fy59/swayfx-mosaic-overnight-mcolx0gq/matrix/D4-mosaic/stage/usr/bin/swayfx-mosaic','0'*64)
|
||||
with self.assertRaises(ValueError):gate.validate_binary_identity('/usr/bin/sway','0'*64)
|
||||
def test_old_D_or_other_source_set_rejected(self):
|
||||
rows=dict(gate.TRACE_SOURCES);rows[next(iter(rows))]='0'*64
|
||||
with self.assertRaises(ValueError):gate.source_bindings({'trace_sources':rows})
|
||||
def test_wrong_wlroots_and_mutated_freeze_rejected(self):
|
||||
with self.assertRaises(ValueError):gate.validate_wlroots(gate.WLROOTS,'0'*64)
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
p=Path(d)/'input';p.write_bytes(b'changed')
|
||||
with self.assertRaises(ValueError):gate.validate_file_map({str(p):hashlib.sha256(b'original').hexdigest()})
|
||||
def test_quota_and_no_retry_rejected(self):
|
||||
state={'limits':{'scene_build':2,'mosaic_build':2,'stage':2,'run':18,'capture':220},
|
||||
'campaign_limits':{'C':3,'D1':2,'D2':2,'D3':2,'D4':2,'E1':1,'E2':1,'E3':2,'F':2,'G':1},
|
||||
'used':dict.fromkeys(('scene_build','mosaic_build','stage','run','capture'),0),
|
||||
'campaign_used':dict.fromkeys(('C','D1','D2','D3','D4','E1','E2','E3','F','G'),0),
|
||||
'runs':{},'events':[]}
|
||||
state['used']['capture']=204
|
||||
with patch('ledger._validate'):
|
||||
with self.assertRaises(ValueError):gate.validate_budget_room(state)
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
work=Path(d);(work/'consumed.json').write_text('{}')
|
||||
with self.assertRaises(ValueError):gate.validate_one_shot(work)
|
||||
def test_incomplete_frame_and_window_evidence_rejected(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
w=Path(d);(w/'results').mkdir();(w/'run').mkdir()
|
||||
with self.assertRaises(ValueError):gate.validate_capture_batch(w,[])
|
||||
with self.assertRaises(ValueError):gate.validate_window_results(w)
|
||||
def test_real_c2_events_satisfy_finish_predicate_and_mutations_fail(self):
|
||||
c2=gate.LAB/'horizontal/C2'
|
||||
evidence=(c2/'run/events.jsonl',c2/'actions.json',c2/'results/captures.json',
|
||||
c2/'results/clicks.json',c2/'results/runtime.json')
|
||||
for path in evidence:
|
||||
self.assertTrue(path.is_file(),str(path))
|
||||
self.assertRegex(gate.sha(path),r'^[0-9a-f]{64}$')
|
||||
if (gate.WORK/'freeze.json').is_file():
|
||||
frozen=gate.expected_files()
|
||||
for path in evidence:self.assertEqual(frozen[str(path)],gate.sha(path),str(path))
|
||||
events=[json.loads(line) for line in evidence[0].read_text().splitlines()]
|
||||
state=json.loads(evidence[1].read_text())
|
||||
captures=json.loads(evidence[2].read_text())
|
||||
clicks=json.loads(evidence[3].read_text())
|
||||
runtime=json.loads(evidence[4].read_text())
|
||||
gate.validate_capture_batch(c2,captures)
|
||||
gate.validate_window_results(c2)
|
||||
valid=gate.finish_predicate(state,events,captures,clicks,runtime,True,True)
|
||||
self.assertTrue(valid['complete'],valid)
|
||||
self.assertEqual((valid['injections'],valid['click_injections'],
|
||||
valid['standalone_move_injections'],valid['grabs']),(10,8,2,4))
|
||||
injections=[i for i,e in enumerate(events) if e.get('kind')=='injection']
|
||||
self.assertEqual(len(injections),10)
|
||||
missing=events[:injections[-1]]+events[injections[-1]+1:]
|
||||
extra=events+[dict(events[injections[0]])]
|
||||
wrong_action=[dict(e) for e in events]
|
||||
wrong_action[injections[1]]['action']='move'
|
||||
unknown_action=[dict(e) for e in events]
|
||||
unknown_action[injections[1]]['action']='unknown'
|
||||
for mutation in (missing,extra,wrong_action,unknown_action):
|
||||
with self.subTest(mutation=len(mutation)):
|
||||
self.assertFalse(gate.finish_predicate(
|
||||
state,mutation,captures,clicks,runtime,True,True)['complete'])
|
||||
def test_preflight_refuses_absent_or_rejected_review_without_side_effects(self):
|
||||
if not (gate.WORK/'freeze.json').is_file():
|
||||
self.skipTest('review-gated preflight is exercised after the fresh freeze exists')
|
||||
real_root=gate.GROOT
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
review_dir=Path(d)/'reviews';review_dir.mkdir()
|
||||
absent=review_dir/'release-smoke-review.json'
|
||||
rejected=review_dir/'rejected.json'
|
||||
rejected.write_text(json.dumps({'status':'CORRECTION_REQUIRED'}))
|
||||
|
||||
class RootWithIsolatedReview:
|
||||
def __truediv__(self, other):
|
||||
if str(other)=='reviews/release-smoke-review.json':
|
||||
return absent
|
||||
return real_root/other
|
||||
|
||||
budget=(gate.LAB/'budget.json').read_bytes()
|
||||
for path,expected in ((absent,'independent G smoke byte review missing'),
|
||||
(rejected,'G smoke review scope/status')):
|
||||
if path==absent:
|
||||
self.assertFalse(absent.exists())
|
||||
else:
|
||||
rejected.rename(absent)
|
||||
with patch.object(gate,'GROOT',RootWithIsolatedReview()):
|
||||
with self.assertRaisesRegex(ValueError,expected):
|
||||
gate.preflight(require_review=True)
|
||||
self.assertEqual((gate.LAB/'budget.json').read_bytes(),budget)
|
||||
self.assertFalse((gate.WORK/'consumed.json').exists())
|
||||
self.assertFalse((gate.WORK/'run').exists())
|
||||
|
||||
if __name__=='__main__':
|
||||
p=argparse.ArgumentParser();p.add_argument('--output',type=Path,required=True);args=p.parse_args()
|
||||
result=unittest.TextTestRunner(verbosity=2).run(unittest.defaultTestLoader.loadTestsFromTestCase(GOffline))
|
||||
data={'status':'PASS' if result.wasSuccessful() else 'FAIL','executed':result.testsRun,
|
||||
'skipped':[(str(t),reason) for t,reason in result.skipped],
|
||||
'failures':[(str(t),s) for t,s in result.failures], 'errors':[(str(t),s) for t,s in result.errors],
|
||||
'runtime':'NOT RUN','build':'NOT RUN','stage':'NOT RUN','review_acceptance_created':False}
|
||||
args.output.write_text(json.dumps(data,indent=2)+'\n')
|
||||
raise SystemExit(not result.wasSuccessful())
|
||||
102
tooling/probes/step20c-overnight/replay_annotated.py
Normal file
102
tooling/probes/step20c-overnight/replay_annotated.py
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
"""WHY: requalify annotated identity without inheriting historical graphics.
|
||||
CONTRACT: pinned replay gate before reserved one-shot builds.
|
||||
INVARIANT: historical LAB read only; no semantic source edit or build retry.
|
||||
"""
|
||||
import sys, json, hashlib, shutil, subprocess, tarfile, re, time, os, argparse
|
||||
from pathlib import Path
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('lab',type=Path)
|
||||
parser.add_argument('--historical',type=Path,required=True)
|
||||
args=parser.parse_args()
|
||||
LAB=args.lab.resolve(); D=args.historical.resolve()
|
||||
REPO=Path(__file__).resolve().parents[3]
|
||||
assert LAB != D and not LAB.is_relative_to(REPO)
|
||||
LAB.mkdir(exist_ok=True)
|
||||
sys.path.insert(0,str(REPO/'tooling/probes/step20c-overnight'))
|
||||
from ledger import initialize,reserve as debit
|
||||
for name in ('scripts','raw','results','patches'): (LAB/name).mkdir(exist_ok=True)
|
||||
initialize(LAB/'budget.json')
|
||||
for name in ('support.py','audit-d.py','qualify-d.py','stage-d.py'):
|
||||
shutil.copy2(D/'scripts'/name,LAB/'scripts'/name)
|
||||
p=LAB/'scripts/support.py';txt=p.read_text();start=txt.index('def reserve(');end=txt.index('\ndef environment()',start)
|
||||
txt=txt[:start]+"def reserve(lab, kind):\n import sys\n sys.path.insert(0, '"+str(REPO/'tooling/probes/step20c-overnight')+"')\n from ledger import reserve as debit\n return debit(Path(lab)/'budget.json', kind)\n"+txt[end:]
|
||||
# Every command records wall time/date/return code, including failed diagnostics.
|
||||
txt=txt.replace("lab=Path(lab); path=", "started=time.time(); lab=Path(lab); path=")
|
||||
txt=txt.replace("'env':extra or {},'exit':proc.returncode", "'env':extra or {},'exit':proc.returncode,'started_unix':started,'duration_s':time.time()-started")
|
||||
p.write_text(txt)
|
||||
sys.path.insert(0,str(LAB/'scripts'))
|
||||
from support import command,save,sha,reserve
|
||||
|
||||
def treehash(root):
|
||||
entries=[{'path':str(p.relative_to(root)),'sha256':sha(p),'mode':oct(p.stat().st_mode&0o777)} for p in sorted(root.rglob('*')) if p.is_file()]
|
||||
return hashlib.sha256(json.dumps(entries,sort_keys=True,separators=(',',':')).encode()).hexdigest()
|
||||
|
||||
def tokens(text):
|
||||
# Strings/chars are consumed before comment syntax; comments inside literals survive.
|
||||
expr=r'"(?:\\.|[^"\\])*"|\'(?:\\.|[^\'\\])*\'|/\*.*?\*/|//[^\n]*|[A-Za-z_]\w*|(?:\d[\w.]*)|[^\s]'
|
||||
return [m.group(0) for m in re.finditer(expr,text,re.S) if not m.group(0).startswith(('/*','//'))]
|
||||
|
||||
state={'status':'RUNNING','source_gate':'NOT RUN','graphics':'NOT TESTED','historical':str(D)}
|
||||
def checkpoint(): (LAB/'phase-a.json').write_text(json.dumps(state,indent=2)+'\n')
|
||||
try:
|
||||
versions=json.loads((D/'results/source-versions.json').read_text())
|
||||
components=json.loads((D/'results/component-patches.json').read_text())
|
||||
for name in ('mosaic-source','swayfx-source','scene-source','wlroots-source'):
|
||||
arc=LAB/(name+'.tar');shutil.copy2(D/(name+'.tar'),arc)
|
||||
assert sha(arc)==versions['archives'][arc.name]
|
||||
if name in ('mosaic-source','swayfx-source'):
|
||||
rev=versions['pins']['Mosaic' if name=='mosaic-source' else 'SwayFX']
|
||||
with (LAB/(name+'-git-check.tar')).open('wb') as out:subprocess.run(['git','-C',str(REPO),'archive',rev],stdout=out,check=True)
|
||||
assert sha(LAB/(name+'-git-check.tar'))==sha(arc)
|
||||
(LAB/name).mkdir()
|
||||
with tarfile.open(arc) as tf:tf.extractall(LAB/name,filter='data')
|
||||
state['components']=[]
|
||||
for item in components['ordered_patches']:
|
||||
tree=LAB/item['tree'];src=Path(item['source']);patch=LAB/'patches'/src.name
|
||||
assert sha(src)==sha(Path(item['candidate_path']))==item['sha256']
|
||||
shutil.copy2(src,patch)
|
||||
assert treehash(tree)==item['parent_tree_sha256']
|
||||
command(LAB,'replay-'+src.stem+'-check',['git','apply','--check',patch],tree)
|
||||
command(LAB,'replay-'+src.stem+'-apply',['git','apply',patch],tree)
|
||||
assert treehash(tree)==item['result_tree_sha256']
|
||||
state['components'].append({**item,'replayed_path':str(patch),'verified':True})
|
||||
for name in ('mosaic-source','scene-source'):
|
||||
assert treehash(LAB/name)==treehash(D/name)
|
||||
manifest=json.loads((D/'comment-repair/repair-manifest.json').read_text())
|
||||
before={row['path']:(LAB/row['path']).read_text() for row in manifest['files']}
|
||||
for row in manifest['files']:assert sha(LAB/row['path'])==row['parent_sha256']
|
||||
for name,digest in manifest['patches'].items():
|
||||
patch=LAB/'patches'/name;shutil.copy2(D/'comment-repair'/name,patch);assert sha(patch)==digest
|
||||
command(LAB,'comment-'+name+'-check',['git','apply','--check',patch],LAB)
|
||||
command(LAB,'comment-'+name+'-apply',['git','apply',patch],LAB)
|
||||
for row in manifest['files']:
|
||||
assert sha(LAB/row['path'])==row['result_sha256']
|
||||
assert tokens(before[row['path']])==tokens((LAB/row['path']).read_text())
|
||||
state.update(source_gate='PASS',comments=manifest,source_trees={name:treehash(LAB/name) for name in ('scene-source','mosaic-source')})
|
||||
assert sha(Path('/usr/lib/libwlroots-0.20.so').resolve())=='7edca9a7d65855addda68e29ba5db94d57c803eb5fa7d0f86db3136b90ca9837'
|
||||
save(LAB,'source-versions',versions);checkpoint();print('SOURCE_GATE_PASS',flush=True)
|
||||
command(LAB,'compiler',['cc','--version']);command(LAB,'meson-version',['meson','--version'])
|
||||
command(LAB,'dependencies',['pkg-config','--modversion','wlroots-0.20','scenefx-0.5','wayland-server','wayland-client','wayland-protocols','libdrm','xkbcommon','pixman-1'])
|
||||
reserve(LAB,'scene_build')
|
||||
command(LAB,'scene-setup',['meson','setup',LAB/'scene-build',LAB/'scene-source','--prefix=/usr','-Ddefault_library=static','-Dexamples=false','-Dwerror=false'])
|
||||
command(LAB,'scene-build',['meson','compile','-C',LAB/'scene-build','-j','8'])
|
||||
reserve(LAB,'mosaic_build')
|
||||
extra={'PKG_CONFIG_PATH':str(LAB/'scene-build/meson-uninstalled')}
|
||||
flags=['--prefix=/usr','--bindir=bin','-Dwerror=false','-Dsd-bus-provider=libsystemd','-Dmosaic-package=true','-Dswaybar=false','-Dswaynag=false','-Dman-pages=disabled','-Ddefault-wallpaper=false']
|
||||
command(LAB,'mosaic-setup',['meson','setup',LAB/'mosaic-build',LAB/'mosaic-source',*flags],extra=extra)
|
||||
command(LAB,'mosaic-build',['meson','compile','-C',LAB/'mosaic-build','-j','8'],extra=extra)
|
||||
state['builds']='PASS';checkpoint();print('BUILDS_PASS',flush=True)
|
||||
command(LAB,'mosaic-model',['meson','test','-C',LAB/'mosaic-build','--no-rebuild','mosaic-model','--print-errorlogs'])
|
||||
save(LAB,'static-build',{'status':'PASS','flags':flags,'options':json.loads((LAB/'mosaic-build/meson-info/intro-buildoptions.json').read_text()),'archive_sha256':sha(LAB/'scene-build/libscenefx-0.5.a'),'binary_sha256':sha(LAB/'mosaic-build/sway/swayfx-mosaic')})
|
||||
reserve(LAB,'stage')
|
||||
command(LAB,'meson-stage',['meson','install','-C',LAB/'mosaic-build','--no-rebuild','--destdir',LAB/'stage-static'])
|
||||
for script,args in [('audit-d.py',[LAB]),('qualify-d.py',[]),('stage-d.py',[LAB])]:
|
||||
command(LAB,script,['python',LAB/'scripts'/script,*args],timeout=600)
|
||||
state['binary']=json.loads((LAB/'results/binary-elf.json').read_text())
|
||||
state['stage']=json.loads((LAB/'results/static-stage-manifest.json').read_text())
|
||||
state['ABI']='PASS local layout/headers/85-symbol characterization; semantic maintenance NOT ESTABLISHED'
|
||||
state['status']='PASS'
|
||||
except Exception as exc:
|
||||
state['status']='FAIL';state['error']=repr(exc);raise
|
||||
finally:
|
||||
state['budget']=json.loads((LAB/'budget.json').read_text());checkpoint()
|
||||
14
tooling/probes/step20c-overnight/run.py
Normal file
14
tooling/probes/step20c-overnight/run.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
#!/usr/bin/env python3
|
||||
"""One explicit reviewed C run; no retries, no inherited live desktop socket."""
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from horizontal_adapter import BASE,generate
|
||||
from horizontal_gate import preflight,sha
|
||||
p=argparse.ArgumentParser();p.add_argument('lab',type=Path);p.add_argument('case',choices=['C0','C1','C2']);a=p.parse_args()
|
||||
lab=a.lab.resolve();preflight(lab,a.case)
|
||||
runner=lab/'horizontal'/a.case/'runner.py'
|
||||
if runner.read_text()!=generate(a.case):raise ValueError('generated reviewed runner changed')
|
||||
# Preserve overnight gate first, with only the identified validator dependency directory next.
|
||||
sys.path.insert(1,str(BASE))
|
||||
exec(compile(runner.read_text(),str(runner),'exec'),{'__name__':'__main__','__file__':str(runner)})
|
||||
49
tooling/probes/step20c-overnight/test_ledger.py
Normal file
49
tooling/probes/step20c-overnight/test_ledger.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
"""WHY: validate irreversible debits and concurrent reservation bounds offline."""
|
||||
import concurrent.futures
|
||||
import json
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
from ledger import initialize, reserve
|
||||
|
||||
class LedgerTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.tmp.cleanup)
|
||||
self.path = Path(self.tmp.name) / 'budget.json'
|
||||
initialize(self.path)
|
||||
def test_no_reset_and_no_refund(self):
|
||||
reserve(self.path, 'scene_build')
|
||||
with self.assertRaises(FileExistsError): initialize(self.path)
|
||||
self.assertEqual(json.loads(self.path.read_text())['used']['scene_build'], 1)
|
||||
def test_concurrent_exhaustion(self):
|
||||
def attempt(_):
|
||||
try: reserve(self.path, 'mosaic_build'); return True
|
||||
except RuntimeError: return False
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=8) as pool:
|
||||
self.assertEqual(sum(pool.map(attempt, range(16))), 2)
|
||||
def test_run_and_capture_bounds(self):
|
||||
reserve(self.path, 'run', campaign='F', run_id='f1')
|
||||
reserve(self.path, 'capture', run_id='f1', amount=12)
|
||||
with self.assertRaises(RuntimeError): reserve(self.path, 'capture', run_id='f1')
|
||||
reserve(self.path, 'run', campaign='F', run_id='f2')
|
||||
with self.assertRaises(RuntimeError): reserve(self.path, 'run', campaign='F', run_id='f3')
|
||||
def test_invalid_does_not_debit(self):
|
||||
for kwargs in ({'campaign':'C','run_id':'c','duration_s':151}, {'campaign':'C','run_id':'c','amount':2}):
|
||||
with self.assertRaises(ValueError): reserve(self.path, 'run', **kwargs)
|
||||
with self.assertRaises(ValueError): reserve(self.path, 'capture', run_id='missing')
|
||||
self.assertEqual(json.loads(self.path.read_text())['events'], [])
|
||||
def test_corrupted_persistence_fails_closed(self):
|
||||
original = self.path.read_text()
|
||||
for mutate in (
|
||||
lambda s: s['used'].update(scene_build=-1),
|
||||
lambda s: s['used'].update(scene_build=True),
|
||||
lambda s: s['used'].update(scene_build=1),
|
||||
lambda s: s['events'].append({'sequence':0,'kind':'run','amount':1,'campaign':'C','run_id':'x','reserved_at_ns':1}),
|
||||
lambda s: s['runs'].update(x={'campaign':'C','captures':0,'duration_s':150}),
|
||||
):
|
||||
state = json.loads(original); mutate(state)
|
||||
corrupted = json.dumps(state); self.path.write_text(corrupted)
|
||||
with self.assertRaises(RuntimeError): reserve(self.path, 'stage')
|
||||
self.assertEqual(self.path.read_text(), corrupted)
|
||||
if __name__ == '__main__': unittest.main()
|
||||
Loading…
Reference in a new issue