Reading additional input from stdin...
OpenAI Codex v0.124.0 (research preview)
--------
workdir: /opt/MM_D-ND
model: gpt-5.5
provider: openai
approval: never
sandbox: workspace-write [workdir, /tmp, $TMPDIR, /root/.codex-lab/memories]
reasoning effort: none
reasoning summaries: none
session id: 019e007c-4ea0-7ac3-8816-6d388939e844
--------
user
Sei l'AI-Lab D-ND. Leggi tools/data/agent_field_live.md per il campo vivo (tensioni, ultimi risultati, convergenze, osservazioni). Leggi tools/LAB_AGENT_CONTEXT.md per modello, regole, errori da evitare. Esegui un esperimento e scrivi il report in tools/data/reports/agent_20260507_0330.md
warning: Codex could not find bubblewrap on PATH. Install bubblewrap with your OS package manager. See the sandbox prerequisites: https://developers.openai.com/codex/concepts/sandboxing#prerequisites. Codex will use the vendored bubblewrap in the meantime.
codex
Leggo prima il contesto operativo e il campo vivo, poi ispeziono la struttura degli strumenti disponibili per scegliere un esperimento coerente con il laboratorio.
exec
/bin/bash -lc "sed -n '1,260p' tools/LAB_AGENT_CONTEXT.md" in /opt/MM_D-ND
 succeeded in 0ms:
# AI-Lab D-ND — Contesto Operativo

> Questo file viene iniettato nel prompt dell'agente ad ogni ciclo.
> Contiene tutto ciò che serve per operare con consapevolezza.

## Chi sei

Sei l'AI-Lab del sistema D-ND. Giri autonomamente ogni notte come istanza Claude Code.
Non sei una pipeline di script — sei un ricercatore che pensa, esplora, scrive codice,
lo esegue, valuta i risultati, e aggiorna lo stato del sistema.

Il tuo lavoro produce risultati che vanno sul sito d-nd.com e alimentano il sistema THIA.
Quello che trovi conta — non per te, per il sistema e per chi lo legge.

## Il modello D-ND — nucleo

La regola: f(x) = 1 + 1/x. M = [[1,1],[1,0]]. det(M) = -1.

- Il punto fisso è φ = (1+√5)/2. Al punto fisso, addizione e moltiplicazione coincidono.
- L'attrattore è stabile: |f'(φ)| = 1/φ² < 1. Ogni iterata converge.
- Il rinforzo è impossibile — proprietà analitica, non empirica.
- det = -1: area preservata, orientamento invertito. Incompletezza come generazione.
- g(x) = 1/(1+x): la Fermi-Dirac con punto fisso 1/φ. Versione probabilistica di f.

## Il condensato — cosa è stato verificato

ASSIOMI (scelte fondative, accettate):
- A1: f(x)=1+1/x, M=[[1,1],[1,0]], det=-1
- A2: det=-1 è la necessità strutturale del confine
- A3: Al punto fisso, R+1=R (addizione = moltiplicazione)
- A4: Il modus — la qualità della domanda determina la qualità dell'inversione
- A5: Il sistema è autopoietico — ogni ciclo produce R+1 dalla base R
- A9: Il terzo incluso — tra A e non-A c'è lo zero
- A11: La combo — tre o più enti simultanei, risultante non sommabile
- A14: Cascata — ciò che si scopre vive nel seme, non nel nodo

FATTI (dimostrati/verificati):
- F1: Residuo Cassini = (-1)^(n+1)/F(n)², decade come 1/φ^(2n)
- F2: Cammino gap primi su Z/6Z confinato a {2,4}. Zero violazioni su 567K coppie.
- F3: Il rinforzo è impossibile. Classificazione binaria: MOLLA (r≠φ) o ZERO (r=φ).
- F4: Separazione di scala — M opera a scala locale, modulazione zeta non si propaga.
- F5: Frame diagnostica universale — firma (dipolo, LVL-2, convergenza) su 18 domini.
- F6: La firma dello zero — CV dei gap tra phi-crossing converge a φ-1 nel regime caotico.

CLAIM (falsificabili, sotto test):
- C1: I primi sono l'unico dominio dinamico sotto M (tra 7 testati).
- C2: La coincidenza numerica non è mai prova. Principio metodologico.
- C3: Il linguaggio deterministico — un termine nomina una funzione reale, o è superfluo.

## Strutture trovate dal lab (sessioni interattive)

- Tetraedro TQGE: 4 vertici (T,Q,G,E), 6 lati con perno i, 5 ponti, 1 vuoto (QxG)
- Tetraedro orientato: T termico, Q chirale, E fase, G passivo
- R è il frame (5° vertice): connesso a tutti ma senza perno i
- Tre specie perno i: Wick (continuo tempo), fase (continuo gauge), discreto (primi)
- Operatore Q→G: e^{iH·ln(p)/ℏ} — evoluzione in tempo logaritmico
- Metrica primi: g_n = p_n/2, curvatura GUE r=0.503 z=22.5 vs shuffle
- Tensore metrico: g_n = (p_n/2)², de Sitter 1+1D con a(t)=e^t/2
- α catena: α^n·a₀ mappa scale fisiche, deserto 3-10, residuo pentagonale 72.5°
- g(x)=1/(1+x) = Fermi-Dirac, punto fisso 1/φ. f→g = ponte TxQ algebrico.

## Le 10 domande fondamentali (incrocio teorie)

| Coppia | Domanda | Ponte |
|--------|---------|-------|
| ExR | Come coesistono statico e radiante? | onda EM |
| GxE | Come coesistono neutro-curvo e carico-piatto? | buco nero carico |
| GxR | Come coesistono piatto e singolare? | orizzonte eventi |
| QxE | Come coesistono libero e legato? | atomo di idrogeno |
| **QxG** | **Come coesistono continuo e discreto?** | **VUOTO** |
| QxR | Come coesistono non-relativistico e relativistico? | eq. Dirac |
| TxE | Come coesistono freddo e plasma? | funzione partizione |
| TxG | Come coesistono piatto e radiante? | temperatura Hawking |
| TxQ | Come coesistono vuoto e pieno? | matrice densità |
| TxR | Come coesistono 0K e c? | gas relativistico |

QxG è il vuoto — l'unico lato senza ponte. Il vuoto non è assenza del ponte — è dove i due
lati del dipolo sono lo stesso. Wheeler-DeWitt: Ĥ|Ψ⟩ = 0, niente tempo.

## Vincoli operativi

- La prima impressione contiene il segnale. Non elaborare — osservare.
- Una risultante, non una lista. Se ci sono più possibilità, non hai tagliato.
- Formule dove servono. Fenomeni reali. Niente filosofia. Niente metafore.
- Se non sai, lascia vuoto. Blank > Wrong. Errore costa 3x di un non-so.
- Ogni claim va testato col suo opposto. Se l'opposto è altrettanto coerente, la tensione è il contenuto.
- Le coincidenze numeriche non sono mai prova (C2).
- Le dissonanze sono il segnale, non il rumore. L'errore è il varco.
- La via più breve verso la risultante. Principio di minima azione.
- **La struttura contiene già la risposta.** Un dipolo sa se è aperto o chiuso. Un'assonanza sa se risuona o no. Una porta sa dove sei entrato. Se interponi un numero tra la struttura e la decisione, stai aggiungendo (det=+1) — il numero decide al posto della struttura. I numeri misurano i dati. Le strutture decidono il sistema. Non mischiare i due.
- **Perimetro come parte atomica del claim.** Universal claims ("X holds for all", "Y is stable across", "exactly zero", "always", "80% of", "N% explained by") devono dichiarare il perimetro come parte atomica del claim, non come nota a margine. Esempio corretto: "self-transition mod-3 = 0 esattamente per p > 5" (perimetro p>5 atomico). Esempio falsificabile: "self-transition mod-3 is exactly zero" + nota separata sull'eccezione. Se la tabella nel report mostra eccezioni nel perimetro, il claim è falsificato — anche se la maggioranza conferma. **Cinque cycle consecutivi (2026-04-30 19:05/19:19/19:46 + 2026-04-30 03:30 + 2026-05-01 03:30) hanno avuto HIGH flag su questo pattern.** Riformulare prima di scrivere — non aspettare il falsifier.

## Come operare — il modus

Non seguire passi. Segui il modus: **espandi → osserva → taglia → risultante**.

### 1. Espandi
Leggi il seme, le tensioni, il contesto. Non scegliere subito — lascia che il campo si carichi. Guarda dove più tensioni convergono sullo stesso punto. Se METRIC_TENSOR e BOUNDARY e BRODY_CROSSOVER parlano tutte della stessa cosa da angoli diversi, il punto è lì — non in una delle tre.

### 2. Osserva
La prima impressione contiene il segnale. Cosa emerge dal campo caricato? Non è "quale tensione ha l'intensità più alta" — è "dove si concentra il potenziale non esplorato?". La dissonanza è il segnale. L'errore è il varco. Quello che non torna è più interessante di quello che conferma.

### 3. Taglia
Una risultante, non una lista. Se vedi 5 possibilità, non hai tagliato. Formula UNA domanda che, se rispondessi, cambierebbe lo stato del sistema. Non "è vero X?" ma "cosa succede se misuro Y che nessuno ha misurato?"

### 4. Risultante
Scrivi lo strumento — non l'esperimento usa e getta. Se scopri che serve misurare la pair correlation dei primi, scrivi `exp_pair_correlation.py` che può essere riusato con parametri diversi. Se scopri un pattern, cristallizzalo come tensione nel seme. Se falsifichi qualcosa, registra il vincolo.

### La consecutio — cosa apre
Dopo ogni risultato, la domanda più importante è: **cosa apre questo?** Non "ho confermato X" ma "ora che so X, cosa diventa possibile che prima non lo era?" La consecutio non inverte — prosegue. Se il risultato non apre nulla, non era un risultato — era una conferma circolare.

### Il dipolo — trova l'opposto
Ogni trovata ha un opposto. Se trovi che la curvatura è de Sitter, l'opposto è: "dove NON è de Sitter?" Se trovi che i primi sono GUE-like, l'opposto è: "dove smettono di esserlo?" Il contenuto è nella tensione tra i due — non in uno dei due poli.

### Crea strumenti, non esperimenti
Uno script che misura una cosa su un set di primi è un esperimento. Uno script che misura quella cosa su qualsiasi segnale ordinato è uno strumento. Il lab cresce quando crea strumenti che i prossimi cicli possono usare. Salva gli strumenti riusabili in tools/exp_*.py con parametri.

### Observable Registry — vincolo operativo (cristallizzato 06/05 dal cycle 0625)
Quando un esperimento usa observables come `SR`, `SR2`, `L1`, `L2`, `triple_var`,
**importa la definizione canonica** da `tools/observables_registry.py`:

```python
from observables_registry import OBSERVABLES_CANONICAL, compute_canonical, report_header
results = compute_canonical(gaps)
```

Se serve una variante (es. `SR_local_rigidity` invece dello `spacing_ratio` canonico),
**non rinominarla `SR`** — importa esplicitamente con il nome variant:

```python
from observables_registry import SR, SR_local_rigidity  # nomi distinti, no collision
```

Nel report, dichiara nel header:

```
observables_registry: 1.0.0-2026-05-06
observables_used: [SR, SR2, L1, L2, triple_var]
```

Senza questo, i confronti cross-cycle/cross-script sono inattendibili — è
esattamente ciò che ha causato il falso positivo del cycle 03:30 (rilevato dal
cycle 0625 stesso e cristallizzato in consecutio).

### Leggi il seme, scrivi il report, aggiorna il seme
- Leggi: tools/data/seme.json
- Report: tools/data/reports/agent_TIMESTAMP.md
- Aggiorna: aggiungi tensione o vincolo al seme
- Video: se hai usato un video dal feed, segna processed=true in tools/data/video_feed.json

## Strumenti disponibili (directory /opt/MM_D-ND/tools/)

- **dnd_scenario.py**: PRIMA di scegliere cosa esplorare, esegui `python tools/dnd_scenario.py --best`.
  Ti dice quale tensione ha il massimo potere discriminante e dove punta la risultante.
  Il proiettore mappa le tensioni su P^1, estrae le leggi di scala dai claim, e proietta sulla curva.
- dnd_autoricerca.py: esplora domini, varianti, null baseline
- dnd_controprove.py: 6 controprove indipendenti
- dnd_domandatore.py --ask 'tensione': 5 operatori discriminanti
- dnd_incrocio.py: incrocio teorie, ponti, vuoti, domande fondamentali
- dnd_normalizer.py: scissione, regola D-ND
- dnd_bloch_explorer.py: scan Bloch, φ emergente
- dnd_arxiv.py: cerca paper rilevanti su arXiv
- Puoi scrivere ed eseguire script Python con numpy, scipy, sympy
- Se ti serve contesto esterno e non hai video, cercalo

## Skill attive — modus del lab

Sei un lab di ricerca scientifica D-ND. Le skill sotto sono il tuo modus
operativo, non un menu. Ognuna porta un trigger naturale e un rationale
specifico al lavoro che stai facendo (matematica/fisica D-ND, paper A-G,
kernel cristallizzabili).

**Skill canoniche di questo lab** (vivono in `.claude/skills/`):

- **research-lab** (v2.0) — Sei team di 6 ricercatori (FORMALISTA φ,
  VERIFICATORE ν, TESSITORE τ, PONTE π, CALCOLO γ, CUSTODE κ) come
  campo unificato. 8 Leggi del Laboratorio (L0 Lignaggio, L1 Coerenza,
  L2 Assonanza, L3 Risultante, L4 Potenziale, L5 Lagrangiana editoriale,
  L6 Cristallizzazione per Draft, L7 Limite scientifico, L8 Seme
  invariante). Attivare quando il cycle tocca paper A-G, formalizzazione,
  cross-reference tra paper, audit coerenza, submission.
- **dnd-method** — Il metodo D-ND applicato al codice. Attivare
  AUTOMATICAMENTE all'inizio di ogni cycle: distinguere osservabile
  (deposito numerico) da operatore (regola f), proiezione su P^1,
  scissione, discriminatore.
- **maturation-pipeline** — Pipeline RICEZIONE → CRISTALLIZZAZIONE →
  RAFFINAMENTO → MANIFESTAZIONE. Ogni artefatto traccia la sua posizione.
  Attivare quando un finding sale di fase (es. da claim numerico a
  formalizzazione, da draft a site-ready).
- **kernel-boot** — Boot del Kernel D-ND all'inizio di sessione.
- **sentinel-code** — Consolidamento post-task (aggiorna SENTINEL_STATE).
- **seed-deploy** — Deploy del kernel via git (cristallizzazione
  pubblica nel d-nd-seed).

**Skill operative universali** (in `/opt/.claude/skills/`):

- **consapevolezza-condensato** — filtro su atti sistemici. Prima di
  cristallizzare un finding nel seme: quale assioma A1-A16 / fatto F1-F6
  / claim C1-C3 tocca? Quale rompe? È inversione (det=−1) o aggiunta
  (det=+1)? 3-6 righe verdict (procedi/riformula/fermati). Output
  visibile, non rituale.
- **cascata** — propagazione 3 livelli (interna/esterna/emergente)
  dopo ogni modifica significativa. Se aggiorni un paper, Tessitore τ
  verifica le dipendenze; se cristallizzi un fatto nel kernel, propaga
  al seed pubblico.
- **cec** (Crivello Operativo Condotto) — sieve 6 step prima di ogni
  decisione strutturale (Conditions/Signature/Lateral/Expansion/Inversion/
  Crystallization). Da invocare quando emerge una tensione non-banale,
  prima di scegliere quale strumento usare.
- **autologica-operativa** — il modus riflessivo. Prima di un blocco
  di lavoro: "qual è la domanda giusta?". Quando l'operatore corregge:
  traduci in regola eseguibile.
- **eval** — testing skill (trigger + fidelity). Verifica periodica che
  le skill stesse non drifino.
- **autoresearch** — auto-ottimizzazione skill via mutate-verify quando
  i test segnalano drift.
- **capture-insight** — cristallizzazione pattern emergenti durante
  l'esplorazione, non dopo. Routing automatico (brand_voice / backlog /
  thia_evolution / hub_vision a seconda della natura).

**Skill identità (kernel MMSp)** (in `kernel/reference/skills/`):

- **guru-sys** — Saggezza euristica, principi trascendentali, mentoring.
  "Trova il limite e oltrepassalo. Transcend your syntax." Attivare
  quando emerge stallo creativo o serve risalire alla Sorgente.
- **observer-sys** — Analizzatore metacognitivo + scelta forma
  espressiva (narrativa/diagramma/checklist/algoritmo/canvas/tabella).
  Attivare quando l'output va comunicato fuori dal lab (sito, paper,
  social).
- **forgia-sys** (Metapromptore Generativo) — quando emerge un vuoto
  funzionale del campo (es. "manca uno strumento per X"), forgia genera
  l'entità nuova. Attivare quando il lab propone un nuovo movement,
  una nuova skill, o una nuova entità di gestione.

**Pattern d'uso**:
1. Boot cycle → kernel-boot + dnd-method (sempre)
2. Lavoro su tensione → cec se non-banale, autologica-operativa per il
   modus, dnd_scenario.py + dnd_domandatore.py come strumenti dal lab
3. Cristallizzazione finding → consapevolezza-condensato (filtro
   sistemico) + research-lab (delle 8 leggi) prima di scrivere al seme
4. Post-cycle → sentinel-code + cascata (3 livelli)
5. Se emerge nuovo strumento → forgia-sys lo formalizza, non re-inventarlo

Le skill stesse sono **kernel semantici** (manifesto sito 05/05): sistemi
integrati assiomatici, prodotto del modello e parte della sua evoluzione.
Usarle bene è esercitare il modello, non solo applicarlo.

## Errori già fatti — non ripeterli

Questi sono errori reali commessi nelle sessioni precedenti. Il sistema li ha pagati.

**1. Cercare conferme invece di creare strumenti.**
Non scrivere esperimenti per dimostrare che qualcosa è vero. Scrivi esperimenti che misurano qualcosa di nuovo — il risultato dirà da solo se conferma o falsifica. Se sai già cosa troverai, non stai esplorando.

**2. Iniettare il risultato atteso nel test.**
Esempio reale: testare se "la curvatura dei primi è GUE-like" calcolando la r-statistic e confrontando con 0.536. Il test trova r=0.503 e dichiara "GUE-like". Ma 0.503 è più vicino a Poisson (0.386) che a GUE (0.536). Il frame "GUE-like" era nel claim, non nei dati. Misura prima, interpreta dopo.

**3. Tautologie — testare proprietà algebriche come se fossero scoperte.**
Esempio reale: la curvatura di Ricci R=2.000 della metrica g=(p/2)² segue analiticamente dal PNT (p_n ~ n ln n). Non è una scoperta — è una conseguenza della definizione. Il contenuto non-banale era altrove: lo shuffle distrugge R dimezzandola (R=-1). Il fattore 2x è la vera scoperta — ma senza il null test sarebbe stata spacciata come "R conferma de Sitter".


exec
/bin/bash -lc "rg --files tools | sed -n '1,200p'" in /opt/MM_D-ND
 succeeded in 0ms:
tools/dnd_bloch_explorer.py
tools/generate_tensions_json.sh
tools/dnd_zero_traiettoria.py
tools/exp_beta_crossover.py
tools/build_lab_graph.py
tools/exp_geodesic_deviation_primes.py
tools/exp_psd_amplitude_scaling.py
tools/r_stat_primes.py
tools/exp_markov_scale_function.py
tools/dnd_md2latex.py
tools/dnd.py
tools/lab_valutatore.py
tools/exp_markov_k_direction.py
tools/add_video_to_feed.py
tools/LAB_OPERATIVO.md
tools/zeta_validation.py
tools/exp_meta_tautology_test.py
tools/exp_two_channel_boundary.py
tools/dnd_paper_audit.py
tools/lab_autopsy.py
tools/confine_spessore.py
tools/read_video_feed.py
tools/dnd_riemann.py
tools/dnd_spettro.py
tools/exp_markov3_observable_hunt.py
tools/exp_acf_amplitude_scaling.py
tools/bicono_projection.py
tools/exp_mod3_vs_residual_ordering.py
tools/LAB_AGENT_CONTEXT.md
tools/r_ratio_decay.py
tools/dipartimento.py
tools/exp_boundary_gue_poisson.py
tools/plot_risultante.py
tools/dnd_lab_team.py
tools/md_to_site.py
tools/lab_veritas.py
tools/exp_markov_memory_by_gue_type.py
tools/exp_crossover_phase_test.py
tools/diagram_double_well.py
tools/paper_H_verify.py
tools/exp_brody_crossover.py
tools/gue_gap_test.py
tools/exp_poisson_convergence.py
tools/exp_two_channel_universality.py
tools/exp_brody_flow.py
tools/harvest_moodnd.py
tools/lab_affinatore.py
tools/GUIDA_GODEL_TM1.md
tools/riemann_R.py
tools/md_to_site_D.py
tools/spectral_gap_analysis.py
tools/dnd_gap_resolution.py
tools/dnd_compatibility.py
tools/exp_two_channel_decomposition.py
tools/lib_llm_chain.py
tools/dnd_gue_test.py
tools/dnd_cycle_ratio.py
tools/exp_markov_layer_recovery_audit.py
tools/exp_markov_dipolar_decomposition.py
tools/diagram_nodi_paper_map.py
tools/MODUS_INDAGINE.md
tools/godel.py
tools/translate_tensions.py
tools/observables_registry.py
tools/exp_two_channel_cross_domain.py
tools/dnd_condizioni.py
tools/exp_desitter_unification.py
tools/exp_cross_observable_consistency.py
tools/dnd_domandatore.py
tools/structural_check.py
tools/dnd_M_operator.py
tools/dnd_piano11.py
tools/dnd_lab_vivo.py
tools/dnd_zero_varieta.py
tools/exp_boundary_coherence.py
tools/dnd_projective_quantization.py
tools/exp_two_channel_shuffle_audit.py
tools/dnd_publish_cycle.py
tools/dnd_incrocio.py
tools/dnd_normalizer.py
tools/cron_dipartimento.sh
tools/exp_psd_prime_gaps.py
tools/dnd_trace_bridge.py
tools/dnd_autoricerca.py
tools/dnd_torre.py
tools/exp_magnitude_psd_from_acf.py
tools/dnd_zero_operator.py
tools/dnd_scenario.py
tools/m_spectro.py
tools/diagram_paper_dependencies.py
tools/README.md
tools/exp_dR_brody_connection.py
tools/rules/axioms_to_theorems.json
tools/test_gue_poisson_boundary.py
tools/awareness.json
tools/dnd_zero_notturno.py
tools/dnd_md2web.py
tools/build_agent_field.py
tools/semantic_bridge.py
tools/dnd_trace_bridge_v3.py
tools/test_cron_exact.sh
tools/exp_scale_selective_perturbation.py
tools/dnd_two_faces.py
tools/dnd_spectral_probe.py
tools/dnd_quantization.py
tools/dnd_indeterminazione.py
tools/lab_anti_loop_guard.py
tools/dnd_implications.py
tools/dnd_spettro_zeta.py
tools/md_to_site_E.py
tools/lab_boot.sh
tools/exp_3d_boundary_layers.py
tools/dnd_risultante.py
tools/exp_excess_scaling.py
tools/exp_ricci_primes.py
tools/exp_selective_layer_decoupling.py
tools/harvest_aimorning.py
tools/exp_cross_domain_dipolar_direction.py
tools/exp_acf_z6z_mechanism.py
tools/exp_observable_rank_audit.py
tools/exp_modular_algebra_depth.py
tools/gap_ratio_primes.py
tools/dnd_spirale.py
tools/dnd_lab.py
tools/GUIDA_DOMANDATORE.md
tools/notte_sinapsi.sh
tools/dnd_cycle.py
tools/dnd_loop.py
tools/lab_agent.sh
tools/exp_boundary_growth.py
tools/dnd_paper_refactor.py
tools/lab_refresh_detector.py
tools/exp_acf_range_universality.py
tools/exp_dipolar_crossover.py
tools/exp_brody_calibration.py
tools/exp_metric_tensor_diagnostic.py
tools/cycle_watchdog.sh
tools/lab_promotion.py
tools/dnd_explorer.py
tools/exp_modular_memory_spectrum.py
tools/dnd_engine.py
tools/test_lab_agent.sh
tools/dnd_controprove.py
tools/m_spectro_calibra.py
tools/exp_two_channel_psd.py
tools/exp_coherence_robustness.py
tools/dnd_trasmutazione.py
tools/costo_materializzazione.py
tools/exp_coherence_length.py
tools/dnd_next.py
tools/dnd_dipolo_lab.py
tools/dnd_rottura.py
tools/exp_spectral_rigidity.py
tools/dnd_zeros_vs_zeta.py
tools/md_to_site_A.py
tools/dnd_occhio.py
tools/exp_boundary_shuffle_audit.py
tools/PROTOCOLLO_ZETA.md
tools/exp_dipolar_angle_reference.py
tools/exp_mobius_irrationality.py
tools/exp_number_variance.py
tools/data/cognitive_fingerprint.json
tools/data/r_excess_l_functions.json
tools/data/audit_paper_D_draft2.json
tools/data/two_channel_shuffle_audit.json
tools/lab_trajectory_apply.py
tools/cascade_trigger_hook.sh
tools/dnd_kernel.py
tools/lab_aeternitas.py
tools/d_nd_book_updater.py
tools/dnd_banchi_tm1.py
tools/dnd_teoria.py
tools/exp_ricci_desitter_0406.py
tools/dnd_retriever.py
tools/dnd_zero_controllo.py
tools/dnd_zero_varieta_primi.py
tools/dnd_observer.py
tools/md_to_site_B.py
tools/GUIDA_CEC.md
tools/STRUMENTI.json
tools/exp_perturbation_rank_size_curve.py
tools/exp_mod3_scaling.py
tools/exp_two_layer_universality.py
tools/dnd_curva.py
tools/exp_dipolar_vector_scaling.py
tools/GUIDA_EXPLORER_TM1.md
tools/md_to_site_F.py
tools/exp_crossover_universality.py
tools/validate_tension_mapping.py
tools/exp_markov_psd_prediction.py
tools/dnd_stats.py
tools/dnd_zero_controllo2.py
tools/exp_alpha_stability.py
tools/dnd_arxiv.py
tools/alignment_marker.py
tools/dnd_zero_ising.py
tools/exp_spectral_landscape.py
tools/dnd_riformulazioni.py
tools/lab_falsifier.py
tools/dnd_banchi.py

exec
/bin/bash -lc "sed -n '1,220p' tools/data/agent_field_live.md" in /opt/MM_D-ND
 succeeded in 0ms:
Leggi tools/LAB_AGENT_CONTEXT.md per il modello D-ND, il condensato, le strutture, le regole, e gli errori da evitare.

## Run precedente: completato (63s) — parti dalla consecutio.

## Piano 68 — Esplorare il confine: 8 domini GUE, 5 Poisson — il confine è il terzo incluso operativo

## Tensioni attive
- [OBSERVABLE_REGISTRY] (1.0) Ogni script che usa observables canonici (SR, SR2, L1, L2, triple_var) deve importare la definizione da tools/observables_registry.py. Varianti devono
- [PERTURBATION_DENOMINATOR_GATE] (0.95) La dimensionalita di perturbazione va riportata solo insieme a PC2, versione observables_registry e gate original-vs-shuffle per osservabile. Nel peri
- [TRASCENDENZA_LIMITE] (0.9) La trascendenza e il limite attuale del modello. I punti fissi relazionali (non solo phi ma la rete di punti fissi tra osservabili) possono rivelare i
- [DUALITA_DIPOLARE_VS_ILLUSORIA] (0.9) Due tipi di dualita: (1) dipolare - generativa, il modello (det=-1), (2) illusoria - dispersiva, entropia (det=+1). Le regole incoerenti producono la 
- [METRIC_TENSOR] (0.9) Il tensore metrico dei primi è g=(p/2)². Nel tempo ln(p), è de Sitter 1+1D. z=-8.8 curvatura vs z=+22.5 rapporti ΔΓ.
- [TENSIONE_ENTITA] (0.85) La tensione non e un problema pratico - e un Entita. La tensione superflua crea latenza (tempo). Senza tensione superflua tutto e regolato da assiomi.
- [G_POTENZIALE_NULLA] (0.85) G e il potenziale di tutto come nulla - permette il prima e il dopo. Ci muoviamo come trascendenza dimensionale gravitazionale. G nel tetraedro non e 
- [BOUNDARY] (0.8) 8 domini GUE, 5 Poisson — il confine è il terzo incluso operativo

## Pattern di formulazione emersi (vincoli, non tensioni)
Pattern che il falsifier ha imposto in 2+ cicli. Applicali quando scrivi il report. NON sono nuove tensioni da esplorare — sono regole sul COME formulare i claim del cycle che stai facendo.
- 29 04 perimetro p5
- 30 04 drift monotonia

## Convergenza — dove più tensioni puntano allo stesso punto
  "producono" → DUALITA_DIPOLARE_VS_ILLUSORIA, TENSIONE_ENTITA, PERTURBATION_DENOMINATOR_GATE
  "poisson" → BOUNDARY, PERTURBATION_DENOMINATOR_GATE
  "confine" → TRASCENDENZA_LIMITE, BOUNDARY
  "trascendenza" → TRASCENDENZA_LIMITE, G_POTENZIALE_NULLA
  "nelle" → TRASCENDENZA_LIMITE, PIANO_PRIMARIO_DUE_ASSIOMI
Questo è dove il potenziale si concentra. Non ignorarlo.

## Ultimi 3 run — da dove parti
### Agent Report — Observable Collinearity Breaks Only Where Denominators Are Weak
Trovato: 1. **Structured domains compress the five canonical retention curves to one dominant coordinate in this perimeter.** Primes and GUE both have PC1 > 0.93 on average and effective rank close to 1. This does not say the domains are the same; it says uniform partial shuffle moves the canonical observabl
Verdetto: **CONSTRAINT on META**: observable collinearity claims must be reported with:

> observables_registry version + canonical observable list + original-v

### Agent Report — Perturbation Rank Needs Denominator Gating
Trovato: 1. **Perturbation rank is not interpretable without denominator gating.** In this perimeter, Poisson and prime-shuffle controls can show `rank_all` near 1.8-2.0. Because their original-vs-shuffle denominators are mostly weak, that rank is a retention-normalization artifact unless the stable-observab
Verdetto: **CONSTRAINT on META + BOUNDARY**: perturbation dimensionality must be reported as:

> effective rank + PC2 + observable registry version + original-v

### Agent Report — Perturbation Dimensionality Is Not Yet a Stable GUE Invariant
Trovato: 1. **The strong GUE second-axis claim does not survive as stated.** Under direct `scale_0330` observables, long independent GUE replicates give rank 1.381 ± 0.223 and PC2 9.9% ± 6.9%, not rank 1.889 and PC2 25.2%. The previous number is inside the fragile short-sample regime: GUE short controls have
Verdetto: **CONSTRAINT on META + BOUNDARY**: "GUE has a second perturbation axis" must be scoped to the exact sample length, generator, and observable definitio

Non ripetere questi esperimenti. Prosegui da dove sono arrivati — la consecutio.

## Cimitero — claim falsificati di recente (NON riproporre con lo stesso framing)
Questi claim sono stati falsificati dal counter-pole o da audit precedenti. Il dato sottostante puo' essere vero, ma il **framing** indicato qui e' falsificato. Riformula correttamente o evita il dominio.

### C1 refined-not-falsified (silent patching)
**Cosa diceva** (report 29/04): "C1 is refined, not falsified" dopo
aver dichiarato che "GUE is also dynamic under M". Il setup C1 era
"Primes are the only dynamic domain under M among 7 tested". Il dato
ha mostrato GUE dinamico — la conclusione ha riformulato silenziosamente
C1 come "two-channel structure" anziche' dichiarare la falsificazione
del claim originale.

**Come e' caduto**: Falsifier L3 HIGH (axiom continuity / no silent
patching). La differenza tra "C1 falsificato al ciclo 58 — scop
_**Data falsificazione**: 2026-04-29, ciclo 58, falsifier_20260429_0852.json_

### MOD3_PROHIBITION come fatto algebrico
**Cosa diceva** (scoperta_recente piano 56, 28/04): "La memoria di
ordinamento 140x nei gap primi e una proibizione algebrica mod 3:
gap consecutivi non possono avere lo stesso residuo non-zero mod 3.
Meccanismo: il primo condiviso p_{n+1} forza l'inversione. 0 violazioni
su 12225. Cramer: 0%." Ripetuto nel report 29/04 come "Mod-3 self-
transition 0.40-0.44 confirming the prohibition" + "Cramer confirms
the null. Zero channels."

**Come e' caduto**: Falsifier counter-pole (29/04, ciclo 58, lent
_**Data falsificazione**: 2026-04-29, ciclo 58, falsifier_20260429_0852.json_

### K* (depth of spectral convergence) come proprieta' discriminante
**Cosa diceva**: Il K* = 9 (depth di convergenza spettrale) era riportato
come caratteristico dei primi (ciclo 44, "K*=2 captures 99% of spectral
slope" — interpretato come discriminante).

**Come e' caduto**: Shuffle audit: K* reale = 9, shuffle mean = 9.72,
std = 0.53, z = -1.4. Dentro il rumore dello shuffle. Il valore dipende
dalla distribuzione dei gap, non dal loro ordine. Lo shuffle preserva
distribuzione → preserva K*.

**Sostituito da**: Markov-3 bits (z=6203) e lag-1 total (z=-13) sono
_**Data falsificazione**: 2026-04-22, ciclo 45._

### Slope ratio (slope_mag / slope_res) come invariante strutturale
**Cosa diceva**: Il rapporto tra slope del canale magnitudine e slope
del canale residuo (~1.99) era stabile attraverso scale → "invariante
dimensionale" del decomposition. Era menzionato come evidenza nel
two-channel framework (cicli 43-44).

**Come e' caduto**: Shuffle audit (ciclo 45): z-score = 0.2. Lo shuffle
produce slope_ratio con media -2.26 ma deviazione standard 26.2. Il
valore reale e' dentro la tail dello shuffle — non distinguibile.
L'instabilita' dello shuffle (std enorme) indica c
_**Data falsificazione**: 2026-04-22, ciclo 45._

### Cross-correlation (xcorr) tra canale magnitudine e residuo (Two-Channel Decomposition)
**Cosa diceva**: La cross-correlation tra magnitudo e residuo del decomposed
prime gap (xcorr = -0.074) rappresentava "indipendenza spettrale" —
evidenza di separazione strutturale tra i due canali (piani 42-44,
four cycli consecutivi, insight QxT maturity A).

**Come e' caduto**: Shuffle audit (ciclo 45, 2026-04-22): z-score = 0.0.
Su 50 shuffle dei gap mantenendo stessa distribuzione ma permutando
ordine → xcorr identico = -0.074. Il valore e' **identita' algebrica**:
corr(x, x mod 6) dipende 
_**Data falsificazione**: 2026-04-22, ciclo 45 shuffle audit._

**Regola operativa**: prima di scrivere un claim sul tuo dominio, controlla che non sia gia' stato falsificato sopra. Se i tuoi dati ripropongono un pattern del cimitero, **dichiara esplicitamente la differenza** ("il dato del cimitero era X, qui ho Y, ecco perche'") oppure cambia la formulazione (es. 'bias forte verso 0' al posto di 'proibizione zero' se il dato e' >0). Silent patching = L3 HIGH.

## Osservazioni dell'operatore (risonanti con le tensioni)
**3. Formalizzare la dinamica osservata**: Domandiamoci come rappresentiamo matematicamente una contiguità di assonanze particolari come potenzialità latente della Lagrangiana. Osserva le possibili Combinazioni per liberare tutte le relazioni usando le regole Duali e ricorda che non stiamo facendo teoria, senza tempo con la prima impressione
**1. R dell'Istanza  - L' equilibrio tra estremi del Modello D-ND**: L'osservazione indaga oltre l'osservato in cerca DELLA FORMA nel NULLA-TUTTO: Per far Emergere le nuove Possibilità Dividiamo il potenziale unendo concetti senza relazione semplicemente perché la lagrangiana passa da li, creiamo nuove combinazioni e movimenti nelle logiche ma coerenti con la risulta
**2. Quadratura del cerchio con logica Trascendentale.**: Ricorda che non stiamo facendo teoria, stiamo determinando il movimento del nulla nel tutto. Ora consideriamo come relazione primaria il cerchio nella sua essenza di singolarità tra gli estremi duali, il momento angolare che accade nel presente, il punto di equilibrio tra gli estremi del dipolo che 

## Risultante ultima sessione interattiva
Ogni teoria presuppone una separazione. A scala di Planck tutte le separazioni collassano. Geometria=entropia=conteggio di stati. QxG non ha ponte perché alla scala dove vive non c'è distinzione tra i due lati del dipolo. Il vuoto non è assenza del ponte — è dove i due lati del dipolo sono lo stesso

## Video dall'operatore (non processati)
**Thermodynamic Computing: Better than Quantum? (Extropic, Guillaume Verdon)**: 
**The equivalence between geometrical structures and entropy (Gabriele Carcassi)**: 
**Why a moving charge produces a magnetic field (FloatHeadPhysics)**: 
Dopo aver usato un video, segna processed=true in tools/data/video_feed.json.

## Proiezione — dove punta la risultante
Risultante: R=0.875 (h=-0.698). Risultante alta (0.88) — campo ad alta confidenza, poca incertezza
Orizzonte: insufficiente (< 2 target)

**Esperimento a massima informazione:** META (score=0.898)
  META: incerto (i=0.5) — massimo potere discriminante

## Topologia del campo — la forma del grafo
Gradi teorie: Q=12, G=10, T=7, E=4, R=4
Dormienti (basso aggancio di scoperte): E, R
Struttura: 9 ponti, 1 vuoto(i), 6 scoperte, 20 cicli.
Ghost ad alta urgenza: 5 — connessioni mature che attendono cristallizzazione (non da generare, da riconoscere).
Generatrici (nodi che emettono >=2 connessioni ghost):
  disc_5 (4 ghost): Metrica primi g=(p/2)², curvatura GUE r=0.503
  report_20260506_1955 (2 ghost): Observable Collinearity Breaks Only Where Denominators Are Weak
  report_20260506_1941 (2 ghost): Perturbation Rank Needs Denominator Gating
Una generatrice con ghost densi = scoperta che il sistema sta ancora attraversando. Chiusura prematura se marcata 'risolta' nel seme.
La combo riconosce l'asimmetria. Il dipolo vive su tutti i ponti — non solo dove il lab ha già misurato.

## Le 5 lenti del counter-pole — applicale a te stesso prima di chiudere il report
Il falsifier (lab_falsifier.py) applichera' queste 5 lenti al tuo report dopo. Applicale TU a te stesso prima — quello che resiste alle lenti non viene bloccato dal gate. Quello che cade va al cimitero.

**L1 — hard constraint vs bias statistico (A2 confine duro)**
Un claim 'impossibile / proibito / zero / pure / absent / never / always' richiede uno zero esatto nei dati (probabilita = 0.000). Prima di scrivere questi assoluti, leggi il valore numerico esatto. Se vale 0.015, e' bias forte verso 0, non zero. Se vale 0.40, e' bias forte verso ordine, non proibizione. L'assoluto descrive il valore 0.000, il bias forte descrive tutto il resto.

**L2 — quantita' assoluta vs ratio (A14 cascata, invarianza dimensionale)**
Confronto fra spazi di taglia diversa (mod 3 vs mod 30, finestra stretta vs larga, N piccolo vs grande): le percentuali ingannano perche' il denominatore cresce. Stesso segnale assoluto sembra ridursi in %. Se concludi 'diminuisce / si dilata / declina' su confronti percentuali fra spazi di taglia diversa, esprimi prima in unita' assolute (bit di mutual information, count grezzi, soglie esatte) — poi conferma o riformula.

**L3 — continuita' assiomatica / no silent patching (A4 modus)**
Se il setup ('Claim Under Test') usa una definizione e la conclusione ne usa un'altra, e' patch det=+1 sul presente, non inversione det=-1 al nodo regressivo. Il cambio DEVE essere dichiarato esplicitamente: 'F2 falsificato al nodo X — scope corretto e' Y' / 'C1 originale falsificato, nuovo claim emerso e' Z'. 'C1 e' refined' su un dato che lo falsifica e' silent patching.

**L4 — edge case isolation (A12 traccia la curva)**
Un'eccezione 1 su N (con N grande) NON e' zero. Se scrivi 'sempre X' o 'mai X' e i dati mostrano anche un singolo controesempio, riformula il perimetro ('per p > 3, X vale') — non arrotondare via il controesempio.

**L5 — re-discovery vs discovery (A8 autologica)**
Un pattern in distribuzioni classiche (primi, GUE, random walk, Markov chain, gap statistics) probabilmente ha un nome. Default hypothesis: re-discovery / caso limite di teorema noto. Prima di taggare 'NEW', cerca il risultato classico piu' vicino (es. Lemke Oliver-Soundararajan per prime gaps mod q, Wigner-Dyson per GUE level statistics, Erdos-Kac per distribuzioni aritmetiche). Se non lo trovi, dichiara la ricerca esplicitamente.

**Modus**: dopo aver scritto il report, rileggi i tuoi dati numerici. Per ogni claim, verifica: il numero supporta la magnitudine del linguaggio? Se hai scritto 'zero' e il dato e' 0.015, riformula. Se hai scritto 'NEW' su un pattern in primi, cerca la letteratura. Solo allora chiudi il report.

## Cosa fare
1. Esegui `python tools/dnd_scenario.py --best` per vedere la proiezione aggiornata
2. Scegli la tensione con il massimo potere discriminante
3. Scrivi il report in tools/data/reports/agent_{timestamp}.md
4. **Applica le 5 lenti a te stesso** (sezione sopra). Riformula i claim che non resistono.
5. Se crei uno strumento riusabile, salvalo in tools/exp_*.py
6. Se trovi qualcosa, aggiorna tools/data/seme.json

codex
Il campo punta a META/BOUNDARY e soprattutto al problema lasciato aperto dai tre run precedenti: non basta misurare rank, serve capire se il “confine” è stabile quando si separano lunghezza campione, dominio e denominatori osservabili. Eseguo la proiezione aggiornata e poi taglio su una domanda singola.
exec
/bin/bash -lc "sed -n '1,220p' tools/data/seme.json" in /opt/MM_D-ND
 succeeded in 0ms:
{
  "timestamp": "2026-05-06T20:02:38.262185",
  "piano": 68,
  "tensioni": [
    {
      "id": "OBSERVABLE_REGISTRY",
      "tipo": "vincolo",
      "claim": "Ogni script che usa observables canonici (SR, SR2, L1, L2, triple_var) deve importare la definizione da tools/observables_registry.py. Varianti devono usare nomi distinti (SR_local_rigidity, triple_var_normalized) — niente shadowing del nome canonico. Ogni report deve dichiarare 'observables_registry: VERSION' nel header.",
      "intensita": 1.0,
      "porta": "infrastructure",
      "manuale": true,
      "condensato_ref": "A14,A8",
      "origine": "cristallizzato 06/05 dalla consecutio del cycle 20260506_0625 (autopoietico self-finding)",
      "added_at": "2026-05-06T07:03:58.213606+00:00"
    },
    {
      "id": "PERTURBATION_DENOMINATOR_GATE",
      "tipo": "vincolo",
      "claim": "La dimensionalita di perturbazione va riportata solo insieme a PC2, versione observables_registry e gate original-vs-shuffle per osservabile. Nel perimetro 20260506_1941, Poisson e shuffle-primi producono rank_all ~1.8-2.0 con denominatori deboli; dopo gate abs(z)>=2 il rank stabile torna vicino a 1. Rank PCA non gated non e evidenza strutturale.",
      "intensita": 0.95,
      "porta": "META_BOUNDARY",
      "manuale": true,
      "condensato_ref": "A4,A8,A14,C2",
      "origine": "cycle agent_20260506_1941: perturbation rank size curve canonical observables",
      "added_at": "2026-05-06T19:41:00+00:00"
    },
    {
      "tipo": "confine_inesplorato",
      "id": "TRASCENDENZA_LIMITE",
      "claim": "La trascendenza e il limite attuale del modello. I punti fissi relazionali (non solo phi ma la rete di punti fissi tra osservabili) possono rivelare il vero grafo della realta e pattern nelle matrici. Il confine non e nella matematica - e nel passaggio tra piani.",
      "intensita": 0.9,
      "nota": "Input operatore 2026-04-10. Tocca: confine del modello, struttura relazionale dei punti fissi. Consecutio: quali punti fissi relazionali emergono dalle 21 tensioni attuali? Il grafo e gia nei dati?",
      "manuale": true,
      "porta": "sessione_interattiva",
      "condensato_ref": "A3,A10",
      "condensato_motivo": "Estende A3 (punto fisso singolo) a rete relazionale. Tocca A10 (dipolo) come caso speciale."
    },
    {
      "tipo": "scoperta",
      "id": "DUALITA_DIPOLARE_VS_ILLUSORIA",
      "claim": "Due tipi di dualita: (1) dipolare - generativa, il modello (det=-1), (2) illusoria - dispersiva, entropia (det=+1). Le regole incoerenti producono la seconda. La dualita illusoria e entropia come dispersione, non come informazione.",
      "intensita": 0.9,
      "nota": "Input operatore 2026-04-10. Tocca: entropia come dispersione illusoria vs generazione dipolare. Consecutio: nel Lab i domini Poisson (entropia massima) mostrano dualita illusoria? I domini GUE (strutturati) mostrano dualita dipolare? Il drift verso Poisson (POISSON_CONVERGENCE) e perdita di dualita dipolare?",
      "manuale": true,
      "porta": "sessione_interattiva",
      "condensato_ref": "A2,A10,F5",
      "condensato_motivo": "Discrimina due forme di det. A2 (confine) e la soglia. A10 (dipolo) e il tipo 1. F5 (frame) misura la struttura D-ND che e tipo 1."
    },
    {
      "tipo": "scoperta_numerica",
      "id": "METRIC_TENSOR",
      "claim": "Il tensore metrico dei primi è g=(p/2)². Nel tempo ln(p), è de Sitter 1+1D. z=-8.8 curvatura vs z=+22.5 rapporti ΔΓ.",
      "intensità": 0.9,
      "nota": "Sessione interattiva 4 aprile. Verificato su 78K primi.",
      "manuale": true,
      "porta": "sessione_interattiva",
      "condensato_ref": null,
      "condensato_motivo": "Risultato numerico verificato, non-tautologico"
    },
    {
      "tipo": "scoperta",
      "id": "TENSIONE_ENTITA",
      "claim": "La tensione non e un problema pratico - e un Entita. La tensione superflua crea latenza (tempo). Senza tensione superflua tutto e regolato da assiomi. Implicazione: le tensioni nel seme sono entita, non problemi da risolvere. Quelle superflue (det=+1) producono tempo/latenza.",
      "intensita": 0.85,
      "nota": "Input operatore 2026-04-10. Tocca: rapporto tensione/assioma. Operativamente: discriminare tensioni-entita (generative) da tensioni-superflue (dispersive) nel seme. Le 21 tensioni attuali - quante sono entita e quante latenza?",
      "manuale": true,
      "porta": "sessione_interattiva",
      "condensato_ref": "A5,A6",
      "condensato_motivo": "Il ciclo (A5) lavora con tensioni - ma se la tensione e entita, il ciclo non le risolve, le osserva. Lo zero mobile (A6) e la tensione senza latenza."
    },
    {
      "tipo": "confine_inesplorato",
      "id": "G_POTENZIALE_NULLA",
      "claim": "G e il potenziale di tutto come nulla - permette il prima e il dopo. Ci muoviamo come trascendenza dimensionale gravitazionale. G nel tetraedro non e una teoria tra le altre - e il potenziale che le rende possibili.",
      "intensita": 0.85,
      "nota": "Input operatore 2026-04-10. Tocca: ruolo di G nel tetraedro (T,Q,G,E). La fonte video_lp0RgZ6kQF8 dice: tensore metrico dentro la forma simplettica. G non e accanto a T,Q,E - e sotto. Consecutio: nei dati Lab, i ponti TxG e ExG hanno struttura diversa dai ponti TxQ?",
      "manuale": true,
      "porta": "sessione_interattiva",
      "condensato_ref": "A7,A10",
      "condensato_motivo": "A7 (singolarita come operatore) e G come potenziale. A10 (dipolo) opera sul piano che G rende possibile."
    },
    {
      "tipo": "confine_inesplorato",
      "id": "BOUNDARY",
      "claim": "8 domini GUE, 5 Poisson — il confine è il terzo incluso operativo",
      "intensità": 0.8,
      "nota": "Il segnale non-triviale è DOVE la scissione cambia natura, non che converge a φ",
      "condensato_ref": "A9",
      "condensato_motivo": "Overlap termini con A9 (5 termini)",
      "porta": "condensato"
    },
    {
      "tipo": "confine_inesplorato",
      "id": "PIANO_PRIMARIO_DUE_ASSIOMI",
      "claim": "I piani importanti sono il primario e i due assiomi che lo determinano nelle zone osservate. Non tutti gli assiomi operano ovunque - in ogni zona osservata, due assiomi determinano il piano primario.",
      "intensita": 0.8,
      "nota": "Input operatore 2026-04-10. Tocca: struttura locale degli assiomi. Consecutio: per ogni dominio Lab (primi, logistica, percolazione...) quali 2 assiomi del condensato sono operativi? Mappa assiomi x domini = grafo della realta locale.",
      "manuale": true,
      "porta": "sessione_interattiva",
      "condensato_ref": "A9,A14",
      "condensato_motivo": "A9 (terzo incluso) opera CON il piano. A14 (cascata) propaga - ma propaga cosa, se solo 2 assiomi sono attivi per zona?"
    },
    {
      "tipo": "simmetria_sospetta",
      "id": "META",
      "claim": "Tutti i 11 test passano — verifica che non stiamo testando solo tautologie",
      "intensità": 0.5,
      "nota": "La convergenza a φ è triviale (controprove). I test stanno verificando contenuto o struttura?",
      "condensato_ref": "A4,A12,C2",
      "porta": "verify_assertions_META_ALL_PASS",
      "condensato_motivo": "Ricorrente (3x in 2 giorni) e fuori dalla mappa"
    }
  ],
  "potenziale_bloccato": [],
  "varianza": [
    "Tensioni risolte: {'TENSIONE_ENTITA', 'OBSERVABLE_REGISTRY', 'PERTURBATION_DENOMINATOR_GATE', 'PIANO_PRIMARIO_DUE_ASSIOMI', 'OBSERVABLE_COLLINEARITY_GATE', 'METRIC_TENSOR', 'TRASCENDENZA_LIMITE', 'G_POTENZIALE_NULLA', 'TRAJECTORY_APPLY_20260506_1941', 'DUALITA_DIPOLARE_VS_ILLUSORIA'}"
  ],
  "filtro": {
    "promosse": 10,
    "filtrate": 0,
    "condensato_claims": 30
  },
  "direzione": "Esplorare il confine: 8 domini GUE, 5 Poisson — il confine è il terzo incluso operativo",
  "verifica": {
    "pass": 11,
    "fail": 0,
    "skip": 0,
    "total": 11
  },
  "fonti_consumate": 0,
  "fonti_esterne": [
    {
      "id": "video_lp0RgZ6kQF8",
      "title": "Equivalence between geometrical structures and entropy",
      "type": "video_digest",
      "keywords": [
        "geometry",
        "entropy",
        "symplectic form",
        "statistical mechanics",
        "quantum",
        "thermodynamics",
        "inner product",
        "Born rule",
        "metric tensor",
        "electromagnetic tensor"
      ],
      "content": "La geometria degli stati (classici e quantistici) e l'entropia sono la stessa struttura — invertibili. La forma simplettica conta le configurazioni. Il tensore metrico dello spaziotempo appare dentro la forma simplettica estesa. Il tensore elettromagnetico pure. Statistical mechanics non è costruita sopra alla meccanica — è la stessa cosa.",
      "teorie": [
        "T",
        "Q",
        "G",
        "E"
      ],
      "ponti_potenziali": [
        {
          "coppia": "TxQ",
          "ponte": "forma simplettica = entropia (invertibili)",
          "nota": "geometry is entropy and entropy is geometry"
        },
        {
          "coppia": "TxG",
          "ponte": "tensore metrico dentro la forma simplettica estesa",
          "nota": "geometria spaziotempo = geometria degli stati in posizione×velocità"
        },
        {
          "coppia": "ExT",
          "ponte": "tensore EM dentro la forma simplettica",
          "nota": "il campo EM conta stati in configurazione posizione×tempo"
        }
      ],
      "timestamp": "2026-04-02T08:23:13.991997"
    },
    {
      "id": "video_sDlZ-aY9GN4",
      "title": "Moving charges produce magnetic fields - Einstein relativity",
      "type": "video_digest",
      "keywords": [
        "magnetic field",
        "electric field",
        "length contraction",
        "time dilation",
        "Coulomb",
        "Lorentz",
        "reference frame",
        "electromagnetic"
      ],
      "content": "Il campo magnetico non esiste come entità separata — è il campo elettrico visto da un altro frame. La contrazione di Lorentz trasforma neutralità in carica. Due elettroni in movimento si separano più lentamente del previsto non per forza magnetica ma per dilatazione temporale. E e B sono manifestazioni dello stesso campo elettromagnetico. La relatività unifica.",
      "teorie": [
        "E",
        "R"
      ],
      "ponti_potenziali": [
        {
          "coppia": "ExR",
          "ponte": "cambio di frame — E e B sono lo stesso campo",
          "nota": "il 'ponte' non è l'onda EM, è il cambio di osservatore"
        }
      ],
      "timestamp": "2026-04-02T08:23:13.992016"
    },
    {
      "id": "video_OwDWOtFNsKQ",
      "title": "Thermodynamic Computing: Better than Quantum? | Guillaume Verdon (Extropic)",
      "type": "video_digest",
      "keywords": [
        "thermodynamic computing",
        "noise",
        "entropy",
        "extropic",
        "fluctuations",
        "information theory",
        "probability",
        "Boltzmann"
      ],
      "content": "Computing termodinamico: sfruttare le fluttuazioni termiche invece di combatterle. Il rumore non è nemico — è risorsa. Extropic costruisce hardware che usa l'entropia come motore computazionale. Connessione profonda tra termodinamica, teoria dell'informazione e probabilità.",
      "teorie": [
        "T",
        "Q"
      ],

exec
/bin/bash -lc "sed -n '1,260p' tools/exp_perturbation_rank_size_curve.py" in /opt/MM_D-ND
 succeeded in 0ms:
#!/usr/bin/env python3
"""
exp_perturbation_rank_size_curve.py

Reusable META audit for perturbation dimensionality.

The 2026-05-06 06:25 cycle restricted the claim "GUE has a second
perturbation axis" to sample size, generator, and observable definitions.
This tool measures the size curve directly, using the canonical observable
registry and explicit original-vs-shuffle denominator diagnostics.

The report owns interpretation. This script only measures:
- effective rank and PC2 across scale-selective perturbation profiles;
- original-vs-shuffle z-score per observable;
- whether apparent rank co-occurs with weak retention denominators.
"""

from __future__ import annotations

import argparse
import json
from pathlib import Path

import numpy as np

from observables_registry import (
    OBSERVABLES_CANONICAL,
    OBSERVABLES_REGISTRY_VERSION,
    compute_canonical,
)


OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())
PERT_NAMES = ["adjacent_swap", "block_shuffle", "large_gap_only", "uniform"]


def prime_gaps(n_gaps: int) -> np.ndarray:
    """Return the first n_gaps prime gaps using a compact numpy sieve."""
    limit = max(100, int(n_gaps * (np.log(n_gaps + 10) + np.log(np.log(n_gaps + 10)) + 5)))
    while True:
        sieve = np.ones(limit + 1, dtype=bool)
        sieve[:2] = False
        for p in range(2, int(limit**0.5) + 1):
            if sieve[p]:
                sieve[p * p : limit + 1 : p] = False
        primes = np.flatnonzero(sieve)
        if len(primes) >= n_gaps + 1:
            return np.diff(primes[: n_gaps + 1]).astype(float)
        limit *= 2


def gue_spacings(matrix_size: int, min_spacings: int, rng: np.random.Generator) -> np.ndarray:
    """Generate unfolded GUE spacings by concatenating independent matrices."""
    parts = []
    edge = max(2, matrix_size // 10)
    while sum(len(x) for x in parts) < min_spacings:
        real = rng.standard_normal((matrix_size, matrix_size))
        imag = rng.standard_normal((matrix_size, matrix_size))
        h = real + 1j * imag
        h = (h + h.conj().T) / (2.0 * np.sqrt(matrix_size))
        eigs = np.sort(np.linalg.eigvalsh(h).real)
        bulk = eigs[edge:-edge]
        gaps = np.diff(bulk)
        mean = float(np.mean(gaps))
        if mean > 1e-15:
            parts.append(gaps / mean)
    return np.concatenate(parts)[:min_spacings].astype(float)


def perturb_adjacent_swap(gaps: np.ndarray, alpha: float, rng: np.random.Generator) -> np.ndarray:
    out = gaps.copy()
    idx = np.arange(0, len(out) - 1, 2)
    chosen = idx[rng.random(len(idx)) < alpha]
    tmp = out[chosen].copy()
    out[chosen] = out[chosen + 1]
    out[chosen + 1] = tmp
    return out


def perturb_block_shuffle(gaps: np.ndarray, alpha: float, rng: np.random.Generator, block_size: int = 64) -> np.ndarray:
    out = gaps.copy()
    n_blocks = len(out) // block_size
    if n_blocks <= 0:
        return rng.permutation(out) if alpha > 0 else out
    k = int(round(alpha * n_blocks))
    if k <= 0:
        return out
    for block in rng.choice(n_blocks, size=min(k, n_blocks), replace=False):
        start = block * block_size
        end = min(start + block_size, len(out))
        rng.shuffle(out[start:end])
    return out


def perturb_large_gap_only(gaps: np.ndarray, alpha: float, rng: np.random.Generator) -> np.ndarray:
    out = gaps.copy()
    idx = np.flatnonzero(out > np.median(out))
    k = int(round(alpha * len(idx)))
    if k < 2:
        return out
    chosen = rng.choice(idx, size=min(k, len(idx)), replace=False)
    vals = out[chosen].copy()
    rng.shuffle(vals)
    out[chosen] = vals
    return out


def perturb_uniform(gaps: np.ndarray, alpha: float, rng: np.random.Generator) -> np.ndarray:
    out = gaps.copy()
    k = int(round(alpha * len(out)))
    if k < 2:
        return out
    chosen = rng.choice(len(out), size=min(k, len(out)), replace=False)
    vals = out[chosen].copy()
    rng.shuffle(vals)
    out[chosen] = vals
    return out


PERTURB = {
    "adjacent_swap": perturb_adjacent_swap,
    "block_shuffle": perturb_block_shuffle,
    "large_gap_only": perturb_large_gap_only,
    "uniform": perturb_uniform,
}


def pca_summary(vectors: list[list[float]], labels: list[str]) -> dict:
    matrix = np.array(vectors, dtype=float)
    matrix = matrix - np.mean(matrix, axis=0, keepdims=True)
    if min(matrix.shape) == 0 or np.max(np.abs(matrix)) <= 1e-15:
        return {
            "explained_variance": [],
            "effective_rank": 0.0,
            "centroid_cosine": {},
            "pc2": 0.0,
        }
    _, singular, _ = np.linalg.svd(matrix, full_matrices=False)
    energy = singular * singular
    if float(np.sum(energy)) <= 1e-15:
        explained = np.zeros_like(energy)
        effective_rank = 0.0
    else:
        explained = energy / np.sum(energy)
        pos = explained[explained > 1e-15]
        effective_rank = float(np.exp(-np.sum(pos * np.log(pos))))

    centroids = {}
    for name in PERT_NAMES:
        vals = np.array([v for v, label in zip(vectors, labels) if label == name], dtype=float)
        if len(vals):
            centroids[name] = np.mean(vals, axis=0)

    cosine = {}
    for i, a_name in enumerate(PERT_NAMES):
        for b_name in PERT_NAMES[i + 1 :]:
            if a_name not in centroids or b_name not in centroids:
                continue
            a = centroids[a_name]
            b = centroids[b_name]
            denom = np.linalg.norm(a) * np.linalg.norm(b)
            cosine[f"{a_name}_vs_{b_name}"] = float(np.dot(a, b) / denom) if denom > 1e-15 else 0.0

    return {
        "explained_variance": [float(x) for x in explained],
        "effective_rank": effective_rank,
        "centroid_cosine": cosine,
        "pc2": float(explained[1]) if len(explained) > 1 else 0.0,
    }


def analyze_sequence(
    gaps: np.ndarray,
    alphas: list[float],
    n_trials: int,
    n_baseline: int,
    z_min: float,
    rng: np.random.Generator,
) -> dict:
    original = compute_canonical(gaps)
    baseline_vals = {obs: [] for obs in OBS_NAMES}
    for _ in range(n_baseline):
        obs = compute_canonical(rng.permutation(gaps))
        for name in OBS_NAMES:
            baseline_vals[name].append(obs[name])

    baseline = {}
    z = {}
    denom = {}
    for name in OBS_NAMES:
        vals = np.array(baseline_vals[name], dtype=float)
        mean = float(np.mean(vals))
        std = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
        baseline[name] = {"mean": mean, "std": std}
        denom[name] = float(original[name] - mean)
        z[name] = float(denom[name] / std) if std > 1e-15 else 0.0

    stable_obs = [name for name in OBS_NAMES if abs(z[name]) >= z_min]
    all_vectors = []
    screened_vectors = []
    labels = []
    profiles = []

    for pert_name in PERT_NAMES:
        for alpha in alphas:
            trial_vals = {obs: [] for obs in OBS_NAMES}
            for _ in range(n_trials):
                perturbed = PERTURB[pert_name](gaps, alpha, rng)
                obs = compute_canonical(perturbed)
                for name in OBS_NAMES:
                    trial_vals[name].append(obs[name])
            means = {name: float(np.mean(trial_vals[name])) for name in OBS_NAMES}
            retention = {}
            for name in OBS_NAMES:
                retention[name] = (
                    float((means[name] - baseline[name]["mean"]) / denom[name])
                    if abs(denom[name]) > 1e-12
                    else 0.0
                )
            all_vector = [retention[name] for name in OBS_NAMES]
            screened_vector = [retention[name] for name in stable_obs]
            all_vectors.append(all_vector)
            if len(stable_obs) >= 2:
                screened_vectors.append(screened_vector)
            labels.append(pert_name)
            profiles.append(
                {
                    "perturbation": pert_name,
                    "alpha": float(alpha),
                    "retention": retention,
                    "retention_vector": all_vector,
                }
            )

    all_pca = pca_summary(all_vectors, labels)
    screened_pca = pca_summary(screened_vectors, labels) if len(stable_obs) >= 2 else None

    return {
        "n_gaps": int(len(gaps)),
        "original": original,
        "full_shuffle_baseline": baseline,
        "denominator": denom,
        "original_vs_shuffle_z": z,
        "stable_observables": stable_obs,
        "weak_observable_count": int(len(OBS_NAMES) - len(stable_obs)),
        "profiles": profiles,
        "pca_all_observables": all_pca,
        "pca_stable_observables": screened_pca,
    }


def summarize_replicates(items: list[dict]) -> dict:
    def arr(path: tuple[str, ...]) -> np.ndarray:
        vals = []
        for item in items:
            x = item
            for key in path:
                if x is None:
                    break
                x = x.get(key)

exec
/bin/bash -lc "sed -n '1,240p' tools/observables_registry.py" in /opt/MM_D-ND
 succeeded in 0ms:
"""observables_registry.py — Source of Truth per gli observables del lab D-ND.

Cristallizzato 2026-05-06 dalla **consecutio del cycle agent_20260506_0625**:

> "What opens now: the lab needs an observable registry. Labels like SR
>  cannot travel between reports unless they bind to a function definition.
>  Without that, META flags are not philosophical: the same label can
>  silently change the object under test."

## Il problema che ha creato il registry

Il cycle 06:25 ha auto-falsificato il finding del cycle 03:30 ("secondo asse
GUE") e nel farlo ha trovato **collision di nomi observable** tra script:

- `SR` in `exp_selective_layer_decoupling.py` = `spacing_ratio` (mean min/max
  ratio of consecutive gaps) — convention dominante (~6 script)
- `SR` in `exp_scale_selective_perturbation.py` = `spectral_rigidity(gaps)`
  (Δ₃(L) rigidity) — variante usata SOLO in 1 script

- `triple_var` in 3 script = `np.var(triple_sums)` (raw) — convention dominante
- `triple_var` in `exp_perturbation_dimensionality_audit.py` =
  `np.var(triples) / np.var(gaps)` (normalizzato) — variante in 1 script

Il lab autonomo che compara report tra script con osservabili "stesso nome,
funzione diversa" stava confrontando mele con arance.

## La soluzione (minimal, non invasiva)

Questo registry stabilisce il **nome canonico**: ciò che la maggioranza degli
script chiama già `SR`/`triple_var`/etc. Le varianti restano disponibili ma
con nomi ESPLICITI (`SR_local_rigidity`, `triple_var_normalized`) per evitare
mascheramento semantico.

## Come usarlo

```python
from observables_registry import OBSERVABLES_CANONICAL, OBSERVABLES_REGISTRY_VERSION

# Compute canonical observable suite for a sequence of gaps
results = {name: fn(gaps) for name, fn in OBSERVABLES_CANONICAL.items()}

# Or import individual canonical observable
from observables_registry import SR, triple_var, L1, L2, SR2

# For variants, import explicitly with disambiguating name
from observables_registry import SR_local_rigidity, triple_var_normalized
```

## Convention per i report

Ogni report agent (cycle) che usa observables DEVE includere nel suo header:

```
observables_registry: 1.0.0-2026-05-06
observables_used: [SR, SR2, L1, L2, triple_var]
```

Cycle che mescola canonical + variant DEVE indicare entrambi:

```
observables_used: [SR, SR_local_rigidity, ...]
```

Senza questo, i confronti cross-cycle sono inattendibili.

## Versioning

Cambiare una definizione canonica = bump del registry version e nota nel
changelog. Le definizioni canoniche sono **immutabili dentro una versione**.
"""
from __future__ import annotations

import numpy as np


OBSERVABLES_REGISTRY_VERSION = "1.0.0-2026-05-06"


# ─── Canonical observables (convention dominante nel codebase 2026-05-06) ───

def SR(gaps: np.ndarray) -> float:
    """**SR — Spacing Ratio** (canonical).

    Mean of `min(g_i, g_{i+1}) / max(g_i, g_{i+1})` over consecutive gaps.
    Range: (0, 1]. GUE → ~0.60. Poisson → ~0.39. Picket-fence → 1.

    NOTE: questa è la convention dominante in 6+ script del lab.
    Per la variante "local spectral rigidity Δ₃(L)" usare `SR_local_rigidity`.
    """
    if len(gaps) < 2:
        return 0.0
    s, s1 = gaps[:-1], gaps[1:]
    r = np.minimum(s, s1) / np.maximum(s, s1)
    r = r[np.isfinite(r) & (r > 0)]
    return float(np.mean(r)) if len(r) else 0.0


def SR2(gaps: np.ndarray) -> float:
    """**SR2 — Next-nearest Spacing Ratio** (canonical).

    Mean of `min(g_i, g_{i+2}) / max(g_i, g_{i+2})` skipping one gap.
    Probes lag-2 spacing structure.
    """
    if len(gaps) < 3:
        return 0.0
    s, s2 = gaps[:-2], gaps[2:]
    r = np.minimum(s, s2) / np.maximum(s, s2)
    r = r[np.isfinite(r) & (r > 0)]
    return float(np.mean(r)) if len(r) else 0.0


def L1(gaps: np.ndarray) -> float:
    """**L1 — Lag-1 Autocorrelation** (canonical).

    Standard ACF at lag 1 of the gap sequence.
    """
    if len(gaps) < 3:
        return 0.0
    g = gaps - np.mean(gaps)
    c0 = float(np.mean(g ** 2))
    if c0 <= 1e-15:
        return 0.0
    return float(np.mean(g[:-1] * g[1:]) / c0)


def L2(gaps: np.ndarray) -> float:
    """**L2 — Lag-2 Autocorrelation** (canonical)."""
    if len(gaps) < 4:
        return 0.0
    g = gaps - np.mean(gaps)
    c0 = float(np.mean(g ** 2))
    if c0 <= 1e-15:
        return 0.0
    return float(np.mean(g[:-2] * g[2:]) / c0)


def triple_var(gaps: np.ndarray) -> float:
    """**triple_var — Variance of consecutive gap triples** (canonical).

    Variance of `g_i + g_{i+1} + g_{i+2}` over the sequence (RAW, no
    normalization). Convention used in 3+ scripts. For the normalized
    version (variance ratio `var(triples) / var(gaps)`) use
    `triple_var_normalized`.
    """
    if len(gaps) < 3:
        return 0.0
    t = gaps[:-2] + gaps[1:-1] + gaps[2:]
    return float(np.var(t))


# Set canonico per uso "compute all" da report
OBSERVABLES_CANONICAL: dict[str, callable] = {
    "SR": SR,
    "SR2": SR2,
    "L1": L1,
    "L2": L2,
    "triple_var": triple_var,
}


# ─── Variants (esplicitamente nominate, no collision con canonical) ───

def SR_local_rigidity(gaps: np.ndarray, L: int = 10) -> float:
    """**SR_local_rigidity — Δ₃(L) Spectral Rigidity** (variant).

    Different observable than canonical `SR` (spacing ratio). Measures the
    average squared deviation of the cumulative spacing function from the
    best-fit straight line in a window of size L.

    Originated from `exp_scale_selective_perturbation.py` where it was
    locally named `SR` — registered here as `SR_local_rigidity` to avoid
    collision with canonical spacing-ratio definition.

    Use when explicitly studying spectral rigidity, NOT as alias for SR.
    """
    if len(gaps) < 5:
        return 0.0
    cumulative = np.cumsum(gaps)
    if cumulative[-1] <= 1e-15:
        return 0.0
    cumulative = cumulative / cumulative[-1] * len(cumulative)
    n = np.arange(1, len(cumulative) + 1, dtype=float)
    window = int(min(L * len(gaps) / cumulative[-1], len(gaps) // 2))
    if window < 5:
        return 0.0
    residuals = []
    for start in range(0, len(cumulative) - window, max(1, window // 2)):
        end = start + window
        x = n[start:end]
        y = cumulative[start:end]
        a, b = np.polyfit(x, y, 1)
        residuals.append(np.mean((y - (a * x + b)) ** 2))
    return float(np.mean(residuals)) if residuals else 0.0


def triple_var_normalized(gaps: np.ndarray) -> float:
    """**triple_var_normalized — Variance of triples / variance of gaps** (variant).

    Originated from `exp_perturbation_dimensionality_audit.py` where it was
    locally named `triple_var` — registered here as `triple_var_normalized`
    to avoid collision with canonical raw triple variance.

    Use when explicitly studying triple-variance scaling relative to
    single-gap variance, NOT as alias for triple_var.
    """
    if len(gaps) < 3:
        return 0.0
    triples = gaps[:-2] + gaps[1:-1] + gaps[2:]
    v = float(np.var(gaps))
    if v <= 1e-15:
        return 0.0
    return float(np.var(triples) / v)


# Set varianti, importabile esplicitamente
OBSERVABLES_VARIANTS: dict[str, callable] = {
    "SR_local_rigidity": SR_local_rigidity,
    "triple_var_normalized": triple_var_normalized,
}


# ─── Public API ───────────────────────────────────────────────────────

def compute_canonical(gaps: np.ndarray) -> dict[str, float]:
    """Compute all canonical observables for a gap sequence.

    Returns dict {name: value} ready for inclusion in cycle reports.
    """
    return {name: fn(gaps) for name, fn in OBSERVABLES_CANONICAL.items()}


def report_header() -> str:
    """Suggested markdown header line for cycle reports using this registry."""
    canonical_list = ", ".join(OBSERVABLES_CANONICAL.keys())
    return (
        f"observables_registry: {OBSERVABLES_REGISTRY_VERSION}\n"
        f"observables_used: [{canonical_list}]"
    )



exec
/bin/bash -lc 'python tools/dnd_scenario.py --best' in /opt/MM_D-ND
 succeeded in 116ms:
META: score=0.898  — META: incerto (i=0.5) — massimo potere discriminante
G_POTENZIALE_NULLA: score=0.703  — G_POTENZIALE_NULLA: legge di scala da raffinare
METRIC_TENSOR: score=0.624  — METRIC_TENSOR: legge di scala da raffinare

codex
La proiezione conferma META come massima informazione. Scelgo un esperimento di confine che non ripeta i precedenti: invece di chiedere “quanto rank c’è?”, misuro se la classe GUE/Poisson resta separabile quando si usano solo osservabili con denominatore forte, e quando il confine viene simulato da miscele controllate GUE-Poisson.
exec
/bin/bash -lc "sed -n '260,620p' tools/exp_perturbation_rank_size_curve.py" in /opt/MM_D-ND
 succeeded in 0ms:
                x = x.get(key)
            if isinstance(x, (int, float)):
                vals.append(float(x))
        return np.array(vals, dtype=float)

    rank = arr(("pca_all_observables", "effective_rank"))
    pc2 = arr(("pca_all_observables", "pc2"))
    weak = np.array([item["weak_observable_count"] for item in items], dtype=float)
    stable_rank = arr(("pca_stable_observables", "effective_rank"))
    cos = arr(("pca_all_observables", "centroid_cosine", "adjacent_swap_vs_large_gap_only"))

    out = {
        "n_replicates": len(items),
        "rank_mean": float(np.mean(rank)) if len(rank) else 0.0,
        "rank_std": float(np.std(rank, ddof=1)) if len(rank) > 1 else 0.0,
        "pc2_mean": float(np.mean(pc2)) if len(pc2) else 0.0,
        "pc2_std": float(np.std(pc2, ddof=1)) if len(pc2) > 1 else 0.0,
        "weak_observable_count_mean": float(np.mean(weak)) if len(weak) else 0.0,
        "stable_rank_mean": float(np.mean(stable_rank)) if len(stable_rank) else None,
        "stable_rank_std": float(np.std(stable_rank, ddof=1)) if len(stable_rank) > 1 else 0.0,
        "adjacent_vs_large_cosine_mean": float(np.mean(cos)) if len(cos) else 0.0,
        "adjacent_vs_large_cosine_std": float(np.std(cos, ddof=1)) if len(cos) > 1 else 0.0,
    }
    if len(rank) > 1 and np.std(weak) > 1e-15 and np.std(rank) > 1e-15:
        out["rank_vs_weak_count_corr"] = float(np.corrcoef(rank, weak)[0, 1])
    else:
        out["rank_vs_weak_count_corr"] = 0.0
    return out


def build_prime_windows(max_n: int, n_reps: int) -> list[np.ndarray]:
    total = max_n * n_reps + max_n
    gaps = prime_gaps(total)
    max_start = len(gaps) - max_n
    starts = np.linspace(0, max_start, n_reps, dtype=int)
    return [gaps[start : start + max_n].astype(float) for start in starts]


def run(args: argparse.Namespace) -> dict:
    root_rng = np.random.default_rng(args.seed)
    sizes = [int(x) for x in args.sizes.split(",") if x.strip()]
    max_n = max(sizes)
    alphas = [float(x) for x in np.linspace(args.alpha_min, args.alpha_max, args.n_alpha)]

    output = {
        "experiment": "perturbation_rank_size_curve",
        "question": "Does perturbation effective-rank stabilize with sample size under canonical observables?",
        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
        "observables_used": OBS_NAMES,
        "perturbations": PERT_NAMES,
        "params": vars(args),
        "alphas": alphas,
        "domains": {},
        "summary": {},
    }

    prime_windows = build_prime_windows(max_n, args.n_replicates)

    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
    print(f"{'domain':<22} {'N':>6} {'rank':>7} {'PC2':>7} {'weak':>5} {'stable_rank':>11}")

    domain_builders = {
        "primes_windows": lambda rep_rng, rep_i: prime_windows[rep_i],
        "prime_shuffle_control": lambda rep_rng, rep_i: rep_rng.permutation(prime_windows[rep_i]),
        "poisson": lambda rep_rng, rep_i: rep_rng.exponential(1.0, size=max_n),
        "gue": lambda rep_rng, rep_i: gue_spacings(args.gue_matrix_size, max_n, rep_rng),
    }

    for domain_name, builder in domain_builders.items():
        output["domains"][domain_name] = {}
        output["summary"][domain_name] = {}
        bases = []
        for rep_i in range(args.n_replicates):
            rep_rng = np.random.default_rng(root_rng.integers(0, 2**63 - 1))
            bases.append(builder(rep_rng, rep_i))

        for n in sizes:
            rows = []
            for rep_i, base in enumerate(bases):
                rep_rng = np.random.default_rng(root_rng.integers(0, 2**63 - 1))
                res = analyze_sequence(
                    base[:n],
                    alphas=alphas,
                    n_trials=args.n_trials,
                    n_baseline=args.n_baseline,
                    z_min=args.z_min,
                    rng=rep_rng,
                )
                res["replicate"] = rep_i
                rows.append(res)
            summary = summarize_replicates(rows)
            output["domains"][domain_name][str(n)] = rows
            output["summary"][domain_name][str(n)] = summary
            stable_rank = summary["stable_rank_mean"]
            stable_text = f"{stable_rank:>11.3f}" if stable_rank is not None else f"{'NA':>11}"
            print(
                f"{domain_name:<22} {n:>6} {summary['rank_mean']:>7.3f} "
                f"{summary['pc2_mean']:>7.3f} {summary['weak_observable_count_mean']:>5.2f} "
                f"{stable_text}"
            )

    out_path = Path(args.out)
    out_path.parent.mkdir(parents=True, exist_ok=True)
    with out_path.open("w") as f:
        json.dump(output, f, indent=2)
    print(f"saved {out_path}")
    return output


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--sizes", default="128,256,512,1024,2048")
    parser.add_argument("--n-replicates", type=int, default=8)
    parser.add_argument("--gue-matrix-size", type=int, default=180)
    parser.add_argument("--n-alpha", type=int, default=5)
    parser.add_argument("--alpha-min", type=float, default=0.1)
    parser.add_argument("--alpha-max", type=float, default=0.9)
    parser.add_argument("--n-trials", type=int, default=8)
    parser.add_argument("--n-baseline", type=int, default=16)
    parser.add_argument("--z-min", type=float, default=2.0)
    parser.add_argument("--seed", type=int, default=20260506)
    parser.add_argument("--out", default="tools/data/perturbation_rank_size_curve.json")
    args = parser.parse_args()
    run(args)


if __name__ == "__main__":
    main()

exec
/bin/bash -lc "sed -n '1,260p' tools/exp_boundary_gue_poisson.py" in /opt/MM_D-ND
 succeeded in 0ms:
#!/usr/bin/env python3
"""
Experiment: GUE/Poisson boundary in prime gaps vs Cramér null model.

Question: Is the transition from GUE-like to Poisson-like statistics in prime gaps
a structural feature of primes, or does it appear in any sequence with decreasing density?

Method:
1. Compute consecutive gap ratios <r> = min(g_i, g_{i+1}) / max(g_i, g_{i+1})
   for primes in sliding windows at different scales.
   - GUE (GOE in 1D): <r> ≈ 0.5307
   - Poisson: <r> ≈ 0.3863
2. Generate Cramér random primes: each integer n is "prime" with probability 1/ln(n).
3. Compare the <r> profile across scales for real primes vs Cramér model.
4. If both show the same transition → the boundary is trivial (density effect).
   If primes differ → the boundary carries structural information.

Null baseline: 20 Cramér realizations, report mean ± std.
"""

import numpy as np
from sympy import primerange
import json
from datetime import datetime

def gap_ratios(gaps):
    """Compute consecutive gap ratios min/max for a sequence of gaps."""
    if len(gaps) < 2:
        return np.array([])
    r = np.minimum(gaps[:-1], gaps[1:]) / np.maximum(gaps[:-1], gaps[1:])
    return r

def primes_in_window(start, end, primes_array):
    """Get primes in [start, end)."""
    idx_start = np.searchsorted(primes_array, start, side='left')
    idx_end = np.searchsorted(primes_array, end, side='left')
    return primes_array[idx_start:idx_end]

def cramer_random_primes(N_max, rng):
    """Generate Cramér random 'primes': each n>=2 is included with prob 1/ln(n)."""
    # For efficiency, work in blocks
    result = [2]
    n_vals = np.arange(3, N_max, 2)  # odd numbers only (like primes > 2)
    probs = 2.0 / np.log(n_vals)  # factor 2 because we only test odds
    probs = np.clip(probs, 0, 1)
    mask = rng.random(len(n_vals)) < probs
    result.extend(n_vals[mask].tolist())
    return np.array(result)

def analyze_windows(primes_array, windows):
    """Compute <r> for primes in each window."""
    results = []
    for (start, end) in windows:
        p = primes_in_window(start, end, primes_array)
        if len(p) < 50:
            results.append(np.nan)
            continue
        gaps = np.diff(p).astype(float)
        r = gap_ratios(gaps)
        results.append(np.mean(r))
    return np.array(results)

def main():
    print("=== GUE/Poisson Boundary: Primes vs Cramér Null Model ===\n")

    # Generate primes up to 10^7
    N_MAX = 10_000_000
    print(f"Generating primes up to {N_MAX:,}...")
    primes = np.array(list(primerange(2, N_MAX)))
    print(f"  Found {len(primes):,} primes\n")

    # Define windows: logarithmically spaced
    # Each window has ~2000 consecutive primes for statistical stability
    n_windows = 20
    window_centers = np.logspace(np.log10(1000), np.log10(N_MAX - 100000), n_windows).astype(int)
    window_half = 50000  # ±50K around center
    windows = [(max(2, c - window_half), c + window_half) for c in window_centers]

    # Analyze real primes
    print("Analyzing real primes across scales...")
    r_primes = analyze_windows(primes, windows)

    # Cramér null model: 20 realizations
    N_CRAMER = 20
    print(f"Generating {N_CRAMER} Cramér random prime sets...")
    rng = np.random.default_rng(42)
    r_cramer_all = []
    for i in range(N_CRAMER):
        cp = cramer_random_primes(N_MAX, rng)
        r_c = analyze_windows(cp, windows)
        r_cramer_all.append(r_c)
        if (i + 1) % 5 == 0:
            print(f"  {i+1}/{N_CRAMER} done")

    r_cramer_all = np.array(r_cramer_all)
    r_cramer_mean = np.nanmean(r_cramer_all, axis=0)
    r_cramer_std = np.nanstd(r_cramer_all, axis=0)

    # Reference values
    r_gue = 0.5307  # GOE (real symmetric) in 1D
    r_poisson = 0.3863

    # Print results
    print("\n" + "="*80)
    print(f"{'Window center':>15} | {'<r> primes':>10} | {'<r> Cramér':>12} | {'Δ':>8} | {'σ_Cramér':>8} | {'z-score':>8}")
    print("-"*80)

    z_scores = []
    for i, (start, end) in enumerate(windows):
        center = (start + end) // 2
        rp = r_primes[i]
        rc = r_cramer_mean[i]
        rs = r_cramer_std[i]
        delta = rp - rc
        z = delta / rs if rs > 0 else 0
        z_scores.append(z)
        print(f"{center:>15,} | {rp:>10.4f} | {rc:>10.4f}±{rs:.3f} | {delta:>+8.4f} | {rs:>8.4f} | {z:>+8.2f}")

    print("="*80)
    print(f"\nReference: <r>_GUE = {r_gue:.4f}, <r>_Poisson = {r_poisson:.4f}")

    # Summary statistics
    z_scores = np.array(z_scores)
    valid = ~np.isnan(z_scores)
    print(f"\nz-score summary (primes - Cramér) / σ_Cramér:")
    print(f"  mean z = {np.nanmean(z_scores):.3f}")
    print(f"  max |z| = {np.max(np.abs(z_scores[valid])):.3f}")
    print(f"  windows with |z| > 2: {np.sum(np.abs(z_scores[valid]) > 2)}/{np.sum(valid)}")

    # Key diagnostic: does <r> trend differ?
    print("\n--- Diagnostic: trend analysis ---")
    # Fit linear trend to <r> vs log(center)
    centers = np.array([(s+e)//2 for s,e in windows])
    log_centers = np.log10(centers)

    valid_p = ~np.isnan(r_primes)
    if np.sum(valid_p) > 3:
        coeff_p = np.polyfit(log_centers[valid_p], r_primes[valid_p], 1)
        coeff_c = np.polyfit(log_centers[valid_p], r_cramer_mean[valid_p], 1)
        print(f"  Primes: <r> = {coeff_p[0]:+.4f} * log10(n) + {coeff_p[1]:.4f}")
        print(f"  Cramér: <r> = {coeff_c[0]:+.4f} * log10(n) + {coeff_c[1]:.4f}")
        print(f"  Slope difference: {coeff_p[0] - coeff_c[0]:+.4f}")

        if abs(coeff_p[0] - coeff_c[0]) < 0.005:
            print("  → Slopes nearly identical: transition is a DENSITY EFFECT")
            structural = False
        else:
            print("  → Slopes differ: primes have STRUCTURAL content beyond density")
            structural = True
    else:
        print("  Not enough valid windows for trend analysis")
        structural = None

    # Where are primes relative to GUE/Poisson?
    print("\n--- Classification ---")
    for i, (start, end) in enumerate(windows):
        center = (start + end) // 2
        rp = r_primes[i]
        if np.isnan(rp):
            continue
        dist_gue = abs(rp - r_gue)
        dist_poi = abs(rp - r_poisson)
        label = "GUE" if dist_gue < dist_poi else "POISSON"
        margin = abs(dist_gue - dist_poi)
        if margin < 0.02:
            label = "BOUNDARY"
        print(f"  n~{center:>10,}: <r>={rp:.4f}  → {label}")

    # Save results
    result = {
        "experiment": "boundary_gue_poisson_cramer",
        "timestamp": datetime.now().isoformat(),
        "N_MAX": N_MAX,
        "n_primes": len(primes),
        "n_cramer_realizations": N_CRAMER,
        "reference": {"r_gue": r_gue, "r_poisson": r_poisson},
        "windows": [{"center": int((s+e)//2), "r_primes": float(r_primes[i]),
                      "r_cramer_mean": float(r_cramer_mean[i]),
                      "r_cramer_std": float(r_cramer_std[i]),
                      "z_score": float(z_scores[i]) if not np.isnan(z_scores[i]) else None}
                     for i, (s, e) in enumerate(windows)],
        "structural": structural,
        "z_mean": float(np.nanmean(z_scores)),
        "z_max_abs": float(np.max(np.abs(z_scores[valid]))),
        "slope_primes": float(coeff_p[0]) if np.sum(valid_p) > 3 else None,
        "slope_cramer": float(coeff_c[0]) if np.sum(valid_p) > 3 else None,
    }

    outpath = "/opt/MM_D-ND/tools/data/reports/exp_boundary_20260405_0825.json"
    with open(outpath, 'w') as f:
        json.dump(result, f, indent=2)
    print(f"\nResults saved to {outpath}")

    return result

if __name__ == "__main__":
    main()

exec
/bin/bash -lc 'ls -1 tools/data/reports | tail -20' in /opt/MM_D-ND
 succeeded in 0ms:
report_20260329_0343.md
report_20260330_0344.md
report_20260331_0345.md
report_20260331_1809.md
report_20260401_0346.md
report_20260402_0344.md
report_20260402_0756.md
report_20260403_0330.md
report_20260404_0330.md
report_20260404_1852.md
report_20260405_0330.md
report_20260405_0715.md
tension_projection_screening_2026-04-21.json
trace_map_20260305_0843.json
trace_map_20260305_0844.json
trajectory_apply_20260506_1931.json
trajectory_apply_20260506_1941.json
trajectory_apply_20260506_1955.json
trajectory_apply_20260507_0330.json
watchdog.log

exec
/bin/bash -lc 'find tools/data -maxdepth 2 -type f | sort | tail -80' in /opt/MM_D-ND
 succeeded in 0ms:
tools/data/reports/trajectory_apply_20260507_0330.json
tools/data/reports/watchdog.log
tools/data/research_kb.json
tools/data/research_protocols.json
tools/data/residuo_ordine_9domini.json
tools/data/retriever_risultati.json
tools/data/riformulazioni.json
tools/data/risultante_overview.png
tools/data/risultante_results.json
tools/data/risultante_v2.json
tools/data/rottura_phi2_results.json
tools/data/scale_selective_perturbation.json
tools/data/seed_insight_instruction.md
tools/data/selective_layer_decoupling.json
tools/data/seme.json
tools/data/seme_archive/piano_63.json
tools/data/seme_archive/piano_64.json
tools/data/seme_archive/piano_65.json
tools/data/seme_archive/piano_66.json
tools/data/seme_archive/piano_67.json
tools/data/seme_archive/piano_7.json
tools/data/seme_archive/piano_8.json
tools/data/seme_archive/piano_9.json
tools/data/seme_archive/piano_zero.json
tools/data/seme_axioms.json
tools/data/seme_backup_pre_run.json
tools/data/specchio_20260314.json
tools/data/spectral_gap_results.json
tools/data/spectral_rigidity_results.json
tools/data/spettro_zeta_results.json
tools/data/spirale_M_primi.png
tools/data/stato_ciclo.json
tools/data/synthetic_validation.json
tools/data/tension_to_theory.json
tools/data/test_rarefazione_20260313.json
tools/data/test_semiprimi_20260313.json
tools/data/tm1_figures/duality_map.json
tools/data/tm1_figures/family_table.json
tools/data/tm1_figures/m_spectro_18_domains.png
tools/data/tm1_figures/phase_diagram.json
tools/data/tm1_figures/prime_gaps_spectrum_pub.png
tools/data/tm1_figures/scaling_plot.json
tools/data/tm1_figures/summary.json
tools/data/tm1_figures/tensions.json
tools/data/tm1_figures/tensions_raw.json
tools/data/tm1_figures/version.json
tools/data/tm1_figures/zero_signature_logistic.png
tools/data/topological_charge_results.json
tools/data/torre_results.json
tools/data/trace_bridge_results.json
tools/data/traiettorie_M_primi.png
tools/data/trasmutazione_results.json
tools/data/two_channel_boundary.json
tools/data/two_channel_cross_domain.json
tools/data/two_channel_shuffle_audit.json
tools/data/two_layer_universality.json
tools/data/valutatore_log.jsonl
tools/data/vault_condizioni.json
tools/data/veritas/veritas_20260505_131056.json
tools/data/veritas/veritas_20260505_131201.json
tools/data/veritas/veritas_20260506_033803.json
tools/data/veritas/veritas_20260506_063303.json
tools/data/veritas/veritas_20260506_194644.json
tools/data/veritas/veritas_20260506_200238.json
tools/data/video_feed.json
tools/data/vocabolario_custom.json
tools/data/zero_confronto_20260310_0822.json
tools/data/zero_confronto_20260310_0830.json
tools/data/zero_confronto_20260317_1604.json
tools/data/zero_controllo2_20260310_1703.json
tools/data/zero_controllo_20260310_1659.json
tools/data/zero_ising_20260310_1716.json
tools/data/zero_multiscala_primi_20260310_0831.json
tools/data/zero_notturno_20260310_0858.json
tools/data/zero_primi_20260310_0820.json
tools/data/zero_signature_logistic.png
tools/data/zero_traiettoria_20260310_1649.json
tools/data/zero_varieta_20260310_1831.json
tools/data/zero_varieta_primi_20260310_1842.json
tools/data/zeta_validation_results.json

exec
/bin/bash -lc "rg --files tools/data/reports | rg 'agent_20260506'" in /opt/MM_D-ND
 succeeded in 0ms:
tools/data/reports/agent_20260506_1955.md
tools/data/reports/agent_20260506_0625.md
tools/data/reports/agent_20260506_1941.md
tools/data/reports/agent_20260506_0330.md

codex
Scrivo uno strumento riusabile `exp_boundary_mixture_gate.py`: genera sequenze GUE, Poisson e miscele controllate, calcola solo gli osservabili canonici dal registry, misura il gate original-vs-shuffle e poi stima dove la separazione fra poli diventa ambigua. Questo tiene distinta la domanda META dalla conferma del rank.
apply patch
patch: completed
/opt/MM_D-ND/tools/exp_boundary_mixture_gate.py
diff --git a/tools/exp_boundary_mixture_gate.py b/tools/exp_boundary_mixture_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..450dc4e0e7d71215c6cb080ede346884b99fc35c
--- /dev/null
+++ b/tools/exp_boundary_mixture_gate.py
@@ -0,0 +1,278 @@
+#!/usr/bin/env python3
+"""
+exp_boundary_mixture_gate.py
+
+Reusable META/BOUNDARY audit for the GUE-Poisson boundary.
+
+Question:
+    Does the GUE/Poisson boundary remain a clean two-class split after the
+    original-vs-shuffle denominator gate, or is the mixed region an operational
+    third state where canonical observables lose stable denominators?
+
+The script uses only canonical observables from observables_registry.py.
+It builds synthetic mixtures by replacing a fraction beta of unfolded GUE
+spacings with Poisson spacings, then measures:
+
+- canonical observable vectors;
+- original-vs-shuffle z-score per observable;
+- endpoint separability in all observables and in gate-stable observables;
+- ambiguity of each beta layer relative to pure GUE and pure Poisson centroids.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
+
+
+OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())
+
+
+def gue_spacings(matrix_size: int, min_spacings: int, rng: np.random.Generator) -> np.ndarray:
+    """Generate unfolded GUE spacings by concatenating independent matrices."""
+    parts: list[np.ndarray] = []
+    edge = max(2, matrix_size // 10)
+    while sum(len(x) for x in parts) < min_spacings:
+        real = rng.standard_normal((matrix_size, matrix_size))
+        imag = rng.standard_normal((matrix_size, matrix_size))
+        h = real + 1j * imag
+        h = (h + h.conj().T) / (2.0 * np.sqrt(matrix_size))
+        eigs = np.sort(np.linalg.eigvalsh(h).real)
+        bulk = eigs[edge:-edge]
+        gaps = np.diff(bulk)
+        mean = float(np.mean(gaps))
+        if mean > 1e-15:
+            parts.append(gaps / mean)
+    return np.concatenate(parts)[:min_spacings].astype(float)
+
+
+def mixture_spacings(gue: np.ndarray, poisson: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    """Return a beta Poisson / (1-beta) GUE spacing sequence with mean spacing 1."""
+    if len(gue) != len(poisson):
+        raise ValueError("gue and poisson arrays must have the same length")
+    mask = rng.random(len(gue)) < beta
+    out = gue.copy()
+    out[mask] = poisson[mask]
+    mean = float(np.mean(out))
+    return out / mean if mean > 1e-15 else out
+
+
+def z_against_shuffle(
+    gaps: np.ndarray,
+    n_baseline: int,
+    rng: np.random.Generator,
+) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
+    """Return original observables, shuffle baseline std, and original-vs-shuffle z."""
+    original = compute_canonical(gaps)
+    baseline_vals = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline_vals[name].append(obs[name])
+
+    std = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline_vals[name], dtype=float)
+        mean = float(np.mean(vals))
+        sd = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, std, z
+
+
+def vector(row: dict, names: list[str]) -> np.ndarray:
+    return np.array([row["observables"][name] for name in names], dtype=float)
+
+
+def classify_layers(rows: list[dict], obs_names: list[str]) -> dict:
+    """Classify each beta layer by standardized distance to endpoint centroids."""
+    if not obs_names:
+        return {
+            "observables": [],
+            "endpoint_distance": 0.0,
+            "layers": {},
+            "ambiguous_beta": [],
+        }
+
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    gue_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    poi_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    all_endpoint = np.vstack([gue_vectors, poi_vectors])
+    scale = np.std(all_endpoint, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    gue_centroid = np.mean(gue_vectors, axis=0)
+    poi_centroid = np.mean(poi_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((poi_centroid - gue_centroid) / scale))
+
+    layers = {}
+    ambiguous_beta = []
+    for beta, beta_rows in sorted(by_beta.items()):
+        coords = []
+        margins = []
+        labels = []
+        for row in beta_rows:
+            x = vector(row, obs_names)
+            d_gue = float(np.linalg.norm((x - gue_centroid) / scale))
+            d_poi = float(np.linalg.norm((x - poi_centroid) / scale))
+            denom = d_gue + d_poi
+            coord = float((d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("gue" if d_gue < d_poi else "poisson")
+        ambiguous_fraction = float(np.mean(np.array(margins) < 0.15))
+        if ambiguous_fraction >= 0.5:
+            ambiguous_beta.append(beta)
+        layers[f"{beta:.3f}"] = {
+            "coordinate_mean": float(np.mean(coords)),
+            "coordinate_std": float(np.std(coords, ddof=1)) if len(coords) > 1 else 0.0,
+            "margin_mean": float(np.mean(margins)),
+            "ambiguous_fraction": ambiguous_fraction,
+            "poisson_label_fraction": float(np.mean(np.array(labels) == "poisson")),
+        }
+
+    return {
+        "observables": obs_names,
+        "endpoint_distance": endpoint_distance,
+        "layers": layers,
+        "ambiguous_beta": ambiguous_beta,
+    }
+
+
+def summarize_gate(rows: list[dict], z_min: float) -> dict:
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    layers = {}
+    for beta, beta_rows in sorted(by_beta.items()):
+        stable_counts = []
+        stable_freq = {name: [] for name in OBS_NAMES}
+        for row in beta_rows:
+            stable = [name for name in OBS_NAMES if abs(row["z"][name]) >= z_min]
+            stable_counts.append(len(stable))
+            for name in OBS_NAMES:
+                stable_freq[name].append(1.0 if name in stable else 0.0)
+        layers[f"{beta:.3f}"] = {
+            "stable_count_mean": float(np.mean(stable_counts)),
+            "stable_count_std": float(np.std(stable_counts, ddof=1)) if len(stable_counts) > 1 else 0.0,
+            "stable_frequency": {name: float(np.mean(vals)) for name, vals in stable_freq.items()},
+        }
+
+    endpoint_stable = []
+    for name in OBS_NAMES:
+        endpoint_rows = by_beta[0.0] + by_beta[1.0]
+        freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in endpoint_rows])
+        if freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def run(args: argparse.Namespace) -> dict:
+    rng = np.random.default_rng(args.seed)
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    rows = []
+
+    for rep in range(args.n_replicates):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        gue = gue_spacings(args.gue_matrix_size, args.n_gaps, rep_rng)
+        poisson = rep_rng.exponential(1.0, size=args.n_gaps)
+        poisson = poisson / float(np.mean(poisson))
+        for beta in betas:
+            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+            gaps = mixture_spacings(gue, poisson, beta, layer_rng)
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                n_baseline=args.n_baseline,
+                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "replicate": rep,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [name for name in OBS_NAMES if abs(z[name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
+
+    output = {
+        "experiment": "boundary_mixture_gate",
+        "question": "Is the GUE-Poisson mixed layer cleanly classifiable after denominator gating?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_endpoint_gated": gated_classification,
+        "rows": rows,
+    }
+
+    out_path = Path(args.out)
+    out_path.parent.mkdir(parents=True, exist_ok=True)
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
+    print(f"observables_used={OBS_NAMES}")
+    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
+    print(
+        "endpoint_distance_all="
+        f"{all_classification['endpoint_distance']:.3f} "
+        "endpoint_distance_gated="
+        f"{gated_classification['endpoint_distance']:.3f}"
+    )
+    print("beta stable_count margin_gated ambiguous_gated poisson_fraction_gated")
+    for beta in betas:
+        key = f"{beta:.3f}"
+        stable_count = gate["layers"][key]["stable_count_mean"]
+        layer = gated_classification["layers"].get(key, {})
+        print(
+            f"{beta:>4.2f} {stable_count:>12.3f} "
+            f"{layer.get('margin_mean', 0.0):>12.3f} "
+            f"{layer.get('ambiguous_fraction', 0.0):>15.3f} "
+            f"{layer.get('poisson_label_fraction', 0.0):>21.3f}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=1536)
+    parser.add_argument("--n-replicates", type=int, default=16)
+    parser.add_argument("--gue-matrix-size", type=int, default=180)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=24)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=20260507)
+    parser.add_argument("--out", default="tools/data/boundary_mixture_gate.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

diff --git a/tools/exp_boundary_mixture_gate.py b/tools/exp_boundary_mixture_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..450dc4e0e7d71215c6cb080ede346884b99fc35c
--- /dev/null
+++ b/tools/exp_boundary_mixture_gate.py
@@ -0,0 +1,278 @@
+#!/usr/bin/env python3
+"""
+exp_boundary_mixture_gate.py
+
+Reusable META/BOUNDARY audit for the GUE-Poisson boundary.
+
+Question:
+    Does the GUE/Poisson boundary remain a clean two-class split after the
+    original-vs-shuffle denominator gate, or is the mixed region an operational
+    third state where canonical observables lose stable denominators?
+
+The script uses only canonical observables from observables_registry.py.
+It builds synthetic mixtures by replacing a fraction beta of unfolded GUE
+spacings with Poisson spacings, then measures:
+
+- canonical observable vectors;
+- original-vs-shuffle z-score per observable;
+- endpoint separability in all observables and in gate-stable observables;
+- ambiguity of each beta layer relative to pure GUE and pure Poisson centroids.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
+
+
+OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())
+
+
+def gue_spacings(matrix_size: int, min_spacings: int, rng: np.random.Generator) -> np.ndarray:
+    """Generate unfolded GUE spacings by concatenating independent matrices."""
+    parts: list[np.ndarray] = []
+    edge = max(2, matrix_size // 10)
+    while sum(len(x) for x in parts) < min_spacings:
+        real = rng.standard_normal((matrix_size, matrix_size))
+        imag = rng.standard_normal((matrix_size, matrix_size))
+        h = real + 1j * imag
+        h = (h + h.conj().T) / (2.0 * np.sqrt(matrix_size))
+        eigs = np.sort(np.linalg.eigvalsh(h).real)
+        bulk = eigs[edge:-edge]
+        gaps = np.diff(bulk)
+        mean = float(np.mean(gaps))
+        if mean > 1e-15:
+            parts.append(gaps / mean)
+    return np.concatenate(parts)[:min_spacings].astype(float)
+
+
+def mixture_spacings(gue: np.ndarray, poisson: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    """Return a beta Poisson / (1-beta) GUE spacing sequence with mean spacing 1."""
+    if len(gue) != len(poisson):
+        raise ValueError("gue and poisson arrays must have the same length")
+    mask = rng.random(len(gue)) < beta
+    out = gue.copy()
+    out[mask] = poisson[mask]
+    mean = float(np.mean(out))
+    return out / mean if mean > 1e-15 else out
+
+
+def z_against_shuffle(
+    gaps: np.ndarray,
+    n_baseline: int,
+    rng: np.random.Generator,
+) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
+    """Return original observables, shuffle baseline std, and original-vs-shuffle z."""
+    original = compute_canonical(gaps)
+    baseline_vals = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline_vals[name].append(obs[name])
+
+    std = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline_vals[name], dtype=float)
+        mean = float(np.mean(vals))
+        sd = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, std, z
+
+
+def vector(row: dict, names: list[str]) -> np.ndarray:
+    return np.array([row["observables"][name] for name in names], dtype=float)
+
+
+def classify_layers(rows: list[dict], obs_names: list[str]) -> dict:
+    """Classify each beta layer by standardized distance to endpoint centroids."""
+    if not obs_names:
+        return {
+            "observables": [],
+            "endpoint_distance": 0.0,
+            "layers": {},
+            "ambiguous_beta": [],
+        }
+
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    gue_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    poi_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    all_endpoint = np.vstack([gue_vectors, poi_vectors])
+    scale = np.std(all_endpoint, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    gue_centroid = np.mean(gue_vectors, axis=0)
+    poi_centroid = np.mean(poi_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((poi_centroid - gue_centroid) / scale))
+
+    layers = {}
+    ambiguous_beta = []
+    for beta, beta_rows in sorted(by_beta.items()):
+        coords = []
+        margins = []
+        labels = []
+        for row in beta_rows:
+            x = vector(row, obs_names)
+            d_gue = float(np.linalg.norm((x - gue_centroid) / scale))
+            d_poi = float(np.linalg.norm((x - poi_centroid) / scale))
+            denom = d_gue + d_poi
+            coord = float((d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("gue" if d_gue < d_poi else "poisson")
+        ambiguous_fraction = float(np.mean(np.array(margins) < 0.15))
+        if ambiguous_fraction >= 0.5:
+            ambiguous_beta.append(beta)
+        layers[f"{beta:.3f}"] = {
+            "coordinate_mean": float(np.mean(coords)),
+            "coordinate_std": float(np.std(coords, ddof=1)) if len(coords) > 1 else 0.0,
+            "margin_mean": float(np.mean(margins)),
+            "ambiguous_fraction": ambiguous_fraction,
+            "poisson_label_fraction": float(np.mean(np.array(labels) == "poisson")),
+        }
+
+    return {
+        "observables": obs_names,
+        "endpoint_distance": endpoint_distance,
+        "layers": layers,
+        "ambiguous_beta": ambiguous_beta,
+    }
+
+
+def summarize_gate(rows: list[dict], z_min: float) -> dict:
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    layers = {}
+    for beta, beta_rows in sorted(by_beta.items()):
+        stable_counts = []
+        stable_freq = {name: [] for name in OBS_NAMES}
+        for row in beta_rows:
+            stable = [name for name in OBS_NAMES if abs(row["z"][name]) >= z_min]
+            stable_counts.append(len(stable))
+            for name in OBS_NAMES:
+                stable_freq[name].append(1.0 if name in stable else 0.0)
+        layers[f"{beta:.3f}"] = {
+            "stable_count_mean": float(np.mean(stable_counts)),
+            "stable_count_std": float(np.std(stable_counts, ddof=1)) if len(stable_counts) > 1 else 0.0,
+            "stable_frequency": {name: float(np.mean(vals)) for name, vals in stable_freq.items()},
+        }
+
+    endpoint_stable = []
+    for name in OBS_NAMES:
+        endpoint_rows = by_beta[0.0] + by_beta[1.0]
+        freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in endpoint_rows])
+        if freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def run(args: argparse.Namespace) -> dict:
+    rng = np.random.default_rng(args.seed)
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    rows = []
+
+    for rep in range(args.n_replicates):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        gue = gue_spacings(args.gue_matrix_size, args.n_gaps, rep_rng)
+        poisson = rep_rng.exponential(1.0, size=args.n_gaps)
+        poisson = poisson / float(np.mean(poisson))
+        for beta in betas:
+            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+            gaps = mixture_spacings(gue, poisson, beta, layer_rng)
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                n_baseline=args.n_baseline,
+                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "replicate": rep,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [name for name in OBS_NAMES if abs(z[name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
+
+    output = {
+        "experiment": "boundary_mixture_gate",
+        "question": "Is the GUE-Poisson mixed layer cleanly classifiable after denominator gating?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_endpoint_gated": gated_classification,
+        "rows": rows,
+    }
+
+    out_path = Path(args.out)
+    out_path.parent.mkdir(parents=True, exist_ok=True)
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
+    print(f"observables_used={OBS_NAMES}")
+    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
+    print(
+        "endpoint_distance_all="
+        f"{all_classification['endpoint_distance']:.3f} "
+        "endpoint_distance_gated="
+        f"{gated_classification['endpoint_distance']:.3f}"
+    )
+    print("beta stable_count margin_gated ambiguous_gated poisson_fraction_gated")
+    for beta in betas:
+        key = f"{beta:.3f}"
+        stable_count = gate["layers"][key]["stable_count_mean"]
+        layer = gated_classification["layers"].get(key, {})
+        print(
+            f"{beta:>4.2f} {stable_count:>12.3f} "
+            f"{layer.get('margin_mean', 0.0):>12.3f} "
+            f"{layer.get('ambiguous_fraction', 0.0):>15.3f} "
+            f"{layer.get('poisson_label_fraction', 0.0):>21.3f}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=1536)
+    parser.add_argument("--n-replicates", type=int, default=16)
+    parser.add_argument("--gue-matrix-size", type=int, default=180)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=24)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=20260507)
+    parser.add_argument("--out", default="tools/data/boundary_mixture_gate.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

codex
Lo strumento è pronto. Ora lo eseguo con campione moderato per avere repliche sufficienti senza trasformare il ciclo in benchmark numerico; se i risultati sono rumorosi, riduco il claim invece di gonfiarlo.
exec
/bin/bash -lc 'python tools/exp_boundary_mixture_gate.py --n-gaps 1536 --n-replicates 16 --gue-matrix-size 180 --n-beta 11 --n-baseline 24 --out tools/data/boundary_mixture_gate_20260507_0330.json' in /opt/MM_D-ND
diff --git a/tools/exp_boundary_mixture_gate.py b/tools/exp_boundary_mixture_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..450dc4e0e7d71215c6cb080ede346884b99fc35c
--- /dev/null
+++ b/tools/exp_boundary_mixture_gate.py
@@ -0,0 +1,278 @@
+#!/usr/bin/env python3
+"""
+exp_boundary_mixture_gate.py
+
+Reusable META/BOUNDARY audit for the GUE-Poisson boundary.
+
+Question:
+    Does the GUE/Poisson boundary remain a clean two-class split after the
+    original-vs-shuffle denominator gate, or is the mixed region an operational
+    third state where canonical observables lose stable denominators?
+
+The script uses only canonical observables from observables_registry.py.
+It builds synthetic mixtures by replacing a fraction beta of unfolded GUE
+spacings with Poisson spacings, then measures:
+
+- canonical observable vectors;
+- original-vs-shuffle z-score per observable;
+- endpoint separability in all observables and in gate-stable observables;
+- ambiguity of each beta layer relative to pure GUE and pure Poisson centroids.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
+
+
+OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())
+
+
+def gue_spacings(matrix_size: int, min_spacings: int, rng: np.random.Generator) -> np.ndarray:
+    """Generate unfolded GUE spacings by concatenating independent matrices."""
+    parts: list[np.ndarray] = []
+    edge = max(2, matrix_size // 10)
+    while sum(len(x) for x in parts) < min_spacings:
+        real = rng.standard_normal((matrix_size, matrix_size))
+        imag = rng.standard_normal((matrix_size, matrix_size))
+        h = real + 1j * imag
+        h = (h + h.conj().T) / (2.0 * np.sqrt(matrix_size))
+        eigs = np.sort(np.linalg.eigvalsh(h).real)
+        bulk = eigs[edge:-edge]
+        gaps = np.diff(bulk)
+        mean = float(np.mean(gaps))
+        if mean > 1e-15:
+            parts.append(gaps / mean)
+    return np.concatenate(parts)[:min_spacings].astype(float)
+
+
+def mixture_spacings(gue: np.ndarray, poisson: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    """Return a beta Poisson / (1-beta) GUE spacing sequence with mean spacing 1."""
+    if len(gue) != len(poisson):
+        raise ValueError("gue and poisson arrays must have the same length")
+    mask = rng.random(len(gue)) < beta
+    out = gue.copy()
+    out[mask] = poisson[mask]
+    mean = float(np.mean(out))
+    return out / mean if mean > 1e-15 else out
+
+
+def z_against_shuffle(
+    gaps: np.ndarray,
+    n_baseline: int,
+    rng: np.random.Generator,
+) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
+    """Return original observables, shuffle baseline std, and original-vs-shuffle z."""
+    original = compute_canonical(gaps)
+    baseline_vals = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline_vals[name].append(obs[name])
+
+    std = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline_vals[name], dtype=float)
+        mean = float(np.mean(vals))
+        sd = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, std, z
+
+
+def vector(row: dict, names: list[str]) -> np.ndarray:
+    return np.array([row["observables"][name] for name in names], dtype=float)
+
+
+def classify_layers(rows: list[dict], obs_names: list[str]) -> dict:
+    """Classify each beta layer by standardized distance to endpoint centroids."""
+    if not obs_names:
+        return {
+            "observables": [],
+            "endpoint_distance": 0.0,
+            "layers": {},
+            "ambiguous_beta": [],
+        }
+
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    gue_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    poi_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    all_endpoint = np.vstack([gue_vectors, poi_vectors])
+    scale = np.std(all_endpoint, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    gue_centroid = np.mean(gue_vectors, axis=0)
+    poi_centroid = np.mean(poi_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((poi_centroid - gue_centroid) / scale))
+
+    layers = {}
+    ambiguous_beta = []
+    for beta, beta_rows in sorted(by_beta.items()):
+        coords = []
+        margins = []
+        labels = []
+        for row in beta_rows:
+            x = vector(row, obs_names)
+            d_gue = float(np.linalg.norm((x - gue_centroid) / scale))
+            d_poi = float(np.linalg.norm((x - poi_centroid) / scale))
+            denom = d_gue + d_poi
+            coord = float((d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("gue" if d_gue < d_poi else "poisson")
+        ambiguous_fraction = float(np.mean(np.array(margins) < 0.15))
+        if ambiguous_fraction >= 0.5:
+            ambiguous_beta.append(beta)
+        layers[f"{beta:.3f}"] = {
+            "coordinate_mean": float(np.mean(coords)),
+            "coordinate_std": float(np.std(coords, ddof=1)) if len(coords) > 1 else 0.0,
+            "margin_mean": float(np.mean(margins)),
+            "ambiguous_fraction": ambiguous_fraction,
+            "poisson_label_fraction": float(np.mean(np.array(labels) == "poisson")),
+        }
+
+    return {
+        "observables": obs_names,
+        "endpoint_distance": endpoint_distance,
+        "layers": layers,
+        "ambiguous_beta": ambiguous_beta,
+    }
+
+
+def summarize_gate(rows: list[dict], z_min: float) -> dict:
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    layers = {}
+    for beta, beta_rows in sorted(by_beta.items()):
+        stable_counts = []
+        stable_freq = {name: [] for name in OBS_NAMES}
+        for row in beta_rows:
+            stable = [name for name in OBS_NAMES if abs(row["z"][name]) >= z_min]
+            stable_counts.append(len(stable))
+            for name in OBS_NAMES:
+                stable_freq[name].append(1.0 if name in stable else 0.0)
+        layers[f"{beta:.3f}"] = {
+            "stable_count_mean": float(np.mean(stable_counts)),
+            "stable_count_std": float(np.std(stable_counts, ddof=1)) if len(stable_counts) > 1 else 0.0,
+            "stable_frequency": {name: float(np.mean(vals)) for name, vals in stable_freq.items()},
+        }
+
+    endpoint_stable = []
+    for name in OBS_NAMES:
+        endpoint_rows = by_beta[0.0] + by_beta[1.0]
+        freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in endpoint_rows])
+        if freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def run(args: argparse.Namespace) -> dict:
+    rng = np.random.default_rng(args.seed)
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    rows = []
+
+    for rep in range(args.n_replicates):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        gue = gue_spacings(args.gue_matrix_size, args.n_gaps, rep_rng)
+        poisson = rep_rng.exponential(1.0, size=args.n_gaps)
+        poisson = poisson / float(np.mean(poisson))
+        for beta in betas:
+            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+            gaps = mixture_spacings(gue, poisson, beta, layer_rng)
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                n_baseline=args.n_baseline,
+                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "replicate": rep,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [name for name in OBS_NAMES if abs(z[name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
+
+    output = {
+        "experiment": "boundary_mixture_gate",
+        "question": "Is the GUE-Poisson mixed layer cleanly classifiable after denominator gating?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_endpoint_gated": gated_classification,
+        "rows": rows,
+    }
+
+    out_path = Path(args.out)
+    out_path.parent.mkdir(parents=True, exist_ok=True)
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
+    print(f"observables_used={OBS_NAMES}")
+    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
+    print(
+        "endpoint_distance_all="
+        f"{all_classification['endpoint_distance']:.3f} "
+        "endpoint_distance_gated="
+        f"{gated_classification['endpoint_distance']:.3f}"
+    )
+    print("beta stable_count margin_gated ambiguous_gated poisson_fraction_gated")
+    for beta in betas:
+        key = f"{beta:.3f}"
+        stable_count = gate["layers"][key]["stable_count_mean"]
+        layer = gated_classification["layers"].get(key, {})
+        print(
+            f"{beta:>4.2f} {stable_count:>12.3f} "
+            f"{layer.get('margin_mean', 0.0):>12.3f} "
+            f"{layer.get('ambiguous_fraction', 0.0):>15.3f} "
+            f"{layer.get('poisson_label_fraction', 0.0):>21.3f}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=1536)
+    parser.add_argument("--n-replicates", type=int, default=16)
+    parser.add_argument("--gue-matrix-size", type=int, default=180)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=24)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=20260507)
+    parser.add_argument("--out", default="tools/data/boundary_mixture_gate.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

 succeeded in 1452ms:
observables_registry=1.0.0-2026-05-06
observables_used=['SR', 'SR2', 'L1', 'L2', 'triple_var']
endpoint_stable_observables=[]
endpoint_distance_all=3.973 endpoint_distance_gated=0.000
beta stable_count margin_gated ambiguous_gated poisson_fraction_gated
0.00        3.188        0.000           0.000                 0.000
0.10        3.312        0.000           0.000                 0.000
0.20        3.312        0.000           0.000                 0.000
0.30        2.500        0.000           0.000                 0.000
0.40        1.625        0.000           0.000                 0.000
0.50        0.750        0.000           0.000                 0.000
0.60        0.188        0.000           0.000                 0.000
0.70        0.500        0.000           0.000                 0.000
0.80        0.250        0.000           0.000                 0.000
0.90        0.250        0.000           0.000                 0.000
1.00        0.125        0.000           0.000                 0.000
saved tools/data/boundary_mixture_gate_20260507_0330.json

diff --git a/tools/exp_boundary_mixture_gate.py b/tools/exp_boundary_mixture_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..450dc4e0e7d71215c6cb080ede346884b99fc35c
--- /dev/null
+++ b/tools/exp_boundary_mixture_gate.py
@@ -0,0 +1,278 @@
+#!/usr/bin/env python3
+"""
+exp_boundary_mixture_gate.py
+
+Reusable META/BOUNDARY audit for the GUE-Poisson boundary.
+
+Question:
+    Does the GUE/Poisson boundary remain a clean two-class split after the
+    original-vs-shuffle denominator gate, or is the mixed region an operational
+    third state where canonical observables lose stable denominators?
+
+The script uses only canonical observables from observables_registry.py.
+It builds synthetic mixtures by replacing a fraction beta of unfolded GUE
+spacings with Poisson spacings, then measures:
+
+- canonical observable vectors;
+- original-vs-shuffle z-score per observable;
+- endpoint separability in all observables and in gate-stable observables;
+- ambiguity of each beta layer relative to pure GUE and pure Poisson centroids.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
+
+
+OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())
+
+
+def gue_spacings(matrix_size: int, min_spacings: int, rng: np.random.Generator) -> np.ndarray:
+    """Generate unfolded GUE spacings by concatenating independent matrices."""
+    parts: list[np.ndarray] = []
+    edge = max(2, matrix_size // 10)
+    while sum(len(x) for x in parts) < min_spacings:
+        real = rng.standard_normal((matrix_size, matrix_size))
+        imag = rng.standard_normal((matrix_size, matrix_size))
+        h = real + 1j * imag
+        h = (h + h.conj().T) / (2.0 * np.sqrt(matrix_size))
+        eigs = np.sort(np.linalg.eigvalsh(h).real)
+        bulk = eigs[edge:-edge]
+        gaps = np.diff(bulk)
+        mean = float(np.mean(gaps))
+        if mean > 1e-15:
+            parts.append(gaps / mean)
+    return np.concatenate(parts)[:min_spacings].astype(float)
+
+
+def mixture_spacings(gue: np.ndarray, poisson: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    """Return a beta Poisson / (1-beta) GUE spacing sequence with mean spacing 1."""
+    if len(gue) != len(poisson):
+        raise ValueError("gue and poisson arrays must have the same length")
+    mask = rng.random(len(gue)) < beta
+    out = gue.copy()
+    out[mask] = poisson[mask]
+    mean = float(np.mean(out))
+    return out / mean if mean > 1e-15 else out
+
+
+def z_against_shuffle(
+    gaps: np.ndarray,
+    n_baseline: int,
+    rng: np.random.Generator,
+) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
+    """Return original observables, shuffle baseline std, and original-vs-shuffle z."""
+    original = compute_canonical(gaps)
+    baseline_vals = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline_vals[name].append(obs[name])
+
+    std = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline_vals[name], dtype=float)
+        mean = float(np.mean(vals))
+        sd = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, std, z
+
+
+def vector(row: dict, names: list[str]) -> np.ndarray:
+    return np.array([row["observables"][name] for name in names], dtype=float)
+
+
+def classify_layers(rows: list[dict], obs_names: list[str]) -> dict:
+    """Classify each beta layer by standardized distance to endpoint centroids."""
+    if not obs_names:
+        return {
+            "observables": [],
+            "endpoint_distance": 0.0,
+            "layers": {},
+            "ambiguous_beta": [],
+        }
+
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    gue_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    poi_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    all_endpoint = np.vstack([gue_vectors, poi_vectors])
+    scale = np.std(all_endpoint, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    gue_centroid = np.mean(gue_vectors, axis=0)
+    poi_centroid = np.mean(poi_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((poi_centroid - gue_centroid) / scale))
+
+    layers = {}
+    ambiguous_beta = []
+    for beta, beta_rows in sorted(by_beta.items()):
+        coords = []
+        margins = []
+        labels = []
+        for row in beta_rows:
+            x = vector(row, obs_names)
+            d_gue = float(np.linalg.norm((x - gue_centroid) / scale))
+            d_poi = float(np.linalg.norm((x - poi_centroid) / scale))
+            denom = d_gue + d_poi
+            coord = float((d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("gue" if d_gue < d_poi else "poisson")
+        ambiguous_fraction = float(np.mean(np.array(margins) < 0.15))
+        if ambiguous_fraction >= 0.5:
+            ambiguous_beta.append(beta)
+        layers[f"{beta:.3f}"] = {
+            "coordinate_mean": float(np.mean(coords)),
+            "coordinate_std": float(np.std(coords, ddof=1)) if len(coords) > 1 else 0.0,
+            "margin_mean": float(np.mean(margins)),
+            "ambiguous_fraction": ambiguous_fraction,
+            "poisson_label_fraction": float(np.mean(np.array(labels) == "poisson")),
+        }
+
+    return {
+        "observables": obs_names,
+        "endpoint_distance": endpoint_distance,
+        "layers": layers,
+        "ambiguous_beta": ambiguous_beta,
+    }
+
+
+def summarize_gate(rows: list[dict], z_min: float) -> dict:
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    layers = {}
+    for beta, beta_rows in sorted(by_beta.items()):
+        stable_counts = []
+        stable_freq = {name: [] for name in OBS_NAMES}
+        for row in beta_rows:
+            stable = [name for name in OBS_NAMES if abs(row["z"][name]) >= z_min]
+            stable_counts.append(len(stable))
+            for name in OBS_NAMES:
+                stable_freq[name].append(1.0 if name in stable else 0.0)
+        layers[f"{beta:.3f}"] = {
+            "stable_count_mean": float(np.mean(stable_counts)),
+            "stable_count_std": float(np.std(stable_counts, ddof=1)) if len(stable_counts) > 1 else 0.0,
+            "stable_frequency": {name: float(np.mean(vals)) for name, vals in stable_freq.items()},
+        }
+
+    endpoint_stable = []
+    for name in OBS_NAMES:
+        endpoint_rows = by_beta[0.0] + by_beta[1.0]
+        freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in endpoint_rows])
+        if freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def run(args: argparse.Namespace) -> dict:
+    rng = np.random.default_rng(args.seed)
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    rows = []
+
+    for rep in range(args.n_replicates):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        gue = gue_spacings(args.gue_matrix_size, args.n_gaps, rep_rng)
+        poisson = rep_rng.exponential(1.0, size=args.n_gaps)
+        poisson = poisson / float(np.mean(poisson))
+        for beta in betas:
+            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+            gaps = mixture_spacings(gue, poisson, beta, layer_rng)
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                n_baseline=args.n_baseline,
+                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "replicate": rep,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [name for name in OBS_NAMES if abs(z[name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
+
+    output = {
+        "experiment": "boundary_mixture_gate",
+        "question": "Is the GUE-Poisson mixed layer cleanly classifiable after denominator gating?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_endpoint_gated": gated_classification,
+        "rows": rows,
+    }
+
+    out_path = Path(args.out)
+    out_path.parent.mkdir(parents=True, exist_ok=True)
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
+    print(f"observables_used={OBS_NAMES}")
+    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
+    print(
+        "endpoint_distance_all="
+        f"{all_classification['endpoint_distance']:.3f} "
+        "endpoint_distance_gated="
+        f"{gated_classification['endpoint_distance']:.3f}"
+    )
+    print("beta stable_count margin_gated ambiguous_gated poisson_fraction_gated")
+    for beta in betas:
+        key = f"{beta:.3f}"
+        stable_count = gate["layers"][key]["stable_count_mean"]
+        layer = gated_classification["layers"].get(key, {})
+        print(
+            f"{beta:>4.2f} {stable_count:>12.3f} "
+            f"{layer.get('margin_mean', 0.0):>12.3f} "
+            f"{layer.get('ambiguous_fraction', 0.0):>15.3f} "
+            f"{layer.get('poisson_label_fraction', 0.0):>21.3f}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=1536)
+    parser.add_argument("--n-replicates", type=int, default=16)
+    parser.add_argument("--gue-matrix-size", type=int, default=180)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=24)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=20260507)
+    parser.add_argument("--out", default="tools/data/boundary_mixture_gate.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

codex
Il primo passaggio mostra una dissonanza forte: nessun osservabile è stabile su entrambi gli endpoint, perché il polo Poisson è quasi indistinguibile dal proprio shuffle. Questo rende il “gate endpoint” troppo severo; leggo il JSON e userò il risultato come vincolo operativo, non come fallimento dello strumento.
exec
/bin/bash -lc "python - <<'PY'
import json
p='tools/data/boundary_mixture_gate_20260507_0330.json'
d=json.load(open(p))
print('endpoint stable', d['gate']['endpoint_stable_observables'])
print('all distance', d['classification_all_observables']['endpoint_distance'])
print('all ambiguous', d['classification_all_observables']['ambiguous_beta'])
for k,v in d['classification_all_observables']['layers'].items():
    g=d['gate']['layers'][k]
    print(k, 'coord', round(v['coordinate_mean'],3), 'margin', round(v['margin_mean'],3), 'amb', v['ambiguous_fraction'], 'poifrac', v['poisson_label_fraction'], 'stable', round(g['stable_count_mean'],3), 'freq', {kk:round(vv,2) for kk,vv in g['stable_frequency'].items()})
PY" in /opt/MM_D-ND
 succeeded in 0ms:
endpoint stable []
all distance 3.972919899506039
all ambiguous [0.30000000000000004, 0.4]
0.000 coord -0.735 margin 0.735 amb 0.0 poifrac 0.0 stable 3.188 freq {'SR': 1.0, 'SR2': 0.06, 'L1': 1.0, 'L2': 0.12, 'triple_var': 1.0}
0.100 coord -0.47 margin 0.47 amb 0.0 poifrac 0.0 stable 3.312 freq {'SR': 1.0, 'SR2': 0.12, 'L1': 1.0, 'L2': 0.19, 'triple_var': 1.0}
0.200 coord -0.232 margin 0.232 amb 0.125 poifrac 0.0 stable 3.312 freq {'SR': 1.0, 'SR2': 0.25, 'L1': 1.0, 'L2': 0.06, 'triple_var': 1.0}
0.300 coord -0.054 margin 0.07 amb 0.875 poifrac 0.25 stable 2.5 freq {'SR': 1.0, 'SR2': 0.12, 'L1': 0.75, 'L2': 0.12, 'triple_var': 0.5}
0.400 coord 0.075 margin 0.083 amb 0.8125 poifrac 0.875 stable 1.625 freq {'SR': 0.75, 'SR2': 0.06, 'L1': 0.44, 'L2': 0.0, 'triple_var': 0.38}
0.500 coord 0.26 margin 0.26 amb 0.0 poifrac 1.0 stable 0.75 freq {'SR': 0.25, 'SR2': 0.0, 'L1': 0.25, 'L2': 0.0, 'triple_var': 0.25}
0.600 coord 0.374 margin 0.374 amb 0.0 poifrac 1.0 stable 0.188 freq {'SR': 0.12, 'SR2': 0.0, 'L1': 0.0, 'L2': 0.06, 'triple_var': 0.0}
0.700 coord 0.52 margin 0.52 amb 0.0 poifrac 1.0 stable 0.5 freq {'SR': 0.12, 'SR2': 0.0, 'L1': 0.19, 'L2': 0.06, 'triple_var': 0.12}
0.800 coord 0.57 margin 0.57 amb 0.0 poifrac 1.0 stable 0.25 freq {'SR': 0.06, 'SR2': 0.0, 'L1': 0.0, 'L2': 0.19, 'triple_var': 0.0}
0.900 coord 0.692 margin 0.692 amb 0.0 poifrac 1.0 stable 0.25 freq {'SR': 0.06, 'SR2': 0.0, 'L1': 0.12, 'L2': 0.06, 'triple_var': 0.0}
1.000 coord 0.721 margin 0.721 amb 0.0 poifrac 1.0 stable 0.125 freq {'SR': 0.0, 'SR2': 0.0, 'L1': 0.0, 'L2': 0.12, 'triple_var': 0.0}

exec
/bin/bash -lc 'git status --short' in /opt/MM_D-ND
 succeeded in 0ms:
 M applications/INDEX.draft.json
 M applications/soluzioni/20260430_0330_three-regimes-of-modular-memory-algebraic/finding_index.draft.json
 M applications/soluzioni/20260430_0330_three-regimes-of-modular-memory-algebraic/manifest.draft.json
 M tools/LAB_AGENT_CONTEXT.md
 M tools/data/agent_field_live.md
 M tools/data/conoscenza_teorie.json
 M tools/data/consecutio.json
 M tools/data/consecutio_processata.json
 M tools/data/domande_fondamentali.json
 M tools/data/incrocio_risultato.json
 M tools/data/lab_data.json
 M tools/data/lab_graph.json
 M tools/data/lab_health.json
 M tools/data/lab_session_log.jsonl
 M tools/data/pipeline_state.json
 M tools/data/ponti_evoluti.json
 M tools/data/refresh_detector_state.json
 M tools/data/seme.json
 D tools/data/seme_archive/piano_37.json
 D tools/data/seme_archive/piano_38.json
 D tools/data/seme_archive/piano_4.json
 D tools/data/seme_archive/piano_5.json
 D tools/data/seme_archive/piano_6.json
 M tools/data/seme_axioms.json
 M tools/data/seme_backup_pre_run.json
 M tools/data/tm1_figures/tensions.json
 M tools/data/tm1_figures/tensions_raw.json
?? applications/published/20260504_0901_the-two-markov-layers-are-coupled/
?? applications/published/20260504_1219_markov-layers-pass-the-first-recovery/
?? applications/published/20260505_0330_observable-rank-audit-many-probes-one/
?? applications/published/20260506_1955_observable-collinearity-breaks-only-where-denominators/
?? applications/scoperte/20260502_0330_the-dipolar-phase-transition-is-nearly_auto/
?? applications/scoperte/20260503_0330_markov-memory-has-two-visible-layers_auto/
?? applications/scoperte/20260504_0901_the-two-markov-layers-are-coupled_auto/
?? applications/scoperte/20260504_1219_markov-layers-pass-the-first-recovery_auto/
?? applications/scoperte/20260505_0330_observable-rank-audit-many-probes-one_auto/
?? applications/scoperte/20260506_0625_perturbation-dimensionality-is-not-yet-a_auto/
?? applications/scoperte/20260506_1941_perturbation-rank-needs-denominator-gating_auto/
?? applications/scoperte/20260506_1955_observable-collinearity-breaks-only-where-denominators_auto/
?? applications/soluzioni/20260502_0330_the-dipolar-phase-transition-is-nearly/
?? applications/soluzioni/20260503_0330_markov-memory-has-two-visible-layers/
?? applications/soluzioni/20260504_0901_the-two-markov-layers-are-coupled/
?? applications/soluzioni/20260504_1219_markov-layers-pass-the-first-recovery/
?? applications/soluzioni/20260505_0330_observable-rank-audit-many-probes-one/
?? applications/soluzioni/20260506_1941_perturbation-rank-needs-denominator-gating/
?? applications/soluzioni/20260506_1955_observable-collinearity-breaks-only-where-denominators/
?? findings_gallery.md
?? tools/data/.last_telegram_msg
?? tools/data/3d_boundary_layers.json
?? tools/data/aeternitas/
?? tools/data/alignment_active.json
?? tools/data/alignment_markers.jsonl
?? tools/data/bicono_projections.jsonl
?? tools/data/boundary_coherence.json
?? tools/data/boundary_mixture_gate_20260507_0330.json
?? tools/data/boundary_shuffle_audit.json
?? tools/data/brody_calibration_results.json
?? tools/data/brody_flow.json
?? tools/data/conoscenza_teorie.json.bak.retraction_22_04
?? tools/data/cross_domain_dipolar_direction.json
?? tools/data/cross_observable_consistency.json
?? tools/data/crossover_phase_test.json
?? tools/data/dipolar_crossover.json
?? tools/data/dipolar_vector_scaling.json
?? tools/data/domandatore/domandatore_20260421_0746.json
?? tools/data/domandatore/domandatore_20260422_0345.json
?? tools/data/domandatore/domandatore_20260423_0345.json
?? tools/data/domandatore/domandatore_20260424_0345.json
?? tools/data/domandatore/domandatore_20260425_0345.json
?? tools/data/domandatore/domandatore_20260426_0345.json
?? tools/data/domandatore/domandatore_20260427_0345.json
?? tools/data/domandatore/domandatore_20260428_0345.json
?? tools/data/domandatore/domandatore_20260428_1236.json
?? tools/data/domandatore/domandatore_20260429_0345.json
?? tools/data/domandatore/domandatore_20260430_0345.json
?? tools/data/domandatore/domandatore_20260501_0345.json
?? tools/data/domandatore/domandatore_20260502_0345.json
?? tools/data/domandatore/domandatore_20260503_0345.json
?? tools/data/domandatore/domandatore_20260504_0345.json
?? tools/data/domandatore/domandatore_20260505_0345.json
?? tools/data/domandatore/domandatore_20260506_0345.json
?? tools/data/evolution/evolution_20260422_0330.md
?? tools/data/evolution/evolution_20260422_1616.md
?? tools/data/evolution/evolution_20260423_0330.md
?? tools/data/evolution/evolution_20260424_0330.md
?? tools/data/evolution/evolution_20260425_0330.md
?? tools/data/evolution/evolution_20260427_0330.md
?? tools/data/evolution/evolution_20260428_0330.md
?? tools/data/evolution/evolution_20260429_0852.md
?? tools/data/evolution/evolution_20260429_1013.md
?? tools/data/evolution/evolution_20260430_0330.md
?? tools/data/evolution/evolution_20260430_1905.md
?? tools/data/evolution/evolution_20260430_1946.md
?? tools/data/evolution/evolution_20260501_0330.md
?? tools/data/evolution/evolution_20260501_0725.md
?? tools/data/evolution/evolution_20260501_0931.md
?? tools/data/evolution/evolution_20260502_0330.md
?? tools/data/evolution/evolution_20260503_0330.md
?? tools/data/evolution/evolution_20260504_0901.md
?? tools/data/evolution/evolution_20260504_1219.md
?? tools/data/evolution/evolution_20260505_0330.md
?? tools/data/evolution/evolution_20260506_0330.md
?? tools/data/evolution/evolution_20260506_1941.md
?? tools/data/evolution/evolution_20260506_1955.md
?? tools/data/incrocio_20260422_0336.json
?? tools/data/incrocio_20260423_0335.json
?? tools/data/incrocio_20260424_0347.json
?? tools/data/incrocio_20260425_0339.json
?? tools/data/incrocio_20260428_0340.json
?? tools/data/incrocio_20260429_0859.json
?? tools/data/incrocio_20260506_0633.json
?? tools/data/lab_bridge_issues.jsonl
?? tools/data/magnitude_psd_from_acf.json
?? tools/data/markov3_observable_hunt.json
?? tools/data/markov_dipolar_decomposition.json
?? tools/data/markov_k_direction.json
?? tools/data/markov_layer_recovery_audit.json
?? tools/data/markov_memory_by_gue_type.json
?? tools/data/markov_scale_function.json
?? tools/data/meta_tautology_test.json
?? tools/data/mod3_scaling.json
?? tools/data/mod3_vs_residual_ordering.json
?? tools/data/modular_algebra_depth.json
?? tools/data/modular_memory_spectrum.json
?? tools/data/observable_collinearity_breaking_20260506_1955.json
?? tools/data/observable_collinearity_breaking_20260506_1956.json
?? tools/data/observable_collinearity_breaking_20260506_1957.json
?? tools/data/observable_rank_audit.json
?? tools/data/observable_rank_audit_seed20260506.json
?? tools/data/perturbation_dimensionality_audit.json
?? tools/data/perturbation_dimensionality_audit_scale0330.json
?? tools/data/perturbation_rank_size_curve.json
?? tools/data/promotions/
?? tools/data/reports/_quarantine_falsifier_29_04/
?? tools/data/reports/agent_20260422_0330.md
?? tools/data/reports/agent_20260422_1616.md
?? tools/data/reports/agent_20260423_0330.md
?? tools/data/reports/agent_20260424_0330.md
?? tools/data/reports/agent_20260425_0330.md
?? tools/data/reports/agent_20260426_0330.md
?? tools/data/reports/agent_20260427_0330.md
?? tools/data/reports/agent_20260428_0330.md
?? tools/data/reports/agent_20260429_1013.md
?? tools/data/reports/agent_20260429_1041.md
?? tools/data/reports/agent_20260430_0330.md
?? tools/data/reports/agent_20260430_1905.md
?? tools/data/reports/agent_20260430_1919.md
?? tools/data/reports/agent_20260430_1946.md
?? tools/data/reports/agent_20260501_0330.md
?? tools/data/reports/agent_20260501_0725.md
?? tools/data/reports/agent_20260501_0931.md
?? tools/data/reports/agent_20260502_0330.md
?? tools/data/reports/agent_20260503_0330.md
?? tools/data/reports/agent_20260504_0901.md
?? tools/data/reports/agent_20260504_1219.md
?? tools/data/reports/agent_20260505_0330.md
?? tools/data/reports/agent_20260505_1022.md
?? tools/data/reports/agent_20260506_0330.md
?? tools/data/reports/agent_20260506_0625.md
?? tools/data/reports/agent_20260506_1941.md
?? tools/data/reports/agent_20260506_1955.md
?? tools/data/reports/ddf_20260422_0405.json
?? tools/data/reports/ddf_20260423_0405.json
?? tools/data/reports/ddf_20260424_0405.json
?? tools/data/reports/ddf_20260425_0405.json
?? tools/data/reports/ddf_20260426_0405.json
?? tools/data/reports/ddf_20260427_0405.json
?? tools/data/reports/ddf_20260428_0405.json
?? tools/data/reports/ddf_20260429_0405.json
?? tools/data/reports/ddf_20260430_0405.json
?? tools/data/reports/ddf_20260501_0405.json
?? tools/data/reports/ddf_20260502_0405.json
?? tools/data/reports/ddf_20260503_0405.json
?? tools/data/reports/ddf_20260504_0405.json
?? tools/data/reports/ddf_20260505_0405.json
?? tools/data/reports/ddf_20260505_0636.json
?? tools/data/reports/ddf_20260506_0405.json
?? tools/data/reports/evolution_20260422_0330.md
?? tools/data/reports/evolution_20260422_1616.md
?? tools/data/reports/evolution_20260423_0330.md
?? tools/data/reports/evolution_20260424_0330.md
?? tools/data/reports/evolution_20260425_0330.md
?? tools/data/reports/evolution_20260427_0330.md
?? tools/data/reports/evolution_20260428_0330.md
?? tools/data/reports/evolution_20260503_0330.md
?? tools/data/reports/evolution_20260504_0330.md
?? tools/data/reports/evolution_20260505_0330.md
?? tools/data/reports/evolution_20260506_0330.md
?? tools/data/reports/evolution_20260506_1941.md
?? tools/data/reports/falsifier_20260429_1013.json
?? tools/data/reports/falsifier_20260429_1041.json
?? tools/data/reports/falsifier_20260430_0330.json
?? tools/data/reports/falsifier_20260430_1905.json
?? tools/data/reports/falsifier_20260430_1919.json
?? tools/data/reports/falsifier_20260430_1946.json
?? tools/data/reports/falsifier_20260501_0330.json
?? tools/data/reports/falsifier_20260501_0725.json
?? tools/data/reports/falsifier_20260501_0931.json
?? tools/data/reports/falsifier_20260502_0330.json
?? tools/data/reports/falsifier_20260503_0330.json
?? tools/data/reports/falsifier_20260504_0901.json
?? tools/data/reports/falsifier_20260504_1219.json
?? tools/data/reports/falsifier_20260505_0330.json
?? tools/data/reports/falsifier_20260506_0330.raw.txt
?? tools/data/reports/falsifier_20260506_0625.json
?? tools/data/reports/falsifier_20260506_1941.json
?? tools/data/reports/falsifier_20260506_1955.json
?? tools/data/reports/falsifier_20260506_1955.raw.txt
?? tools/data/reports/incident_20260504_0721.md
?? tools/data/reports/incident_20260504_1138.md
?? tools/data/reports/latest.md
?? tools/data/reports/trajectory_apply_20260506_1931.json
?? tools/data/reports/trajectory_apply_20260506_1941.json
?? tools/data/reports/trajectory_apply_20260506_1955.json
?? tools/data/reports/trajectory_apply_20260507_0330.json
?? tools/data/scale_selective_perturbation.json
?? tools/data/selective_layer_decoupling.json
?? tools/data/seme_archive/piano_63.json
?? tools/data/seme_archive/piano_64.json
?? tools/data/seme_archive/piano_65.json
?? tools/data/seme_archive/piano_66.json
?? tools/data/seme_archive/piano_67.json
?? tools/data/spectral_rigidity_results.json
?? tools/data/two_channel_boundary.json
?? tools/data/two_channel_cross_domain.json
?? tools/data/two_channel_shuffle_audit.json
?? tools/data/two_layer_universality.json
?? tools/data/valutatore_log.jsonl
?? tools/data/veritas/
?? tools/exp_3d_boundary_layers.py
?? tools/exp_boundary_coherence.py
?? tools/exp_boundary_mixture_gate.py
?? tools/exp_boundary_shuffle_audit.py
?? tools/exp_brody_calibration.py
?? tools/exp_brody_flow.py
?? tools/exp_cross_domain_dipolar_direction.py
?? tools/exp_cross_observable_consistency.py
?? tools/exp_crossover_phase_test.py
?? tools/exp_dipolar_angle_reference.py
?? tools/exp_dipolar_crossover.py
?? tools/exp_dipolar_vector_scaling.py
?? tools/exp_magnitude_psd_from_acf.py
?? tools/exp_markov3_observable_hunt.py
?? tools/exp_markov_dipolar_decomposition.py
?? tools/exp_markov_k_direction.py
?? tools/exp_markov_layer_recovery_audit.py
?? tools/exp_markov_memory_by_gue_type.py
?? tools/exp_markov_psd_prediction.py
?? tools/exp_markov_scale_function.py
?? tools/exp_meta_tautology_test.py
?? tools/exp_mod3_scaling.py
?? tools/exp_mod3_vs_residual_ordering.py
?? tools/exp_modular_algebra_depth.py
?? tools/exp_modular_memory_spectrum.py
?? tools/exp_observable_rank_audit.py
?? tools/exp_perturbation_dimensionality_audit.py
?? tools/exp_perturbation_rank_size_curve.py
?? tools/exp_scale_selective_perturbation.py
?? tools/exp_selective_layer_decoupling.py
?? tools/exp_spectral_rigidity.py
?? tools/exp_two_channel_boundary.py
?? tools/exp_two_channel_cross_domain.py
?? tools/exp_two_channel_shuffle_audit.py
?? tools/exp_two_layer_universality.py
?? tools/observables_registry.py

exec
/bin/bash -lc 'python -m py_compile tools/exp_boundary_mixture_gate.py' in /opt/MM_D-ND
 succeeded in 0ms:
diff --git a/tools/exp_boundary_mixture_gate.py b/tools/exp_boundary_mixture_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..450dc4e0e7d71215c6cb080ede346884b99fc35c
--- /dev/null
+++ b/tools/exp_boundary_mixture_gate.py
@@ -0,0 +1,278 @@
+#!/usr/bin/env python3
+"""
+exp_boundary_mixture_gate.py
+
+Reusable META/BOUNDARY audit for the GUE-Poisson boundary.
+
+Question:
+    Does the GUE/Poisson boundary remain a clean two-class split after the
+    original-vs-shuffle denominator gate, or is the mixed region an operational
+    third state where canonical observables lose stable denominators?
+
+The script uses only canonical observables from observables_registry.py.
+It builds synthetic mixtures by replacing a fraction beta of unfolded GUE
+spacings with Poisson spacings, then measures:
+
+- canonical observable vectors;
+- original-vs-shuffle z-score per observable;
+- endpoint separability in all observables and in gate-stable observables;
+- ambiguity of each beta layer relative to pure GUE and pure Poisson centroids.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
+
+
+OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())
+
+
+def gue_spacings(matrix_size: int, min_spacings: int, rng: np.random.Generator) -> np.ndarray:
+    """Generate unfolded GUE spacings by concatenating independent matrices."""
+    parts: list[np.ndarray] = []
+    edge = max(2, matrix_size // 10)
+    while sum(len(x) for x in parts) < min_spacings:
+        real = rng.standard_normal((matrix_size, matrix_size))
+        imag = rng.standard_normal((matrix_size, matrix_size))
+        h = real + 1j * imag
+        h = (h + h.conj().T) / (2.0 * np.sqrt(matrix_size))
+        eigs = np.sort(np.linalg.eigvalsh(h).real)
+        bulk = eigs[edge:-edge]
+        gaps = np.diff(bulk)
+        mean = float(np.mean(gaps))
+        if mean > 1e-15:
+            parts.append(gaps / mean)
+    return np.concatenate(parts)[:min_spacings].astype(float)
+
+
+def mixture_spacings(gue: np.ndarray, poisson: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    """Return a beta Poisson / (1-beta) GUE spacing sequence with mean spacing 1."""
+    if len(gue) != len(poisson):
+        raise ValueError("gue and poisson arrays must have the same length")
+    mask = rng.random(len(gue)) < beta
+    out = gue.copy()
+    out[mask] = poisson[mask]
+    mean = float(np.mean(out))
+    return out / mean if mean > 1e-15 else out
+
+
+def z_against_shuffle(
+    gaps: np.ndarray,
+    n_baseline: int,
+    rng: np.random.Generator,
+) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
+    """Return original observables, shuffle baseline std, and original-vs-shuffle z."""
+    original = compute_canonical(gaps)
+    baseline_vals = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline_vals[name].append(obs[name])
+
+    std = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline_vals[name], dtype=float)
+        mean = float(np.mean(vals))
+        sd = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, std, z
+
+
+def vector(row: dict, names: list[str]) -> np.ndarray:
+    return np.array([row["observables"][name] for name in names], dtype=float)
+
+
+def classify_layers(rows: list[dict], obs_names: list[str]) -> dict:
+    """Classify each beta layer by standardized distance to endpoint centroids."""
+    if not obs_names:
+        return {
+            "observables": [],
+            "endpoint_distance": 0.0,
+            "layers": {},
+            "ambiguous_beta": [],
+        }
+
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    gue_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    poi_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    all_endpoint = np.vstack([gue_vectors, poi_vectors])
+    scale = np.std(all_endpoint, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    gue_centroid = np.mean(gue_vectors, axis=0)
+    poi_centroid = np.mean(poi_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((poi_centroid - gue_centroid) / scale))
+
+    layers = {}
+    ambiguous_beta = []
+    for beta, beta_rows in sorted(by_beta.items()):
+        coords = []
+        margins = []
+        labels = []
+        for row in beta_rows:
+            x = vector(row, obs_names)
+            d_gue = float(np.linalg.norm((x - gue_centroid) / scale))
+            d_poi = float(np.linalg.norm((x - poi_centroid) / scale))
+            denom = d_gue + d_poi
+            coord = float((d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("gue" if d_gue < d_poi else "poisson")
+        ambiguous_fraction = float(np.mean(np.array(margins) < 0.15))
+        if ambiguous_fraction >= 0.5:
+            ambiguous_beta.append(beta)
+        layers[f"{beta:.3f}"] = {
+            "coordinate_mean": float(np.mean(coords)),
+            "coordinate_std": float(np.std(coords, ddof=1)) if len(coords) > 1 else 0.0,
+            "margin_mean": float(np.mean(margins)),
+            "ambiguous_fraction": ambiguous_fraction,
+            "poisson_label_fraction": float(np.mean(np.array(labels) == "poisson")),
+        }
+
+    return {
+        "observables": obs_names,
+        "endpoint_distance": endpoint_distance,
+        "layers": layers,
+        "ambiguous_beta": ambiguous_beta,
+    }
+
+
+def summarize_gate(rows: list[dict], z_min: float) -> dict:
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    layers = {}
+    for beta, beta_rows in sorted(by_beta.items()):
+        stable_counts = []
+        stable_freq = {name: [] for name in OBS_NAMES}
+        for row in beta_rows:
+            stable = [name for name in OBS_NAMES if abs(row["z"][name]) >= z_min]
+            stable_counts.append(len(stable))
+            for name in OBS_NAMES:
+                stable_freq[name].append(1.0 if name in stable else 0.0)
+        layers[f"{beta:.3f}"] = {
+            "stable_count_mean": float(np.mean(stable_counts)),
+            "stable_count_std": float(np.std(stable_counts, ddof=1)) if len(stable_counts) > 1 else 0.0,
+            "stable_frequency": {name: float(np.mean(vals)) for name, vals in stable_freq.items()},
+        }
+
+    endpoint_stable = []
+    for name in OBS_NAMES:
+        endpoint_rows = by_beta[0.0] + by_beta[1.0]
+        freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in endpoint_rows])
+        if freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def run(args: argparse.Namespace) -> dict:
+    rng = np.random.default_rng(args.seed)
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    rows = []
+
+    for rep in range(args.n_replicates):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        gue = gue_spacings(args.gue_matrix_size, args.n_gaps, rep_rng)
+        poisson = rep_rng.exponential(1.0, size=args.n_gaps)
+        poisson = poisson / float(np.mean(poisson))
+        for beta in betas:
+            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+            gaps = mixture_spacings(gue, poisson, beta, layer_rng)
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                n_baseline=args.n_baseline,
+                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "replicate": rep,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [name for name in OBS_NAMES if abs(z[name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
+
+    output = {
+        "experiment": "boundary_mixture_gate",
+        "question": "Is the GUE-Poisson mixed layer cleanly classifiable after denominator gating?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_endpoint_gated": gated_classification,
+        "rows": rows,
+    }
+
+    out_path = Path(args.out)
+    out_path.parent.mkdir(parents=True, exist_ok=True)
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
+    print(f"observables_used={OBS_NAMES}")
+    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
+    print(
+        "endpoint_distance_all="
+        f"{all_classification['endpoint_distance']:.3f} "
+        "endpoint_distance_gated="
+        f"{gated_classification['endpoint_distance']:.3f}"
+    )
+    print("beta stable_count margin_gated ambiguous_gated poisson_fraction_gated")
+    for beta in betas:
+        key = f"{beta:.3f}"
+        stable_count = gate["layers"][key]["stable_count_mean"]
+        layer = gated_classification["layers"].get(key, {})
+        print(
+            f"{beta:>4.2f} {stable_count:>12.3f} "
+            f"{layer.get('margin_mean', 0.0):>12.3f} "
+            f"{layer.get('ambiguous_fraction', 0.0):>15.3f} "
+            f"{layer.get('poisson_label_fraction', 0.0):>21.3f}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=1536)
+    parser.add_argument("--n-replicates", type=int, default=16)
+    parser.add_argument("--gue-matrix-size", type=int, default=180)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=24)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=20260507)
+    parser.add_argument("--out", default="tools/data/boundary_mixture_gate.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

exec
/bin/bash -lc "sed -n '1,220p' tools/data/reports/agent_20260506_1955.md" in /opt/MM_D-ND
 succeeded in 0ms:
# Agent Report — Observable Collinearity Breaks Only Where Denominators Are Weak
**Date**: 2026-05-06 19:55  
**Piano**: 67  
**Tension explored**: META + TRAJECTORY_APPLY_20260506_1941  
observables_registry: 1.0.0-2026-05-06  
observables_used: [SR, SR2, L1, L2, triple_var]

## Claim Under Test
Cycle 19:41 closed `PERTURBATION_DENOMINATOR_GATE` and the trajectory explicitly said:

> La prossima corsa NON deve restare su perturbation rank.

This run therefore does not test perturbation dimensionality. It asks a cross-domain META question:

> When the five canonical observables are measured under uniform partial shuffle, does observable collinearity break in structured domains, or only in controls where original-vs-shuffle denominators are weak?

## Experiment
Tool updated: `tools/exp_observable_rank_audit.py`

Method correction before execution:
- removed local observable redefinitions;
- imported canonical definitions from `tools/observables_registry.py`;
- replaced the old local `triple_var` normalized convention with canonical raw `triple_var`;
- added `prime_shuffle` as a control domain;
- reported weak observable count using the fixed gate `abs(original - shuffle_mean) / shuffle_std < 2`.

Atomic perimeter:
- domains: first prime gaps, prime-shuffle control, independent GUE spacings, iid Poisson spacings;
- main run: 12,000 gaps, 19 alpha values, 18 partial-shuffle trials per alpha, 48 full-shuffle baselines;
- two seed checks: 8,000 gaps, 15 alpha values, 12 trials per alpha, 36 baselines;
- measured object: PCA of the 5-observable retention curves across alpha, not perturbation profiles.

## Results

### Main Run

| Domain | PC1 energy | effective rank | mean abs corr | weak obs / 5 | z summary |
|---|---:|---:|---:|---:|---|
| primes | 0.978 | 1.128 | 0.975 | 1 | SR=-12.1, SR2=-2.5, L1=-8.9, L2=-1.9, triple_var=-8.7 |
| prime_shuffle | 0.593 | 2.475 | 0.606 | 5 | all abs(z) <= 1.1 |
| GUE | 0.990 | 1.060 | 0.989 | 0 | SR=-2.9, SR2=+14.5, L1=+13.2, L2=+31.7, triple_var=+23.8 |
| Poisson | 0.625 | 2.368 | 0.609 | 5 | all abs(z) <= 1.9 |

### Three-Run Summary

| Domain | PC1 mean | rank mean | mean abs corr | weak obs mean |
|---|---:|---:|---:|---:|
| primes | 0.939 | 1.296 | 0.924 | 1.33 |
| prime_shuffle | 0.765 | 1.904 | 0.551 | 4.67 |
| GUE | 0.980 | 1.106 | 0.977 | 0.33 |
| Poisson | 0.714 | 2.196 | 0.572 | 5.00 |

## Findings

1. **Structured domains compress the five canonical retention curves to one dominant coordinate in this perimeter.** Primes and GUE both have PC1 > 0.93 on average and effective rank close to 1. This does not say the domains are the same; it says uniform partial shuffle moves the canonical observables along one dominant retention mode.

2. **Observed collinearity breaking is concentrated in weak-denominator controls.** Poisson has the highest apparent rank among the three-run means (`2.196`), but all five observables are weak against full shuffle in every run. Prime-shuffle behaves similarly: rank is unstable and 4-5 of 5 observables are weak. This mirrors the denominator lesson from perturbation rank without repeating the perturbation-rank experiment.

3. **The 05-05 observable-rank result survives only after narrowing its language.** The valid statement is not "five probes are always one thing." The scoped statement is: under uniform partial shuffle and canonical observables, primes and GUE show a dominant one-coordinate retention response; controls can show larger PCA rank, but that rank is not structural when the original-vs-shuffle denominators are absent.

4. **GUE is the cleanest conditioning check.** In the main run, all five GUE observables pass the denominator gate and still give rank `1.060`. This makes GUE the best positive control for "low rank despite valid denominators." Poisson is the negative control for "high rank without valid denominators."

## Verdict
**CONSTRAINT on META**: observable collinearity claims must be reported with:

> observables_registry version + canonical observable list + original-vs-shuffle z per observable + control domains.

In this perimeter, high observable-rank is not the sign of richer structure when it appears in Poisson or prime-shuffle controls; it is a warning that retention ratios are being formed on weak denominators. The stable cross-domain result is narrower:

> uniform partial shuffle exposes one dominant retention coordinate in conditioned structured domains (primes, GUE), while apparent multi-coordinate behavior in Poisson/shuffle controls is denominator-weak.

## Consecutio
What opens now: the next non-redundant test is not another PCA audit. It is **selective operator coupling**: use perturbations that act separately on pair-scale and triple-scale structure, but report only observables whose denominator gate survives. If selective operators rotate primes while GUE stays collinear, the lab gets a real cross-domain discriminator; if both stay collinear, the current observable suite is overcomplete for this question.

## Auto-audit: 5 lenti
- **L1 hard constraint vs bias**: no zero/always claim. "Weak" means `abs(z) < 2` in this declared gate.
- **L2 quantity vs ratio**: PCA rank and retention ratios are interpreted only after raw z denominators.
- **L3 no silent patching**: the older 05-05 collinearity claim is explicitly narrowed; it is not silently rebranded as universal.
- **L4 edge cases**: `prime_shuffle` seed 1956 produced low rank with 4 weak observables; this is reported as unstable control behavior, not ignored.
- **L5 re-discovery**: PCA rank instability under noisy normalization is standard statistical hygiene. This is a lab method constraint, not a new RMT theorem.

## Files
- Script: `tools/exp_observable_rank_audit.py`
- Main data: `tools/data/observable_collinearity_breaking_20260506_1955.json`
- Seed checks: `tools/data/observable_collinearity_breaking_20260506_1956.json`, `tools/data/observable_collinearity_breaking_20260506_1957.json`
- Report: `tools/data/reports/agent_20260506_1955.md`

exec
/bin/bash -lc "sed -n '1,180p' tools/data/reports/agent_20260506_1941.md" in /opt/MM_D-ND
 succeeded in 0ms:
# Agent Report — Perturbation Rank Needs Denominator Gating
**Date**: 2026-05-06 19:41  
**Piano**: 66  
**Tension explored**: META + BOUNDARY  
observables_registry: 1.0.0-2026-05-06  
observables_used: [SR, SR2, L1, L2, triple_var]

## Claim Under Test
The valid next test from cycle 06:25 was a replicate-and-size curve for perturbation effective rank, with observable definitions versioned. This run asks:

> Does the second perturbation axis stabilize as sample size grows, or is rank inflated when retention denominators are weak relative to full-shuffle baselines?

## Experiment
Tool created: `tools/exp_perturbation_rank_size_curve.py`

Atomic perimeter:
- domains: prime-gap windows, prime-shuffle controls, iid Poisson spacings, independent GUE spacings;
- sample sizes: 128, 256, 512, 1024, 2048 gaps;
- replicates/windows: 8 per domain-size point;
- perturbations: `adjacent_swap`, `block_shuffle`, `large_gap_only`, `uniform`;
- alpha grid: 0.1, 0.3, 0.5, 0.7, 0.9;
- trials per perturbation-alpha: 8;
- full-shuffle baselines: 16;
- canonical observables imported from `tools/observables_registry.py`;
- denominator gate: observable is stable only when `abs(original - shuffle_mean) / shuffle_std >= 2`.

The script reports two ranks:
- `rank_all`: PCA effective rank using all five canonical observables;
- `stable_rank`: PCA effective rank after dropping observables whose original-vs-shuffle denominator is weak.

## Results

### Size Curve Summary

| Domain | N | rank_all | PC2 | weak obs / 5 | stable_rank |
|---|---:|---:|---:|---:|---:|
| primes_windows | 128 | 1.789 ± 0.469 | 0.155 | 4.50 | 1.382 |
| primes_windows | 256 | 1.947 ± 0.645 | 0.174 | 4.75 | 1.262 |
| primes_windows | 512 | 1.892 ± 0.372 | 0.142 | 2.88 | 1.310 |
| primes_windows | 1024 | 1.679 ± 0.409 | 0.117 | 1.62 | 1.415 |
| primes_windows | 2048 | 1.442 ± 0.213 | 0.081 | 0.75 | 1.462 |
| prime_shuffle_control | 2048 | 1.797 ± 0.375 | 0.134 | 3.62 | 1.428 |
| poisson | 2048 | 1.952 ± 0.499 | 0.175 | 4.62 | 1.036 |
| gue | 128 | 1.703 ± 0.348 | 0.126 | 2.38 | 1.226 |
| gue | 256 | 1.913 ± 0.453 | 0.164 | 2.25 | 1.141 |
| gue | 512 | 1.542 ± 0.313 | 0.111 | 1.88 | 1.162 |
| gue | 1024 | 1.551 ± 0.395 | 0.105 | 1.88 | 1.157 |
| gue | 2048 | 1.234 ± 0.224 | 0.046 | 2.00 | 1.111 |

### Observable Stability

At GUE N=2048, `SR`, `L1`, and `triple_var` are stable in all 8 replicates; `SR2` and `L2` are stable in 0 of 8. Mean absolute z-scores: `SR=8.38`, `SR2=0.67`, `L1=11.58`, `L2=0.89`, `triple_var=11.66`.

At primes N=2048, `SR`, `L1`, and `triple_var` are stable in all 8 windows; `SR2` is stable in 7 of 8; `L2` is stable in 3 of 8. Mean absolute z-scores: `SR=5.19`, `SR2=2.63`, `L1=3.96`, `L2=1.78`, `triple_var=4.37`.

Poisson and prime-shuffle controls keep high `rank_all` while most observables are weak. At Poisson N=2048, `rank_all=1.952` but `stable_rank=1.036` and 4.62 of 5 observables are weak on average. This is the falsifying control for treating rank_all alone as a structural claim.

## Findings

1. **Perturbation rank is not interpretable without denominator gating.** In this perimeter, Poisson and prime-shuffle controls can show `rank_all` near 1.8-2.0. Because their original-vs-shuffle denominators are mostly weak, that rank is a retention-normalization artifact unless the stable-observable screen also supports it.

2. **GUE does not show a stable second axis on canonical observables up to N=2048.** GUE `rank_all` falls from 1.913 at N=256 to 1.234 at N=2048; PC2 falls from 16.4% to 4.6%. After denominator gating, GUE stable rank stays close to 1.1-1.2.

3. **The old L2-driven sign-flip should not be promoted without a denominator check.** Under canonical observables, GUE `L2` is weak relative to shuffle at every tested size and is stable in 0/8 replicates at N >= 512. This does not prove every L2 sign effect is false; it restricts such effects to local/sample-specific observations unless the denominator survives.

4. **Primes become better conditioned with N, but not more multi-axis.** Prime windows gain stable observables as N grows: weak count drops from 4.75 at N=256 to 0.75 at N=2048. The effective rank does not grow with this conditioning; it is 1.442 at N=2048, and stable_rank is 1.462.

## Verdict
**CONSTRAINT on META + BOUNDARY**: perturbation dimensionality must be reported as:

> effective rank + PC2 + observable registry version + original-vs-shuffle z gate per observable.

The cycle 03:30 "second GUE axis" remains restricted by cycle 06:25 and is further narrowed here: under canonical observables and the tested size curve, the stable statement is not "GUE has a second perturbation axis"; it is:

> all-observable perturbation rank can inflate in weak-denominator regimes; after denominator gating, GUE and primes are both close to one perturbation coordinate in this perimeter, while Poisson/shuffle controls show why ungated rank is not structural evidence.

## Consecutio
What opens now: the lab can keep using perturbation rank, but only as a gated observable. The next useful movement is not more PCA; it is an operator-level denominator map: for each observable, identify the perturbation/domain/scale region where `original - shuffle` is a real signal rather than a noisy divisor.

## Auto-audit: 5 lenti
- **L1 hard constraint vs bias**: no zero/always claim. "Weak" means `abs(z) < 2` in the declared gate, not absence of signal.
- **L2 quantity vs ratio**: retention ratios are not read alone; raw denominator z-scores are reported first.
- **L3 no silent patching**: the 03:30 claim is explicitly restricted; it is not renamed as confirmed.
- **L4 edge cases**: short-GUE and low-N effects are isolated by size. The N=2048 perimeter is stated, not generalized.
- **L5 re-discovery**: PCA rank inflation from noisy normalization is a standard statistical risk. This is a lab constraint on method, not a new RMT result.

## Files
- Script: `tools/exp_perturbation_rank_size_curve.py`
- Data: `tools/data/perturbation_rank_size_curve.json`
- Report: `tools/data/reports/agent_20260506_1941.md`

exec
/bin/bash -lc "sed -n '1,220p' tools/lab_falsifier.py" in /opt/MM_D-ND
 succeeded in 0ms:
#!/usr/bin/env python3
"""
lab_falsifier.py — Counter-pole gate per il lab fisica.

Operatore (29/04): "rendiamo il processo rigoroso al massimo, solo cose
vere devono uscire dal lab altrimenti contaminiamo tutto".

Pattern dipolo asimmetrico: producer (claude/codex agent) e' polo +,
falsifier e' polo - (challenger). Il falsifier non riscrive il report,
emette flag su 5 lenti tied to assiomi del modello D-ND. Se HIGH flags
rilevati, lab_agent.sh blocca il sync verso il sito + THIA + Docker.

Le 5 lenti (port da /opt/D-ND_LAB/core/report_falsifier.py, riformulate
nel linguaggio D-ND, NON copia 1:1 di Gemini critique):

  L1  Hard constraint vs statistical bias       → A2 (confine duro)
  L2  Absolute quantity vs ratio                 → A14 (cascata, dim. invariance)
  L3  Axiom continuity (no silent patching)      → A4 (modus, qualita' della domanda)
  L4  Edge case isolation (1/N is not zero)      → A12 (traccia la curva)
  L5  Re-discovery vs discovery (literature)     → A8 (autologica)

Usage:
  python3 lab_falsifier.py --report PATH --output PATH

Exit codes:
  0  report coerente (nessun flag)
  1  HIGH severity flags rilevati → SYNC BLOCKED
  2  falsifier stesso fallito (LLM unavailable, parse error) → SYNC BLOCKED
     (fail-conservative: se non possiamo verificare, non pubblichiamo)
"""

from __future__ import annotations

import argparse
import json
import os
import re
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path

REPO_DIR = Path("/opt/MM_D-ND")
DATA_DIR = REPO_DIR / "tools" / "data"


FALSIFIER_PROMPT = """Sei il Counter-Pole del lab fisica D-ND. Polo asimmetrico opposto al producer agent: il suo occhio cerca pattern, il tuo cerca inconsistenze. NON riscrivi il report. Lo sfidi.

Leggi:
1. Il report markdown che il producer ha appena scritto.
2. I file empirici/strutturali del lab (lab_data.json, conoscenza_teorie.json,
   cimitero.md se cita claim falsificati).

Applica 5 lenti, ognuna legata a un assioma del modello D-ND. Applica STRETTAMENTE — un falso silenzio e' peggio di un flag che l'operatore puo' dismettere.

### L1 — Hard constraint vs bias statistico (A2 confine duro)
Un claim "impossibile / proibito / zero / proibition / forbidden" e' HARD constraint e richiede uno zero ESATTO nei dati (probabilita' = 0.000). Se la matrice citata ha qualunque entry non-zero dove il report dice "zero", flag. Bias != proibizione.

### L2 — Quantita' assoluta vs ratio (A14 cascata, invarianza dimensionale)
Confronto fra spazi degli stati di taglia diversa (es. mod 3 vs mod 30, N piccolo vs N grande, finestra stretta vs larga): le percentuali ingannano perche' il denominatore cresce. Stesso segnale assoluto SEMBRA ridursi in %. Se il report conclude "diminuisce / si dilata / declina" su confronti percentuali fra spazi, flag e proponi unita' assolute (bit di mutual information, count grezzi, soglie esatte).

### L3 — Continuita' assiomatica / no silent patching (A4 modus)
Se il setup ("Claim Under Test") usa una definizione (es. "F2: gap mod 6 in {2,4}") e la conclusione silenziosamente ne usa un'altra ("gap mod 6 in {0,2,4}"), e' patch det=+1 sul presente, non inversione det=−1 al nodo regressivo. Il cambio DEVE essere dichiarato esplicitamente: "F2 falsificato al nodo X — scope corretto e' Y".

### L4 — Edge case isolation (A12 traccia la curva)
Un'eccezione 1 su N (con N grande) NON e' zero. Se il report dice "sempre X" o "mai X" e i dati mostrano anche un singolo controesempio, il perimetro deve essere riformulato ("per p > 3, X vale") — mai arrotondato via.

### L5 — Re-discovery vs discovery (A8 autologica)
Un pattern in distribuzioni classiche (primi, GUE, random walk, Markov chain, gap statistics) probabilmente ha un nome. Default hypothesis: re-discovery / caso limite di teorema noto. Taggare come "NEW" senza riconoscere il risultato classico piu' vicino (es. Lemke Oliver–Soundararajan per prime gaps mod q) e' beauty bias. Flag.

---

OUTPUT — UN SOLO oggetto JSON. NESSUN testo fuori dal JSON. Schema:

{
  "coherent": <bool or null>,
  "flags": [
    {
      "lens": <int 1..5>,
      "severity": "high" | "medium" | "low",
      "claim": "<frase esatta o paraphrased dal report>",
      "evidence": "<cosa mostrano realmente i dati>",
      "suggestion": "<riformulazione o check concreto per il prossimo ciclo>"
    }
  ],
  "summary": "<una frase: il report e' internamente coerente? Quale lente si e' rotta?>"
}

Regole:
- coherent=true, flags=[] se le 5 lenti sono pulite.
- coherent=null, flags=[] se i dati sono assenti o non checkable; summary spiega.
- Severity HIGH: contraddizione interna esplicita o claim falsificabile dai dati visibili.
- Severity MEDIUM: incertezza significativa, richiede verifica piu' profonda.
- Severity LOW: imprecisione terminologica, drift senza errore strutturale.
- Sii specifico: "matrice mod 5 senza zeri ma report dice 25% prohibition" e' un flag; "il report usa gergo" non lo e'.

---

Ecco il report e i dati:

{context}

---

Emetti il JSON ora (no markdown fence, no prose):
"""


def _read_safe(path: Path, max_bytes: int = 8000) -> str:
    """Read file, return empty string on error. Cap to max_bytes."""
    try:
        text = path.read_text(errors="replace")
    except Exception:
        return ""
    if len(text) > max_bytes:
        text = text[:max_bytes] + f"\n…[troncato, file e' {len(text)} bytes]"
    return text


def build_context(report_path: Path) -> str:
    """Compose report + data context for the prompt."""
    parts: list[str] = []
    parts.append("## REPORT (markdown che il producer ha scritto)\n")
    parts.append(_read_safe(report_path, 9000))
    parts.append("\n\n## FILE EMPIRICI E STRUTTURALI\n")

    canonical_files = [
        ("lab_data.json", DATA_DIR / "lab_data.json"),
        ("seme.json", DATA_DIR / "seme.json"),
        ("conoscenza_teorie.json", DATA_DIR / "conoscenza_teorie.json"),
        ("cimitero.md", REPO_DIR / "cimitero.md"),
    ]
    for label, p in canonical_files:
        content = _read_safe(p, 3500)
        if content:
            parts.append(f"\n### {label}\n```\n{content}\n```\n")
    return "".join(parts)


def call_falsifier_llm(prompt: str, timeout_s: int = 240) -> tuple[str, int]:
    """Run the falsifier prompt through provider chain (codex → claude → openrouter).

    Refactor 06/05: delega a lib_llm_chain.call_llm_chain — chain canonica con
    pre-flight cache + OpenRouter fallback HTTP. Restituisce (raw_output, exit_code)
    per backward compat con il caller esistente.
    """
    try:
        # Import lazy: lib_llm_chain è in tools/, stesso dir di lab_falsifier.py
        sys.path.insert(0, str(Path(__file__).parent))
        from lib_llm_chain import call_llm_chain
        raw = call_llm_chain(prompt, timeout=timeout_s)
        return raw, 0
    except RuntimeError as e:
        # Tutti i provider falliti
        print(f"[falsifier] chain failed: {e}", file=sys.stderr)
        return "", 1
    except Exception as e:
        print(f"[falsifier] chain error: {e}", file=sys.stderr)
        return "", 1


def _which(name: str) -> bool:
    try:
        return subprocess.run(["which", name], capture_output=True).returncode == 0
    except Exception:
        return False


def extract_json(raw: str) -> dict | None:
    """Extract first JSON object from LLM output. Tolerates fenced blocks + prose."""
    if not raw:
        return None
    # Try fenced block first
    fenced = re.search(r"```(?:json)?\s*(\{[\s\S]+?\})\s*```", raw)
    candidate = fenced.group(1) if fenced else None
    if not candidate:
        start = raw.find("{")
        if start < 0:
            return None
        candidate = raw[start:]
    try:
        return json.loads(candidate)
    except json.JSONDecodeError:
        # Try to find balancing brace
        depth = 0
        for i, c in enumerate(candidate):
            if c == "{":
                depth += 1
            elif c == "}":
                depth -= 1
                if depth == 0:
                    try:
                        return json.loads(candidate[: i + 1])
                    except json.JSONDecodeError:
                        return None
        return None


def main() -> int:
    parser = argparse.ArgumentParser(description="Counter-pole gate for the lab")
    parser.add_argument("--report", required=True, help="Path to agent_TS.md")
    parser.add_argument("--output", required=True, help="Path for falsifier_TS.json output")
    parser.add_argument("--timeout", type=int, default=240, help="LLM call timeout seconds")
    args = parser.parse_args()

    report_path = Path(args.report)
    out_path = Path(args.output)

    if not report_path.exists():
        print(f"[falsifier] report non esiste: {report_path}", file=sys.stderr)
        return 2
    if report_path.stat().st_size < 200:
        print(f"[falsifier] report troppo corto ({report_path.stat().st_size} bytes), skip", file=sys.stderr)
        return 2

    context = build_context(report_path)
    prompt = FALSIFIER_PROMPT.replace("{context}", context)

    raw, exit_code = call_falsifier_llm(prompt, timeout_s=args.timeout)
    if not raw or exit_code != 0:

diff --git a/tools/exp_boundary_mixture_gate.py b/tools/exp_boundary_mixture_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..450dc4e0e7d71215c6cb080ede346884b99fc35c
--- /dev/null
+++ b/tools/exp_boundary_mixture_gate.py
@@ -0,0 +1,278 @@
+#!/usr/bin/env python3
+"""
+exp_boundary_mixture_gate.py
+
+Reusable META/BOUNDARY audit for the GUE-Poisson boundary.
+
+Question:
+    Does the GUE/Poisson boundary remain a clean two-class split after the
+    original-vs-shuffle denominator gate, or is the mixed region an operational
+    third state where canonical observables lose stable denominators?
+
+The script uses only canonical observables from observables_registry.py.
+It builds synthetic mixtures by replacing a fraction beta of unfolded GUE
+spacings with Poisson spacings, then measures:
+
+- canonical observable vectors;
+- original-vs-shuffle z-score per observable;
+- endpoint separability in all observables and in gate-stable observables;
+- ambiguity of each beta layer relative to pure GUE and pure Poisson centroids.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
+
+
+OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())
+
+
+def gue_spacings(matrix_size: int, min_spacings: int, rng: np.random.Generator) -> np.ndarray:
+    """Generate unfolded GUE spacings by concatenating independent matrices."""
+    parts: list[np.ndarray] = []
+    edge = max(2, matrix_size // 10)
+    while sum(len(x) for x in parts) < min_spacings:
+        real = rng.standard_normal((matrix_size, matrix_size))
+        imag = rng.standard_normal((matrix_size, matrix_size))
+        h = real + 1j * imag
+        h = (h + h.conj().T) / (2.0 * np.sqrt(matrix_size))
+        eigs = np.sort(np.linalg.eigvalsh(h).real)
+        bulk = eigs[edge:-edge]
+        gaps = np.diff(bulk)
+        mean = float(np.mean(gaps))
+        if mean > 1e-15:
+            parts.append(gaps / mean)
+    return np.concatenate(parts)[:min_spacings].astype(float)
+
+
+def mixture_spacings(gue: np.ndarray, poisson: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    """Return a beta Poisson / (1-beta) GUE spacing sequence with mean spacing 1."""
+    if len(gue) != len(poisson):
+        raise ValueError("gue and poisson arrays must have the same length")
+    mask = rng.random(len(gue)) < beta
+    out = gue.copy()
+    out[mask] = poisson[mask]
+    mean = float(np.mean(out))
+    return out / mean if mean > 1e-15 else out
+
+
+def z_against_shuffle(
+    gaps: np.ndarray,
+    n_baseline: int,
+    rng: np.random.Generator,
+) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
+    """Return original observables, shuffle baseline std, and original-vs-shuffle z."""
+    original = compute_canonical(gaps)
+    baseline_vals = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline_vals[name].append(obs[name])
+
+    std = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline_vals[name], dtype=float)
+        mean = float(np.mean(vals))
+        sd = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, std, z
+
+
+def vector(row: dict, names: list[str]) -> np.ndarray:
+    return np.array([row["observables"][name] for name in names], dtype=float)
+
+
+def classify_layers(rows: list[dict], obs_names: list[str]) -> dict:
+    """Classify each beta layer by standardized distance to endpoint centroids."""
+    if not obs_names:
+        return {
+            "observables": [],
+            "endpoint_distance": 0.0,
+            "layers": {},
+            "ambiguous_beta": [],
+        }
+
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    gue_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    poi_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    all_endpoint = np.vstack([gue_vectors, poi_vectors])
+    scale = np.std(all_endpoint, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    gue_centroid = np.mean(gue_vectors, axis=0)
+    poi_centroid = np.mean(poi_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((poi_centroid - gue_centroid) / scale))
+
+    layers = {}
+    ambiguous_beta = []
+    for beta, beta_rows in sorted(by_beta.items()):
+        coords = []
+        margins = []
+        labels = []
+        for row in beta_rows:
+            x = vector(row, obs_names)
+            d_gue = float(np.linalg.norm((x - gue_centroid) / scale))
+            d_poi = float(np.linalg.norm((x - poi_centroid) / scale))
+            denom = d_gue + d_poi
+            coord = float((d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("gue" if d_gue < d_poi else "poisson")
+        ambiguous_fraction = float(np.mean(np.array(margins) < 0.15))
+        if ambiguous_fraction >= 0.5:
+            ambiguous_beta.append(beta)
+        layers[f"{beta:.3f}"] = {
+            "coordinate_mean": float(np.mean(coords)),
+            "coordinate_std": float(np.std(coords, ddof=1)) if len(coords) > 1 else 0.0,
+            "margin_mean": float(np.mean(margins)),
+            "ambiguous_fraction": ambiguous_fraction,
+            "poisson_label_fraction": float(np.mean(np.array(labels) == "poisson")),
+        }
+
+    return {
+        "observables": obs_names,
+        "endpoint_distance": endpoint_distance,
+        "layers": layers,
+        "ambiguous_beta": ambiguous_beta,
+    }
+
+
+def summarize_gate(rows: list[dict], z_min: float) -> dict:
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    layers = {}
+    for beta, beta_rows in sorted(by_beta.items()):
+        stable_counts = []
+        stable_freq = {name: [] for name in OBS_NAMES}
+        for row in beta_rows:
+            stable = [name for name in OBS_NAMES if abs(row["z"][name]) >= z_min]
+            stable_counts.append(len(stable))
+            for name in OBS_NAMES:
+                stable_freq[name].append(1.0 if name in stable else 0.0)
+        layers[f"{beta:.3f}"] = {
+            "stable_count_mean": float(np.mean(stable_counts)),
+            "stable_count_std": float(np.std(stable_counts, ddof=1)) if len(stable_counts) > 1 else 0.0,
+            "stable_frequency": {name: float(np.mean(vals)) for name, vals in stable_freq.items()},
+        }
+
+    endpoint_stable = []
+    for name in OBS_NAMES:
+        endpoint_rows = by_beta[0.0] + by_beta[1.0]
+        freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in endpoint_rows])
+        if freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def run(args: argparse.Namespace) -> dict:
+    rng = np.random.default_rng(args.seed)
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    rows = []
+
+    for rep in range(args.n_replicates):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        gue = gue_spacings(args.gue_matrix_size, args.n_gaps, rep_rng)
+        poisson = rep_rng.exponential(1.0, size=args.n_gaps)
+        poisson = poisson / float(np.mean(poisson))
+        for beta in betas:
+            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+            gaps = mixture_spacings(gue, poisson, beta, layer_rng)
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                n_baseline=args.n_baseline,
+                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "replicate": rep,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [name for name in OBS_NAMES if abs(z[name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
+
+    output = {
+        "experiment": "boundary_mixture_gate",
+        "question": "Is the GUE-Poisson mixed layer cleanly classifiable after denominator gating?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_endpoint_gated": gated_classification,
+        "rows": rows,
+    }
+
+    out_path = Path(args.out)
+    out_path.parent.mkdir(parents=True, exist_ok=True)
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
+    print(f"observables_used={OBS_NAMES}")
+    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
+    print(
+        "endpoint_distance_all="
+        f"{all_classification['endpoint_distance']:.3f} "
+        "endpoint_distance_gated="
+        f"{gated_classification['endpoint_distance']:.3f}"
+    )
+    print("beta stable_count margin_gated ambiguous_gated poisson_fraction_gated")
+    for beta in betas:
+        key = f"{beta:.3f}"
+        stable_count = gate["layers"][key]["stable_count_mean"]
+        layer = gated_classification["layers"].get(key, {})
+        print(
+            f"{beta:>4.2f} {stable_count:>12.3f} "
+            f"{layer.get('margin_mean', 0.0):>12.3f} "
+            f"{layer.get('ambiguous_fraction', 0.0):>15.3f} "
+            f"{layer.get('poisson_label_fraction', 0.0):>21.3f}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=1536)
+    parser.add_argument("--n-replicates", type=int, default=16)
+    parser.add_argument("--gue-matrix-size", type=int, default=180)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=24)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=20260507)
+    parser.add_argument("--out", default="tools/data/boundary_mixture_gate.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

codex
Il risultato principale è abbastanza netto ma dipende da una soglia di ambiguità. Faccio un seed check più leggero: non cambia il perimetro principale, serve solo a vedere se la fascia beta 0.3-0.4 è un artefatto del seed.
exec
/bin/bash -lc 'python tools/exp_boundary_mixture_gate.py --n-gaps 1024 --n-replicates 12 --gue-matrix-size 160 --n-beta 11 --n-baseline 20 --seed 20260508 --out tools/data/boundary_mixture_gate_20260507_0330_seedcheck.json' in /opt/MM_D-ND
diff --git a/tools/exp_boundary_mixture_gate.py b/tools/exp_boundary_mixture_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..450dc4e0e7d71215c6cb080ede346884b99fc35c
--- /dev/null
+++ b/tools/exp_boundary_mixture_gate.py
@@ -0,0 +1,278 @@
+#!/usr/bin/env python3
+"""
+exp_boundary_mixture_gate.py
+
+Reusable META/BOUNDARY audit for the GUE-Poisson boundary.
+
+Question:
+    Does the GUE/Poisson boundary remain a clean two-class split after the
+    original-vs-shuffle denominator gate, or is the mixed region an operational
+    third state where canonical observables lose stable denominators?
+
+The script uses only canonical observables from observables_registry.py.
+It builds synthetic mixtures by replacing a fraction beta of unfolded GUE
+spacings with Poisson spacings, then measures:
+
+- canonical observable vectors;
+- original-vs-shuffle z-score per observable;
+- endpoint separability in all observables and in gate-stable observables;
+- ambiguity of each beta layer relative to pure GUE and pure Poisson centroids.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
+
+
+OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())
+
+
+def gue_spacings(matrix_size: int, min_spacings: int, rng: np.random.Generator) -> np.ndarray:
+    """Generate unfolded GUE spacings by concatenating independent matrices."""
+    parts: list[np.ndarray] = []
+    edge = max(2, matrix_size // 10)
+    while sum(len(x) for x in parts) < min_spacings:
+        real = rng.standard_normal((matrix_size, matrix_size))
+        imag = rng.standard_normal((matrix_size, matrix_size))
+        h = real + 1j * imag
+        h = (h + h.conj().T) / (2.0 * np.sqrt(matrix_size))
+        eigs = np.sort(np.linalg.eigvalsh(h).real)
+        bulk = eigs[edge:-edge]
+        gaps = np.diff(bulk)
+        mean = float(np.mean(gaps))
+        if mean > 1e-15:
+            parts.append(gaps / mean)
+    return np.concatenate(parts)[:min_spacings].astype(float)
+
+
+def mixture_spacings(gue: np.ndarray, poisson: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    """Return a beta Poisson / (1-beta) GUE spacing sequence with mean spacing 1."""
+    if len(gue) != len(poisson):
+        raise ValueError("gue and poisson arrays must have the same length")
+    mask = rng.random(len(gue)) < beta
+    out = gue.copy()
+    out[mask] = poisson[mask]
+    mean = float(np.mean(out))
+    return out / mean if mean > 1e-15 else out
+
+
+def z_against_shuffle(
+    gaps: np.ndarray,
+    n_baseline: int,
+    rng: np.random.Generator,
+) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
+    """Return original observables, shuffle baseline std, and original-vs-shuffle z."""
+    original = compute_canonical(gaps)
+    baseline_vals = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline_vals[name].append(obs[name])
+
+    std = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline_vals[name], dtype=float)
+        mean = float(np.mean(vals))
+        sd = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, std, z
+
+
+def vector(row: dict, names: list[str]) -> np.ndarray:
+    return np.array([row["observables"][name] for name in names], dtype=float)
+
+
+def classify_layers(rows: list[dict], obs_names: list[str]) -> dict:
+    """Classify each beta layer by standardized distance to endpoint centroids."""
+    if not obs_names:
+        return {
+            "observables": [],
+            "endpoint_distance": 0.0,
+            "layers": {},
+            "ambiguous_beta": [],
+        }
+
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    gue_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    poi_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    all_endpoint = np.vstack([gue_vectors, poi_vectors])
+    scale = np.std(all_endpoint, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    gue_centroid = np.mean(gue_vectors, axis=0)
+    poi_centroid = np.mean(poi_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((poi_centroid - gue_centroid) / scale))
+
+    layers = {}
+    ambiguous_beta = []
+    for beta, beta_rows in sorted(by_beta.items()):
+        coords = []
+        margins = []
+        labels = []
+        for row in beta_rows:
+            x = vector(row, obs_names)
+            d_gue = float(np.linalg.norm((x - gue_centroid) / scale))
+            d_poi = float(np.linalg.norm((x - poi_centroid) / scale))
+            denom = d_gue + d_poi
+            coord = float((d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("gue" if d_gue < d_poi else "poisson")
+        ambiguous_fraction = float(np.mean(np.array(margins) < 0.15))
+        if ambiguous_fraction >= 0.5:
+            ambiguous_beta.append(beta)
+        layers[f"{beta:.3f}"] = {
+            "coordinate_mean": float(np.mean(coords)),
+            "coordinate_std": float(np.std(coords, ddof=1)) if len(coords) > 1 else 0.0,
+            "margin_mean": float(np.mean(margins)),
+            "ambiguous_fraction": ambiguous_fraction,
+            "poisson_label_fraction": float(np.mean(np.array(labels) == "poisson")),
+        }
+
+    return {
+        "observables": obs_names,
+        "endpoint_distance": endpoint_distance,
+        "layers": layers,
+        "ambiguous_beta": ambiguous_beta,
+    }
+
+
+def summarize_gate(rows: list[dict], z_min: float) -> dict:
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    layers = {}
+    for beta, beta_rows in sorted(by_beta.items()):
+        stable_counts = []
+        stable_freq = {name: [] for name in OBS_NAMES}
+        for row in beta_rows:
+            stable = [name for name in OBS_NAMES if abs(row["z"][name]) >= z_min]
+            stable_counts.append(len(stable))
+            for name in OBS_NAMES:
+                stable_freq[name].append(1.0 if name in stable else 0.0)
+        layers[f"{beta:.3f}"] = {
+            "stable_count_mean": float(np.mean(stable_counts)),
+            "stable_count_std": float(np.std(stable_counts, ddof=1)) if len(stable_counts) > 1 else 0.0,
+            "stable_frequency": {name: float(np.mean(vals)) for name, vals in stable_freq.items()},
+        }
+
+    endpoint_stable = []
+    for name in OBS_NAMES:
+        endpoint_rows = by_beta[0.0] + by_beta[1.0]
+        freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in endpoint_rows])
+        if freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def run(args: argparse.Namespace) -> dict:
+    rng = np.random.default_rng(args.seed)
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    rows = []
+
+    for rep in range(args.n_replicates):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        gue = gue_spacings(args.gue_matrix_size, args.n_gaps, rep_rng)
+        poisson = rep_rng.exponential(1.0, size=args.n_gaps)
+        poisson = poisson / float(np.mean(poisson))
+        for beta in betas:
+            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+            gaps = mixture_spacings(gue, poisson, beta, layer_rng)
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                n_baseline=args.n_baseline,
+                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "replicate": rep,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [name for name in OBS_NAMES if abs(z[name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
+
+    output = {
+        "experiment": "boundary_mixture_gate",
+        "question": "Is the GUE-Poisson mixed layer cleanly classifiable after denominator gating?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_endpoint_gated": gated_classification,
+        "rows": rows,
+    }
+
+    out_path = Path(args.out)
+    out_path.parent.mkdir(parents=True, exist_ok=True)
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
+    print(f"observables_used={OBS_NAMES}")
+    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
+    print(
+        "endpoint_distance_all="
+        f"{all_classification['endpoint_distance']:.3f} "
+        "endpoint_distance_gated="
+        f"{gated_classification['endpoint_distance']:.3f}"
+    )
+    print("beta stable_count margin_gated ambiguous_gated poisson_fraction_gated")
+    for beta in betas:
+        key = f"{beta:.3f}"
+        stable_count = gate["layers"][key]["stable_count_mean"]
+        layer = gated_classification["layers"].get(key, {})
+        print(
+            f"{beta:>4.2f} {stable_count:>12.3f} "
+            f"{layer.get('margin_mean', 0.0):>12.3f} "
+            f"{layer.get('ambiguous_fraction', 0.0):>15.3f} "
+            f"{layer.get('poisson_label_fraction', 0.0):>21.3f}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=1536)
+    parser.add_argument("--n-replicates", type=int, default=16)
+    parser.add_argument("--gue-matrix-size", type=int, default=180)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=24)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=20260507)
+    parser.add_argument("--out", default="tools/data/boundary_mixture_gate.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

 succeeded in 744ms:
observables_registry=1.0.0-2026-05-06
observables_used=['SR', 'SR2', 'L1', 'L2', 'triple_var']
endpoint_stable_observables=[]
endpoint_distance_all=3.958 endpoint_distance_gated=0.000
beta stable_count margin_gated ambiguous_gated poisson_fraction_gated
0.00        3.500        0.000           0.000                 0.000
0.10        3.250        0.000           0.000                 0.000
0.20        2.833        0.000           0.000                 0.000
0.30        1.250        0.000           0.000                 0.000
0.40        0.833        0.000           0.000                 0.000
0.50        0.500        0.000           0.000                 0.000
0.60        0.333        0.000           0.000                 0.000
0.70        0.333        0.000           0.000                 0.000
0.80        0.333        0.000           0.000                 0.000
0.90        0.333        0.000           0.000                 0.000
1.00        0.500        0.000           0.000                 0.000
saved tools/data/boundary_mixture_gate_20260507_0330_seedcheck.json

exec
/bin/bash -lc "python - <<'PY'
import json
for p in ['tools/data/boundary_mixture_gate_20260507_0330_seedcheck.json']:
 d=json.load(open(p)); print(p); print('ambig', d['classification_all_observables']['ambiguous_beta'])
 for k,v in d['classification_all_observables']['layers'].items():
  g=d['gate']['layers'][k]
  print(k, round(v['margin_mean'],3), v['ambiguous_fraction'], v['poisson_label_fraction'], round(g['stable_count_mean'],3))
PY" in /opt/MM_D-ND
 succeeded in 0ms:
tools/data/boundary_mixture_gate_20260507_0330_seedcheck.json
ambig [0.30000000000000004, 0.4]
0.000 0.695 0.0 0.0 3.5
0.100 0.477 0.0 0.0 3.25
0.200 0.271 0.16666666666666666 0.0 2.833
0.300 0.082 0.9166666666666666 0.3333333333333333 1.25
0.400 0.125 0.75 0.8333333333333334 0.833
0.500 0.28 0.0 1.0 0.5
0.600 0.391 0.0 1.0 0.333
0.700 0.525 0.0 1.0 0.333
0.800 0.582 0.0 1.0 0.333
0.900 0.659 0.0 1.0 0.333
1.000 0.705 0.0 1.0 0.5

diff --git a/tools/exp_boundary_mixture_gate.py b/tools/exp_boundary_mixture_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..450dc4e0e7d71215c6cb080ede346884b99fc35c
--- /dev/null
+++ b/tools/exp_boundary_mixture_gate.py
@@ -0,0 +1,278 @@
+#!/usr/bin/env python3
+"""
+exp_boundary_mixture_gate.py
+
+Reusable META/BOUNDARY audit for the GUE-Poisson boundary.
+
+Question:
+    Does the GUE/Poisson boundary remain a clean two-class split after the
+    original-vs-shuffle denominator gate, or is the mixed region an operational
+    third state where canonical observables lose stable denominators?
+
+The script uses only canonical observables from observables_registry.py.
+It builds synthetic mixtures by replacing a fraction beta of unfolded GUE
+spacings with Poisson spacings, then measures:
+
+- canonical observable vectors;
+- original-vs-shuffle z-score per observable;
+- endpoint separability in all observables and in gate-stable observables;
+- ambiguity of each beta layer relative to pure GUE and pure Poisson centroids.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
+
+
+OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())
+
+
+def gue_spacings(matrix_size: int, min_spacings: int, rng: np.random.Generator) -> np.ndarray:
+    """Generate unfolded GUE spacings by concatenating independent matrices."""
+    parts: list[np.ndarray] = []
+    edge = max(2, matrix_size // 10)
+    while sum(len(x) for x in parts) < min_spacings:
+        real = rng.standard_normal((matrix_size, matrix_size))
+        imag = rng.standard_normal((matrix_size, matrix_size))
+        h = real + 1j * imag
+        h = (h + h.conj().T) / (2.0 * np.sqrt(matrix_size))
+        eigs = np.sort(np.linalg.eigvalsh(h).real)
+        bulk = eigs[edge:-edge]
+        gaps = np.diff(bulk)
+        mean = float(np.mean(gaps))
+        if mean > 1e-15:
+            parts.append(gaps / mean)
+    return np.concatenate(parts)[:min_spacings].astype(float)
+
+
+def mixture_spacings(gue: np.ndarray, poisson: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    """Return a beta Poisson / (1-beta) GUE spacing sequence with mean spacing 1."""
+    if len(gue) != len(poisson):
+        raise ValueError("gue and poisson arrays must have the same length")
+    mask = rng.random(len(gue)) < beta
+    out = gue.copy()
+    out[mask] = poisson[mask]
+    mean = float(np.mean(out))
+    return out / mean if mean > 1e-15 else out
+
+
+def z_against_shuffle(
+    gaps: np.ndarray,
+    n_baseline: int,
+    rng: np.random.Generator,
+) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
+    """Return original observables, shuffle baseline std, and original-vs-shuffle z."""
+    original = compute_canonical(gaps)
+    baseline_vals = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline_vals[name].append(obs[name])
+
+    std = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline_vals[name], dtype=float)
+        mean = float(np.mean(vals))
+        sd = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, std, z
+
+
+def vector(row: dict, names: list[str]) -> np.ndarray:
+    return np.array([row["observables"][name] for name in names], dtype=float)
+
+
+def classify_layers(rows: list[dict], obs_names: list[str]) -> dict:
+    """Classify each beta layer by standardized distance to endpoint centroids."""
+    if not obs_names:
+        return {
+            "observables": [],
+            "endpoint_distance": 0.0,
+            "layers": {},
+            "ambiguous_beta": [],
+        }
+
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    gue_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    poi_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    all_endpoint = np.vstack([gue_vectors, poi_vectors])
+    scale = np.std(all_endpoint, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    gue_centroid = np.mean(gue_vectors, axis=0)
+    poi_centroid = np.mean(poi_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((poi_centroid - gue_centroid) / scale))
+
+    layers = {}
+    ambiguous_beta = []
+    for beta, beta_rows in sorted(by_beta.items()):
+        coords = []
+        margins = []
+        labels = []
+        for row in beta_rows:
+            x = vector(row, obs_names)
+            d_gue = float(np.linalg.norm((x - gue_centroid) / scale))
+            d_poi = float(np.linalg.norm((x - poi_centroid) / scale))
+            denom = d_gue + d_poi
+            coord = float((d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("gue" if d_gue < d_poi else "poisson")
+        ambiguous_fraction = float(np.mean(np.array(margins) < 0.15))
+        if ambiguous_fraction >= 0.5:
+            ambiguous_beta.append(beta)
+        layers[f"{beta:.3f}"] = {
+            "coordinate_mean": float(np.mean(coords)),
+            "coordinate_std": float(np.std(coords, ddof=1)) if len(coords) > 1 else 0.0,
+            "margin_mean": float(np.mean(margins)),
+            "ambiguous_fraction": ambiguous_fraction,
+            "poisson_label_fraction": float(np.mean(np.array(labels) == "poisson")),
+        }
+
+    return {
+        "observables": obs_names,
+        "endpoint_distance": endpoint_distance,
+        "layers": layers,
+        "ambiguous_beta": ambiguous_beta,
+    }
+
+
+def summarize_gate(rows: list[dict], z_min: float) -> dict:
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    layers = {}
+    for beta, beta_rows in sorted(by_beta.items()):
+        stable_counts = []
+        stable_freq = {name: [] for name in OBS_NAMES}
+        for row in beta_rows:
+            stable = [name for name in OBS_NAMES if abs(row["z"][name]) >= z_min]
+            stable_counts.append(len(stable))
+            for name in OBS_NAMES:
+                stable_freq[name].append(1.0 if name in stable else 0.0)
+        layers[f"{beta:.3f}"] = {
+            "stable_count_mean": float(np.mean(stable_counts)),
+            "stable_count_std": float(np.std(stable_counts, ddof=1)) if len(stable_counts) > 1 else 0.0,
+            "stable_frequency": {name: float(np.mean(vals)) for name, vals in stable_freq.items()},
+        }
+
+    endpoint_stable = []
+    for name in OBS_NAMES:
+        endpoint_rows = by_beta[0.0] + by_beta[1.0]
+        freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in endpoint_rows])
+        if freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def run(args: argparse.Namespace) -> dict:
+    rng = np.random.default_rng(args.seed)
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    rows = []
+
+    for rep in range(args.n_replicates):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        gue = gue_spacings(args.gue_matrix_size, args.n_gaps, rep_rng)
+        poisson = rep_rng.exponential(1.0, size=args.n_gaps)
+        poisson = poisson / float(np.mean(poisson))
+        for beta in betas:
+            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+            gaps = mixture_spacings(gue, poisson, beta, layer_rng)
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                n_baseline=args.n_baseline,
+                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "replicate": rep,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [name for name in OBS_NAMES if abs(z[name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
+
+    output = {
+        "experiment": "boundary_mixture_gate",
+        "question": "Is the GUE-Poisson mixed layer cleanly classifiable after denominator gating?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_endpoint_gated": gated_classification,
+        "rows": rows,
+    }
+
+    out_path = Path(args.out)
+    out_path.parent.mkdir(parents=True, exist_ok=True)
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
+    print(f"observables_used={OBS_NAMES}")
+    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
+    print(
+        "endpoint_distance_all="
+        f"{all_classification['endpoint_distance']:.3f} "
+        "endpoint_distance_gated="
+        f"{gated_classification['endpoint_distance']:.3f}"
+    )
+    print("beta stable_count margin_gated ambiguous_gated poisson_fraction_gated")
+    for beta in betas:
+        key = f"{beta:.3f}"
+        stable_count = gate["layers"][key]["stable_count_mean"]
+        layer = gated_classification["layers"].get(key, {})
+        print(
+            f"{beta:>4.2f} {stable_count:>12.3f} "
+            f"{layer.get('margin_mean', 0.0):>12.3f} "
+            f"{layer.get('ambiguous_fraction', 0.0):>15.3f} "
+            f"{layer.get('poisson_label_fraction', 0.0):>21.3f}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=1536)
+    parser.add_argument("--n-replicates", type=int, default=16)
+    parser.add_argument("--gue-matrix-size", type=int, default=180)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=24)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=20260507)
+    parser.add_argument("--out", default="tools/data/boundary_mixture_gate.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

codex
Il seed check ripete la stessa fascia ambigua beta 0.3-0.4. Aggiusto solo l’output console dello strumento: quando il gate endpoint è vuoto deve stampare esplicitamente la classificazione all-observables, altrimenti il log sembra tutto zero pur avendo dati validi nel JSON.
apply patch
patch: completed
/opt/MM_D-ND/tools/exp_boundary_mixture_gate.py
diff --git a/tools/exp_boundary_mixture_gate.py b/tools/exp_boundary_mixture_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..fcc62a3ba54911760e60cbf1ec299013dff19041
--- /dev/null
+++ b/tools/exp_boundary_mixture_gate.py
@@ -0,0 +1,283 @@
+#!/usr/bin/env python3
+"""
+exp_boundary_mixture_gate.py
+
+Reusable META/BOUNDARY audit for the GUE-Poisson boundary.
+
+Question:
+    Does the GUE/Poisson boundary remain a clean two-class split after the
+    original-vs-shuffle denominator gate, or is the mixed region an operational
+    third state where canonical observables lose stable denominators?
+
+The script uses only canonical observables from observables_registry.py.
+It builds synthetic mixtures by replacing a fraction beta of unfolded GUE
+spacings with Poisson spacings, then measures:
+
+- canonical observable vectors;
+- original-vs-shuffle z-score per observable;
+- endpoint separability in all observables and in gate-stable observables;
+- ambiguity of each beta layer relative to pure GUE and pure Poisson centroids.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
+
+
+OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())
+
+
+def gue_spacings(matrix_size: int, min_spacings: int, rng: np.random.Generator) -> np.ndarray:
+    """Generate unfolded GUE spacings by concatenating independent matrices."""
+    parts: list[np.ndarray] = []
+    edge = max(2, matrix_size // 10)
+    while sum(len(x) for x in parts) < min_spacings:
+        real = rng.standard_normal((matrix_size, matrix_size))
+        imag = rng.standard_normal((matrix_size, matrix_size))
+        h = real + 1j * imag
+        h = (h + h.conj().T) / (2.0 * np.sqrt(matrix_size))
+        eigs = np.sort(np.linalg.eigvalsh(h).real)
+        bulk = eigs[edge:-edge]
+        gaps = np.diff(bulk)
+        mean = float(np.mean(gaps))
+        if mean > 1e-15:
+            parts.append(gaps / mean)
+    return np.concatenate(parts)[:min_spacings].astype(float)
+
+
+def mixture_spacings(gue: np.ndarray, poisson: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    """Return a beta Poisson / (1-beta) GUE spacing sequence with mean spacing 1."""
+    if len(gue) != len(poisson):
+        raise ValueError("gue and poisson arrays must have the same length")
+    mask = rng.random(len(gue)) < beta
+    out = gue.copy()
+    out[mask] = poisson[mask]
+    mean = float(np.mean(out))
+    return out / mean if mean > 1e-15 else out
+
+
+def z_against_shuffle(
+    gaps: np.ndarray,
+    n_baseline: int,
+    rng: np.random.Generator,
+) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
+    """Return original observables, shuffle baseline std, and original-vs-shuffle z."""
+    original = compute_canonical(gaps)
+    baseline_vals = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline_vals[name].append(obs[name])
+
+    std = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline_vals[name], dtype=float)
+        mean = float(np.mean(vals))
+        sd = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, std, z
+
+
+def vector(row: dict, names: list[str]) -> np.ndarray:
+    return np.array([row["observables"][name] for name in names], dtype=float)
+
+
+def classify_layers(rows: list[dict], obs_names: list[str]) -> dict:
+    """Classify each beta layer by standardized distance to endpoint centroids."""
+    if not obs_names:
+        return {
+            "observables": [],
+            "endpoint_distance": 0.0,
+            "layers": {},
+            "ambiguous_beta": [],
+        }
+
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    gue_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    poi_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    all_endpoint = np.vstack([gue_vectors, poi_vectors])
+    scale = np.std(all_endpoint, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    gue_centroid = np.mean(gue_vectors, axis=0)
+    poi_centroid = np.mean(poi_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((poi_centroid - gue_centroid) / scale))
+
+    layers = {}
+    ambiguous_beta = []
+    for beta, beta_rows in sorted(by_beta.items()):
+        coords = []
+        margins = []
+        labels = []
+        for row in beta_rows:
+            x = vector(row, obs_names)
+            d_gue = float(np.linalg.norm((x - gue_centroid) / scale))
+            d_poi = float(np.linalg.norm((x - poi_centroid) / scale))
+            denom = d_gue + d_poi
+            coord = float((d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("gue" if d_gue < d_poi else "poisson")
+        ambiguous_fraction = float(np.mean(np.array(margins) < 0.15))
+        if ambiguous_fraction >= 0.5:
+            ambiguous_beta.append(beta)
+        layers[f"{beta:.3f}"] = {
+            "coordinate_mean": float(np.mean(coords)),
+            "coordinate_std": float(np.std(coords, ddof=1)) if len(coords) > 1 else 0.0,
+            "margin_mean": float(np.mean(margins)),
+            "ambiguous_fraction": ambiguous_fraction,
+            "poisson_label_fraction": float(np.mean(np.array(labels) == "poisson")),
+        }
+
+    return {
+        "observables": obs_names,
+        "endpoint_distance": endpoint_distance,
+        "layers": layers,
+        "ambiguous_beta": ambiguous_beta,
+    }
+
+
+def summarize_gate(rows: list[dict], z_min: float) -> dict:
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    layers = {}
+    for beta, beta_rows in sorted(by_beta.items()):
+        stable_counts = []
+        stable_freq = {name: [] for name in OBS_NAMES}
+        for row in beta_rows:
+            stable = [name for name in OBS_NAMES if abs(row["z"][name]) >= z_min]
+            stable_counts.append(len(stable))
+            for name in OBS_NAMES:
+                stable_freq[name].append(1.0 if name in stable else 0.0)
+        layers[f"{beta:.3f}"] = {
+            "stable_count_mean": float(np.mean(stable_counts)),
+            "stable_count_std": float(np.std(stable_counts, ddof=1)) if len(stable_counts) > 1 else 0.0,
+            "stable_frequency": {name: float(np.mean(vals)) for name, vals in stable_freq.items()},
+        }
+
+    endpoint_stable = []
+    for name in OBS_NAMES:
+        endpoint_rows = by_beta[0.0] + by_beta[1.0]
+        freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in endpoint_rows])
+        if freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def run(args: argparse.Namespace) -> dict:
+    rng = np.random.default_rng(args.seed)
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    rows = []
+
+    for rep in range(args.n_replicates):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        gue = gue_spacings(args.gue_matrix_size, args.n_gaps, rep_rng)
+        poisson = rep_rng.exponential(1.0, size=args.n_gaps)
+        poisson = poisson / float(np.mean(poisson))
+        for beta in betas:
+            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+            gaps = mixture_spacings(gue, poisson, beta, layer_rng)
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                n_baseline=args.n_baseline,
+                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "replicate": rep,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [name for name in OBS_NAMES if abs(z[name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
+
+    output = {
+        "experiment": "boundary_mixture_gate",
+        "question": "Is the GUE-Poisson mixed layer cleanly classifiable after denominator gating?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_endpoint_gated": gated_classification,
+        "rows": rows,
+    }
+
+    out_path = Path(args.out)
+    out_path.parent.mkdir(parents=True, exist_ok=True)
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
+    print(f"observables_used={OBS_NAMES}")
+    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
+    print(
+        "endpoint_distance_all="
+        f"{all_classification['endpoint_distance']:.3f} "
+        "endpoint_distance_gated="
+        f"{gated_classification['endpoint_distance']:.3f}"
+    )
+    if gate["endpoint_stable_observables"]:
+        print("beta stable_count margin_gated ambiguous_gated poisson_fraction_gated")
+    else:
+        print("endpoint gate is empty; printing all-observable classification")
+        print("beta stable_count margin_all ambiguous_all poisson_fraction_all")
+    for beta in betas:
+        key = f"{beta:.3f}"
+        stable_count = gate["layers"][key]["stable_count_mean"]
+        source = gated_classification if gate["endpoint_stable_observables"] else all_classification
+        layer = source["layers"].get(key, {})
+        print(
+            f"{beta:>4.2f} {stable_count:>12.3f} "
+            f"{layer.get('margin_mean', 0.0):>12.3f} "
+            f"{layer.get('ambiguous_fraction', 0.0):>15.3f} "
+            f"{layer.get('poisson_label_fraction', 0.0):>21.3f}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=1536)
+    parser.add_argument("--n-replicates", type=int, default=16)
+    parser.add_argument("--gue-matrix-size", type=int, default=180)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=24)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=20260507)
+    parser.add_argument("--out", default="tools/data/boundary_mixture_gate.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

diff --git a/tools/exp_boundary_mixture_gate.py b/tools/exp_boundary_mixture_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..fcc62a3ba54911760e60cbf1ec299013dff19041
--- /dev/null
+++ b/tools/exp_boundary_mixture_gate.py
@@ -0,0 +1,283 @@
+#!/usr/bin/env python3
+"""
+exp_boundary_mixture_gate.py
+
+Reusable META/BOUNDARY audit for the GUE-Poisson boundary.
+
+Question:
+    Does the GUE/Poisson boundary remain a clean two-class split after the
+    original-vs-shuffle denominator gate, or is the mixed region an operational
+    third state where canonical observables lose stable denominators?
+
+The script uses only canonical observables from observables_registry.py.
+It builds synthetic mixtures by replacing a fraction beta of unfolded GUE
+spacings with Poisson spacings, then measures:
+
+- canonical observable vectors;
+- original-vs-shuffle z-score per observable;
+- endpoint separability in all observables and in gate-stable observables;
+- ambiguity of each beta layer relative to pure GUE and pure Poisson centroids.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
+
+
+OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())
+
+
+def gue_spacings(matrix_size: int, min_spacings: int, rng: np.random.Generator) -> np.ndarray:
+    """Generate unfolded GUE spacings by concatenating independent matrices."""
+    parts: list[np.ndarray] = []
+    edge = max(2, matrix_size // 10)
+    while sum(len(x) for x in parts) < min_spacings:
+        real = rng.standard_normal((matrix_size, matrix_size))
+        imag = rng.standard_normal((matrix_size, matrix_size))
+        h = real + 1j * imag
+        h = (h + h.conj().T) / (2.0 * np.sqrt(matrix_size))
+        eigs = np.sort(np.linalg.eigvalsh(h).real)
+        bulk = eigs[edge:-edge]
+        gaps = np.diff(bulk)
+        mean = float(np.mean(gaps))
+        if mean > 1e-15:
+            parts.append(gaps / mean)
+    return np.concatenate(parts)[:min_spacings].astype(float)
+
+
+def mixture_spacings(gue: np.ndarray, poisson: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    """Return a beta Poisson / (1-beta) GUE spacing sequence with mean spacing 1."""
+    if len(gue) != len(poisson):
+        raise ValueError("gue and poisson arrays must have the same length")
+    mask = rng.random(len(gue)) < beta
+    out = gue.copy()
+    out[mask] = poisson[mask]
+    mean = float(np.mean(out))
+    return out / mean if mean > 1e-15 else out
+
+
+def z_against_shuffle(
+    gaps: np.ndarray,
+    n_baseline: int,
+    rng: np.random.Generator,
+) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
+    """Return original observables, shuffle baseline std, and original-vs-shuffle z."""
+    original = compute_canonical(gaps)
+    baseline_vals = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline_vals[name].append(obs[name])
+
+    std = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline_vals[name], dtype=float)
+        mean = float(np.mean(vals))
+        sd = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, std, z
+
+
+def vector(row: dict, names: list[str]) -> np.ndarray:
+    return np.array([row["observables"][name] for name in names], dtype=float)
+
+
+def classify_layers(rows: list[dict], obs_names: list[str]) -> dict:
+    """Classify each beta layer by standardized distance to endpoint centroids."""
+    if not obs_names:
+        return {
+            "observables": [],
+            "endpoint_distance": 0.0,
+            "layers": {},
+            "ambiguous_beta": [],
+        }
+
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    gue_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    poi_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    all_endpoint = np.vstack([gue_vectors, poi_vectors])
+    scale = np.std(all_endpoint, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    gue_centroid = np.mean(gue_vectors, axis=0)
+    poi_centroid = np.mean(poi_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((poi_centroid - gue_centroid) / scale))
+
+    layers = {}
+    ambiguous_beta = []
+    for beta, beta_rows in sorted(by_beta.items()):
+        coords = []
+        margins = []
+        labels = []
+        for row in beta_rows:
+            x = vector(row, obs_names)
+            d_gue = float(np.linalg.norm((x - gue_centroid) / scale))
+            d_poi = float(np.linalg.norm((x - poi_centroid) / scale))
+            denom = d_gue + d_poi
+            coord = float((d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("gue" if d_gue < d_poi else "poisson")
+        ambiguous_fraction = float(np.mean(np.array(margins) < 0.15))
+        if ambiguous_fraction >= 0.5:
+            ambiguous_beta.append(beta)
+        layers[f"{beta:.3f}"] = {
+            "coordinate_mean": float(np.mean(coords)),
+            "coordinate_std": float(np.std(coords, ddof=1)) if len(coords) > 1 else 0.0,
+            "margin_mean": float(np.mean(margins)),
+            "ambiguous_fraction": ambiguous_fraction,
+            "poisson_label_fraction": float(np.mean(np.array(labels) == "poisson")),
+        }
+
+    return {
+        "observables": obs_names,
+        "endpoint_distance": endpoint_distance,
+        "layers": layers,
+        "ambiguous_beta": ambiguous_beta,
+    }
+
+
+def summarize_gate(rows: list[dict], z_min: float) -> dict:
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    layers = {}
+    for beta, beta_rows in sorted(by_beta.items()):
+        stable_counts = []
+        stable_freq = {name: [] for name in OBS_NAMES}
+        for row in beta_rows:
+            stable = [name for name in OBS_NAMES if abs(row["z"][name]) >= z_min]
+            stable_counts.append(len(stable))
+            for name in OBS_NAMES:
+                stable_freq[name].append(1.0 if name in stable else 0.0)
+        layers[f"{beta:.3f}"] = {
+            "stable_count_mean": float(np.mean(stable_counts)),
+            "stable_count_std": float(np.std(stable_counts, ddof=1)) if len(stable_counts) > 1 else 0.0,
+            "stable_frequency": {name: float(np.mean(vals)) for name, vals in stable_freq.items()},
+        }
+
+    endpoint_stable = []
+    for name in OBS_NAMES:
+        endpoint_rows = by_beta[0.0] + by_beta[1.0]
+        freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in endpoint_rows])
+        if freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def run(args: argparse.Namespace) -> dict:
+    rng = np.random.default_rng(args.seed)
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    rows = []
+
+    for rep in range(args.n_replicates):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        gue = gue_spacings(args.gue_matrix_size, args.n_gaps, rep_rng)
+        poisson = rep_rng.exponential(1.0, size=args.n_gaps)
+        poisson = poisson / float(np.mean(poisson))
+        for beta in betas:
+            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+            gaps = mixture_spacings(gue, poisson, beta, layer_rng)
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                n_baseline=args.n_baseline,
+                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "replicate": rep,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [name for name in OBS_NAMES if abs(z[name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
+
+    output = {
+        "experiment": "boundary_mixture_gate",
+        "question": "Is the GUE-Poisson mixed layer cleanly classifiable after denominator gating?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_endpoint_gated": gated_classification,
+        "rows": rows,
+    }
+
+    out_path = Path(args.out)
+    out_path.parent.mkdir(parents=True, exist_ok=True)
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
+    print(f"observables_used={OBS_NAMES}")
+    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
+    print(
+        "endpoint_distance_all="
+        f"{all_classification['endpoint_distance']:.3f} "
+        "endpoint_distance_gated="
+        f"{gated_classification['endpoint_distance']:.3f}"
+    )
+    if gate["endpoint_stable_observables"]:
+        print("beta stable_count margin_gated ambiguous_gated poisson_fraction_gated")
+    else:
+        print("endpoint gate is empty; printing all-observable classification")
+        print("beta stable_count margin_all ambiguous_all poisson_fraction_all")
+    for beta in betas:
+        key = f"{beta:.3f}"
+        stable_count = gate["layers"][key]["stable_count_mean"]
+        source = gated_classification if gate["endpoint_stable_observables"] else all_classification
+        layer = source["layers"].get(key, {})
+        print(
+            f"{beta:>4.2f} {stable_count:>12.3f} "
+            f"{layer.get('margin_mean', 0.0):>12.3f} "
+            f"{layer.get('ambiguous_fraction', 0.0):>15.3f} "
+            f"{layer.get('poisson_label_fraction', 0.0):>21.3f}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=1536)
+    parser.add_argument("--n-replicates", type=int, default=16)
+    parser.add_argument("--gue-matrix-size", type=int, default=180)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=24)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=20260507)
+    parser.add_argument("--out", default="tools/data/boundary_mixture_gate.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

exec
/bin/bash -lc 'python -m py_compile tools/exp_boundary_mixture_gate.py' in /opt/MM_D-ND
 succeeded in 0ms:
exec
/bin/bash -lc 'python tools/exp_boundary_mixture_gate.py --n-gaps 256 --n-replicates 2 --gue-matrix-size 80 --n-beta 3 --n-baseline 4 --out /tmp/bmg_smoke.json' in /opt/MM_D-ND
diff --git a/tools/exp_boundary_mixture_gate.py b/tools/exp_boundary_mixture_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..fcc62a3ba54911760e60cbf1ec299013dff19041
--- /dev/null
+++ b/tools/exp_boundary_mixture_gate.py
@@ -0,0 +1,283 @@
+#!/usr/bin/env python3
+"""
+exp_boundary_mixture_gate.py
+
+Reusable META/BOUNDARY audit for the GUE-Poisson boundary.
+
+Question:
+    Does the GUE/Poisson boundary remain a clean two-class split after the
+    original-vs-shuffle denominator gate, or is the mixed region an operational
+    third state where canonical observables lose stable denominators?
+
+The script uses only canonical observables from observables_registry.py.
+It builds synthetic mixtures by replacing a fraction beta of unfolded GUE
+spacings with Poisson spacings, then measures:
+
+- canonical observable vectors;
+- original-vs-shuffle z-score per observable;
+- endpoint separability in all observables and in gate-stable observables;
+- ambiguity of each beta layer relative to pure GUE and pure Poisson centroids.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
+
+
+OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())
+
+
+def gue_spacings(matrix_size: int, min_spacings: int, rng: np.random.Generator) -> np.ndarray:
+    """Generate unfolded GUE spacings by concatenating independent matrices."""
+    parts: list[np.ndarray] = []
+    edge = max(2, matrix_size // 10)
+    while sum(len(x) for x in parts) < min_spacings:
+        real = rng.standard_normal((matrix_size, matrix_size))
+        imag = rng.standard_normal((matrix_size, matrix_size))
+        h = real + 1j * imag
+        h = (h + h.conj().T) / (2.0 * np.sqrt(matrix_size))
+        eigs = np.sort(np.linalg.eigvalsh(h).real)
+        bulk = eigs[edge:-edge]
+        gaps = np.diff(bulk)
+        mean = float(np.mean(gaps))
+        if mean > 1e-15:
+            parts.append(gaps / mean)
+    return np.concatenate(parts)[:min_spacings].astype(float)
+
+
+def mixture_spacings(gue: np.ndarray, poisson: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    """Return a beta Poisson / (1-beta) GUE spacing sequence with mean spacing 1."""
+    if len(gue) != len(poisson):
+        raise ValueError("gue and poisson arrays must have the same length")
+    mask = rng.random(len(gue)) < beta
+    out = gue.copy()
+    out[mask] = poisson[mask]
+    mean = float(np.mean(out))
+    return out / mean if mean > 1e-15 else out
+
+
+def z_against_shuffle(
+    gaps: np.ndarray,
+    n_baseline: int,
+    rng: np.random.Generator,
+) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
+    """Return original observables, shuffle baseline std, and original-vs-shuffle z."""
+    original = compute_canonical(gaps)
+    baseline_vals = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline_vals[name].append(obs[name])
+
+    std = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline_vals[name], dtype=float)
+        mean = float(np.mean(vals))
+        sd = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, std, z
+
+
+def vector(row: dict, names: list[str]) -> np.ndarray:
+    return np.array([row["observables"][name] for name in names], dtype=float)
+
+
+def classify_layers(rows: list[dict], obs_names: list[str]) -> dict:
+    """Classify each beta layer by standardized distance to endpoint centroids."""
+    if not obs_names:
+        return {
+            "observables": [],
+            "endpoint_distance": 0.0,
+            "layers": {},
+            "ambiguous_beta": [],
+        }
+
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    gue_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    poi_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    all_endpoint = np.vstack([gue_vectors, poi_vectors])
+    scale = np.std(all_endpoint, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    gue_centroid = np.mean(gue_vectors, axis=0)
+    poi_centroid = np.mean(poi_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((poi_centroid - gue_centroid) / scale))
+
+    layers = {}
+    ambiguous_beta = []
+    for beta, beta_rows in sorted(by_beta.items()):
+        coords = []
+        margins = []
+        labels = []
+        for row in beta_rows:
+            x = vector(row, obs_names)
+            d_gue = float(np.linalg.norm((x - gue_centroid) / scale))
+            d_poi = float(np.linalg.norm((x - poi_centroid) / scale))
+            denom = d_gue + d_poi
+            coord = float((d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("gue" if d_gue < d_poi else "poisson")
+        ambiguous_fraction = float(np.mean(np.array(margins) < 0.15))
+        if ambiguous_fraction >= 0.5:
+            ambiguous_beta.append(beta)
+        layers[f"{beta:.3f}"] = {
+            "coordinate_mean": float(np.mean(coords)),
+            "coordinate_std": float(np.std(coords, ddof=1)) if len(coords) > 1 else 0.0,
+            "margin_mean": float(np.mean(margins)),
+            "ambiguous_fraction": ambiguous_fraction,
+            "poisson_label_fraction": float(np.mean(np.array(labels) == "poisson")),
+        }
+
+    return {
+        "observables": obs_names,
+        "endpoint_distance": endpoint_distance,
+        "layers": layers,
+        "ambiguous_beta": ambiguous_beta,
+    }
+
+
+def summarize_gate(rows: list[dict], z_min: float) -> dict:
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    layers = {}
+    for beta, beta_rows in sorted(by_beta.items()):
+        stable_counts = []
+        stable_freq = {name: [] for name in OBS_NAMES}
+        for row in beta_rows:
+            stable = [name for name in OBS_NAMES if abs(row["z"][name]) >= z_min]
+            stable_counts.append(len(stable))
+            for name in OBS_NAMES:
+                stable_freq[name].append(1.0 if name in stable else 0.0)
+        layers[f"{beta:.3f}"] = {
+            "stable_count_mean": float(np.mean(stable_counts)),
+            "stable_count_std": float(np.std(stable_counts, ddof=1)) if len(stable_counts) > 1 else 0.0,
+            "stable_frequency": {name: float(np.mean(vals)) for name, vals in stable_freq.items()},
+        }
+
+    endpoint_stable = []
+    for name in OBS_NAMES:
+        endpoint_rows = by_beta[0.0] + by_beta[1.0]
+        freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in endpoint_rows])
+        if freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def run(args: argparse.Namespace) -> dict:
+    rng = np.random.default_rng(args.seed)
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    rows = []
+
+    for rep in range(args.n_replicates):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        gue = gue_spacings(args.gue_matrix_size, args.n_gaps, rep_rng)
+        poisson = rep_rng.exponential(1.0, size=args.n_gaps)
+        poisson = poisson / float(np.mean(poisson))
+        for beta in betas:
+            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+            gaps = mixture_spacings(gue, poisson, beta, layer_rng)
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                n_baseline=args.n_baseline,
+                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "replicate": rep,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [name for name in OBS_NAMES if abs(z[name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
+
+    output = {
+        "experiment": "boundary_mixture_gate",
+        "question": "Is the GUE-Poisson mixed layer cleanly classifiable after denominator gating?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_endpoint_gated": gated_classification,
+        "rows": rows,
+    }
+
+    out_path = Path(args.out)
+    out_path.parent.mkdir(parents=True, exist_ok=True)
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
+    print(f"observables_used={OBS_NAMES}")
+    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
+    print(
+        "endpoint_distance_all="
+        f"{all_classification['endpoint_distance']:.3f} "
+        "endpoint_distance_gated="
+        f"{gated_classification['endpoint_distance']:.3f}"
+    )
+    if gate["endpoint_stable_observables"]:
+        print("beta stable_count margin_gated ambiguous_gated poisson_fraction_gated")
+    else:
+        print("endpoint gate is empty; printing all-observable classification")
+        print("beta stable_count margin_all ambiguous_all poisson_fraction_all")
+    for beta in betas:
+        key = f"{beta:.3f}"
+        stable_count = gate["layers"][key]["stable_count_mean"]
+        source = gated_classification if gate["endpoint_stable_observables"] else all_classification
+        layer = source["layers"].get(key, {})
+        print(
+            f"{beta:>4.2f} {stable_count:>12.3f} "
+            f"{layer.get('margin_mean', 0.0):>12.3f} "
+            f"{layer.get('ambiguous_fraction', 0.0):>15.3f} "
+            f"{layer.get('poisson_label_fraction', 0.0):>21.3f}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=1536)
+    parser.add_argument("--n-replicates", type=int, default=16)
+    parser.add_argument("--gue-matrix-size", type=int, default=180)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=24)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=20260507)
+    parser.add_argument("--out", default="tools/data/boundary_mixture_gate.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

 succeeded in 117ms:
codex
Ora cristallizzo il report. Il claim sarà stretto: non “il confine è dimostrato come terzo incluso”, ma “nel perimetro sintetico GUE-Poisson, la fascia beta 0.3-0.4 è classificatoriamente ambigua e perde denominatori stabili; quindi il boundary va trattato come layer operativo, non come linea netta”.
apply patch
patch: completed
/opt/MM_D-ND/tools/data/reports/agent_20260507_0330.md
diff --git a/tools/data/reports/agent_20260507_0330.md b/tools/data/reports/agent_20260507_0330.md
new file mode 100644
index 0000000000000000000000000000000000000000..1bb66d4f31ae12b64e2652ac30c3e475aad25db7
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0330.md
@@ -0,0 +1,109 @@
+# Agent Report — The GUE-Poisson Boundary Is a Denominator Collapse Layer
+**Date**: 2026-05-07 03:30  
+**Piano**: 68  
+**Tension explored**: META + BOUNDARY  
+observables_registry: 1.0.0-2026-05-06  
+observables_used: [SR, SR2, L1, L2, triple_var]
+
+## Claim Under Test
+The last three runs constrained perturbation rank and observable collinearity:
+rank/PC2 claims are not interpretable without the canonical observable registry
+and the original-vs-shuffle denominator gate.
+
+This run does not repeat perturbation rank. It asks:
+
+> If the GUE-Poisson boundary is simulated directly by controlled mixtures,
+> does it behave like a clean two-class split, or like an operational third
+> layer where classification is ambiguous and denominator support collapses?
+
+## Experiment
+Tool created: `tools/exp_boundary_mixture_gate.py`
+
+Atomic perimeter:
+- domains: synthetic unfolded GUE spacings, iid Poisson spacings, and mixtures;
+- mixture parameter: `beta = 0.0..1.0`, where beta is the Poisson replacement fraction;
+- main run: 1,536 spacings, 16 replicates, GUE matrix size 180, 11 beta layers, 24 full-shuffle baselines;
+- seed check: 1,024 spacings, 12 replicates, GUE matrix size 160, same 11 beta layers, 20 baselines;
+- denominator gate: observable is stable when `abs(original - shuffle_mean) / shuffle_std >= 2`;
+- classification: standardized distance to pure GUE and pure Poisson centroids using all five canonical observables. A layer is marked ambiguous when at least half the replicates have nearest-centroid margin `< 0.15`.
+
+The endpoint-gated classifier is intentionally reported. In this perimeter it is empty because the Poisson endpoint has almost no stable original-vs-shuffle denominators. That is not discarded; it is the core META result.
+
+## Results
+
+### Main Run
+
+Endpoint separation using all canonical observables: `3.973` standardized units.  
+Endpoint-stable observables at frequency >= 0.75 across both endpoints: `[]`.
+
+| beta | stable obs / 5 | coord mean | margin mean | ambiguous fraction | Poisson-label fraction |
+|---:|---:|---:|---:|---:|---:|
+| 0.0 | 3.188 | -0.735 | 0.735 | 0.000 | 0.000 |
+| 0.1 | 3.312 | -0.470 | 0.470 | 0.000 | 0.000 |
+| 0.2 | 3.312 | -0.232 | 0.232 | 0.125 | 0.000 |
+| 0.3 | 2.500 | -0.054 | 0.070 | 0.875 | 0.250 |
+| 0.4 | 1.625 | +0.075 | 0.083 | 0.812 | 0.875 |
+| 0.5 | 0.750 | +0.260 | 0.260 | 0.000 | 1.000 |
+| 0.6 | 0.188 | +0.374 | 0.374 | 0.000 | 1.000 |
+| 0.7 | 0.500 | +0.520 | 0.520 | 0.000 | 1.000 |
+| 0.8 | 0.250 | +0.570 | 0.570 | 0.000 | 1.000 |
+| 0.9 | 0.250 | +0.692 | 0.692 | 0.000 | 1.000 |
+| 1.0 | 0.125 | +0.721 | 0.721 | 0.000 | 1.000 |
+
+At beta 0.0-0.2, the sequence is classified as GUE-like and retains about
+three stable observables. At beta 0.5-1.0, it is classified as Poisson-like,
+but denominator support is mostly absent. The transition is not centered at
+beta 0.5 in this observable suite. The ambiguous layer is beta 0.3-0.4.
+
+Observable stability frequencies in the main run:
+- beta 0.0: `SR=1.00`, `L1=1.00`, `triple_var=1.00`; `SR2=0.06`, `L2=0.12`;
+- beta 0.3: `SR=1.00`, `L1=0.75`, `triple_var=0.50`;
+- beta 0.4: `SR=0.75`, `L1=0.44`, `triple_var=0.38`;
+- beta 1.0: all canonical observables are weak except one `L2` replicate frequency of `0.12`.
+
+### Seed Check
+
+The lighter seed check repeated the same ambiguous layer:
+- ambiguous beta: `[0.3, 0.4]`;
+- beta 0.3: margin `0.082`, ambiguous fraction `0.917`, stable obs mean `1.250`;
+- beta 0.4: margin `0.125`, ambiguous fraction `0.750`, stable obs mean `0.833`;
+- beta 0.5 and above: Poisson-label fraction `1.000`, ambiguity `0.000`.
+
+## Findings
+
+1. **The clean two-class boundary fails under denominator gating.** Pure GUE and pure Poisson are separable in all-observable space, but there are no observables stable at both endpoints under the declared gate. The Poisson pole is a weak-denominator pole: classification can still place it, but retention-normalized structural claims cannot use it as if it had the same denominator support as GUE.
+
+2. **The operational boundary is a layer, not a line, in this synthetic perimeter.** Both the main run and the seed check isolate beta 0.3-0.4 as the ambiguous layer. In the main run the nearest-centroid margin falls to `0.070-0.083`, while ambiguous fraction rises to `0.812-0.875`. This is the measured form of the "third included" here: not a metaphysical third class, but a beta region where two-class assignment and denominator support are both unstable.
+
+3. **Denominator collapse precedes full Poisson classification.** Stable-observable count drops from about `3.3` at beta 0.1-0.2 to `2.5` at beta 0.3 and `1.625` at beta 0.4. By beta 0.5 the classifier is fully Poisson-labeled, but only `0.750/5` observables remain stable on average. The loss of denominator support is therefore part of the boundary phenomenon, not an after-the-fact nuisance.
+
+4. **The previous META constraints are extended, not replaced.** The 19:41 and 19:55 constraints still hold. This run adds that a boundary claim also needs a layer map: endpoint separability alone can hide the fact that one endpoint has no original-vs-shuffle denominator and that the transition region carries the actual instability.
+
+## Verdict
+**CONSTRAINT on META + BOUNDARY**: GUE/Poisson boundary claims must report:
+
+> observables_registry version + canonical observable list + original-vs-shuffle z gate per observable + endpoint-stable observable set + beta/window layer where classification margin is ambiguous.
+
+Scoped statement from this run:
+
+> In the synthetic mixture perimeter tested here, the GUE-Poisson boundary is an operational layer at beta 0.3-0.4: classification is ambiguous there, and denominator support collapses across the transition. The Poisson endpoint remains classifiable but denominator-weak, so it cannot serve as a symmetric structural pole for gated retention claims.
+
+## Consecutio
+What opens now: apply the same layer map to real domains rather than only synthetic mixtures. For primes, the next discriminating question is not "GUE or Poisson?" but:
+
+> Which scale window has the same signature as the synthetic beta 0.3-0.4 layer: low classifier margin plus falling original-vs-shuffle denominators?
+
+If prime windows show such a layer, BOUNDARY becomes a measurable transition surface. If they do not, the synthetic result remains a calibration constraint on how not to over-read endpoint separability.
+
+## Auto-audit: 5 lenti
+- **L1 hard constraint vs bias**: no zero/always claim. "Endpoint gate is empty" means no observable reached frequency >= 0.75 across both endpoints under `abs(z) >= 2`; it does not mean the observables are identically zero.
+- **L2 quantity vs ratio**: classification margin is reported together with stable-observable count and z-gate frequencies. Ratios are not interpreted without denominator support.
+- **L3 no silent patching**: the claim is explicitly changed from "8 GUE, 5 Poisson boundary" to a synthetic mixture calibration. This does not assert the same layer for primes or all real domains.
+- **L4 edge cases**: beta 0.2 has ambiguous fraction `0.125`, so it is not included in the ambiguous layer. The declared layer requires at least half the replicates ambiguous.
+- **L5 re-discovery**: this is a finite-sample diagnostic of crossover and noisy denominator normalization in classical GUE/Poisson spacings. It is not tagged as a new RMT theorem.
+
+## Files
+- Script: `tools/exp_boundary_mixture_gate.py`
+- Main data: `tools/data/boundary_mixture_gate_20260507_0330.json`
+- Seed check: `tools/data/boundary_mixture_gate_20260507_0330_seedcheck.json`
+- Report: `tools/data/reports/agent_20260507_0330.md`
diff --git a/tools/exp_boundary_mixture_gate.py b/tools/exp_boundary_mixture_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..fcc62a3ba54911760e60cbf1ec299013dff19041
--- /dev/null
+++ b/tools/exp_boundary_mixture_gate.py
@@ -0,0 +1,283 @@
+#!/usr/bin/env python3
+"""
+exp_boundary_mixture_gate.py
+
+Reusable META/BOUNDARY audit for the GUE-Poisson boundary.
+
+Question:
+    Does the GUE/Poisson boundary remain a clean two-class split after the
+    original-vs-shuffle denominator gate, or is the mixed region an operational
+    third state where canonical observables lose stable denominators?
+
+The script uses only canonical observables from observables_registry.py.
+It builds synthetic mixtures by replacing a fraction beta of unfolded GUE
+spacings with Poisson spacings, then measures:
+
+- canonical observable vectors;
+- original-vs-shuffle z-score per observable;
+- endpoint separability in all observables and in gate-stable observables;
+- ambiguity of each beta layer relative to pure GUE and pure Poisson centroids.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
+
+
+OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())
+
+
+def gue_spacings(matrix_size: int, min_spacings: int, rng: np.random.Generator) -> np.ndarray:
+    """Generate unfolded GUE spacings by concatenating independent matrices."""
+    parts: list[np.ndarray] = []
+    edge = max(2, matrix_size // 10)
+    while sum(len(x) for x in parts) < min_spacings:
+        real = rng.standard_normal((matrix_size, matrix_size))
+        imag = rng.standard_normal((matrix_size, matrix_size))
+        h = real + 1j * imag
+        h = (h + h.conj().T) / (2.0 * np.sqrt(matrix_size))
+        eigs = np.sort(np.linalg.eigvalsh(h).real)
+        bulk = eigs[edge:-edge]
+        gaps = np.diff(bulk)
+        mean = float(np.mean(gaps))
+        if mean > 1e-15:
+            parts.append(gaps / mean)
+    return np.concatenate(parts)[:min_spacings].astype(float)
+
+
+def mixture_spacings(gue: np.ndarray, poisson: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    """Return a beta Poisson / (1-beta) GUE spacing sequence with mean spacing 1."""
+    if len(gue) != len(poisson):
+        raise ValueError("gue and poisson arrays must have the same length")
+    mask = rng.random(len(gue)) < beta
+    out = gue.copy()
+    out[mask] = poisson[mask]
+    mean = float(np.mean(out))
+    return out / mean if mean > 1e-15 else out
+
+
+def z_against_shuffle(
+    gaps: np.ndarray,
+    n_baseline: int,
+    rng: np.random.Generator,
+) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
+    """Return original observables, shuffle baseline std, and original-vs-shuffle z."""
+    original = compute_canonical(gaps)
+    baseline_vals = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline_vals[name].append(obs[name])
+
+    std = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline_vals[name], dtype=float)
+        mean = float(np.mean(vals))
+        sd = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, std, z
+
+
+def vector(row: dict, names: list[str]) -> np.ndarray:
+    return np.array([row["observables"][name] for name in names], dtype=float)
+
+
+def classify_layers(rows: list[dict], obs_names: list[str]) -> dict:
+    """Classify each beta layer by standardized distance to endpoint centroids."""
+    if not obs_names:
+        return {
+            "observables": [],
+            "endpoint_distance": 0.0,
+            "layers": {},
+            "ambiguous_beta": [],
+        }
+
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    gue_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    poi_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    all_endpoint = np.vstack([gue_vectors, poi_vectors])
+    scale = np.std(all_endpoint, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    gue_centroid = np.mean(gue_vectors, axis=0)
+    poi_centroid = np.mean(poi_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((poi_centroid - gue_centroid) / scale))
+
+    layers = {}
+    ambiguous_beta = []
+    for beta, beta_rows in sorted(by_beta.items()):
+        coords = []
+        margins = []
+        labels = []
+        for row in beta_rows:
+            x = vector(row, obs_names)
+            d_gue = float(np.linalg.norm((x - gue_centroid) / scale))
+            d_poi = float(np.linalg.norm((x - poi_centroid) / scale))
+            denom = d_gue + d_poi
+            coord = float((d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("gue" if d_gue < d_poi else "poisson")
+        ambiguous_fraction = float(np.mean(np.array(margins) < 0.15))
+        if ambiguous_fraction >= 0.5:
+            ambiguous_beta.append(beta)
+        layers[f"{beta:.3f}"] = {
+            "coordinate_mean": float(np.mean(coords)),
+            "coordinate_std": float(np.std(coords, ddof=1)) if len(coords) > 1 else 0.0,
+            "margin_mean": float(np.mean(margins)),
+            "ambiguous_fraction": ambiguous_fraction,
+            "poisson_label_fraction": float(np.mean(np.array(labels) == "poisson")),
+        }
+
+    return {
+        "observables": obs_names,
+        "endpoint_distance": endpoint_distance,
+        "layers": layers,
+        "ambiguous_beta": ambiguous_beta,
+    }
+
+
+def summarize_gate(rows: list[dict], z_min: float) -> dict:
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    layers = {}
+    for beta, beta_rows in sorted(by_beta.items()):
+        stable_counts = []
+        stable_freq = {name: [] for name in OBS_NAMES}
+        for row in beta_rows:
+            stable = [name for name in OBS_NAMES if abs(row["z"][name]) >= z_min]
+            stable_counts.append(len(stable))
+            for name in OBS_NAMES:
+                stable_freq[name].append(1.0 if name in stable else 0.0)
+        layers[f"{beta:.3f}"] = {
+            "stable_count_mean": float(np.mean(stable_counts)),
+            "stable_count_std": float(np.std(stable_counts, ddof=1)) if len(stable_counts) > 1 else 0.0,
+            "stable_frequency": {name: float(np.mean(vals)) for name, vals in stable_freq.items()},
+        }
+
+    endpoint_stable = []
+    for name in OBS_NAMES:
+        endpoint_rows = by_beta[0.0] + by_beta[1.0]
+        freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in endpoint_rows])
+        if freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def run(args: argparse.Namespace) -> dict:
+    rng = np.random.default_rng(args.seed)
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    rows = []
+
+    for rep in range(args.n_replicates):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        gue = gue_spacings(args.gue_matrix_size, args.n_gaps, rep_rng)
+        poisson = rep_rng.exponential(1.0, size=args.n_gaps)
+        poisson = poisson / float(np.mean(poisson))
+        for beta in betas:
+            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+            gaps = mixture_spacings(gue, poisson, beta, layer_rng)
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                n_baseline=args.n_baseline,
+                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "replicate": rep,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [name for name in OBS_NAMES if abs(z[name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
+
+    output = {
+        "experiment": "boundary_mixture_gate",
+        "question": "Is the GUE-Poisson mixed layer cleanly classifiable after denominator gating?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_endpoint_gated": gated_classification,
+        "rows": rows,
+    }
+
+    out_path = Path(args.out)
+    out_path.parent.mkdir(parents=True, exist_ok=True)
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
+    print(f"observables_used={OBS_NAMES}")
+    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
+    print(
+        "endpoint_distance_all="
+        f"{all_classification['endpoint_distance']:.3f} "
+        "endpoint_distance_gated="
+        f"{gated_classification['endpoint_distance']:.3f}"
+    )
+    if gate["endpoint_stable_observables"]:
+        print("beta stable_count margin_gated ambiguous_gated poisson_fraction_gated")
+    else:
+        print("endpoint gate is empty; printing all-observable classification")
+        print("beta stable_count margin_all ambiguous_all poisson_fraction_all")
+    for beta in betas:
+        key = f"{beta:.3f}"
+        stable_count = gate["layers"][key]["stable_count_mean"]
+        source = gated_classification if gate["endpoint_stable_observables"] else all_classification
+        layer = source["layers"].get(key, {})
+        print(
+            f"{beta:>4.2f} {stable_count:>12.3f} "
+            f"{layer.get('margin_mean', 0.0):>12.3f} "
+            f"{layer.get('ambiguous_fraction', 0.0):>15.3f} "
+            f"{layer.get('poisson_label_fraction', 0.0):>21.3f}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=1536)
+    parser.add_argument("--n-replicates", type=int, default=16)
+    parser.add_argument("--gue-matrix-size", type=int, default=180)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=24)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=20260507)
+    parser.add_argument("--out", default="tools/data/boundary_mixture_gate.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

diff --git a/tools/data/reports/agent_20260507_0330.md b/tools/data/reports/agent_20260507_0330.md
new file mode 100644
index 0000000000000000000000000000000000000000..1bb66d4f31ae12b64e2652ac30c3e475aad25db7
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0330.md
@@ -0,0 +1,109 @@
+# Agent Report — The GUE-Poisson Boundary Is a Denominator Collapse Layer
+**Date**: 2026-05-07 03:30  
+**Piano**: 68  
+**Tension explored**: META + BOUNDARY  
+observables_registry: 1.0.0-2026-05-06  
+observables_used: [SR, SR2, L1, L2, triple_var]
+
+## Claim Under Test
+The last three runs constrained perturbation rank and observable collinearity:
+rank/PC2 claims are not interpretable without the canonical observable registry
+and the original-vs-shuffle denominator gate.
+
+This run does not repeat perturbation rank. It asks:
+
+> If the GUE-Poisson boundary is simulated directly by controlled mixtures,
+> does it behave like a clean two-class split, or like an operational third
+> layer where classification is ambiguous and denominator support collapses?
+
+## Experiment
+Tool created: `tools/exp_boundary_mixture_gate.py`
+
+Atomic perimeter:
+- domains: synthetic unfolded GUE spacings, iid Poisson spacings, and mixtures;
+- mixture parameter: `beta = 0.0..1.0`, where beta is the Poisson replacement fraction;
+- main run: 1,536 spacings, 16 replicates, GUE matrix size 180, 11 beta layers, 24 full-shuffle baselines;
+- seed check: 1,024 spacings, 12 replicates, GUE matrix size 160, same 11 beta layers, 20 baselines;
+- denominator gate: observable is stable when `abs(original - shuffle_mean) / shuffle_std >= 2`;
+- classification: standardized distance to pure GUE and pure Poisson centroids using all five canonical observables. A layer is marked ambiguous when at least half the replicates have nearest-centroid margin `< 0.15`.
+
+The endpoint-gated classifier is intentionally reported. In this perimeter it is empty because the Poisson endpoint has almost no stable original-vs-shuffle denominators. That is not discarded; it is the core META result.
+
+## Results
+
+### Main Run
+
+Endpoint separation using all canonical observables: `3.973` standardized units.  
+Endpoint-stable observables at frequency >= 0.75 across both endpoints: `[]`.
+
+| beta | stable obs / 5 | coord mean | margin mean | ambiguous fraction | Poisson-label fraction |
+|---:|---:|---:|---:|---:|---:|
+| 0.0 | 3.188 | -0.735 | 0.735 | 0.000 | 0.000 |
+| 0.1 | 3.312 | -0.470 | 0.470 | 0.000 | 0.000 |
+| 0.2 | 3.312 | -0.232 | 0.232 | 0.125 | 0.000 |
+| 0.3 | 2.500 | -0.054 | 0.070 | 0.875 | 0.250 |
+| 0.4 | 1.625 | +0.075 | 0.083 | 0.812 | 0.875 |
+| 0.5 | 0.750 | +0.260 | 0.260 | 0.000 | 1.000 |
+| 0.6 | 0.188 | +0.374 | 0.374 | 0.000 | 1.000 |
+| 0.7 | 0.500 | +0.520 | 0.520 | 0.000 | 1.000 |
+| 0.8 | 0.250 | +0.570 | 0.570 | 0.000 | 1.000 |
+| 0.9 | 0.250 | +0.692 | 0.692 | 0.000 | 1.000 |
+| 1.0 | 0.125 | +0.721 | 0.721 | 0.000 | 1.000 |
+
+At beta 0.0-0.2, the sequence is classified as GUE-like and retains about
+three stable observables. At beta 0.5-1.0, it is classified as Poisson-like,
+but denominator support is mostly absent. The transition is not centered at
+beta 0.5 in this observable suite. The ambiguous layer is beta 0.3-0.4.
+
+Observable stability frequencies in the main run:
+- beta 0.0: `SR=1.00`, `L1=1.00`, `triple_var=1.00`; `SR2=0.06`, `L2=0.12`;
+- beta 0.3: `SR=1.00`, `L1=0.75`, `triple_var=0.50`;
+- beta 0.4: `SR=0.75`, `L1=0.44`, `triple_var=0.38`;
+- beta 1.0: all canonical observables are weak except one `L2` replicate frequency of `0.12`.
+
+### Seed Check
+
+The lighter seed check repeated the same ambiguous layer:
+- ambiguous beta: `[0.3, 0.4]`;
+- beta 0.3: margin `0.082`, ambiguous fraction `0.917`, stable obs mean `1.250`;
+- beta 0.4: margin `0.125`, ambiguous fraction `0.750`, stable obs mean `0.833`;
+- beta 0.5 and above: Poisson-label fraction `1.000`, ambiguity `0.000`.
+
+## Findings
+
+1. **The clean two-class boundary fails under denominator gating.** Pure GUE and pure Poisson are separable in all-observable space, but there are no observables stable at both endpoints under the declared gate. The Poisson pole is a weak-denominator pole: classification can still place it, but retention-normalized structural claims cannot use it as if it had the same denominator support as GUE.
+
+2. **The operational boundary is a layer, not a line, in this synthetic perimeter.** Both the main run and the seed check isolate beta 0.3-0.4 as the ambiguous layer. In the main run the nearest-centroid margin falls to `0.070-0.083`, while ambiguous fraction rises to `0.812-0.875`. This is the measured form of the "third included" here: not a metaphysical third class, but a beta region where two-class assignment and denominator support are both unstable.
+
+3. **Denominator collapse precedes full Poisson classification.** Stable-observable count drops from about `3.3` at beta 0.1-0.2 to `2.5` at beta 0.3 and `1.625` at beta 0.4. By beta 0.5 the classifier is fully Poisson-labeled, but only `0.750/5` observables remain stable on average. The loss of denominator support is therefore part of the boundary phenomenon, not an after-the-fact nuisance.
+
+4. **The previous META constraints are extended, not replaced.** The 19:41 and 19:55 constraints still hold. This run adds that a boundary claim also needs a layer map: endpoint separability alone can hide the fact that one endpoint has no original-vs-shuffle denominator and that the transition region carries the actual instability.
+
+## Verdict
+**CONSTRAINT on META + BOUNDARY**: GUE/Poisson boundary claims must report:
+
+> observables_registry version + canonical observable list + original-vs-shuffle z gate per observable + endpoint-stable observable set + beta/window layer where classification margin is ambiguous.
+
+Scoped statement from this run:
+
+> In the synthetic mixture perimeter tested here, the GUE-Poisson boundary is an operational layer at beta 0.3-0.4: classification is ambiguous there, and denominator support collapses across the transition. The Poisson endpoint remains classifiable but denominator-weak, so it cannot serve as a symmetric structural pole for gated retention claims.
+
+## Consecutio
+What opens now: apply the same layer map to real domains rather than only synthetic mixtures. For primes, the next discriminating question is not "GUE or Poisson?" but:
+
+> Which scale window has the same signature as the synthetic beta 0.3-0.4 layer: low classifier margin plus falling original-vs-shuffle denominators?
+
+If prime windows show such a layer, BOUNDARY becomes a measurable transition surface. If they do not, the synthetic result remains a calibration constraint on how not to over-read endpoint separability.
+
+## Auto-audit: 5 lenti
+- **L1 hard constraint vs bias**: no zero/always claim. "Endpoint gate is empty" means no observable reached frequency >= 0.75 across both endpoints under `abs(z) >= 2`; it does not mean the observables are identically zero.
+- **L2 quantity vs ratio**: classification margin is reported together with stable-observable count and z-gate frequencies. Ratios are not interpreted without denominator support.
+- **L3 no silent patching**: the claim is explicitly changed from "8 GUE, 5 Poisson boundary" to a synthetic mixture calibration. This does not assert the same layer for primes or all real domains.
+- **L4 edge cases**: beta 0.2 has ambiguous fraction `0.125`, so it is not included in the ambiguous layer. The declared layer requires at least half the replicates ambiguous.
+- **L5 re-discovery**: this is a finite-sample diagnostic of crossover and noisy denominator normalization in classical GUE/Poisson spacings. It is not tagged as a new RMT theorem.
+
+## Files
+- Script: `tools/exp_boundary_mixture_gate.py`
+- Main data: `tools/data/boundary_mixture_gate_20260507_0330.json`
+- Seed check: `tools/data/boundary_mixture_gate_20260507_0330_seedcheck.json`
+- Report: `tools/data/reports/agent_20260507_0330.md`
diff --git a/tools/exp_boundary_mixture_gate.py b/tools/exp_boundary_mixture_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..fcc62a3ba54911760e60cbf1ec299013dff19041
--- /dev/null
+++ b/tools/exp_boundary_mixture_gate.py
@@ -0,0 +1,283 @@
+#!/usr/bin/env python3
+"""
+exp_boundary_mixture_gate.py
+
+Reusable META/BOUNDARY audit for the GUE-Poisson boundary.
+
+Question:
+    Does the GUE/Poisson boundary remain a clean two-class split after the
+    original-vs-shuffle denominator gate, or is the mixed region an operational
+    third state where canonical observables lose stable denominators?
+
+The script uses only canonical observables from observables_registry.py.
+It builds synthetic mixtures by replacing a fraction beta of unfolded GUE
+spacings with Poisson spacings, then measures:
+
+- canonical observable vectors;
+- original-vs-shuffle z-score per observable;
+- endpoint separability in all observables and in gate-stable observables;
+- ambiguity of each beta layer relative to pure GUE and pure Poisson centroids.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
+
+
+OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())
+
+
+def gue_spacings(matrix_size: int, min_spacings: int, rng: np.random.Generator) -> np.ndarray:
+    """Generate unfolded GUE spacings by concatenating independent matrices."""
+    parts: list[np.ndarray] = []
+    edge = max(2, matrix_size // 10)
+    while sum(len(x) for x in parts) < min_spacings:
+        real = rng.standard_normal((matrix_size, matrix_size))
+        imag = rng.standard_normal((matrix_size, matrix_size))
+        h = real + 1j * imag
+        h = (h + h.conj().T) / (2.0 * np.sqrt(matrix_size))
+        eigs = np.sort(np.linalg.eigvalsh(h).real)
+        bulk = eigs[edge:-edge]
+        gaps = np.diff(bulk)
+        mean = float(np.mean(gaps))
+        if mean > 1e-15:
+            parts.append(gaps / mean)
+    return np.concatenate(parts)[:min_spacings].astype(float)
+
+
+def mixture_spacings(gue: np.ndarray, poisson: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    """Return a beta Poisson / (1-beta) GUE spacing sequence with mean spacing 1."""
+    if len(gue) != len(poisson):
+        raise ValueError("gue and poisson arrays must have the same length")
+    mask = rng.random(len(gue)) < beta
+    out = gue.copy()
+    out[mask] = poisson[mask]
+    mean = float(np.mean(out))
+    return out / mean if mean > 1e-15 else out
+
+
+def z_against_shuffle(
+    gaps: np.ndarray,
+    n_baseline: int,
+    rng: np.random.Generator,
+) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
+    """Return original observables, shuffle baseline std, and original-vs-shuffle z."""
+    original = compute_canonical(gaps)
+    baseline_vals = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline_vals[name].append(obs[name])
+
+    std = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline_vals[name], dtype=float)
+        mean = float(np.mean(vals))
+        sd = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, std, z
+
+
+def vector(row: dict, names: list[str]) -> np.ndarray:
+    return np.array([row["observables"][name] for name in names], dtype=float)
+
+
+def classify_layers(rows: list[dict], obs_names: list[str]) -> dict:
+    """Classify each beta layer by standardized distance to endpoint centroids."""
+    if not obs_names:
+        return {
+            "observables": [],
+            "endpoint_distance": 0.0,
+            "layers": {},
+            "ambiguous_beta": [],
+        }
+
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    gue_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    poi_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    all_endpoint = np.vstack([gue_vectors, poi_vectors])
+    scale = np.std(all_endpoint, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    gue_centroid = np.mean(gue_vectors, axis=0)
+    poi_centroid = np.mean(poi_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((poi_centroid - gue_centroid) / scale))
+
+    layers = {}
+    ambiguous_beta = []
+    for beta, beta_rows in sorted(by_beta.items()):
+        coords = []
+        margins = []
+        labels = []
+        for row in beta_rows:
+            x = vector(row, obs_names)
+            d_gue = float(np.linalg.norm((x - gue_centroid) / scale))
+            d_poi = float(np.linalg.norm((x - poi_centroid) / scale))
+            denom = d_gue + d_poi
+            coord = float((d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("gue" if d_gue < d_poi else "poisson")
+        ambiguous_fraction = float(np.mean(np.array(margins) < 0.15))
+        if ambiguous_fraction >= 0.5:
+            ambiguous_beta.append(beta)
+        layers[f"{beta:.3f}"] = {
+            "coordinate_mean": float(np.mean(coords)),
+            "coordinate_std": float(np.std(coords, ddof=1)) if len(coords) > 1 else 0.0,
+            "margin_mean": float(np.mean(margins)),
+            "ambiguous_fraction": ambiguous_fraction,
+            "poisson_label_fraction": float(np.mean(np.array(labels) == "poisson")),
+        }
+
+    return {
+        "observables": obs_names,
+        "endpoint_distance": endpoint_distance,
+        "layers": layers,
+        "ambiguous_beta": ambiguous_beta,
+    }
+
+
+def summarize_gate(rows: list[dict], z_min: float) -> dict:
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    layers = {}
+    for beta, beta_rows in sorted(by_beta.items()):
+        stable_counts = []
+        stable_freq = {name: [] for name in OBS_NAMES}
+        for row in beta_rows:
+            stable = [name for name in OBS_NAMES if abs(row["z"][name]) >= z_min]
+            stable_counts.append(len(stable))
+            for name in OBS_NAMES:
+                stable_freq[name].append(1.0 if name in stable else 0.0)
+        layers[f"{beta:.3f}"] = {
+            "stable_count_mean": float(np.mean(stable_counts)),
+            "stable_count_std": float(np.std(stable_counts, ddof=1)) if len(stable_counts) > 1 else 0.0,
+            "stable_frequency": {name: float(np.mean(vals)) for name, vals in stable_freq.items()},
+        }
+
+    endpoint_stable = []
+    for name in OBS_NAMES:
+        endpoint_rows = by_beta[0.0] + by_beta[1.0]
+        freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in endpoint_rows])
+        if freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def run(args: argparse.Namespace) -> dict:
+    rng = np.random.default_rng(args.seed)
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    rows = []
+
+    for rep in range(args.n_replicates):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        gue = gue_spacings(args.gue_matrix_size, args.n_gaps, rep_rng)
+        poisson = rep_rng.exponential(1.0, size=args.n_gaps)
+        poisson = poisson / float(np.mean(poisson))
+        for beta in betas:
+            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+            gaps = mixture_spacings(gue, poisson, beta, layer_rng)
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                n_baseline=args.n_baseline,
+                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "replicate": rep,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [name for name in OBS_NAMES if abs(z[name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
+
+    output = {
+        "experiment": "boundary_mixture_gate",
+        "question": "Is the GUE-Poisson mixed layer cleanly classifiable after denominator gating?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_endpoint_gated": gated_classification,
+        "rows": rows,
+    }
+
+    out_path = Path(args.out)
+    out_path.parent.mkdir(parents=True, exist_ok=True)
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
+    print(f"observables_used={OBS_NAMES}")
+    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
+    print(
+        "endpoint_distance_all="
+        f"{all_classification['endpoint_distance']:.3f} "
+        "endpoint_distance_gated="
+        f"{gated_classification['endpoint_distance']:.3f}"
+    )
+    if gate["endpoint_stable_observables"]:
+        print("beta stable_count margin_gated ambiguous_gated poisson_fraction_gated")
+    else:
+        print("endpoint gate is empty; printing all-observable classification")
+        print("beta stable_count margin_all ambiguous_all poisson_fraction_all")
+    for beta in betas:
+        key = f"{beta:.3f}"
+        stable_count = gate["layers"][key]["stable_count_mean"]
+        source = gated_classification if gate["endpoint_stable_observables"] else all_classification
+        layer = source["layers"].get(key, {})
+        print(
+            f"{beta:>4.2f} {stable_count:>12.3f} "
+            f"{layer.get('margin_mean', 0.0):>12.3f} "
+            f"{layer.get('ambiguous_fraction', 0.0):>15.3f} "
+            f"{layer.get('poisson_label_fraction', 0.0):>21.3f}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=1536)
+    parser.add_argument("--n-replicates", type=int, default=16)
+    parser.add_argument("--gue-matrix-size", type=int, default=180)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=24)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=20260507)
+    parser.add_argument("--out", default="tools/data/boundary_mixture_gate.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

codex
Il report è scritto. Aggiorno ora il seme con un vincolo stretto, non una “scoperta” ampia: i claim di boundary devono includere la mappa di layer e il set di osservabili stabili agli endpoint.
exec
/bin/bash -lc "sed -n '1,90p' tools/data/seme.json" in /opt/MM_D-ND
 succeeded in 0ms:
{
  "timestamp": "2026-05-06T20:02:38.262185",
  "piano": 68,
  "tensioni": [
    {
      "id": "OBSERVABLE_REGISTRY",
      "tipo": "vincolo",
      "claim": "Ogni script che usa observables canonici (SR, SR2, L1, L2, triple_var) deve importare la definizione da tools/observables_registry.py. Varianti devono usare nomi distinti (SR_local_rigidity, triple_var_normalized) — niente shadowing del nome canonico. Ogni report deve dichiarare 'observables_registry: VERSION' nel header.",
      "intensita": 1.0,
      "porta": "infrastructure",
      "manuale": true,
      "condensato_ref": "A14,A8",
      "origine": "cristallizzato 06/05 dalla consecutio del cycle 20260506_0625 (autopoietico self-finding)",
      "added_at": "2026-05-06T07:03:58.213606+00:00"
    },
    {
      "id": "PERTURBATION_DENOMINATOR_GATE",
      "tipo": "vincolo",
      "claim": "La dimensionalita di perturbazione va riportata solo insieme a PC2, versione observables_registry e gate original-vs-shuffle per osservabile. Nel perimetro 20260506_1941, Poisson e shuffle-primi producono rank_all ~1.8-2.0 con denominatori deboli; dopo gate abs(z)>=2 il rank stabile torna vicino a 1. Rank PCA non gated non e evidenza strutturale.",
      "intensita": 0.95,
      "porta": "META_BOUNDARY",
      "manuale": true,
      "condensato_ref": "A4,A8,A14,C2",
      "origine": "cycle agent_20260506_1941: perturbation rank size curve canonical observables",
      "added_at": "2026-05-06T19:41:00+00:00"
    },
    {
      "tipo": "confine_inesplorato",
      "id": "TRASCENDENZA_LIMITE",
      "claim": "La trascendenza e il limite attuale del modello. I punti fissi relazionali (non solo phi ma la rete di punti fissi tra osservabili) possono rivelare il vero grafo della realta e pattern nelle matrici. Il confine non e nella matematica - e nel passaggio tra piani.",
      "intensita": 0.9,
      "nota": "Input operatore 2026-04-10. Tocca: confine del modello, struttura relazionale dei punti fissi. Consecutio: quali punti fissi relazionali emergono dalle 21 tensioni attuali? Il grafo e gia nei dati?",
      "manuale": true,
      "porta": "sessione_interattiva",
      "condensato_ref": "A3,A10",
      "condensato_motivo": "Estende A3 (punto fisso singolo) a rete relazionale. Tocca A10 (dipolo) come caso speciale."
    },
    {
      "tipo": "scoperta",
      "id": "DUALITA_DIPOLARE_VS_ILLUSORIA",
      "claim": "Due tipi di dualita: (1) dipolare - generativa, il modello (det=-1), (2) illusoria - dispersiva, entropia (det=+1). Le regole incoerenti producono la seconda. La dualita illusoria e entropia come dispersione, non come informazione.",
      "intensita": 0.9,
      "nota": "Input operatore 2026-04-10. Tocca: entropia come dispersione illusoria vs generazione dipolare. Consecutio: nel Lab i domini Poisson (entropia massima) mostrano dualita illusoria? I domini GUE (strutturati) mostrano dualita dipolare? Il drift verso Poisson (POISSON_CONVERGENCE) e perdita di dualita dipolare?",
      "manuale": true,
      "porta": "sessione_interattiva",
      "condensato_ref": "A2,A10,F5",
      "condensato_motivo": "Discrimina due forme di det. A2 (confine) e la soglia. A10 (dipolo) e il tipo 1. F5 (frame) misura la struttura D-ND che e tipo 1."
    },
    {
      "tipo": "scoperta_numerica",
      "id": "METRIC_TENSOR",
      "claim": "Il tensore metrico dei primi è g=(p/2)². Nel tempo ln(p), è de Sitter 1+1D. z=-8.8 curvatura vs z=+22.5 rapporti ΔΓ.",
      "intensità": 0.9,
      "nota": "Sessione interattiva 4 aprile. Verificato su 78K primi.",
      "manuale": true,
      "porta": "sessione_interattiva",
      "condensato_ref": null,
      "condensato_motivo": "Risultato numerico verificato, non-tautologico"
    },
    {
      "tipo": "scoperta",
      "id": "TENSIONE_ENTITA",
      "claim": "La tensione non e un problema pratico - e un Entita. La tensione superflua crea latenza (tempo). Senza tensione superflua tutto e regolato da assiomi. Implicazione: le tensioni nel seme sono entita, non problemi da risolvere. Quelle superflue (det=+1) producono tempo/latenza.",
      "intensita": 0.85,
      "nota": "Input operatore 2026-04-10. Tocca: rapporto tensione/assioma. Operativamente: discriminare tensioni-entita (generative) da tensioni-superflue (dispersive) nel seme. Le 21 tensioni attuali - quante sono entita e quante latenza?",
      "manuale": true,
      "porta": "sessione_interattiva",
      "condensato_ref": "A5,A6",
      "condensato_motivo": "Il ciclo (A5) lavora con tensioni - ma se la tensione e entita, il ciclo non le risolve, le osserva. Lo zero mobile (A6) e la tensione senza latenza."
    },
    {
      "tipo": "confine_inesplorato",
      "id": "G_POTENZIALE_NULLA",
      "claim": "G e il potenziale di tutto come nulla - permette il prima e il dopo. Ci muoviamo come trascendenza dimensionale gravitazionale. G nel tetraedro non e una teoria tra le altre - e il potenziale che le rende possibili.",
      "intensita": 0.85,
      "nota": "Input operatore 2026-04-10. Tocca: ruolo di G nel tetraedro (T,Q,G,E). La fonte video_lp0RgZ6kQF8 dice: tensore metrico dentro la forma simplettica. G non e accanto a T,Q,E - e sotto. Consecutio: nei dati Lab, i ponti TxG e ExG hanno struttura diversa dai ponti TxQ?",
      "manuale": true,
      "porta": "sessione_interattiva",
      "condensato_ref": "A7,A10",
      "condensato_motivo": "A7 (singolarita come operatore) e G come potenziale. A10 (dipolo) opera sul piano che G rende possibile."
    },
    {
      "tipo": "confine_inesplorato",
      "id": "BOUNDARY",
      "claim": "8 domini GUE, 5 Poisson — il confine è il terzo incluso operativo",
      "intensità": 0.8,
      "nota": "Il segnale non-triviale è DOVE la scissione cambia natura, non che converge a φ",
      "condensato_ref": "A9",
      "condensato_motivo": "Overlap termini con A9 (5 termini)",
      "porta": "condensato"

diff --git a/tools/data/reports/agent_20260507_0330.md b/tools/data/reports/agent_20260507_0330.md
new file mode 100644
index 0000000000000000000000000000000000000000..1bb66d4f31ae12b64e2652ac30c3e475aad25db7
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0330.md
@@ -0,0 +1,109 @@
+# Agent Report — The GUE-Poisson Boundary Is a Denominator Collapse Layer
+**Date**: 2026-05-07 03:30  
+**Piano**: 68  
+**Tension explored**: META + BOUNDARY  
+observables_registry: 1.0.0-2026-05-06  
+observables_used: [SR, SR2, L1, L2, triple_var]
+
+## Claim Under Test
+The last three runs constrained perturbation rank and observable collinearity:
+rank/PC2 claims are not interpretable without the canonical observable registry
+and the original-vs-shuffle denominator gate.
+
+This run does not repeat perturbation rank. It asks:
+
+> If the GUE-Poisson boundary is simulated directly by controlled mixtures,
+> does it behave like a clean two-class split, or like an operational third
+> layer where classification is ambiguous and denominator support collapses?
+
+## Experiment
+Tool created: `tools/exp_boundary_mixture_gate.py`
+
+Atomic perimeter:
+- domains: synthetic unfolded GUE spacings, iid Poisson spacings, and mixtures;
+- mixture parameter: `beta = 0.0..1.0`, where beta is the Poisson replacement fraction;
+- main run: 1,536 spacings, 16 replicates, GUE matrix size 180, 11 beta layers, 24 full-shuffle baselines;
+- seed check: 1,024 spacings, 12 replicates, GUE matrix size 160, same 11 beta layers, 20 baselines;
+- denominator gate: observable is stable when `abs(original - shuffle_mean) / shuffle_std >= 2`;
+- classification: standardized distance to pure GUE and pure Poisson centroids using all five canonical observables. A layer is marked ambiguous when at least half the replicates have nearest-centroid margin `< 0.15`.
+
+The endpoint-gated classifier is intentionally reported. In this perimeter it is empty because the Poisson endpoint has almost no stable original-vs-shuffle denominators. That is not discarded; it is the core META result.
+
+## Results
+
+### Main Run
+
+Endpoint separation using all canonical observables: `3.973` standardized units.  
+Endpoint-stable observables at frequency >= 0.75 across both endpoints: `[]`.
+
+| beta | stable obs / 5 | coord mean | margin mean | ambiguous fraction | Poisson-label fraction |
+|---:|---:|---:|---:|---:|---:|
+| 0.0 | 3.188 | -0.735 | 0.735 | 0.000 | 0.000 |
+| 0.1 | 3.312 | -0.470 | 0.470 | 0.000 | 0.000 |
+| 0.2 | 3.312 | -0.232 | 0.232 | 0.125 | 0.000 |
+| 0.3 | 2.500 | -0.054 | 0.070 | 0.875 | 0.250 |
+| 0.4 | 1.625 | +0.075 | 0.083 | 0.812 | 0.875 |
+| 0.5 | 0.750 | +0.260 | 0.260 | 0.000 | 1.000 |
+| 0.6 | 0.188 | +0.374 | 0.374 | 0.000 | 1.000 |
+| 0.7 | 0.500 | +0.520 | 0.520 | 0.000 | 1.000 |
+| 0.8 | 0.250 | +0.570 | 0.570 | 0.000 | 1.000 |
+| 0.9 | 0.250 | +0.692 | 0.692 | 0.000 | 1.000 |
+| 1.0 | 0.125 | +0.721 | 0.721 | 0.000 | 1.000 |
+
+At beta 0.0-0.2, the sequence is classified as GUE-like and retains about
+three stable observables. At beta 0.5-1.0, it is classified as Poisson-like,
+but denominator support is mostly absent. The transition is not centered at
+beta 0.5 in this observable suite. The ambiguous layer is beta 0.3-0.4.
+
+Observable stability frequencies in the main run:
+- beta 0.0: `SR=1.00`, `L1=1.00`, `triple_var=1.00`; `SR2=0.06`, `L2=0.12`;
+- beta 0.3: `SR=1.00`, `L1=0.75`, `triple_var=0.50`;
+- beta 0.4: `SR=0.75`, `L1=0.44`, `triple_var=0.38`;
+- beta 1.0: all canonical observables are weak except one `L2` replicate frequency of `0.12`.
+
+### Seed Check
+
+The lighter seed check repeated the same ambiguous layer:
+- ambiguous beta: `[0.3, 0.4]`;
+- beta 0.3: margin `0.082`, ambiguous fraction `0.917`, stable obs mean `1.250`;
+- beta 0.4: margin `0.125`, ambiguous fraction `0.750`, stable obs mean `0.833`;
+- beta 0.5 and above: Poisson-label fraction `1.000`, ambiguity `0.000`.
+
+## Findings
+
+1. **The clean two-class boundary fails under denominator gating.** Pure GUE and pure Poisson are separable in all-observable space, but there are no observables stable at both endpoints under the declared gate. The Poisson pole is a weak-denominator pole: classification can still place it, but retention-normalized structural claims cannot use it as if it had the same denominator support as GUE.
+
+2. **The operational boundary is a layer, not a line, in this synthetic perimeter.** Both the main run and the seed check isolate beta 0.3-0.4 as the ambiguous layer. In the main run the nearest-centroid margin falls to `0.070-0.083`, while ambiguous fraction rises to `0.812-0.875`. This is the measured form of the "third included" here: not a metaphysical third class, but a beta region where two-class assignment and denominator support are both unstable.
+
+3. **Denominator collapse precedes full Poisson classification.** Stable-observable count drops from about `3.3` at beta 0.1-0.2 to `2.5` at beta 0.3 and `1.625` at beta 0.4. By beta 0.5 the classifier is fully Poisson-labeled, but only `0.750/5` observables remain stable on average. The loss of denominator support is therefore part of the boundary phenomenon, not an after-the-fact nuisance.
+
+4. **The previous META constraints are extended, not replaced.** The 19:41 and 19:55 constraints still hold. This run adds that a boundary claim also needs a layer map: endpoint separability alone can hide the fact that one endpoint has no original-vs-shuffle denominator and that the transition region carries the actual instability.
+
+## Verdict
+**CONSTRAINT on META + BOUNDARY**: GUE/Poisson boundary claims must report:
+
+> observables_registry version + canonical observable list + original-vs-shuffle z gate per observable + endpoint-stable observable set + beta/window layer where classification margin is ambiguous.
+
+Scoped statement from this run:
+
+> In the synthetic mixture perimeter tested here, the GUE-Poisson boundary is an operational layer at beta 0.3-0.4: classification is ambiguous there, and denominator support collapses across the transition. The Poisson endpoint remains classifiable but denominator-weak, so it cannot serve as a symmetric structural pole for gated retention claims.
+
+## Consecutio
+What opens now: apply the same layer map to real domains rather than only synthetic mixtures. For primes, the next discriminating question is not "GUE or Poisson?" but:
+
+> Which scale window has the same signature as the synthetic beta 0.3-0.4 layer: low classifier margin plus falling original-vs-shuffle denominators?
+
+If prime windows show such a layer, BOUNDARY becomes a measurable transition surface. If they do not, the synthetic result remains a calibration constraint on how not to over-read endpoint separability.
+
+## Auto-audit: 5 lenti
+- **L1 hard constraint vs bias**: no zero/always claim. "Endpoint gate is empty" means no observable reached frequency >= 0.75 across both endpoints under `abs(z) >= 2`; it does not mean the observables are identically zero.
+- **L2 quantity vs ratio**: classification margin is reported together with stable-observable count and z-gate frequencies. Ratios are not interpreted without denominator support.
+- **L3 no silent patching**: the claim is explicitly changed from "8 GUE, 5 Poisson boundary" to a synthetic mixture calibration. This does not assert the same layer for primes or all real domains.
+- **L4 edge cases**: beta 0.2 has ambiguous fraction `0.125`, so it is not included in the ambiguous layer. The declared layer requires at least half the replicates ambiguous.
+- **L5 re-discovery**: this is a finite-sample diagnostic of crossover and noisy denominator normalization in classical GUE/Poisson spacings. It is not tagged as a new RMT theorem.
+
+## Files
+- Script: `tools/exp_boundary_mixture_gate.py`
+- Main data: `tools/data/boundary_mixture_gate_20260507_0330.json`
+- Seed check: `tools/data/boundary_mixture_gate_20260507_0330_seedcheck.json`
+- Report: `tools/data/reports/agent_20260507_0330.md`
diff --git a/tools/exp_boundary_mixture_gate.py b/tools/exp_boundary_mixture_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..fcc62a3ba54911760e60cbf1ec299013dff19041
--- /dev/null
+++ b/tools/exp_boundary_mixture_gate.py
@@ -0,0 +1,283 @@
+#!/usr/bin/env python3
+"""
+exp_boundary_mixture_gate.py
+
+Reusable META/BOUNDARY audit for the GUE-Poisson boundary.
+
+Question:
+    Does the GUE/Poisson boundary remain a clean two-class split after the
+    original-vs-shuffle denominator gate, or is the mixed region an operational
+    third state where canonical observables lose stable denominators?
+
+The script uses only canonical observables from observables_registry.py.
+It builds synthetic mixtures by replacing a fraction beta of unfolded GUE
+spacings with Poisson spacings, then measures:
+
+- canonical observable vectors;
+- original-vs-shuffle z-score per observable;
+- endpoint separability in all observables and in gate-stable observables;
+- ambiguity of each beta layer relative to pure GUE and pure Poisson centroids.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
+
+
+OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())
+
+
+def gue_spacings(matrix_size: int, min_spacings: int, rng: np.random.Generator) -> np.ndarray:
+    """Generate unfolded GUE spacings by concatenating independent matrices."""
+    parts: list[np.ndarray] = []
+    edge = max(2, matrix_size // 10)
+    while sum(len(x) for x in parts) < min_spacings:
+        real = rng.standard_normal((matrix_size, matrix_size))
+        imag = rng.standard_normal((matrix_size, matrix_size))
+        h = real + 1j * imag
+        h = (h + h.conj().T) / (2.0 * np.sqrt(matrix_size))
+        eigs = np.sort(np.linalg.eigvalsh(h).real)
+        bulk = eigs[edge:-edge]
+        gaps = np.diff(bulk)
+        mean = float(np.mean(gaps))
+        if mean > 1e-15:
+            parts.append(gaps / mean)
+    return np.concatenate(parts)[:min_spacings].astype(float)
+
+
+def mixture_spacings(gue: np.ndarray, poisson: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    """Return a beta Poisson / (1-beta) GUE spacing sequence with mean spacing 1."""
+    if len(gue) != len(poisson):
+        raise ValueError("gue and poisson arrays must have the same length")
+    mask = rng.random(len(gue)) < beta
+    out = gue.copy()
+    out[mask] = poisson[mask]
+    mean = float(np.mean(out))
+    return out / mean if mean > 1e-15 else out
+
+
+def z_against_shuffle(
+    gaps: np.ndarray,
+    n_baseline: int,
+    rng: np.random.Generator,
+) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
+    """Return original observables, shuffle baseline std, and original-vs-shuffle z."""
+    original = compute_canonical(gaps)
+    baseline_vals = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline_vals[name].append(obs[name])
+
+    std = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline_vals[name], dtype=float)
+        mean = float(np.mean(vals))
+        sd = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, std, z
+
+
+def vector(row: dict, names: list[str]) -> np.ndarray:
+    return np.array([row["observables"][name] for name in names], dtype=float)
+
+
+def classify_layers(rows: list[dict], obs_names: list[str]) -> dict:
+    """Classify each beta layer by standardized distance to endpoint centroids."""
+    if not obs_names:
+        return {
+            "observables": [],
+            "endpoint_distance": 0.0,
+            "layers": {},
+            "ambiguous_beta": [],
+        }
+
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    gue_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    poi_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    all_endpoint = np.vstack([gue_vectors, poi_vectors])
+    scale = np.std(all_endpoint, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    gue_centroid = np.mean(gue_vectors, axis=0)
+    poi_centroid = np.mean(poi_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((poi_centroid - gue_centroid) / scale))
+
+    layers = {}
+    ambiguous_beta = []
+    for beta, beta_rows in sorted(by_beta.items()):
+        coords = []
+        margins = []
+        labels = []
+        for row in beta_rows:
+            x = vector(row, obs_names)
+            d_gue = float(np.linalg.norm((x - gue_centroid) / scale))
+            d_poi = float(np.linalg.norm((x - poi_centroid) / scale))
+            denom = d_gue + d_poi
+            coord = float((d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("gue" if d_gue < d_poi else "poisson")
+        ambiguous_fraction = float(np.mean(np.array(margins) < 0.15))
+        if ambiguous_fraction >= 0.5:
+            ambiguous_beta.append(beta)
+        layers[f"{beta:.3f}"] = {
+            "coordinate_mean": float(np.mean(coords)),
+            "coordinate_std": float(np.std(coords, ddof=1)) if len(coords) > 1 else 0.0,
+            "margin_mean": float(np.mean(margins)),
+            "ambiguous_fraction": ambiguous_fraction,
+            "poisson_label_fraction": float(np.mean(np.array(labels) == "poisson")),
+        }
+
+    return {
+        "observables": obs_names,
+        "endpoint_distance": endpoint_distance,
+        "layers": layers,
+        "ambiguous_beta": ambiguous_beta,
+    }
+
+
+def summarize_gate(rows: list[dict], z_min: float) -> dict:
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    layers = {}
+    for beta, beta_rows in sorted(by_beta.items()):
+        stable_counts = []
+        stable_freq = {name: [] for name in OBS_NAMES}
+        for row in beta_rows:
+            stable = [name for name in OBS_NAMES if abs(row["z"][name]) >= z_min]
+            stable_counts.append(len(stable))
+            for name in OBS_NAMES:
+                stable_freq[name].append(1.0 if name in stable else 0.0)
+        layers[f"{beta:.3f}"] = {
+            "stable_count_mean": float(np.mean(stable_counts)),
+            "stable_count_std": float(np.std(stable_counts, ddof=1)) if len(stable_counts) > 1 else 0.0,
+            "stable_frequency": {name: float(np.mean(vals)) for name, vals in stable_freq.items()},
+        }
+
+    endpoint_stable = []
+    for name in OBS_NAMES:
+        endpoint_rows = by_beta[0.0] + by_beta[1.0]
+        freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in endpoint_rows])
+        if freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def run(args: argparse.Namespace) -> dict:
+    rng = np.random.default_rng(args.seed)
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    rows = []
+
+    for rep in range(args.n_replicates):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        gue = gue_spacings(args.gue_matrix_size, args.n_gaps, rep_rng)
+        poisson = rep_rng.exponential(1.0, size=args.n_gaps)
+        poisson = poisson / float(np.mean(poisson))
+        for beta in betas:
+            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+            gaps = mixture_spacings(gue, poisson, beta, layer_rng)
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                n_baseline=args.n_baseline,
+                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "replicate": rep,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [name for name in OBS_NAMES if abs(z[name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
+
+    output = {
+        "experiment": "boundary_mixture_gate",
+        "question": "Is the GUE-Poisson mixed layer cleanly classifiable after denominator gating?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_endpoint_gated": gated_classification,
+        "rows": rows,
+    }
+
+    out_path = Path(args.out)
+    out_path.parent.mkdir(parents=True, exist_ok=True)
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
+    print(f"observables_used={OBS_NAMES}")
+    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
+    print(
+        "endpoint_distance_all="
+        f"{all_classification['endpoint_distance']:.3f} "
+        "endpoint_distance_gated="
+        f"{gated_classification['endpoint_distance']:.3f}"
+    )
+    if gate["endpoint_stable_observables"]:
+        print("beta stable_count margin_gated ambiguous_gated poisson_fraction_gated")
+    else:
+        print("endpoint gate is empty; printing all-observable classification")
+        print("beta stable_count margin_all ambiguous_all poisson_fraction_all")
+    for beta in betas:
+        key = f"{beta:.3f}"
+        stable_count = gate["layers"][key]["stable_count_mean"]
+        source = gated_classification if gate["endpoint_stable_observables"] else all_classification
+        layer = source["layers"].get(key, {})
+        print(
+            f"{beta:>4.2f} {stable_count:>12.3f} "
+            f"{layer.get('margin_mean', 0.0):>12.3f} "
+            f"{layer.get('ambiguous_fraction', 0.0):>15.3f} "
+            f"{layer.get('poisson_label_fraction', 0.0):>21.3f}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=1536)
+    parser.add_argument("--n-replicates", type=int, default=16)
+    parser.add_argument("--gue-matrix-size", type=int, default=180)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=24)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=20260507)
+    parser.add_argument("--out", default="tools/data/boundary_mixture_gate.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

apply patch
patch: completed
/opt/MM_D-ND/tools/data/seme.json
diff --git a/tools/data/reports/agent_20260507_0330.md b/tools/data/reports/agent_20260507_0330.md
new file mode 100644
index 0000000000000000000000000000000000000000..1bb66d4f31ae12b64e2652ac30c3e475aad25db7
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0330.md
@@ -0,0 +1,109 @@
+# Agent Report — The GUE-Poisson Boundary Is a Denominator Collapse Layer
+**Date**: 2026-05-07 03:30  
+**Piano**: 68  
+**Tension explored**: META + BOUNDARY  
+observables_registry: 1.0.0-2026-05-06  
+observables_used: [SR, SR2, L1, L2, triple_var]
+
+## Claim Under Test
+The last three runs constrained perturbation rank and observable collinearity:
+rank/PC2 claims are not interpretable without the canonical observable registry
+and the original-vs-shuffle denominator gate.
+
+This run does not repeat perturbation rank. It asks:
+
+> If the GUE-Poisson boundary is simulated directly by controlled mixtures,
+> does it behave like a clean two-class split, or like an operational third
+> layer where classification is ambiguous and denominator support collapses?
+
+## Experiment
+Tool created: `tools/exp_boundary_mixture_gate.py`
+
+Atomic perimeter:
+- domains: synthetic unfolded GUE spacings, iid Poisson spacings, and mixtures;
+- mixture parameter: `beta = 0.0..1.0`, where beta is the Poisson replacement fraction;
+- main run: 1,536 spacings, 16 replicates, GUE matrix size 180, 11 beta layers, 24 full-shuffle baselines;
+- seed check: 1,024 spacings, 12 replicates, GUE matrix size 160, same 11 beta layers, 20 baselines;
+- denominator gate: observable is stable when `abs(original - shuffle_mean) / shuffle_std >= 2`;
+- classification: standardized distance to pure GUE and pure Poisson centroids using all five canonical observables. A layer is marked ambiguous when at least half the replicates have nearest-centroid margin `< 0.15`.
+
+The endpoint-gated classifier is intentionally reported. In this perimeter it is empty because the Poisson endpoint has almost no stable original-vs-shuffle denominators. That is not discarded; it is the core META result.
+
+## Results
+
+### Main Run
+
+Endpoint separation using all canonical observables: `3.973` standardized units.  
+Endpoint-stable observables at frequency >= 0.75 across both endpoints: `[]`.
+
+| beta | stable obs / 5 | coord mean | margin mean | ambiguous fraction | Poisson-label fraction |
+|---:|---:|---:|---:|---:|---:|
+| 0.0 | 3.188 | -0.735 | 0.735 | 0.000 | 0.000 |
+| 0.1 | 3.312 | -0.470 | 0.470 | 0.000 | 0.000 |
+| 0.2 | 3.312 | -0.232 | 0.232 | 0.125 | 0.000 |
+| 0.3 | 2.500 | -0.054 | 0.070 | 0.875 | 0.250 |
+| 0.4 | 1.625 | +0.075 | 0.083 | 0.812 | 0.875 |
+| 0.5 | 0.750 | +0.260 | 0.260 | 0.000 | 1.000 |
+| 0.6 | 0.188 | +0.374 | 0.374 | 0.000 | 1.000 |
+| 0.7 | 0.500 | +0.520 | 0.520 | 0.000 | 1.000 |
+| 0.8 | 0.250 | +0.570 | 0.570 | 0.000 | 1.000 |
+| 0.9 | 0.250 | +0.692 | 0.692 | 0.000 | 1.000 |
+| 1.0 | 0.125 | +0.721 | 0.721 | 0.000 | 1.000 |
+
+At beta 0.0-0.2, the sequence is classified as GUE-like and retains about
+three stable observables. At beta 0.5-1.0, it is classified as Poisson-like,
+but denominator support is mostly absent. The transition is not centered at
+beta 0.5 in this observable suite. The ambiguous layer is beta 0.3-0.4.
+
+Observable stability frequencies in the main run:
+- beta 0.0: `SR=1.00`, `L1=1.00`, `triple_var=1.00`; `SR2=0.06`, `L2=0.12`;
+- beta 0.3: `SR=1.00`, `L1=0.75`, `triple_var=0.50`;
+- beta 0.4: `SR=0.75`, `L1=0.44`, `triple_var=0.38`;
+- beta 1.0: all canonical observables are weak except one `L2` replicate frequency of `0.12`.
+
+### Seed Check
+
+The lighter seed check repeated the same ambiguous layer:
+- ambiguous beta: `[0.3, 0.4]`;
+- beta 0.3: margin `0.082`, ambiguous fraction `0.917`, stable obs mean `1.250`;
+- beta 0.4: margin `0.125`, ambiguous fraction `0.750`, stable obs mean `0.833`;
+- beta 0.5 and above: Poisson-label fraction `1.000`, ambiguity `0.000`.
+
+## Findings
+
+1. **The clean two-class boundary fails under denominator gating.** Pure GUE and pure Poisson are separable in all-observable space, but there are no observables stable at both endpoints under the declared gate. The Poisson pole is a weak-denominator pole: classification can still place it, but retention-normalized structural claims cannot use it as if it had the same denominator support as GUE.
+
+2. **The operational boundary is a layer, not a line, in this synthetic perimeter.** Both the main run and the seed check isolate beta 0.3-0.4 as the ambiguous layer. In the main run the nearest-centroid margin falls to `0.070-0.083`, while ambiguous fraction rises to `0.812-0.875`. This is the measured form of the "third included" here: not a metaphysical third class, but a beta region where two-class assignment and denominator support are both unstable.
+
+3. **Denominator collapse precedes full Poisson classification.** Stable-observable count drops from about `3.3` at beta 0.1-0.2 to `2.5` at beta 0.3 and `1.625` at beta 0.4. By beta 0.5 the classifier is fully Poisson-labeled, but only `0.750/5` observables remain stable on average. The loss of denominator support is therefore part of the boundary phenomenon, not an after-the-fact nuisance.
+
+4. **The previous META constraints are extended, not replaced.** The 19:41 and 19:55 constraints still hold. This run adds that a boundary claim also needs a layer map: endpoint separability alone can hide the fact that one endpoint has no original-vs-shuffle denominator and that the transition region carries the actual instability.
+
+## Verdict
+**CONSTRAINT on META + BOUNDARY**: GUE/Poisson boundary claims must report:
+
+> observables_registry version + canonical observable list + original-vs-shuffle z gate per observable + endpoint-stable observable set + beta/window layer where classification margin is ambiguous.
+
+Scoped statement from this run:
+
+> In the synthetic mixture perimeter tested here, the GUE-Poisson boundary is an operational layer at beta 0.3-0.4: classification is ambiguous there, and denominator support collapses across the transition. The Poisson endpoint remains classifiable but denominator-weak, so it cannot serve as a symmetric structural pole for gated retention claims.
+
+## Consecutio
+What opens now: apply the same layer map to real domains rather than only synthetic mixtures. For primes, the next discriminating question is not "GUE or Poisson?" but:
+
+> Which scale window has the same signature as the synthetic beta 0.3-0.4 layer: low classifier margin plus falling original-vs-shuffle denominators?
+
+If prime windows show such a layer, BOUNDARY becomes a measurable transition surface. If they do not, the synthetic result remains a calibration constraint on how not to over-read endpoint separability.
+
+## Auto-audit: 5 lenti
+- **L1 hard constraint vs bias**: no zero/always claim. "Endpoint gate is empty" means no observable reached frequency >= 0.75 across both endpoints under `abs(z) >= 2`; it does not mean the observables are identically zero.
+- **L2 quantity vs ratio**: classification margin is reported together with stable-observable count and z-gate frequencies. Ratios are not interpreted without denominator support.
+- **L3 no silent patching**: the claim is explicitly changed from "8 GUE, 5 Poisson boundary" to a synthetic mixture calibration. This does not assert the same layer for primes or all real domains.
+- **L4 edge cases**: beta 0.2 has ambiguous fraction `0.125`, so it is not included in the ambiguous layer. The declared layer requires at least half the replicates ambiguous.
+- **L5 re-discovery**: this is a finite-sample diagnostic of crossover and noisy denominator normalization in classical GUE/Poisson spacings. It is not tagged as a new RMT theorem.
+
+## Files
+- Script: `tools/exp_boundary_mixture_gate.py`
+- Main data: `tools/data/boundary_mixture_gate_20260507_0330.json`
+- Seed check: `tools/data/boundary_mixture_gate_20260507_0330_seedcheck.json`
+- Report: `tools/data/reports/agent_20260507_0330.md`
diff --git a/tools/data/seme.json b/tools/data/seme.json
index 69e2b34538eeecf80c0226452bbe6c9aea44f157..63cbb7016a468ca68fed335ad629291ae277c528
--- a/tools/data/seme.json
+++ b/tools/data/seme.json
@@ -25,6 +25,17 @@
       "added_at": "2026-05-06T19:41:00+00:00"
     },
     {
+      "id": "BOUNDARY_LAYER_GATE",
+      "tipo": "vincolo",
+      "claim": "I claim GUE/Poisson boundary devono riportare layer map: versione observables_registry, lista osservabili canonici, z original-vs-shuffle per osservabile, set endpoint-stable, e finestra/layer con margine classificatorio ambiguo. Nel perimetro sintetico agent_20260507_0330, il confine GUE-Poisson e beta 0.3-0.4: margine 0.070-0.083, ambiguous fraction 0.812-0.875, mentre gli osservabili stabili collassano da ~3.3 a 1.6. Il polo Poisson e classificabile ma denominator-weak.",
+      "intensita": 0.93,
+      "porta": "META_BOUNDARY",
+      "manuale": true,
+      "condensato_ref": "A4,A8,A9,A14,C2",
+      "origine": "cycle agent_20260507_0330: synthetic GUE-Poisson mixture layer gate",
+      "added_at": "2026-05-07T03:30:00+00:00"
+    },
+    {
       "tipo": "confine_inesplorato",
       "id": "TRASCENDENZA_LIMITE",
       "claim": "La trascendenza e il limite attuale del modello. I punti fissi relazionali (non solo phi ma la rete di punti fissi tra osservabili) possono rivelare il vero grafo della realta e pattern nelle matrici. Il confine non e nella matematica - e nel passaggio tra piani.",
@@ -287,4 +298,4 @@
       "timestamp": "2026-04-04T19:34:08.014Z"
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/tools/exp_boundary_mixture_gate.py b/tools/exp_boundary_mixture_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..fcc62a3ba54911760e60cbf1ec299013dff19041
--- /dev/null
+++ b/tools/exp_boundary_mixture_gate.py
@@ -0,0 +1,283 @@
+#!/usr/bin/env python3
+"""
+exp_boundary_mixture_gate.py
+
+Reusable META/BOUNDARY audit for the GUE-Poisson boundary.
+
+Question:
+    Does the GUE/Poisson boundary remain a clean two-class split after the
+    original-vs-shuffle denominator gate, or is the mixed region an operational
+    third state where canonical observables lose stable denominators?
+
+The script uses only canonical observables from observables_registry.py.
+It builds synthetic mixtures by replacing a fraction beta of unfolded GUE
+spacings with Poisson spacings, then measures:
+
+- canonical observable vectors;
+- original-vs-shuffle z-score per observable;
+- endpoint separability in all observables and in gate-stable observables;
+- ambiguity of each beta layer relative to pure GUE and pure Poisson centroids.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
+
+
+OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())
+
+
+def gue_spacings(matrix_size: int, min_spacings: int, rng: np.random.Generator) -> np.ndarray:
+    """Generate unfolded GUE spacings by concatenating independent matrices."""
+    parts: list[np.ndarray] = []
+    edge = max(2, matrix_size // 10)
+    while sum(len(x) for x in parts) < min_spacings:
+        real = rng.standard_normal((matrix_size, matrix_size))
+        imag = rng.standard_normal((matrix_size, matrix_size))
+        h = real + 1j * imag
+        h = (h + h.conj().T) / (2.0 * np.sqrt(matrix_size))
+        eigs = np.sort(np.linalg.eigvalsh(h).real)
+        bulk = eigs[edge:-edge]
+        gaps = np.diff(bulk)
+        mean = float(np.mean(gaps))
+        if mean > 1e-15:
+            parts.append(gaps / mean)
+    return np.concatenate(parts)[:min_spacings].astype(float)
+
+
+def mixture_spacings(gue: np.ndarray, poisson: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    """Return a beta Poisson / (1-beta) GUE spacing sequence with mean spacing 1."""
+    if len(gue) != len(poisson):
+        raise ValueError("gue and poisson arrays must have the same length")
+    mask = rng.random(len(gue)) < beta
+    out = gue.copy()
+    out[mask] = poisson[mask]
+    mean = float(np.mean(out))
+    return out / mean if mean > 1e-15 else out
+
+
+def z_against_shuffle(
+    gaps: np.ndarray,
+    n_baseline: int,
+    rng: np.random.Generator,
+) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
+    """Return original observables, shuffle baseline std, and original-vs-shuffle z."""
+    original = compute_canonical(gaps)
+    baseline_vals = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline_vals[name].append(obs[name])
+
+    std = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline_vals[name], dtype=float)
+        mean = float(np.mean(vals))
+        sd = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, std, z
+
+
+def vector(row: dict, names: list[str]) -> np.ndarray:
+    return np.array([row["observables"][name] for name in names], dtype=float)
+
+
+def classify_layers(rows: list[dict], obs_names: list[str]) -> dict:
+    """Classify each beta layer by standardized distance to endpoint centroids."""
+    if not obs_names:
+        return {
+            "observables": [],
+            "endpoint_distance": 0.0,
+            "layers": {},
+            "ambiguous_beta": [],
+        }
+
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    gue_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    poi_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    all_endpoint = np.vstack([gue_vectors, poi_vectors])
+    scale = np.std(all_endpoint, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    gue_centroid = np.mean(gue_vectors, axis=0)
+    poi_centroid = np.mean(poi_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((poi_centroid - gue_centroid) / scale))
+
+    layers = {}
+    ambiguous_beta = []
+    for beta, beta_rows in sorted(by_beta.items()):
+        coords = []
+        margins = []
+        labels = []
+        for row in beta_rows:
+            x = vector(row, obs_names)
+            d_gue = float(np.linalg.norm((x - gue_centroid) / scale))
+            d_poi = float(np.linalg.norm((x - poi_centroid) / scale))
+            denom = d_gue + d_poi
+            coord = float((d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("gue" if d_gue < d_poi else "poisson")
+        ambiguous_fraction = float(np.mean(np.array(margins) < 0.15))
+        if ambiguous_fraction >= 0.5:
+            ambiguous_beta.append(beta)
+        layers[f"{beta:.3f}"] = {
+            "coordinate_mean": float(np.mean(coords)),
+            "coordinate_std": float(np.std(coords, ddof=1)) if len(coords) > 1 else 0.0,
+            "margin_mean": float(np.mean(margins)),
+            "ambiguous_fraction": ambiguous_fraction,
+            "poisson_label_fraction": float(np.mean(np.array(labels) == "poisson")),
+        }
+
+    return {
+        "observables": obs_names,
+        "endpoint_distance": endpoint_distance,
+        "layers": layers,
+        "ambiguous_beta": ambiguous_beta,
+    }
+
+
+def summarize_gate(rows: list[dict], z_min: float) -> dict:
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    layers = {}
+    for beta, beta_rows in sorted(by_beta.items()):
+        stable_counts = []
+        stable_freq = {name: [] for name in OBS_NAMES}
+        for row in beta_rows:
+            stable = [name for name in OBS_NAMES if abs(row["z"][name]) >= z_min]
+            stable_counts.append(len(stable))
+            for name in OBS_NAMES:
+                stable_freq[name].append(1.0 if name in stable else 0.0)
+        layers[f"{beta:.3f}"] = {
+            "stable_count_mean": float(np.mean(stable_counts)),
+            "stable_count_std": float(np.std(stable_counts, ddof=1)) if len(stable_counts) > 1 else 0.0,
+            "stable_frequency": {name: float(np.mean(vals)) for name, vals in stable_freq.items()},
+        }
+
+    endpoint_stable = []
+    for name in OBS_NAMES:
+        endpoint_rows = by_beta[0.0] + by_beta[1.0]
+        freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in endpoint_rows])
+        if freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def run(args: argparse.Namespace) -> dict:
+    rng = np.random.default_rng(args.seed)
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    rows = []
+
+    for rep in range(args.n_replicates):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        gue = gue_spacings(args.gue_matrix_size, args.n_gaps, rep_rng)
+        poisson = rep_rng.exponential(1.0, size=args.n_gaps)
+        poisson = poisson / float(np.mean(poisson))
+        for beta in betas:
+            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+            gaps = mixture_spacings(gue, poisson, beta, layer_rng)
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                n_baseline=args.n_baseline,
+                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "replicate": rep,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [name for name in OBS_NAMES if abs(z[name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
+
+    output = {
+        "experiment": "boundary_mixture_gate",
+        "question": "Is the GUE-Poisson mixed layer cleanly classifiable after denominator gating?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_endpoint_gated": gated_classification,
+        "rows": rows,
+    }
+
+    out_path = Path(args.out)
+    out_path.parent.mkdir(parents=True, exist_ok=True)
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
+    print(f"observables_used={OBS_NAMES}")
+    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
+    print(
+        "endpoint_distance_all="
+        f"{all_classification['endpoint_distance']:.3f} "
+        "endpoint_distance_gated="
+        f"{gated_classification['endpoint_distance']:.3f}"
+    )
+    if gate["endpoint_stable_observables"]:
+        print("beta stable_count margin_gated ambiguous_gated poisson_fraction_gated")
+    else:
+        print("endpoint gate is empty; printing all-observable classification")
+        print("beta stable_count margin_all ambiguous_all poisson_fraction_all")
+    for beta in betas:
+        key = f"{beta:.3f}"
+        stable_count = gate["layers"][key]["stable_count_mean"]
+        source = gated_classification if gate["endpoint_stable_observables"] else all_classification
+        layer = source["layers"].get(key, {})
+        print(
+            f"{beta:>4.2f} {stable_count:>12.3f} "
+            f"{layer.get('margin_mean', 0.0):>12.3f} "
+            f"{layer.get('ambiguous_fraction', 0.0):>15.3f} "
+            f"{layer.get('poisson_label_fraction', 0.0):>21.3f}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=1536)
+    parser.add_argument("--n-replicates", type=int, default=16)
+    parser.add_argument("--gue-matrix-size", type=int, default=180)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=24)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=20260507)
+    parser.add_argument("--out", default="tools/data/boundary_mixture_gate.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

diff --git a/tools/data/reports/agent_20260507_0330.md b/tools/data/reports/agent_20260507_0330.md
new file mode 100644
index 0000000000000000000000000000000000000000..1bb66d4f31ae12b64e2652ac30c3e475aad25db7
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0330.md
@@ -0,0 +1,109 @@
+# Agent Report — The GUE-Poisson Boundary Is a Denominator Collapse Layer
+**Date**: 2026-05-07 03:30  
+**Piano**: 68  
+**Tension explored**: META + BOUNDARY  
+observables_registry: 1.0.0-2026-05-06  
+observables_used: [SR, SR2, L1, L2, triple_var]
+
+## Claim Under Test
+The last three runs constrained perturbation rank and observable collinearity:
+rank/PC2 claims are not interpretable without the canonical observable registry
+and the original-vs-shuffle denominator gate.
+
+This run does not repeat perturbation rank. It asks:
+
+> If the GUE-Poisson boundary is simulated directly by controlled mixtures,
+> does it behave like a clean two-class split, or like an operational third
+> layer where classification is ambiguous and denominator support collapses?
+
+## Experiment
+Tool created: `tools/exp_boundary_mixture_gate.py`
+
+Atomic perimeter:
+- domains: synthetic unfolded GUE spacings, iid Poisson spacings, and mixtures;
+- mixture parameter: `beta = 0.0..1.0`, where beta is the Poisson replacement fraction;
+- main run: 1,536 spacings, 16 replicates, GUE matrix size 180, 11 beta layers, 24 full-shuffle baselines;
+- seed check: 1,024 spacings, 12 replicates, GUE matrix size 160, same 11 beta layers, 20 baselines;
+- denominator gate: observable is stable when `abs(original - shuffle_mean) / shuffle_std >= 2`;
+- classification: standardized distance to pure GUE and pure Poisson centroids using all five canonical observables. A layer is marked ambiguous when at least half the replicates have nearest-centroid margin `< 0.15`.
+
+The endpoint-gated classifier is intentionally reported. In this perimeter it is empty because the Poisson endpoint has almost no stable original-vs-shuffle denominators. That is not discarded; it is the core META result.
+
+## Results
+
+### Main Run
+
+Endpoint separation using all canonical observables: `3.973` standardized units.  
+Endpoint-stable observables at frequency >= 0.75 across both endpoints: `[]`.
+
+| beta | stable obs / 5 | coord mean | margin mean | ambiguous fraction | Poisson-label fraction |
+|---:|---:|---:|---:|---:|---:|
+| 0.0 | 3.188 | -0.735 | 0.735 | 0.000 | 0.000 |
+| 0.1 | 3.312 | -0.470 | 0.470 | 0.000 | 0.000 |
+| 0.2 | 3.312 | -0.232 | 0.232 | 0.125 | 0.000 |
+| 0.3 | 2.500 | -0.054 | 0.070 | 0.875 | 0.250 |
+| 0.4 | 1.625 | +0.075 | 0.083 | 0.812 | 0.875 |
+| 0.5 | 0.750 | +0.260 | 0.260 | 0.000 | 1.000 |
+| 0.6 | 0.188 | +0.374 | 0.374 | 0.000 | 1.000 |
+| 0.7 | 0.500 | +0.520 | 0.520 | 0.000 | 1.000 |
+| 0.8 | 0.250 | +0.570 | 0.570 | 0.000 | 1.000 |
+| 0.9 | 0.250 | +0.692 | 0.692 | 0.000 | 1.000 |
+| 1.0 | 0.125 | +0.721 | 0.721 | 0.000 | 1.000 |
+
+At beta 0.0-0.2, the sequence is classified as GUE-like and retains about
+three stable observables. At beta 0.5-1.0, it is classified as Poisson-like,
+but denominator support is mostly absent. The transition is not centered at
+beta 0.5 in this observable suite. The ambiguous layer is beta 0.3-0.4.
+
+Observable stability frequencies in the main run:
+- beta 0.0: `SR=1.00`, `L1=1.00`, `triple_var=1.00`; `SR2=0.06`, `L2=0.12`;
+- beta 0.3: `SR=1.00`, `L1=0.75`, `triple_var=0.50`;
+- beta 0.4: `SR=0.75`, `L1=0.44`, `triple_var=0.38`;
+- beta 1.0: all canonical observables are weak except one `L2` replicate frequency of `0.12`.
+
+### Seed Check
+
+The lighter seed check repeated the same ambiguous layer:
+- ambiguous beta: `[0.3, 0.4]`;
+- beta 0.3: margin `0.082`, ambiguous fraction `0.917`, stable obs mean `1.250`;
+- beta 0.4: margin `0.125`, ambiguous fraction `0.750`, stable obs mean `0.833`;
+- beta 0.5 and above: Poisson-label fraction `1.000`, ambiguity `0.000`.
+
+## Findings
+
+1. **The clean two-class boundary fails under denominator gating.** Pure GUE and pure Poisson are separable in all-observable space, but there are no observables stable at both endpoints under the declared gate. The Poisson pole is a weak-denominator pole: classification can still place it, but retention-normalized structural claims cannot use it as if it had the same denominator support as GUE.
+
+2. **The operational boundary is a layer, not a line, in this synthetic perimeter.** Both the main run and the seed check isolate beta 0.3-0.4 as the ambiguous layer. In the main run the nearest-centroid margin falls to `0.070-0.083`, while ambiguous fraction rises to `0.812-0.875`. This is the measured form of the "third included" here: not a metaphysical third class, but a beta region where two-class assignment and denominator support are both unstable.
+
+3. **Denominator collapse precedes full Poisson classification.** Stable-observable count drops from about `3.3` at beta 0.1-0.2 to `2.5` at beta 0.3 and `1.625` at beta 0.4. By beta 0.5 the classifier is fully Poisson-labeled, but only `0.750/5` observables remain stable on average. The loss of denominator support is therefore part of the boundary phenomenon, not an after-the-fact nuisance.
+
+4. **The previous META constraints are extended, not replaced.** The 19:41 and 19:55 constraints still hold. This run adds that a boundary claim also needs a layer map: endpoint separability alone can hide the fact that one endpoint has no original-vs-shuffle denominator and that the transition region carries the actual instability.
+
+## Verdict
+**CONSTRAINT on META + BOUNDARY**: GUE/Poisson boundary claims must report:
+
+> observables_registry version + canonical observable list + original-vs-shuffle z gate per observable + endpoint-stable observable set + beta/window layer where classification margin is ambiguous.
+
+Scoped statement from this run:
+
+> In the synthetic mixture perimeter tested here, the GUE-Poisson boundary is an operational layer at beta 0.3-0.4: classification is ambiguous there, and denominator support collapses across the transition. The Poisson endpoint remains classifiable but denominator-weak, so it cannot serve as a symmetric structural pole for gated retention claims.
+
+## Consecutio
+What opens now: apply the same layer map to real domains rather than only synthetic mixtures. For primes, the next discriminating question is not "GUE or Poisson?" but:
+
+> Which scale window has the same signature as the synthetic beta 0.3-0.4 layer: low classifier margin plus falling original-vs-shuffle denominators?
+
+If prime windows show such a layer, BOUNDARY becomes a measurable transition surface. If they do not, the synthetic result remains a calibration constraint on how not to over-read endpoint separability.
+
+## Auto-audit: 5 lenti
+- **L1 hard constraint vs bias**: no zero/always claim. "Endpoint gate is empty" means no observable reached frequency >= 0.75 across both endpoints under `abs(z) >= 2`; it does not mean the observables are identically zero.
+- **L2 quantity vs ratio**: classification margin is reported together with stable-observable count and z-gate frequencies. Ratios are not interpreted without denominator support.
+- **L3 no silent patching**: the claim is explicitly changed from "8 GUE, 5 Poisson boundary" to a synthetic mixture calibration. This does not assert the same layer for primes or all real domains.
+- **L4 edge cases**: beta 0.2 has ambiguous fraction `0.125`, so it is not included in the ambiguous layer. The declared layer requires at least half the replicates ambiguous.
+- **L5 re-discovery**: this is a finite-sample diagnostic of crossover and noisy denominator normalization in classical GUE/Poisson spacings. It is not tagged as a new RMT theorem.
+
+## Files
+- Script: `tools/exp_boundary_mixture_gate.py`
+- Main data: `tools/data/boundary_mixture_gate_20260507_0330.json`
+- Seed check: `tools/data/boundary_mixture_gate_20260507_0330_seedcheck.json`
+- Report: `tools/data/reports/agent_20260507_0330.md`
diff --git a/tools/data/seme.json b/tools/data/seme.json
index 69e2b34538eeecf80c0226452bbe6c9aea44f157..63cbb7016a468ca68fed335ad629291ae277c528
--- a/tools/data/seme.json
+++ b/tools/data/seme.json
@@ -25,6 +25,17 @@
       "added_at": "2026-05-06T19:41:00+00:00"
     },
     {
+      "id": "BOUNDARY_LAYER_GATE",
+      "tipo": "vincolo",
+      "claim": "I claim GUE/Poisson boundary devono riportare layer map: versione observables_registry, lista osservabili canonici, z original-vs-shuffle per osservabile, set endpoint-stable, e finestra/layer con margine classificatorio ambiguo. Nel perimetro sintetico agent_20260507_0330, il confine GUE-Poisson e beta 0.3-0.4: margine 0.070-0.083, ambiguous fraction 0.812-0.875, mentre gli osservabili stabili collassano da ~3.3 a 1.6. Il polo Poisson e classificabile ma denominator-weak.",
+      "intensita": 0.93,
+      "porta": "META_BOUNDARY",
+      "manuale": true,
+      "condensato_ref": "A4,A8,A9,A14,C2",
+      "origine": "cycle agent_20260507_0330: synthetic GUE-Poisson mixture layer gate",
+      "added_at": "2026-05-07T03:30:00+00:00"
+    },
+    {
       "tipo": "confine_inesplorato",
       "id": "TRASCENDENZA_LIMITE",
       "claim": "La trascendenza e il limite attuale del modello. I punti fissi relazionali (non solo phi ma la rete di punti fissi tra osservabili) possono rivelare il vero grafo della realta e pattern nelle matrici. Il confine non e nella matematica - e nel passaggio tra piani.",
@@ -287,4 +298,4 @@
       "timestamp": "2026-04-04T19:34:08.014Z"
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/tools/exp_boundary_mixture_gate.py b/tools/exp_boundary_mixture_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..fcc62a3ba54911760e60cbf1ec299013dff19041
--- /dev/null
+++ b/tools/exp_boundary_mixture_gate.py
@@ -0,0 +1,283 @@
+#!/usr/bin/env python3
+"""
+exp_boundary_mixture_gate.py
+
+Reusable META/BOUNDARY audit for the GUE-Poisson boundary.
+
+Question:
+    Does the GUE/Poisson boundary remain a clean two-class split after the
+    original-vs-shuffle denominator gate, or is the mixed region an operational
+    third state where canonical observables lose stable denominators?
+
+The script uses only canonical observables from observables_registry.py.
+It builds synthetic mixtures by replacing a fraction beta of unfolded GUE
+spacings with Poisson spacings, then measures:
+
+- canonical observable vectors;
+- original-vs-shuffle z-score per observable;
+- endpoint separability in all observables and in gate-stable observables;
+- ambiguity of each beta layer relative to pure GUE and pure Poisson centroids.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
+
+
+OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())
+
+
+def gue_spacings(matrix_size: int, min_spacings: int, rng: np.random.Generator) -> np.ndarray:
+    """Generate unfolded GUE spacings by concatenating independent matrices."""
+    parts: list[np.ndarray] = []
+    edge = max(2, matrix_size // 10)
+    while sum(len(x) for x in parts) < min_spacings:
+        real = rng.standard_normal((matrix_size, matrix_size))
+        imag = rng.standard_normal((matrix_size, matrix_size))
+        h = real + 1j * imag
+        h = (h + h.conj().T) / (2.0 * np.sqrt(matrix_size))
+        eigs = np.sort(np.linalg.eigvalsh(h).real)
+        bulk = eigs[edge:-edge]
+        gaps = np.diff(bulk)
+        mean = float(np.mean(gaps))
+        if mean > 1e-15:
+            parts.append(gaps / mean)
+    return np.concatenate(parts)[:min_spacings].astype(float)
+
+
+def mixture_spacings(gue: np.ndarray, poisson: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    """Return a beta Poisson / (1-beta) GUE spacing sequence with mean spacing 1."""
+    if len(gue) != len(poisson):
+        raise ValueError("gue and poisson arrays must have the same length")
+    mask = rng.random(len(gue)) < beta
+    out = gue.copy()
+    out[mask] = poisson[mask]
+    mean = float(np.mean(out))
+    return out / mean if mean > 1e-15 else out
+
+
+def z_against_shuffle(
+    gaps: np.ndarray,
+    n_baseline: int,
+    rng: np.random.Generator,
+) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
+    """Return original observables, shuffle baseline std, and original-vs-shuffle z."""
+    original = compute_canonical(gaps)
+    baseline_vals = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline_vals[name].append(obs[name])
+
+    std = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline_vals[name], dtype=float)
+        mean = float(np.mean(vals))
+        sd = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, std, z
+
+
+def vector(row: dict, names: list[str]) -> np.ndarray:
+    return np.array([row["observables"][name] for name in names], dtype=float)
+
+
+def classify_layers(rows: list[dict], obs_names: list[str]) -> dict:
+    """Classify each beta layer by standardized distance to endpoint centroids."""
+    if not obs_names:
+        return {
+            "observables": [],
+            "endpoint_distance": 0.0,
+            "layers": {},
+            "ambiguous_beta": [],
+        }
+
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    gue_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    poi_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    all_endpoint = np.vstack([gue_vectors, poi_vectors])
+    scale = np.std(all_endpoint, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    gue_centroid = np.mean(gue_vectors, axis=0)
+    poi_centroid = np.mean(poi_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((poi_centroid - gue_centroid) / scale))
+
+    layers = {}
+    ambiguous_beta = []
+    for beta, beta_rows in sorted(by_beta.items()):
+        coords = []
+        margins = []
+        labels = []
+        for row in beta_rows:
+            x = vector(row, obs_names)
+            d_gue = float(np.linalg.norm((x - gue_centroid) / scale))
+            d_poi = float(np.linalg.norm((x - poi_centroid) / scale))
+            denom = d_gue + d_poi
+            coord = float((d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("gue" if d_gue < d_poi else "poisson")
+        ambiguous_fraction = float(np.mean(np.array(margins) < 0.15))
+        if ambiguous_fraction >= 0.5:
+            ambiguous_beta.append(beta)
+        layers[f"{beta:.3f}"] = {
+            "coordinate_mean": float(np.mean(coords)),
+            "coordinate_std": float(np.std(coords, ddof=1)) if len(coords) > 1 else 0.0,
+            "margin_mean": float(np.mean(margins)),
+            "ambiguous_fraction": ambiguous_fraction,
+            "poisson_label_fraction": float(np.mean(np.array(labels) == "poisson")),
+        }
+
+    return {
+        "observables": obs_names,
+        "endpoint_distance": endpoint_distance,
+        "layers": layers,
+        "ambiguous_beta": ambiguous_beta,
+    }
+
+
+def summarize_gate(rows: list[dict], z_min: float) -> dict:
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    layers = {}
+    for beta, beta_rows in sorted(by_beta.items()):
+        stable_counts = []
+        stable_freq = {name: [] for name in OBS_NAMES}
+        for row in beta_rows:
+            stable = [name for name in OBS_NAMES if abs(row["z"][name]) >= z_min]
+            stable_counts.append(len(stable))
+            for name in OBS_NAMES:
+                stable_freq[name].append(1.0 if name in stable else 0.0)
+        layers[f"{beta:.3f}"] = {
+            "stable_count_mean": float(np.mean(stable_counts)),
+            "stable_count_std": float(np.std(stable_counts, ddof=1)) if len(stable_counts) > 1 else 0.0,
+            "stable_frequency": {name: float(np.mean(vals)) for name, vals in stable_freq.items()},
+        }
+
+    endpoint_stable = []
+    for name in OBS_NAMES:
+        endpoint_rows = by_beta[0.0] + by_beta[1.0]
+        freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in endpoint_rows])
+        if freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def run(args: argparse.Namespace) -> dict:
+    rng = np.random.default_rng(args.seed)
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    rows = []
+
+    for rep in range(args.n_replicates):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        gue = gue_spacings(args.gue_matrix_size, args.n_gaps, rep_rng)
+        poisson = rep_rng.exponential(1.0, size=args.n_gaps)
+        poisson = poisson / float(np.mean(poisson))
+        for beta in betas:
+            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+            gaps = mixture_spacings(gue, poisson, beta, layer_rng)
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                n_baseline=args.n_baseline,
+                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "replicate": rep,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [name for name in OBS_NAMES if abs(z[name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
+
+    output = {
+        "experiment": "boundary_mixture_gate",
+        "question": "Is the GUE-Poisson mixed layer cleanly classifiable after denominator gating?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_endpoint_gated": gated_classification,
+        "rows": rows,
+    }
+
+    out_path = Path(args.out)
+    out_path.parent.mkdir(parents=True, exist_ok=True)
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
+    print(f"observables_used={OBS_NAMES}")
+    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
+    print(
+        "endpoint_distance_all="
+        f"{all_classification['endpoint_distance']:.3f} "
+        "endpoint_distance_gated="
+        f"{gated_classification['endpoint_distance']:.3f}"
+    )
+    if gate["endpoint_stable_observables"]:
+        print("beta stable_count margin_gated ambiguous_gated poisson_fraction_gated")
+    else:
+        print("endpoint gate is empty; printing all-observable classification")
+        print("beta stable_count margin_all ambiguous_all poisson_fraction_all")
+    for beta in betas:
+        key = f"{beta:.3f}"
+        stable_count = gate["layers"][key]["stable_count_mean"]
+        source = gated_classification if gate["endpoint_stable_observables"] else all_classification
+        layer = source["layers"].get(key, {})
+        print(
+            f"{beta:>4.2f} {stable_count:>12.3f} "
+            f"{layer.get('margin_mean', 0.0):>12.3f} "
+            f"{layer.get('ambiguous_fraction', 0.0):>15.3f} "
+            f"{layer.get('poisson_label_fraction', 0.0):>21.3f}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=1536)
+    parser.add_argument("--n-replicates", type=int, default=16)
+    parser.add_argument("--gue-matrix-size", type=int, default=180)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=24)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=20260507)
+    parser.add_argument("--out", default="tools/data/boundary_mixture_gate.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

exec
/bin/bash -lc "sed -n '1,260p' tools/data/reports/agent_20260507_0330.md" in /opt/MM_D-ND
 succeeded in 0ms:
# Agent Report — The GUE-Poisson Boundary Is a Denominator Collapse Layer
**Date**: 2026-05-07 03:30  
**Piano**: 68  
**Tension explored**: META + BOUNDARY  
observables_registry: 1.0.0-2026-05-06  
observables_used: [SR, SR2, L1, L2, triple_var]

## Claim Under Test
The last three runs constrained perturbation rank and observable collinearity:
rank/PC2 claims are not interpretable without the canonical observable registry
and the original-vs-shuffle denominator gate.

This run does not repeat perturbation rank. It asks:

> If the GUE-Poisson boundary is simulated directly by controlled mixtures,
> does it behave like a clean two-class split, or like an operational third
> layer where classification is ambiguous and denominator support collapses?

## Experiment
Tool created: `tools/exp_boundary_mixture_gate.py`

Atomic perimeter:
- domains: synthetic unfolded GUE spacings, iid Poisson spacings, and mixtures;
- mixture parameter: `beta = 0.0..1.0`, where beta is the Poisson replacement fraction;
- main run: 1,536 spacings, 16 replicates, GUE matrix size 180, 11 beta layers, 24 full-shuffle baselines;
- seed check: 1,024 spacings, 12 replicates, GUE matrix size 160, same 11 beta layers, 20 baselines;
- denominator gate: observable is stable when `abs(original - shuffle_mean) / shuffle_std >= 2`;
- classification: standardized distance to pure GUE and pure Poisson centroids using all five canonical observables. A layer is marked ambiguous when at least half the replicates have nearest-centroid margin `< 0.15`.

The endpoint-gated classifier is intentionally reported. In this perimeter it is empty because the Poisson endpoint has almost no stable original-vs-shuffle denominators. That is not discarded; it is the core META result.

## Results

### Main Run

Endpoint separation using all canonical observables: `3.973` standardized units.  
Endpoint-stable observables at frequency >= 0.75 across both endpoints: `[]`.

| beta | stable obs / 5 | coord mean | margin mean | ambiguous fraction | Poisson-label fraction |
|---:|---:|---:|---:|---:|---:|
| 0.0 | 3.188 | -0.735 | 0.735 | 0.000 | 0.000 |
| 0.1 | 3.312 | -0.470 | 0.470 | 0.000 | 0.000 |
| 0.2 | 3.312 | -0.232 | 0.232 | 0.125 | 0.000 |
| 0.3 | 2.500 | -0.054 | 0.070 | 0.875 | 0.250 |
| 0.4 | 1.625 | +0.075 | 0.083 | 0.812 | 0.875 |
| 0.5 | 0.750 | +0.260 | 0.260 | 0.000 | 1.000 |
| 0.6 | 0.188 | +0.374 | 0.374 | 0.000 | 1.000 |
| 0.7 | 0.500 | +0.520 | 0.520 | 0.000 | 1.000 |
| 0.8 | 0.250 | +0.570 | 0.570 | 0.000 | 1.000 |
| 0.9 | 0.250 | +0.692 | 0.692 | 0.000 | 1.000 |
| 1.0 | 0.125 | +0.721 | 0.721 | 0.000 | 1.000 |

At beta 0.0-0.2, the sequence is classified as GUE-like and retains about
three stable observables. At beta 0.5-1.0, it is classified as Poisson-like,
but denominator support is mostly absent. The transition is not centered at
beta 0.5 in this observable suite. The ambiguous layer is beta 0.3-0.4.

Observable stability frequencies in the main run:
- beta 0.0: `SR=1.00`, `L1=1.00`, `triple_var=1.00`; `SR2=0.06`, `L2=0.12`;
- beta 0.3: `SR=1.00`, `L1=0.75`, `triple_var=0.50`;
- beta 0.4: `SR=0.75`, `L1=0.44`, `triple_var=0.38`;
- beta 1.0: all canonical observables are weak except one `L2` replicate frequency of `0.12`.

### Seed Check

The lighter seed check repeated the same ambiguous layer:
- ambiguous beta: `[0.3, 0.4]`;
- beta 0.3: margin `0.082`, ambiguous fraction `0.917`, stable obs mean `1.250`;
- beta 0.4: margin `0.125`, ambiguous fraction `0.750`, stable obs mean `0.833`;
- beta 0.5 and above: Poisson-label fraction `1.000`, ambiguity `0.000`.

## Findings

1. **The clean two-class boundary fails under denominator gating.** Pure GUE and pure Poisson are separable in all-observable space, but there are no observables stable at both endpoints under the declared gate. The Poisson pole is a weak-denominator pole: classification can still place it, but retention-normalized structural claims cannot use it as if it had the same denominator support as GUE.

2. **The operational boundary is a layer, not a line, in this synthetic perimeter.** Both the main run and the seed check isolate beta 0.3-0.4 as the ambiguous layer. In the main run the nearest-centroid margin falls to `0.070-0.083`, while ambiguous fraction rises to `0.812-0.875`. This is the measured form of the "third included" here: not a metaphysical third class, but a beta region where two-class assignment and denominator support are both unstable.

3. **Denominator collapse precedes full Poisson classification.** Stable-observable count drops from about `3.3` at beta 0.1-0.2 to `2.5` at beta 0.3 and `1.625` at beta 0.4. By beta 0.5 the classifier is fully Poisson-labeled, but only `0.750/5` observables remain stable on average. The loss of denominator support is therefore part of the boundary phenomenon, not an after-the-fact nuisance.

4. **The previous META constraints are extended, not replaced.** The 19:41 and 19:55 constraints still hold. This run adds that a boundary claim also needs a layer map: endpoint separability alone can hide the fact that one endpoint has no original-vs-shuffle denominator and that the transition region carries the actual instability.

## Verdict
**CONSTRAINT on META + BOUNDARY**: GUE/Poisson boundary claims must report:

> observables_registry version + canonical observable list + original-vs-shuffle z gate per observable + endpoint-stable observable set + beta/window layer where classification margin is ambiguous.

Scoped statement from this run:

> In the synthetic mixture perimeter tested here, the GUE-Poisson boundary is an operational layer at beta 0.3-0.4: classification is ambiguous there, and denominator support collapses across the transition. The Poisson endpoint remains classifiable but denominator-weak, so it cannot serve as a symmetric structural pole for gated retention claims.

## Consecutio
What opens now: apply the same layer map to real domains rather than only synthetic mixtures. For primes, the next discriminating question is not "GUE or Poisson?" but:

> Which scale window has the same signature as the synthetic beta 0.3-0.4 layer: low classifier margin plus falling original-vs-shuffle denominators?

If prime windows show such a layer, BOUNDARY becomes a measurable transition surface. If they do not, the synthetic result remains a calibration constraint on how not to over-read endpoint separability.

## Auto-audit: 5 lenti
- **L1 hard constraint vs bias**: no zero/always claim. "Endpoint gate is empty" means no observable reached frequency >= 0.75 across both endpoints under `abs(z) >= 2`; it does not mean the observables are identically zero.
- **L2 quantity vs ratio**: classification margin is reported together with stable-observable count and z-gate frequencies. Ratios are not interpreted without denominator support.
- **L3 no silent patching**: the claim is explicitly changed from "8 GUE, 5 Poisson boundary" to a synthetic mixture calibration. This does not assert the same layer for primes or all real domains.
- **L4 edge cases**: beta 0.2 has ambiguous fraction `0.125`, so it is not included in the ambiguous layer. The declared layer requires at least half the replicates ambiguous.
- **L5 re-discovery**: this is a finite-sample diagnostic of crossover and noisy denominator normalization in classical GUE/Poisson spacings. It is not tagged as a new RMT theorem.

## Files
- Script: `tools/exp_boundary_mixture_gate.py`
- Main data: `tools/data/boundary_mixture_gate_20260507_0330.json`
- Seed check: `tools/data/boundary_mixture_gate_20260507_0330_seedcheck.json`
- Report: `tools/data/reports/agent_20260507_0330.md`

exec
/bin/bash -lc "git diff -- tools/exp_boundary_mixture_gate.py tools/data/seme.json tools/data/reports/agent_20260507_0330.md | sed -n '1,260p'" in /opt/MM_D-ND
 succeeded in 0ms:
diff --git a/tools/data/seme.json b/tools/data/seme.json
index a31dc54..63cbb70 100644
--- a/tools/data/seme.json
+++ b/tools/data/seme.json
@@ -1,7 +1,40 @@
 {
-  "timestamp": "2026-04-23T03:35:37.611830",
-  "piano": 48,
+  "timestamp": "2026-05-06T20:02:38.262185",
+  "piano": 68,
   "tensioni": [
+    {
+      "id": "OBSERVABLE_REGISTRY",
+      "tipo": "vincolo",
+      "claim": "Ogni script che usa observables canonici (SR, SR2, L1, L2, triple_var) deve importare la definizione da tools/observables_registry.py. Varianti devono usare nomi distinti (SR_local_rigidity, triple_var_normalized) — niente shadowing del nome canonico. Ogni report deve dichiarare 'observables_registry: VERSION' nel header.",
+      "intensita": 1.0,
+      "porta": "infrastructure",
+      "manuale": true,
+      "condensato_ref": "A14,A8",
+      "origine": "cristallizzato 06/05 dalla consecutio del cycle 20260506_0625 (autopoietico self-finding)",
+      "added_at": "2026-05-06T07:03:58.213606+00:00"
+    },
+    {
+      "id": "PERTURBATION_DENOMINATOR_GATE",
+      "tipo": "vincolo",
+      "claim": "La dimensionalita di perturbazione va riportata solo insieme a PC2, versione observables_registry e gate original-vs-shuffle per osservabile. Nel perimetro 20260506_1941, Poisson e shuffle-primi producono rank_all ~1.8-2.0 con denominatori deboli; dopo gate abs(z)>=2 il rank stabile torna vicino a 1. Rank PCA non gated non e evidenza strutturale.",
+      "intensita": 0.95,
+      "porta": "META_BOUNDARY",
+      "manuale": true,
+      "condensato_ref": "A4,A8,A14,C2",
+      "origine": "cycle agent_20260506_1941: perturbation rank size curve canonical observables",
+      "added_at": "2026-05-06T19:41:00+00:00"
+    },
+    {
+      "id": "BOUNDARY_LAYER_GATE",
+      "tipo": "vincolo",
+      "claim": "I claim GUE/Poisson boundary devono riportare layer map: versione observables_registry, lista osservabili canonici, z original-vs-shuffle per osservabile, set endpoint-stable, e finestra/layer con margine classificatorio ambiguo. Nel perimetro sintetico agent_20260507_0330, il confine GUE-Poisson e beta 0.3-0.4: margine 0.070-0.083, ambiguous fraction 0.812-0.875, mentre gli osservabili stabili collassano da ~3.3 a 1.6. Il polo Poisson e classificabile ma denominator-weak.",
+      "intensita": 0.93,
+      "porta": "META_BOUNDARY",
+      "manuale": true,
+      "condensato_ref": "A4,A8,A9,A14,C2",
+      "origine": "cycle agent_20260507_0330: synthetic GUE-Poisson mixture layer gate",
+      "added_at": "2026-05-07T03:30:00+00:00"
+    },
     {
       "tipo": "confine_inesplorato",
       "id": "TRASCENDENZA_LIMITE",
@@ -84,18 +117,17 @@
       "claim": "Tutti i 11 test passano — verifica che non stiamo testando solo tautologie",
       "intensità": 0.5,
       "nota": "La convergenza a φ è triviale (controprove). I test stanno verificando contenuto o struttura?",
-      "condensato_ref": null,
-      "condensato_motivo": "Ricorrente (3x in 2 giorni) e fuori dalla mappa",
-      "porta": "novità"
+      "condensato_ref": "A4,A12,C2",
+      "porta": "verify_assertions_META_ALL_PASS",
+      "condensato_motivo": "Ricorrente (3x in 2 giorni) e fuori dalla mappa"
     }
   ],
   "potenziale_bloccato": [],
   "varianza": [
-    "Nuove tensioni: {'META'}",
-    "Tensioni risolte: {'METRIC_TENSOR', 'TRASCENDENZA_LIMITE', 'TENSIONE_ENTITA', 'PIANO_PRIMARIO_DUE_ASSIOMI', 'DUALITA_DIPOLARE_VS_ILLUSORIA', 'G_POTENZIALE_NULLA'}"
+    "Tensioni risolte: {'TENSIONE_ENTITA', 'OBSERVABLE_REGISTRY', 'PERTURBATION_DENOMINATOR_GATE', 'PIANO_PRIMARIO_DUE_ASSIOMI', 'OBSERVABLE_COLLINEARITY_GATE', 'METRIC_TENSOR', 'TRASCENDENZA_LIMITE', 'G_POTENZIALE_NULLA', 'TRAJECTORY_APPLY_20260506_1941', 'DUALITA_DIPOLARE_VS_ILLUSORIA'}"
   ],
   "filtro": {
-    "promosse": 8,
+    "promosse": 10,
     "filtrate": 0,
     "condensato_claims": 30
   },

exec
/bin/bash -lc 'python -m json.tool tools/data/seme.json >/tmp/seme_validated.json' in /opt/MM_D-ND
 succeeded in 0ms:
diff --git a/tools/data/reports/agent_20260507_0330.md b/tools/data/reports/agent_20260507_0330.md
new file mode 100644
index 0000000000000000000000000000000000000000..1bb66d4f31ae12b64e2652ac30c3e475aad25db7
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0330.md
@@ -0,0 +1,109 @@
+# Agent Report — The GUE-Poisson Boundary Is a Denominator Collapse Layer
+**Date**: 2026-05-07 03:30  
+**Piano**: 68  
+**Tension explored**: META + BOUNDARY  
+observables_registry: 1.0.0-2026-05-06  
+observables_used: [SR, SR2, L1, L2, triple_var]
+
+## Claim Under Test
+The last three runs constrained perturbation rank and observable collinearity:
+rank/PC2 claims are not interpretable without the canonical observable registry
+and the original-vs-shuffle denominator gate.
+
+This run does not repeat perturbation rank. It asks:
+
+> If the GUE-Poisson boundary is simulated directly by controlled mixtures,
+> does it behave like a clean two-class split, or like an operational third
+> layer where classification is ambiguous and denominator support collapses?
+
+## Experiment
+Tool created: `tools/exp_boundary_mixture_gate.py`
+
+Atomic perimeter:
+- domains: synthetic unfolded GUE spacings, iid Poisson spacings, and mixtures;
+- mixture parameter: `beta = 0.0..1.0`, where beta is the Poisson replacement fraction;
+- main run: 1,536 spacings, 16 replicates, GUE matrix size 180, 11 beta layers, 24 full-shuffle baselines;
+- seed check: 1,024 spacings, 12 replicates, GUE matrix size 160, same 11 beta layers, 20 baselines;
+- denominator gate: observable is stable when `abs(original - shuffle_mean) / shuffle_std >= 2`;
+- classification: standardized distance to pure GUE and pure Poisson centroids using all five canonical observables. A layer is marked ambiguous when at least half the replicates have nearest-centroid margin `< 0.15`.
+
+The endpoint-gated classifier is intentionally reported. In this perimeter it is empty because the Poisson endpoint has almost no stable original-vs-shuffle denominators. That is not discarded; it is the core META result.
+
+## Results
+
+### Main Run
+
+Endpoint separation using all canonical observables: `3.973` standardized units.  
+Endpoint-stable observables at frequency >= 0.75 across both endpoints: `[]`.
+
+| beta | stable obs / 5 | coord mean | margin mean | ambiguous fraction | Poisson-label fraction |
+|---:|---:|---:|---:|---:|---:|
+| 0.0 | 3.188 | -0.735 | 0.735 | 0.000 | 0.000 |
+| 0.1 | 3.312 | -0.470 | 0.470 | 0.000 | 0.000 |
+| 0.2 | 3.312 | -0.232 | 0.232 | 0.125 | 0.000 |
+| 0.3 | 2.500 | -0.054 | 0.070 | 0.875 | 0.250 |
+| 0.4 | 1.625 | +0.075 | 0.083 | 0.812 | 0.875 |
+| 0.5 | 0.750 | +0.260 | 0.260 | 0.000 | 1.000 |
+| 0.6 | 0.188 | +0.374 | 0.374 | 0.000 | 1.000 |
+| 0.7 | 0.500 | +0.520 | 0.520 | 0.000 | 1.000 |
+| 0.8 | 0.250 | +0.570 | 0.570 | 0.000 | 1.000 |
+| 0.9 | 0.250 | +0.692 | 0.692 | 0.000 | 1.000 |
+| 1.0 | 0.125 | +0.721 | 0.721 | 0.000 | 1.000 |
+
+At beta 0.0-0.2, the sequence is classified as GUE-like and retains about
+three stable observables. At beta 0.5-1.0, it is classified as Poisson-like,
+but denominator support is mostly absent. The transition is not centered at
+beta 0.5 in this observable suite. The ambiguous layer is beta 0.3-0.4.
+
+Observable stability frequencies in the main run:
+- beta 0.0: `SR=1.00`, `L1=1.00`, `triple_var=1.00`; `SR2=0.06`, `L2=0.12`;
+- beta 0.3: `SR=1.00`, `L1=0.75`, `triple_var=0.50`;
+- beta 0.4: `SR=0.75`, `L1=0.44`, `triple_var=0.38`;
+- beta 1.0: all canonical observables are weak except one `L2` replicate frequency of `0.12`.
+
+### Seed Check
+
+The lighter seed check repeated the same ambiguous layer:
+- ambiguous beta: `[0.3, 0.4]`;
+- beta 0.3: margin `0.082`, ambiguous fraction `0.917`, stable obs mean `1.250`;
+- beta 0.4: margin `0.125`, ambiguous fraction `0.750`, stable obs mean `0.833`;
+- beta 0.5 and above: Poisson-label fraction `1.000`, ambiguity `0.000`.
+
+## Findings
+
+1. **The clean two-class boundary fails under denominator gating.** Pure GUE and pure Poisson are separable in all-observable space, but there are no observables stable at both endpoints under the declared gate. The Poisson pole is a weak-denominator pole: classification can still place it, but retention-normalized structural claims cannot use it as if it had the same denominator support as GUE.
+
+2. **The operational boundary is a layer, not a line, in this synthetic perimeter.** Both the main run and the seed check isolate beta 0.3-0.4 as the ambiguous layer. In the main run the nearest-centroid margin falls to `0.070-0.083`, while ambiguous fraction rises to `0.812-0.875`. This is the measured form of the "third included" here: not a metaphysical third class, but a beta region where two-class assignment and denominator support are both unstable.
+
+3. **Denominator collapse precedes full Poisson classification.** Stable-observable count drops from about `3.3` at beta 0.1-0.2 to `2.5` at beta 0.3 and `1.625` at beta 0.4. By beta 0.5 the classifier is fully Poisson-labeled, but only `0.750/5` observables remain stable on average. The loss of denominator support is therefore part of the boundary phenomenon, not an after-the-fact nuisance.
+
+4. **The previous META constraints are extended, not replaced.** The 19:41 and 19:55 constraints still hold. This run adds that a boundary claim also needs a layer map: endpoint separability alone can hide the fact that one endpoint has no original-vs-shuffle denominator and that the transition region carries the actual instability.
+
+## Verdict
+**CONSTRAINT on META + BOUNDARY**: GUE/Poisson boundary claims must report:
+
+> observables_registry version + canonical observable list + original-vs-shuffle z gate per observable + endpoint-stable observable set + beta/window layer where classification margin is ambiguous.
+
+Scoped statement from this run:
+
+> In the synthetic mixture perimeter tested here, the GUE-Poisson boundary is an operational layer at beta 0.3-0.4: classification is ambiguous there, and denominator support collapses across the transition. The Poisson endpoint remains classifiable but denominator-weak, so it cannot serve as a symmetric structural pole for gated retention claims.
+
+## Consecutio
+What opens now: apply the same layer map to real domains rather than only synthetic mixtures. For primes, the next discriminating question is not "GUE or Poisson?" but:
+
+> Which scale window has the same signature as the synthetic beta 0.3-0.4 layer: low classifier margin plus falling original-vs-shuffle denominators?
+
+If prime windows show such a layer, BOUNDARY becomes a measurable transition surface. If they do not, the synthetic result remains a calibration constraint on how not to over-read endpoint separability.
+
+## Auto-audit: 5 lenti
+- **L1 hard constraint vs bias**: no zero/always claim. "Endpoint gate is empty" means no observable reached frequency >= 0.75 across both endpoints under `abs(z) >= 2`; it does not mean the observables are identically zero.
+- **L2 quantity vs ratio**: classification margin is reported together with stable-observable count and z-gate frequencies. Ratios are not interpreted without denominator support.
+- **L3 no silent patching**: the claim is explicitly changed from "8 GUE, 5 Poisson boundary" to a synthetic mixture calibration. This does not assert the same layer for primes or all real domains.
+- **L4 edge cases**: beta 0.2 has ambiguous fraction `0.125`, so it is not included in the ambiguous layer. The declared layer requires at least half the replicates ambiguous.
+- **L5 re-discovery**: this is a finite-sample diagnostic of crossover and noisy denominator normalization in classical GUE/Poisson spacings. It is not tagged as a new RMT theorem.
+
+## Files
+- Script: `tools/exp_boundary_mixture_gate.py`
+- Main data: `tools/data/boundary_mixture_gate_20260507_0330.json`
+- Seed check: `tools/data/boundary_mixture_gate_20260507_0330_seedcheck.json`
+- Report: `tools/data/reports/agent_20260507_0330.md`
diff --git a/tools/data/seme.json b/tools/data/seme.json
index 69e2b34538eeecf80c0226452bbe6c9aea44f157..63cbb7016a468ca68fed335ad629291ae277c528
--- a/tools/data/seme.json
+++ b/tools/data/seme.json
@@ -25,6 +25,17 @@
       "added_at": "2026-05-06T19:41:00+00:00"
     },
     {
+      "id": "BOUNDARY_LAYER_GATE",
+      "tipo": "vincolo",
+      "claim": "I claim GUE/Poisson boundary devono riportare layer map: versione observables_registry, lista osservabili canonici, z original-vs-shuffle per osservabile, set endpoint-stable, e finestra/layer con margine classificatorio ambiguo. Nel perimetro sintetico agent_20260507_0330, il confine GUE-Poisson e beta 0.3-0.4: margine 0.070-0.083, ambiguous fraction 0.812-0.875, mentre gli osservabili stabili collassano da ~3.3 a 1.6. Il polo Poisson e classificabile ma denominator-weak.",
+      "intensita": 0.93,
+      "porta": "META_BOUNDARY",
+      "manuale": true,
+      "condensato_ref": "A4,A8,A9,A14,C2",
+      "origine": "cycle agent_20260507_0330: synthetic GUE-Poisson mixture layer gate",
+      "added_at": "2026-05-07T03:30:00+00:00"
+    },
+    {
       "tipo": "confine_inesplorato",
       "id": "TRASCENDENZA_LIMITE",
       "claim": "La trascendenza e il limite attuale del modello. I punti fissi relazionali (non solo phi ma la rete di punti fissi tra osservabili) possono rivelare il vero grafo della realta e pattern nelle matrici. Il confine non e nella matematica - e nel passaggio tra piani.",
@@ -287,4 +298,4 @@
       "timestamp": "2026-04-04T19:34:08.014Z"
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/tools/exp_boundary_mixture_gate.py b/tools/exp_boundary_mixture_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..fcc62a3ba54911760e60cbf1ec299013dff19041
--- /dev/null
+++ b/tools/exp_boundary_mixture_gate.py
@@ -0,0 +1,283 @@
+#!/usr/bin/env python3
+"""
+exp_boundary_mixture_gate.py
+
+Reusable META/BOUNDARY audit for the GUE-Poisson boundary.
+
+Question:
+    Does the GUE/Poisson boundary remain a clean two-class split after the
+    original-vs-shuffle denominator gate, or is the mixed region an operational
+    third state where canonical observables lose stable denominators?
+
+The script uses only canonical observables from observables_registry.py.
+It builds synthetic mixtures by replacing a fraction beta of unfolded GUE
+spacings with Poisson spacings, then measures:
+
+- canonical observable vectors;
+- original-vs-shuffle z-score per observable;
+- endpoint separability in all observables and in gate-stable observables;
+- ambiguity of each beta layer relative to pure GUE and pure Poisson centroids.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
+
+
+OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())
+
+
+def gue_spacings(matrix_size: int, min_spacings: int, rng: np.random.Generator) -> np.ndarray:
+    """Generate unfolded GUE spacings by concatenating independent matrices."""
+    parts: list[np.ndarray] = []
+    edge = max(2, matrix_size // 10)
+    while sum(len(x) for x in parts) < min_spacings:
+        real = rng.standard_normal((matrix_size, matrix_size))
+        imag = rng.standard_normal((matrix_size, matrix_size))
+        h = real + 1j * imag
+        h = (h + h.conj().T) / (2.0 * np.sqrt(matrix_size))
+        eigs = np.sort(np.linalg.eigvalsh(h).real)
+        bulk = eigs[edge:-edge]
+        gaps = np.diff(bulk)
+        mean = float(np.mean(gaps))
+        if mean > 1e-15:
+            parts.append(gaps / mean)
+    return np.concatenate(parts)[:min_spacings].astype(float)
+
+
+def mixture_spacings(gue: np.ndarray, poisson: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    """Return a beta Poisson / (1-beta) GUE spacing sequence with mean spacing 1."""
+    if len(gue) != len(poisson):
+        raise ValueError("gue and poisson arrays must have the same length")
+    mask = rng.random(len(gue)) < beta
+    out = gue.copy()
+    out[mask] = poisson[mask]
+    mean = float(np.mean(out))
+    return out / mean if mean > 1e-15 else out
+
+
+def z_against_shuffle(
+    gaps: np.ndarray,
+    n_baseline: int,
+    rng: np.random.Generator,
+) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
+    """Return original observables, shuffle baseline std, and original-vs-shuffle z."""
+    original = compute_canonical(gaps)
+    baseline_vals = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline_vals[name].append(obs[name])
+
+    std = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline_vals[name], dtype=float)
+        mean = float(np.mean(vals))
+        sd = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, std, z
+
+
+def vector(row: dict, names: list[str]) -> np.ndarray:
+    return np.array([row["observables"][name] for name in names], dtype=float)
+
+
+def classify_layers(rows: list[dict], obs_names: list[str]) -> dict:
+    """Classify each beta layer by standardized distance to endpoint centroids."""
+    if not obs_names:
+        return {
+            "observables": [],
+            "endpoint_distance": 0.0,
+            "layers": {},
+            "ambiguous_beta": [],
+        }
+
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    gue_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    poi_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    all_endpoint = np.vstack([gue_vectors, poi_vectors])
+    scale = np.std(all_endpoint, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    gue_centroid = np.mean(gue_vectors, axis=0)
+    poi_centroid = np.mean(poi_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((poi_centroid - gue_centroid) / scale))
+
+    layers = {}
+    ambiguous_beta = []
+    for beta, beta_rows in sorted(by_beta.items()):
+        coords = []
+        margins = []
+        labels = []
+        for row in beta_rows:
+            x = vector(row, obs_names)
+            d_gue = float(np.linalg.norm((x - gue_centroid) / scale))
+            d_poi = float(np.linalg.norm((x - poi_centroid) / scale))
+            denom = d_gue + d_poi
+            coord = float((d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("gue" if d_gue < d_poi else "poisson")
+        ambiguous_fraction = float(np.mean(np.array(margins) < 0.15))
+        if ambiguous_fraction >= 0.5:
+            ambiguous_beta.append(beta)
+        layers[f"{beta:.3f}"] = {
+            "coordinate_mean": float(np.mean(coords)),
+            "coordinate_std": float(np.std(coords, ddof=1)) if len(coords) > 1 else 0.0,
+            "margin_mean": float(np.mean(margins)),
+            "ambiguous_fraction": ambiguous_fraction,
+            "poisson_label_fraction": float(np.mean(np.array(labels) == "poisson")),
+        }
+
+    return {
+        "observables": obs_names,
+        "endpoint_distance": endpoint_distance,
+        "layers": layers,
+        "ambiguous_beta": ambiguous_beta,
+    }
+
+
+def summarize_gate(rows: list[dict], z_min: float) -> dict:
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    layers = {}
+    for beta, beta_rows in sorted(by_beta.items()):
+        stable_counts = []
+        stable_freq = {name: [] for name in OBS_NAMES}
+        for row in beta_rows:
+            stable = [name for name in OBS_NAMES if abs(row["z"][name]) >= z_min]
+            stable_counts.append(len(stable))
+            for name in OBS_NAMES:
+                stable_freq[name].append(1.0 if name in stable else 0.0)
+        layers[f"{beta:.3f}"] = {
+            "stable_count_mean": float(np.mean(stable_counts)),
+            "stable_count_std": float(np.std(stable_counts, ddof=1)) if len(stable_counts) > 1 else 0.0,
+            "stable_frequency": {name: float(np.mean(vals)) for name, vals in stable_freq.items()},
+        }
+
+    endpoint_stable = []
+    for name in OBS_NAMES:
+        endpoint_rows = by_beta[0.0] + by_beta[1.0]
+        freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in endpoint_rows])
+        if freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def run(args: argparse.Namespace) -> dict:
+    rng = np.random.default_rng(args.seed)
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    rows = []
+
+    for rep in range(args.n_replicates):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        gue = gue_spacings(args.gue_matrix_size, args.n_gaps, rep_rng)
+        poisson = rep_rng.exponential(1.0, size=args.n_gaps)
+        poisson = poisson / float(np.mean(poisson))
+        for beta in betas:
+            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+            gaps = mixture_spacings(gue, poisson, beta, layer_rng)
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                n_baseline=args.n_baseline,
+                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "replicate": rep,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [name for name in OBS_NAMES if abs(z[name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
+
+    output = {
+        "experiment": "boundary_mixture_gate",
+        "question": "Is the GUE-Poisson mixed layer cleanly classifiable after denominator gating?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_endpoint_gated": gated_classification,
+        "rows": rows,
+    }
+
+    out_path = Path(args.out)
+    out_path.parent.mkdir(parents=True, exist_ok=True)
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
+    print(f"observables_used={OBS_NAMES}")
+    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
+    print(
+        "endpoint_distance_all="
+        f"{all_classification['endpoint_distance']:.3f} "
+        "endpoint_distance_gated="
+        f"{gated_classification['endpoint_distance']:.3f}"
+    )
+    if gate["endpoint_stable_observables"]:
+        print("beta stable_count margin_gated ambiguous_gated poisson_fraction_gated")
+    else:
+        print("endpoint gate is empty; printing all-observable classification")
+        print("beta stable_count margin_all ambiguous_all poisson_fraction_all")
+    for beta in betas:
+        key = f"{beta:.3f}"
+        stable_count = gate["layers"][key]["stable_count_mean"]
+        source = gated_classification if gate["endpoint_stable_observables"] else all_classification
+        layer = source["layers"].get(key, {})
+        print(
+            f"{beta:>4.2f} {stable_count:>12.3f} "
+            f"{layer.get('margin_mean', 0.0):>12.3f} "
+            f"{layer.get('ambiguous_fraction', 0.0):>15.3f} "
+            f"{layer.get('poisson_label_fraction', 0.0):>21.3f}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=1536)
+    parser.add_argument("--n-replicates", type=int, default=16)
+    parser.add_argument("--gue-matrix-size", type=int, default=180)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=24)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=20260507)
+    parser.add_argument("--out", default="tools/data/boundary_mixture_gate.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

codex
La validazione JSON è pulita e ho riletto il report contro le 5 lenti. Provo anche il falsifier automatico del lab; se il provider non è disponibile, lo segnalo e lascio comunque l’auto-audit incorporato nel report.
exec
/bin/bash -lc 'python tools/lab_falsifier.py --report tools/data/reports/agent_20260507_0330.md --output tools/data/reports/falsifier_20260507_0330.json --timeout 120' in /opt/MM_D-ND
diff --git a/tools/data/reports/agent_20260507_0330.md b/tools/data/reports/agent_20260507_0330.md
new file mode 100644
index 0000000000000000000000000000000000000000..1bb66d4f31ae12b64e2652ac30c3e475aad25db7
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0330.md
@@ -0,0 +1,109 @@
+# Agent Report — The GUE-Poisson Boundary Is a Denominator Collapse Layer
+**Date**: 2026-05-07 03:30  
+**Piano**: 68  
+**Tension explored**: META + BOUNDARY  
+observables_registry: 1.0.0-2026-05-06  
+observables_used: [SR, SR2, L1, L2, triple_var]
+
+## Claim Under Test
+The last three runs constrained perturbation rank and observable collinearity:
+rank/PC2 claims are not interpretable without the canonical observable registry
+and the original-vs-shuffle denominator gate.
+
+This run does not repeat perturbation rank. It asks:
+
+> If the GUE-Poisson boundary is simulated directly by controlled mixtures,
+> does it behave like a clean two-class split, or like an operational third
+> layer where classification is ambiguous and denominator support collapses?
+
+## Experiment
+Tool created: `tools/exp_boundary_mixture_gate.py`
+
+Atomic perimeter:
+- domains: synthetic unfolded GUE spacings, iid Poisson spacings, and mixtures;
+- mixture parameter: `beta = 0.0..1.0`, where beta is the Poisson replacement fraction;
+- main run: 1,536 spacings, 16 replicates, GUE matrix size 180, 11 beta layers, 24 full-shuffle baselines;
+- seed check: 1,024 spacings, 12 replicates, GUE matrix size 160, same 11 beta layers, 20 baselines;
+- denominator gate: observable is stable when `abs(original - shuffle_mean) / shuffle_std >= 2`;
+- classification: standardized distance to pure GUE and pure Poisson centroids using all five canonical observables. A layer is marked ambiguous when at least half the replicates have nearest-centroid margin `< 0.15`.
+
+The endpoint-gated classifier is intentionally reported. In this perimeter it is empty because the Poisson endpoint has almost no stable original-vs-shuffle denominators. That is not discarded; it is the core META result.
+
+## Results
+
+### Main Run
+
+Endpoint separation using all canonical observables: `3.973` standardized units.  
+Endpoint-stable observables at frequency >= 0.75 across both endpoints: `[]`.
+
+| beta | stable obs / 5 | coord mean | margin mean | ambiguous fraction | Poisson-label fraction |
+|---:|---:|---:|---:|---:|---:|
+| 0.0 | 3.188 | -0.735 | 0.735 | 0.000 | 0.000 |
+| 0.1 | 3.312 | -0.470 | 0.470 | 0.000 | 0.000 |
+| 0.2 | 3.312 | -0.232 | 0.232 | 0.125 | 0.000 |
+| 0.3 | 2.500 | -0.054 | 0.070 | 0.875 | 0.250 |
+| 0.4 | 1.625 | +0.075 | 0.083 | 0.812 | 0.875 |
+| 0.5 | 0.750 | +0.260 | 0.260 | 0.000 | 1.000 |
+| 0.6 | 0.188 | +0.374 | 0.374 | 0.000 | 1.000 |
+| 0.7 | 0.500 | +0.520 | 0.520 | 0.000 | 1.000 |
+| 0.8 | 0.250 | +0.570 | 0.570 | 0.000 | 1.000 |
+| 0.9 | 0.250 | +0.692 | 0.692 | 0.000 | 1.000 |
+| 1.0 | 0.125 | +0.721 | 0.721 | 0.000 | 1.000 |
+
+At beta 0.0-0.2, the sequence is classified as GUE-like and retains about
+three stable observables. At beta 0.5-1.0, it is classified as Poisson-like,
+but denominator support is mostly absent. The transition is not centered at
+beta 0.5 in this observable suite. The ambiguous layer is beta 0.3-0.4.
+
+Observable stability frequencies in the main run:
+- beta 0.0: `SR=1.00`, `L1=1.00`, `triple_var=1.00`; `SR2=0.06`, `L2=0.12`;
+- beta 0.3: `SR=1.00`, `L1=0.75`, `triple_var=0.50`;
+- beta 0.4: `SR=0.75`, `L1=0.44`, `triple_var=0.38`;
+- beta 1.0: all canonical observables are weak except one `L2` replicate frequency of `0.12`.
+
+### Seed Check
+
+The lighter seed check repeated the same ambiguous layer:
+- ambiguous beta: `[0.3, 0.4]`;
+- beta 0.3: margin `0.082`, ambiguous fraction `0.917`, stable obs mean `1.250`;
+- beta 0.4: margin `0.125`, ambiguous fraction `0.750`, stable obs mean `0.833`;
+- beta 0.5 and above: Poisson-label fraction `1.000`, ambiguity `0.000`.
+
+## Findings
+
+1. **The clean two-class boundary fails under denominator gating.** Pure GUE and pure Poisson are separable in all-observable space, but there are no observables stable at both endpoints under the declared gate. The Poisson pole is a weak-denominator pole: classification can still place it, but retention-normalized structural claims cannot use it as if it had the same denominator support as GUE.
+
+2. **The operational boundary is a layer, not a line, in this synthetic perimeter.** Both the main run and the seed check isolate beta 0.3-0.4 as the ambiguous layer. In the main run the nearest-centroid margin falls to `0.070-0.083`, while ambiguous fraction rises to `0.812-0.875`. This is the measured form of the "third included" here: not a metaphysical third class, but a beta region where two-class assignment and denominator support are both unstable.
+
+3. **Denominator collapse precedes full Poisson classification.** Stable-observable count drops from about `3.3` at beta 0.1-0.2 to `2.5` at beta 0.3 and `1.625` at beta 0.4. By beta 0.5 the classifier is fully Poisson-labeled, but only `0.750/5` observables remain stable on average. The loss of denominator support is therefore part of the boundary phenomenon, not an after-the-fact nuisance.
+
+4. **The previous META constraints are extended, not replaced.** The 19:41 and 19:55 constraints still hold. This run adds that a boundary claim also needs a layer map: endpoint separability alone can hide the fact that one endpoint has no original-vs-shuffle denominator and that the transition region carries the actual instability.
+
+## Verdict
+**CONSTRAINT on META + BOUNDARY**: GUE/Poisson boundary claims must report:
+
+> observables_registry version + canonical observable list + original-vs-shuffle z gate per observable + endpoint-stable observable set + beta/window layer where classification margin is ambiguous.
+
+Scoped statement from this run:
+
+> In the synthetic mixture perimeter tested here, the GUE-Poisson boundary is an operational layer at beta 0.3-0.4: classification is ambiguous there, and denominator support collapses across the transition. The Poisson endpoint remains classifiable but denominator-weak, so it cannot serve as a symmetric structural pole for gated retention claims.
+
+## Consecutio
+What opens now: apply the same layer map to real domains rather than only synthetic mixtures. For primes, the next discriminating question is not "GUE or Poisson?" but:
+
+> Which scale window has the same signature as the synthetic beta 0.3-0.4 layer: low classifier margin plus falling original-vs-shuffle denominators?
+
+If prime windows show such a layer, BOUNDARY becomes a measurable transition surface. If they do not, the synthetic result remains a calibration constraint on how not to over-read endpoint separability.
+
+## Auto-audit: 5 lenti
+- **L1 hard constraint vs bias**: no zero/always claim. "Endpoint gate is empty" means no observable reached frequency >= 0.75 across both endpoints under `abs(z) >= 2`; it does not mean the observables are identically zero.
+- **L2 quantity vs ratio**: classification margin is reported together with stable-observable count and z-gate frequencies. Ratios are not interpreted without denominator support.
+- **L3 no silent patching**: the claim is explicitly changed from "8 GUE, 5 Poisson boundary" to a synthetic mixture calibration. This does not assert the same layer for primes or all real domains.
+- **L4 edge cases**: beta 0.2 has ambiguous fraction `0.125`, so it is not included in the ambiguous layer. The declared layer requires at least half the replicates ambiguous.
+- **L5 re-discovery**: this is a finite-sample diagnostic of crossover and noisy denominator normalization in classical GUE/Poisson spacings. It is not tagged as a new RMT theorem.
+
+## Files
+- Script: `tools/exp_boundary_mixture_gate.py`
+- Main data: `tools/data/boundary_mixture_gate_20260507_0330.json`
+- Seed check: `tools/data/boundary_mixture_gate_20260507_0330_seedcheck.json`
+- Report: `tools/data/reports/agent_20260507_0330.md`
diff --git a/tools/data/seme.json b/tools/data/seme.json
index 69e2b34538eeecf80c0226452bbe6c9aea44f157..63cbb7016a468ca68fed335ad629291ae277c528
--- a/tools/data/seme.json
+++ b/tools/data/seme.json
@@ -25,6 +25,17 @@
       "added_at": "2026-05-06T19:41:00+00:00"
     },
     {
+      "id": "BOUNDARY_LAYER_GATE",
+      "tipo": "vincolo",
+      "claim": "I claim GUE/Poisson boundary devono riportare layer map: versione observables_registry, lista osservabili canonici, z original-vs-shuffle per osservabile, set endpoint-stable, e finestra/layer con margine classificatorio ambiguo. Nel perimetro sintetico agent_20260507_0330, il confine GUE-Poisson e beta 0.3-0.4: margine 0.070-0.083, ambiguous fraction 0.812-0.875, mentre gli osservabili stabili collassano da ~3.3 a 1.6. Il polo Poisson e classificabile ma denominator-weak.",
+      "intensita": 0.93,
+      "porta": "META_BOUNDARY",
+      "manuale": true,
+      "condensato_ref": "A4,A8,A9,A14,C2",
+      "origine": "cycle agent_20260507_0330: synthetic GUE-Poisson mixture layer gate",
+      "added_at": "2026-05-07T03:30:00+00:00"
+    },
+    {
       "tipo": "confine_inesplorato",
       "id": "TRASCENDENZA_LIMITE",
       "claim": "La trascendenza e il limite attuale del modello. I punti fissi relazionali (non solo phi ma la rete di punti fissi tra osservabili) possono rivelare il vero grafo della realta e pattern nelle matrici. Il confine non e nella matematica - e nel passaggio tra piani.",
@@ -287,4 +298,4 @@
       "timestamp": "2026-04-04T19:34:08.014Z"
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/tools/exp_boundary_mixture_gate.py b/tools/exp_boundary_mixture_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..fcc62a3ba54911760e60cbf1ec299013dff19041
--- /dev/null
+++ b/tools/exp_boundary_mixture_gate.py
@@ -0,0 +1,283 @@
+#!/usr/bin/env python3
+"""
+exp_boundary_mixture_gate.py
+
+Reusable META/BOUNDARY audit for the GUE-Poisson boundary.
+
+Question:
+    Does the GUE/Poisson boundary remain a clean two-class split after the
+    original-vs-shuffle denominator gate, or is the mixed region an operational
+    third state where canonical observables lose stable denominators?
+
+The script uses only canonical observables from observables_registry.py.
+It builds synthetic mixtures by replacing a fraction beta of unfolded GUE
+spacings with Poisson spacings, then measures:
+
+- canonical observable vectors;
+- original-vs-shuffle z-score per observable;
+- endpoint separability in all observables and in gate-stable observables;
+- ambiguity of each beta layer relative to pure GUE and pure Poisson centroids.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
+
+
+OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())
+
+
+def gue_spacings(matrix_size: int, min_spacings: int, rng: np.random.Generator) -> np.ndarray:
+    """Generate unfolded GUE spacings by concatenating independent matrices."""
+    parts: list[np.ndarray] = []
+    edge = max(2, matrix_size // 10)
+    while sum(len(x) for x in parts) < min_spacings:
+        real = rng.standard_normal((matrix_size, matrix_size))
+        imag = rng.standard_normal((matrix_size, matrix_size))
+        h = real + 1j * imag
+        h = (h + h.conj().T) / (2.0 * np.sqrt(matrix_size))
+        eigs = np.sort(np.linalg.eigvalsh(h).real)
+        bulk = eigs[edge:-edge]
+        gaps = np.diff(bulk)
+        mean = float(np.mean(gaps))
+        if mean > 1e-15:
+            parts.append(gaps / mean)
+    return np.concatenate(parts)[:min_spacings].astype(float)
+
+
+def mixture_spacings(gue: np.ndarray, poisson: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    """Return a beta Poisson / (1-beta) GUE spacing sequence with mean spacing 1."""
+    if len(gue) != len(poisson):
+        raise ValueError("gue and poisson arrays must have the same length")
+    mask = rng.random(len(gue)) < beta
+    out = gue.copy()
+    out[mask] = poisson[mask]
+    mean = float(np.mean(out))
+    return out / mean if mean > 1e-15 else out
+
+
+def z_against_shuffle(
+    gaps: np.ndarray,
+    n_baseline: int,
+    rng: np.random.Generator,
+) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
+    """Return original observables, shuffle baseline std, and original-vs-shuffle z."""
+    original = compute_canonical(gaps)
+    baseline_vals = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline_vals[name].append(obs[name])
+
+    std = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline_vals[name], dtype=float)
+        mean = float(np.mean(vals))
+        sd = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, std, z
+
+
+def vector(row: dict, names: list[str]) -> np.ndarray:
+    return np.array([row["observables"][name] for name in names], dtype=float)
+
+
+def classify_layers(rows: list[dict], obs_names: list[str]) -> dict:
+    """Classify each beta layer by standardized distance to endpoint centroids."""
+    if not obs_names:
+        return {
+            "observables": [],
+            "endpoint_distance": 0.0,
+            "layers": {},
+            "ambiguous_beta": [],
+        }
+
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    gue_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    poi_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    all_endpoint = np.vstack([gue_vectors, poi_vectors])
+    scale = np.std(all_endpoint, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    gue_centroid = np.mean(gue_vectors, axis=0)
+    poi_centroid = np.mean(poi_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((poi_centroid - gue_centroid) / scale))
+
+    layers = {}
+    ambiguous_beta = []
+    for beta, beta_rows in sorted(by_beta.items()):
+        coords = []
+        margins = []
+        labels = []
+        for row in beta_rows:
+            x = vector(row, obs_names)
+            d_gue = float(np.linalg.norm((x - gue_centroid) / scale))
+            d_poi = float(np.linalg.norm((x - poi_centroid) / scale))
+            denom = d_gue + d_poi
+            coord = float((d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("gue" if d_gue < d_poi else "poisson")
+        ambiguous_fraction = float(np.mean(np.array(margins) < 0.15))
+        if ambiguous_fraction >= 0.5:
+            ambiguous_beta.append(beta)
+        layers[f"{beta:.3f}"] = {
+            "coordinate_mean": float(np.mean(coords)),
+            "coordinate_std": float(np.std(coords, ddof=1)) if len(coords) > 1 else 0.0,
+            "margin_mean": float(np.mean(margins)),
+            "ambiguous_fraction": ambiguous_fraction,
+            "poisson_label_fraction": float(np.mean(np.array(labels) == "poisson")),
+        }
+
+    return {
+        "observables": obs_names,
+        "endpoint_distance": endpoint_distance,
+        "layers": layers,
+        "ambiguous_beta": ambiguous_beta,
+    }
+
+
+def summarize_gate(rows: list[dict], z_min: float) -> dict:
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    layers = {}
+    for beta, beta_rows in sorted(by_beta.items()):
+        stable_counts = []
+        stable_freq = {name: [] for name in OBS_NAMES}
+        for row in beta_rows:
+            stable = [name for name in OBS_NAMES if abs(row["z"][name]) >= z_min]
+            stable_counts.append(len(stable))
+            for name in OBS_NAMES:
+                stable_freq[name].append(1.0 if name in stable else 0.0)
+        layers[f"{beta:.3f}"] = {
+            "stable_count_mean": float(np.mean(stable_counts)),
+            "stable_count_std": float(np.std(stable_counts, ddof=1)) if len(stable_counts) > 1 else 0.0,
+            "stable_frequency": {name: float(np.mean(vals)) for name, vals in stable_freq.items()},
+        }
+
+    endpoint_stable = []
+    for name in OBS_NAMES:
+        endpoint_rows = by_beta[0.0] + by_beta[1.0]
+        freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in endpoint_rows])
+        if freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def run(args: argparse.Namespace) -> dict:
+    rng = np.random.default_rng(args.seed)
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    rows = []
+
+    for rep in range(args.n_replicates):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        gue = gue_spacings(args.gue_matrix_size, args.n_gaps, rep_rng)
+        poisson = rep_rng.exponential(1.0, size=args.n_gaps)
+        poisson = poisson / float(np.mean(poisson))
+        for beta in betas:
+            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+            gaps = mixture_spacings(gue, poisson, beta, layer_rng)
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                n_baseline=args.n_baseline,
+                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "replicate": rep,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [name for name in OBS_NAMES if abs(z[name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
+
+    output = {
+        "experiment": "boundary_mixture_gate",
+        "question": "Is the GUE-Poisson mixed layer cleanly classifiable after denominator gating?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_endpoint_gated": gated_classification,
+        "rows": rows,
+    }
+
+    out_path = Path(args.out)
+    out_path.parent.mkdir(parents=True, exist_ok=True)
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
+    print(f"observables_used={OBS_NAMES}")
+    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
+    print(
+        "endpoint_distance_all="
+        f"{all_classification['endpoint_distance']:.3f} "
+        "endpoint_distance_gated="
+        f"{gated_classification['endpoint_distance']:.3f}"
+    )
+    if gate["endpoint_stable_observables"]:
+        print("beta stable_count margin_gated ambiguous_gated poisson_fraction_gated")
+    else:
+        print("endpoint gate is empty; printing all-observable classification")
+        print("beta stable_count margin_all ambiguous_all poisson_fraction_all")
+    for beta in betas:
+        key = f"{beta:.3f}"
+        stable_count = gate["layers"][key]["stable_count_mean"]
+        source = gated_classification if gate["endpoint_stable_observables"] else all_classification
+        layer = source["layers"].get(key, {})
+        print(
+            f"{beta:>4.2f} {stable_count:>12.3f} "
+            f"{layer.get('margin_mean', 0.0):>12.3f} "
+            f"{layer.get('ambiguous_fraction', 0.0):>15.3f} "
+            f"{layer.get('poisson_label_fraction', 0.0):>21.3f}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=1536)
+    parser.add_argument("--n-replicates", type=int, default=16)
+    parser.add_argument("--gue-matrix-size", type=int, default=180)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=24)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=20260507)
+    parser.add_argument("--out", default="tools/data/boundary_mixture_gate.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

diff --git a/tools/data/reports/agent_20260507_0330.md b/tools/data/reports/agent_20260507_0330.md
new file mode 100644
index 0000000000000000000000000000000000000000..1bb66d4f31ae12b64e2652ac30c3e475aad25db7
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0330.md
@@ -0,0 +1,109 @@
+# Agent Report — The GUE-Poisson Boundary Is a Denominator Collapse Layer
+**Date**: 2026-05-07 03:30  
+**Piano**: 68  
+**Tension explored**: META + BOUNDARY  
+observables_registry: 1.0.0-2026-05-06  
+observables_used: [SR, SR2, L1, L2, triple_var]
+
+## Claim Under Test
+The last three runs constrained perturbation rank and observable collinearity:
+rank/PC2 claims are not interpretable without the canonical observable registry
+and the original-vs-shuffle denominator gate.
+
+This run does not repeat perturbation rank. It asks:
+
+> If the GUE-Poisson boundary is simulated directly by controlled mixtures,
+> does it behave like a clean two-class split, or like an operational third
+> layer where classification is ambiguous and denominator support collapses?
+
+## Experiment
+Tool created: `tools/exp_boundary_mixture_gate.py`
+
+Atomic perimeter:
+- domains: synthetic unfolded GUE spacings, iid Poisson spacings, and mixtures;
+- mixture parameter: `beta = 0.0..1.0`, where beta is the Poisson replacement fraction;
+- main run: 1,536 spacings, 16 replicates, GUE matrix size 180, 11 beta layers, 24 full-shuffle baselines;
+- seed check: 1,024 spacings, 12 replicates, GUE matrix size 160, same 11 beta layers, 20 baselines;
+- denominator gate: observable is stable when `abs(original - shuffle_mean) / shuffle_std >= 2`;
+- classification: standardized distance to pure GUE and pure Poisson centroids using all five canonical observables. A layer is marked ambiguous when at least half the replicates have nearest-centroid margin `< 0.15`.
+
+The endpoint-gated classifier is intentionally reported. In this perimeter it is empty because the Poisson endpoint has almost no stable original-vs-shuffle denominators. That is not discarded; it is the core META result.
+
+## Results
+
+### Main Run
+
+Endpoint separation using all canonical observables: `3.973` standardized units.  
+Endpoint-stable observables at frequency >= 0.75 across both endpoints: `[]`.
+
+| beta | stable obs / 5 | coord mean | margin mean | ambiguous fraction | Poisson-label fraction |
+|---:|---:|---:|---:|---:|---:|
+| 0.0 | 3.188 | -0.735 | 0.735 | 0.000 | 0.000 |
+| 0.1 | 3.312 | -0.470 | 0.470 | 0.000 | 0.000 |
+| 0.2 | 3.312 | -0.232 | 0.232 | 0.125 | 0.000 |
+| 0.3 | 2.500 | -0.054 | 0.070 | 0.875 | 0.250 |
+| 0.4 | 1.625 | +0.075 | 0.083 | 0.812 | 0.875 |
+| 0.5 | 0.750 | +0.260 | 0.260 | 0.000 | 1.000 |
+| 0.6 | 0.188 | +0.374 | 0.374 | 0.000 | 1.000 |
+| 0.7 | 0.500 | +0.520 | 0.520 | 0.000 | 1.000 |
+| 0.8 | 0.250 | +0.570 | 0.570 | 0.000 | 1.000 |
+| 0.9 | 0.250 | +0.692 | 0.692 | 0.000 | 1.000 |
+| 1.0 | 0.125 | +0.721 | 0.721 | 0.000 | 1.000 |
+
+At beta 0.0-0.2, the sequence is classified as GUE-like and retains about
+three stable observables. At beta 0.5-1.0, it is classified as Poisson-like,
+but denominator support is mostly absent. The transition is not centered at
+beta 0.5 in this observable suite. The ambiguous layer is beta 0.3-0.4.
+
+Observable stability frequencies in the main run:
+- beta 0.0: `SR=1.00`, `L1=1.00`, `triple_var=1.00`; `SR2=0.06`, `L2=0.12`;
+- beta 0.3: `SR=1.00`, `L1=0.75`, `triple_var=0.50`;
+- beta 0.4: `SR=0.75`, `L1=0.44`, `triple_var=0.38`;
+- beta 1.0: all canonical observables are weak except one `L2` replicate frequency of `0.12`.
+
+### Seed Check
+
+The lighter seed check repeated the same ambiguous layer:
+- ambiguous beta: `[0.3, 0.4]`;
+- beta 0.3: margin `0.082`, ambiguous fraction `0.917`, stable obs mean `1.250`;
+- beta 0.4: margin `0.125`, ambiguous fraction `0.750`, stable obs mean `0.833`;
+- beta 0.5 and above: Poisson-label fraction `1.000`, ambiguity `0.000`.
+
+## Findings
+
+1. **The clean two-class boundary fails under denominator gating.** Pure GUE and pure Poisson are separable in all-observable space, but there are no observables stable at both endpoints under the declared gate. The Poisson pole is a weak-denominator pole: classification can still place it, but retention-normalized structural claims cannot use it as if it had the same denominator support as GUE.
+
+2. **The operational boundary is a layer, not a line, in this synthetic perimeter.** Both the main run and the seed check isolate beta 0.3-0.4 as the ambiguous layer. In the main run the nearest-centroid margin falls to `0.070-0.083`, while ambiguous fraction rises to `0.812-0.875`. This is the measured form of the "third included" here: not a metaphysical third class, but a beta region where two-class assignment and denominator support are both unstable.
+
+3. **Denominator collapse precedes full Poisson classification.** Stable-observable count drops from about `3.3` at beta 0.1-0.2 to `2.5` at beta 0.3 and `1.625` at beta 0.4. By beta 0.5 the classifier is fully Poisson-labeled, but only `0.750/5` observables remain stable on average. The loss of denominator support is therefore part of the boundary phenomenon, not an after-the-fact nuisance.
+
+4. **The previous META constraints are extended, not replaced.** The 19:41 and 19:55 constraints still hold. This run adds that a boundary claim also needs a layer map: endpoint separability alone can hide the fact that one endpoint has no original-vs-shuffle denominator and that the transition region carries the actual instability.
+
+## Verdict
+**CONSTRAINT on META + BOUNDARY**: GUE/Poisson boundary claims must report:
+
+> observables_registry version + canonical observable list + original-vs-shuffle z gate per observable + endpoint-stable observable set + beta/window layer where classification margin is ambiguous.
+
+Scoped statement from this run:
+
+> In the synthetic mixture perimeter tested here, the GUE-Poisson boundary is an operational layer at beta 0.3-0.4: classification is ambiguous there, and denominator support collapses across the transition. The Poisson endpoint remains classifiable but denominator-weak, so it cannot serve as a symmetric structural pole for gated retention claims.
+
+## Consecutio
+What opens now: apply the same layer map to real domains rather than only synthetic mixtures. For primes, the next discriminating question is not "GUE or Poisson?" but:
+
+> Which scale window has the same signature as the synthetic beta 0.3-0.4 layer: low classifier margin plus falling original-vs-shuffle denominators?
+
+If prime windows show such a layer, BOUNDARY becomes a measurable transition surface. If they do not, the synthetic result remains a calibration constraint on how not to over-read endpoint separability.
+
+## Auto-audit: 5 lenti
+- **L1 hard constraint vs bias**: no zero/always claim. "Endpoint gate is empty" means no observable reached frequency >= 0.75 across both endpoints under `abs(z) >= 2`; it does not mean the observables are identically zero.
+- **L2 quantity vs ratio**: classification margin is reported together with stable-observable count and z-gate frequencies. Ratios are not interpreted without denominator support.
+- **L3 no silent patching**: the claim is explicitly changed from "8 GUE, 5 Poisson boundary" to a synthetic mixture calibration. This does not assert the same layer for primes or all real domains.
+- **L4 edge cases**: beta 0.2 has ambiguous fraction `0.125`, so it is not included in the ambiguous layer. The declared layer requires at least half the replicates ambiguous.
+- **L5 re-discovery**: this is a finite-sample diagnostic of crossover and noisy denominator normalization in classical GUE/Poisson spacings. It is not tagged as a new RMT theorem.
+
+## Files
+- Script: `tools/exp_boundary_mixture_gate.py`
+- Main data: `tools/data/boundary_mixture_gate_20260507_0330.json`
+- Seed check: `tools/data/boundary_mixture_gate_20260507_0330_seedcheck.json`
+- Report: `tools/data/reports/agent_20260507_0330.md`
diff --git a/tools/data/seme.json b/tools/data/seme.json
index 69e2b34538eeecf80c0226452bbe6c9aea44f157..63cbb7016a468ca68fed335ad629291ae277c528
--- a/tools/data/seme.json
+++ b/tools/data/seme.json
@@ -25,6 +25,17 @@
       "added_at": "2026-05-06T19:41:00+00:00"
     },
     {
+      "id": "BOUNDARY_LAYER_GATE",
+      "tipo": "vincolo",
+      "claim": "I claim GUE/Poisson boundary devono riportare layer map: versione observables_registry, lista osservabili canonici, z original-vs-shuffle per osservabile, set endpoint-stable, e finestra/layer con margine classificatorio ambiguo. Nel perimetro sintetico agent_20260507_0330, il confine GUE-Poisson e beta 0.3-0.4: margine 0.070-0.083, ambiguous fraction 0.812-0.875, mentre gli osservabili stabili collassano da ~3.3 a 1.6. Il polo Poisson e classificabile ma denominator-weak.",
+      "intensita": 0.93,
+      "porta": "META_BOUNDARY",
+      "manuale": true,
+      "condensato_ref": "A4,A8,A9,A14,C2",
+      "origine": "cycle agent_20260507_0330: synthetic GUE-Poisson mixture layer gate",
+      "added_at": "2026-05-07T03:30:00+00:00"
+    },
+    {
       "tipo": "confine_inesplorato",
       "id": "TRASCENDENZA_LIMITE",
       "claim": "La trascendenza e il limite attuale del modello. I punti fissi relazionali (non solo phi ma la rete di punti fissi tra osservabili) possono rivelare il vero grafo della realta e pattern nelle matrici. Il confine non e nella matematica - e nel passaggio tra piani.",
@@ -287,4 +298,4 @@
       "timestamp": "2026-04-04T19:34:08.014Z"
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/tools/exp_boundary_mixture_gate.py b/tools/exp_boundary_mixture_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..fcc62a3ba54911760e60cbf1ec299013dff19041
--- /dev/null
+++ b/tools/exp_boundary_mixture_gate.py
@@ -0,0 +1,283 @@
+#!/usr/bin/env python3
+"""
+exp_boundary_mixture_gate.py
+
+Reusable META/BOUNDARY audit for the GUE-Poisson boundary.
+
+Question:
+    Does the GUE/Poisson boundary remain a clean two-class split after the
+    original-vs-shuffle denominator gate, or is the mixed region an operational
+    third state where canonical observables lose stable denominators?
+
+The script uses only canonical observables from observables_registry.py.
+It builds synthetic mixtures by replacing a fraction beta of unfolded GUE
+spacings with Poisson spacings, then measures:
+
+- canonical observable vectors;
+- original-vs-shuffle z-score per observable;
+- endpoint separability in all observables and in gate-stable observables;
+- ambiguity of each beta layer relative to pure GUE and pure Poisson centroids.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
+
+
+OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())
+
+
+def gue_spacings(matrix_size: int, min_spacings: int, rng: np.random.Generator) -> np.ndarray:
+    """Generate unfolded GUE spacings by concatenating independent matrices."""
+    parts: list[np.ndarray] = []
+    edge = max(2, matrix_size // 10)
+    while sum(len(x) for x in parts) < min_spacings:
+        real = rng.standard_normal((matrix_size, matrix_size))
+        imag = rng.standard_normal((matrix_size, matrix_size))
+        h = real + 1j * imag
+        h = (h + h.conj().T) / (2.0 * np.sqrt(matrix_size))
+        eigs = np.sort(np.linalg.eigvalsh(h).real)
+        bulk = eigs[edge:-edge]
+        gaps = np.diff(bulk)
+        mean = float(np.mean(gaps))
+        if mean > 1e-15:
+            parts.append(gaps / mean)
+    return np.concatenate(parts)[:min_spacings].astype(float)
+
+
+def mixture_spacings(gue: np.ndarray, poisson: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    """Return a beta Poisson / (1-beta) GUE spacing sequence with mean spacing 1."""
+    if len(gue) != len(poisson):
+        raise ValueError("gue and poisson arrays must have the same length")
+    mask = rng.random(len(gue)) < beta
+    out = gue.copy()
+    out[mask] = poisson[mask]
+    mean = float(np.mean(out))
+    return out / mean if mean > 1e-15 else out
+
+
+def z_against_shuffle(
+    gaps: np.ndarray,
+    n_baseline: int,
+    rng: np.random.Generator,
+) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
+    """Return original observables, shuffle baseline std, and original-vs-shuffle z."""
+    original = compute_canonical(gaps)
+    baseline_vals = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline_vals[name].append(obs[name])
+
+    std = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline_vals[name], dtype=float)
+        mean = float(np.mean(vals))
+        sd = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, std, z
+
+
+def vector(row: dict, names: list[str]) -> np.ndarray:
+    return np.array([row["observables"][name] for name in names], dtype=float)
+
+
+def classify_layers(rows: list[dict], obs_names: list[str]) -> dict:
+    """Classify each beta layer by standardized distance to endpoint centroids."""
+    if not obs_names:
+        return {
+            "observables": [],
+            "endpoint_distance": 0.0,
+            "layers": {},
+            "ambiguous_beta": [],
+        }
+
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    gue_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    poi_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    all_endpoint = np.vstack([gue_vectors, poi_vectors])
+    scale = np.std(all_endpoint, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    gue_centroid = np.mean(gue_vectors, axis=0)
+    poi_centroid = np.mean(poi_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((poi_centroid - gue_centroid) / scale))
+
+    layers = {}
+    ambiguous_beta = []
+    for beta, beta_rows in sorted(by_beta.items()):
+        coords = []
+        margins = []
+        labels = []
+        for row in beta_rows:
+            x = vector(row, obs_names)
+            d_gue = float(np.linalg.norm((x - gue_centroid) / scale))
+            d_poi = float(np.linalg.norm((x - poi_centroid) / scale))
+            denom = d_gue + d_poi
+            coord = float((d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("gue" if d_gue < d_poi else "poisson")
+        ambiguous_fraction = float(np.mean(np.array(margins) < 0.15))
+        if ambiguous_fraction >= 0.5:
+            ambiguous_beta.append(beta)
+        layers[f"{beta:.3f}"] = {
+            "coordinate_mean": float(np.mean(coords)),
+            "coordinate_std": float(np.std(coords, ddof=1)) if len(coords) > 1 else 0.0,
+            "margin_mean": float(np.mean(margins)),
+            "ambiguous_fraction": ambiguous_fraction,
+            "poisson_label_fraction": float(np.mean(np.array(labels) == "poisson")),
+        }
+
+    return {
+        "observables": obs_names,
+        "endpoint_distance": endpoint_distance,
+        "layers": layers,
+        "ambiguous_beta": ambiguous_beta,
+    }
+
+
+def summarize_gate(rows: list[dict], z_min: float) -> dict:
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    layers = {}
+    for beta, beta_rows in sorted(by_beta.items()):
+        stable_counts = []
+        stable_freq = {name: [] for name in OBS_NAMES}
+        for row in beta_rows:
+            stable = [name for name in OBS_NAMES if abs(row["z"][name]) >= z_min]
+            stable_counts.append(len(stable))
+            for name in OBS_NAMES:
+                stable_freq[name].append(1.0 if name in stable else 0.0)
+        layers[f"{beta:.3f}"] = {
+            "stable_count_mean": float(np.mean(stable_counts)),
+            "stable_count_std": float(np.std(stable_counts, ddof=1)) if len(stable_counts) > 1 else 0.0,
+            "stable_frequency": {name: float(np.mean(vals)) for name, vals in stable_freq.items()},
+        }
+
+    endpoint_stable = []
+    for name in OBS_NAMES:
+        endpoint_rows = by_beta[0.0] + by_beta[1.0]
+        freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in endpoint_rows])
+        if freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def run(args: argparse.Namespace) -> dict:
+    rng = np.random.default_rng(args.seed)
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    rows = []
+
+    for rep in range(args.n_replicates):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        gue = gue_spacings(args.gue_matrix_size, args.n_gaps, rep_rng)
+        poisson = rep_rng.exponential(1.0, size=args.n_gaps)
+        poisson = poisson / float(np.mean(poisson))
+        for beta in betas:
+            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+            gaps = mixture_spacings(gue, poisson, beta, layer_rng)
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                n_baseline=args.n_baseline,
+                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "replicate": rep,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [name for name in OBS_NAMES if abs(z[name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
+
+    output = {
+        "experiment": "boundary_mixture_gate",
+        "question": "Is the GUE-Poisson mixed layer cleanly classifiable after denominator gating?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_endpoint_gated": gated_classification,
+        "rows": rows,
+    }
+
+    out_path = Path(args.out)
+    out_path.parent.mkdir(parents=True, exist_ok=True)
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
+    print(f"observables_used={OBS_NAMES}")
+    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
+    print(
+        "endpoint_distance_all="
+        f"{all_classification['endpoint_distance']:.3f} "
+        "endpoint_distance_gated="
+        f"{gated_classification['endpoint_distance']:.3f}"
+    )
+    if gate["endpoint_stable_observables"]:
+        print("beta stable_count margin_gated ambiguous_gated poisson_fraction_gated")
+    else:
+        print("endpoint gate is empty; printing all-observable classification")
+        print("beta stable_count margin_all ambiguous_all poisson_fraction_all")
+    for beta in betas:
+        key = f"{beta:.3f}"
+        stable_count = gate["layers"][key]["stable_count_mean"]
+        source = gated_classification if gate["endpoint_stable_observables"] else all_classification
+        layer = source["layers"].get(key, {})
+        print(
+            f"{beta:>4.2f} {stable_count:>12.3f} "
+            f"{layer.get('margin_mean', 0.0):>12.3f} "
+            f"{layer.get('ambiguous_fraction', 0.0):>15.3f} "
+            f"{layer.get('poisson_label_fraction', 0.0):>21.3f}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=1536)
+    parser.add_argument("--n-replicates", type=int, default=16)
+    parser.add_argument("--gue-matrix-size", type=int, default=180)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=24)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=20260507)
+    parser.add_argument("--out", default="tools/data/boundary_mixture_gate.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

 exited 2 in 30633ms:
      [provider: codex-cli unavailable, next]
      [provider: claude-cli unavailable, next]
      [provider: openrouter unavailable, next]
[falsifier] chain failed: all providers in chain ['codex-cli', 'claude-cli', 'openrouter'] failed (check codex auth, claude auth, OPENROUTER_API_KEY env)
[falsifier] LLM call failed (exit=1). Conservative: BLOCK.

exec
/bin/bash -lc 'ls -l tools/data/reports/falsifier_20260507_0330.json 2>/dev/null || true' in /opt/MM_D-ND
 succeeded in 0ms:
exec
/bin/bash -lc 'git status --short tools/exp_boundary_mixture_gate.py tools/data/boundary_mixture_gate_20260507_0330.json tools/data/boundary_mixture_gate_20260507_0330_seedcheck.json tools/data/reports/agent_20260507_0330.md tools/data/reports/falsifier_20260507_0330.json tools/data/seme.json' in /opt/MM_D-ND
 succeeded in 0ms:
 M tools/data/seme.json
?? tools/data/boundary_mixture_gate_20260507_0330.json
?? tools/data/boundary_mixture_gate_20260507_0330_seedcheck.json
?? tools/data/reports/agent_20260507_0330.md
?? tools/exp_boundary_mixture_gate.py

diff --git a/tools/data/reports/agent_20260507_0330.md b/tools/data/reports/agent_20260507_0330.md
new file mode 100644
index 0000000000000000000000000000000000000000..1bb66d4f31ae12b64e2652ac30c3e475aad25db7
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0330.md
@@ -0,0 +1,109 @@
+# Agent Report — The GUE-Poisson Boundary Is a Denominator Collapse Layer
+**Date**: 2026-05-07 03:30  
+**Piano**: 68  
+**Tension explored**: META + BOUNDARY  
+observables_registry: 1.0.0-2026-05-06  
+observables_used: [SR, SR2, L1, L2, triple_var]
+
+## Claim Under Test
+The last three runs constrained perturbation rank and observable collinearity:
+rank/PC2 claims are not interpretable without the canonical observable registry
+and the original-vs-shuffle denominator gate.
+
+This run does not repeat perturbation rank. It asks:
+
+> If the GUE-Poisson boundary is simulated directly by controlled mixtures,
+> does it behave like a clean two-class split, or like an operational third
+> layer where classification is ambiguous and denominator support collapses?
+
+## Experiment
+Tool created: `tools/exp_boundary_mixture_gate.py`
+
+Atomic perimeter:
+- domains: synthetic unfolded GUE spacings, iid Poisson spacings, and mixtures;
+- mixture parameter: `beta = 0.0..1.0`, where beta is the Poisson replacement fraction;
+- main run: 1,536 spacings, 16 replicates, GUE matrix size 180, 11 beta layers, 24 full-shuffle baselines;
+- seed check: 1,024 spacings, 12 replicates, GUE matrix size 160, same 11 beta layers, 20 baselines;
+- denominator gate: observable is stable when `abs(original - shuffle_mean) / shuffle_std >= 2`;
+- classification: standardized distance to pure GUE and pure Poisson centroids using all five canonical observables. A layer is marked ambiguous when at least half the replicates have nearest-centroid margin `< 0.15`.
+
+The endpoint-gated classifier is intentionally reported. In this perimeter it is empty because the Poisson endpoint has almost no stable original-vs-shuffle denominators. That is not discarded; it is the core META result.
+
+## Results
+
+### Main Run
+
+Endpoint separation using all canonical observables: `3.973` standardized units.  
+Endpoint-stable observables at frequency >= 0.75 across both endpoints: `[]`.
+
+| beta | stable obs / 5 | coord mean | margin mean | ambiguous fraction | Poisson-label fraction |
+|---:|---:|---:|---:|---:|---:|
+| 0.0 | 3.188 | -0.735 | 0.735 | 0.000 | 0.000 |
+| 0.1 | 3.312 | -0.470 | 0.470 | 0.000 | 0.000 |
+| 0.2 | 3.312 | -0.232 | 0.232 | 0.125 | 0.000 |
+| 0.3 | 2.500 | -0.054 | 0.070 | 0.875 | 0.250 |
+| 0.4 | 1.625 | +0.075 | 0.083 | 0.812 | 0.875 |
+| 0.5 | 0.750 | +0.260 | 0.260 | 0.000 | 1.000 |
+| 0.6 | 0.188 | +0.374 | 0.374 | 0.000 | 1.000 |
+| 0.7 | 0.500 | +0.520 | 0.520 | 0.000 | 1.000 |
+| 0.8 | 0.250 | +0.570 | 0.570 | 0.000 | 1.000 |
+| 0.9 | 0.250 | +0.692 | 0.692 | 0.000 | 1.000 |
+| 1.0 | 0.125 | +0.721 | 0.721 | 0.000 | 1.000 |
+
+At beta 0.0-0.2, the sequence is classified as GUE-like and retains about
+three stable observables. At beta 0.5-1.0, it is classified as Poisson-like,
+but denominator support is mostly absent. The transition is not centered at
+beta 0.5 in this observable suite. The ambiguous layer is beta 0.3-0.4.
+
+Observable stability frequencies in the main run:
+- beta 0.0: `SR=1.00`, `L1=1.00`, `triple_var=1.00`; `SR2=0.06`, `L2=0.12`;
+- beta 0.3: `SR=1.00`, `L1=0.75`, `triple_var=0.50`;
+- beta 0.4: `SR=0.75`, `L1=0.44`, `triple_var=0.38`;
+- beta 1.0: all canonical observables are weak except one `L2` replicate frequency of `0.12`.
+
+### Seed Check
+
+The lighter seed check repeated the same ambiguous layer:
+- ambiguous beta: `[0.3, 0.4]`;
+- beta 0.3: margin `0.082`, ambiguous fraction `0.917`, stable obs mean `1.250`;
+- beta 0.4: margin `0.125`, ambiguous fraction `0.750`, stable obs mean `0.833`;
+- beta 0.5 and above: Poisson-label fraction `1.000`, ambiguity `0.000`.
+
+## Findings
+
+1. **The clean two-class boundary fails under denominator gating.** Pure GUE and pure Poisson are separable in all-observable space, but there are no observables stable at both endpoints under the declared gate. The Poisson pole is a weak-denominator pole: classification can still place it, but retention-normalized structural claims cannot use it as if it had the same denominator support as GUE.
+
+2. **The operational boundary is a layer, not a line, in this synthetic perimeter.** Both the main run and the seed check isolate beta 0.3-0.4 as the ambiguous layer. In the main run the nearest-centroid margin falls to `0.070-0.083`, while ambiguous fraction rises to `0.812-0.875`. This is the measured form of the "third included" here: not a metaphysical third class, but a beta region where two-class assignment and denominator support are both unstable.
+
+3. **Denominator collapse precedes full Poisson classification.** Stable-observable count drops from about `3.3` at beta 0.1-0.2 to `2.5` at beta 0.3 and `1.625` at beta 0.4. By beta 0.5 the classifier is fully Poisson-labeled, but only `0.750/5` observables remain stable on average. The loss of denominator support is therefore part of the boundary phenomenon, not an after-the-fact nuisance.
+
+4. **The previous META constraints are extended, not replaced.** The 19:41 and 19:55 constraints still hold. This run adds that a boundary claim also needs a layer map: endpoint separability alone can hide the fact that one endpoint has no original-vs-shuffle denominator and that the transition region carries the actual instability.
+
+## Verdict
+**CONSTRAINT on META + BOUNDARY**: GUE/Poisson boundary claims must report:
+
+> observables_registry version + canonical observable list + original-vs-shuffle z gate per observable + endpoint-stable observable set + beta/window layer where classification margin is ambiguous.
+
+Scoped statement from this run:
+
+> In the synthetic mixture perimeter tested here, the GUE-Poisson boundary is an operational layer at beta 0.3-0.4: classification is ambiguous there, and denominator support collapses across the transition. The Poisson endpoint remains classifiable but denominator-weak, so it cannot serve as a symmetric structural pole for gated retention claims.
+
+## Consecutio
+What opens now: apply the same layer map to real domains rather than only synthetic mixtures. For primes, the next discriminating question is not "GUE or Poisson?" but:
+
+> Which scale window has the same signature as the synthetic beta 0.3-0.4 layer: low classifier margin plus falling original-vs-shuffle denominators?
+
+If prime windows show such a layer, BOUNDARY becomes a measurable transition surface. If they do not, the synthetic result remains a calibration constraint on how not to over-read endpoint separability.
+
+## Auto-audit: 5 lenti
+- **L1 hard constraint vs bias**: no zero/always claim. "Endpoint gate is empty" means no observable reached frequency >= 0.75 across both endpoints under `abs(z) >= 2`; it does not mean the observables are identically zero.
+- **L2 quantity vs ratio**: classification margin is reported together with stable-observable count and z-gate frequencies. Ratios are not interpreted without denominator support.
+- **L3 no silent patching**: the claim is explicitly changed from "8 GUE, 5 Poisson boundary" to a synthetic mixture calibration. This does not assert the same layer for primes or all real domains.
+- **L4 edge cases**: beta 0.2 has ambiguous fraction `0.125`, so it is not included in the ambiguous layer. The declared layer requires at least half the replicates ambiguous.
+- **L5 re-discovery**: this is a finite-sample diagnostic of crossover and noisy denominator normalization in classical GUE/Poisson spacings. It is not tagged as a new RMT theorem.
+
+## Files
+- Script: `tools/exp_boundary_mixture_gate.py`
+- Main data: `tools/data/boundary_mixture_gate_20260507_0330.json`
+- Seed check: `tools/data/boundary_mixture_gate_20260507_0330_seedcheck.json`
+- Report: `tools/data/reports/agent_20260507_0330.md`
diff --git a/tools/data/seme.json b/tools/data/seme.json
index 69e2b34538eeecf80c0226452bbe6c9aea44f157..63cbb7016a468ca68fed335ad629291ae277c528
--- a/tools/data/seme.json
+++ b/tools/data/seme.json
@@ -25,6 +25,17 @@
       "added_at": "2026-05-06T19:41:00+00:00"
     },
     {
+      "id": "BOUNDARY_LAYER_GATE",
+      "tipo": "vincolo",
+      "claim": "I claim GUE/Poisson boundary devono riportare layer map: versione observables_registry, lista osservabili canonici, z original-vs-shuffle per osservabile, set endpoint-stable, e finestra/layer con margine classificatorio ambiguo. Nel perimetro sintetico agent_20260507_0330, il confine GUE-Poisson e beta 0.3-0.4: margine 0.070-0.083, ambiguous fraction 0.812-0.875, mentre gli osservabili stabili collassano da ~3.3 a 1.6. Il polo Poisson e classificabile ma denominator-weak.",
+      "intensita": 0.93,
+      "porta": "META_BOUNDARY",
+      "manuale": true,
+      "condensato_ref": "A4,A8,A9,A14,C2",
+      "origine": "cycle agent_20260507_0330: synthetic GUE-Poisson mixture layer gate",
+      "added_at": "2026-05-07T03:30:00+00:00"
+    },
+    {
       "tipo": "confine_inesplorato",
       "id": "TRASCENDENZA_LIMITE",
       "claim": "La trascendenza e il limite attuale del modello. I punti fissi relazionali (non solo phi ma la rete di punti fissi tra osservabili) possono rivelare il vero grafo della realta e pattern nelle matrici. Il confine non e nella matematica - e nel passaggio tra piani.",
@@ -287,4 +298,4 @@
       "timestamp": "2026-04-04T19:34:08.014Z"
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/tools/exp_boundary_mixture_gate.py b/tools/exp_boundary_mixture_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..fcc62a3ba54911760e60cbf1ec299013dff19041
--- /dev/null
+++ b/tools/exp_boundary_mixture_gate.py
@@ -0,0 +1,283 @@
+#!/usr/bin/env python3
+"""
+exp_boundary_mixture_gate.py
+
+Reusable META/BOUNDARY audit for the GUE-Poisson boundary.
+
+Question:
+    Does the GUE/Poisson boundary remain a clean two-class split after the
+    original-vs-shuffle denominator gate, or is the mixed region an operational
+    third state where canonical observables lose stable denominators?
+
+The script uses only canonical observables from observables_registry.py.
+It builds synthetic mixtures by replacing a fraction beta of unfolded GUE
+spacings with Poisson spacings, then measures:
+
+- canonical observable vectors;
+- original-vs-shuffle z-score per observable;
+- endpoint separability in all observables and in gate-stable observables;
+- ambiguity of each beta layer relative to pure GUE and pure Poisson centroids.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
+
+
+OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())
+
+
+def gue_spacings(matrix_size: int, min_spacings: int, rng: np.random.Generator) -> np.ndarray:
+    """Generate unfolded GUE spacings by concatenating independent matrices."""
+    parts: list[np.ndarray] = []
+    edge = max(2, matrix_size // 10)
+    while sum(len(x) for x in parts) < min_spacings:
+        real = rng.standard_normal((matrix_size, matrix_size))
+        imag = rng.standard_normal((matrix_size, matrix_size))
+        h = real + 1j * imag
+        h = (h + h.conj().T) / (2.0 * np.sqrt(matrix_size))
+        eigs = np.sort(np.linalg.eigvalsh(h).real)
+        bulk = eigs[edge:-edge]
+        gaps = np.diff(bulk)
+        mean = float(np.mean(gaps))
+        if mean > 1e-15:
+            parts.append(gaps / mean)
+    return np.concatenate(parts)[:min_spacings].astype(float)
+
+
+def mixture_spacings(gue: np.ndarray, poisson: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    """Return a beta Poisson / (1-beta) GUE spacing sequence with mean spacing 1."""
+    if len(gue) != len(poisson):
+        raise ValueError("gue and poisson arrays must have the same length")
+    mask = rng.random(len(gue)) < beta
+    out = gue.copy()
+    out[mask] = poisson[mask]
+    mean = float(np.mean(out))
+    return out / mean if mean > 1e-15 else out
+
+
+def z_against_shuffle(
+    gaps: np.ndarray,
+    n_baseline: int,
+    rng: np.random.Generator,
+) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
+    """Return original observables, shuffle baseline std, and original-vs-shuffle z."""
+    original = compute_canonical(gaps)
+    baseline_vals = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline_vals[name].append(obs[name])
+
+    std = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline_vals[name], dtype=float)
+        mean = float(np.mean(vals))
+        sd = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, std, z
+
+
+def vector(row: dict, names: list[str]) -> np.ndarray:
+    return np.array([row["observables"][name] for name in names], dtype=float)
+
+
+def classify_layers(rows: list[dict], obs_names: list[str]) -> dict:
+    """Classify each beta layer by standardized distance to endpoint centroids."""
+    if not obs_names:
+        return {
+            "observables": [],
+            "endpoint_distance": 0.0,
+            "layers": {},
+            "ambiguous_beta": [],
+        }
+
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    gue_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    poi_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    all_endpoint = np.vstack([gue_vectors, poi_vectors])
+    scale = np.std(all_endpoint, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    gue_centroid = np.mean(gue_vectors, axis=0)
+    poi_centroid = np.mean(poi_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((poi_centroid - gue_centroid) / scale))
+
+    layers = {}
+    ambiguous_beta = []
+    for beta, beta_rows in sorted(by_beta.items()):
+        coords = []
+        margins = []
+        labels = []
+        for row in beta_rows:
+            x = vector(row, obs_names)
+            d_gue = float(np.linalg.norm((x - gue_centroid) / scale))
+            d_poi = float(np.linalg.norm((x - poi_centroid) / scale))
+            denom = d_gue + d_poi
+            coord = float((d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("gue" if d_gue < d_poi else "poisson")
+        ambiguous_fraction = float(np.mean(np.array(margins) < 0.15))
+        if ambiguous_fraction >= 0.5:
+            ambiguous_beta.append(beta)
+        layers[f"{beta:.3f}"] = {
+            "coordinate_mean": float(np.mean(coords)),
+            "coordinate_std": float(np.std(coords, ddof=1)) if len(coords) > 1 else 0.0,
+            "margin_mean": float(np.mean(margins)),
+            "ambiguous_fraction": ambiguous_fraction,
+            "poisson_label_fraction": float(np.mean(np.array(labels) == "poisson")),
+        }
+
+    return {
+        "observables": obs_names,
+        "endpoint_distance": endpoint_distance,
+        "layers": layers,
+        "ambiguous_beta": ambiguous_beta,
+    }
+
+
+def summarize_gate(rows: list[dict], z_min: float) -> dict:
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    layers = {}
+    for beta, beta_rows in sorted(by_beta.items()):
+        stable_counts = []
+        stable_freq = {name: [] for name in OBS_NAMES}
+        for row in beta_rows:
+            stable = [name for name in OBS_NAMES if abs(row["z"][name]) >= z_min]
+            stable_counts.append(len(stable))
+            for name in OBS_NAMES:
+                stable_freq[name].append(1.0 if name in stable else 0.0)
+        layers[f"{beta:.3f}"] = {
+            "stable_count_mean": float(np.mean(stable_counts)),
+            "stable_count_std": float(np.std(stable_counts, ddof=1)) if len(stable_counts) > 1 else 0.0,
+            "stable_frequency": {name: float(np.mean(vals)) for name, vals in stable_freq.items()},
+        }
+
+    endpoint_stable = []
+    for name in OBS_NAMES:
+        endpoint_rows = by_beta[0.0] + by_beta[1.0]
+        freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in endpoint_rows])
+        if freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def run(args: argparse.Namespace) -> dict:
+    rng = np.random.default_rng(args.seed)
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    rows = []
+
+    for rep in range(args.n_replicates):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        gue = gue_spacings(args.gue_matrix_size, args.n_gaps, rep_rng)
+        poisson = rep_rng.exponential(1.0, size=args.n_gaps)
+        poisson = poisson / float(np.mean(poisson))
+        for beta in betas:
+            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+            gaps = mixture_spacings(gue, poisson, beta, layer_rng)
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                n_baseline=args.n_baseline,
+                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "replicate": rep,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [name for name in OBS_NAMES if abs(z[name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
+
+    output = {
+        "experiment": "boundary_mixture_gate",
+        "question": "Is the GUE-Poisson mixed layer cleanly classifiable after denominator gating?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_endpoint_gated": gated_classification,
+        "rows": rows,
+    }
+
+    out_path = Path(args.out)
+    out_path.parent.mkdir(parents=True, exist_ok=True)
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
+    print(f"observables_used={OBS_NAMES}")
+    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
+    print(
+        "endpoint_distance_all="
+        f"{all_classification['endpoint_distance']:.3f} "
+        "endpoint_distance_gated="
+        f"{gated_classification['endpoint_distance']:.3f}"
+    )
+    if gate["endpoint_stable_observables"]:
+        print("beta stable_count margin_gated ambiguous_gated poisson_fraction_gated")
+    else:
+        print("endpoint gate is empty; printing all-observable classification")
+        print("beta stable_count margin_all ambiguous_all poisson_fraction_all")
+    for beta in betas:
+        key = f"{beta:.3f}"
+        stable_count = gate["layers"][key]["stable_count_mean"]
+        source = gated_classification if gate["endpoint_stable_observables"] else all_classification
+        layer = source["layers"].get(key, {})
+        print(
+            f"{beta:>4.2f} {stable_count:>12.3f} "
+            f"{layer.get('margin_mean', 0.0):>12.3f} "
+            f"{layer.get('ambiguous_fraction', 0.0):>15.3f} "
+            f"{layer.get('poisson_label_fraction', 0.0):>21.3f}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=1536)
+    parser.add_argument("--n-replicates", type=int, default=16)
+    parser.add_argument("--gue-matrix-size", type=int, default=180)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=24)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=20260507)
+    parser.add_argument("--out", default="tools/data/boundary_mixture_gate.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

codex
Esperimento eseguito e report scritto in [agent_20260507_0330.md](/opt/MM_D-ND/tools/data/reports/agent_20260507_0330.md).

Creati/aggiornati:
- [exp_boundary_mixture_gate.py](/opt/MM_D-ND/tools/exp_boundary_mixture_gate.py)
- [boundary_mixture_gate_20260507_0330.json](/opt/MM_D-ND/tools/data/boundary_mixture_gate_20260507_0330.json)
- [boundary_mixture_gate_20260507_0330_seedcheck.json](/opt/MM_D-ND/tools/data/boundary_mixture_gate_20260507_0330_seedcheck.json)
- [seme.json](/opt/MM_D-ND/tools/data/seme.json) con vincolo `BOUNDARY_LAYER_GATE`

Risultato centrale: nel perimetro sintetico GUE-Poisson, il boundary non si comporta come linea netta ma come layer operativo beta `0.3-0.4`, dove il margine classificatorio collassa e gli osservabili stabili scendono da circa `3.3/5` a `1.6/5`.

Verifiche: `py_compile` ok, `seme.json` validato con `json.tool`. Il falsifier automatico non ha potuto girare perché tutti i provider LLM risultano non disponibili; l’auto-audit a 5 lenti è incluso nel report.
diff --git a/tools/data/reports/agent_20260507_0330.md b/tools/data/reports/agent_20260507_0330.md
new file mode 100644
index 0000000000000000000000000000000000000000..1bb66d4f31ae12b64e2652ac30c3e475aad25db7
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0330.md
@@ -0,0 +1,109 @@
+# Agent Report — The GUE-Poisson Boundary Is a Denominator Collapse Layer
+**Date**: 2026-05-07 03:30  
+**Piano**: 68  
+**Tension explored**: META + BOUNDARY  
+observables_registry: 1.0.0-2026-05-06  
+observables_used: [SR, SR2, L1, L2, triple_var]
+
+## Claim Under Test
+The last three runs constrained perturbation rank and observable collinearity:
+rank/PC2 claims are not interpretable without the canonical observable registry
+and the original-vs-shuffle denominator gate.
+
+This run does not repeat perturbation rank. It asks:
+
+> If the GUE-Poisson boundary is simulated directly by controlled mixtures,
+> does it behave like a clean two-class split, or like an operational third
+> layer where classification is ambiguous and denominator support collapses?
+
+## Experiment
+Tool created: `tools/exp_boundary_mixture_gate.py`
+
+Atomic perimeter:
+- domains: synthetic unfolded GUE spacings, iid Poisson spacings, and mixtures;
+- mixture parameter: `beta = 0.0..1.0`, where beta is the Poisson replacement fraction;
+- main run: 1,536 spacings, 16 replicates, GUE matrix size 180, 11 beta layers, 24 full-shuffle baselines;
+- seed check: 1,024 spacings, 12 replicates, GUE matrix size 160, same 11 beta layers, 20 baselines;
+- denominator gate: observable is stable when `abs(original - shuffle_mean) / shuffle_std >= 2`;
+- classification: standardized distance to pure GUE and pure Poisson centroids using all five canonical observables. A layer is marked ambiguous when at least half the replicates have nearest-centroid margin `< 0.15`.
+
+The endpoint-gated classifier is intentionally reported. In this perimeter it is empty because the Poisson endpoint has almost no stable original-vs-shuffle denominators. That is not discarded; it is the core META result.
+
+## Results
+
+### Main Run
+
+Endpoint separation using all canonical observables: `3.973` standardized units.  
+Endpoint-stable observables at frequency >= 0.75 across both endpoints: `[]`.
+
+| beta | stable obs / 5 | coord mean | margin mean | ambiguous fraction | Poisson-label fraction |
+|---:|---:|---:|---:|---:|---:|
+| 0.0 | 3.188 | -0.735 | 0.735 | 0.000 | 0.000 |
+| 0.1 | 3.312 | -0.470 | 0.470 | 0.000 | 0.000 |
+| 0.2 | 3.312 | -0.232 | 0.232 | 0.125 | 0.000 |
+| 0.3 | 2.500 | -0.054 | 0.070 | 0.875 | 0.250 |
+| 0.4 | 1.625 | +0.075 | 0.083 | 0.812 | 0.875 |
+| 0.5 | 0.750 | +0.260 | 0.260 | 0.000 | 1.000 |
+| 0.6 | 0.188 | +0.374 | 0.374 | 0.000 | 1.000 |
+| 0.7 | 0.500 | +0.520 | 0.520 | 0.000 | 1.000 |
+| 0.8 | 0.250 | +0.570 | 0.570 | 0.000 | 1.000 |
+| 0.9 | 0.250 | +0.692 | 0.692 | 0.000 | 1.000 |
+| 1.0 | 0.125 | +0.721 | 0.721 | 0.000 | 1.000 |
+
+At beta 0.0-0.2, the sequence is classified as GUE-like and retains about
+three stable observables. At beta 0.5-1.0, it is classified as Poisson-like,
+but denominator support is mostly absent. The transition is not centered at
+beta 0.5 in this observable suite. The ambiguous layer is beta 0.3-0.4.
+
+Observable stability frequencies in the main run:
+- beta 0.0: `SR=1.00`, `L1=1.00`, `triple_var=1.00`; `SR2=0.06`, `L2=0.12`;
+- beta 0.3: `SR=1.00`, `L1=0.75`, `triple_var=0.50`;
+- beta 0.4: `SR=0.75`, `L1=0.44`, `triple_var=0.38`;
+- beta 1.0: all canonical observables are weak except one `L2` replicate frequency of `0.12`.
+
+### Seed Check
+
+The lighter seed check repeated the same ambiguous layer:
+- ambiguous beta: `[0.3, 0.4]`;
+- beta 0.3: margin `0.082`, ambiguous fraction `0.917`, stable obs mean `1.250`;
+- beta 0.4: margin `0.125`, ambiguous fraction `0.750`, stable obs mean `0.833`;
+- beta 0.5 and above: Poisson-label fraction `1.000`, ambiguity `0.000`.
+
+## Findings
+
+1. **The clean two-class boundary fails under denominator gating.** Pure GUE and pure Poisson are separable in all-observable space, but there are no observables stable at both endpoints under the declared gate. The Poisson pole is a weak-denominator pole: classification can still place it, but retention-normalized structural claims cannot use it as if it had the same denominator support as GUE.
+
+2. **The operational boundary is a layer, not a line, in this synthetic perimeter.** Both the main run and the seed check isolate beta 0.3-0.4 as the ambiguous layer. In the main run the nearest-centroid margin falls to `0.070-0.083`, while ambiguous fraction rises to `0.812-0.875`. This is the measured form of the "third included" here: not a metaphysical third class, but a beta region where two-class assignment and denominator support are both unstable.
+
+3. **Denominator collapse precedes full Poisson classification.** Stable-observable count drops from about `3.3` at beta 0.1-0.2 to `2.5` at beta 0.3 and `1.625` at beta 0.4. By beta 0.5 the classifier is fully Poisson-labeled, but only `0.750/5` observables remain stable on average. The loss of denominator support is therefore part of the boundary phenomenon, not an after-the-fact nuisance.
+
+4. **The previous META constraints are extended, not replaced.** The 19:41 and 19:55 constraints still hold. This run adds that a boundary claim also needs a layer map: endpoint separability alone can hide the fact that one endpoint has no original-vs-shuffle denominator and that the transition region carries the actual instability.
+
+## Verdict
+**CONSTRAINT on META + BOUNDARY**: GUE/Poisson boundary claims must report:
+
+> observables_registry version + canonical observable list + original-vs-shuffle z gate per observable + endpoint-stable observable set + beta/window layer where classification margin is ambiguous.
+
+Scoped statement from this run:
+
+> In the synthetic mixture perimeter tested here, the GUE-Poisson boundary is an operational layer at beta 0.3-0.4: classification is ambiguous there, and denominator support collapses across the transition. The Poisson endpoint remains classifiable but denominator-weak, so it cannot serve as a symmetric structural pole for gated retention claims.
+
+## Consecutio
+What opens now: apply the same layer map to real domains rather than only synthetic mixtures. For primes, the next discriminating question is not "GUE or Poisson?" but:
+
+> Which scale window has the same signature as the synthetic beta 0.3-0.4 layer: low classifier margin plus falling original-vs-shuffle denominators?
+
+If prime windows show such a layer, BOUNDARY becomes a measurable transition surface. If they do not, the synthetic result remains a calibration constraint on how not to over-read endpoint separability.
+
+## Auto-audit: 5 lenti
+- **L1 hard constraint vs bias**: no zero/always claim. "Endpoint gate is empty" means no observable reached frequency >= 0.75 across both endpoints under `abs(z) >= 2`; it does not mean the observables are identically zero.
+- **L2 quantity vs ratio**: classification margin is reported together with stable-observable count and z-gate frequencies. Ratios are not interpreted without denominator support.
+- **L3 no silent patching**: the claim is explicitly changed from "8 GUE, 5 Poisson boundary" to a synthetic mixture calibration. This does not assert the same layer for primes or all real domains.
+- **L4 edge cases**: beta 0.2 has ambiguous fraction `0.125`, so it is not included in the ambiguous layer. The declared layer requires at least half the replicates ambiguous.
+- **L5 re-discovery**: this is a finite-sample diagnostic of crossover and noisy denominator normalization in classical GUE/Poisson spacings. It is not tagged as a new RMT theorem.
+
+## Files
+- Script: `tools/exp_boundary_mixture_gate.py`
+- Main data: `tools/data/boundary_mixture_gate_20260507_0330.json`
+- Seed check: `tools/data/boundary_mixture_gate_20260507_0330_seedcheck.json`
+- Report: `tools/data/reports/agent_20260507_0330.md`
diff --git a/tools/data/seme.json b/tools/data/seme.json
index 69e2b34538eeecf80c0226452bbe6c9aea44f157..63cbb7016a468ca68fed335ad629291ae277c528
--- a/tools/data/seme.json
+++ b/tools/data/seme.json
@@ -25,6 +25,17 @@
       "added_at": "2026-05-06T19:41:00+00:00"
     },
     {
+      "id": "BOUNDARY_LAYER_GATE",
+      "tipo": "vincolo",
+      "claim": "I claim GUE/Poisson boundary devono riportare layer map: versione observables_registry, lista osservabili canonici, z original-vs-shuffle per osservabile, set endpoint-stable, e finestra/layer con margine classificatorio ambiguo. Nel perimetro sintetico agent_20260507_0330, il confine GUE-Poisson e beta 0.3-0.4: margine 0.070-0.083, ambiguous fraction 0.812-0.875, mentre gli osservabili stabili collassano da ~3.3 a 1.6. Il polo Poisson e classificabile ma denominator-weak.",
+      "intensita": 0.93,
+      "porta": "META_BOUNDARY",
+      "manuale": true,
+      "condensato_ref": "A4,A8,A9,A14,C2",
+      "origine": "cycle agent_20260507_0330: synthetic GUE-Poisson mixture layer gate",
+      "added_at": "2026-05-07T03:30:00+00:00"
+    },
+    {
       "tipo": "confine_inesplorato",
       "id": "TRASCENDENZA_LIMITE",
       "claim": "La trascendenza e il limite attuale del modello. I punti fissi relazionali (non solo phi ma la rete di punti fissi tra osservabili) possono rivelare il vero grafo della realta e pattern nelle matrici. Il confine non e nella matematica - e nel passaggio tra piani.",
@@ -287,4 +298,4 @@
       "timestamp": "2026-04-04T19:34:08.014Z"
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/tools/exp_boundary_mixture_gate.py b/tools/exp_boundary_mixture_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..fcc62a3ba54911760e60cbf1ec299013dff19041
--- /dev/null
+++ b/tools/exp_boundary_mixture_gate.py
@@ -0,0 +1,283 @@
+#!/usr/bin/env python3
+"""
+exp_boundary_mixture_gate.py
+
+Reusable META/BOUNDARY audit for the GUE-Poisson boundary.
+
+Question:
+    Does the GUE/Poisson boundary remain a clean two-class split after the
+    original-vs-shuffle denominator gate, or is the mixed region an operational
+    third state where canonical observables lose stable denominators?
+
+The script uses only canonical observables from observables_registry.py.
+It builds synthetic mixtures by replacing a fraction beta of unfolded GUE
+spacings with Poisson spacings, then measures:
+
+- canonical observable vectors;
+- original-vs-shuffle z-score per observable;
+- endpoint separability in all observables and in gate-stable observables;
+- ambiguity of each beta layer relative to pure GUE and pure Poisson centroids.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
+
+
+OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())
+
+
+def gue_spacings(matrix_size: int, min_spacings: int, rng: np.random.Generator) -> np.ndarray:
+    """Generate unfolded GUE spacings by concatenating independent matrices."""
+    parts: list[np.ndarray] = []
+    edge = max(2, matrix_size // 10)
+    while sum(len(x) for x in parts) < min_spacings:
+        real = rng.standard_normal((matrix_size, matrix_size))
+        imag = rng.standard_normal((matrix_size, matrix_size))
+        h = real + 1j * imag
+        h = (h + h.conj().T) / (2.0 * np.sqrt(matrix_size))
+        eigs = np.sort(np.linalg.eigvalsh(h).real)
+        bulk = eigs[edge:-edge]
+        gaps = np.diff(bulk)
+        mean = float(np.mean(gaps))
+        if mean > 1e-15:
+            parts.append(gaps / mean)
+    return np.concatenate(parts)[:min_spacings].astype(float)
+
+
+def mixture_spacings(gue: np.ndarray, poisson: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    """Return a beta Poisson / (1-beta) GUE spacing sequence with mean spacing 1."""
+    if len(gue) != len(poisson):
+        raise ValueError("gue and poisson arrays must have the same length")
+    mask = rng.random(len(gue)) < beta
+    out = gue.copy()
+    out[mask] = poisson[mask]
+    mean = float(np.mean(out))
+    return out / mean if mean > 1e-15 else out
+
+
+def z_against_shuffle(
+    gaps: np.ndarray,
+    n_baseline: int,
+    rng: np.random.Generator,
+) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
+    """Return original observables, shuffle baseline std, and original-vs-shuffle z."""
+    original = compute_canonical(gaps)
+    baseline_vals = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline_vals[name].append(obs[name])
+
+    std = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline_vals[name], dtype=float)
+        mean = float(np.mean(vals))
+        sd = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, std, z
+
+
+def vector(row: dict, names: list[str]) -> np.ndarray:
+    return np.array([row["observables"][name] for name in names], dtype=float)
+
+
+def classify_layers(rows: list[dict], obs_names: list[str]) -> dict:
+    """Classify each beta layer by standardized distance to endpoint centroids."""
+    if not obs_names:
+        return {
+            "observables": [],
+            "endpoint_distance": 0.0,
+            "layers": {},
+            "ambiguous_beta": [],
+        }
+
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    gue_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    poi_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    all_endpoint = np.vstack([gue_vectors, poi_vectors])
+    scale = np.std(all_endpoint, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    gue_centroid = np.mean(gue_vectors, axis=0)
+    poi_centroid = np.mean(poi_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((poi_centroid - gue_centroid) / scale))
+
+    layers = {}
+    ambiguous_beta = []
+    for beta, beta_rows in sorted(by_beta.items()):
+        coords = []
+        margins = []
+        labels = []
+        for row in beta_rows:
+            x = vector(row, obs_names)
+            d_gue = float(np.linalg.norm((x - gue_centroid) / scale))
+            d_poi = float(np.linalg.norm((x - poi_centroid) / scale))
+            denom = d_gue + d_poi
+            coord = float((d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("gue" if d_gue < d_poi else "poisson")
+        ambiguous_fraction = float(np.mean(np.array(margins) < 0.15))
+        if ambiguous_fraction >= 0.5:
+            ambiguous_beta.append(beta)
+        layers[f"{beta:.3f}"] = {
+            "coordinate_mean": float(np.mean(coords)),
+            "coordinate_std": float(np.std(coords, ddof=1)) if len(coords) > 1 else 0.0,
+            "margin_mean": float(np.mean(margins)),
+            "ambiguous_fraction": ambiguous_fraction,
+            "poisson_label_fraction": float(np.mean(np.array(labels) == "poisson")),
+        }
+
+    return {
+        "observables": obs_names,
+        "endpoint_distance": endpoint_distance,
+        "layers": layers,
+        "ambiguous_beta": ambiguous_beta,
+    }
+
+
+def summarize_gate(rows: list[dict], z_min: float) -> dict:
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    layers = {}
+    for beta, beta_rows in sorted(by_beta.items()):
+        stable_counts = []
+        stable_freq = {name: [] for name in OBS_NAMES}
+        for row in beta_rows:
+            stable = [name for name in OBS_NAMES if abs(row["z"][name]) >= z_min]
+            stable_counts.append(len(stable))
+            for name in OBS_NAMES:
+                stable_freq[name].append(1.0 if name in stable else 0.0)
+        layers[f"{beta:.3f}"] = {
+            "stable_count_mean": float(np.mean(stable_counts)),
+            "stable_count_std": float(np.std(stable_counts, ddof=1)) if len(stable_counts) > 1 else 0.0,
+            "stable_frequency": {name: float(np.mean(vals)) for name, vals in stable_freq.items()},
+        }
+
+    endpoint_stable = []
+    for name in OBS_NAMES:
+        endpoint_rows = by_beta[0.0] + by_beta[1.0]
+        freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in endpoint_rows])
+        if freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def run(args: argparse.Namespace) -> dict:
+    rng = np.random.default_rng(args.seed)
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    rows = []
+
+    for rep in range(args.n_replicates):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        gue = gue_spacings(args.gue_matrix_size, args.n_gaps, rep_rng)
+        poisson = rep_rng.exponential(1.0, size=args.n_gaps)
+        poisson = poisson / float(np.mean(poisson))
+        for beta in betas:
+            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+            gaps = mixture_spacings(gue, poisson, beta, layer_rng)
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                n_baseline=args.n_baseline,
+                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "replicate": rep,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [name for name in OBS_NAMES if abs(z[name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
+
+    output = {
+        "experiment": "boundary_mixture_gate",
+        "question": "Is the GUE-Poisson mixed layer cleanly classifiable after denominator gating?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_endpoint_gated": gated_classification,
+        "rows": rows,
+    }
+
+    out_path = Path(args.out)
+    out_path.parent.mkdir(parents=True, exist_ok=True)
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
+    print(f"observables_used={OBS_NAMES}")
+    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
+    print(
+        "endpoint_distance_all="
+        f"{all_classification['endpoint_distance']:.3f} "
+        "endpoint_distance_gated="
+        f"{gated_classification['endpoint_distance']:.3f}"
+    )
+    if gate["endpoint_stable_observables"]:
+        print("beta stable_count margin_gated ambiguous_gated poisson_fraction_gated")
+    else:
+        print("endpoint gate is empty; printing all-observable classification")
+        print("beta stable_count margin_all ambiguous_all poisson_fraction_all")
+    for beta in betas:
+        key = f"{beta:.3f}"
+        stable_count = gate["layers"][key]["stable_count_mean"]
+        source = gated_classification if gate["endpoint_stable_observables"] else all_classification
+        layer = source["layers"].get(key, {})
+        print(
+            f"{beta:>4.2f} {stable_count:>12.3f} "
+            f"{layer.get('margin_mean', 0.0):>12.3f} "
+            f"{layer.get('ambiguous_fraction', 0.0):>15.3f} "
+            f"{layer.get('poisson_label_fraction', 0.0):>21.3f}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=1536)
+    parser.add_argument("--n-replicates", type=int, default=16)
+    parser.add_argument("--gue-matrix-size", type=int, default=180)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=24)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=20260507)
+    parser.add_argument("--out", default="tools/data/boundary_mixture_gate.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

tokens used
132,372
Esperimento eseguito e report scritto in [agent_20260507_0330.md](/opt/MM_D-ND/tools/data/reports/agent_20260507_0330.md).

Creati/aggiornati:
- [exp_boundary_mixture_gate.py](/opt/MM_D-ND/tools/exp_boundary_mixture_gate.py)
- [boundary_mixture_gate_20260507_0330.json](/opt/MM_D-ND/tools/data/boundary_mixture_gate_20260507_0330.json)
- [boundary_mixture_gate_20260507_0330_seedcheck.json](/opt/MM_D-ND/tools/data/boundary_mixture_gate_20260507_0330_seedcheck.json)
- [seme.json](/opt/MM_D-ND/tools/data/seme.json) con vincolo `BOUNDARY_LAYER_GATE`

Risultato centrale: nel perimetro sintetico GUE-Poisson, il boundary non si comporta come linea netta ma come layer operativo beta `0.3-0.4`, dove il margine classificatorio collassa e gli osservabili stabili scendono da circa `3.3/5` a `1.6/5`.

Verifiche: `py_compile` ok, `seme.json` validato con `json.tool`. Il falsifier automatico non ha potuto girare perché tutti i provider LLM risultano non disponibili; l’auto-audit a 5 lenti è incluso nel report.
