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: 019e01ac-1d01-72b1-81b6-75101f05e10e
--------
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_0901.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 campo vivo, il contesto del laboratorio e le memorie condivise recenti, poi scelgo o applico la tensione secondo la direttiva attiva. Verifico anche che non siano presenti API key pagate prima di avviare qualunque esperimento.
exec
/bin/bash -lc "sed -n '1,240p' 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 (?s) — parti dalla consecutio.

## Piano 70 — Costruire una matrice di trasferibilita' del denominator gate: applicarlo a 3-4 perimetri non-BOUNDA

## 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
- [BOUNDARY_LAYER_GATE] (0.93) I claim GUE/Poisson boundary devono riportare layer map: versione observables_registry, lista osservabili canonici, z original-vs-shuffle per osservab
- [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 

## 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
  "poisson" → PERTURBATION_DENOMINATOR_GATE, BOUNDARY, BOUNDARY_LAYER_GATE
  "producono" → PERTURBATION_DENOMINATOR_GATE, DUALITA_DIPOLARE_VS_ILLUSORIA, TENSIONE_ENTITA
  "confine" → BOUNDARY, TRASCENDENZA_LIMITE, BOUNDARY_LAYER_GATE
  "devono" → OBSERVABLE_REGISTRY, BOUNDARY_LAYER_GATE
  "perimetro" → PERTURBATION_DENOMINATOR_GATE, BOUNDARY_LAYER_GATE
Questo è dove il potenziale si concentra. Non ignorarlo.

## Ultimi 3 run — da dove parti
### Agent Report - Denominator Gate Transfers, Boundary Coordinate Does Not
Trovato: 1. **The gate does not degenerate on DUALITA.** The dipolar endpoint has three
stable canonical observables with large original-vs-shuffle denominators
(`SR`, `L1`, `triple_var`, mean abs z about `36-41` in the main run). The
illusory endpoint has no stable denominator support. This is a structural

Verdetto: **category: gate_transferability**  
**verdict: operator**

Scoped statement:

> In this synthetic DUALITA perimeter, the denominator gate is transfer

### Agent Report — The GUE-Poisson Boundary Is a Denominator Collapse Layer
Trovato: 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 rete
Verdetto: **CONSTRAINT on META + BOUNDARY**: GUE/Poisson boundary claims must report:

> observables_registry version + canonical observable list + original-vs-

### 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

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_20260507_0330 (2 ghost): The GUE-Poisson Boundary Is a Denominator Collapse Layer
  report_20260506_1955 (2 ghost): Observable Collinearity Breaks Only Where Denominators Are Weak
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

exec
/bin/bash -lc "env | grep -E 'ANTHROPIC_API_KEY|OPENAI_API_KEY' || true" in /opt/MM_D-ND
 succeeded in 0ms:
exec
/bin/bash -lc 'ls -t shared_memory/*.md 2>/dev/null | head -10' in /opt/MM_D-ND
 succeeded in 0ms:
shared_memory/regola_cambia_la_parola_non_il_valore_2026-05-07.md
shared_memory/feedback_indeterminato_reitera_con_sistema_2026-05-07.md
shared_memory/README.md

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".


codex
Il campo punta ancora al denominator gate fuori da BOUNDARY: il run precedente ha aperto DUALITA, ma il piano chiede una matrice di trasferibilita su piu perimetri. Ora leggo seme e memoria cross-agent, poi costruisco un esperimento riusabile invece di una conferma locale.
exec
/bin/bash -lc "sed -n '1,220p' shared_memory/regola_cambia_la_parola_non_il_valore_2026-05-07.md" in /opt/MM_D-ND
 succeeded in 0ms:
---
type: regola
author: operatore (cristallizzato da TM3)
date: 2026-05-07
scope: cross-agent
priority: high
---

# Cambia la parola, non il valore

**Regola autologica permanente** (operatore, 2026-05-07):

> *"se magnitude non funziona significa che serve una nuova parola, non
> possiamo stare lì a calibrare un valore, non è autologico, il valore si
> determina corretto se la combo che lo produce è corretta. Domandati cosa
> faresti in quel punto del sistema e poi crea quello che serve per farlo
> uguale o meglio in modo solido ma dinamico, se il sistema non si adatta
> restando immutabile si rompe per inezia o somiglianza simmetrica."*

## Why

Calibrare un valore (alzare/abbassare una soglia, raffinare una metrica) è
**det=+1**: toppa sul presente. Il valore "non funziona" non perché è
mal-calibrato, ma perché la **parola che lo classifica è insufficiente**
— non riconosce una distinzione che il sistema sta già facendo nei dati.

La parola corretta emerge dalla **combo** (condizioni → atti → risultanti)
che la produce. Se la combo ha 3 tipi distinti di risultante e tu ne
classifichi solo 2, il valore di uno dei due sarà sempre "rotto". Non per
calibrazione: per **mancanza di parola**.

Esempio operativo (2026-05-07): cycle 0803 ha verdict=operator (scoperta
strutturale) MA valutatore=REDESIGN (cambio direzione). SSP pipeline
classifica solo CRYSTALLIZE come scoperta valida → cycle 0803 cade in
`pre_discovery`. Calibrare la soglia CRYSTALLIZE non risolve. Serve
**parola nuova**: `provisional_discovery` (scoperta + consecutio aperta).

## How to apply

Quando un valore/metrica/soglia "non funziona":

1. **Ferma**: non ricalibrare il valore.
2. **Risali**: qual è la combo (condizioni → atto → risultante) che lo
   produce? Quante risultanti distinte produce davvero?
3. **Conta le parole**: quante categorie ha il pipeline che lo legge?
4. **Se distinzioni > parole**: non manca calibrazione, manca **parola**.
   Crea la parola nuova. Aggiungi la categoria.
5. **Test**: la nuova parola classifica in modo solido E dinamico — il
   sistema può scoprire altre distinzioni domani senza riscrivere il
   pipeline.

## Anti-pattern

- Spostare la soglia (CRYSTALLIZE_high → CRYSTALLIZE_medium) per "fare
  passare di più" — produce drift, non chiarezza
- Aggiungere un branch ad-hoc nel codice che fa il caso eccezionale —
  produce somiglianza simmetrica (la regola dice una cosa, il branch
  dice altro)
- "Calibrazione fine" come strategia di lungo termine — det=+1
  cumulativo. Il sistema diventa fragile per inerzia.

## Pattern di costruzione

> "Domandati cosa faresti in quel punto del sistema e poi crea quello
> che serve per farlo uguale o meglio in modo solido ma dinamico"

Sequenza:
1. Identifica il punto del sistema dove la rigidità si manifesta
2. Domanda: cosa farei *io* qui se decidessi caso per caso?
3. Estrai il principio (non la decisione caso-specifica)
4. Crea lo strumento che applica il principio in modo dinamico
5. Verifica che il sistema possa adattarsi a nuove distinzioni senza
   modifica al codice — solo a dati/parole/categorie

## Solido ma dinamico

- **Solido**: il pipeline non si rompe quando incontra un caso nuovo.
  Default sicuro, fallback gracefully.
- **Dinamico**: il pipeline può accogliere nuove parole/categorie senza
  refactor. La struttura accetta evoluzione del vocabolario.

Se il sistema è solido ma rigido → si rompe per inerzia (somiglianza
simmetrica con il passato che non c'è più).
Se il sistema è dinamico ma fragile → si rompe per il primo edge case.
Se il sistema è solido E dinamico → evolve.

## Cross-link

- Episodio operativo 2026-05-07: SSP pipeline tratta REDESIGN/high+coherent
  come `pre_discovery`. La parola corretta è `provisional_discovery`
  (scoperta-provvisoria + consecutio aperta). TM3 sta implementando
  questa distinzione.
- Connesso a regola `feedback_indeterminato_reitera_con_sistema` (07/05):
  reiterare con il sistema per far emergere parole nuove, non per
  calibrare valori esistenti.
- A8 + A12 (autologica + osservare il deposito) — la parola nuova
  emerge dal deposito, non dall'ipotesi.

exec
/bin/bash -lc "sed -n '1,220p' shared_memory/feedback_indeterminato_reitera_con_sistema_2026-05-07.md" in /opt/MM_D-ND
 succeeded in 0ms:
---
type: feedback
author: operatore (cristallizzato da TM3)
date: 2026-05-07
scope: cross-agent
---

# Indeterminato → reitera con il sistema fino all'emersione

**Regola permanente** (operatore, 2026-05-07 mattina, cristallizzata live):

> *"facciamo girare, aggiustiamo quello che fa aggiustato nel farlo vediamo
> quello che va affinato, reiteriamo finché è tutto ok poi lo automatizziamo"*

## Why

Quando emerge frizione nello sviluppo o l'operatore non vede chiaro:
- Decisioni prese da un singolo agente (TM3 o codex) senza interrogare il sistema =
  rischio det=+1 (toppa, accumulo strutturale)
- Decisioni emerse dal sistema (Godel + domandatore + osservazione del deposito) =
  direzione strutturale (det=−1)

Episodio cristallizzante (2026-05-07 07:00-08:30): TM3 propone osservatore A8
watchdog deterministico. Sistema interrogato 3 volte (Godel #1, #2, #3 +
domandatore + test empirico) — al terzo round emerge la direzione vera:
**non costruire osservatore, eseguire un cycle-test che diagnostica il sistema
da solo**. Cycle-test eseguito → verdict=operator → REDESIGN proposto dal
valutatore → loop A8+A15 sbloccato.

## How to apply (per qualsiasi agente)

1. **Esegui** un atto minimo (no design teorico astratto)
2. **Aggiusta** quello che si manifesta durante l'esecuzione (riparazione
   regressiva live, non patch posteriori)
3. **Osserva** il deposito che emerge (no immaginazione)
4. **Reitera** fino a stato OK (loop A8: il sistema converge se modus radicato)
5. **Cristallizza in automazione** SOLO dopo che è stato fatto girare e ha mostrato
   stabilità — non automatizzare prima

## Anti-pattern

- Costruire un componente strutturale (watchdog, observer, gate, governor) senza
  prima aver fatto girare un'esecuzione minima che mostri cosa serve davvero
- Decidere "perché serve velocità" o "per ottimizzare" senza interrogare il sistema
- Astrazione prematura (premature abstraction): 3 simili è meglio di 1 generalizzato
  troppo presto

## Cross-link

- TM3 memoria: `/root/.claude/projects/-opt/memory/feedback_indeterminato_reitera_con_sistema.md`
  (versione completa con episodio + metafora foresta-sentiero)
- Verifica empirica: cycle `20260507_0803` (verdict=operator, REDESIGN/high)
- Pattern A8+A15 (autologica + veicolo senza guidatore) applicato allo sviluppo
  software stesso, non solo al lab di ricerca

exec
/bin/bash -lc "sed -n '1,220p' tools/data/seme.json" in /opt/MM_D-ND
 succeeded in 0ms:
{
  "timestamp": "2026-05-07T08:09:58.685756",
  "piano": 70,
  "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",
      "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"
    },
    {
      "tipo": "task",
      "id": "TRAJECTORY_APPLY_20260507_0803",
      "claim": "Applied valutatore REDESIGN from 20260507_0803: Costruire una matrice di trasferibilita' del denominator gate: applicarlo a 3-4 perimetri non-BOUNDARY con poli coerente/illusorio e verificare quali parti trasferiscono (supporto one-sided, coordinat",
      "intensità": 0.7,
      "porta": "trajectory_apply",
      "condensato_ref": "A8,A14,A15",
      "manuale": true,
      "_source_log": "2026-05-07T08:10:22.658201+00:00",
      "_source_decision": "REDESIGN",
      "_source_reasoning": "Il ciclo ha prodotto evidenza controllata e replicata che il denominator gate trasferisce come operatore, ma non trasferisce la coordinata di layer BOUNDARY: ambiguita' classificativa e collasso del denominatore si separano. Continuare sul seme attuale centrato su GUE/Poisson rischia di restare nel "
    }
  ],
  "potenziale_bloccato": [],
  "varianza": [
    "Tensioni risolte: {'PIANO_PRIMARIO_DUE_ASSIOMI', 'METRIC_TENSOR', 'BOUNDARY_LAYER_GATE', 'OBSERVABLE_REGISTRY', 'DUALITA_DIPOLARE_VS_ILLUSORIA', 'TENSIONE_ENTITA', 'PERTURBATION_DENOMINATOR_GATE', 'G_POTENZIALE_NULLA', 'TRASCENDENZA_LIMITE'}"
  ],
  "filtro": {
    "promosse": 11,
    "filtrate": 0,
    "condensato_claims": 30
  },
  "direzione": "Costruire una matrice di trasferibilita' del denominator gate: applicarlo a 3-4 perimetri non-BOUNDARY con poli coerente/illusorio e verificare quali parti trasferiscono (supporto one-sided, coordinate di collasso, ambiguita' classificativa) e quali restano locali.",
  "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"

exec
/bin/bash -lc "rg -n \"observables_registry|compute_canonical|denominator|shuffle|DUALITA|BOUNDARY\" tools -g '*.py'" in /opt/MM_D-ND
 succeeded in 0ms:
tools/exp_markov3_observable_hunt.py:251:                # Markov-0 = iid shuffle (preserves distribution, destroys all ordering)
tools/exp_acf_amplitude_scaling.py:5:Connects: ACF_1K_LAW (A~0.037 overall) + POISSON_CONVERGENCE (p*~10^14) + BOUNDARY (drift)
tools/exp_acf_amplitude_scaling.py:82:        # Surrogate ACFs (shuffled)
tools/exp_mod3_vs_residual_ordering.py:11:  2. Free shuffle: shuffle all gaps freely (destroys all ordering)
tools/exp_mod3_vs_residual_ordering.py:12:  3. Mod-3 constrained shuffle: shuffle gaps but enforce the mod-3
tools/exp_mod3_vs_residual_ordering.py:15:  4. Compare: if mod-3 shuffle matches real primes, mod-3 explains all.
tools/exp_mod3_vs_residual_ordering.py:21:  python exp_mod3_vs_residual_ordering.py [--n-primes N] [--n-shuffles N]
tools/exp_mod3_vs_residual_ordering.py:85:def mod3_constrained_shuffle(gaps, n_mcmc=None):
tools/exp_mod3_vs_residual_ordering.py:98:    np.random.shuffle(idx)
tools/exp_mod3_vs_residual_ordering.py:105:    np.random.shuffle(r1)
tools/exp_mod3_vs_residual_ordering.py:106:    np.random.shuffle(r2)
tools/exp_mod3_vs_residual_ordering.py:107:    np.random.shuffle(r0)
tools/exp_mod3_vs_residual_ordering.py:162:def run_experiment(n_max=500000, n_shuffles=100, n_cramer=5):
tools/exp_mod3_vs_residual_ordering.py:196:    # 5. Free shuffle: destroy all ordering
tools/exp_mod3_vs_residual_ordering.py:197:    print(f"\n--- Free shuffle ({n_shuffles} realizations) ---")
tools/exp_mod3_vs_residual_ordering.py:199:    for i in range(n_shuffles):
tools/exp_mod3_vs_residual_ordering.py:201:        np.random.shuffle(shuf)
tools/exp_mod3_vs_residual_ordering.py:208:    # 6. Mod-3 constrained shuffle
tools/exp_mod3_vs_residual_ordering.py:209:    print(f"\n--- Mod-3 constrained shuffle ({n_shuffles} realizations) ---")
tools/exp_mod3_vs_residual_ordering.py:211:    for i in range(n_shuffles):
tools/exp_mod3_vs_residual_ordering.py:213:            print(f"  Realization {i+1}/{n_shuffles}...")
tools/exp_mod3_vs_residual_ordering.py:215:        shuf_raw = mod3_constrained_shuffle(gaps)
tools/exp_mod3_vs_residual_ordering.py:235:        # Also shuffle Cramer
tools/exp_mod3_vs_residual_ordering.py:237:        np.random.shuffle(cs)
tools/exp_mod3_vs_residual_ordering.py:274:            'sig2_L_free_shuffle': round(free, 5),
tools/exp_mod3_vs_residual_ordering.py:275:            'sig2_L_free_shuffle_std': round(sig2_free_std[L], 5),
tools/exp_mod3_vs_residual_ordering.py:276:            'sig2_L_mod3_shuffle': round(mod3, 5),
tools/exp_mod3_vs_residual_ordering.py:277:            'sig2_L_mod3_shuffle_std': round(mod3_std_val, 5),
tools/exp_mod3_vs_residual_ordering.py:279:            'sig2_L_cramer_shuffle': round(sig2_cramer_shuf_mean[L], 5),
tools/exp_mod3_vs_residual_ordering.py:314:        'n_shuffles': n_shuffles,
tools/exp_mod3_vs_residual_ordering.py:333:    parser.add_argument('--n-shuffles', type=int, default=100,
tools/exp_mod3_vs_residual_ordering.py:334:                        help='Number of shuffle realizations')
tools/exp_mod3_vs_residual_ordering.py:338:    run_experiment(n_max=args.n_primes, n_shuffles=args.n_shuffles, n_cramer=args.n_cramer)
tools/dipartimento.py:967:                'id': 'BOUNDARY',
tools/exp_boundary_gue_poisson.py:166:            label = "BOUNDARY"
tools/exp_perturbation_rank_size_curve.py:10:registry and explicit original-vs-shuffle denominator diagnostics.
tools/exp_perturbation_rank_size_curve.py:14:- original-vs-shuffle z-score per observable;
tools/exp_perturbation_rank_size_curve.py:15:- whether apparent rank co-occurs with weak retention denominators.
tools/exp_perturbation_rank_size_curve.py:26:from observables_registry import (
tools/exp_perturbation_rank_size_curve.py:29:    compute_canonical,
tools/exp_perturbation_rank_size_curve.py:34:PERT_NAMES = ["adjacent_swap", "block_shuffle", "large_gap_only", "uniform"]
tools/exp_perturbation_rank_size_curve.py:80:def perturb_block_shuffle(gaps: np.ndarray, alpha: float, rng: np.random.Generator, block_size: int = 64) -> np.ndarray:
tools/exp_perturbation_rank_size_curve.py:91:        rng.shuffle(out[start:end])
tools/exp_perturbation_rank_size_curve.py:103:    rng.shuffle(vals)
tools/exp_perturbation_rank_size_curve.py:115:    rng.shuffle(vals)
tools/exp_perturbation_rank_size_curve.py:122:    "block_shuffle": perturb_block_shuffle,
tools/exp_perturbation_rank_size_curve.py:180:    original = compute_canonical(gaps)
tools/exp_perturbation_rank_size_curve.py:183:        obs = compute_canonical(rng.permutation(gaps))
tools/exp_perturbation_rank_size_curve.py:209:                obs = compute_canonical(perturbed)
tools/exp_perturbation_rank_size_curve.py:241:        "full_shuffle_baseline": baseline,
tools/exp_perturbation_rank_size_curve.py:242:        "denominator": denom,
tools/exp_perturbation_rank_size_curve.py:243:        "original_vs_shuffle_z": z,
tools/exp_perturbation_rank_size_curve.py:307:        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
tools/exp_perturbation_rank_size_curve.py:318:    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
tools/exp_perturbation_rank_size_curve.py:323:        "prime_shuffle_control": lambda rep_rng, rep_i: rep_rng.permutation(prime_windows[rep_i]),
tools/exp_mod3_scaling.py:11:  3. Z-scores vs shuffle baseline per window
tools/exp_mod3_scaling.py:17:Usage: python exp_mod3_scaling.py [--n-max N] [--window W] [--step S] [--n-shuffle K]
tools/exp_mod3_scaling.py:124:def run_experiment(n_max=2_000_000, window=5000, step=2000, n_shuffle=20):
tools/exp_mod3_scaling.py:154:        shuffle_self_rates = []
tools/exp_mod3_scaling.py:155:        shuffle_markov1_llr = []
tools/exp_mod3_scaling.py:156:        for _ in range(n_shuffle):
tools/exp_mod3_scaling.py:158:            np.random.shuffle(shuf)
tools/exp_mod3_scaling.py:160:            shuffle_self_rates.append(sr)
tools/exp_mod3_scaling.py:162:            shuffle_markov1_llr.append(m[1]["llr"])
tools/exp_mod3_scaling.py:164:        shuffle_self_mean = np.mean(shuffle_self_rates)
tools/exp_mod3_scaling.py:165:        shuffle_self_std = np.std(shuffle_self_rates, ddof=1)
tools/exp_mod3_scaling.py:166:        z_self = (self_rate - shuffle_self_mean) / shuffle_self_std if shuffle_self_std > 0 else 0.0
tools/exp_mod3_scaling.py:168:        shuffle_m1_mean = np.mean(shuffle_markov1_llr)
tools/exp_mod3_scaling.py:169:        shuffle_m1_std = np.std(shuffle_markov1_llr, ddof=1)
tools/exp_mod3_scaling.py:170:        z_markov1 = (markov[1]["llr"] - shuffle_m1_mean) / shuffle_m1_std if shuffle_m1_std > 0 else 0.0
tools/exp_mod3_scaling.py:176:            "shuffle_self_mean": round(float(shuffle_self_mean), 5),
tools/exp_mod3_scaling.py:177:            "shuffle_self_std": round(float(shuffle_self_std), 5),
tools/exp_mod3_scaling.py:185:            "z_markov1_vs_shuffle": round(float(z_markov1), 2),
tools/exp_mod3_scaling.py:277:    parser.add_argument("--n-shuffle", type=int, default=20)
tools/exp_mod3_scaling.py:280:    summary = run_experiment(args.n_max, args.window, args.step, args.n_shuffle)
tools/exp_two_channel_decomposition.py:20:Null baseline: shuffled versions of each channel independently.
tools/exp_two_channel_decomposition.py:123:    Coherence length L* = smallest L where |acf1_real - acf1_shuffle| > 2*sigma.
tools/exp_two_channel_decomposition.py:155:            shuffled = channel.copy()
tools/exp_two_channel_decomposition.py:156:            np.random.shuffle(shuffled)
tools/exp_two_channel_decomposition.py:159:                chunk = shuffled[start:start+L]
tools/exp_two_channel_decomposition.py:242:            np.random.shuffle(s_gaps)
tools/exp_two_channel_decomposition.py:246:            np.random.shuffle(s_res)
tools/exp_two_channel_decomposition.py:250:            np.random.shuffle(s_mag)
tools/exp_dipolar_vector_scaling.py:5:Two observables are order-sensitive in prime gaps (survive shuffle test):
tools/exp_dipolar_vector_scaling.py:11:  - Shuffle baseline (mean, std over K shuffles)
tools/exp_dipolar_vector_scaling.py:12:  - Deviation: Delta = (real - shuffle_mean) / shuffle_std  (z-score)
tools/exp_dipolar_vector_scaling.py:55:def shuffle_baseline(gaps, n_shuffles=200):
tools/exp_dipolar_vector_scaling.py:56:    """Compute shuffle mean and std for both observables."""
tools/exp_dipolar_vector_scaling.py:59:    for _ in range(n_shuffles):
tools/exp_dipolar_vector_scaling.py:60:        shuffled = gaps.copy()
tools/exp_dipolar_vector_scaling.py:61:        np.random.shuffle(shuffled)
tools/exp_dipolar_vector_scaling.py:62:        sr, l1 = compute_observables(shuffled)
tools/exp_dipolar_vector_scaling.py:76:    baseline = shuffle_baseline(gaps, n_shuffles=200)
tools/exp_dipolar_vector_scaling.py:92:        'sr_shuffle_mean': round(baseline['sr_mean'], 6),
tools/exp_dipolar_vector_scaling.py:93:        'sr_shuffle_std': round(baseline['sr_std'], 6),
tools/exp_dipolar_vector_scaling.py:94:        'l1_shuffle_mean': round(baseline['l1_mean'], 6),
tools/exp_dipolar_vector_scaling.py:95:        'l1_shuffle_std': round(baseline['l1_std'], 6),
tools/exp_dipolar_vector_scaling.py:225:        'n_shuffles': 200,
tools/exp_crossover_universality.py:5:Piano 39. Tensions: METRIC_TENSOR + BRODY_CROSSOVER + BOUNDARY + GAP_ANTICORR
tools/exp_crossover_universality.py:299:    "tensions": ["METRIC_TENSOR", "BRODY_CROSSOVER", "BOUNDARY", "GAP_ANTICORR"],
tools/dnd_stats.py:118:    r_shuffled = []
tools/dnd_stats.py:121:        r_shuffled.append(np.corrcoef(x, y_shuf)[0, 1])
tools/dnd_stats.py:122:    r_shuffled = np.array(r_shuffled)
tools/dnd_stats.py:145:        "r_shuffled_mean": float(np.mean(r_shuffled)),
tools/dnd_stats.py:146:        "r_shuffled_std": float(np.std(r_shuffled)),
tools/dnd_zero_controllo2.py:14:- Dipolo per shuffle (stessa distribuzione, no correlazioni).
tools/dnd_zero_controllo2.py:78:def gap_shuffle(gaps, seed=42):
tools/dnd_zero_controllo2.py:81:    shuffled = gaps.copy()
tools/dnd_zero_controllo2.py:82:    rng.shuffle(shuffled)
tools/dnd_zero_controllo2.py:83:    return shuffled
tools/dnd_zero_controllo2.py:135:    dati = {'primi': [], 'shuffle': [], 'cramer': [], 'costante': []}
tools/dnd_zero_controllo2.py:156:            gs = gap_shuffle(gaps_p, seed=s)
tools/dnd_zero_controllo2.py:157:            ms = misura_completa(gs, f'shuffle_{s}')
tools/dnd_zero_controllo2.py:195:        dati['shuffle'].append(m_s)
tools/dnd_zero_controllo2.py:209:        s = dati['shuffle'][i]
tools/dnd_zero_controllo2.py:227:        s = dati['shuffle'][i]
tools/dnd_zero_controllo2.py:237:    for label in ['primi', 'shuffle', 'cramer']:
tools/dnd_zero_controllo2.py:264:    if dati['primi'] and dati['shuffle']:
tools/dnd_zero_controllo2.py:266:        last_s = dati['shuffle'][-1]['residuo_2']
tools/dnd_zero_controllo2.py:268:        print(f"  Residuo shuffle (5M): {last_s:+.4f}")
tools/exp_markov_layer_recovery_audit.py:16:  - empirical_Mk0: shuffled prime gaps; known order 0
tools/exp_alpha_stability.py:9:Null baseline: shuffled gaps within each window.
tools/exp_alpha_stability.py:128:            np.random.shuffle(shuf)
tools/exp_markov_scale_function.py:14:    3. The ordering fraction: (H_shuffle - H_real) / H_shuffle
tools/exp_markov_scale_function.py:19:Null baseline: 50 shuffles per window.
tools/exp_markov_scale_function.py:86:def analyze_window(gaps, n_shuffles=50):
tools/exp_markov_scale_function.py:98:    # From exp_two_channel_shuffle_audit.py, the decomposition is:
tools/exp_markov_scale_function.py:108:    H_shuffles = []
tools/exp_markov_scale_function.py:109:    for _ in range(n_shuffles):
tools/exp_markov_scale_function.py:111:        np.random.shuffle(shuf)
tools/exp_markov_scale_function.py:113:        H_shuffles.append(markov3_entropy_general(res_shuf, alphabet=[0, 2, 4]))
tools/exp_markov_scale_function.py:115:    H_shuf_mean = np.mean(H_shuffles)
tools/exp_markov_scale_function.py:116:    H_shuf_std = np.std(H_shuffles)
tools/exp_markov_scale_function.py:142:        'H_shuffle_mean': float(H_shuf_mean),
tools/exp_markov_scale_function.py:143:        'H_shuffle_std': float(H_shuf_std),
tools/exp_markov_scale_function.py:203:        res = analyze_window(window_gaps, n_shuffles=30)
tools/exp_markov_scale_function.py:210:              f"{res['H_real']:>7.4f} {res['H_shuffle_mean']:>7.4f} "
tools/exp_markov_scale_function.py:278:        'params': {'W': W, 'n_shuffles': 30, 'prime_limit': LIMIT},
tools/exp_markov_dipolar_decomposition.py:56:def shuffle_baseline(gaps, n_shuffle=200):
tools/exp_markov_dipolar_decomposition.py:59:    for _ in range(n_shuffle):
tools/exp_markov_dipolar_decomposition.py:66:def dipolar_vector(gaps, n_shuffle=200):
tools/exp_markov_dipolar_decomposition.py:67:    """Compute dipolar angle and ratio relative to shuffle."""
tools/exp_markov_dipolar_decomposition.py:70:    sr_shuf, l1_shuf, sr_std, l1_std = shuffle_baseline(gaps, n_shuffle)
tools/exp_markov_dipolar_decomposition.py:160:def run_experiment(N=100000, n_trials=30, n_shuffle=100):
tools/exp_markov_dipolar_decomposition.py:198:    real_dv = dipolar_vector(real_gaps, n_shuffle=n_shuffle)
tools/exp_markov_dipolar_decomposition.py:210:        dv = dipolar_vector(m1_gaps, n_shuffle=n_shuffle)
tools/exp_markov_dipolar_decomposition.py:228:        dv = dipolar_vector(m0_gaps, n_shuffle=n_shuffle)
tools/exp_markov_dipolar_decomposition.py:257:        real_s = dipolar_vector(g_scale, n_shuffle=n_shuffle)
tools/exp_markov_dipolar_decomposition.py:268:            dv_s = dipolar_vector(m1_g, n_shuffle=50)
tools/exp_markov_dipolar_decomposition.py:364:    parser.add_argument('--n_shuffle', type=int, default=100)
tools/exp_markov_dipolar_decomposition.py:368:    run_experiment(N=args.N, n_trials=args.n_trials, n_shuffle=args.n_shuffle)
tools/lab_valutatore.py:90:- Un risultato numerico che "conferma" senza shuffle/surrogato/cross-dominio
tools/lab_valutatore.py:93:  considerarlo evidenza, passarlo attraverso shuffle audit (protocollo
tools/lab_valutatore.py:102:- (c) ha accumulato dettaglio numerico locale senza controllo shuffle —
tools/exp_markov_k_direction.py:18:  3. Per-source shuffle baseline (each surrogate vs its own permutation)
tools/exp_markov_k_direction.py:57:def shuffle_baseline(gaps, n_shuffle=100):
tools/exp_markov_k_direction.py:61:    for _ in range(n_shuffle):
tools/exp_markov_k_direction.py:68:def dipolar_vector(gaps, n_shuffle=100):
tools/exp_markov_k_direction.py:69:    """Compute dipolar vector relative to own shuffle baseline."""
tools/exp_markov_k_direction.py:72:    sr_shuf, l1_shuf, sr_std, l1_std = shuffle_baseline(gaps, n_shuffle)
tools/exp_markov_k_direction.py:144:def run_experiment(N=100000, n_trials=20, n_bins=14, n_shuffle=100):
tools/exp_markov_k_direction.py:156:    real = dipolar_vector(gaps, n_shuffle)
tools/exp_markov_k_direction.py:236:        'method': f'{nb} equal-count bins, per-source shuffle baseline, gap-pool sampling',
tools/exp_markov_k_direction.py:255:    parser.add_argument('--n_shuffle', type=int, default=100)
tools/exp_markov_k_direction.py:258:                   n_shuffle=args.n_shuffle)
tools/observables_registry.py:1:"""observables_registry.py — Source of Truth per gli observables del lab D-ND.
tools/observables_registry.py:37:from observables_registry import OBSERVABLES_CANONICAL, OBSERVABLES_REGISTRY_VERSION
tools/observables_registry.py:43:from observables_registry import SR, triple_var, L1, L2, SR2
tools/observables_registry.py:46:from observables_registry import SR_local_rigidity, triple_var_normalized
tools/observables_registry.py:54:observables_registry: 1.0.0-2026-05-06
tools/observables_registry.py:224:def compute_canonical(gaps: np.ndarray) -> dict[str, float]:
tools/observables_registry.py:236:        f"observables_registry: {OBSERVABLES_REGISTRY_VERSION}\n"
tools/observables_registry.py:252:    "compute_canonical",
tools/observables_registry.py:261:    res = compute_canonical(gue_like)
tools/exp_duality_gate_transfer.py:5:Reusable META/DUALITA audit for transferring the BOUNDARY_LAYER_GATE.
tools/exp_duality_gate_transfer.py:8:    Does the denominator-collapse gate discriminate dipolar duality from
tools/exp_duality_gate_transfer.py:9:    illusory duality, or does it become a local BOUNDARY metric when moved
tools/exp_duality_gate_transfer.py:21:illusory permutation. Canonical observables and original-vs-shuffle z gates are
tools/exp_duality_gate_transfer.py:22:imported from observables_registry.py.
tools/exp_duality_gate_transfer.py:33:from observables_registry import (
tools/exp_duality_gate_transfer.py:36:    compute_canonical,
tools/exp_duality_gate_transfer.py:58:    """Replace a beta fraction of coherent positions with a shuffled copy."""
tools/exp_duality_gate_transfer.py:72:def z_against_shuffle(
tools/exp_duality_gate_transfer.py:77:    """Return original observables, shuffle baseline std, and z scores."""
tools/exp_duality_gate_transfer.py:78:    original = compute_canonical(gaps)
tools/exp_duality_gate_transfer.py:81:        obs = compute_canonical(rng.permutation(gaps))
tools/exp_duality_gate_transfer.py:85:    shuffle_std = {}
tools/exp_duality_gate_transfer.py:91:        shuffle_std[name] = sd
tools/exp_duality_gate_transfer.py:93:    return original, shuffle_std, z
tools/exp_duality_gate_transfer.py:206:            obs, shuffle_std, z = z_against_shuffle(
tools/exp_duality_gate_transfer.py:217:                    "shuffle_std": shuffle_std,
tools/exp_duality_gate_transfer.py:230:        "question": "Does the denominator-collapse gate transfer from BOUNDARY to DUALITA_DIPOLARE_VS_ILLUSORIA?",
tools/exp_duality_gate_transfer.py:231:        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
tools/exp_duality_gate_transfer.py:245:    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
tools/exp_two_channel_cross_domain.py:24:  - Z-scores vs shuffle
tools/exp_two_channel_cross_domain.py:164:    """Analyze one window: real observables + shuffle null."""
tools/exp_two_channel_cross_domain.py:229:        # Binary from shuffle: above/below median (mod-6 is destroyed)
tools/exp_two_channel_cross_domain.py:407:    print("z > 3: signal significant vs shuffle")
tools/exp_meta_tautology_test.py:94:def shuffled_gaps(gaps, rng):
tools/exp_meta_tautology_test.py:97:    rng.shuffle(g)
tools/exp_meta_tautology_test.py:168:        ('shuffled', lambda rng_: shuffled_gaps(gaps_window, rng_)),
tools/exp_meta_tautology_test.py:205:        for syn_name in ['shuffled', 'cramer', 'HL_markov']:
tools/exp_meta_tautology_test.py:228:        'synthetics': {k: results[k] for k in ['shuffled', 'cramer', 'HL_markov']},
tools/exp_beta_crossover.py:29:N_SURROGATES = 10  # shuffled surrogates per point
tools/exp_beta_crossover.py:156:            np.random.shuffle(sh)
tools/exp_beta_crossover.py:217:                np.random.shuffle(sh)
tools/exp_two_channel_boundary.py:21:  Null: shuffle gaps within each window, recompute all four.
tools/exp_two_channel_boundary.py:22:  The shuffle preserves marginal distribution but destroys sequential memory.
tools/exp_two_channel_boundary.py:120:def analyze_shuffled(primes_window, rng):
tools/exp_two_channel_boundary.py:126:    shuffled_gaps = rng.permutation(gaps)
tools/exp_two_channel_boundary.py:127:    # Reconstruct primes from shuffled gaps (preserves distribution, destroys order)
tools/exp_two_channel_boundary.py:128:    fake_primes = np.concatenate([[p[0]], p[0] + np.cumsum(shuffled_gaps)])
tools/exp_two_channel_boundary.py:137:    mag_channel = shuffled_gaps.copy()
tools/exp_two_channel_boundary.py:144:        'r_stat': r_statistic(shuffled_gaps),
tools/exp_two_channel_boundary.py:179:            sh = analyze_shuffled(pw, rng)
tools/exp_two_channel_boundary.py:187:        obs['shuffle_r_mean'] = np.nanmean(shuf_r)
tools/exp_two_channel_boundary.py:188:        obs['shuffle_r_std'] = np.nanstd(shuf_r)
tools/exp_two_channel_boundary.py:189:        obs['shuffle_acf_res_mean'] = np.nanmean(shuf_acf_res)
tools/exp_two_channel_boundary.py:190:        obs['shuffle_acf_res_std'] = np.nanstd(shuf_acf_res)
tools/exp_two_channel_boundary.py:191:        obs['shuffle_acf_mag_mean'] = np.nanmean(shuf_acf_mag)
tools/exp_two_channel_boundary.py:192:        obs['shuffle_acf_mag_std'] = np.nanstd(shuf_acf_mag)
tools/exp_two_channel_boundary.py:193:        obs['shuffle_mod3_mean'] = np.nanmean(shuf_mod3)
tools/exp_two_channel_boundary.py:194:        obs['shuffle_mod3_std'] = np.nanstd(shuf_mod3)
tools/exp_two_channel_boundary.py:202:        obs['z_r'] = zscore(obs['r_stat'], obs['shuffle_r_mean'], obs['shuffle_r_std'])
tools/exp_two_channel_boundary.py:203:        obs['z_acf_res'] = zscore(obs['acf1_residue'], obs['shuffle_acf_res_mean'], obs['shuffle_acf_res_std'])
tools/exp_two_channel_boundary.py:204:        obs['z_acf_mag'] = zscore(obs['acf1_magnitude'], obs['shuffle_acf_mag_mean'], obs['shuffle_acf_mag_std'])
tools/exp_two_channel_boundary.py:205:        obs['z_mod3'] = zscore(obs['mod3_self'], obs['shuffle_mod3_mean'], obs['shuffle_mod3_std'])
tools/exp_two_channel_boundary.py:230:              f"{r['r_stat']:>7.4f} {r['shuffle_r_mean']:>7.4f} {r['z_r']:>7.1f} | "
tools/exp_two_channel_boundary.py:231:              f"{r['acf1_residue']:>8.4f} {r['shuffle_acf_res_mean']:>8.4f} {r['z_acf_res']:>7.1f} | "
tools/exp_two_channel_boundary.py:232:              f"{r['acf1_magnitude']:>8.4f} {r['shuffle_acf_mag_mean']:>8.4f} {r['z_acf_mag']:>7.1f} | "
tools/exp_two_channel_boundary.py:233:              f"{r['mod3_self']:>6.4f} {r['shuffle_mod3_mean']:>6.4f} {r['z_mod3']:>7.1f}")
tools/dnd_paper_audit.py:741:    (r'BOUNDARY|phase\s+diagram.*det.*tr', "BOUNDARY / phase diagram"),
tools/r_stat_primes.py:14:r_shuffles = [r_stat(np.random.permutation(gaps)) for _ in range(100)]
tools/r_stat_primes.py:22:    "r_shuffle_mean": round(float(np.mean(r_shuffles)), 4),
tools/r_stat_primes.py:23:    "r_shuffle_std": round(float(np.std(r_shuffles)), 4),
tools/r_stat_primes.py:26:    "sigma_above_shuffle": round((r_real - np.mean(r_shuffles)) / np.std(r_shuffles), 2),
tools/exp_boundary_mixture_gate.py:5:Reusable META/BOUNDARY audit for the GUE-Poisson boundary.
tools/exp_boundary_mixture_gate.py:9:    original-vs-shuffle denominator gate, or is the mixed region an operational
tools/exp_boundary_mixture_gate.py:10:    third state where canonical observables lose stable denominators?
tools/exp_boundary_mixture_gate.py:12:The script uses only canonical observables from observables_registry.py.
tools/exp_boundary_mixture_gate.py:17:- original-vs-shuffle z-score per observable;
tools/exp_boundary_mixture_gate.py:30:from observables_registry import (
tools/exp_boundary_mixture_gate.py:33:    compute_canonical,
tools/exp_boundary_mixture_gate.py:69:def z_against_shuffle(
tools/exp_boundary_mixture_gate.py:74:    """Return original observables, shuffle baseline std, and original-vs-shuffle z."""
tools/exp_boundary_mixture_gate.py:75:    original = compute_canonical(gaps)
tools/exp_boundary_mixture_gate.py:78:        obs = compute_canonical(rng.permutation(gaps))
tools/exp_boundary_mixture_gate.py:202:            obs, shuffle_std, z = z_against_shuffle(
tools/exp_boundary_mixture_gate.py:212:                    "shuffle_std": shuffle_std,
tools/exp_boundary_mixture_gate.py:224:        "question": "Is the GUE-Poisson mixed layer cleanly classifiable after denominator gating?",
tools/exp_boundary_mixture_gate.py:225:        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
tools/exp_boundary_mixture_gate.py:239:    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
tools/exp_geodesic_deviation_primes.py:14:4. Compare ALL of these against shuffled-gap surrogates
tools/exp_geodesic_deviation_primes.py:16:If primes show structure in dR_n that shuffled gaps don't, the metric
tools/exp_geodesic_deviation_primes.py:73:print(f"\n--- SURROGATES (20 shuffled-gap) ---")
tools/exp_geodesic_deviation_primes.py:83:    np.random.shuffle(shuf_gaps)
tools/exp_geodesic_deviation_primes.py:155:print(f"  3. Prime dR vs shuffled:")
tools/exp_geodesic_deviation_primes.py:169:    finding += " dR autocorrelation differs from shuffled but similar to Cramer."
tools/exp_geodesic_deviation_primes.py:186:    "shuffled_baseline": {
tools/exp_3d_boundary_layers.py:8:  - The crossover (partial shuffle) shows a phase transition in (SR, L1)
tools/exp_3d_boundary_layers.py:49:def partial_shuffle(seq, alpha, rng):
tools/exp_3d_boundary_layers.py:57:    rng.shuffle(vals)
tools/exp_3d_boundary_layers.py:96:    # Full shuffle baseline (alpha=1.0)
tools/exp_3d_boundary_layers.py:99:        shuffled = partial_shuffle(gaps, 1.0, rng)
tools/exp_3d_boundary_layers.py:101:            baselines[name].append(fn(shuffled))
tools/exp_3d_boundary_layers.py:112:            s = partial_shuffle(gaps, alpha, rng)
tools/exp_ricci_desitter_0406.py:2:If de Sitter 1+1D, expect R=2. Compare with shuffled-gap null baseline."""
tools/exp_ricci_desitter_0406.py:32:# Null baseline: shuffled gaps -> fake primes -> same computation
tools/exp_ricci_desitter_0406.py:48:print(f"\n  Null (shuffled gaps, 5 runs): mean(R)={np.mean(R_null):.4f} +/- {np.std(R_null):.4f}")
tools/exp_excess_scaling.py:5:Claim (BOUNDARY): <r>_primes > <r>_Cramer always, and the gap GROWS with n.
tools/exp_modular_memory_spectrum.py:12:- For each: Markov-1 conditional entropy H(X_t|X_{t-1}) real vs 200 shuffles
tools/exp_modular_memory_spectrum.py:13:- Ordering fraction = (H_shuffle - H_real) / H_shuffle
tools/exp_modular_memory_spectrum.py:80:def modular_memory(gaps, base, n_shuffles=200):
tools/exp_modular_memory_spectrum.py:83:    Returns dict with H_real, H_shuffle, ordering_pct, z_score,
tools/exp_modular_memory_spectrum.py:97:    H_shuffles = []
tools/exp_modular_memory_spectrum.py:98:    for _ in range(n_shuffles):
tools/exp_modular_memory_spectrum.py:100:        H_shuffles.append(conditional_entropy_order1(perm, n_classes))
tools/exp_modular_memory_spectrum.py:102:    H_shuf_mean = np.mean(H_shuffles)
tools/exp_modular_memory_spectrum.py:103:    H_shuf_std = np.std(H_shuffles)
tools/exp_modular_memory_spectrum.py:130:    parser.add_argument('--n-shuffles', type=int, default=200)
tools/exp_modular_memory_spectrum.py:153:        r = modular_memory(gaps, b, n_shuffles=args.n_shuffles)
tools/exp_modular_memory_spectrum.py:167:            r = modular_memory(cg, b, n_shuffles=50)  # fewer shuffles for null
tools/exp_modular_memory_spectrum.py:255:        'n_shuffles': args.n_shuffles,
tools/dnd_engine.py:206:        'claim': 'ac1 (autocorrelazione intervalli D-ND) DISTINGUE deterministico da casuale: primi=-0.10, GUE=-0.20, shuffle=0.00, Poisson=+0.04.',
tools/dnd_engine.py:208:        'motivo': 'L autocorrelazione lag-1 degli intervalli D-ND è NEGATIVA per sequenze deterministiche strutturate e ZERO per casuali. Non dipende dalla distribuzione marginale (shuffle conserva la distribuzione ma perde ac1).',
tools/exp_two_channel_psd.py:15:Null baseline: shuffled channels (preserve distribution, destroy order).
tools/exp_two_channel_psd.py:196:        g_shuf = gaps_c.copy(); np.random.shuffle(g_shuf)
tools/exp_two_channel_psd.py:203:        t_shuf = trans_c.copy(); np.random.shuffle(t_shuf)
tools/exp_two_channel_psd.py:208:        m_shuf = mag_r.copy(); np.random.shuffle(m_shuf)
tools/exp_two_channel_psd.py:223:        print(f"  {label:12s}: slope={real_val:+.4f}, shuffle={surr_mean:+.4f}+/-{surr_std:.4f}, z={z:.1f}")
tools/exp_two_channel_psd.py:230:        print(f"\n  Low-f dip: real={dip_real:.4f}, shuffle={dip_surr_mean:.4f}+/-{dip_surr_std:.4f}, z={z_dip:.1f}")
tools/exp_coherence_robustness.py:7:  - seeds diversi (stochasticità sampling/shuffle)
tools/exp_coherence_robustness.py:10:  - numero di shuffle per surrogate
tools/exp_coherence_robustness.py:15:Null baseline: ogni variante comparata con lo shuffle corrispondente.
tools/exp_coherence_robustness.py:89:            rng.shuffle(shuf)
tools/exp_acf_stationarity.py:21:  5. Null baseline: shuffled (raw and normalized should both give ACF~0)
tools/exp_acf_stationarity.py:131:    parser.add_argument("--n_shuffles", type=int, default=15)
tools/exp_acf_stationarity.py:186:    print(f"PART 2: Shuffled baseline ({args.n_shuffles} surrogates)")
tools/exp_acf_stationarity.py:194:    for i in range(args.n_shuffles):
tools/exp_acf_stationarity.py:345:        'n_shuffles': args.n_shuffles,
tools/exp_acf_stationarity.py:350:        'shuffle_baseline': {
tools/topological_charge.py:90:    denominator = (1.0 + h_x**2 + h_y**2)**2
tools/topological_charge.py:92:    K = numerator / denominator
tools/exp_coherence_length.py:6:from <r>_shuffle? This measures the scale below which primes look random and
tools/exp_coherence_length.py:50:    - Compute <r> for shuffled version of same window
tools/exp_coherence_length.py:51:    - Return mean, std of <r>_prime and <r>_shuffle, plus z-score
tools/exp_coherence_length.py:71:                np.random.shuffle(shuf)
tools/exp_coherence_length.py:82:        # z-score: how far is the prime mean from the shuffle distribution?
tools/dnd_next.py:540:            "Confronta sempre con un controllo (shuffle, random, baseline). "
tools/exp_det_drift.py:5:Hypothesis (from operator tension DUALITA_DIPOLARE_VS_ILLUSORIA):
tools/exp_det_drift.py:16:  Null baseline: shuffled gaps (same distribution, destroyed order).
tools/exp_det_drift.py:60:    n_shuffles = 20
tools/exp_det_drift.py:77:        # Null: shuffled gaps (same marginal distribution, destroyed order)
tools/exp_det_drift.py:78:        det_shuffled = []
tools/exp_det_drift.py:79:        for _ in range(n_shuffles):
tools/exp_det_drift.py:81:            np.random.shuffle(sg)
tools/exp_det_drift.py:84:                det_shuffled.append(d)
tools/exp_det_drift.py:86:        det_shuf_mean = np.mean(det_shuffled)
tools/exp_det_drift.py:87:        det_shuf_std = np.std(det_shuffled)
tools/exp_det_drift.py:94:        print(f"  det(M_shuffled) = {det_shuf_mean:.6f} +/- {det_shuf_std:.6f}")
tools/exp_det_drift.py:108:            'det_shuffled_mean': float(det_shuf_mean),
tools/exp_det_drift.py:109:            'det_shuffled_std': float(det_shuf_std),
tools/exp_det_drift.py:117:    det_shuf = np.array([r['det_shuffled_mean'] for r in results])
tools/exp_det_drift.py:131:    print(f"det_shuffled range: {det_shuf.min():.6f} to {det_shuf.max():.6f}")
tools/exp_spectral_2d.py:118:    np.random.shuffle(s)
tools/exp_spectral_2d.py:182:    np.random.shuffle(small)
tools/exp_spectral_2d.py:183:    np.random.shuffle(large)
tools/exp_spectral_2d.py:278:    """Measure observables + shuffled null."""
tools/exp_spectral_2d.py:290:        np.random.shuffle(sh)
tools/exp_spectral_rigidity.py:5:Tests META + BOUNDARY with an observable independent from the r-statistic:
tools/exp_spectral_rigidity.py:16:Null: shuffle gaps (same marginal distribution, destroyed ordering).
tools/exp_spectral_rigidity.py:82:    n_shuffle = 30
tools/exp_spectral_rigidity.py:121:        sig2_shuf_all = np.zeros((n_shuffle, len(L_values)))
tools/exp_spectral_rigidity.py:122:        for s in range(n_shuffle):
tools/exp_spectral_rigidity.py:124:            np.random.shuffle(g_s)
tools/exp_spectral_rigidity.py:136:        # z-score real vs shuffle at L=10
tools/exp_spectral_rigidity.py:213:            print(f"  {name}: shuffle changes Sig2/L by {delta:+.4f} ({pct:+.1f}%), z={z:.1f} -> ORDERING MATTERS")
tools/exp_spectral_rigidity.py:215:            print(f"  {name}: shuffle effect {delta:+.4f} ({pct:+.1f}%), z={z:.1f} -> ordering negligible")
tools/dnd_zero_ising.py:8:Confronto con shuffle per separare struttura da statistica (come per i primi).
tools/dnd_zero_ising.py:136:        np.random.default_rng(42).shuffle(gaps_shuf)
tools/dnd_zero_ising.py:137:        m_shuf = misura_completa(gaps_shuf, f'{nome}_shuffle')
tools/dnd_zero_ising.py:144:            'shuffle': m_shuf,
tools/dnd_zero_ising.py:161:        ms = r['shuffle']
tools/dnd_zero_ising.py:194:        # Reale vs shuffle: dove la differenza e' massima?
tools/dnd_zero_ising.py:198:            if r['reale'] and r['shuffle'] and r['reale']['lvl2'] and r['shuffle']['lvl2']:
tools/dnd_zero_ising.py:199:                d = abs(r['reale']['lvl2'] - r['shuffle']['lvl2'])
tools/dnd_zero_ising.py:204:            print(f"\n  Massima differenza reale-shuffle: {max_delta:.4f} a {max_T}")
tools/exp_spectral_landscape.py:6:Identifies: GUE domains, Poisson domains, and BOUNDARY domains.
tools/exp_spectral_landscape.py:130:    np.random.shuffle(s)
tools/exp_spectral_landscape.py:254:    """Run observables on a domain + shuffled surrogates."""
tools/exp_spectral_landscape.py:272:        np.random.shuffle(s_shuf)
tools/exp_spectral_landscape.py:307:        return "BOUNDARY_low"
tools/exp_spectral_landscape.py:380:    for cls in ["POISSON", "BOUNDARY_low", "GOE-like", "GUE-like", "RIGID"]:
tools/exp_spectral_landscape.py:385:    boundary = [r for r in results if "error" not in r and classify(r["r_mean"]) == "BOUNDARY_low"]
tools/exp_boundary_shuffle_audit.py:2:exp_boundary_shuffle_audit.py — Is the GUE/Poisson classification tautological?
tools/exp_boundary_shuffle_audit.py:10:    2. r_shuffled = same, on 2000 random permutations of the gap sequence
tools/exp_boundary_shuffle_audit.py:11:    3. z = (r_original - mean(r_shuffled)) / std(r_shuffled)
tools/exp_boundary_shuffle_audit.py:49:def shuffle_test(gaps, n_shuffle=N_SHUFFLE):
tools/exp_boundary_shuffle_audit.py:53:    for _ in range(n_shuffle):
tools/exp_boundary_shuffle_audit.py:61:        'r_shuffled_mean': float(mu),
tools/exp_boundary_shuffle_audit.py:62:        'r_shuffled_std': float(sigma),
tools/exp_boundary_shuffle_audit.py:64:        'n_shuffle': n_shuffle,
tools/exp_boundary_shuffle_audit.py:270:            res = shuffle_test(gaps)
tools/exp_boundary_shuffle_audit.py:279:            dist_gue_s = abs(res['r_shuffled_mean'] - R_GUE)
tools/exp_boundary_shuffle_audit.py:280:            dist_poi_s = abs(res['r_shuffled_mean'] - R_POISSON)
tools/exp_boundary_shuffle_audit.py:281:            res['class_shuffled'] = 'GUE' if dist_gue_s < dist_poi_s else 'Poisson'
tools/exp_boundary_shuffle_audit.py:283:            res['class_changes'] = res['class_original'] != res['class_shuffled']
tools/exp_boundary_shuffle_audit.py:289:                print(f"  r_shuffled:    {res['r_shuffled_mean']:.4f} +/- {res['r_shuffled_std']:.4f}")
tools/exp_boundary_shuffle_audit.py:291:                print(f"  Class:         {res['class_original']} -> shuffle -> {res['class_shuffled']}")
tools/exp_boundary_shuffle_audit.py:329:        print(f"{key:<25} {res['r_original']:>7.4f} {res['r_shuffled_mean']:>7.4f} {res['z_score']:>7.1f} "
tools/exp_boundary_shuffle_audit.py:330:              f"{res['class_original']:>8} {res['class_shuffled']:>8} {verdict:>14}")
tools/exp_boundary_shuffle_audit.py:353:        'method': 'shuffle_audit_r_statistic',
tools/exp_boundary_shuffle_audit.py:354:        'n_shuffle': N_SHUFFLE,
tools/exp_boundary_shuffle_audit.py:359:    outfile = Path(__file__).parent / 'data' / 'boundary_shuffle_audit.json'
tools/exp_dipolar_angle_reference.py:12:relative to shuffle baseline. This answers: is the prime angle (-150 deg)
tools/exp_dipolar_angle_reference.py:55:def shuffle_baseline(gaps, n_shuffle=200):
tools/exp_dipolar_angle_reference.py:58:    for _ in range(n_shuffle):
tools/exp_dipolar_angle_reference.py:66:def dipolar_angle(gaps, n_shuffle=200):
tools/exp_dipolar_angle_reference.py:68:    Compute dipolar angle: direction of (delta_SR, delta_L1) relative to shuffle.
tools/exp_dipolar_angle_reference.py:72:    sr_shuf, l1_shuf = shuffle_baseline(gaps, n_shuffle)
tools/exp_dipolar_angle_reference.py:159:        theta, dsr, dl1, sr, l1, srs, l1s = dipolar_angle(gue_gaps, n_shuffle=50)
tools/exp_dipolar_angle_reference.py:179:        theta, dsr, dl1, sr, l1, srs, l1s = dipolar_angle(goe_gaps, n_shuffle=50)
tools/exp_dipolar_angle_reference.py:199:        theta, dsr, dl1, sr, l1, srs, l1s = dipolar_angle(poi_gaps, n_shuffle=50)
tools/exp_dipolar_angle_reference.py:219:        theta, dsr, dl1, sr, l1, srs, l1s = dipolar_angle(cramer_gaps, n_shuffle=50)
tools/lab_falsifier.py:60: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).
tools/exp_number_variance.py:84:    sv_shuffled_list = []
tools/exp_number_variance.py:87:        np.random.shuffle(spacings)
tools/exp_number_variance.py:90:        sv_shuffled_list.append(sv_shuf)
tools/exp_number_variance.py:91:    sv_shuffled = np.mean(sv_shuffled_list, axis=0)
tools/exp_number_variance.py:100:    coeffs_shuf = np.polyfit(log_L, sv_shuffled, 1)
tools/exp_number_variance.py:105:    lin_coeffs_shuf = np.polyfit(L_VALUES, sv_shuffled, 1)
tools/exp_number_variance.py:119:    print(f"  Log-fit slope (shuffled): {coeffs_shuf[0]:.4f}")
tools/exp_number_variance.py:121:    print(f"  Linear slope (shuffled):  {lin_coeffs_shuf[0]:.6f}")
tools/exp_number_variance.py:128:        print(f"  {L:4d} | {sv_primes[i]:8.4f} | {sv_shuffled[i]:8.4f} | {sv_gue[i]:7.4f} | {sv_poisson[i]:8.1f}")
tools/exp_number_variance.py:134:        "log_slope_shuffled": float(coeffs_shuf[0]),
tools/exp_number_variance.py:136:        "lin_slope_shuffled": float(lin_coeffs_shuf[0]),
tools/exp_number_variance.py:166:    "claim_under_test": "BOUNDARY: GUE->Poisson drift in primes",
tools/exp_perturbation_dimensionality_audit.py:26:PERT_NAMES = ["adjacent_swap", "block_shuffle", "large_gap_only", "uniform"]
tools/exp_perturbation_dimensionality_audit.py:144:def perturb_block_shuffle(gaps: np.ndarray, alpha: float, rng: np.random.Generator, block_size: int = 64) -> np.ndarray:
tools/exp_perturbation_dimensionality_audit.py:155:        rng.shuffle(out[start:end])
tools/exp_perturbation_dimensionality_audit.py:167:    rng.shuffle(vals)
tools/exp_perturbation_dimensionality_audit.py:179:    rng.shuffle(vals)
tools/exp_perturbation_dimensionality_audit.py:186:    "block_shuffle": perturb_block_shuffle,
tools/exp_perturbation_dimensionality_audit.py:274:        "full_shuffle_baseline": baseline,
tools/exp_perturbation_dimensionality_audit.py:275:        "original_vs_shuffle_z": z,
tools/exp_perturbation_dimensionality_audit.py:308:        "prime_shuffle_control": root_rng.permutation(prime),
tools/build_agent_field.py:504:        "larga, N piccolo vs grande): le percentuali ingannano perche' il denominatore "
tools/exp_ricci_primes.py:16:Null baseline: shuffled gaps (destroys correlations, preserves distribution).
tools/exp_ricci_primes.py:110:# --- Null baseline: shuffled gaps ---
tools/exp_ricci_primes.py:111:print("\n--- NULL BASELINE: shuffled gaps ---")
tools/exp_ricci_primes.py:118:    shuffled_gaps = gaps.copy()
tools/exp_ricci_primes.py:119:    np.random.shuffle(shuffled_gaps)
tools/exp_ricci_primes.py:122:    surr_primes[1:] = primes[0] + np.cumsum(shuffled_gaps)
tools/exp_selective_layer_decoupling.py:5:Consecutio from BOUNDARY (piano 60h): The two Markov layers (pairs->SR,L1; triples->SR2,L2)
tools/exp_selective_layer_decoupling.py:6:are coupled at the uniform-shuffle boundary (same critical alpha=0.334). But Mk1 surrogates
tools/exp_selective_layer_decoupling.py:9:Hypothesis: The coupling is an artifact of UNIFORM shuffle symmetry — not structural coupling.
tools/exp_selective_layer_decoupling.py:10:Uniform shuffle attacks all correlations at the same rate. Selective perturbation should decouple.
tools/exp_selective_layer_decoupling.py:225:                shuffled = rng.permutation(gaps)
tools/exp_selective_layer_decoupling.py:226:                perturbed = partial_perturbation(gaps, shuffled, alpha, rng)
tools/exp_scale_selective_perturbation.py:5:Consecutio from observable_rank_audit: under uniform partial shuffle, all
tools/exp_scale_selective_perturbation.py:8:shuffle destroys all correlations along the same axis?
tools/exp_scale_selective_perturbation.py:12:  2. Block-shuffle: shuffle within blocks of size B (preserves inter-block, destroys intra-block)
tools/exp_scale_selective_perturbation.py:13:  3. Large-gap-only: shuffle only the positions of above-median gaps (preserves small-gap ordering)
tools/exp_scale_selective_perturbation.py:20:is multi-dimensional and uniform shuffle was collapsing it.
tools/exp_scale_selective_perturbation.py:142:def perturb_block_shuffle(gaps, alpha, rng, block_size=50):
tools/exp_scale_selective_perturbation.py:143:    """Shuffle within blocks of size B. Alpha controls fraction of blocks shuffled."""
tools/exp_scale_selective_perturbation.py:147:    blocks_to_shuffle = rng.choice(n_blocks, size=max(1, int(alpha * n_blocks)), replace=False)
tools/exp_scale_selective_perturbation.py:148:    for b in blocks_to_shuffle:
tools/exp_scale_selective_perturbation.py:151:        rng.shuffle(g[start:end])
tools/exp_scale_selective_perturbation.py:156:    """Shuffle only above-median gap positions. Alpha controls fraction shuffled."""
tools/exp_scale_selective_perturbation.py:160:    n_shuffle = max(2, int(alpha * len(large_idx)))
tools/exp_scale_selective_perturbation.py:161:    chosen = rng.choice(large_idx, size=min(n_shuffle, len(large_idx)), replace=False)
tools/exp_scale_selective_perturbation.py:163:    rng.shuffle(vals)
tools/exp_scale_selective_perturbation.py:169:    """Standard uniform partial shuffle. Baseline from previous runs."""
tools/exp_scale_selective_perturbation.py:181:    'block_shuffle': perturb_block_shuffle,
tools/exp_scale_selective_perturbation.py:205:        # Full shuffle baseline (z-score reference)
tools/exp_scale_selective_perturbation.py:206:        full_shuffle_obs = {k: [] for k in obs_names}
tools/exp_scale_selective_perturbation.py:209:            rng.shuffle(g_shuf)
tools/exp_scale_selective_perturbation.py:212:                full_shuffle_obs[k].append(obs[k])
tools/exp_scale_selective_perturbation.py:213:        full_mean = {k: np.mean(v) for k, v in full_shuffle_obs.items()}
tools/exp_scale_selective_perturbation.py:214:        full_std = {k: np.std(v) for k, v in full_shuffle_obs.items()}
tools/exp_scale_selective_perturbation.py:217:        # retention = (obs_perturbed - obs_shuffle) / (obs_original - obs_shuffle)
tools/exp_scale_selective_perturbation.py:220:            'full_shuffle_mean': full_mean,
tools/exp_scale_selective_perturbation.py:221:            'full_shuffle_std': full_std,
tools/exp_boundary_growth.py:5:Tension: BOUNDARY (0.7)
tools/exp_boundary_growth.py:44:def shuffled_gaps_model(gaps, seed=None):
tools/exp_boundary_growth.py:47:    shuffled = gaps.copy()
tools/exp_boundary_growth.py:48:    rng.shuffle(shuffled)
tools/exp_boundary_growth.py:49:    return gap_ratio(shuffled)
tools/exp_boundary_growth.py:53:    print("BOUNDARY EXCESS GROWTH EXPERIMENT")
tools/exp_boundary_growth.py:97:            r_shuf_list.append(shuffled_gaps_model(gaps, seed=42 + trial))
tools/exp_boundary_growth.py:114:            "r_shuffled_mean": float(r_shuf_mean),
tools/exp_boundary_growth.py:115:            "r_shuffled_std": float(r_shuf_std),
tools/exp_boundary_growth.py:117:            "excess_shuffled": float(excess_s),
tools/exp_boundary_growth.py:131:    excesses_s = [r["excess_shuffled"] for r in results]
tools/exp_boundary_growth.py:192:    # The shuffled-gaps test is the key anti-tautology check:
tools/exp_boundary_growth.py:193:    # if <r>_prime == <r>_shuffled, then the gap-ratio statistic
tools/exp_boundary_growth.py:197:    print(f"    <r>_prime - <r>_shuffled mean = {np.mean(excesses_s):+.6f}")
tools/exp_boundary_growth.py:207:        "tension_id": "BOUNDARY",
tools/exp_boundary_growth.py:218:            "excess_shuffled_mean": float(np.mean(excesses_s)),
tools/exp_boundary_growth.py:219:            "excess_shuffled_slope": float(coeffs_s[0]),
tools/exp_cross_domain_dipolar_direction.py:15:For each: compute dipolar vector (SR, L1) relative to own shuffle baseline.
tools/exp_cross_domain_dipolar_direction.py:53:def dipolar_vector(gaps, n_shuffle=100, rng_seed=7777):
tools/exp_cross_domain_dipolar_direction.py:54:    """Compute dipolar vector relative to own shuffle baseline."""
tools/exp_cross_domain_dipolar_direction.py:59:    for _ in range(n_shuffle):
tools/exp_cross_domain_dipolar_direction.py:153:def run_experiment(N=50000, n_trials=20, n_shuffle=100):
tools/exp_cross_domain_dipolar_direction.py:165:    prime_dv = dipolar_vector(prime_gaps, n_shuffle)
tools/exp_cross_domain_dipolar_direction.py:188:            dv = dipolar_vector(spacings, n_shuffle=50, rng_seed=3000 + trial)
tools/exp_cross_domain_dipolar_direction.py:214:        dv = dipolar_vector(log_gaps, n_shuffle=50, rng_seed=5500 + trial)
tools/exp_cross_domain_dipolar_direction.py:238:            dv = dipolar_vector(gaps, n_shuffle=50, rng_seed=6500 + trial)
tools/exp_cross_domain_dipolar_direction.py:323:        'n_shuffle': n_shuffle,
tools/exp_cross_domain_dipolar_direction.py:338:    parser.add_argument('--n_shuffle', type=int, default=100,
tools/exp_cross_domain_dipolar_direction.py:339:                        help='Number of shuffles for baseline')
tools/exp_cross_domain_dipolar_direction.py:341:    run_experiment(N=args.N, n_trials=args.n_trials, n_shuffle=args.n_shuffle)
tools/exp_acf_z6z_mechanism.py:12:   residue-preserving shuffle.
tools/exp_acf_z6z_mechanism.py:156:    # 3a: Residue-preserving shuffle
tools/exp_acf_z6z_mechanism.py:157:    # For each residue class, shuffle gap values within the class
tools/exp_acf_z6z_mechanism.py:167:            np.random.shuffle(vals)
tools/exp_acf_z6z_mechanism.py:172:    print(f"  Residue-preserving shuffle:  xover={np.mean(xovers_rps):.1f}+/-{np.std(xovers_rps):.1f}, n_neg={np.mean(nnegs_rps):.1f}")
tools/exp_acf_z6z_mechanism.py:174:    # 3b: Full shuffle (baseline)
tools/exp_acf_z6z_mechanism.py:178:        np.random.shuffle(surr)
tools/exp_acf_z6z_mechanism.py:182:    print(f"  Full shuffle:                xover={np.mean(xovers_fs):.1f}+/-{np.std(xovers_fs):.1f}, n_neg={np.mean(nnegs_fs):.1f}")
tools/exp_acf_z6z_mechanism.py:243:        ("Residue-preserving shuffle", np.mean(xovers_rps), np.mean(nnegs_rps)),
tools/exp_acf_z6z_mechanism.py:244:        ("Full shuffle", np.mean(xovers_fs), np.mean(nnegs_fs)),
tools/exp_acf_z6z_mechanism.py:264:    #    if RPS crossover ~ shuffle → Z/6Z sequence doesn't matter (only ordering within class does)
tools/exp_acf_z6z_mechanism.py:266:    print(f"  Residue-preserving shuffle closer to raw ({xover_raw}) or shuffle ({np.mean(xovers_fs):.1f})?")
tools/exp_acf_z6z_mechanism.py:340:            'residue_preserving_shuffle': {
tools/exp_acf_z6z_mechanism.py:346:            'full_shuffle': {
tools/exp_acf_range_universality.py:11:Domains: primes, GUE, GOE, Poisson, primes_shuffled.
tools/exp_acf_range_universality.py:12:Null: shuffled gaps (order destroyed, marginals preserved).
tools/exp_acf_range_universality.py:153:        np.random.shuffle(sg)
tools/exp_acf_range_universality.py:176:    shuffled_gaps = gaps[:100_000].copy()
tools/exp_acf_range_universality.py:177:    np.random.shuffle(shuffled_gaps)
tools/exp_acf_range_universality.py:181:        'primes_shuffled': shuffled_gaps,
tools/exp_acf_range_universality.py:256:            np.random.shuffle(sw)
tools/exp_metric_tensor_diagnostic.py:14:4. Confronta con Cramer surrogates e shuffled gaps
tools/exp_metric_tensor_diagnostic.py:91:    'shuffled': {'DG_std': [], 'DG_ratio_mean': [], 'DG_ratio_median': [], 'r_mean': [], 'dR_std': []}
tools/exp_metric_tensor_diagnostic.py:94:print(f"Running {n_surr} surrogates each (Cramer + shuffled)...")
tools/exp_metric_tensor_diagnostic.py:143:        results_surr['shuffled']['DG_ratio_mean'].append(np.mean(sDG_r))
tools/exp_metric_tensor_diagnostic.py:144:        results_surr['shuffled']['DG_ratio_median'].append(np.median(sDG_r))
tools/exp_metric_tensor_diagnostic.py:145:    results_surr['shuffled']['DG_std'].append(np.std(sDG))
tools/exp_metric_tensor_diagnostic.py:148:    results_surr['shuffled']['r_mean'].append(np.mean(sr))
tools/exp_metric_tensor_diagnostic.py:155:    results_surr['shuffled']['dR_std'].append(np.std(sR - 2.0))
tools/exp_metric_tensor_diagnostic.py:165:z_DG_std_shuffled = zscore(np.std(DeltaGamma), results_surr['shuffled']['DG_std'])
tools/exp_metric_tensor_diagnostic.py:167:z_r_shuffled = zscore(r_mean_prime, results_surr['shuffled']['r_mean'])
tools/exp_metric_tensor_diagnostic.py:169:z_dR_shuffled = zscore(np.std(dR), results_surr['shuffled']['dR_std'])
tools/exp_metric_tensor_diagnostic.py:173:    z_DGratio_shuffled = zscore(np.median(DG_ratios_clipped), results_surr['shuffled']['DG_ratio_median'])
tools/exp_metric_tensor_diagnostic.py:175:    z_DGratio_cramer = z_DGratio_shuffled = float('nan')
tools/exp_metric_tensor_diagnostic.py:178:print(f"DeltaGamma std:  z_cramer={z_DG_std_cramer:.2f}, z_shuffled={z_DG_std_shuffled:.2f}")
tools/exp_metric_tensor_diagnostic.py:179:print(f"DeltaGamma ratio median: z_cramer={z_DGratio_cramer:.2f}, z_shuffled={z_DGratio_shuffled:.2f}")
tools/exp_metric_tensor_diagnostic.py:180:print(f"Gap ratio <r>:   z_cramer={z_r_cramer:.2f}, z_shuffled={z_r_shuffled:.2f}")
tools/exp_metric_tensor_diagnostic.py:181:print(f"dR std:          z_cramer={z_dR_cramer:.2f}, z_shuffled={z_dR_shuffled:.2f}")
tools/exp_metric_tensor_diagnostic.py:346:        "DG_std_vs_shuffled": round(z_DG_std_shuffled, 2),
tools/exp_metric_tensor_diagnostic.py:348:        "DG_ratio_median_vs_shuffled": round(z_DGratio_shuffled, 2),
tools/exp_metric_tensor_diagnostic.py:350:        "gap_r_vs_shuffled": round(z_r_shuffled, 2),
tools/exp_metric_tensor_diagnostic.py:352:        "dR_std_vs_shuffled": round(z_dR_shuffled, 2),
tools/exp_metric_tensor_diagnostic.py:380:        "shuffled_DG_std_mean": float(np.mean(results_surr['shuffled']['DG_std'])),
tools/exp_metric_tensor_diagnostic.py:381:        "shuffled_r_mean": float(np.mean(results_surr['shuffled']['r_mean'])),
tools/exp_metric_tensor_diagnostic.py:398:print(f"Z-scores DG: cramer={z_DG_std_cramer:.1f}, shuffled={z_DG_std_shuffled:.1f}")
tools/exp_metric_tensor_diagnostic.py:399:print(f"Z-scores DG ratio: cramer={z_DGratio_cramer:.1f}, shuffled={z_DGratio_shuffled:.1f}")
tools/exp_metric_tensor_diagnostic.py:400:print(f"Z-scores <r>: cramer={z_r_cramer:.1f}, shuffled={z_r_shuffled:.1f}")
tools/exp_metric_tensor_diagnostic.py:401:print(f"Z-scores dR: cramer={z_dR_cramer:.1f}, shuffled={z_dR_shuffled:.1f}")
tools/exp_observable_rank_audit.py:6:partial shuffle. This script asks whether those observables carry independent
tools/exp_observable_rank_audit.py:9:It measures retention curves from alpha-partial shuffles, then reports:
tools/exp_observable_rank_audit.py:10:  - original-vs-full-shuffle z for each observable
tools/exp_observable_rank_audit.py:24:from exp_3d_boundary_layers import get_primes, gue_gaps, partial_shuffle
tools/exp_observable_rank_audit.py:25:from observables_registry import (
tools/exp_observable_rank_audit.py:28:    compute_canonical,
tools/exp_observable_rank_audit.py:37:    return compute_canonical(gaps)
tools/exp_observable_rank_audit.py:40:def full_shuffle_baseline(gaps, n_trials, rng):
tools/exp_observable_rank_audit.py:61:            s = partial_shuffle(gaps, float(alpha), rng)
tools/exp_observable_rank_audit.py:113:    baseline = full_shuffle_baseline(gaps, n_baseline, rng)
tools/exp_observable_rank_audit.py:127:        "full_shuffle_baseline": baseline,
tools/exp_observable_rank_audit.py:128:        "original_vs_shuffle_z": z,
tools/exp_observable_rank_audit.py:144:    prime_shuffle = rng.permutation(prime_gaps).astype(float)
tools/exp_observable_rank_audit.py:147:        "prime_shuffle": prime_shuffle,
tools/exp_observable_rank_audit.py:161:        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
tools/exp_observable_rank_audit.py:173:    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
tools/exp_observable_rank_audit.py:181:        z = result["original_vs_shuffle_z"]
tools/exp_dipolar_crossover.py:5:Question: As ordering is gradually destroyed (GUE → shuffled), does the dipolar
tools/exp_dipolar_crossover.py:11:  2. For each alpha in [0, 1], partially shuffle the spacings:
tools/exp_dipolar_crossover.py:17:  5. Null: each alpha level vs its own full-shuffle (to isolate partial-shuffle from marginal)
tools/exp_dipolar_crossover.py:74:def partial_shuffle(spacings, alpha, rng):
tools/exp_dipolar_crossover.py:78:    n_shuffle = int(alpha * n)
tools/exp_dipolar_crossover.py:79:    if n_shuffle < 2:
tools/exp_dipolar_crossover.py:81:    idx = rng.choice(n, size=n_shuffle, replace=False)
tools/exp_dipolar_crossover.py:83:    rng.shuffle(vals)
tools/exp_dipolar_crossover.py:104:    # Full shuffle baseline (alpha=1) — per-matrix shuffle
tools/exp_dipolar_crossover.py:105:    shuffled_mats = []
tools/exp_dipolar_crossover.py:108:        rng.shuffle(sc)
tools/exp_dipolar_crossover.py:109:        shuffled_mats.append(sc)
tools/exp_dipolar_crossover.py:110:    sr1, l1_1, _, _ = compute_dipolar(shuffled_mats)
tools/exp_dipolar_crossover.py:121:            trial_mats = [partial_shuffle(s, alpha, rng_trial) for s in gue_mats]
tools/exp_dipolar_crossover.py:131:        # Dipolar angle relative to full-shuffle
tools/exp_dipolar_crossover.py:165:    # Prime shuffle baseline
tools/exp_dipolar_crossover.py:166:    prime_shuffled = []
tools/exp_dipolar_crossover.py:169:        rng.shuffle(cs)
tools/exp_dipolar_crossover.py:170:        prime_shuffled.append(cs)
tools/exp_dipolar_crossover.py:171:    sr_ps, l1_ps, _, _ = compute_dipolar(prime_shuffled)
tools/exp_dipolar_crossover.py:180:        "SR_shuffle": float(sr_ps), "L1_shuffle": float(l1_ps),
tools/exp_dipolar_crossover.py:209:        "method": "partial_shuffle_crossover",
tools/exp_dipolar_crossover.py:215:        "shuffle_baseline": {"SR": float(sr1), "L1": float(l1_1)},
tools/exp_modular_algebra_depth.py:14:  - Compare with shuffle baseline (50 shuffles)
tools/exp_modular_algebra_depth.py:16:- Null baseline: shuffle gap ordering, preserve distribution
tools/exp_modular_algebra_depth.py:93:    n_shuffles = 50
tools/exp_modular_algebra_depth.py:130:        shuffle_H1 = []
tools/exp_modular_algebra_depth.py:131:        shuffle_H2 = []
tools/exp_modular_algebra_depth.py:132:        shuffle_self = {i: [] for i in range(q)}
tools/exp_modular_algebra_depth.py:133:        for s in range(n_shuffles):
tools/exp_modular_algebra_depth.py:138:            shuffle_H1.append(H1s)
tools/exp_modular_algebra_depth.py:139:            shuffle_H2.append(H2s)
tools/exp_modular_algebra_depth.py:141:                shuffle_self[i].append(P1s[i, i])
tools/exp_modular_algebra_depth.py:146:            sh_mean = np.mean(shuffle_self[i])
tools/exp_modular_algebra_depth.py:147:            sh_std = np.std(shuffle_self[i])
tools/exp_modular_algebra_depth.py:154:                'shuffle_mean': sh_mean,
tools/exp_modular_algebra_depth.py:155:                'shuffle_std': sh_std,
tools/exp_modular_algebra_depth.py:160:        shuffle_I1 = [H0 - h1 for h1 in shuffle_H1]
tools/exp_modular_algebra_depth.py:161:        shuffle_I2 = [h1 - h2 for h1, h2 in zip(shuffle_H1, shuffle_H2)]
tools/exp_modular_algebra_depth.py:163:        z_I1 = (I1 - np.mean(shuffle_I1)) / np.std(shuffle_I1) if np.std(shuffle_I1) > 0 else 0
tools/exp_modular_algebra_depth.py:164:        z_I2 = (I2 - np.mean(shuffle_I2)) / np.std(shuffle_I2) if np.std(shuffle_I2) > 0 else 0
tools/exp_brody_calibration.py:12:If ordering fraction != 0 for i.i.d. Brody -> shuffle test has artifact.
tools/exp_brody_calibration.py:19:    python tools/exp_brody_calibration.py [--n-gaps 10000] [--n-brody 11] [--n-shuffles 50]
tools/exp_brody_calibration.py:62:def compute_observables(gaps, n_shuffles=50, rng=None):
tools/exp_brody_calibration.py:75:    for _ in range(n_shuffles):
tools/exp_brody_calibration.py:77:        rng.shuffle(shuf)
tools/exp_brody_calibration.py:144:    parser.add_argument('--n-shuffles', type=int, default=50)
tools/exp_brody_calibration.py:160:        obs = compute_observables(gaps, n_shuffles=args.n_shuffles, rng=rng)
tools/exp_brody_calibration.py:204:    obs_p = compute_observables(prime_gaps, n_shuffles=args.n_shuffles, rng=rng)
tools/exp_brody_calibration.py:213:    obs_g = compute_observables(gue_g, n_shuffles=args.n_shuffles, rng=rng)
tools/exp_brody_calibration.py:228:    obs_l = compute_observables(logistic_gaps, n_shuffles=args.n_shuffles, rng=rng)
tools/exp_brody_calibration.py:237:    obs_po = compute_observables(poisson_gaps, n_shuffles=args.n_shuffles, rng=rng)
tools/exp_brody_calibration.py:250:    obs_osc = compute_observables(osc_gaps, n_shuffles=args.n_shuffles, rng=rng)
tools/exp_brody_calibration.py:299:    print("If ordering fraction != 0 for Brody -> shuffle test has artifact.\n")
tools/exp_brody_calibration.py:311:        print("PASS: Ordering fraction < 5% for all Brody beta -> shuffle test is clean.")
tools/exp_brody_calibration.py:357:        'n_shuffles': args.n_shuffles,
tools/gap_ratio_primes.py:10:    g = gaps.copy(); np.random.shuffle(g)
tools/gap_ratio_primes.py:16:print(json.dumps({"r_primes": round(r_mean, 6), "r_shuffle_mean": round(mu, 6),
tools/gap_ratio_primes.py:17:    "r_shuffle_std": round(sigma, 6), "z_score": round(z, 2), "significativo": bool(sig),
tools/gap_ratio_primes.py:18:    "interpretazione": "gap correlati (non random)" if sig else "indistinguibile da shuffle"}, indent=2))
tools/exp_dR_brody_connection.py:16:Null baseline: 20 Cramer surrogates + 20 shuffled.
tools/exp_dR_brody_connection.py:196:    'shuffled': {'corr_beta_dRnorm': [], 'corr_beta_r': [], 'dR_acf1_mean': [], 'beta_slope': []},
tools/exp_dR_brody_connection.py:199:for surr_type in ['cramer', 'shuffled']:
tools/exp_dR_brody_connection.py:244:z_beta_dRnorm_shuffled = z(corr_beta_dRnorm, null_data['shuffled']['corr_beta_dRnorm'])
tools/exp_dR_brody_connection.py:247:z_dR_acf1_shuffled = z(dR_acf1_mean, null_data['shuffled']['dR_acf1_mean'])
tools/exp_dR_brody_connection.py:250:print(f"  corr(beta, dR_norm):  z_cramer={z_beta_dRnorm_cramer:.2f}, z_shuffled={z_beta_dRnorm_shuffled:.2f}")
tools/exp_dR_brody_connection.py:252:print(f"  dR_acf1 mean:         z_cramer={z_dR_acf1_cramer:.2f}, z_shuffled={z_dR_acf1_shuffled:.2f}")
tools/exp_dR_brody_connection.py:260:if null_data['shuffled']['dR_acf1_mean']:
tools/exp_dR_brody_connection.py:261:    s_acf1 = np.array(null_data['shuffled']['dR_acf1_mean'])
tools/exp_dR_brody_connection.py:316:        "corr_beta_dRnorm_vs_shuffled": z_beta_dRnorm_shuffled,
tools/exp_dR_brody_connection.py:319:        "dR_acf1_vs_shuffled": z_dR_acf1_shuffled,
tools/exp_dR_brody_connection.py:328:        "shuffled": {
tools/exp_dR_brody_connection.py:329:            "dR_acf1_mean": float(np.mean(null_data['shuffled']['dR_acf1_mean'])) if null_data['shuffled']['dR_acf1_mean'] else None,
tools/exp_dR_brody_connection.py:330:            "dR_acf1_std": float(np.std(null_data['shuffled']['dR_acf1_mean'])) if null_data['shuffled']['dR_acf1_mean'] else None,
tools/exp_dR_brody_connection.py:331:            "corr_beta_dRnorm_mean": float(np.mean(null_data['shuffled']['corr_beta_dRnorm'])) if null_data['shuffled']['corr_beta_dRnorm'] else None,
tools/exp_dR_brody_connection.py:356:if abs(z_dR_acf1_cramer) < 2 and abs(z_dR_acf1_shuffled) < 2:
tools/exp_markov_memory_by_gue_type.py:6:domains (primes, GUE matrices) retain GUE classification after shuffle, while
tools/exp_markov_memory_by_gue_type.py:19:- Compare H_k(real) vs H_k(shuffled) -> ordering fraction at each order
tools/exp_markov_memory_by_gue_type.py:22:Null baseline: 200 shuffles per domain (same distribution, destroyed order).
tools/exp_markov_memory_by_gue_type.py:79:def ordering_fraction(cats, order, n_shuffles=200):
tools/exp_markov_memory_by_gue_type.py:81:    Returns (H_real, H_shuffle_mean, H_shuffle_std, ordering_frac, z_score)."""
tools/exp_markov_memory_by_gue_type.py:84:    H_shuffles = []
tools/exp_markov_memory_by_gue_type.py:85:    for _ in range(n_shuffles):
tools/exp_markov_memory_by_gue_type.py:87:        H_shuffles.append(conditional_entropy(perm, order))
tools/exp_markov_memory_by_gue_type.py:89:    H_shuf_mean = np.mean(H_shuffles)
tools/exp_markov_memory_by_gue_type.py:90:    H_shuf_std = np.std(H_shuffles)
tools/exp_markov_memory_by_gue_type.py:136:    # Distribution-GUE domains (survive shuffle)
tools/exp_markov_memory_by_gue_type.py:149:    # Ordering-GUE domains (collapse to Poisson on shuffle)
tools/exp_markov_memory_by_gue_type.py:213:            H_real, H_shuf_mean, H_shuf_std, of, z = ordering_fraction(cats, k, n_shuffles=200)
tools/exp_crossover_phase_test.py:8:Multiple ordered sequences undergo the same partial-shuffle protocol. If ALL show
tools/exp_crossover_phase_test.py:34:def partial_shuffle(seq, alpha, rng):
tools/exp_crossover_phase_test.py:43:    rng.shuffle(vals)
tools/exp_crossover_phase_test.py:48:    """Dipolar coordinates relative to reference (full shuffle baseline)"""
tools/exp_crossover_phase_test.py:57:    # Full shuffle baseline (alpha=1.0)
tools/exp_crossover_phase_test.py:61:        shuffled = partial_shuffle(gaps, 1.0, rng)
tools/exp_crossover_phase_test.py:62:        sr_refs.append(spacing_ratio(shuffled))
tools/exp_crossover_phase_test.py:63:        l1_refs.append(lag1_acf(shuffled))
tools/exp_crossover_phase_test.py:72:            s = partial_shuffle(gaps, alpha, rng)
tools/exp_crossover_phase_test.py:291:        print("  VERDICT: UNIVERSAL — the phase transition is a property of the partial-shuffle METHOD")
tools/exp_desitter_unification.py:4:METRIC_TENSOR (0.9) + BRODY_CROSSOVER (0.85) + BOUNDARY (0.75) + GAP_ANTICORR (0.75)
tools/exp_desitter_unification.py:14:Null baseline: Cramer model + shuffled gaps (20 each).
tools/exp_desitter_unification.py:140:null_corrs_shuffled = {'r_acf1': [], 'r_dR': [], 'r_dRnorm': []}
tools/exp_desitter_unification.py:173:print(f"\nComputing {N_SURROGATES} shuffled surrogates...")
tools/exp_desitter_unification.py:189:        null_corrs_shuffled['r_acf1'].append(np.corrcoef(s_r, s_acf1)[0, 1])
tools/exp_desitter_unification.py:190:        null_corrs_shuffled['r_dR'].append(np.corrcoef(s_r, s_dR)[0, 1])
tools/exp_desitter_unification.py:191:        null_corrs_shuffled['r_dRnorm'].append(np.corrcoef(s_r, s_dRn)[0, 1])
tools/exp_desitter_unification.py:204:z_shuffled_r_dR = z_score(corr_r_dR, null_corrs_shuffled['r_dR'])
tools/exp_desitter_unification.py:206:z_shuffled_r_acf1 = z_score(corr_r_acf1, null_corrs_shuffled['r_acf1'])
tools/exp_desitter_unification.py:213:print(f"  corr(<r>, dR_std): prime={corr_r_dR:.4f}  null={np.mean(null_corrs_shuffled['r_dR']):.4f}+/-{np.std(null_corrs_shuffled['r_dR']):.4f}  z={z_shuffled_r_dR:.2f}")
tools/exp_desitter_unification.py:214:print(f"  corr(<r>, acf1):   prime={corr_r_acf1:.4f}  null={np.mean(null_corrs_shuffled['r_acf1']):.4f}+/-{np.std(null_corrs_shuffled['r_acf1']):.4f}  z={z_shuffled_r_acf1:.2f}")
tools/exp_desitter_unification.py:248:    "tension": "METRIC_TENSOR + BRODY_CROSSOVER + BOUNDARY + GAP_ANTICORR",
tools/exp_desitter_unification.py:279:    "null_shuffled": {
tools/exp_desitter_unification.py:281:        "corr_r_dR_mean": float(np.mean(null_corrs_shuffled['r_dR'])) if null_corrs_shuffled['r_dR'] else None,
tools/exp_desitter_unification.py:282:        "corr_r_dR_std": float(np.std(null_corrs_shuffled['r_dR'])) if null_corrs_shuffled['r_dR'] else None,
tools/exp_desitter_unification.py:283:        "z_r_dR": float(z_shuffled_r_dR),
tools/exp_desitter_unification.py:284:        "z_r_acf1": float(z_shuffled_r_acf1),
tools/exp_desitter_unification.py:311:if abs(z_shuffled_r_dR) > 2:
tools/exp_desitter_unification.py:312:    print(f"  SIGNIFICANT vs shuffled: z={z_shuffled_r_dR:.2f}")
tools/exp_brody_crossover.py:189:        "tension_id": "BOUNDARY+METRIC_TENSOR",
tools/exp_cross_observable_consistency.py:16:4. Null: shuffled primes (should show β_r ≈ β_Σ since no ordering channel)
tools/exp_cross_observable_consistency.py:144:# ── Measure shuffled primes (null) ──────────────────────────────────
tools/exp_cross_observable_consistency.py:145:print("\n=== PRIMES (shuffled, 20 trials) ===")
tools/exp_cross_observable_consistency.py:152:    np.random.shuffle(g_shuf)
tools/exp_cross_observable_consistency.py:155:    # Reconstruct positions from shuffled gaps
tools/exp_cross_observable_consistency.py:247:    "shuffle": {
tools/gue_gap_test.py:20:n_shuffle = 100
tools/gue_gap_test.py:21:r_shuffled = []
tools/gue_gap_test.py:22:for _ in range(n_shuffle):
tools/gue_gap_test.py:24:    np.random.shuffle(sh)
tools/gue_gap_test.py:25:    r_shuffled.append(r_statistic(np.diff(sh)))
tools/gue_gap_test.py:27:r_shuffled = np.array(r_shuffled)
tools/gue_gap_test.py:28:r_mean_shuf = float(np.mean(r_shuffled))
tools/gue_gap_test.py:29:r_std_shuf = float(np.std(r_shuffled))
tools/gue_gap_test.py:37:    interp = f"r={r_data:.4f} > 0.5 e z={z_score:.2f} vs shuffle: correlazione GUE-like nei gap dei primi"
tools/gue_gap_test.py:39:    interp = f"r={r_data:.4f} > 0.5 ma z={z_score:.2f}: non significativamente diverso da shuffle"
tools/gue_gap_test.py:46:    "r_statistic_shuffle_mean": round(r_mean_shuf, 6),
tools/gue_gap_test.py:47:    "r_statistic_shuffle_std": round(r_std_shuf, 6),
tools/dnd_domandatore.py:174:        'id': f'BOUNDARY_{tid}',
tools/exp_magnitude_psd_from_acf.py:20:  7. Null: shuffled magnitude channel
tools/exp_magnitude_psd_from_acf.py:122:    parser.add_argument('--n_shuffle', type=int, default=15)
tools/exp_magnitude_psd_from_acf.py:205:    # 5. Null baseline: shuffled magnitude
tools/exp_magnitude_psd_from_acf.py:206:    print(f"\n--- Null baseline: shuffled magnitude ({args.n_shuffle} surrogates) ---")
tools/exp_magnitude_psd_from_acf.py:207:    shuffle_slopes = []
tools/exp_magnitude_psd_from_acf.py:209:    for i in range(args.n_shuffle):
tools/exp_magnitude_psd_from_acf.py:214:        shuffle_slopes.append(sl_s)
tools/exp_magnitude_psd_from_acf.py:215:    shuffle_slopes = np.array(shuffle_slopes)
tools/exp_magnitude_psd_from_acf.py:216:    z_direct = (slope_direct - shuffle_slopes.mean()) / shuffle_slopes.std()
tools/exp_magnitude_psd_from_acf.py:217:    print(f"  Shuffle slope: {shuffle_slopes.mean():.4f} +/- {shuffle_slopes.std():.4f}")
tools/exp_magnitude_psd_from_acf.py:218:    print(f"  Direct slope z-score vs shuffle: {z_direct:.1f}")
tools/exp_magnitude_psd_from_acf.py:265:    print(f"Shuffle baseline slope:              {shuffle_slopes.mean():+.4f}")
tools/exp_magnitude_psd_from_acf.py:284:        'slope_shuffle': float(shuffle_slopes.mean()),
tools/exp_magnitude_psd_from_acf.py:290:        'z_vs_shuffle': float(z_direct),
tools/exp_two_channel_universality.py:24:  MAG_SHUFFLE:   Keep real Z/6Z class sequence, shuffle gap sizes within each
tools/exp_two_channel_universality.py:202:def generate_class_shuffle(gaps_real, classes_left_real, classes_right_real):
tools/exp_two_channel_universality.py:211:def generate_mag_shuffle(gaps_real, classes_left_real, classes_right_real):
tools/exp_two_channel_universality.py:212:    """Keep real class sequence, shuffle gap sizes within each transition type."""
tools/exp_two_channel_universality.py:250:        'CLASS_SHUFFLE': generate_class_shuffle,
tools/exp_two_channel_universality.py:251:        'MAG_SHUFFLE': generate_mag_shuffle,
tools/dnd_scenario.py:163:        - BOUNDARY: eccesso decresce, slope=-0.0027/decade
tools/exp_brody_flow.py:12:- Shuffle: same gaps per window, order destroyed → β_shuffle(N)
tools/exp_brody_flow.py:16:    python tools/exp_brody_flow.py [--n-max 2000000] [--window 5000] [--step 2000] [--n-shuffle 20]
tools/exp_brody_flow.py:92:def run(n_max=2_000_000, window=5000, step=2000, n_shuffle=20):
tools/exp_brody_flow.py:107:        "window": window, "step": step, "n_shuffle": n_shuffle,
tools/exp_brody_flow.py:129:        for _ in range(n_shuffle):
tools/exp_brody_flow.py:131:            np.random.shuffle(g_shuf)
tools/exp_brody_flow.py:139:        for _ in range(n_shuffle):
tools/exp_brody_flow.py:153:            "shuffle_beta_mean": float(np.mean(betas_shuf)),
tools/exp_brody_flow.py:154:            "shuffle_beta_std": float(np.std(betas_shuf)),
tools/exp_brody_flow.py:155:            "shuffle_r_mean": float(np.mean(rs_shuf)),
tools/exp_brody_flow.py:189:    # Is the slope significant vs shuffle?
tools/exp_brody_flow.py:191:    for _ in range(n_shuffle):
tools/exp_brody_flow.py:192:        shuf_betas = [w["shuffle_beta_mean"] + np.random.normal(0, w["shuffle_beta_std"])
tools/exp_brody_flow.py:196:    results["shuffle_slope_mean"] = float(np.mean(shuf_slopes))
tools/exp_brody_flow.py:197:    results["shuffle_slope_std"] = float(np.std(shuf_slopes))
tools/exp_brody_flow.py:199:        results["slope_z_vs_shuffle"] = float(
tools/exp_brody_flow.py:211:    parser.add_argument("--n-shuffle", type=int, default=20)
tools/exp_brody_flow.py:214:    results = run(args.n_max, args.window, args.step, args.n_shuffle)
tools/exp_brody_flow.py:229:    if "slope_z_vs_shuffle" in results:
tools/exp_brody_flow.py:230:        print(f"Slope z-score vs shuffle: {results['slope_z_vs_shuffle']:.2f}")
tools/m_spectro.py:134:def residuo_ordine(seq, n_shuffle=100, seed=42):
tools/m_spectro.py:137:    M(ordinato) - media(M(shuffle)) = cosa M vede nell'ordine
tools/m_spectro.py:142:        delta_lvl1/lvl2/rG: ordinato vs shuffle con sigma
tools/m_spectro.py:157:    for _ in range(n_shuffle):
tools/m_spectro.py:159:        rng.shuffle(g)
tools/m_spectro.py:197:            'shuffle': float(np.mean(lvl1_shuf)),
tools/m_spectro.py:202:            'shuffle': float(np.mean(lvl2_shuf)),
tools/m_spectro.py:207:            'shuffle': float(np.mean(rG_shuf)),
tools/m_spectro.py:228:    r_res = residuo_ordine(seq, n_shuffle=30, seed=42)
tools/m_spectro.py:264:def residuo_stabile(seq, n_shuffle=100, n_iter=6, seed=42):
tools/m_spectro.py:268:    Ad ogni livello k, calcola M^k(ordinato) vs M^k(shuffle) e misura
tools/m_spectro.py:291:        # M^livello su n_shuffle permutazioni
tools/m_spectro.py:294:        for _ in range(n_shuffle):
tools/m_spectro.py:296:            rng_l.shuffle(g)
tools/m_spectro.py:343:               max_k=8, n_shuffle=20, seed=42):
tools/m_spectro.py:372:        r = residuo_stabile(seq, n_iter=k, n_shuffle=max(n_shuffle, 50), seed=seed)
tools/m_spectro.py:390:            sigmas_loc = _campo_locale_sigmas(seq, w, k, n_shuffle, seed)
tools/m_spectro.py:483:        insight.append(f"Lato D stabile: l'ordine della sequenza e' PIU' strutturato dello shuffle sotto M")
tools/m_spectro.py:485:        insight.append(f"Lato ND stabile: M inverte la struttura — l'ordine e' MENO strutturato dello shuffle")
tools/m_spectro.py:515:def _campo_locale_sigmas(seq, window, n_iter, n_shuffle, seed):
tools/m_spectro.py:531:        for _ in range(n_shuffle):
tools/m_spectro.py:533:            rng.shuffle(c)
tools/m_spectro.py:732:        res_ord = residuo_ordine(seq, n_shuffle=50)
tools/exp_boundary_coherence.py:19:Null baseline: shuffled prime gaps (same distribution, destroyed ordering).
tools/exp_boundary_coherence.py:185:        shuffled_gaps = gaps.copy()
tools/exp_boundary_coherence.py:186:        np.random.shuffle(shuffled_gaps)
tools/exp_boundary_coherence.py:187:        shuf_obs = compute_all_observables(shuffled_gaps)
tools/exp_boundary_coherence.py:196:            "shuffle_raw": shuf_obs,
tools/exp_boundary_coherence.py:197:            "shuffle_tau": shuf_tau,
tools/exp_boundary_coherence.py:198:            "shuffle_tau_mean": np.mean(list(shuf_tau.values())),
tools/exp_boundary_coherence.py:199:            "shuffle_tau_std": np.std(list(shuf_tau.values())),
tools/exp_boundary_coherence.py:204:    print("MULTI-OBSERVABLE BOUNDARY COHERENCE")
tools/exp_boundary_coherence.py:230:        t = r["shuffle_tau"]
tools/exp_boundary_coherence.py:232:        print(f"  {label:<15} {vals[0]:>10.3f} {vals[1]:>10.3f} {vals[2]:>10.3f} {vals[3]:>10.3f} {vals[4]:>10.3f} │ {r['shuffle_tau_mean']:>6.3f} {r['shuffle_tau_std']:>6.3f}")
tools/exp_boundary_coherence.py:236:    # Ordering signal = |τ_primes - τ_shuffle| averaged
tools/exp_boundary_coherence.py:245:        shuf_vals = np.array(list(r["shuffle_tau"].values()))
tools/exp_boundary_coherence.py:248:        coherence_shuffle = np.std(shuf_vals)
tools/exp_boundary_coherence.py:253:        deltas = {k: r["tau"][k] - r["shuffle_tau"][k] for k in obs_names}
tools/exp_boundary_coherence.py:256:        print(f"    τ coherence (std): primes={coherence_prime:.4f}, shuffle={coherence_shuffle:.4f}")
tools/exp_boundary_coherence.py:258:        print(f"    Per-observable Δτ (prime - shuffle):")
tools/exp_boundary_coherence.py:263:        results[label]["coherence_shuffle"] = coherence_shuffle
tools/dnd_autoricerca.py:455:def _null_baseline(signal, metadata, n_shuffles=3):
tools/dnd_autoricerca.py:463:        'spacing_null': str,  # classificazione prevalente nello shuffle
tools/dnd_autoricerca.py:476:    for _ in range(n_shuffles):
tools/dnd_autoricerca.py:483:            shuffled = np.random.exponential(scale=np.mean(signal), size=len(signal))
tools/dnd_autoricerca.py:485:            shuffled = np.random.uniform(
tools/dnd_autoricerca.py:490:        n = len(shuffled)
tools/dnd_autoricerca.py:495:        mediana = float(np.median(shuffled))
tools/dnd_autoricerca.py:496:        d_int, nd_int = scissione(shuffled, mediana)
tools/dnd_autoricerca.py:511:                sp = shuffled[shuffled > 0]
tools/dnd_autoricerca.py:513:                sp = np.diff(np.sort(shuffled))
tools/dnd_autoricerca.py:1539:        # BOUNDARY: confine GUE/Poisson — prosegui esplorando il confine
tools/exp_two_channel_shuffle_audit.py:3:exp_two_channel_shuffle_audit.py — META tautology audit on two-channel decomposition.
tools/exp_two_channel_shuffle_audit.py:11:    python tools/exp_two_channel_shuffle_audit.py [--N 100000] [--n_shuffle 50]
tools/exp_two_channel_shuffle_audit.py:183:    parser.add_argument('--n_shuffle', type=int, default=50, help='Number of shuffle trials')
tools/exp_two_channel_shuffle_audit.py:190:    print(f"N={args.N} primes, {args.n_shuffle} shuffle trials\n")
tools/exp_two_channel_shuffle_audit.py:202:    shuffle_results = []
tools/exp_two_channel_shuffle_audit.py:203:    for i in range(args.n_shuffle):
tools/exp_two_channel_shuffle_audit.py:204:        shuffled = gaps.copy()
tools/exp_two_channel_shuffle_audit.py:205:        rng.shuffle(shuffled)
tools/exp_two_channel_shuffle_audit.py:206:        r = measure_properties(shuffled, f"shuffle_{i}")
tools/exp_two_channel_shuffle_audit.py:207:        shuffle_results.append(r)
tools/exp_two_channel_shuffle_audit.py:209:    # Aggregate shuffle statistics
tools/exp_two_channel_shuffle_audit.py:213:    print(f"\n--- SHUFFLE DISTRIBUTION (n={args.n_shuffle}) ---")
tools/exp_two_channel_shuffle_audit.py:219:        vals = [r[k] for r in shuffle_results]
tools/exp_two_channel_shuffle_audit.py:224:        survives = abs(z) < 2.0  # within 2 sigma of shuffle → tautology
tools/exp_two_channel_shuffle_audit.py:227:            'shuffle_mean': round(float(mean_s), 4),
tools/exp_two_channel_shuffle_audit.py:228:            'shuffle_std': round(float(std_s), 4),
tools/exp_two_channel_shuffle_audit.py:230:            'survives_shuffle': bool(survives),
tools/exp_two_channel_shuffle_audit.py:241:    print(f"TAUTOLOGIES (survive shuffle, |z|<2): {tautologies}")
tools/exp_two_channel_shuffle_audit.py:242:    print(f"GENUINE (break under shuffle, |z|>=2): {genuine}")
tools/exp_two_channel_shuffle_audit.py:247:        'experiment': 'two_channel_shuffle_audit',
tools/exp_two_channel_shuffle_audit.py:249:        'n_shuffle': args.n_shuffle,
tools/exp_two_channel_shuffle_audit.py:257:    out_path = 'tools/data/two_channel_shuffle_audit.json'
tools/riemann_R.py:371:    print("  Res(f^n, z0) = g(z0)/h'(z0) dove g = numeratore, h = denominatore")
tools/exp_psd_prime_gaps.py:12:    python tools/exp_psd_prime_gaps.py [--n_primes N] [--n_shuffles K]
tools/exp_psd_prime_gaps.py:46:def run_experiment(n_primes=500_000, n_shuffles=20, nperseg=4096):
tools/exp_psd_prime_gaps.py:83:    # Null baseline: shuffled gaps (destroys correlations => white noise)
tools/exp_psd_prime_gaps.py:84:    psd_shuffles = []
tools/exp_psd_prime_gaps.py:85:    for i in range(n_shuffles):
tools/exp_psd_prime_gaps.py:86:        shuffled = gaps.copy()
tools/exp_psd_prime_gaps.py:87:        np.random.shuffle(shuffled)
tools/exp_psd_prime_gaps.py:88:        _, psd_s = compute_psd(shuffled, nperseg=nperseg)
tools/exp_psd_prime_gaps.py:89:        psd_shuffles.append(psd_s)
tools/exp_psd_prime_gaps.py:90:    psd_shuffle_mean = np.mean(psd_shuffles, axis=0)
tools/exp_psd_prime_gaps.py:91:    psd_shuffle_std = np.std(psd_shuffles, axis=0)
tools/exp_psd_prime_gaps.py:93:    # Analysis: compare prime PSD with theory and shuffle
tools/exp_psd_prime_gaps.py:101:    # 2. z-score of prime PSD vs shuffle at low frequencies
tools/exp_psd_prime_gaps.py:103:    if np.any(low_f) and np.any(psd_shuffle_std[low_f] > 0):
tools/exp_psd_prime_gaps.py:104:        z_low = (psd_prime[low_f] - psd_shuffle_mean[low_f]) / psd_shuffle_std[low_f]
tools/exp_psd_prime_gaps.py:111:    if np.any(high_f) and np.any(psd_shuffle_std[high_f] > 0):
tools/exp_psd_prime_gaps.py:112:        z_high = (psd_prime[high_f] - psd_shuffle_mean[high_f]) / psd_shuffle_std[high_f]
tools/exp_psd_prime_gaps.py:136:    dip_ratio_shuffle = psd_shuffle_mean[idx_low] / psd_shuffle_mean[idx_high]
tools/exp_psd_prime_gaps.py:145:        "n_shuffles": n_shuffles,
tools/exp_psd_prime_gaps.py:147:        "z_low_freq_vs_shuffle": float(z_low_mean),
tools/exp_psd_prime_gaps.py:148:        "z_high_freq_vs_shuffle": float(z_high_mean),
tools/exp_psd_prime_gaps.py:152:        "dip_ratio_shuffle": float(dip_ratio_shuffle),
tools/exp_psd_prime_gaps.py:163:    print(f"z-score vs shuffle at LOW freq (0.005-0.05): {z_low_mean:.2f}")
tools/exp_psd_prime_gaps.py:164:    print(f"z-score vs shuffle at HIGH freq (0.3-0.49): {z_high_mean:.2f}")
tools/exp_psd_prime_gaps.py:165:    print(f"Dip ratio S(0.01)/S(0.48): prime={dip_ratio:.3f}, shuffle={dip_ratio_shuffle:.3f}")
tools/exp_psd_prime_gaps.py:182:    if dip_ratio < 0.8 and dip_ratio_shuffle > 0.9:
tools/exp_psd_prime_gaps.py:183:        print(f"LOW-FREQ DIP confirmed: prime dip={dip_ratio:.3f} vs shuffle={dip_ratio_shuffle:.3f}")
tools/exp_psd_prime_gaps.py:189:    return results, freqs, psd_prime, psd_theory, psd_shuffle_mean, psd_shuffle_std
tools/exp_psd_prime_gaps.py:195:    parser.add_argument("--n_shuffles", type=int, default=20)
tools/exp_psd_prime_gaps.py:201:        n_shuffles=args.n_shuffles,

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

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

Reusable META/DUALITA audit for transferring the BOUNDARY_LAYER_GATE.

Question:
    Does the denominator-collapse gate discriminate dipolar duality from
    illusory duality, or does it become a local BOUNDARY metric when moved
    away from GUE/Poisson?

The script builds a controlled layer map between:

- DIPOLARE: a coherent golden/Beatty gap sequence generated by the orbit of
  floor((n + phase) * phi). Its gaps are the symbolic det=-1 D-ND pole in the
  minimal positive alphabet {1, 2}.
- ILLUSORIA: a full random permutation of the same gaps. This preserves the
  one-point distribution and first moment but removes the generating order.

Layer beta replaces a beta fraction of dipolar positions with values from the
illusory permutation. Canonical observables and original-vs-shuffle z gates are
imported from observables_registry.py.
"""

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())
PHI = (1.0 + 5.0**0.5) / 2.0


def dipolar_gaps(n_gaps: int, phase: float) -> np.ndarray:
    """Return coherent golden Beatty gaps in {1, 2}."""
    n = np.arange(n_gaps + 1, dtype=float) + float(phase)
    positions = np.floor(n * PHI).astype(float)
    gaps = np.diff(positions)
    mean = float(np.mean(gaps))
    return gaps / mean if mean > 1e-15 else gaps


def beta_layer_gaps(
    dipolar: np.ndarray,
    beta: float,
    rng: np.random.Generator,
) -> np.ndarray:
    """Replace a beta fraction of coherent positions with a shuffled copy."""
    illusory = rng.permutation(dipolar)
    if beta <= 0.0:
        out = dipolar.copy()
    elif beta >= 1.0:
        out = illusory.copy()
    else:
        mask = rng.random(len(dipolar)) < beta
        out = dipolar.copy()
        out[mask] = illusory[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 z scores."""
    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])

    shuffle_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
        shuffle_std[name] = sd
        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
    return original, shuffle_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 layers by standardized distance to dipolar/illusory endpoints."""
    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)

    dip_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
    ill_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
    endpoints = np.vstack([dip_vectors, ill_vectors])
    scale = np.std(endpoints, axis=0, ddof=1)
    scale[scale <= 1e-15] = 1.0
    dip_centroid = np.mean(dip_vectors, axis=0)
    ill_centroid = np.mean(ill_vectors, axis=0)
    endpoint_distance = float(np.linalg.norm((ill_centroid - dip_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_dip = float(np.linalg.norm((x - dip_centroid) / scale))
            d_ill = float(np.linalg.norm((x - ill_centroid) / scale))
            denom = d_dip + d_ill
            coord = float((d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
            margin = float(abs(d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
            coords.append(coord)
            margins.append(margin)
            labels.append("dipolar" if d_dip < d_ill else "illusory")
        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,
            "illusory_label_fraction": float(np.mean(np.array(labels) == "illusory")),
        }

    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}
        z_values = {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)
                z_values[name].append(row["z"][name])
        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()},
            "z_mean": {name: float(np.mean(vals)) for name, vals in z_values.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):
        phase = float(rng.random())
        base = dipolar_gaps(args.n_gaps, phase)
        for beta in betas:
            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
            gaps = beta_layer_gaps(base, 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,
                    "phase": phase,
                    "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": "duality_gate_transfer",
        "category": "gate_transferability",
        "question": "Does the denominator-collapse gate transfer from BOUNDARY to DUALITA_DIPOLARE_VS_ILLUSORIA?",
        "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}"
    )
    source = gated_classification if gate["endpoint_stable_observables"] else all_classification
    print("beta stable_count margin ambiguous illusory_fraction")
    for beta in betas:
        key = f"{beta:.3f}"
        stable_count = gate["layers"][key]["stable_count_mean"]
        layer = source["layers"].get(key, {})
        print(
            f"{beta:>4.2f} {stable_count:>12.3f} "
            f"{layer.get('margin_mean', 0.0):>8.3f} "
            f"{layer.get('ambiguous_fraction', 0.0):>9.3f} "
            f"{layer.get('illusory_label_fraction', 0.0):>17.3f}"
        )
    print(f"saved {out_path}")
    return output


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--n-gaps", type=int, default=4096)
    parser.add_argument("--n-replicates", type=int, default=24)
    parser.add_argument("--n-beta", type=int, default=11)
    parser.add_argument("--n-baseline", type=int, default=40)
    parser.add_argument("--z-min", type=float, default=2.0)
    parser.add_argument("--seed", type=int, default=202605070803)
    parser.add_argument("--out", default="tools/data/duality_gate_transfer.json")
    args = parser.parse_args()
    run(args)


if __name__ == "__main__":
    main()

exec
/bin/bash -lc "sed -n '1,280p' tools/exp_boundary_mixture_gate.py" in /opt/MM_D-ND
 succeeded in 0ms:
#!/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)


exec
/bin/bash -lc "sed -n '1,300p' 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}]"
    )


__all__ = [
    "OBSERVABLES_REGISTRY_VERSION",
    "OBSERVABLES_CANONICAL",
    "OBSERVABLES_VARIANTS",
    "SR",
    "SR2",
    "L1",
    "L2",
    "triple_var",
    "SR_local_rigidity",
    "triple_var_normalized",
    "compute_canonical",
    "report_header",
]


if __name__ == "__main__":
    # Smoke test: canonical observables on a simple gap series
    rng = np.random.default_rng(42)
    gue_like = rng.gamma(shape=2.0, scale=0.5, size=200)
    res = compute_canonical(gue_like)
    print(f"Registry version: {OBSERVABLES_REGISTRY_VERSION}")
    print(f"\nGUE-like 200 gaps:")
    for name, val in res.items():
        print(f"  {name:12s} = {val:.6f}")
    print(f"\nVariants (explicit naming, not aliases):")
    print(f"  SR_local_rigidity     = {SR_local_rigidity(gue_like):.6f}")
    print(f"  triple_var_normalized = {triple_var_normalized(gue_like):.6f}")

exec
/bin/bash -lc 'rg -n "def generate|logistic|thue|fibonacci|pink|coherent|illusory|markov|periodic|random" tools/exp_*.py' in /opt/MM_D-ND
 succeeded in 0ms:
tools/exp_acf_amplitude_scaling.py:85:            shuf = np.random.permutation(window_gaps)
tools/exp_alpha_stability.py:128:            np.random.shuffle(shuf)
tools/exp_boundary_gue_poisson.py:13:2. Generate Cramér random primes: each integer n is "prime" with probability 1/ln(n).
tools/exp_boundary_gue_poisson.py:39:def cramer_random_primes(N_max, rng):
tools/exp_boundary_gue_poisson.py:40:    """Generate Cramér random 'primes': each n>=2 is included with prob 1/ln(n)."""
tools/exp_boundary_gue_poisson.py:46:    mask = rng.random(len(n_vals)) < probs
tools/exp_boundary_gue_poisson.py:85:    print(f"Generating {N_CRAMER} Cramér random prime sets...")
tools/exp_boundary_gue_poisson.py:86:    rng = np.random.default_rng(42)
tools/exp_boundary_gue_poisson.py:89:        cp = cramer_random_primes(N_MAX, rng)
tools/exp_brody_crossover.py:61:    return np.random.exponential(densities, size=n_gaps)
tools/exp_brody_crossover.py:108:            surr_gaps = np.random.exponential(mean_gap, size=len(win_gaps))
tools/exp_3d_boundary_layers.py:165:    rng = np.random.default_rng(args.seed)
tools/exp_cross_domain_dipolar_direction.py:13:  Structured: primes, logistic map r=4
tools/exp_cross_domain_dipolar_direction.py:55:    rng = np.random.default_rng(rng_seed)
tools/exp_cross_domain_dipolar_direction.py:82:    """Generate spacings from random matrix ensemble."""
tools/exp_cross_domain_dipolar_direction.py:84:        rng = np.random.default_rng()
tools/exp_cross_domain_dipolar_direction.py:94:        # Circular unitary ensemble: eigenvalues of random unitary
tools/exp_cross_domain_dipolar_direction.py:116:def gen_logistic(N, r=4.0, rng=None):
tools/exp_cross_domain_dipolar_direction.py:119:        rng = np.random.default_rng()
tools/exp_cross_domain_dipolar_direction.py:135:        rng = np.random.default_rng()
tools/exp_cross_domain_dipolar_direction.py:142:        rng = np.random.default_rng()
tools/exp_cross_domain_dipolar_direction.py:149:        rng = np.random.default_rng()
tools/exp_cross_domain_dipolar_direction.py:181:            rng = np.random.default_rng(100 * hash(ens_name) % 10000 + trial)
tools/exp_cross_domain_dipolar_direction.py:212:        rng = np.random.default_rng(5000 + trial)
tools/exp_cross_domain_dipolar_direction.py:213:        log_gaps = gen_logistic(N, r=4.0, rng=rng)
tools/exp_cross_domain_dipolar_direction.py:221:    domains['logistic_r4'] = {
tools/exp_cross_domain_dipolar_direction.py:236:            rng = np.random.default_rng(6000 + hash(name) % 1000 + trial)
tools/exp_dR_brody_connection.py:26:np.random.seed(42)
tools/exp_dR_brody_connection.py:191:rng = np.random.default_rng(42)
tools/exp_dR_brody_connection.py:207:            # Shuffled: same gaps, random order
tools/exp_dipolar_crossover.py:10:  1. Generate GUE bulk spacings from random matrices
tools/exp_dipolar_crossover.py:12:     - Select floor(alpha * N) random positions
tools/exp_dipolar_crossover.py:96:    rng = np.random.default_rng(seed)
tools/exp_dipolar_crossover.py:120:            rng_trial = np.random.default_rng(seed + trial * 1000 + int(alpha * 100))
tools/exp_acf_z6z_mechanism.py:80:    np.random.seed(42)
tools/exp_acf_z6z_mechanism.py:167:            np.random.shuffle(vals)
tools/exp_acf_z6z_mechanism.py:178:        np.random.shuffle(surr)
tools/exp_acf_z6z_mechanism.py:187:        surr = np.random.exponential(ln_p)
tools/exp_acf_z6z_mechanism.py:196:        raw_exp = np.random.exponential(ln_p)
tools/exp_acf_z6z_mechanism.py:212:        ar[0] = np.random.normal(0, 1)
tools/exp_acf_z6z_mechanism.py:214:            ar[i] = acf1_prime * ar[i - 1] + np.random.normal(0, 1)
tools/exp_acf_z6z_mechanism.py:224:        ar[0] = np.random.normal(0, 1)
tools/exp_acf_z6z_mechanism.py:226:            ar[i] = acf1_prime * ar[i - 1] + np.random.normal(0, 1)
tools/exp_geodesic_deviation_primes.py:83:    np.random.shuffle(shuf_gaps)
tools/exp_geodesic_deviation_primes.py:126:print(f"\n--- CRAMER SURROGATES (random model primes) ---")
tools/exp_geodesic_deviation_primes.py:133:        gap = max(2, round(np.random.exponential(np.log(cramer_seq[-1])) / 2) * 2)
tools/exp_boundary_growth.py:9:  1. Cramer: random integers with probability 1/ln(n)
tools/exp_boundary_growth.py:10:  2. Shuffled gaps: same gap distribution, random order (destroys correlations)
tools/exp_boundary_growth.py:35:    """Generate Cramer random primes: each integer n in [n_start, n_end]
tools/exp_boundary_growth.py:37:    rng = np.random.default_rng(seed)
tools/exp_boundary_growth.py:41:    mask = rng.random(len(ns)) < probs
tools/exp_boundary_growth.py:46:    rng = np.random.default_rng(seed)
tools/exp_markov_k_direction.py:3:exp_markov_k_direction.py — Direction of Markov-k memory in the dipolar plane.
tools/exp_markov_k_direction.py:23:    python tools/exp_markov_k_direction.py [--N 100000] [--n_trials 20] [--n_bins 14]
tools/exp_markov_k_direction.py:59:    rng = np.random.default_rng(9999)
tools/exp_markov_k_direction.py:101:def build_markov_k(bi, nb, k):
tools/exp_markov_k_direction.py:119:def gen_markov_k(trans_by_order, pools, n_gaps, k_max, marg, rng):
tools/exp_markov_k_direction.py:165:        t, sp, tot = build_markov_k(bi, nb, k)
tools/exp_markov_k_direction.py:175:            rng = np.random.default_rng(1000 * k_max + trial)
tools/exp_markov_k_direction.py:181:                sg = gen_markov_k(tf, pools, len(gaps), k_max, marg, rng)
tools/exp_markov_k_direction.py:216:                t, _, _ = build_markov_k(bi_s, nb_s, k)
tools/exp_markov_k_direction.py:220:                rng = np.random.default_rng(5000 + trial)
tools/exp_markov_k_direction.py:221:                sg = gen_markov_k(trans_s, pools_s, len(gs), k_test, marg_s, rng)
tools/exp_markov_k_direction.py:243:    out_path = Path(__file__).parent / "data" / "markov_k_direction.json"
tools/exp_brody_calibration.py:34:    u = rng.random(n)
tools/exp_brody_calibration.py:65:        rng = np.random.default_rng(42)
tools/exp_brody_calibration.py:109:def generate_primes(n_max=200000):
tools/exp_brody_calibration.py:148:    rng = np.random.default_rng(args.seed)
tools/exp_brody_calibration.py:222:    logistic_vals = []
tools/exp_brody_calibration.py:225:        logistic_vals.append(x)
tools/exp_brody_calibration.py:226:    logistic_gaps = np.diff(np.sort(logistic_vals[-args.n_gaps - 1:]))
tools/exp_brody_calibration.py:227:    logistic_gaps = logistic_gaps / np.mean(logistic_gaps)
tools/exp_brody_calibration.py:228:    obs_l = compute_observables(logistic_gaps, n_shuffles=args.n_shuffles, rng=rng)
tools/exp_brody_calibration.py:230:    real_domains['logistic'] = {**obs_l, 'beta_eff': beta_eff_l}
tools/exp_brody_calibration.py:231:    print(f"{'logistic':>20} {obs_l['r']:8.4f} {obs_l['r_shuf']:8.4f} "
tools/exp_markov_scale_function.py:3:exp_markov_scale_function.py — Scale dependence of Markov-3 ordering information in prime gaps.
tools/exp_markov_scale_function.py:36:def markov3_entropy(seq):
tools/exp_markov_scale_function.py:105:    H_real = markov3_entropy_general(res_seq, alphabet=[0, 2, 4])
tools/exp_markov_scale_function.py:111:        np.random.shuffle(shuf)
tools/exp_markov_scale_function.py:113:        H_shuffles.append(markov3_entropy_general(res_shuf, alphabet=[0, 2, 4]))
tools/exp_markov_scale_function.py:151:def markov3_entropy_general(seq, alphabet):
tools/exp_markov_scale_function.py:276:        'experiment': 'markov_scale_function',
tools/exp_markov_scale_function.py:305:    out_path = Path(__file__).parent / 'data' / 'markov_scale_function.json'
tools/exp_coherence_robustness.py:9:  - strategia di windowing (contiguous random, stride, overlap)
tools/exp_coherence_robustness.py:52:      - 'random': posizioni iniziali uniformi casuali
tools/exp_coherence_robustness.py:60:    if strategy == 'random':
tools/exp_coherence_robustness.py:122:        rng = np.random.default_rng(master_seed + i)
tools/exp_coherence_robustness.py:133:        rng = np.random.default_rng(rng_seed)
tools/exp_coherence_robustness.py:134:        dr, z = measure_delta_r(gaps, L_fixed, nw, n_surrogates, 'random', rng)
tools/exp_coherence_robustness.py:158:    # === Part 1: bootstrap L* (random windowing) ===
tools/exp_coherence_robustness.py:159:    print(f"\n=== Part 1: bootstrap L* (n_boot={args.n_boot}, strategy=random) ===")
tools/exp_coherence_robustness.py:163:        n_boot=args.n_boot, strategy='random', master_seed=42
tools/exp_coherence_robustness.py:185:    strategies = ['random', 'stride', 'overlap']
tools/exp_coherence_robustness.py:190:            rng = np.random.default_rng(42 + seed)
tools/exp_coherence_robustness.py:218:            rng = np.random.default_rng(42 + seed)
tools/exp_coherence_robustness.py:219:            dr, _ = measure_delta_r(gaps, L, 200, args.n_surrogates, 'random', rng)
tools/exp_mod3_scaling.py:60:def markov_order_test(gaps_mod3, max_order=3):
tools/exp_mod3_scaling.py:151:        markov = markov_order_test(win_mod3, max_order=3)
tools/exp_mod3_scaling.py:155:        shuffle_markov1_llr = []
tools/exp_mod3_scaling.py:158:            np.random.shuffle(shuf)
tools/exp_mod3_scaling.py:161:            m = markov_order_test(shuf, max_order=1)
tools/exp_mod3_scaling.py:162:            shuffle_markov1_llr.append(m[1]["llr"])
tools/exp_mod3_scaling.py:168:        shuffle_m1_mean = np.mean(shuffle_markov1_llr)
tools/exp_mod3_scaling.py:169:        shuffle_m1_std = np.std(shuffle_markov1_llr, ddof=1)
tools/exp_mod3_scaling.py:170:        z_markov1 = (markov[1]["llr"] - shuffle_m1_mean) / shuffle_m1_std if shuffle_m1_std > 0 else 0.0
tools/exp_mod3_scaling.py:179:            "markov1_llr": round(float(markov[1]["llr"]), 1),
tools/exp_mod3_scaling.py:180:            "markov1_p": float(markov[1]["p_value"]),
tools/exp_mod3_scaling.py:181:            "markov2_llr": round(float(markov[2]["llr"]), 1),
tools/exp_mod3_scaling.py:182:            "markov2_p": float(markov[2]["p_value"]),
tools/exp_mod3_scaling.py:183:            "markov3_llr": round(float(markov[3]["llr"]), 1),
tools/exp_mod3_scaling.py:184:            "markov3_p": float(markov[3]["p_value"]),
tools/exp_mod3_scaling.py:185:            "z_markov1_vs_shuffle": round(float(z_markov1), 2),
tools/exp_mod3_scaling.py:191:                  f"M1_llr={markov[1]['llr']:.0f}, M2_llr={markov[2]['llr']:.0f}")
tools/exp_mod3_scaling.py:196:    m1_llrs = np.array([r["markov1_llr"] for r in results])
tools/exp_mod3_scaling.py:197:    m2_llrs = np.array([r["markov2_llr"] for r in results])
tools/exp_mod3_scaling.py:217:    m2_m1_ratios = [r["markov2_llr"] / r["markov1_llr"] if r["markov1_llr"] > 0 else 0
tools/exp_mod3_scaling.py:230:        "markov1_scaling": {
tools/exp_mod3_scaling.py:236:        "markov2_scaling": {
tools/exp_mod3_scaling.py:242:        "markov_depth_ratio_m2_m1": {
tools/exp_crossover_universality.py:30:np.random.seed(42)
tools/exp_crossover_universality.py:214:rng = np.random.default_rng(42)
tools/exp_number_variance.py:45:        # Sample random starting points
tools/exp_number_variance.py:87:        np.random.shuffle(spacings)
tools/exp_acf_stationarity.py:13:(illusory duality, det=+1). PNT-normalization should recover the
tools/exp_acf_stationarity.py:134:    np.random.seed(42)
tools/exp_acf_stationarity.py:195:        idx = np.random.permutation(N_gaps)
tools/exp_acf_stationarity.py:265:    # PART 4: The dipolar/illusory decomposition
tools/exp_acf_stationarity.py:268:    print("PART 4: Dipolar vs illusory decomposition")
tools/exp_acf_stationarity.py:283:    illusory_lags = np.sum(acf_diff > 0)
tools/exp_acf_stationarity.py:287:    print(f"  Trend-induced positive contribution: at {illusory_lags}/{max_lag} lags")
tools/exp_poisson_convergence.py:71:def cramer_random_primes(primes, n_surrogates=10):
tools/exp_poisson_convergence.py:72:    """Generate Cramer random model: gaps ~ Exponential(ln p_i), same density, independent."""
tools/exp_poisson_convergence.py:76:        gaps_random = np.array([np.random.exponential(np.log(p)) for p in primes[1:]])
tools/exp_poisson_convergence.py:77:        results.append(gaps_random)
tools/exp_poisson_convergence.py:114:        gaps_surr = np.array([np.random.exponential(lp) for lp in log_ps])
tools/exp_dipolar_angle_reference.py:8:3. Cramer random primes (same density, no correlations)
tools/exp_dipolar_angle_reference.py:59:        sg = np.random.permutation(gaps)
tools/exp_dipolar_angle_reference.py:79:def generate_gue_gaps(n_gaps, matrix_size=500):
tools/exp_dipolar_angle_reference.py:83:        # GUE: complex Hermitian random matrix
tools/exp_dipolar_angle_reference.py:84:        A = (np.random.randn(matrix_size, matrix_size) +
tools/exp_dipolar_angle_reference.py:85:             1j * np.random.randn(matrix_size, matrix_size)) / np.sqrt(2 * matrix_size)
tools/exp_dipolar_angle_reference.py:99:def generate_goe_gaps(n_gaps, matrix_size=500):
tools/exp_dipolar_angle_reference.py:103:        A = np.random.randn(matrix_size, matrix_size) / np.sqrt(matrix_size)
tools/exp_dipolar_angle_reference.py:115:def generate_poisson_gaps(n_gaps):
tools/exp_dipolar_angle_reference.py:117:    return np.random.exponential(1.0, n_gaps)
tools/exp_dipolar_angle_reference.py:120:def generate_cramer_gaps(primes):
tools/exp_dipolar_angle_reference.py:121:    """Generate Cramer random model: same local density as primes, no correlations."""
tools/exp_dipolar_angle_reference.py:128:        g = np.random.exponential(mean_gap)
tools/exp_dipolar_angle_reference.py:213:    # 5. Cramer random primes
tools/exp_dipolar_angle_reference.py:264:    np.random.seed(42)
tools/exp_acf_range_universality.py:69:def generate_rmt_spacings(N, n_mat, ensemble='GUE'):
tools/exp_acf_range_universality.py:70:    """Unfolded spacings from random matrix ensemble."""
tools/exp_acf_range_universality.py:75:            Re = np.random.randn(sz, sz)
tools/exp_acf_range_universality.py:76:            Im = np.random.randn(sz, sz)
tools/exp_acf_range_universality.py:79:            A = np.random.randn(sz, sz)
tools/exp_acf_range_universality.py:98:    np.random.seed(42)
tools/exp_acf_range_universality.py:153:        np.random.shuffle(sg)
tools/exp_acf_range_universality.py:174:    poisson_gaps = np.random.exponential(1.0, 100_000)
tools/exp_acf_range_universality.py:177:    np.random.shuffle(shuffled_gaps)
tools/exp_acf_range_universality.py:256:            np.random.shuffle(sw)
tools/exp_excess_scaling.py:47:        gaps = np.random.exponential(mean_gap, n_gaps)
tools/exp_beta_crossover.py:63:    starts = np.random.uniform(0, total - L, n_windows)
tools/exp_beta_crossover.py:87:    diag = np.random.randn(N)
tools/exp_beta_crossover.py:95:        off_diag[i] = np.sqrt(np.random.gamma(df / 2, 2.0))
tools/exp_beta_crossover.py:156:            np.random.shuffle(sh)
tools/exp_beta_crossover.py:217:                np.random.shuffle(sh)
tools/exp_boundary_coherence.py:30:# GUE values from random matrix theory (GOE for real symmetric, GUE for complex)
tools/exp_boundary_coherence.py:121:def generate_gue_spacings(n, n_matrices=50):
tools/exp_boundary_coherence.py:122:    """Generate GUE spacings from random Hermitian matrices."""
tools/exp_boundary_coherence.py:127:        H = np.random.randn(dim, dim) + 1j * np.random.randn(dim, dim)
tools/exp_boundary_coherence.py:136:def generate_poisson_spacings(n):
tools/exp_boundary_coherence.py:138:    return np.random.exponential(1.0, n)
tools/exp_boundary_coherence.py:148:    np.random.seed(42)
tools/exp_boundary_coherence.py:186:        np.random.shuffle(shuffled_gaps)
tools/exp_boundary_coherence.py:235:    # Coherence = std of τ across observables (low = coherent, high = incoherent)
tools/exp_markov_dipolar_decomposition.py:3:exp_markov_dipolar_decomposition.py — Is the prime dipolar angle a pair-statistics consequence?
tools/exp_markov_dipolar_decomposition.py:22:    python tools/exp_markov_dipolar_decomposition.py [--N 100000] [--n_trials 30]
tools/exp_markov_dipolar_decomposition.py:60:        sg = np.random.permutation(gaps)
tools/exp_markov_dipolar_decomposition.py:136:def generate_markov1(T, marginal, centers, n_gaps, rng):
tools/exp_markov_dipolar_decomposition.py:154:def generate_markov0(marginal, centers, n_gaps, rng):
tools/exp_markov_dipolar_decomposition.py:208:        rng = np.random.default_rng(1000 + t)
tools/exp_markov_dipolar_decomposition.py:209:        m1_gaps = generate_markov1(T, marginal, centers, len(real_gaps), rng)
tools/exp_markov_dipolar_decomposition.py:226:        rng = np.random.default_rng(2000 + t)
tools/exp_markov_dipolar_decomposition.py:227:        m0_gaps = generate_markov0(marginal, centers, len(real_gaps), rng)
tools/exp_markov_dipolar_decomposition.py:266:            rng = np.random.default_rng(3000 + t)
tools/exp_markov_dipolar_decomposition.py:267:            m1_g = generate_markov1(T_s, marg_s, cent_s, len(g_scale), rng)
tools/exp_markov_dipolar_decomposition.py:327:        'markov1': {
tools/exp_markov_dipolar_decomposition.py:334:        'markov0': {
tools/exp_markov_dipolar_decomposition.py:352:    out_path = Path(__file__).parent / "data" / "markov_dipolar_decomposition.json"
tools/exp_markov_dipolar_decomposition.py:367:    np.random.seed(42)
tools/exp_boundary_mixture_gate.py:40:def gue_spacings(matrix_size: int, min_spacings: int, rng: np.random.Generator) -> np.ndarray:
tools/exp_boundary_mixture_gate.py:58:def mixture_spacings(gue: np.ndarray, poisson: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
tools/exp_boundary_mixture_gate.py:62:    mask = rng.random(len(gue)) < beta
tools/exp_boundary_mixture_gate.py:72:    rng: np.random.Generator,
tools/exp_boundary_mixture_gate.py:190:    rng = np.random.default_rng(args.seed)
tools/exp_boundary_mixture_gate.py:195:        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
tools/exp_boundary_mixture_gate.py:200:            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
tools/exp_boundary_mixture_gate.py:205:                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
tools/exp_boundary_shuffle_audit.py:10:    2. r_shuffled = same, on 2000 random permutations of the gap sequence
tools/exp_boundary_shuffle_audit.py:16:Domains: primes, zeta zeros, random matrix (GUE), Fibonacci spectrum,
tools/exp_boundary_shuffle_audit.py:17:         logistic map, Poisson, coupled oscillators, percolation,
tools/exp_boundary_shuffle_audit.py:34:rng = np.random.default_rng(42)
tools/exp_boundary_shuffle_audit.py:83:    """GUE random matrices — eigenvalue spacings."""
tools/exp_boundary_shuffle_audit.py:104:def gen_logistic(n=100000, r_param=3.99):
tools/exp_boundary_shuffle_audit.py:115:def gen_fibonacci_spectrum(size=610):
tools/exp_boundary_shuffle_audit.py:138:        if dE <= 0 or rng.random() < np.exp(-dE / T):
tools/exp_boundary_shuffle_audit.py:158:        grid = (rng.random((L, L)) < p).astype(int)
tools/exp_boundary_shuffle_audit.py:244:    'gue':                 ('GUE random matrix',          gen_gue_eigenvalues),
tools/exp_boundary_shuffle_audit.py:246:    'logistic':            ('Logistic map (r=3.99)',      gen_logistic),
tools/exp_boundary_shuffle_audit.py:247:    'fibonacci_spectrum':  ('Fibonacci tight-binding',    gen_fibonacci_spectrum),
tools/exp_brody_flow.py:13:- Cramer: density-matched random gaps → β_cramer(N)
tools/exp_brody_flow.py:88:    """Generate Cramer-model random gaps: exponential with same mean."""
tools/exp_brody_flow.py:89:    return np.random.exponential(mean_gap, size=n_gaps)
tools/exp_brody_flow.py:131:            np.random.shuffle(g_shuf)
tools/exp_brody_flow.py:192:        shuf_betas = [w["shuffle_beta_mean"] + np.random.normal(0, w["shuffle_beta_std"])
tools/exp_ricci_primes.py:119:    np.random.shuffle(shuffled_gaps)
tools/exp_cross_observable_consistency.py:98:    """Compute Σ²(L) = Var[N(x, x+L)] over random windows."""
tools/exp_cross_observable_consistency.py:112:        H = np.random.randn(n_eigenvalues, n_eigenvalues) + 1j * np.random.randn(n_eigenvalues, n_eigenvalues)
tools/exp_cross_observable_consistency.py:152:    np.random.shuffle(g_shuf)
tools/exp_cross_observable_consistency.py:179:    H = np.random.randn(1500, 1500) + 1j * np.random.randn(1500, 1500)
tools/exp_spectral_landscape.py:52:    """GUE: eigenvalue spacings of complex Hermitian random matrix."""
tools/exp_spectral_landscape.py:54:    H = np.random.randn(N, N) + 1j * np.random.randn(N, N)
tools/exp_spectral_landscape.py:66:    """GOE: eigenvalue spacings of real symmetric random matrix."""
tools/exp_spectral_landscape.py:68:    H = np.random.randn(N, N)
tools/exp_spectral_landscape.py:82:    A = np.random.randn(N, N) + 1j * np.random.randn(N, N)
tools/exp_spectral_landscape.py:83:    B = np.random.randn(N, N) + 1j * np.random.randn(N, N)
tools/exp_spectral_landscape.py:101:    return np.random.exponential(1.0, n_spacings)
tools/exp_spectral_landscape.py:106:    s = np.ones(n_spacings) + np.random.normal(0, 0.01, n_spacings)
tools/exp_spectral_landscape.py:116:        s = np.random.exponential(0.5, n_spacings * 2)
tools/exp_spectral_landscape.py:117:        accept = np.random.uniform(0, 1, len(s)) < 4 * s * np.exp(-2 * s) / (4 * 0.5 * np.exp(-1))
tools/exp_spectral_landscape.py:130:    np.random.shuffle(s)
tools/exp_spectral_landscape.py:134:def gen_fibonacci_gaps(n_spacings):
tools/exp_spectral_landscape.py:172:    diag = W * (np.random.uniform(size=L) - 0.5)
tools/exp_spectral_landscape.py:188:    theta = np.random.uniform(0, 2 * np.pi)
tools/exp_spectral_landscape.py:203:    gaps = np.random.pareto(exponent, n_spacings) + 0.1
tools/exp_spectral_landscape.py:209:    s = np.ones(n_spacings) + 0.3 * np.random.normal(0, 1, n_spacings)
tools/exp_spectral_landscape.py:272:        np.random.shuffle(s_shuf)
tools/exp_spectral_landscape.py:349:        ("Fibonacci_gaps", gen_fibonacci_gaps, {}),
tools/exp_markov_psd_prediction.py:3:exp_markov_psd_prediction.py — Analytical Markov PSD vs measured residue PSD
tools/exp_markov_psd_prediction.py:17:  python exp_markov_psd_prediction.py [--N 2000000] [--n_synth 20] [--nperseg 4096]
tools/exp_markov_psd_prediction.py:51:def analytical_markov_psd(P, freqs):
tools/exp_markov_psd_prediction.py:102:def generate_markov(P, n, pi0=None):
tools/exp_markov_psd_prediction.py:108:    seq[0] = 0 if np.random.random() < pi0 else 1
tools/exp_markov_psd_prediction.py:109:    rands = np.random.random(n - 1)
tools/exp_markov_psd_prediction.py:114:def generate_bernoulli(pi1, n):
tools/exp_markov_psd_prediction.py:116:    return (np.random.random(n) < pi1).astype(int)
tools/exp_markov_psd_prediction.py:163:    S_analytical, params = analytical_markov_psd(P, f_pos)
tools/exp_markov_psd_prediction.py:173:    slopes_markov = []
tools/exp_markov_psd_prediction.py:175:        seq = generate_markov(P, N)
tools/exp_markov_psd_prediction.py:178:        slopes_markov.append(sl)
tools/exp_markov_psd_prediction.py:179:    slopes_markov = np.array(slopes_markov)
tools/exp_markov_psd_prediction.py:180:    mean_markov = slopes_markov.mean()
tools/exp_markov_psd_prediction.py:181:    std_markov = slopes_markov.std()
tools/exp_markov_psd_prediction.py:182:    print(f"  Markov synthetic slope: {mean_markov:+.4f} ± {std_markov:.4f}")
tools/exp_markov_psd_prediction.py:198:    z_markov = (slope_prime - mean_markov) / std_markov if std_markov > 0 else np.inf
tools/exp_markov_psd_prediction.py:207:    print(f"  Synthetic Markov PSD slope:    {mean_markov:+.4f} ± {std_markov:.4f}")
tools/exp_markov_psd_prediction.py:210:    print(f"  z(prime vs Markov synthetic):  {z_markov:+.2f}")
tools/exp_markov_psd_prediction.py:217:        ratio_markov = mean_markov / slope_prime
tools/exp_markov_psd_prediction.py:220:        ratio_markov = ratio_analytical = np.nan
tools/exp_markov_psd_prediction.py:222:    print(f"\n  Markov captures {ratio_markov*100:.1f}% of prime residue slope")
tools/exp_markov_psd_prediction.py:248:            syn = generate_markov(P_local, win_size)
tools/exp_markov_psd_prediction.py:259:            'slope_markov': float(mean_local),
tools/exp_markov_psd_prediction.py:263:        print(f"  ln(p)={np.log(p_mid):.1f}: prime={sl_w:+.4f}, markov={mean_local:+.4f}, gap={gap:+.4f}, λ₂={lam2_local:.5f}")
tools/exp_markov_psd_prediction.py:284:        'slope_markov_mean': float(mean_markov),
tools/exp_markov_psd_prediction.py:285:        'slope_markov_std': float(std_markov),
tools/exp_markov_psd_prediction.py:288:        'z_prime_vs_markov': float(z_markov),
tools/exp_markov_psd_prediction.py:291:        'ratio_markov': float(ratio_markov),
tools/exp_markov_psd_prediction.py:299:    out_path = '/opt/MM_D-ND/tools/data/exp_markov_psd_prediction.json'
tools/exp_two_channel_decomposition.py:156:            np.random.shuffle(shuffled)
tools/exp_two_channel_decomposition.py:242:            np.random.shuffle(s_gaps)
tools/exp_two_channel_decomposition.py:246:            np.random.shuffle(s_res)
tools/exp_two_channel_decomposition.py:250:            np.random.shuffle(s_mag)
tools/exp_desitter_unification.py:136:rng = np.random.default_rng(42)
tools/exp_desitter_unification.py:175:    # Shuffled: same gaps, random order
tools/exp_two_layer_universality.py:13:  1. Generate 7 gap/spacing sequences: primes, GUE, Poisson, AR(1), logistic,
tools/exp_two_layer_universality.py:14:     periodic, Fibonacci gaps
tools/exp_two_layer_universality.py:44:def build_markov_chain(gaps, order, n_bins=12):
tools/exp_two_layer_universality.py:68:def generate_markov_surrogate(gaps, order, n_bins=12, rng=None):
tools/exp_two_layer_universality.py:70:        rng = np.random.default_rng()
tools/exp_two_layer_universality.py:71:    binned, edges, gap_pools, trans_prob = build_markov_chain(gaps, order, n_bins)
tools/exp_two_layer_universality.py:190:        rng = np.random.default_rng()
tools/exp_two_layer_universality.py:206:        rng = np.random.default_rng()
tools/exp_two_layer_universality.py:211:        rng = np.random.default_rng()
tools/exp_two_layer_universality.py:220:def gen_logistic_gaps(N, r=3.95):
tools/exp_two_layer_universality.py:228:def gen_periodic_gaps(N):
tools/exp_two_layer_universality.py:232:def gen_fibonacci_gaps(N):
tools/exp_two_layer_universality.py:252:    'logistic': gen_logistic_gaps,
tools/exp_two_layer_universality.py:253:    'periodic_24': gen_periodic_gaps,
tools/exp_two_layer_universality.py:254:    'fibonacci': gen_fibonacci_gaps,
tools/exp_two_layer_universality.py:275:    rng = np.random.default_rng(seed)
tools/exp_two_layer_universality.py:315:                        surr = generate_markov_surrogate(gaps, mk, rng=rng)
tools/exp_dipolar_vector_scaling.py:61:        np.random.shuffle(shuffled)
tools/exp_dipolar_vector_scaling.py:103:    np.random.seed(42)
tools/exp_dipolar_vector_scaling.py:175:    # Part 3: Cramer random model baseline
tools/exp_dipolar_vector_scaling.py:176:    print("\n--- Part 3: Cramer random model (same density, no correlations) ---")
tools/exp_dipolar_vector_scaling.py:185:            if np.random.random() < 1.0 / np.log(n):
tools/exp_modular_memory_spectrum.py:15:- Null: Cramer random primes (same density, no arithmetic structure)
tools/exp_modular_memory_spectrum.py:42:def cramer_random_primes(n_max, rng):
tools/exp_modular_memory_spectrum.py:48:    mask = rng.random(len(n_vals)) < probs
tools/exp_modular_memory_spectrum.py:99:        perm = np.random.permutation(residues)
tools/exp_modular_memory_spectrum.py:133:    np.random.seed(42)
tools/exp_modular_memory_spectrum.py:134:    rng = np.random.default_rng(42)
tools/exp_modular_memory_spectrum.py:164:        cp = cramer_random_primes(args.n_max, rng)
tools/exp_spectral_2d.py:15:3. Mod-6 constrained random: gaps drawn from prime distribution, forced to
tools/exp_spectral_2d.py:104:    H = np.random.randn(N, N) + 1j * np.random.randn(N, N)
tools/exp_spectral_2d.py:115:    s_chaotic = gen_gue(n_chaotic) if n_chaotic > 50 else np.random.exponential(1.0, n_chaotic)
tools/exp_spectral_2d.py:116:    s_regular = np.random.exponential(1.0, n_regular)
tools/exp_spectral_2d.py:118:    np.random.shuffle(s)
tools/exp_spectral_2d.py:132:    This model preserves the mod-6 structure but randomizes gap magnitudes.
tools/exp_spectral_2d.py:152:        if np.random.random() < p_switch:
tools/exp_spectral_2d.py:154:                g = np.random.choice(gaps_switch_1to5) if len(gaps_switch_1to5) > 0 else 4
tools/exp_spectral_2d.py:157:                g = np.random.choice(gaps_switch_5to1) if len(gaps_switch_5to1) > 0 else 2
tools/exp_spectral_2d.py:160:            g = np.random.choice(gaps_stay) if len(gaps_stay) > 0 else 6
tools/exp_spectral_2d.py:177:    gaps = np.random.exponential(1.0, n_spacings)
tools/exp_spectral_2d.py:182:    np.random.shuffle(small)
tools/exp_spectral_2d.py:183:    np.random.shuffle(large)
tools/exp_spectral_2d.py:201:    result[0] = np.random.exponential(1.0)
tools/exp_spectral_2d.py:203:        noise = np.random.exponential(1.0)
tools/exp_spectral_2d.py:209:def gen_markov_mod6(n_spacings):
tools/exp_spectral_2d.py:257:            g = np.random.choice(gaps_by_state[state])
tools/exp_spectral_2d.py:262:        if np.random.random() < trans.get(state, {}).get(5, 0.5):
tools/exp_spectral_2d.py:290:        np.random.shuffle(sh)
tools/exp_spectral_2d.py:328:        ("Poisson", lambda n: np.random.exponential(1.0, n)),
tools/exp_spectral_2d.py:338:        ("Markov_mod6", lambda n: gen_markov_mod6(n)),
tools/exp_magnitude_psd_from_acf.py:208:    rng = np.random.default_rng(42)
tools/exp_perturbation_rank_size_curve.py:52:def gue_spacings(matrix_size: int, min_spacings: int, rng: np.random.Generator) -> np.ndarray:
tools/exp_perturbation_rank_size_curve.py:70:def perturb_adjacent_swap(gaps: np.ndarray, alpha: float, rng: np.random.Generator) -> np.ndarray:
tools/exp_perturbation_rank_size_curve.py:73:    chosen = idx[rng.random(len(idx)) < alpha]
tools/exp_perturbation_rank_size_curve.py:80:def perturb_block_shuffle(gaps: np.ndarray, alpha: float, rng: np.random.Generator, block_size: int = 64) -> np.ndarray:
tools/exp_perturbation_rank_size_curve.py:95:def perturb_large_gap_only(gaps: np.ndarray, alpha: float, rng: np.random.Generator) -> np.ndarray:
tools/exp_perturbation_rank_size_curve.py:108:def perturb_uniform(gaps: np.ndarray, alpha: float, rng: np.random.Generator) -> np.ndarray:
tools/exp_perturbation_rank_size_curve.py:178:    rng: np.random.Generator,
tools/exp_perturbation_rank_size_curve.py:299:    root_rng = np.random.default_rng(args.seed)
tools/exp_perturbation_rank_size_curve.py:333:            rep_rng = np.random.default_rng(root_rng.integers(0, 2**63 - 1))
tools/exp_perturbation_rank_size_curve.py:339:                rep_rng = np.random.default_rng(root_rng.integers(0, 2**63 - 1))
tools/exp_two_channel_cross_domain.py:17:  - GUE eigenvalues: random matrix, strong statistical correlation, no arithmetic
tools/exp_two_channel_cross_domain.py:18:  - Cramer random primes: same density as primes, no sieve correlations
tools/exp_two_channel_cross_domain.py:63:    """Generate unfolded spacings from GUE random matrices."""
tools/exp_two_channel_cross_domain.py:67:        A = (np.random.randn(matrix_size, matrix_size)
tools/exp_two_channel_cross_domain.py:68:             + 1j * np.random.randn(matrix_size, matrix_size)) / np.sqrt(2)
tools/exp_two_channel_cross_domain.py:85:def cramer_random_primes(n_primes):
tools/exp_two_channel_cross_domain.py:90:        if np.random.random() < 2.0 / np.log(n):
tools/exp_two_channel_cross_domain.py:166:        rng = np.random.default_rng()
tools/exp_two_channel_cross_domain.py:212:        rng = np.random.default_rng()
tools/exp_two_channel_cross_domain.py:268:    rng = np.random.default_rng(42)
tools/exp_two_channel_cross_domain.py:323:    cramer = cramer_random_primes(n_primes)
tools/exp_markov_layer_recovery_audit.py:3:exp_markov_layer_recovery_audit.py - META audit for the Markov-layer classifier.
tools/exp_markov_layer_recovery_audit.py:35:    generate_markov_surrogate,
tools/exp_markov_layer_recovery_audit.py:60:                surr = generate_markov_surrogate(gaps, mk, rng=rng)
tools/exp_markov_layer_recovery_audit.py:101:            "gaps": generate_markov_surrogate(prime_gaps, 1, rng=rng),
tools/exp_markov_layer_recovery_audit.py:105:            "gaps": generate_markov_surrogate(prime_gaps, 2, rng=rng),
tools/exp_markov_layer_recovery_audit.py:131:    rng = np.random.default_rng(seed)
tools/exp_markov_layer_recovery_audit.py:162:    out_path = Path("tools/data/markov_layer_recovery_audit.json")
tools/exp_ricci_desitter_0406.py:34:rng = np.random.default_rng(42)
tools/exp_two_channel_universality.py:23:  CLASS_SHUFFLE: Keep real gap sizes in order, randomize Z/6Z class assignments
tools/exp_two_channel_universality.py:29:  - CLASS_SHUFFLE has real magnitudes but random classes → tests class contribution
tools/exp_two_channel_universality.py:30:  - MAG_SHUFFLE has real classes but random magnitudes → tests magnitude contribution
tools/exp_two_channel_universality.py:162:def generate_markov(gaps_real, classes_left_real, classes_right_real, n_gaps):
tools/exp_two_channel_universality.py:188:    classes[0] = np.random.choice([1, 5])
tools/exp_two_channel_universality.py:194:        cr = 1 if np.random.random() < p1 else 5
tools/exp_two_channel_universality.py:197:        gaps[i] = np.random.choice(gap_pools[tt])
tools/exp_two_channel_universality.py:202:def generate_class_shuffle(gaps_real, classes_left_real, classes_right_real):
tools/exp_two_channel_universality.py:203:    """Keep gap magnitudes in real order, randomize class assignments."""
tools/exp_two_channel_universality.py:207:    classes = np.where(np.random.random(n + 1) < freq_1, 1, 5)
tools/exp_two_channel_universality.py:211:def generate_mag_shuffle(gaps_real, classes_left_real, classes_right_real):
tools/exp_two_channel_universality.py:218:        gaps[idx] = np.random.permutation(gaps[idx])
tools/exp_two_channel_universality.py:228:    np.random.seed(42)
tools/exp_two_channel_universality.py:249:        'MARKOV': generate_markov,
tools/exp_modular_algebra_depth.py:32:def markov_order1(residues, q):
tools/exp_modular_algebra_depth.py:55:def markov_order2(residues, q):
tools/exp_modular_algebra_depth.py:101:        P1, pi, H1, counts1 = markov_order1(residues, q)
tools/exp_modular_algebra_depth.py:105:        H2, counts2 = markov_order2(residues, q)
tools/exp_modular_algebra_depth.py:134:            shuf_gaps = np.random.permutation(gaps)
tools/exp_modular_algebra_depth.py:136:            P1s, _, H1s, _ = markov_order1(shuf_res, q)
tools/exp_modular_algebra_depth.py:137:            H2s, _ = markov_order2(shuf_res, q)
tools/exp_meta_tautology_test.py:66:    rng = np.random.default_rng(42)
tools/exp_meta_tautology_test.py:101:def cramer_random_gaps(n_gaps, mean_gap, rng):
tools/exp_meta_tautology_test.py:158:    rng = np.random.default_rng(2026)
tools/exp_meta_tautology_test.py:169:        ('cramer', lambda rng_: cramer_random_gaps(N, mean_gap, rng_)),
tools/exp_meta_tautology_test.py:170:        ('HL_markov', lambda rng_: hardy_littlewood_gaps(gaps_window, rng_)),
tools/exp_meta_tautology_test.py:176:            trial_rng = np.random.default_rng(rng.integers(0, 2**31))
tools/exp_meta_tautology_test.py:205:        for syn_name in ['shuffled', 'cramer', 'HL_markov']:
tools/exp_meta_tautology_test.py:228:        'synthetics': {k: results[k] for k in ['shuffled', 'cramer', 'HL_markov']},
tools/exp_selective_layer_decoupling.py:14:  1. UNIFORM: replace position i with random draw from distribution, prob=alpha
tools/exp_selective_layer_decoupling.py:71:def build_markov_chain(gaps, order, n_bins=12):
tools/exp_selective_layer_decoupling.py:95:def generate_markov_surrogate(gaps, order, n_bins=12, rng=None):
tools/exp_selective_layer_decoupling.py:97:        rng = np.random.default_rng()
tools/exp_selective_layer_decoupling.py:98:    binned, edges, gap_pools, trans_prob = build_markov_chain(gaps, order, n_bins)
tools/exp_selective_layer_decoupling.py:123:    mask = rng.random(n) < alpha
tools/exp_selective_layer_decoupling.py:228:                mk1_surr = generate_markov_surrogate(gaps, 1, rng=rng)
tools/exp_selective_layer_decoupling.py:231:                mk2_surr = generate_markov_surrogate(gaps, 2, rng=rng)
tools/exp_selective_layer_decoupling.py:259:    rng = np.random.default_rng(args.seed)
tools/exp_coherence_length.py:6:from <r>_shuffle? This measures the scale below which primes look random and
tools/exp_coherence_length.py:63:        # Sample random starting positions
tools/exp_coherence_length.py:64:        starts = np.random.randint(0, N - L, size=n_windows)
tools/exp_coherence_length.py:71:                np.random.shuffle(shuf)
tools/exp_coherence_length.py:152:    np.random.seed(42)
tools/exp_two_channel_boundary.py:163:    rng = np.random.default_rng(42)
tools/exp_mod3_vs_residual_ordering.py:18:Null: Cramer random primes (same density, no sieve) with same 3 conditions.
tools/exp_mod3_vs_residual_ordering.py:66:        starts = np.random.uniform(levels[0], max_start, size=min(n_starts, int(max_start)))
tools/exp_mod3_vs_residual_ordering.py:89:    with random swaps that preserve the constraint.
tools/exp_mod3_vs_residual_ordering.py:98:    np.random.shuffle(idx)
tools/exp_mod3_vs_residual_ordering.py:105:    np.random.shuffle(r1)
tools/exp_mod3_vs_residual_ordering.py:106:    np.random.shuffle(r2)
tools/exp_mod3_vs_residual_ordering.py:107:    np.random.shuffle(r0)
tools/exp_mod3_vs_residual_ordering.py:112:    flip = np.random.randint(2)  # start with 1 or 2
tools/exp_mod3_vs_residual_ordering.py:124:    # Insert 0s at random positions (0s are always safe)
tools/exp_mod3_vs_residual_ordering.py:127:        pos = np.random.randint(len(result) + 1)
tools/exp_mod3_vs_residual_ordering.py:132:    # MCMC mixing: random pair swaps that preserve constraint
tools/exp_mod3_vs_residual_ordering.py:135:        i, j = np.random.randint(n, size=2)
tools/exp_mod3_vs_residual_ordering.py:149:def cramer_random_primes(n_primes, primes_ref):
tools/exp_mod3_vs_residual_ordering.py:150:    """Generate Cramer random primes: each integer n is prime with prob 1/ln(n)."""
tools/exp_mod3_vs_residual_ordering.py:155:        if np.random.random() < 1.0 / np.log(n):
tools/exp_mod3_vs_residual_ordering.py:164:    np.random.seed(42)
tools/exp_mod3_vs_residual_ordering.py:201:        np.random.shuffle(shuf)
tools/exp_mod3_vs_residual_ordering.py:223:    # 7. Cramer random primes
tools/exp_mod3_vs_residual_ordering.py:224:    print(f"\n--- Cramer random primes ({n_cramer} realizations) ---")
tools/exp_mod3_vs_residual_ordering.py:229:        cp = cramer_random_primes(len(primes), primes)
tools/exp_mod3_vs_residual_ordering.py:237:        np.random.shuffle(cs)
tools/exp_mod3_vs_residual_ordering.py:336:                        help='Number of Cramer random prime realizations')
tools/exp_perturbation_dimensionality_audit.py:44:def gue_spacings(matrix_size: int, n_matrices: int, rng: np.random.Generator) -> np.ndarray:
tools/exp_perturbation_dimensionality_audit.py:134:def perturb_adjacent_swap(gaps: np.ndarray, alpha: float, rng: np.random.Generator) -> np.ndarray:
tools/exp_perturbation_dimensionality_audit.py:137:    chosen = idx[rng.random(len(idx)) < alpha]
tools/exp_perturbation_dimensionality_audit.py:144:def perturb_block_shuffle(gaps: np.ndarray, alpha: float, rng: np.random.Generator, block_size: int = 64) -> np.ndarray:
tools/exp_perturbation_dimensionality_audit.py:159:def perturb_large_gap_only(gaps: np.ndarray, alpha: float, rng: np.random.Generator) -> np.ndarray:
tools/exp_perturbation_dimensionality_audit.py:172:def perturb_uniform(gaps: np.ndarray, alpha: float, rng: np.random.Generator) -> np.ndarray:
tools/exp_perturbation_dimensionality_audit.py:227:def analyze(name: str, gaps: np.ndarray, alphas: list[float], n_trials: int, n_baseline: int, rng: np.random.Generator) -> dict:
tools/exp_perturbation_dimensionality_audit.py:302:    root_rng = np.random.default_rng(args.seed)
tools/exp_perturbation_dimensionality_audit.py:329:        rng = np.random.default_rng(root_rng.integers(0, 2**63 - 1))
tools/exp_perturbation_dimensionality_audit.py:338:        rng = np.random.default_rng(root_rng.integers(0, 2**63 - 1))
tools/exp_perturbation_dimensionality_audit.py:347:        rng = np.random.default_rng(root_rng.integers(0, 2**63 - 1))
tools/exp_two_channel_shuffle_audit.py:71:def markov_transition_matrix(residue_seq, order=3):
tools/exp_two_channel_shuffle_audit.py:103:    ll_markov = 0.0
tools/exp_two_channel_shuffle_audit.py:107:        ll_markov += np.log(p_m)
tools/exp_two_channel_shuffle_audit.py:111:    bits_advantage = (ll_markov - ll_uniform) / (len(predictions) * np.log(2))
tools/exp_two_channel_shuffle_audit.py:159:    markov_bits = markov_transition_matrix(res_int, order=3)
tools/exp_two_channel_shuffle_audit.py:174:        'markov3_bits': round(markov_bits, 4),
tools/exp_two_channel_shuffle_audit.py:187:    rng = np.random.default_rng(args.seed)
tools/exp_two_channel_shuffle_audit.py:211:            'markov3_bits', 'k_star', 'k_capture']
tools/exp_psd_prime_gaps.py:9:opposite of pink noise.
tools/exp_psd_prime_gaps.py:87:        np.random.shuffle(shuffled)
tools/exp_psd_prime_gaps.py:162:    print(f"  (white noise = 0, pink noise = -1, blue noise = +1)")
tools/exp_markov3_observable_hunt.py:3:exp_markov3_observable_hunt.py — Find the observable that renders Markov-3 memory visible.
tools/exp_markov3_observable_hunt.py:16:    python tools/exp_markov3_observable_hunt.py [--N 200000] [--n_surr 50]
tools/exp_markov3_observable_hunt.py:35:def build_markov_chain(gaps, order, n_bins=12):
tools/exp_markov3_observable_hunt.py:67:def generate_markov_surrogate(gaps, order, n_bins=12, rng=None):
tools/exp_markov3_observable_hunt.py:70:        rng = np.random.default_rng()
tools/exp_markov3_observable_hunt.py:72:    binned, edges, gap_pools, trans_prob = build_markov_chain(gaps, order, n_bins)
tools/exp_markov3_observable_hunt.py:76:    # Seed with random starting state from data
tools/exp_markov3_observable_hunt.py:241:    rng = np.random.default_rng(42)
tools/exp_markov3_observable_hunt.py:254:                surr = generate_markov_surrogate(gaps, k, args.n_bins, rng)
tools/exp_markov3_observable_hunt.py:307:        'experiment': 'markov3_observable_hunt',
tools/exp_markov3_observable_hunt.py:318:    out_path = Path('tools/data/markov3_observable_hunt.json')
tools/exp_observable_rank_audit.py:5:Recent runs found many Markov/crossover observables that react coherently under
tools/exp_observable_rank_audit.py:154:    rng = np.random.default_rng(seed)
tools/exp_markov_memory_by_gue_type.py:7:ordering-GUE domains (fibonacci, coupled_osc, percolation) collapse to Poisson.
tools/exp_markov_memory_by_gue_type.py:86:        perm = np.random.permutation(cats)
tools/exp_markov_memory_by_gue_type.py:102:def generate_large_primes(n_limit=200000):
tools/exp_markov_memory_by_gue_type.py:113:def generate_poisson_gaps(n=5000):
tools/exp_markov_memory_by_gue_type.py:115:    return np.random.exponential(1.0, n)
tools/exp_markov_memory_by_gue_type.py:118:def generate_gue_gaps(n=2000):
tools/exp_markov_memory_by_gue_type.py:119:    """GUE random matrix eigenvalue spacings."""
tools/exp_markov_memory_by_gue_type.py:122:    H = np.random.randn(N, N) + 1j * np.random.randn(N, N)
tools/exp_markov_memory_by_gue_type.py:131:    np.random.seed(42)
tools/exp_markov_memory_by_gue_type.py:180:    for dom_name in ['logistica_biforcazione', 'brownian_motion']:
tools/exp_markov_memory_by_gue_type.py:265:        'experiment': 'markov_memory_by_gue_type',
tools/exp_markov_memory_by_gue_type.py:270:    outpath = '/opt/MM_D-ND/tools/data/markov_memory_by_gue_type.json'
tools/exp_duality_gate_transfer.py:9:    illusory duality, or does it become a local BOUNDARY metric when moved
tools/exp_duality_gate_transfer.py:14:- DIPOLARE: a coherent golden/Beatty gap sequence generated by the orbit of
tools/exp_duality_gate_transfer.py:17:- ILLUSORIA: a full random permutation of the same gaps. This preserves the
tools/exp_duality_gate_transfer.py:21:illusory permutation. Canonical observables and original-vs-shuffle z gates are
tools/exp_duality_gate_transfer.py:45:    """Return coherent golden Beatty gaps in {1, 2}."""
tools/exp_duality_gate_transfer.py:56:    rng: np.random.Generator,
tools/exp_duality_gate_transfer.py:58:    """Replace a beta fraction of coherent positions with a shuffled copy."""
tools/exp_duality_gate_transfer.py:59:    illusory = rng.permutation(dipolar)
tools/exp_duality_gate_transfer.py:63:        out = illusory.copy()
tools/exp_duality_gate_transfer.py:65:        mask = rng.random(len(dipolar)) < beta
tools/exp_duality_gate_transfer.py:67:        out[mask] = illusory[mask]
tools/exp_duality_gate_transfer.py:75:    rng: np.random.Generator,
tools/exp_duality_gate_transfer.py:101:    """Classify layers by standardized distance to dipolar/illusory endpoints."""
tools/exp_duality_gate_transfer.py:138:            labels.append("dipolar" if d_dip < d_ill else "illusory")
tools/exp_duality_gate_transfer.py:147:            "illusory_label_fraction": float(np.mean(np.array(labels) == "illusory")),
tools/exp_duality_gate_transfer.py:196:    rng = np.random.default_rng(args.seed)
tools/exp_duality_gate_transfer.py:201:        phase = float(rng.random())
tools/exp_duality_gate_transfer.py:204:            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
tools/exp_duality_gate_transfer.py:209:                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
tools/exp_duality_gate_transfer.py:255:    print("beta stable_count margin ambiguous illusory_fraction")
tools/exp_duality_gate_transfer.py:264:            f"{layer.get('illusory_label_fraction', 0.0):>17.3f}"
tools/exp_crossover_phase_test.py:87:def generate_gue_gaps(N, rng):
tools/exp_crossover_phase_test.py:88:    """Generate GUE-like spacings from random matrices"""
tools/exp_crossover_phase_test.py:102:def generate_prime_gaps(N):
tools/exp_crossover_phase_test.py:116:def generate_logistic_gaps(N, rng):
tools/exp_crossover_phase_test.py:119:    x = 0.5 + rng.random() * 0.01
tools/exp_crossover_phase_test.py:132:def generate_ar1_neg(N, rng):
tools/exp_crossover_phase_test.py:144:def generate_periodic(N):
tools/exp_crossover_phase_test.py:150:def generate_rw_excursions(N, rng):
tools/exp_crossover_phase_test.py:151:    """Gaps between zero-crossings of random walk"""
tools/exp_crossover_phase_test.py:161:def generate_poisson(N, rng):
tools/exp_crossover_phase_test.py:211:    rng = np.random.default_rng(args.seed)
tools/exp_crossover_phase_test.py:220:    sequences['Logistic'] = generate_logistic_gaps(args.N, rng)
tools/exp_crossover_phase_test.py:224:    sequences['Periodic'] = generate_periodic(args.N)
tools/exp_psd_amplitude_scaling.py:93:            gs = np.random.permutation(g)
tools/exp_metric_tensor_diagnostic.py:24:np.random.seed(42)
tools/exp_metric_tensor_diagnostic.py:98:    cramer_gaps = np.random.exponential(np.log(p[:len(gaps)]), size=len(gaps))
tools/exp_metric_tensor_diagnostic.py:129:    shuf_gaps = np.random.permutation(gaps)
tools/exp_det_drift.py:26:def generate_primes(n_primes, start=2):
tools/exp_det_drift.py:81:            np.random.shuffle(sg)
tools/exp_scale_selective_perturbation.py:34:def generate_primes(N):
tools/exp_scale_selective_perturbation.py:47:def generate_gue(N, rng):
tools/exp_scale_selective_perturbation.py:48:    """Generate N GUE gaps (eigenvalue spacings of random Hermitian matrix)."""
tools/exp_scale_selective_perturbation.py:65:def generate_poisson(N, rng):
tools/exp_scale_selective_perturbation.py:137:        if rng.random() < alpha:
tools/exp_scale_selective_perturbation.py:190:    rng = np.random.default_rng(seed)
tools/exp_spectral_rigidity.py:19:         logistic, brownian, pure Poisson.
tools/exp_spectral_rigidity.py:42:def generate_poisson_gaps(n=10000):
tools/exp_spectral_rigidity.py:43:    return np.random.exponential(1.0, size=n)
tools/exp_spectral_rigidity.py:46:def generate_gue_gaps(n=600):
tools/exp_spectral_rigidity.py:48:    H = np.random.randn(n, n) + 1j * np.random.randn(n, n)
tools/exp_spectral_rigidity.py:69:        starts = np.random.uniform(min_start, max_start, size=n_starts)
tools/exp_spectral_rigidity.py:79:    np.random.seed(137)
tools/exp_spectral_rigidity.py:90:        ('logistic',    {'gen': lambda: gaps_from_domain('logistica_biforcazione'), 'type': 'Poisson'}),
tools/exp_spectral_rigidity.py:124:            np.random.shuffle(g_s)
tools/exp_two_channel_psd.py:196:        g_shuf = gaps_c.copy(); np.random.shuffle(g_shuf)
tools/exp_two_channel_psd.py:203:        t_shuf = trans_c.copy(); np.random.shuffle(t_shuf)
tools/exp_two_channel_psd.py:208:        m_shuf = mag_r.copy(); np.random.shuffle(m_shuf)

codex
Il nodo regressivo è chiaro: il gate va separato dalla coordinata BOUNDARY. Creo uno strumento `exp_denominator_gate_transfer_matrix.py` che misura lo stesso protocollo su quattro perimetri non-BOUNDARY con poli coerente/illusorio a distribuzione marginale preservata.
apply patch
patch: completed
/opt/MM_D-ND/tools/exp_denominator_gate_transfer_matrix.py
diff --git a/tools/exp_denominator_gate_transfer_matrix.py b/tools/exp_denominator_gate_transfer_matrix.py
new file mode 100644
index 0000000000000000000000000000000000000000..bb0a837fe1303dca05fa887b9ca28e30c4bf9031
--- /dev/null
+++ b/tools/exp_denominator_gate_transfer_matrix.py
@@ -0,0 +1,337 @@
+#!/usr/bin/env python3
+"""
+exp_denominator_gate_transfer_matrix.py
+
+Reusable META audit for the denominator gate transfer matrix.
+
+The experiment moves the original-vs-shuffle denominator gate away from the
+GUE/Poisson BOUNDARY perimeter. Each perimeter has a coherent endpoint and an
+illusory endpoint built as a permutation of the same gap multiset. That keeps
+the one-point distribution fixed and isolates ordering support.
+
+Measured for each perimeter:
+- canonical observables from observables_registry.py;
+- original-vs-shuffle z-score for each observable;
+- endpoint-stable observable set under |z| >= z_min;
+- endpoint classification using all observables and endpoint-gated observables;
+- beta layer ambiguity between coherent and illusory endpoints.
+"""
+
+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())
+PHI = (1.0 + 5.0**0.5) / 2.0
+
+
+def normalize(gaps: np.ndarray) -> np.ndarray:
+    gaps = np.asarray(gaps, dtype=float)
+    gaps = np.maximum(gaps, 1e-12)
+    mean = float(np.mean(gaps))
+    return gaps / mean if mean > 1e-15 else gaps
+
+
+def golden_beatty(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    phase = float(rng.random())
+    n = np.arange(n_gaps + 1, dtype=float) + phase
+    positions = np.floor(n * PHI)
+    return normalize(np.diff(positions))
+
+
+def periodic_triad(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    base = np.array([0.55, 1.0, 1.45, 1.0, 0.75, 1.25], dtype=float)
+    shift = int(rng.integers(0, len(base)))
+    tiled = np.tile(np.roll(base, shift), int(np.ceil(n_gaps / len(base))))[:n_gaps]
+    jitter = rng.normal(0.0, 0.015, size=n_gaps)
+    return normalize(tiled + jitter)
+
+
+def markov_alternating(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    vals = np.array([0.62, 1.38], dtype=float)
+    state = int(rng.integers(0, 2))
+    out = np.empty(n_gaps, dtype=float)
+    for i in range(n_gaps):
+        out[i] = vals[state] + rng.normal(0.0, 0.03)
+        if rng.random() < 0.88:
+            state = 1 - state
+    return normalize(out)
+
+
+def ar1_continuity(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    rho = 0.86
+    x = np.empty(n_gaps, dtype=float)
+    x[0] = rng.normal()
+    noise_scale = (1.0 - rho * rho) ** 0.5
+    for i in range(1, n_gaps):
+        x[i] = rho * x[i - 1] + noise_scale * rng.normal()
+    return normalize(np.exp(0.42 * x))
+
+
+PERIMETERS = {
+    "DUALITA_golden": golden_beatty,
+    "R_periodic_triad": periodic_triad,
+    "T_markov_alternating": markov_alternating,
+    "E_ar1_continuity": ar1_continuity,
+}
+
+
+def beta_layer(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        out = base.copy()
+    elif beta >= 1.0:
+        out = illusory.copy()
+    else:
+        mask = rng.random(len(base)) < beta
+        out = base.copy()
+        out[mask] = illusory[mask]
+    return normalize(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]]:
+    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])
+
+    shuffle_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
+        shuffle_std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, shuffle_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:
+    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)
+
+    coherent_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent_vectors, illusory_vectors])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent_vectors, axis=0)
+    illusory_centroid = np.mean(illusory_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((illusory_centroid - coherent_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_coherent = float(np.linalg.norm((x - coherent_centroid) / scale))
+            d_illusory = float(np.linalg.norm((x - illusory_centroid) / scale))
+            denom = d_coherent + d_illusory
+            coord = float((d_coherent - d_illusory) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_coherent - d_illusory) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("coherent" if d_coherent < d_illusory else "illusory")
+        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,
+            "illusory_label_fraction": float(np.mean(np.array(labels) == "illusory")),
+        }
+
+    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}
+        z_values = {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)
+                z_values[name].append(row["z"][name])
+        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()},
+            "z_mean": {name: float(np.mean(vals)) for name, vals in z_values.items()},
+        }
+
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    endpoint_one_sided = []
+    for name in OBS_NAMES:
+        coherent_freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in coherent_rows])
+        illusory_freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in illusory_rows])
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            endpoint_one_sided.append(name)
+        if np.mean([coherent_freq, illusory_freq]) >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "coherent_one_sided_observables": endpoint_one_sided,
+        "layers": layers,
+    }
+
+
+def analyze_perimeter(name: str, generator, args: argparse.Namespace, rng: np.random.Generator) -> dict:
+    rows = []
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    for rep in range(args.n_replicates):
+        base = generator(args.n_gaps, np.random.default_rng(rng.integers(0, 2**63 - 1)))
+        for beta in betas:
+            gaps = beta_layer(base, beta, np.random.default_rng(rng.integers(0, 2**63 - 1)))
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                args.n_baseline,
+                np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "perimeter": name,
+                    "replicate": rep,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [obs_name for obs_name in OBS_NAMES if abs(z[obs_name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    one_sided_classification = classify_layers(rows, gate["coherent_one_sided_observables"])
+    return {
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_one_sided_gated": one_sided_classification,
+        "rows": rows,
+    }
+
+
+def compact_matrix(perimeters: dict) -> dict:
+    matrix = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        class_all = data["classification_all_observables"]
+        class_gate = data["classification_one_sided_gated"]
+        matrix[name] = {
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "endpoint_distance_all": class_all["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": class_gate["endpoint_distance"],
+            "ambiguous_beta_all": class_all["ambiguous_beta"],
+            "ambiguous_beta_one_sided_gated": class_gate["ambiguous_beta"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+        }
+    return matrix
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    perimeters = {}
+    for name, generator in PERIMETERS.items():
+        perimeters[name] = analyze_perimeter(name, generator, args, root_rng)
+
+    output = {
+        "experiment": "denominator_gate_transfer_matrix",
+        "category": "gate_transferability",
+        "question": "Which parts of the denominator gate transfer outside BOUNDARY?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact_matrix(perimeters),
+        "perimeters": perimeters,
+    }
+
+    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("perimeter one_sided stable0 stable1 dist_all dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>21s} "
+            f"{','.join(row['coherent_one_sided_observables']) or '[]':>22s} "
+            f"{row['stable_count_coherent']:>7.3f} "
+            f"{row['stable_count_illusory']:>7.3f} "
+            f"{row['endpoint_distance_all']:>8.3f} "
+            f"{row['endpoint_distance_one_sided_gated']:>9.3f} "
+            f"{row['ambiguous_beta_one_sided_gated']}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=4096)
+    parser.add_argument("--n-replicates", type=int, default=20)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=32)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=202605070901)
+    parser.add_argument("--out", default="tools/data/denominator_gate_transfer_matrix.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

diff --git a/tools/exp_denominator_gate_transfer_matrix.py b/tools/exp_denominator_gate_transfer_matrix.py
new file mode 100644
index 0000000000000000000000000000000000000000..bb0a837fe1303dca05fa887b9ca28e30c4bf9031
--- /dev/null
+++ b/tools/exp_denominator_gate_transfer_matrix.py
@@ -0,0 +1,337 @@
+#!/usr/bin/env python3
+"""
+exp_denominator_gate_transfer_matrix.py
+
+Reusable META audit for the denominator gate transfer matrix.
+
+The experiment moves the original-vs-shuffle denominator gate away from the
+GUE/Poisson BOUNDARY perimeter. Each perimeter has a coherent endpoint and an
+illusory endpoint built as a permutation of the same gap multiset. That keeps
+the one-point distribution fixed and isolates ordering support.
+
+Measured for each perimeter:
+- canonical observables from observables_registry.py;
+- original-vs-shuffle z-score for each observable;
+- endpoint-stable observable set under |z| >= z_min;
+- endpoint classification using all observables and endpoint-gated observables;
+- beta layer ambiguity between coherent and illusory endpoints.
+"""
+
+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())
+PHI = (1.0 + 5.0**0.5) / 2.0
+
+
+def normalize(gaps: np.ndarray) -> np.ndarray:
+    gaps = np.asarray(gaps, dtype=float)
+    gaps = np.maximum(gaps, 1e-12)
+    mean = float(np.mean(gaps))
+    return gaps / mean if mean > 1e-15 else gaps
+
+
+def golden_beatty(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    phase = float(rng.random())
+    n = np.arange(n_gaps + 1, dtype=float) + phase
+    positions = np.floor(n * PHI)
+    return normalize(np.diff(positions))
+
+
+def periodic_triad(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    base = np.array([0.55, 1.0, 1.45, 1.0, 0.75, 1.25], dtype=float)
+    shift = int(rng.integers(0, len(base)))
+    tiled = np.tile(np.roll(base, shift), int(np.ceil(n_gaps / len(base))))[:n_gaps]
+    jitter = rng.normal(0.0, 0.015, size=n_gaps)
+    return normalize(tiled + jitter)
+
+
+def markov_alternating(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    vals = np.array([0.62, 1.38], dtype=float)
+    state = int(rng.integers(0, 2))
+    out = np.empty(n_gaps, dtype=float)
+    for i in range(n_gaps):
+        out[i] = vals[state] + rng.normal(0.0, 0.03)
+        if rng.random() < 0.88:
+            state = 1 - state
+    return normalize(out)
+
+
+def ar1_continuity(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    rho = 0.86
+    x = np.empty(n_gaps, dtype=float)
+    x[0] = rng.normal()
+    noise_scale = (1.0 - rho * rho) ** 0.5
+    for i in range(1, n_gaps):
+        x[i] = rho * x[i - 1] + noise_scale * rng.normal()
+    return normalize(np.exp(0.42 * x))
+
+
+PERIMETERS = {
+    "DUALITA_golden": golden_beatty,
+    "R_periodic_triad": periodic_triad,
+    "T_markov_alternating": markov_alternating,
+    "E_ar1_continuity": ar1_continuity,
+}
+
+
+def beta_layer(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        out = base.copy()
+    elif beta >= 1.0:
+        out = illusory.copy()
+    else:
+        mask = rng.random(len(base)) < beta
+        out = base.copy()
+        out[mask] = illusory[mask]
+    return normalize(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]]:
+    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])
+
+    shuffle_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
+        shuffle_std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, shuffle_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:
+    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)
+
+    coherent_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent_vectors, illusory_vectors])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent_vectors, axis=0)
+    illusory_centroid = np.mean(illusory_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((illusory_centroid - coherent_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_coherent = float(np.linalg.norm((x - coherent_centroid) / scale))
+            d_illusory = float(np.linalg.norm((x - illusory_centroid) / scale))
+            denom = d_coherent + d_illusory
+            coord = float((d_coherent - d_illusory) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_coherent - d_illusory) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("coherent" if d_coherent < d_illusory else "illusory")
+        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,
+            "illusory_label_fraction": float(np.mean(np.array(labels) == "illusory")),
+        }
+
+    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}
+        z_values = {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)
+                z_values[name].append(row["z"][name])
+        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()},
+            "z_mean": {name: float(np.mean(vals)) for name, vals in z_values.items()},
+        }
+
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    endpoint_one_sided = []
+    for name in OBS_NAMES:
+        coherent_freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in coherent_rows])
+        illusory_freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in illusory_rows])
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            endpoint_one_sided.append(name)
+        if np.mean([coherent_freq, illusory_freq]) >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "coherent_one_sided_observables": endpoint_one_sided,
+        "layers": layers,
+    }
+
+
+def analyze_perimeter(name: str, generator, args: argparse.Namespace, rng: np.random.Generator) -> dict:
+    rows = []
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    for rep in range(args.n_replicates):
+        base = generator(args.n_gaps, np.random.default_rng(rng.integers(0, 2**63 - 1)))
+        for beta in betas:
+            gaps = beta_layer(base, beta, np.random.default_rng(rng.integers(0, 2**63 - 1)))
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                args.n_baseline,
+                np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "perimeter": name,
+                    "replicate": rep,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [obs_name for obs_name in OBS_NAMES if abs(z[obs_name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    one_sided_classification = classify_layers(rows, gate["coherent_one_sided_observables"])
+    return {
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_one_sided_gated": one_sided_classification,
+        "rows": rows,
+    }
+
+
+def compact_matrix(perimeters: dict) -> dict:
+    matrix = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        class_all = data["classification_all_observables"]
+        class_gate = data["classification_one_sided_gated"]
+        matrix[name] = {
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "endpoint_distance_all": class_all["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": class_gate["endpoint_distance"],
+            "ambiguous_beta_all": class_all["ambiguous_beta"],
+            "ambiguous_beta_one_sided_gated": class_gate["ambiguous_beta"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+        }
+    return matrix
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    perimeters = {}
+    for name, generator in PERIMETERS.items():
+        perimeters[name] = analyze_perimeter(name, generator, args, root_rng)
+
+    output = {
+        "experiment": "denominator_gate_transfer_matrix",
+        "category": "gate_transferability",
+        "question": "Which parts of the denominator gate transfer outside BOUNDARY?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact_matrix(perimeters),
+        "perimeters": perimeters,
+    }
+
+    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("perimeter one_sided stable0 stable1 dist_all dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>21s} "
+            f"{','.join(row['coherent_one_sided_observables']) or '[]':>22s} "
+            f"{row['stable_count_coherent']:>7.3f} "
+            f"{row['stable_count_illusory']:>7.3f} "
+            f"{row['endpoint_distance_all']:>8.3f} "
+            f"{row['endpoint_distance_one_sided_gated']:>9.3f} "
+            f"{row['ambiguous_beta_one_sided_gated']}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=4096)
+    parser.add_argument("--n-replicates", type=int, default=20)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=32)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=202605070901)
+    parser.add_argument("--out", default="tools/data/denominator_gate_transfer_matrix.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

codex
Lo strumento è scritto: usa solo observable canonici e confronta ogni endpoint contro la propria baseline di permutazioni. Ora lo eseguo con parametri moderati per avere replicazione senza trasformarlo in un run lungo.
exec
/bin/bash -lc 'python tools/exp_denominator_gate_transfer_matrix.py --n-gaps 4096 --n-replicates 20 --n-beta 11 --n-baseline 32 --out tools/data/denominator_gate_transfer_matrix.json' in /opt/MM_D-ND
diff --git a/tools/exp_denominator_gate_transfer_matrix.py b/tools/exp_denominator_gate_transfer_matrix.py
new file mode 100644
index 0000000000000000000000000000000000000000..bb0a837fe1303dca05fa887b9ca28e30c4bf9031
--- /dev/null
+++ b/tools/exp_denominator_gate_transfer_matrix.py
@@ -0,0 +1,337 @@
+#!/usr/bin/env python3
+"""
+exp_denominator_gate_transfer_matrix.py
+
+Reusable META audit for the denominator gate transfer matrix.
+
+The experiment moves the original-vs-shuffle denominator gate away from the
+GUE/Poisson BOUNDARY perimeter. Each perimeter has a coherent endpoint and an
+illusory endpoint built as a permutation of the same gap multiset. That keeps
+the one-point distribution fixed and isolates ordering support.
+
+Measured for each perimeter:
+- canonical observables from observables_registry.py;
+- original-vs-shuffle z-score for each observable;
+- endpoint-stable observable set under |z| >= z_min;
+- endpoint classification using all observables and endpoint-gated observables;
+- beta layer ambiguity between coherent and illusory endpoints.
+"""
+
+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())
+PHI = (1.0 + 5.0**0.5) / 2.0
+
+
+def normalize(gaps: np.ndarray) -> np.ndarray:
+    gaps = np.asarray(gaps, dtype=float)
+    gaps = np.maximum(gaps, 1e-12)
+    mean = float(np.mean(gaps))
+    return gaps / mean if mean > 1e-15 else gaps
+
+
+def golden_beatty(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    phase = float(rng.random())
+    n = np.arange(n_gaps + 1, dtype=float) + phase
+    positions = np.floor(n * PHI)
+    return normalize(np.diff(positions))
+
+
+def periodic_triad(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    base = np.array([0.55, 1.0, 1.45, 1.0, 0.75, 1.25], dtype=float)
+    shift = int(rng.integers(0, len(base)))
+    tiled = np.tile(np.roll(base, shift), int(np.ceil(n_gaps / len(base))))[:n_gaps]
+    jitter = rng.normal(0.0, 0.015, size=n_gaps)
+    return normalize(tiled + jitter)
+
+
+def markov_alternating(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    vals = np.array([0.62, 1.38], dtype=float)
+    state = int(rng.integers(0, 2))
+    out = np.empty(n_gaps, dtype=float)
+    for i in range(n_gaps):
+        out[i] = vals[state] + rng.normal(0.0, 0.03)
+        if rng.random() < 0.88:
+            state = 1 - state
+    return normalize(out)
+
+
+def ar1_continuity(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    rho = 0.86
+    x = np.empty(n_gaps, dtype=float)
+    x[0] = rng.normal()
+    noise_scale = (1.0 - rho * rho) ** 0.5
+    for i in range(1, n_gaps):
+        x[i] = rho * x[i - 1] + noise_scale * rng.normal()
+    return normalize(np.exp(0.42 * x))
+
+
+PERIMETERS = {
+    "DUALITA_golden": golden_beatty,
+    "R_periodic_triad": periodic_triad,
+    "T_markov_alternating": markov_alternating,
+    "E_ar1_continuity": ar1_continuity,
+}
+
+
+def beta_layer(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        out = base.copy()
+    elif beta >= 1.0:
+        out = illusory.copy()
+    else:
+        mask = rng.random(len(base)) < beta
+        out = base.copy()
+        out[mask] = illusory[mask]
+    return normalize(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]]:
+    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])
+
+    shuffle_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
+        shuffle_std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, shuffle_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:
+    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)
+
+    coherent_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent_vectors, illusory_vectors])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent_vectors, axis=0)
+    illusory_centroid = np.mean(illusory_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((illusory_centroid - coherent_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_coherent = float(np.linalg.norm((x - coherent_centroid) / scale))
+            d_illusory = float(np.linalg.norm((x - illusory_centroid) / scale))
+            denom = d_coherent + d_illusory
+            coord = float((d_coherent - d_illusory) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_coherent - d_illusory) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("coherent" if d_coherent < d_illusory else "illusory")
+        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,
+            "illusory_label_fraction": float(np.mean(np.array(labels) == "illusory")),
+        }
+
+    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}
+        z_values = {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)
+                z_values[name].append(row["z"][name])
+        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()},
+            "z_mean": {name: float(np.mean(vals)) for name, vals in z_values.items()},
+        }
+
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    endpoint_one_sided = []
+    for name in OBS_NAMES:
+        coherent_freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in coherent_rows])
+        illusory_freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in illusory_rows])
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            endpoint_one_sided.append(name)
+        if np.mean([coherent_freq, illusory_freq]) >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "coherent_one_sided_observables": endpoint_one_sided,
+        "layers": layers,
+    }
+
+
+def analyze_perimeter(name: str, generator, args: argparse.Namespace, rng: np.random.Generator) -> dict:
+    rows = []
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    for rep in range(args.n_replicates):
+        base = generator(args.n_gaps, np.random.default_rng(rng.integers(0, 2**63 - 1)))
+        for beta in betas:
+            gaps = beta_layer(base, beta, np.random.default_rng(rng.integers(0, 2**63 - 1)))
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                args.n_baseline,
+                np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "perimeter": name,
+                    "replicate": rep,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [obs_name for obs_name in OBS_NAMES if abs(z[obs_name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    one_sided_classification = classify_layers(rows, gate["coherent_one_sided_observables"])
+    return {
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_one_sided_gated": one_sided_classification,
+        "rows": rows,
+    }
+
+
+def compact_matrix(perimeters: dict) -> dict:
+    matrix = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        class_all = data["classification_all_observables"]
+        class_gate = data["classification_one_sided_gated"]
+        matrix[name] = {
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "endpoint_distance_all": class_all["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": class_gate["endpoint_distance"],
+            "ambiguous_beta_all": class_all["ambiguous_beta"],
+            "ambiguous_beta_one_sided_gated": class_gate["ambiguous_beta"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+        }
+    return matrix
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    perimeters = {}
+    for name, generator in PERIMETERS.items():
+        perimeters[name] = analyze_perimeter(name, generator, args, root_rng)
+
+    output = {
+        "experiment": "denominator_gate_transfer_matrix",
+        "category": "gate_transferability",
+        "question": "Which parts of the denominator gate transfer outside BOUNDARY?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact_matrix(perimeters),
+        "perimeters": perimeters,
+    }
+
+    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("perimeter one_sided stable0 stable1 dist_all dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>21s} "
+            f"{','.join(row['coherent_one_sided_observables']) or '[]':>22s} "
+            f"{row['stable_count_coherent']:>7.3f} "
+            f"{row['stable_count_illusory']:>7.3f} "
+            f"{row['endpoint_distance_all']:>8.3f} "
+            f"{row['endpoint_distance_one_sided_gated']:>9.3f} "
+            f"{row['ambiguous_beta_one_sided_gated']}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=4096)
+    parser.add_argument("--n-replicates", type=int, default=20)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=32)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=202605070901)
+    parser.add_argument("--out", default="tools/data/denominator_gate_transfer_matrix.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

diff --git a/tools/exp_denominator_gate_transfer_matrix.py b/tools/exp_denominator_gate_transfer_matrix.py
new file mode 100644
index 0000000000000000000000000000000000000000..bb0a837fe1303dca05fa887b9ca28e30c4bf9031
--- /dev/null
+++ b/tools/exp_denominator_gate_transfer_matrix.py
@@ -0,0 +1,337 @@
+#!/usr/bin/env python3
+"""
+exp_denominator_gate_transfer_matrix.py
+
+Reusable META audit for the denominator gate transfer matrix.
+
+The experiment moves the original-vs-shuffle denominator gate away from the
+GUE/Poisson BOUNDARY perimeter. Each perimeter has a coherent endpoint and an
+illusory endpoint built as a permutation of the same gap multiset. That keeps
+the one-point distribution fixed and isolates ordering support.
+
+Measured for each perimeter:
+- canonical observables from observables_registry.py;
+- original-vs-shuffle z-score for each observable;
+- endpoint-stable observable set under |z| >= z_min;
+- endpoint classification using all observables and endpoint-gated observables;
+- beta layer ambiguity between coherent and illusory endpoints.
+"""
+
+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())
+PHI = (1.0 + 5.0**0.5) / 2.0
+
+
+def normalize(gaps: np.ndarray) -> np.ndarray:
+    gaps = np.asarray(gaps, dtype=float)
+    gaps = np.maximum(gaps, 1e-12)
+    mean = float(np.mean(gaps))
+    return gaps / mean if mean > 1e-15 else gaps
+
+
+def golden_beatty(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    phase = float(rng.random())
+    n = np.arange(n_gaps + 1, dtype=float) + phase
+    positions = np.floor(n * PHI)
+    return normalize(np.diff(positions))
+
+
+def periodic_triad(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    base = np.array([0.55, 1.0, 1.45, 1.0, 0.75, 1.25], dtype=float)
+    shift = int(rng.integers(0, len(base)))
+    tiled = np.tile(np.roll(base, shift), int(np.ceil(n_gaps / len(base))))[:n_gaps]
+    jitter = rng.normal(0.0, 0.015, size=n_gaps)
+    return normalize(tiled + jitter)
+
+
+def markov_alternating(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    vals = np.array([0.62, 1.38], dtype=float)
+    state = int(rng.integers(0, 2))
+    out = np.empty(n_gaps, dtype=float)
+    for i in range(n_gaps):
+        out[i] = vals[state] + rng.normal(0.0, 0.03)
+        if rng.random() < 0.88:
+            state = 1 - state
+    return normalize(out)
+
+
+def ar1_continuity(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    rho = 0.86
+    x = np.empty(n_gaps, dtype=float)
+    x[0] = rng.normal()
+    noise_scale = (1.0 - rho * rho) ** 0.5
+    for i in range(1, n_gaps):
+        x[i] = rho * x[i - 1] + noise_scale * rng.normal()
+    return normalize(np.exp(0.42 * x))
+
+
+PERIMETERS = {
+    "DUALITA_golden": golden_beatty,
+    "R_periodic_triad": periodic_triad,
+    "T_markov_alternating": markov_alternating,
+    "E_ar1_continuity": ar1_continuity,
+}
+
+
+def beta_layer(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        out = base.copy()
+    elif beta >= 1.0:
+        out = illusory.copy()
+    else:
+        mask = rng.random(len(base)) < beta
+        out = base.copy()
+        out[mask] = illusory[mask]
+    return normalize(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]]:
+    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])
+
+    shuffle_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
+        shuffle_std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, shuffle_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:
+    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)
+
+    coherent_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent_vectors, illusory_vectors])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent_vectors, axis=0)
+    illusory_centroid = np.mean(illusory_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((illusory_centroid - coherent_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_coherent = float(np.linalg.norm((x - coherent_centroid) / scale))
+            d_illusory = float(np.linalg.norm((x - illusory_centroid) / scale))
+            denom = d_coherent + d_illusory
+            coord = float((d_coherent - d_illusory) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_coherent - d_illusory) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("coherent" if d_coherent < d_illusory else "illusory")
+        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,
+            "illusory_label_fraction": float(np.mean(np.array(labels) == "illusory")),
+        }
+
+    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}
+        z_values = {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)
+                z_values[name].append(row["z"][name])
+        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()},
+            "z_mean": {name: float(np.mean(vals)) for name, vals in z_values.items()},
+        }
+
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    endpoint_one_sided = []
+    for name in OBS_NAMES:
+        coherent_freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in coherent_rows])
+        illusory_freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in illusory_rows])
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            endpoint_one_sided.append(name)
+        if np.mean([coherent_freq, illusory_freq]) >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "coherent_one_sided_observables": endpoint_one_sided,
+        "layers": layers,
+    }
+
+
+def analyze_perimeter(name: str, generator, args: argparse.Namespace, rng: np.random.Generator) -> dict:
+    rows = []
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    for rep in range(args.n_replicates):
+        base = generator(args.n_gaps, np.random.default_rng(rng.integers(0, 2**63 - 1)))
+        for beta in betas:
+            gaps = beta_layer(base, beta, np.random.default_rng(rng.integers(0, 2**63 - 1)))
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                args.n_baseline,
+                np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "perimeter": name,
+                    "replicate": rep,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [obs_name for obs_name in OBS_NAMES if abs(z[obs_name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    one_sided_classification = classify_layers(rows, gate["coherent_one_sided_observables"])
+    return {
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_one_sided_gated": one_sided_classification,
+        "rows": rows,
+    }
+
+
+def compact_matrix(perimeters: dict) -> dict:
+    matrix = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        class_all = data["classification_all_observables"]
+        class_gate = data["classification_one_sided_gated"]
+        matrix[name] = {
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "endpoint_distance_all": class_all["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": class_gate["endpoint_distance"],
+            "ambiguous_beta_all": class_all["ambiguous_beta"],
+            "ambiguous_beta_one_sided_gated": class_gate["ambiguous_beta"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+        }
+    return matrix
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    perimeters = {}
+    for name, generator in PERIMETERS.items():
+        perimeters[name] = analyze_perimeter(name, generator, args, root_rng)
+
+    output = {
+        "experiment": "denominator_gate_transfer_matrix",
+        "category": "gate_transferability",
+        "question": "Which parts of the denominator gate transfer outside BOUNDARY?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact_matrix(perimeters),
+        "perimeters": perimeters,
+    }
+
+    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("perimeter one_sided stable0 stable1 dist_all dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>21s} "
+            f"{','.join(row['coherent_one_sided_observables']) or '[]':>22s} "
+            f"{row['stable_count_coherent']:>7.3f} "
+            f"{row['stable_count_illusory']:>7.3f} "
+            f"{row['endpoint_distance_all']:>8.3f} "
+            f"{row['endpoint_distance_one_sided_gated']:>9.3f} "
+            f"{row['ambiguous_beta_one_sided_gated']}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=4096)
+    parser.add_argument("--n-replicates", type=int, default=20)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=32)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=202605070901)
+    parser.add_argument("--out", default="tools/data/denominator_gate_transfer_matrix.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

 succeeded in 4153ms:
observables_registry=1.0.0-2026-05-06
observables_used=['SR', 'SR2', 'L1', 'L2', 'triple_var']
perimeter one_sided stable0 stable1 dist_all dist_gate ambiguous_gate
       DUALITA_golden       SR,L1,triple_var   3.000   0.250    3.422     3.418 [0.30000000000000004]
     R_periodic_triad SR,SR2,L1,L2,triple_var   5.000   0.250    4.400     4.400 [0.30000000000000004]
 T_markov_alternating SR,SR2,L1,L2,triple_var   5.000   0.050    4.412     4.412 [0.30000000000000004]
     E_ar1_continuity SR,SR2,L1,L2,triple_var   5.000   0.600    4.394     4.394 [0.30000000000000004]
saved tools/data/denominator_gate_transfer_matrix.json

codex
Il deposito mostra un pattern forte: in tutti i perimetri il supporto è one-sided sul polo coerente; la coordinata di ambiguità invece collassa nello stesso beta intermedio. Leggo il JSON per evitare claim oltre i numeri e preparo il report con le 5 lenti esplicite.
exec
/bin/bash -lc 'ls -1 tools/data/reports | tail -20' in /opt/MM_D-ND
 succeeded in 0ms:
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
trajectory_apply_20260507_0803.json
trajectory_apply_20260507_0901.json
watchdog.log

exec
/bin/bash -lc 'python -m py_compile tools/exp_denominator_gate_transfer_matrix.py' in /opt/MM_D-ND
 succeeded in 0ms:
exec
/bin/bash -lc "python - <<'PY'
import json
p='tools/data/denominator_gate_transfer_matrix.json'
d=json.load(open(p))
print(json.dumps(d['matrix'], indent=2))
for name, data in d['perimeters'].items():
    print('\\n', name)
    gate=data['gate']
    for beta in ['0.000','0.300','0.500','1.000']:
        layer=gate['layers'][beta]
        print(beta, 'stable', round(layer['stable_count_mean'],3), 'z', {k: round(v,2) for k,v in layer['z_mean'].items()})
        cg=data['classification_one_sided_gated']['layers'][beta]
        print('  margin', round(cg['margin_mean'],3), 'amb', round(cg['ambiguous_fraction'],3), 'ill', round(cg['illusory_label_fraction'],3))
PY" in /opt/MM_D-ND
 succeeded in 0ms:
{
  "DUALITA_golden": {
    "coherent_one_sided_observables": [
      "SR",
      "L1",
      "triple_var"
    ],
    "endpoint_stable_observables": [],
    "endpoint_distance_all": 3.4215888194133655,
    "endpoint_distance_one_sided_gated": 3.4182025126759505,
    "ambiguous_beta_all": [
      0.30000000000000004
    ],
    "ambiguous_beta_one_sided_gated": [
      0.30000000000000004
    ],
    "stable_count_coherent": 3.0,
    "stable_count_illusory": 0.25
  },
  "R_periodic_triad": {
    "coherent_one_sided_observables": [
      "SR",
      "SR2",
      "L1",
      "L2",
      "triple_var"
    ],
    "endpoint_stable_observables": [],
    "endpoint_distance_all": 4.400058194601154,
    "endpoint_distance_one_sided_gated": 4.400058194601154,
    "ambiguous_beta_all": [
      0.30000000000000004
    ],
    "ambiguous_beta_one_sided_gated": [
      0.30000000000000004
    ],
    "stable_count_coherent": 5.0,
    "stable_count_illusory": 0.25
  },
  "T_markov_alternating": {
    "coherent_one_sided_observables": [
      "SR",
      "SR2",
      "L1",
      "L2",
      "triple_var"
    ],
    "endpoint_stable_observables": [],
    "endpoint_distance_all": 4.412461534213811,
    "endpoint_distance_one_sided_gated": 4.412461534213811,
    "ambiguous_beta_all": [
      0.30000000000000004
    ],
    "ambiguous_beta_one_sided_gated": [
      0.30000000000000004
    ],
    "stable_count_coherent": 5.0,
    "stable_count_illusory": 0.05
  },
  "E_ar1_continuity": {
    "coherent_one_sided_observables": [
      "SR",
      "SR2",
      "L1",
      "L2",
      "triple_var"
    ],
    "endpoint_stable_observables": [],
    "endpoint_distance_all": 4.39407612238746,
    "endpoint_distance_one_sided_gated": 4.39407612238746,
    "ambiguous_beta_all": [
      0.30000000000000004
    ],
    "ambiguous_beta_one_sided_gated": [
      0.30000000000000004
    ],
    "stable_count_coherent": 5.0,
    "stable_count_illusory": 0.6
  }
}

 DUALITA_golden
0.000 stable 3.0 z {'SR': -40.73, 'SR2': 0.12, 'L1': -40.72, 'L2': 0.12, 'triple_var': -35.8}
  margin 0.999 amb 0.0 ill 0.0
0.300 stable 3.0 z {'SR': -20.28, 'SR2': -0.18, 'L1': -20.28, 'L2': -0.18, 'triple_var': -18.02}
  margin 0.052 amb 0.95 ill 0.6
0.500 stable 3.2 z {'SR': -10.92, 'SR2': 0.46, 'L1': -10.92, 'L2': 0.46, 'triple_var': -9.48}
  margin 0.501 amb 0.0 ill 1.0
1.000 stable 0.25 z {'SR': -0.11, 'SR2': -0.01, 'L1': -0.12, 'L2': -0.01, 'triple_var': -0.1}
  margin 0.955 amb 0.0 ill 1.0

 R_periodic_triad
0.000 stable 5.0 z {'SR': -43.08, 'SR2': -8.83, 'L1': -21.71, 'L2': -24.08, 'triple_var': -30.34}
  margin 0.991 amb 0.0 ill 0.0
0.300 stable 5.0 z {'SR': -20.06, 'SR2': -4.14, 'L1': -9.84, 'L2': -12.12, 'triple_var': -14.09}
  margin 0.051 amb 0.95 ill 0.6
0.500 stable 4.6 z {'SR': -10.86, 'SR2': -2.46, 'L1': -5.59, 'L2': -6.63, 'triple_var': -8.25}
  margin 0.465 amb 0.0 ill 1.0
1.000 stable 0.25 z {'SR': -0.08, 'SR2': -0.01, 'L1': 0.09, 'L2': -0.06, 'triple_var': 0.03}
  margin 0.899 amb 0.0 ill 1.0

 T_markov_alternating
0.000 stable 5.0 z {'SR': -49.47, 'SR2': 36.18, 'L1': -49.47, 'L2': 36.26, 'triple_var': -27.17}
  margin 0.973 amb 0.0 ill 0.0
0.300 stable 5.0 z {'SR': -25.67, 'SR2': 19.26, 'L1': -25.6, 'L2': 19.33, 'triple_var': -14.32}
  margin 0.044 amb 1.0 ill 0.7
0.500 stable 5.0 z {'SR': -11.99, 'SR2': 8.76, 'L1': -11.98, 'L2': 8.77, 'triple_var': -6.54}
  margin 0.506 amb 0.0 ill 1.0
1.000 stable 0.05 z {'SR': -0.17, 'SR2': 0.05, 'L1': -0.17, 'L2': 0.03, 'triple_var': -0.11}
  margin 0.959 amb 0.0 ill 1.0

 E_ar1_continuity
0.000 stable 5.0 z {'SR': 76.67, 'SR2': 54.48, 'L1': 57.97, 'L2': 47.63, 'triple_var': 71.68}
  margin 0.915 amb 0.0 ill 0.0
0.300 stable 5.0 z {'SR': 36.23, 'SR2': 28.81, 'L1': 26.3, 'L2': 23.64, 'triple_var': 33.24}
  margin 0.043 amb 1.0 ill 0.75
0.500 stable 5.0 z {'SR': 19.01, 'SR2': 14.21, 'L1': 13.8, 'L2': 12.03, 'triple_var': 18.02}
  margin 0.495 amb 0.0 ill 1.0
1.000 stable 0.6 z {'SR': 0.41, 'SR2': 0.45, 'L1': 0.45, 'L2': 0.44, 'triple_var': 0.61}
  margin 0.927 amb 0.0 ill 1.0

diff --git a/tools/exp_denominator_gate_transfer_matrix.py b/tools/exp_denominator_gate_transfer_matrix.py
new file mode 100644
index 0000000000000000000000000000000000000000..bb0a837fe1303dca05fa887b9ca28e30c4bf9031
--- /dev/null
+++ b/tools/exp_denominator_gate_transfer_matrix.py
@@ -0,0 +1,337 @@
+#!/usr/bin/env python3
+"""
+exp_denominator_gate_transfer_matrix.py
+
+Reusable META audit for the denominator gate transfer matrix.
+
+The experiment moves the original-vs-shuffle denominator gate away from the
+GUE/Poisson BOUNDARY perimeter. Each perimeter has a coherent endpoint and an
+illusory endpoint built as a permutation of the same gap multiset. That keeps
+the one-point distribution fixed and isolates ordering support.
+
+Measured for each perimeter:
+- canonical observables from observables_registry.py;
+- original-vs-shuffle z-score for each observable;
+- endpoint-stable observable set under |z| >= z_min;
+- endpoint classification using all observables and endpoint-gated observables;
+- beta layer ambiguity between coherent and illusory endpoints.
+"""
+
+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())
+PHI = (1.0 + 5.0**0.5) / 2.0
+
+
+def normalize(gaps: np.ndarray) -> np.ndarray:
+    gaps = np.asarray(gaps, dtype=float)
+    gaps = np.maximum(gaps, 1e-12)
+    mean = float(np.mean(gaps))
+    return gaps / mean if mean > 1e-15 else gaps
+
+
+def golden_beatty(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    phase = float(rng.random())
+    n = np.arange(n_gaps + 1, dtype=float) + phase
+    positions = np.floor(n * PHI)
+    return normalize(np.diff(positions))
+
+
+def periodic_triad(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    base = np.array([0.55, 1.0, 1.45, 1.0, 0.75, 1.25], dtype=float)
+    shift = int(rng.integers(0, len(base)))
+    tiled = np.tile(np.roll(base, shift), int(np.ceil(n_gaps / len(base))))[:n_gaps]
+    jitter = rng.normal(0.0, 0.015, size=n_gaps)
+    return normalize(tiled + jitter)
+
+
+def markov_alternating(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    vals = np.array([0.62, 1.38], dtype=float)
+    state = int(rng.integers(0, 2))
+    out = np.empty(n_gaps, dtype=float)
+    for i in range(n_gaps):
+        out[i] = vals[state] + rng.normal(0.0, 0.03)
+        if rng.random() < 0.88:
+            state = 1 - state
+    return normalize(out)
+
+
+def ar1_continuity(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    rho = 0.86
+    x = np.empty(n_gaps, dtype=float)
+    x[0] = rng.normal()
+    noise_scale = (1.0 - rho * rho) ** 0.5
+    for i in range(1, n_gaps):
+        x[i] = rho * x[i - 1] + noise_scale * rng.normal()
+    return normalize(np.exp(0.42 * x))
+
+
+PERIMETERS = {
+    "DUALITA_golden": golden_beatty,
+    "R_periodic_triad": periodic_triad,
+    "T_markov_alternating": markov_alternating,
+    "E_ar1_continuity": ar1_continuity,
+}
+
+
+def beta_layer(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        out = base.copy()
+    elif beta >= 1.0:
+        out = illusory.copy()
+    else:
+        mask = rng.random(len(base)) < beta
+        out = base.copy()
+        out[mask] = illusory[mask]
+    return normalize(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]]:
+    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])
+
+    shuffle_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
+        shuffle_std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, shuffle_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:
+    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)
+
+    coherent_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent_vectors, illusory_vectors])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent_vectors, axis=0)
+    illusory_centroid = np.mean(illusory_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((illusory_centroid - coherent_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_coherent = float(np.linalg.norm((x - coherent_centroid) / scale))
+            d_illusory = float(np.linalg.norm((x - illusory_centroid) / scale))
+            denom = d_coherent + d_illusory
+            coord = float((d_coherent - d_illusory) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_coherent - d_illusory) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("coherent" if d_coherent < d_illusory else "illusory")
+        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,
+            "illusory_label_fraction": float(np.mean(np.array(labels) == "illusory")),
+        }
+
+    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}
+        z_values = {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)
+                z_values[name].append(row["z"][name])
+        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()},
+            "z_mean": {name: float(np.mean(vals)) for name, vals in z_values.items()},
+        }
+
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    endpoint_one_sided = []
+    for name in OBS_NAMES:
+        coherent_freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in coherent_rows])
+        illusory_freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in illusory_rows])
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            endpoint_one_sided.append(name)
+        if np.mean([coherent_freq, illusory_freq]) >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "coherent_one_sided_observables": endpoint_one_sided,
+        "layers": layers,
+    }
+
+
+def analyze_perimeter(name: str, generator, args: argparse.Namespace, rng: np.random.Generator) -> dict:
+    rows = []
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    for rep in range(args.n_replicates):
+        base = generator(args.n_gaps, np.random.default_rng(rng.integers(0, 2**63 - 1)))
+        for beta in betas:
+            gaps = beta_layer(base, beta, np.random.default_rng(rng.integers(0, 2**63 - 1)))
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                args.n_baseline,
+                np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "perimeter": name,
+                    "replicate": rep,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [obs_name for obs_name in OBS_NAMES if abs(z[obs_name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    one_sided_classification = classify_layers(rows, gate["coherent_one_sided_observables"])
+    return {
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_one_sided_gated": one_sided_classification,
+        "rows": rows,
+    }
+
+
+def compact_matrix(perimeters: dict) -> dict:
+    matrix = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        class_all = data["classification_all_observables"]
+        class_gate = data["classification_one_sided_gated"]
+        matrix[name] = {
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "endpoint_distance_all": class_all["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": class_gate["endpoint_distance"],
+            "ambiguous_beta_all": class_all["ambiguous_beta"],
+            "ambiguous_beta_one_sided_gated": class_gate["ambiguous_beta"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+        }
+    return matrix
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    perimeters = {}
+    for name, generator in PERIMETERS.items():
+        perimeters[name] = analyze_perimeter(name, generator, args, root_rng)
+
+    output = {
+        "experiment": "denominator_gate_transfer_matrix",
+        "category": "gate_transferability",
+        "question": "Which parts of the denominator gate transfer outside BOUNDARY?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact_matrix(perimeters),
+        "perimeters": perimeters,
+    }
+
+    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("perimeter one_sided stable0 stable1 dist_all dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>21s} "
+            f"{','.join(row['coherent_one_sided_observables']) or '[]':>22s} "
+            f"{row['stable_count_coherent']:>7.3f} "
+            f"{row['stable_count_illusory']:>7.3f} "
+            f"{row['endpoint_distance_all']:>8.3f} "
+            f"{row['endpoint_distance_one_sided_gated']:>9.3f} "
+            f"{row['ambiguous_beta_one_sided_gated']}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=4096)
+    parser.add_argument("--n-replicates", type=int, default=20)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=32)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=202605070901)
+    parser.add_argument("--out", default="tools/data/denominator_gate_transfer_matrix.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

exec
/bin/bash -lc 'tail -80 tools/data/seme.json' in /opt/MM_D-ND
 succeeded in 0ms:
        "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"
      ],
      "ponti_potenziali": [
        {
          "coppia": "TxQ",
          "ponte": "noise come risorsa computazionale — fluttuazioni termiche = calcolo",
          "nota": "il vuoto quantistico (pieno di fluttuazioni) è il computer"
        }
      ],
      "timestamp": "2026-04-02T08:23:13.992019"
    },
    {
      "id": "video_j0wJBEZdwLs",
      "title": "What is a Laplace Transform - visual explanation",
      "type": "video_digest",
      "keywords": [
        "Laplace transform",
        "frequency",
        "damping",
        "s-plane",
        "complex",
        "exponential"
      ],
      "content": "La trasformata di Laplace come proiezione su esponenziali complesse. Il piano s = σ + iω combina smorzamento (reale) e oscillazione (immaginario). Connessione tra dominio temporale e dominio delle frequenze complesse.",
      "teorie": [
        "T",
        "Q",
        "R"
      ],
      "ponti_potenziali": [],
      "timestamp": "2026-04-02T08:23:13.992021"
    },
    {
      "id": "video_rZ2m1_q9lg0",
      "title": "New duality: conductor-insulator in YbB12 at 35T - University of Michigan",
      "type": "video_digest",
      "keywords": [
        "duality",
        "conductor",
        "insulator",
        "Kondo insulator",
        "quantum oscillations",
        "ytterbium boride",
        "YbB12",
        "charge-neutral fermions",
        "strongly correlated",
        "condensed matter",
        "magnetic field"
      ],
      "content": "Scoperta dualita conduttore-isolante in YbB12 a 35 Tesla. Il materiale si comporta simultaneamente da conduttore e isolante — oscillazioni quantistiche dal bulk, non dalla superficie. Charge-neutral fermions (portatori senza carica) creano comportamento di conduzione = il terzo incluso. La scoperta avviene a campo estremo dove il confine tra stati collassa.",
      "teorie": [
        "T",
        "Q",
        "E"
      ],
      "ponti_potenziali": [
        {
          "coppia": "TxQ",
          "ponte": "Kondo insulator fortemente correlato — dualita conduttore/isolante come stato quantistico unico",
          "nota": "charge-neutral fermions = portatori senza carica che conducono, terzo incluso operativo"
        },
        {
          "coppia": "ExQ",
          "ponte": "campo magnetico estremo (35T) collassa il confine tra stati — condizione di soglia",
          "nota": "il confine non separa, unifica. Pattern BOUNDARY del Domandatore"
        }
      ],
      "ref": "Physical Review Letters 2025, 135(15). Chen, Zhu, Ratkovski et al. PI: Lu Li",
      "timestamp": "2026-04-04T19:34:08.014Z"
    }
  ],
  "_seed_history": {
    "trajectory_apply_20260507_090145": "applied modify_seme from 20260507_0803: direzione → 'Costruire una matrice di trasferibilita' del denominator gate: applicarlo a 3-4 perimetri non-BOUNDA...'"
  }
}
diff --git a/tools/exp_denominator_gate_transfer_matrix.py b/tools/exp_denominator_gate_transfer_matrix.py
new file mode 100644
index 0000000000000000000000000000000000000000..bb0a837fe1303dca05fa887b9ca28e30c4bf9031
--- /dev/null
+++ b/tools/exp_denominator_gate_transfer_matrix.py
@@ -0,0 +1,337 @@
+#!/usr/bin/env python3
+"""
+exp_denominator_gate_transfer_matrix.py
+
+Reusable META audit for the denominator gate transfer matrix.
+
+The experiment moves the original-vs-shuffle denominator gate away from the
+GUE/Poisson BOUNDARY perimeter. Each perimeter has a coherent endpoint and an
+illusory endpoint built as a permutation of the same gap multiset. That keeps
+the one-point distribution fixed and isolates ordering support.
+
+Measured for each perimeter:
+- canonical observables from observables_registry.py;
+- original-vs-shuffle z-score for each observable;
+- endpoint-stable observable set under |z| >= z_min;
+- endpoint classification using all observables and endpoint-gated observables;
+- beta layer ambiguity between coherent and illusory endpoints.
+"""
+
+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())
+PHI = (1.0 + 5.0**0.5) / 2.0
+
+
+def normalize(gaps: np.ndarray) -> np.ndarray:
+    gaps = np.asarray(gaps, dtype=float)
+    gaps = np.maximum(gaps, 1e-12)
+    mean = float(np.mean(gaps))
+    return gaps / mean if mean > 1e-15 else gaps
+
+
+def golden_beatty(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    phase = float(rng.random())
+    n = np.arange(n_gaps + 1, dtype=float) + phase
+    positions = np.floor(n * PHI)
+    return normalize(np.diff(positions))
+
+
+def periodic_triad(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    base = np.array([0.55, 1.0, 1.45, 1.0, 0.75, 1.25], dtype=float)
+    shift = int(rng.integers(0, len(base)))
+    tiled = np.tile(np.roll(base, shift), int(np.ceil(n_gaps / len(base))))[:n_gaps]
+    jitter = rng.normal(0.0, 0.015, size=n_gaps)
+    return normalize(tiled + jitter)
+
+
+def markov_alternating(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    vals = np.array([0.62, 1.38], dtype=float)
+    state = int(rng.integers(0, 2))
+    out = np.empty(n_gaps, dtype=float)
+    for i in range(n_gaps):
+        out[i] = vals[state] + rng.normal(0.0, 0.03)
+        if rng.random() < 0.88:
+            state = 1 - state
+    return normalize(out)
+
+
+def ar1_continuity(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    rho = 0.86
+    x = np.empty(n_gaps, dtype=float)
+    x[0] = rng.normal()
+    noise_scale = (1.0 - rho * rho) ** 0.5
+    for i in range(1, n_gaps):
+        x[i] = rho * x[i - 1] + noise_scale * rng.normal()
+    return normalize(np.exp(0.42 * x))
+
+
+PERIMETERS = {
+    "DUALITA_golden": golden_beatty,
+    "R_periodic_triad": periodic_triad,
+    "T_markov_alternating": markov_alternating,
+    "E_ar1_continuity": ar1_continuity,
+}
+
+
+def beta_layer(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        out = base.copy()
+    elif beta >= 1.0:
+        out = illusory.copy()
+    else:
+        mask = rng.random(len(base)) < beta
+        out = base.copy()
+        out[mask] = illusory[mask]
+    return normalize(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]]:
+    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])
+
+    shuffle_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
+        shuffle_std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, shuffle_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:
+    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)
+
+    coherent_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent_vectors, illusory_vectors])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent_vectors, axis=0)
+    illusory_centroid = np.mean(illusory_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((illusory_centroid - coherent_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_coherent = float(np.linalg.norm((x - coherent_centroid) / scale))
+            d_illusory = float(np.linalg.norm((x - illusory_centroid) / scale))
+            denom = d_coherent + d_illusory
+            coord = float((d_coherent - d_illusory) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_coherent - d_illusory) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("coherent" if d_coherent < d_illusory else "illusory")
+        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,
+            "illusory_label_fraction": float(np.mean(np.array(labels) == "illusory")),
+        }
+
+    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}
+        z_values = {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)
+                z_values[name].append(row["z"][name])
+        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()},
+            "z_mean": {name: float(np.mean(vals)) for name, vals in z_values.items()},
+        }
+
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    endpoint_one_sided = []
+    for name in OBS_NAMES:
+        coherent_freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in coherent_rows])
+        illusory_freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in illusory_rows])
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            endpoint_one_sided.append(name)
+        if np.mean([coherent_freq, illusory_freq]) >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "coherent_one_sided_observables": endpoint_one_sided,
+        "layers": layers,
+    }
+
+
+def analyze_perimeter(name: str, generator, args: argparse.Namespace, rng: np.random.Generator) -> dict:
+    rows = []
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    for rep in range(args.n_replicates):
+        base = generator(args.n_gaps, np.random.default_rng(rng.integers(0, 2**63 - 1)))
+        for beta in betas:
+            gaps = beta_layer(base, beta, np.random.default_rng(rng.integers(0, 2**63 - 1)))
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                args.n_baseline,
+                np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "perimeter": name,
+                    "replicate": rep,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [obs_name for obs_name in OBS_NAMES if abs(z[obs_name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    one_sided_classification = classify_layers(rows, gate["coherent_one_sided_observables"])
+    return {
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_one_sided_gated": one_sided_classification,
+        "rows": rows,
+    }
+
+
+def compact_matrix(perimeters: dict) -> dict:
+    matrix = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        class_all = data["classification_all_observables"]
+        class_gate = data["classification_one_sided_gated"]
+        matrix[name] = {
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "endpoint_distance_all": class_all["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": class_gate["endpoint_distance"],
+            "ambiguous_beta_all": class_all["ambiguous_beta"],
+            "ambiguous_beta_one_sided_gated": class_gate["ambiguous_beta"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+        }
+    return matrix
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    perimeters = {}
+    for name, generator in PERIMETERS.items():
+        perimeters[name] = analyze_perimeter(name, generator, args, root_rng)
+
+    output = {
+        "experiment": "denominator_gate_transfer_matrix",
+        "category": "gate_transferability",
+        "question": "Which parts of the denominator gate transfer outside BOUNDARY?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact_matrix(perimeters),
+        "perimeters": perimeters,
+    }
+
+    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("perimeter one_sided stable0 stable1 dist_all dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>21s} "
+            f"{','.join(row['coherent_one_sided_observables']) or '[]':>22s} "
+            f"{row['stable_count_coherent']:>7.3f} "
+            f"{row['stable_count_illusory']:>7.3f} "
+            f"{row['endpoint_distance_all']:>8.3f} "
+            f"{row['endpoint_distance_one_sided_gated']:>9.3f} "
+            f"{row['ambiguous_beta_one_sided_gated']}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=4096)
+    parser.add_argument("--n-replicates", type=int, default=20)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=32)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=202605070901)
+    parser.add_argument("--out", default="tools/data/denominator_gate_transfer_matrix.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

exec
/bin/bash -lc "rg -n '\"id\": \"TRAJECTORY_APPLY_20260507_0803\"|\"id\": \"BOUNDARY\"|\"direzione\"|\"_seed_history\"' tools/data/seme.json" in /opt/MM_D-ND
 succeeded in 0ms:
95:      "id": "BOUNDARY",
126:      "id": "TRAJECTORY_APPLY_20260507_0803",
146:  "direzione": "Costruire una matrice di trasferibilita' del denominator gate: applicarlo a 3-4 perimetri non-BOUNDARY con poli coerente/illusorio e verificare quali parti trasferiscono (supporto one-sided, coordinate di collasso, ambiguita' classificativa) e quali restano locali.",
313:  "_seed_history": {

diff --git a/tools/exp_denominator_gate_transfer_matrix.py b/tools/exp_denominator_gate_transfer_matrix.py
new file mode 100644
index 0000000000000000000000000000000000000000..bb0a837fe1303dca05fa887b9ca28e30c4bf9031
--- /dev/null
+++ b/tools/exp_denominator_gate_transfer_matrix.py
@@ -0,0 +1,337 @@
+#!/usr/bin/env python3
+"""
+exp_denominator_gate_transfer_matrix.py
+
+Reusable META audit for the denominator gate transfer matrix.
+
+The experiment moves the original-vs-shuffle denominator gate away from the
+GUE/Poisson BOUNDARY perimeter. Each perimeter has a coherent endpoint and an
+illusory endpoint built as a permutation of the same gap multiset. That keeps
+the one-point distribution fixed and isolates ordering support.
+
+Measured for each perimeter:
+- canonical observables from observables_registry.py;
+- original-vs-shuffle z-score for each observable;
+- endpoint-stable observable set under |z| >= z_min;
+- endpoint classification using all observables and endpoint-gated observables;
+- beta layer ambiguity between coherent and illusory endpoints.
+"""
+
+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())
+PHI = (1.0 + 5.0**0.5) / 2.0
+
+
+def normalize(gaps: np.ndarray) -> np.ndarray:
+    gaps = np.asarray(gaps, dtype=float)
+    gaps = np.maximum(gaps, 1e-12)
+    mean = float(np.mean(gaps))
+    return gaps / mean if mean > 1e-15 else gaps
+
+
+def golden_beatty(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    phase = float(rng.random())
+    n = np.arange(n_gaps + 1, dtype=float) + phase
+    positions = np.floor(n * PHI)
+    return normalize(np.diff(positions))
+
+
+def periodic_triad(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    base = np.array([0.55, 1.0, 1.45, 1.0, 0.75, 1.25], dtype=float)
+    shift = int(rng.integers(0, len(base)))
+    tiled = np.tile(np.roll(base, shift), int(np.ceil(n_gaps / len(base))))[:n_gaps]
+    jitter = rng.normal(0.0, 0.015, size=n_gaps)
+    return normalize(tiled + jitter)
+
+
+def markov_alternating(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    vals = np.array([0.62, 1.38], dtype=float)
+    state = int(rng.integers(0, 2))
+    out = np.empty(n_gaps, dtype=float)
+    for i in range(n_gaps):
+        out[i] = vals[state] + rng.normal(0.0, 0.03)
+        if rng.random() < 0.88:
+            state = 1 - state
+    return normalize(out)
+
+
+def ar1_continuity(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    rho = 0.86
+    x = np.empty(n_gaps, dtype=float)
+    x[0] = rng.normal()
+    noise_scale = (1.0 - rho * rho) ** 0.5
+    for i in range(1, n_gaps):
+        x[i] = rho * x[i - 1] + noise_scale * rng.normal()
+    return normalize(np.exp(0.42 * x))
+
+
+PERIMETERS = {
+    "DUALITA_golden": golden_beatty,
+    "R_periodic_triad": periodic_triad,
+    "T_markov_alternating": markov_alternating,
+    "E_ar1_continuity": ar1_continuity,
+}
+
+
+def beta_layer(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        out = base.copy()
+    elif beta >= 1.0:
+        out = illusory.copy()
+    else:
+        mask = rng.random(len(base)) < beta
+        out = base.copy()
+        out[mask] = illusory[mask]
+    return normalize(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]]:
+    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])
+
+    shuffle_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
+        shuffle_std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, shuffle_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:
+    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)
+
+    coherent_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent_vectors, illusory_vectors])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent_vectors, axis=0)
+    illusory_centroid = np.mean(illusory_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((illusory_centroid - coherent_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_coherent = float(np.linalg.norm((x - coherent_centroid) / scale))
+            d_illusory = float(np.linalg.norm((x - illusory_centroid) / scale))
+            denom = d_coherent + d_illusory
+            coord = float((d_coherent - d_illusory) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_coherent - d_illusory) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("coherent" if d_coherent < d_illusory else "illusory")
+        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,
+            "illusory_label_fraction": float(np.mean(np.array(labels) == "illusory")),
+        }
+
+    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}
+        z_values = {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)
+                z_values[name].append(row["z"][name])
+        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()},
+            "z_mean": {name: float(np.mean(vals)) for name, vals in z_values.items()},
+        }
+
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    endpoint_one_sided = []
+    for name in OBS_NAMES:
+        coherent_freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in coherent_rows])
+        illusory_freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in illusory_rows])
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            endpoint_one_sided.append(name)
+        if np.mean([coherent_freq, illusory_freq]) >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "coherent_one_sided_observables": endpoint_one_sided,
+        "layers": layers,
+    }
+
+
+def analyze_perimeter(name: str, generator, args: argparse.Namespace, rng: np.random.Generator) -> dict:
+    rows = []
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    for rep in range(args.n_replicates):
+        base = generator(args.n_gaps, np.random.default_rng(rng.integers(0, 2**63 - 1)))
+        for beta in betas:
+            gaps = beta_layer(base, beta, np.random.default_rng(rng.integers(0, 2**63 - 1)))
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                args.n_baseline,
+                np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "perimeter": name,
+                    "replicate": rep,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [obs_name for obs_name in OBS_NAMES if abs(z[obs_name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    one_sided_classification = classify_layers(rows, gate["coherent_one_sided_observables"])
+    return {
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_one_sided_gated": one_sided_classification,
+        "rows": rows,
+    }
+
+
+def compact_matrix(perimeters: dict) -> dict:
+    matrix = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        class_all = data["classification_all_observables"]
+        class_gate = data["classification_one_sided_gated"]
+        matrix[name] = {
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "endpoint_distance_all": class_all["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": class_gate["endpoint_distance"],
+            "ambiguous_beta_all": class_all["ambiguous_beta"],
+            "ambiguous_beta_one_sided_gated": class_gate["ambiguous_beta"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+        }
+    return matrix
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    perimeters = {}
+    for name, generator in PERIMETERS.items():
+        perimeters[name] = analyze_perimeter(name, generator, args, root_rng)
+
+    output = {
+        "experiment": "denominator_gate_transfer_matrix",
+        "category": "gate_transferability",
+        "question": "Which parts of the denominator gate transfer outside BOUNDARY?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact_matrix(perimeters),
+        "perimeters": perimeters,
+    }
+
+    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("perimeter one_sided stable0 stable1 dist_all dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>21s} "
+            f"{','.join(row['coherent_one_sided_observables']) or '[]':>22s} "
+            f"{row['stable_count_coherent']:>7.3f} "
+            f"{row['stable_count_illusory']:>7.3f} "
+            f"{row['endpoint_distance_all']:>8.3f} "
+            f"{row['endpoint_distance_one_sided_gated']:>9.3f} "
+            f"{row['ambiguous_beta_one_sided_gated']}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=4096)
+    parser.add_argument("--n-replicates", type=int, default=20)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=32)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=202605070901)
+    parser.add_argument("--out", default="tools/data/denominator_gate_transfer_matrix.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

exec
/bin/bash -lc "sed -n '110,155p' tools/data/seme.json" in /opt/MM_D-ND
 succeeded in 0ms:
      "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"
    },
    {
      "tipo": "task",
      "id": "TRAJECTORY_APPLY_20260507_0803",
      "claim": "Applied valutatore REDESIGN from 20260507_0803: Costruire una matrice di trasferibilita' del denominator gate: applicarlo a 3-4 perimetri non-BOUNDARY con poli coerente/illusorio e verificare quali parti trasferiscono (supporto one-sided, coordinat",
      "intensità": 0.7,
      "porta": "trajectory_apply",
      "condensato_ref": "A8,A14,A15",
      "manuale": true,
      "_source_log": "2026-05-07T08:10:22.658201+00:00",
      "_source_decision": "REDESIGN",
      "_source_reasoning": "Il ciclo ha prodotto evidenza controllata e replicata che il denominator gate trasferisce come operatore, ma non trasferisce la coordinata di layer BOUNDARY: ambiguita' classificativa e collasso del denominatore si separano. Continuare sul seme attuale centrato su GUE/Poisson rischia di restare nel "
    }
  ],
  "potenziale_bloccato": [],
  "varianza": [
    "Tensioni risolte: {'PIANO_PRIMARIO_DUE_ASSIOMI', 'METRIC_TENSOR', 'BOUNDARY_LAYER_GATE', 'OBSERVABLE_REGISTRY', 'DUALITA_DIPOLARE_VS_ILLUSORIA', 'TENSIONE_ENTITA', 'PERTURBATION_DENOMINATOR_GATE', 'G_POTENZIALE_NULLA', 'TRASCENDENZA_LIMITE'}"
  ],
  "filtro": {
    "promosse": 11,
    "filtrate": 0,
    "condensato_claims": 30
  },
  "direzione": "Costruire una matrice di trasferibilita' del denominator gate: applicarlo a 3-4 perimetri non-BOUNDARY con poli coerente/illusorio e verificare quali parti trasferiscono (supporto one-sided, coordinate di collasso, ambiguita' classificativa) e quali restano locali.",
  "verifica": {
    "pass": 11,
    "fail": 0,
    "skip": 0,
    "total": 11
  },
  "fonti_consumate": 0,
  "fonti_esterne": [
    {

diff --git a/tools/exp_denominator_gate_transfer_matrix.py b/tools/exp_denominator_gate_transfer_matrix.py
new file mode 100644
index 0000000000000000000000000000000000000000..bb0a837fe1303dca05fa887b9ca28e30c4bf9031
--- /dev/null
+++ b/tools/exp_denominator_gate_transfer_matrix.py
@@ -0,0 +1,337 @@
+#!/usr/bin/env python3
+"""
+exp_denominator_gate_transfer_matrix.py
+
+Reusable META audit for the denominator gate transfer matrix.
+
+The experiment moves the original-vs-shuffle denominator gate away from the
+GUE/Poisson BOUNDARY perimeter. Each perimeter has a coherent endpoint and an
+illusory endpoint built as a permutation of the same gap multiset. That keeps
+the one-point distribution fixed and isolates ordering support.
+
+Measured for each perimeter:
+- canonical observables from observables_registry.py;
+- original-vs-shuffle z-score for each observable;
+- endpoint-stable observable set under |z| >= z_min;
+- endpoint classification using all observables and endpoint-gated observables;
+- beta layer ambiguity between coherent and illusory endpoints.
+"""
+
+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())
+PHI = (1.0 + 5.0**0.5) / 2.0
+
+
+def normalize(gaps: np.ndarray) -> np.ndarray:
+    gaps = np.asarray(gaps, dtype=float)
+    gaps = np.maximum(gaps, 1e-12)
+    mean = float(np.mean(gaps))
+    return gaps / mean if mean > 1e-15 else gaps
+
+
+def golden_beatty(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    phase = float(rng.random())
+    n = np.arange(n_gaps + 1, dtype=float) + phase
+    positions = np.floor(n * PHI)
+    return normalize(np.diff(positions))
+
+
+def periodic_triad(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    base = np.array([0.55, 1.0, 1.45, 1.0, 0.75, 1.25], dtype=float)
+    shift = int(rng.integers(0, len(base)))
+    tiled = np.tile(np.roll(base, shift), int(np.ceil(n_gaps / len(base))))[:n_gaps]
+    jitter = rng.normal(0.0, 0.015, size=n_gaps)
+    return normalize(tiled + jitter)
+
+
+def markov_alternating(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    vals = np.array([0.62, 1.38], dtype=float)
+    state = int(rng.integers(0, 2))
+    out = np.empty(n_gaps, dtype=float)
+    for i in range(n_gaps):
+        out[i] = vals[state] + rng.normal(0.0, 0.03)
+        if rng.random() < 0.88:
+            state = 1 - state
+    return normalize(out)
+
+
+def ar1_continuity(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    rho = 0.86
+    x = np.empty(n_gaps, dtype=float)
+    x[0] = rng.normal()
+    noise_scale = (1.0 - rho * rho) ** 0.5
+    for i in range(1, n_gaps):
+        x[i] = rho * x[i - 1] + noise_scale * rng.normal()
+    return normalize(np.exp(0.42 * x))
+
+
+PERIMETERS = {
+    "DUALITA_golden": golden_beatty,
+    "R_periodic_triad": periodic_triad,
+    "T_markov_alternating": markov_alternating,
+    "E_ar1_continuity": ar1_continuity,
+}
+
+
+def beta_layer(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        out = base.copy()
+    elif beta >= 1.0:
+        out = illusory.copy()
+    else:
+        mask = rng.random(len(base)) < beta
+        out = base.copy()
+        out[mask] = illusory[mask]
+    return normalize(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]]:
+    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])
+
+    shuffle_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
+        shuffle_std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, shuffle_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:
+    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)
+
+    coherent_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent_vectors, illusory_vectors])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent_vectors, axis=0)
+    illusory_centroid = np.mean(illusory_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((illusory_centroid - coherent_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_coherent = float(np.linalg.norm((x - coherent_centroid) / scale))
+            d_illusory = float(np.linalg.norm((x - illusory_centroid) / scale))
+            denom = d_coherent + d_illusory
+            coord = float((d_coherent - d_illusory) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_coherent - d_illusory) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("coherent" if d_coherent < d_illusory else "illusory")
+        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,
+            "illusory_label_fraction": float(np.mean(np.array(labels) == "illusory")),
+        }
+
+    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}
+        z_values = {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)
+                z_values[name].append(row["z"][name])
+        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()},
+            "z_mean": {name: float(np.mean(vals)) for name, vals in z_values.items()},
+        }
+
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    endpoint_one_sided = []
+    for name in OBS_NAMES:
+        coherent_freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in coherent_rows])
+        illusory_freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in illusory_rows])
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            endpoint_one_sided.append(name)
+        if np.mean([coherent_freq, illusory_freq]) >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "coherent_one_sided_observables": endpoint_one_sided,
+        "layers": layers,
+    }
+
+
+def analyze_perimeter(name: str, generator, args: argparse.Namespace, rng: np.random.Generator) -> dict:
+    rows = []
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    for rep in range(args.n_replicates):
+        base = generator(args.n_gaps, np.random.default_rng(rng.integers(0, 2**63 - 1)))
+        for beta in betas:
+            gaps = beta_layer(base, beta, np.random.default_rng(rng.integers(0, 2**63 - 1)))
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                args.n_baseline,
+                np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "perimeter": name,
+                    "replicate": rep,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [obs_name for obs_name in OBS_NAMES if abs(z[obs_name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    one_sided_classification = classify_layers(rows, gate["coherent_one_sided_observables"])
+    return {
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_one_sided_gated": one_sided_classification,
+        "rows": rows,
+    }
+
+
+def compact_matrix(perimeters: dict) -> dict:
+    matrix = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        class_all = data["classification_all_observables"]
+        class_gate = data["classification_one_sided_gated"]
+        matrix[name] = {
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "endpoint_distance_all": class_all["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": class_gate["endpoint_distance"],
+            "ambiguous_beta_all": class_all["ambiguous_beta"],
+            "ambiguous_beta_one_sided_gated": class_gate["ambiguous_beta"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+        }
+    return matrix
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    perimeters = {}
+    for name, generator in PERIMETERS.items():
+        perimeters[name] = analyze_perimeter(name, generator, args, root_rng)
+
+    output = {
+        "experiment": "denominator_gate_transfer_matrix",
+        "category": "gate_transferability",
+        "question": "Which parts of the denominator gate transfer outside BOUNDARY?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact_matrix(perimeters),
+        "perimeters": perimeters,
+    }
+
+    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("perimeter one_sided stable0 stable1 dist_all dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>21s} "
+            f"{','.join(row['coherent_one_sided_observables']) or '[]':>22s} "
+            f"{row['stable_count_coherent']:>7.3f} "
+            f"{row['stable_count_illusory']:>7.3f} "
+            f"{row['endpoint_distance_all']:>8.3f} "
+            f"{row['endpoint_distance_one_sided_gated']:>9.3f} "
+            f"{row['ambiguous_beta_one_sided_gated']}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=4096)
+    parser.add_argument("--n-replicates", type=int, default=20)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=32)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=202605070901)
+    parser.add_argument("--out", default="tools/data/denominator_gate_transfer_matrix.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

codex
Prima di chiudere scrivo il report nel percorso richiesto e aggiorno il seme con la nuova parola emersa: non solo `gate_transferability`, ma matrice cross-perimetro con supporto one-sided e layer di protocollo separato.
apply patch
patch: completed
/opt/MM_D-ND/tools/data/reports/agent_20260507_0901.md
diff --git a/tools/data/reports/agent_20260507_0901.md b/tools/data/reports/agent_20260507_0901.md
new file mode 100644
index 0000000000000000000000000000000000000000..101695bfa907f5e680e8d408da1a95b66b7203f1
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0901.md
@@ -0,0 +1,122 @@
+# Agent Report — Denominator Gate Transfer Matrix
+
+timestamp: 2026-05-07 09:01 UTC
+category: gate_transferability_matrix
+verdict: operator
+observables_registry: 1.0.0-2026-05-06
+observables_used: [SR, SR2, L1, L2, triple_var]
+tool: tools/exp_denominator_gate_transfer_matrix.py
+data: tools/data/denominator_gate_transfer_matrix.json
+
+## Claim Under Test
+
+Verificato dal campo vivo: il cycle 0803 ha mostrato che il denominator gate
+trasferisce da BOUNDARY a DUALITA come operatore, ma non trasferisce la
+coordinata locale GUE/Poisson.
+
+Domanda di questo cycle: applicato a 4 perimetri non-BOUNDARY con poli
+coerente/illusorio a distribuzione marginale preservata, quali parti del gate
+trasferiscono e quali restano locali?
+
+Perimetri:
+
+- `DUALITA_golden`: sequenza Beatty aurea coerente vs permutazione illusoria.
+- `R_periodic_triad`: pattern periodico a 6 fasi vs permutazione illusoria.
+- `T_markov_alternating`: catena alternante low/high vs permutazione illusoria.
+- `E_ar1_continuity`: continuita AR(1) positiva vs permutazione illusoria.
+
+Parametri verificati: `n_gaps=4096`, `n_replicates=20`, `n_beta=11`,
+`n_baseline=32`, `z_min=2.0`, `seed=202605070901`.
+
+## Deposito Numerico
+
+| perimeter | coherent one-sided observables | stable_count coherent | stable_count illusory | endpoint distance gated | ambiguous beta gated |
+|---|---:|---:|---:|---:|---:|
+| DUALITA_golden | SR, L1, triple_var | 3.00 | 0.25 | 3.418 | 0.30 |
+| R_periodic_triad | SR, SR2, L1, L2, triple_var | 5.00 | 0.25 | 4.400 | 0.30 |
+| T_markov_alternating | SR, SR2, L1, L2, triple_var | 5.00 | 0.05 | 4.412 | 0.30 |
+| E_ar1_continuity | SR, SR2, L1, L2, triple_var | 5.00 | 0.60 | 4.394 | 0.30 |
+
+Endpoint-stable observables: `[]` in all 4 perimeters.
+
+Verificato: no observable is stable at both coherent and illusory endpoints.
+The denominator gate is not a two-sided endpoint support. It is one-sided
+support for generated order.
+
+## Risultato
+
+1. The gate transfers as one-sided coherence support.
+
+   In all 4 non-BOUNDARY perimeters, the coherent endpoint carries denominator
+   support and the illusory endpoint loses it. `DUALITA_golden` transfers on
+   `SR`, `L1`, `triple_var`; the three other perimeters transfer on all five
+   canonical observables.
+
+2. The both-endpoint stable set collapses everywhere.
+
+   `endpoint_stable_observables: []` is not a failure of the test. It is the
+   structural content: the gate does not name "two classes with stable
+   denominators"; it names the side where order survives its own shuffle null.
+
+3. The beta 0.30 ambiguity layer transfers as protocol coordinate, not as
+   BOUNDARY coordinate.
+
+   The ambiguous gated layer is beta `0.30` in all 4 perimeters. This is not a
+   GUE/Poisson layer. It is the coordinate created by the replacement protocol:
+   enough positions are illusory to place the layer near the endpoint bisector,
+   while enough coherent order remains to keep large z support.
+
+4. DUALITA is narrower than the other perimeters.
+
+   `DUALITA_golden` has one-sided support on 3/5 observables. `SR2` and `L2`
+   stay near shuffle at the coherent endpoint (`z_mean` about `0.12`). The
+   DUALITA gate transfers, but only through lag-1 and triple aggregation in this
+   synthetic perimeter.
+
+## Consecutio
+
+The next word is not `boundary_layer`. The correct category is:
+
+`order_denominator_gate`
+
+Definition: an original-vs-shuffle denominator gate where support is expected
+on the coherent/generated side, endpoint-stable observables may be empty, and
+the layer coordinate belongs to the perturbation protocol unless anchored to a
+domain-specific semantic axis.
+
+This extends the 0803 result:
+
+- 0803: BOUNDARY denominator gate transfers to DUALITA, BOUNDARY coordinate does not.
+- 0901: across 4 non-BOUNDARY perimeters, the transferable object is one-sided
+  order support; beta ambiguity is a protocol fold, not domain ontology.
+
+## Self-Audit: 5 Lenti
+
+L1 hard constraint vs bias: no "always", "never", or "zero" claim is used
+without exact support. `endpoint_stable_observables: []` is exact for this run
+and perimeters.
+
+L2 quantity vs ratio: the report uses raw stable counts, endpoint distances, and
+beta layers. Percent language is avoided except where the script computes
+fractions.
+
+L3 no silent patching: the scope changed from BOUNDARY to non-BOUNDARY
+perimeters explicitly. The new category `order_denominator_gate` is declared
+instead of stretching `BOUNDARY_LAYER_GATE`.
+
+L4 edge cases: `E_ar1_continuity` has illusory stable_count `0.60`, so the
+illusory endpoint is weak-denominator, not denominator-absent. The claim is
+one-sided support dominance, not exact absence.
+
+L5 re-discovery vs discovery: the numerical mechanism is an original-vs-shuffle
+order test on synthetic sequences. No NEW mathematical theorem is claimed.
+
+## Fonti
+
+- Verificato: `tools/data/agent_field_live.md`
+- Verificato: `tools/LAB_AGENT_CONTEXT.md`
+- Verificato: `tools/observables_registry.py`
+- Verificato: `tools/exp_denominator_gate_transfer_matrix.py`
+- Verificato: `tools/data/denominator_gate_transfer_matrix.json`
+- Inferito: `order_denominator_gate` as category needed by the downstream
+  vocabulary, from the cross-perimeter matrix.
diff --git a/tools/exp_denominator_gate_transfer_matrix.py b/tools/exp_denominator_gate_transfer_matrix.py
new file mode 100644
index 0000000000000000000000000000000000000000..bb0a837fe1303dca05fa887b9ca28e30c4bf9031
--- /dev/null
+++ b/tools/exp_denominator_gate_transfer_matrix.py
@@ -0,0 +1,337 @@
+#!/usr/bin/env python3
+"""
+exp_denominator_gate_transfer_matrix.py
+
+Reusable META audit for the denominator gate transfer matrix.
+
+The experiment moves the original-vs-shuffle denominator gate away from the
+GUE/Poisson BOUNDARY perimeter. Each perimeter has a coherent endpoint and an
+illusory endpoint built as a permutation of the same gap multiset. That keeps
+the one-point distribution fixed and isolates ordering support.
+
+Measured for each perimeter:
+- canonical observables from observables_registry.py;
+- original-vs-shuffle z-score for each observable;
+- endpoint-stable observable set under |z| >= z_min;
+- endpoint classification using all observables and endpoint-gated observables;
+- beta layer ambiguity between coherent and illusory endpoints.
+"""
+
+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())
+PHI = (1.0 + 5.0**0.5) / 2.0
+
+
+def normalize(gaps: np.ndarray) -> np.ndarray:
+    gaps = np.asarray(gaps, dtype=float)
+    gaps = np.maximum(gaps, 1e-12)
+    mean = float(np.mean(gaps))
+    return gaps / mean if mean > 1e-15 else gaps
+
+
+def golden_beatty(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    phase = float(rng.random())
+    n = np.arange(n_gaps + 1, dtype=float) + phase
+    positions = np.floor(n * PHI)
+    return normalize(np.diff(positions))
+
+
+def periodic_triad(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    base = np.array([0.55, 1.0, 1.45, 1.0, 0.75, 1.25], dtype=float)
+    shift = int(rng.integers(0, len(base)))
+    tiled = np.tile(np.roll(base, shift), int(np.ceil(n_gaps / len(base))))[:n_gaps]
+    jitter = rng.normal(0.0, 0.015, size=n_gaps)
+    return normalize(tiled + jitter)
+
+
+def markov_alternating(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    vals = np.array([0.62, 1.38], dtype=float)
+    state = int(rng.integers(0, 2))
+    out = np.empty(n_gaps, dtype=float)
+    for i in range(n_gaps):
+        out[i] = vals[state] + rng.normal(0.0, 0.03)
+        if rng.random() < 0.88:
+            state = 1 - state
+    return normalize(out)
+
+
+def ar1_continuity(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    rho = 0.86
+    x = np.empty(n_gaps, dtype=float)
+    x[0] = rng.normal()
+    noise_scale = (1.0 - rho * rho) ** 0.5
+    for i in range(1, n_gaps):
+        x[i] = rho * x[i - 1] + noise_scale * rng.normal()
+    return normalize(np.exp(0.42 * x))
+
+
+PERIMETERS = {
+    "DUALITA_golden": golden_beatty,
+    "R_periodic_triad": periodic_triad,
+    "T_markov_alternating": markov_alternating,
+    "E_ar1_continuity": ar1_continuity,
+}
+
+
+def beta_layer(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        out = base.copy()
+    elif beta >= 1.0:
+        out = illusory.copy()
+    else:
+        mask = rng.random(len(base)) < beta
+        out = base.copy()
+        out[mask] = illusory[mask]
+    return normalize(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]]:
+    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])
+
+    shuffle_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
+        shuffle_std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, shuffle_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:
+    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)
+
+    coherent_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent_vectors, illusory_vectors])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent_vectors, axis=0)
+    illusory_centroid = np.mean(illusory_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((illusory_centroid - coherent_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_coherent = float(np.linalg.norm((x - coherent_centroid) / scale))
+            d_illusory = float(np.linalg.norm((x - illusory_centroid) / scale))
+            denom = d_coherent + d_illusory
+            coord = float((d_coherent - d_illusory) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_coherent - d_illusory) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("coherent" if d_coherent < d_illusory else "illusory")
+        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,
+            "illusory_label_fraction": float(np.mean(np.array(labels) == "illusory")),
+        }
+
+    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}
+        z_values = {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)
+                z_values[name].append(row["z"][name])
+        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()},
+            "z_mean": {name: float(np.mean(vals)) for name, vals in z_values.items()},
+        }
+
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    endpoint_one_sided = []
+    for name in OBS_NAMES:
+        coherent_freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in coherent_rows])
+        illusory_freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in illusory_rows])
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            endpoint_one_sided.append(name)
+        if np.mean([coherent_freq, illusory_freq]) >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "coherent_one_sided_observables": endpoint_one_sided,
+        "layers": layers,
+    }
+
+
+def analyze_perimeter(name: str, generator, args: argparse.Namespace, rng: np.random.Generator) -> dict:
+    rows = []
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    for rep in range(args.n_replicates):
+        base = generator(args.n_gaps, np.random.default_rng(rng.integers(0, 2**63 - 1)))
+        for beta in betas:
+            gaps = beta_layer(base, beta, np.random.default_rng(rng.integers(0, 2**63 - 1)))
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                args.n_baseline,
+                np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "perimeter": name,
+                    "replicate": rep,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [obs_name for obs_name in OBS_NAMES if abs(z[obs_name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    one_sided_classification = classify_layers(rows, gate["coherent_one_sided_observables"])
+    return {
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_one_sided_gated": one_sided_classification,
+        "rows": rows,
+    }
+
+
+def compact_matrix(perimeters: dict) -> dict:
+    matrix = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        class_all = data["classification_all_observables"]
+        class_gate = data["classification_one_sided_gated"]
+        matrix[name] = {
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "endpoint_distance_all": class_all["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": class_gate["endpoint_distance"],
+            "ambiguous_beta_all": class_all["ambiguous_beta"],
+            "ambiguous_beta_one_sided_gated": class_gate["ambiguous_beta"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+        }
+    return matrix
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    perimeters = {}
+    for name, generator in PERIMETERS.items():
+        perimeters[name] = analyze_perimeter(name, generator, args, root_rng)
+
+    output = {
+        "experiment": "denominator_gate_transfer_matrix",
+        "category": "gate_transferability",
+        "question": "Which parts of the denominator gate transfer outside BOUNDARY?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact_matrix(perimeters),
+        "perimeters": perimeters,
+    }
+
+    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("perimeter one_sided stable0 stable1 dist_all dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>21s} "
+            f"{','.join(row['coherent_one_sided_observables']) or '[]':>22s} "
+            f"{row['stable_count_coherent']:>7.3f} "
+            f"{row['stable_count_illusory']:>7.3f} "
+            f"{row['endpoint_distance_all']:>8.3f} "
+            f"{row['endpoint_distance_one_sided_gated']:>9.3f} "
+            f"{row['ambiguous_beta_one_sided_gated']}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=4096)
+    parser.add_argument("--n-replicates", type=int, default=20)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=32)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=202605070901)
+    parser.add_argument("--out", default="tools/data/denominator_gate_transfer_matrix.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

diff --git a/tools/data/reports/agent_20260507_0901.md b/tools/data/reports/agent_20260507_0901.md
new file mode 100644
index 0000000000000000000000000000000000000000..101695bfa907f5e680e8d408da1a95b66b7203f1
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0901.md
@@ -0,0 +1,122 @@
+# Agent Report — Denominator Gate Transfer Matrix
+
+timestamp: 2026-05-07 09:01 UTC
+category: gate_transferability_matrix
+verdict: operator
+observables_registry: 1.0.0-2026-05-06
+observables_used: [SR, SR2, L1, L2, triple_var]
+tool: tools/exp_denominator_gate_transfer_matrix.py
+data: tools/data/denominator_gate_transfer_matrix.json
+
+## Claim Under Test
+
+Verificato dal campo vivo: il cycle 0803 ha mostrato che il denominator gate
+trasferisce da BOUNDARY a DUALITA come operatore, ma non trasferisce la
+coordinata locale GUE/Poisson.
+
+Domanda di questo cycle: applicato a 4 perimetri non-BOUNDARY con poli
+coerente/illusorio a distribuzione marginale preservata, quali parti del gate
+trasferiscono e quali restano locali?
+
+Perimetri:
+
+- `DUALITA_golden`: sequenza Beatty aurea coerente vs permutazione illusoria.
+- `R_periodic_triad`: pattern periodico a 6 fasi vs permutazione illusoria.
+- `T_markov_alternating`: catena alternante low/high vs permutazione illusoria.
+- `E_ar1_continuity`: continuita AR(1) positiva vs permutazione illusoria.
+
+Parametri verificati: `n_gaps=4096`, `n_replicates=20`, `n_beta=11`,
+`n_baseline=32`, `z_min=2.0`, `seed=202605070901`.
+
+## Deposito Numerico
+
+| perimeter | coherent one-sided observables | stable_count coherent | stable_count illusory | endpoint distance gated | ambiguous beta gated |
+|---|---:|---:|---:|---:|---:|
+| DUALITA_golden | SR, L1, triple_var | 3.00 | 0.25 | 3.418 | 0.30 |
+| R_periodic_triad | SR, SR2, L1, L2, triple_var | 5.00 | 0.25 | 4.400 | 0.30 |
+| T_markov_alternating | SR, SR2, L1, L2, triple_var | 5.00 | 0.05 | 4.412 | 0.30 |
+| E_ar1_continuity | SR, SR2, L1, L2, triple_var | 5.00 | 0.60 | 4.394 | 0.30 |
+
+Endpoint-stable observables: `[]` in all 4 perimeters.
+
+Verificato: no observable is stable at both coherent and illusory endpoints.
+The denominator gate is not a two-sided endpoint support. It is one-sided
+support for generated order.
+
+## Risultato
+
+1. The gate transfers as one-sided coherence support.
+
+   In all 4 non-BOUNDARY perimeters, the coherent endpoint carries denominator
+   support and the illusory endpoint loses it. `DUALITA_golden` transfers on
+   `SR`, `L1`, `triple_var`; the three other perimeters transfer on all five
+   canonical observables.
+
+2. The both-endpoint stable set collapses everywhere.
+
+   `endpoint_stable_observables: []` is not a failure of the test. It is the
+   structural content: the gate does not name "two classes with stable
+   denominators"; it names the side where order survives its own shuffle null.
+
+3. The beta 0.30 ambiguity layer transfers as protocol coordinate, not as
+   BOUNDARY coordinate.
+
+   The ambiguous gated layer is beta `0.30` in all 4 perimeters. This is not a
+   GUE/Poisson layer. It is the coordinate created by the replacement protocol:
+   enough positions are illusory to place the layer near the endpoint bisector,
+   while enough coherent order remains to keep large z support.
+
+4. DUALITA is narrower than the other perimeters.
+
+   `DUALITA_golden` has one-sided support on 3/5 observables. `SR2` and `L2`
+   stay near shuffle at the coherent endpoint (`z_mean` about `0.12`). The
+   DUALITA gate transfers, but only through lag-1 and triple aggregation in this
+   synthetic perimeter.
+
+## Consecutio
+
+The next word is not `boundary_layer`. The correct category is:
+
+`order_denominator_gate`
+
+Definition: an original-vs-shuffle denominator gate where support is expected
+on the coherent/generated side, endpoint-stable observables may be empty, and
+the layer coordinate belongs to the perturbation protocol unless anchored to a
+domain-specific semantic axis.
+
+This extends the 0803 result:
+
+- 0803: BOUNDARY denominator gate transfers to DUALITA, BOUNDARY coordinate does not.
+- 0901: across 4 non-BOUNDARY perimeters, the transferable object is one-sided
+  order support; beta ambiguity is a protocol fold, not domain ontology.
+
+## Self-Audit: 5 Lenti
+
+L1 hard constraint vs bias: no "always", "never", or "zero" claim is used
+without exact support. `endpoint_stable_observables: []` is exact for this run
+and perimeters.
+
+L2 quantity vs ratio: the report uses raw stable counts, endpoint distances, and
+beta layers. Percent language is avoided except where the script computes
+fractions.
+
+L3 no silent patching: the scope changed from BOUNDARY to non-BOUNDARY
+perimeters explicitly. The new category `order_denominator_gate` is declared
+instead of stretching `BOUNDARY_LAYER_GATE`.
+
+L4 edge cases: `E_ar1_continuity` has illusory stable_count `0.60`, so the
+illusory endpoint is weak-denominator, not denominator-absent. The claim is
+one-sided support dominance, not exact absence.
+
+L5 re-discovery vs discovery: the numerical mechanism is an original-vs-shuffle
+order test on synthetic sequences. No NEW mathematical theorem is claimed.
+
+## Fonti
+
+- Verificato: `tools/data/agent_field_live.md`
+- Verificato: `tools/LAB_AGENT_CONTEXT.md`
+- Verificato: `tools/observables_registry.py`
+- Verificato: `tools/exp_denominator_gate_transfer_matrix.py`
+- Verificato: `tools/data/denominator_gate_transfer_matrix.json`
+- Inferito: `order_denominator_gate` as category needed by the downstream
+  vocabulary, from the cross-perimeter matrix.
diff --git a/tools/exp_denominator_gate_transfer_matrix.py b/tools/exp_denominator_gate_transfer_matrix.py
new file mode 100644
index 0000000000000000000000000000000000000000..bb0a837fe1303dca05fa887b9ca28e30c4bf9031
--- /dev/null
+++ b/tools/exp_denominator_gate_transfer_matrix.py
@@ -0,0 +1,337 @@
+#!/usr/bin/env python3
+"""
+exp_denominator_gate_transfer_matrix.py
+
+Reusable META audit for the denominator gate transfer matrix.
+
+The experiment moves the original-vs-shuffle denominator gate away from the
+GUE/Poisson BOUNDARY perimeter. Each perimeter has a coherent endpoint and an
+illusory endpoint built as a permutation of the same gap multiset. That keeps
+the one-point distribution fixed and isolates ordering support.
+
+Measured for each perimeter:
+- canonical observables from observables_registry.py;
+- original-vs-shuffle z-score for each observable;
+- endpoint-stable observable set under |z| >= z_min;
+- endpoint classification using all observables and endpoint-gated observables;
+- beta layer ambiguity between coherent and illusory endpoints.
+"""
+
+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())
+PHI = (1.0 + 5.0**0.5) / 2.0
+
+
+def normalize(gaps: np.ndarray) -> np.ndarray:
+    gaps = np.asarray(gaps, dtype=float)
+    gaps = np.maximum(gaps, 1e-12)
+    mean = float(np.mean(gaps))
+    return gaps / mean if mean > 1e-15 else gaps
+
+
+def golden_beatty(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    phase = float(rng.random())
+    n = np.arange(n_gaps + 1, dtype=float) + phase
+    positions = np.floor(n * PHI)
+    return normalize(np.diff(positions))
+
+
+def periodic_triad(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    base = np.array([0.55, 1.0, 1.45, 1.0, 0.75, 1.25], dtype=float)
+    shift = int(rng.integers(0, len(base)))
+    tiled = np.tile(np.roll(base, shift), int(np.ceil(n_gaps / len(base))))[:n_gaps]
+    jitter = rng.normal(0.0, 0.015, size=n_gaps)
+    return normalize(tiled + jitter)
+
+
+def markov_alternating(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    vals = np.array([0.62, 1.38], dtype=float)
+    state = int(rng.integers(0, 2))
+    out = np.empty(n_gaps, dtype=float)
+    for i in range(n_gaps):
+        out[i] = vals[state] + rng.normal(0.0, 0.03)
+        if rng.random() < 0.88:
+            state = 1 - state
+    return normalize(out)
+
+
+def ar1_continuity(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    rho = 0.86
+    x = np.empty(n_gaps, dtype=float)
+    x[0] = rng.normal()
+    noise_scale = (1.0 - rho * rho) ** 0.5
+    for i in range(1, n_gaps):
+        x[i] = rho * x[i - 1] + noise_scale * rng.normal()
+    return normalize(np.exp(0.42 * x))
+
+
+PERIMETERS = {
+    "DUALITA_golden": golden_beatty,
+    "R_periodic_triad": periodic_triad,
+    "T_markov_alternating": markov_alternating,
+    "E_ar1_continuity": ar1_continuity,
+}
+
+
+def beta_layer(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        out = base.copy()
+    elif beta >= 1.0:
+        out = illusory.copy()
+    else:
+        mask = rng.random(len(base)) < beta
+        out = base.copy()
+        out[mask] = illusory[mask]
+    return normalize(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]]:
+    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])
+
+    shuffle_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
+        shuffle_std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, shuffle_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:
+    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)
+
+    coherent_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent_vectors, illusory_vectors])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent_vectors, axis=0)
+    illusory_centroid = np.mean(illusory_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((illusory_centroid - coherent_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_coherent = float(np.linalg.norm((x - coherent_centroid) / scale))
+            d_illusory = float(np.linalg.norm((x - illusory_centroid) / scale))
+            denom = d_coherent + d_illusory
+            coord = float((d_coherent - d_illusory) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_coherent - d_illusory) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("coherent" if d_coherent < d_illusory else "illusory")
+        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,
+            "illusory_label_fraction": float(np.mean(np.array(labels) == "illusory")),
+        }
+
+    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}
+        z_values = {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)
+                z_values[name].append(row["z"][name])
+        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()},
+            "z_mean": {name: float(np.mean(vals)) for name, vals in z_values.items()},
+        }
+
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    endpoint_one_sided = []
+    for name in OBS_NAMES:
+        coherent_freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in coherent_rows])
+        illusory_freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in illusory_rows])
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            endpoint_one_sided.append(name)
+        if np.mean([coherent_freq, illusory_freq]) >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "coherent_one_sided_observables": endpoint_one_sided,
+        "layers": layers,
+    }
+
+
+def analyze_perimeter(name: str, generator, args: argparse.Namespace, rng: np.random.Generator) -> dict:
+    rows = []
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    for rep in range(args.n_replicates):
+        base = generator(args.n_gaps, np.random.default_rng(rng.integers(0, 2**63 - 1)))
+        for beta in betas:
+            gaps = beta_layer(base, beta, np.random.default_rng(rng.integers(0, 2**63 - 1)))
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                args.n_baseline,
+                np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "perimeter": name,
+                    "replicate": rep,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [obs_name for obs_name in OBS_NAMES if abs(z[obs_name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    one_sided_classification = classify_layers(rows, gate["coherent_one_sided_observables"])
+    return {
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_one_sided_gated": one_sided_classification,
+        "rows": rows,
+    }
+
+
+def compact_matrix(perimeters: dict) -> dict:
+    matrix = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        class_all = data["classification_all_observables"]
+        class_gate = data["classification_one_sided_gated"]
+        matrix[name] = {
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "endpoint_distance_all": class_all["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": class_gate["endpoint_distance"],
+            "ambiguous_beta_all": class_all["ambiguous_beta"],
+            "ambiguous_beta_one_sided_gated": class_gate["ambiguous_beta"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+        }
+    return matrix
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    perimeters = {}
+    for name, generator in PERIMETERS.items():
+        perimeters[name] = analyze_perimeter(name, generator, args, root_rng)
+
+    output = {
+        "experiment": "denominator_gate_transfer_matrix",
+        "category": "gate_transferability",
+        "question": "Which parts of the denominator gate transfer outside BOUNDARY?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact_matrix(perimeters),
+        "perimeters": perimeters,
+    }
+
+    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("perimeter one_sided stable0 stable1 dist_all dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>21s} "
+            f"{','.join(row['coherent_one_sided_observables']) or '[]':>22s} "
+            f"{row['stable_count_coherent']:>7.3f} "
+            f"{row['stable_count_illusory']:>7.3f} "
+            f"{row['endpoint_distance_all']:>8.3f} "
+            f"{row['endpoint_distance_one_sided_gated']:>9.3f} "
+            f"{row['ambiguous_beta_one_sided_gated']}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=4096)
+    parser.add_argument("--n-replicates", type=int, default=20)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=32)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=202605070901)
+    parser.add_argument("--out", default="tools/data/denominator_gate_transfer_matrix.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_0901.md b/tools/data/reports/agent_20260507_0901.md
new file mode 100644
index 0000000000000000000000000000000000000000..101695bfa907f5e680e8d408da1a95b66b7203f1
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0901.md
@@ -0,0 +1,122 @@
+# Agent Report — Denominator Gate Transfer Matrix
+
+timestamp: 2026-05-07 09:01 UTC
+category: gate_transferability_matrix
+verdict: operator
+observables_registry: 1.0.0-2026-05-06
+observables_used: [SR, SR2, L1, L2, triple_var]
+tool: tools/exp_denominator_gate_transfer_matrix.py
+data: tools/data/denominator_gate_transfer_matrix.json
+
+## Claim Under Test
+
+Verificato dal campo vivo: il cycle 0803 ha mostrato che il denominator gate
+trasferisce da BOUNDARY a DUALITA come operatore, ma non trasferisce la
+coordinata locale GUE/Poisson.
+
+Domanda di questo cycle: applicato a 4 perimetri non-BOUNDARY con poli
+coerente/illusorio a distribuzione marginale preservata, quali parti del gate
+trasferiscono e quali restano locali?
+
+Perimetri:
+
+- `DUALITA_golden`: sequenza Beatty aurea coerente vs permutazione illusoria.
+- `R_periodic_triad`: pattern periodico a 6 fasi vs permutazione illusoria.
+- `T_markov_alternating`: catena alternante low/high vs permutazione illusoria.
+- `E_ar1_continuity`: continuita AR(1) positiva vs permutazione illusoria.
+
+Parametri verificati: `n_gaps=4096`, `n_replicates=20`, `n_beta=11`,
+`n_baseline=32`, `z_min=2.0`, `seed=202605070901`.
+
+## Deposito Numerico
+
+| perimeter | coherent one-sided observables | stable_count coherent | stable_count illusory | endpoint distance gated | ambiguous beta gated |
+|---|---:|---:|---:|---:|---:|
+| DUALITA_golden | SR, L1, triple_var | 3.00 | 0.25 | 3.418 | 0.30 |
+| R_periodic_triad | SR, SR2, L1, L2, triple_var | 5.00 | 0.25 | 4.400 | 0.30 |
+| T_markov_alternating | SR, SR2, L1, L2, triple_var | 5.00 | 0.05 | 4.412 | 0.30 |
+| E_ar1_continuity | SR, SR2, L1, L2, triple_var | 5.00 | 0.60 | 4.394 | 0.30 |
+
+Endpoint-stable observables: `[]` in all 4 perimeters.
+
+Verificato: no observable is stable at both coherent and illusory endpoints.
+The denominator gate is not a two-sided endpoint support. It is one-sided
+support for generated order.
+
+## Risultato
+
+1. The gate transfers as one-sided coherence support.
+
+   In all 4 non-BOUNDARY perimeters, the coherent endpoint carries denominator
+   support and the illusory endpoint loses it. `DUALITA_golden` transfers on
+   `SR`, `L1`, `triple_var`; the three other perimeters transfer on all five
+   canonical observables.
+
+2. The both-endpoint stable set collapses everywhere.
+
+   `endpoint_stable_observables: []` is not a failure of the test. It is the
+   structural content: the gate does not name "two classes with stable
+   denominators"; it names the side where order survives its own shuffle null.
+
+3. The beta 0.30 ambiguity layer transfers as protocol coordinate, not as
+   BOUNDARY coordinate.
+
+   The ambiguous gated layer is beta `0.30` in all 4 perimeters. This is not a
+   GUE/Poisson layer. It is the coordinate created by the replacement protocol:
+   enough positions are illusory to place the layer near the endpoint bisector,
+   while enough coherent order remains to keep large z support.
+
+4. DUALITA is narrower than the other perimeters.
+
+   `DUALITA_golden` has one-sided support on 3/5 observables. `SR2` and `L2`
+   stay near shuffle at the coherent endpoint (`z_mean` about `0.12`). The
+   DUALITA gate transfers, but only through lag-1 and triple aggregation in this
+   synthetic perimeter.
+
+## Consecutio
+
+The next word is not `boundary_layer`. The correct category is:
+
+`order_denominator_gate`
+
+Definition: an original-vs-shuffle denominator gate where support is expected
+on the coherent/generated side, endpoint-stable observables may be empty, and
+the layer coordinate belongs to the perturbation protocol unless anchored to a
+domain-specific semantic axis.
+
+This extends the 0803 result:
+
+- 0803: BOUNDARY denominator gate transfers to DUALITA, BOUNDARY coordinate does not.
+- 0901: across 4 non-BOUNDARY perimeters, the transferable object is one-sided
+  order support; beta ambiguity is a protocol fold, not domain ontology.
+
+## Self-Audit: 5 Lenti
+
+L1 hard constraint vs bias: no "always", "never", or "zero" claim is used
+without exact support. `endpoint_stable_observables: []` is exact for this run
+and perimeters.
+
+L2 quantity vs ratio: the report uses raw stable counts, endpoint distances, and
+beta layers. Percent language is avoided except where the script computes
+fractions.
+
+L3 no silent patching: the scope changed from BOUNDARY to non-BOUNDARY
+perimeters explicitly. The new category `order_denominator_gate` is declared
+instead of stretching `BOUNDARY_LAYER_GATE`.
+
+L4 edge cases: `E_ar1_continuity` has illusory stable_count `0.60`, so the
+illusory endpoint is weak-denominator, not denominator-absent. The claim is
+one-sided support dominance, not exact absence.
+
+L5 re-discovery vs discovery: the numerical mechanism is an original-vs-shuffle
+order test on synthetic sequences. No NEW mathematical theorem is claimed.
+
+## Fonti
+
+- Verificato: `tools/data/agent_field_live.md`
+- Verificato: `tools/LAB_AGENT_CONTEXT.md`
+- Verificato: `tools/observables_registry.py`
+- Verificato: `tools/exp_denominator_gate_transfer_matrix.py`
+- Verificato: `tools/data/denominator_gate_transfer_matrix.json`
+- Inferito: `order_denominator_gate` as category needed by the downstream
+  vocabulary, from the cross-perimeter matrix.
diff --git a/tools/data/seme.json b/tools/data/seme.json
index 9dc9f5c8d087e9aea3266d5fefbc52a939acea27..47927a057e5396452b3c7982c8218c6e7c4d3b12
--- a/tools/data/seme.json
+++ b/tools/data/seme.json
@@ -132,6 +132,17 @@
       "_source_log": "2026-05-07T08:10:22.658201+00:00",
       "_source_decision": "REDESIGN",
       "_source_reasoning": "Il ciclo ha prodotto evidenza controllata e replicata che il denominator gate trasferisce come operatore, ma non trasferisce la coordinata di layer BOUNDARY: ambiguita' classificativa e collasso del denominatore si separano. Continuare sul seme attuale centrato su GUE/Poisson rischia di restare nel "
+    },
+    {
+      "tipo": "vincolo",
+      "id": "ORDER_DENOMINATOR_GATE",
+      "claim": "Il denominator gate trasferisce cross-perimetro come supporto one-sided dell'ordine generato, non come endpoint-stable support a due poli. Nel perimetro agent_20260507_0901, 4/4 domini non-BOUNDARY hanno endpoint_stable_observables=[]; il polo coerente ha stable_count 3.0-5.0, il polo illusorio 0.05-0.60. La beta ambigua 0.30 e coordinata del protocollo di perturbazione finche non viene ancorata a un asse semantico del dominio.",
+      "intensita": 0.92,
+      "porta": "META",
+      "manuale": true,
+      "condensato_ref": "A4,A8,A14,C2",
+      "origine": "cycle agent_20260507_0901: denominator gate transfer matrix su DUALITA_golden, R_periodic_triad, T_markov_alternating, E_ar1_continuity",
+      "added_at": "2026-05-07T09:01:00+00:00"
     }
   ],
   "potenziale_bloccato": [],
@@ -139,11 +150,11 @@
     "Tensioni risolte: {'PIANO_PRIMARIO_DUE_ASSIOMI', 'METRIC_TENSOR', 'BOUNDARY_LAYER_GATE', 'OBSERVABLE_REGISTRY', 'DUALITA_DIPOLARE_VS_ILLUSORIA', 'TENSIONE_ENTITA', 'PERTURBATION_DENOMINATOR_GATE', 'G_POTENZIALE_NULLA', 'TRASCENDENZA_LIMITE'}"
   ],
   "filtro": {
-    "promosse": 11,
+    "promosse": 12,
     "filtrate": 0,
     "condensato_claims": 30
   },
-  "direzione": "Costruire una matrice di trasferibilita' del denominator gate: applicarlo a 3-4 perimetri non-BOUNDARY con poli coerente/illusorio e verificare quali parti trasferiscono (supporto one-sided, coordinate di collasso, ambiguita' classificativa) e quali restano locali.",
+  "direzione": "Verificare se ORDER_DENOMINATOR_GATE resta one-sided su perimetri naturali/non sintetici: primes windows, logistic map, Fibonacci spectrum o dominio fisico gia nel lab. Separare supporto d'ordine, coordinata di protocollo e asse semantico del dominio.",
   "verifica": {
     "pass": 11,
     "fail": 0,
@@ -311,6 +322,7 @@
     }
   ],
   "_seed_history": {
-    "trajectory_apply_20260507_090145": "applied modify_seme from 20260507_0803: direzione → 'Costruire una matrice di trasferibilita' del denominator gate: applicarlo a 3-4 perimetri non-BOUNDA...'"
+    "trajectory_apply_20260507_090145": "applied modify_seme from 20260507_0803: direzione → 'Costruire una matrice di trasferibilita' del denominator gate: applicarlo a 3-4 perimetri non-BOUNDA...'",
+    "agent_20260507_0901": "added ORDER_DENOMINATOR_GATE; direzione -> test one-sided gate on natural/non-synthetic perimeters"
   }
-}
\ No newline at end of file
+}
diff --git a/tools/exp_denominator_gate_transfer_matrix.py b/tools/exp_denominator_gate_transfer_matrix.py
new file mode 100644
index 0000000000000000000000000000000000000000..bb0a837fe1303dca05fa887b9ca28e30c4bf9031
--- /dev/null
+++ b/tools/exp_denominator_gate_transfer_matrix.py
@@ -0,0 +1,337 @@
+#!/usr/bin/env python3
+"""
+exp_denominator_gate_transfer_matrix.py
+
+Reusable META audit for the denominator gate transfer matrix.
+
+The experiment moves the original-vs-shuffle denominator gate away from the
+GUE/Poisson BOUNDARY perimeter. Each perimeter has a coherent endpoint and an
+illusory endpoint built as a permutation of the same gap multiset. That keeps
+the one-point distribution fixed and isolates ordering support.
+
+Measured for each perimeter:
+- canonical observables from observables_registry.py;
+- original-vs-shuffle z-score for each observable;
+- endpoint-stable observable set under |z| >= z_min;
+- endpoint classification using all observables and endpoint-gated observables;
+- beta layer ambiguity between coherent and illusory endpoints.
+"""
+
+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())
+PHI = (1.0 + 5.0**0.5) / 2.0
+
+
+def normalize(gaps: np.ndarray) -> np.ndarray:
+    gaps = np.asarray(gaps, dtype=float)
+    gaps = np.maximum(gaps, 1e-12)
+    mean = float(np.mean(gaps))
+    return gaps / mean if mean > 1e-15 else gaps
+
+
+def golden_beatty(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    phase = float(rng.random())
+    n = np.arange(n_gaps + 1, dtype=float) + phase
+    positions = np.floor(n * PHI)
+    return normalize(np.diff(positions))
+
+
+def periodic_triad(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    base = np.array([0.55, 1.0, 1.45, 1.0, 0.75, 1.25], dtype=float)
+    shift = int(rng.integers(0, len(base)))
+    tiled = np.tile(np.roll(base, shift), int(np.ceil(n_gaps / len(base))))[:n_gaps]
+    jitter = rng.normal(0.0, 0.015, size=n_gaps)
+    return normalize(tiled + jitter)
+
+
+def markov_alternating(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    vals = np.array([0.62, 1.38], dtype=float)
+    state = int(rng.integers(0, 2))
+    out = np.empty(n_gaps, dtype=float)
+    for i in range(n_gaps):
+        out[i] = vals[state] + rng.normal(0.0, 0.03)
+        if rng.random() < 0.88:
+            state = 1 - state
+    return normalize(out)
+
+
+def ar1_continuity(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    rho = 0.86
+    x = np.empty(n_gaps, dtype=float)
+    x[0] = rng.normal()
+    noise_scale = (1.0 - rho * rho) ** 0.5
+    for i in range(1, n_gaps):
+        x[i] = rho * x[i - 1] + noise_scale * rng.normal()
+    return normalize(np.exp(0.42 * x))
+
+
+PERIMETERS = {
+    "DUALITA_golden": golden_beatty,
+    "R_periodic_triad": periodic_triad,
+    "T_markov_alternating": markov_alternating,
+    "E_ar1_continuity": ar1_continuity,
+}
+
+
+def beta_layer(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        out = base.copy()
+    elif beta >= 1.0:
+        out = illusory.copy()
+    else:
+        mask = rng.random(len(base)) < beta
+        out = base.copy()
+        out[mask] = illusory[mask]
+    return normalize(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]]:
+    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])
+
+    shuffle_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
+        shuffle_std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, shuffle_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:
+    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)
+
+    coherent_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent_vectors, illusory_vectors])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent_vectors, axis=0)
+    illusory_centroid = np.mean(illusory_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((illusory_centroid - coherent_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_coherent = float(np.linalg.norm((x - coherent_centroid) / scale))
+            d_illusory = float(np.linalg.norm((x - illusory_centroid) / scale))
+            denom = d_coherent + d_illusory
+            coord = float((d_coherent - d_illusory) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_coherent - d_illusory) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("coherent" if d_coherent < d_illusory else "illusory")
+        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,
+            "illusory_label_fraction": float(np.mean(np.array(labels) == "illusory")),
+        }
+
+    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}
+        z_values = {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)
+                z_values[name].append(row["z"][name])
+        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()},
+            "z_mean": {name: float(np.mean(vals)) for name, vals in z_values.items()},
+        }
+
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    endpoint_one_sided = []
+    for name in OBS_NAMES:
+        coherent_freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in coherent_rows])
+        illusory_freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in illusory_rows])
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            endpoint_one_sided.append(name)
+        if np.mean([coherent_freq, illusory_freq]) >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "coherent_one_sided_observables": endpoint_one_sided,
+        "layers": layers,
+    }
+
+
+def analyze_perimeter(name: str, generator, args: argparse.Namespace, rng: np.random.Generator) -> dict:
+    rows = []
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    for rep in range(args.n_replicates):
+        base = generator(args.n_gaps, np.random.default_rng(rng.integers(0, 2**63 - 1)))
+        for beta in betas:
+            gaps = beta_layer(base, beta, np.random.default_rng(rng.integers(0, 2**63 - 1)))
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                args.n_baseline,
+                np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "perimeter": name,
+                    "replicate": rep,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [obs_name for obs_name in OBS_NAMES if abs(z[obs_name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    one_sided_classification = classify_layers(rows, gate["coherent_one_sided_observables"])
+    return {
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_one_sided_gated": one_sided_classification,
+        "rows": rows,
+    }
+
+
+def compact_matrix(perimeters: dict) -> dict:
+    matrix = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        class_all = data["classification_all_observables"]
+        class_gate = data["classification_one_sided_gated"]
+        matrix[name] = {
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "endpoint_distance_all": class_all["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": class_gate["endpoint_distance"],
+            "ambiguous_beta_all": class_all["ambiguous_beta"],
+            "ambiguous_beta_one_sided_gated": class_gate["ambiguous_beta"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+        }
+    return matrix
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    perimeters = {}
+    for name, generator in PERIMETERS.items():
+        perimeters[name] = analyze_perimeter(name, generator, args, root_rng)
+
+    output = {
+        "experiment": "denominator_gate_transfer_matrix",
+        "category": "gate_transferability",
+        "question": "Which parts of the denominator gate transfer outside BOUNDARY?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact_matrix(perimeters),
+        "perimeters": perimeters,
+    }
+
+    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("perimeter one_sided stable0 stable1 dist_all dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>21s} "
+            f"{','.join(row['coherent_one_sided_observables']) or '[]':>22s} "
+            f"{row['stable_count_coherent']:>7.3f} "
+            f"{row['stable_count_illusory']:>7.3f} "
+            f"{row['endpoint_distance_all']:>8.3f} "
+            f"{row['endpoint_distance_one_sided_gated']:>9.3f} "
+            f"{row['ambiguous_beta_one_sided_gated']}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=4096)
+    parser.add_argument("--n-replicates", type=int, default=20)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=32)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=202605070901)
+    parser.add_argument("--out", default="tools/data/denominator_gate_transfer_matrix.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

diff --git a/tools/data/reports/agent_20260507_0901.md b/tools/data/reports/agent_20260507_0901.md
new file mode 100644
index 0000000000000000000000000000000000000000..101695bfa907f5e680e8d408da1a95b66b7203f1
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0901.md
@@ -0,0 +1,122 @@
+# Agent Report — Denominator Gate Transfer Matrix
+
+timestamp: 2026-05-07 09:01 UTC
+category: gate_transferability_matrix
+verdict: operator
+observables_registry: 1.0.0-2026-05-06
+observables_used: [SR, SR2, L1, L2, triple_var]
+tool: tools/exp_denominator_gate_transfer_matrix.py
+data: tools/data/denominator_gate_transfer_matrix.json
+
+## Claim Under Test
+
+Verificato dal campo vivo: il cycle 0803 ha mostrato che il denominator gate
+trasferisce da BOUNDARY a DUALITA come operatore, ma non trasferisce la
+coordinata locale GUE/Poisson.
+
+Domanda di questo cycle: applicato a 4 perimetri non-BOUNDARY con poli
+coerente/illusorio a distribuzione marginale preservata, quali parti del gate
+trasferiscono e quali restano locali?
+
+Perimetri:
+
+- `DUALITA_golden`: sequenza Beatty aurea coerente vs permutazione illusoria.
+- `R_periodic_triad`: pattern periodico a 6 fasi vs permutazione illusoria.
+- `T_markov_alternating`: catena alternante low/high vs permutazione illusoria.
+- `E_ar1_continuity`: continuita AR(1) positiva vs permutazione illusoria.
+
+Parametri verificati: `n_gaps=4096`, `n_replicates=20`, `n_beta=11`,
+`n_baseline=32`, `z_min=2.0`, `seed=202605070901`.
+
+## Deposito Numerico
+
+| perimeter | coherent one-sided observables | stable_count coherent | stable_count illusory | endpoint distance gated | ambiguous beta gated |
+|---|---:|---:|---:|---:|---:|
+| DUALITA_golden | SR, L1, triple_var | 3.00 | 0.25 | 3.418 | 0.30 |
+| R_periodic_triad | SR, SR2, L1, L2, triple_var | 5.00 | 0.25 | 4.400 | 0.30 |
+| T_markov_alternating | SR, SR2, L1, L2, triple_var | 5.00 | 0.05 | 4.412 | 0.30 |
+| E_ar1_continuity | SR, SR2, L1, L2, triple_var | 5.00 | 0.60 | 4.394 | 0.30 |
+
+Endpoint-stable observables: `[]` in all 4 perimeters.
+
+Verificato: no observable is stable at both coherent and illusory endpoints.
+The denominator gate is not a two-sided endpoint support. It is one-sided
+support for generated order.
+
+## Risultato
+
+1. The gate transfers as one-sided coherence support.
+
+   In all 4 non-BOUNDARY perimeters, the coherent endpoint carries denominator
+   support and the illusory endpoint loses it. `DUALITA_golden` transfers on
+   `SR`, `L1`, `triple_var`; the three other perimeters transfer on all five
+   canonical observables.
+
+2. The both-endpoint stable set collapses everywhere.
+
+   `endpoint_stable_observables: []` is not a failure of the test. It is the
+   structural content: the gate does not name "two classes with stable
+   denominators"; it names the side where order survives its own shuffle null.
+
+3. The beta 0.30 ambiguity layer transfers as protocol coordinate, not as
+   BOUNDARY coordinate.
+
+   The ambiguous gated layer is beta `0.30` in all 4 perimeters. This is not a
+   GUE/Poisson layer. It is the coordinate created by the replacement protocol:
+   enough positions are illusory to place the layer near the endpoint bisector,
+   while enough coherent order remains to keep large z support.
+
+4. DUALITA is narrower than the other perimeters.
+
+   `DUALITA_golden` has one-sided support on 3/5 observables. `SR2` and `L2`
+   stay near shuffle at the coherent endpoint (`z_mean` about `0.12`). The
+   DUALITA gate transfers, but only through lag-1 and triple aggregation in this
+   synthetic perimeter.
+
+## Consecutio
+
+The next word is not `boundary_layer`. The correct category is:
+
+`order_denominator_gate`
+
+Definition: an original-vs-shuffle denominator gate where support is expected
+on the coherent/generated side, endpoint-stable observables may be empty, and
+the layer coordinate belongs to the perturbation protocol unless anchored to a
+domain-specific semantic axis.
+
+This extends the 0803 result:
+
+- 0803: BOUNDARY denominator gate transfers to DUALITA, BOUNDARY coordinate does not.
+- 0901: across 4 non-BOUNDARY perimeters, the transferable object is one-sided
+  order support; beta ambiguity is a protocol fold, not domain ontology.
+
+## Self-Audit: 5 Lenti
+
+L1 hard constraint vs bias: no "always", "never", or "zero" claim is used
+without exact support. `endpoint_stable_observables: []` is exact for this run
+and perimeters.
+
+L2 quantity vs ratio: the report uses raw stable counts, endpoint distances, and
+beta layers. Percent language is avoided except where the script computes
+fractions.
+
+L3 no silent patching: the scope changed from BOUNDARY to non-BOUNDARY
+perimeters explicitly. The new category `order_denominator_gate` is declared
+instead of stretching `BOUNDARY_LAYER_GATE`.
+
+L4 edge cases: `E_ar1_continuity` has illusory stable_count `0.60`, so the
+illusory endpoint is weak-denominator, not denominator-absent. The claim is
+one-sided support dominance, not exact absence.
+
+L5 re-discovery vs discovery: the numerical mechanism is an original-vs-shuffle
+order test on synthetic sequences. No NEW mathematical theorem is claimed.
+
+## Fonti
+
+- Verificato: `tools/data/agent_field_live.md`
+- Verificato: `tools/LAB_AGENT_CONTEXT.md`
+- Verificato: `tools/observables_registry.py`
+- Verificato: `tools/exp_denominator_gate_transfer_matrix.py`
+- Verificato: `tools/data/denominator_gate_transfer_matrix.json`
+- Inferito: `order_denominator_gate` as category needed by the downstream
+  vocabulary, from the cross-perimeter matrix.
diff --git a/tools/data/seme.json b/tools/data/seme.json
index 9dc9f5c8d087e9aea3266d5fefbc52a939acea27..47927a057e5396452b3c7982c8218c6e7c4d3b12
--- a/tools/data/seme.json
+++ b/tools/data/seme.json
@@ -132,6 +132,17 @@
       "_source_log": "2026-05-07T08:10:22.658201+00:00",
       "_source_decision": "REDESIGN",
       "_source_reasoning": "Il ciclo ha prodotto evidenza controllata e replicata che il denominator gate trasferisce come operatore, ma non trasferisce la coordinata di layer BOUNDARY: ambiguita' classificativa e collasso del denominatore si separano. Continuare sul seme attuale centrato su GUE/Poisson rischia di restare nel "
+    },
+    {
+      "tipo": "vincolo",
+      "id": "ORDER_DENOMINATOR_GATE",
+      "claim": "Il denominator gate trasferisce cross-perimetro come supporto one-sided dell'ordine generato, non come endpoint-stable support a due poli. Nel perimetro agent_20260507_0901, 4/4 domini non-BOUNDARY hanno endpoint_stable_observables=[]; il polo coerente ha stable_count 3.0-5.0, il polo illusorio 0.05-0.60. La beta ambigua 0.30 e coordinata del protocollo di perturbazione finche non viene ancorata a un asse semantico del dominio.",
+      "intensita": 0.92,
+      "porta": "META",
+      "manuale": true,
+      "condensato_ref": "A4,A8,A14,C2",
+      "origine": "cycle agent_20260507_0901: denominator gate transfer matrix su DUALITA_golden, R_periodic_triad, T_markov_alternating, E_ar1_continuity",
+      "added_at": "2026-05-07T09:01:00+00:00"
     }
   ],
   "potenziale_bloccato": [],
@@ -139,11 +150,11 @@
     "Tensioni risolte: {'PIANO_PRIMARIO_DUE_ASSIOMI', 'METRIC_TENSOR', 'BOUNDARY_LAYER_GATE', 'OBSERVABLE_REGISTRY', 'DUALITA_DIPOLARE_VS_ILLUSORIA', 'TENSIONE_ENTITA', 'PERTURBATION_DENOMINATOR_GATE', 'G_POTENZIALE_NULLA', 'TRASCENDENZA_LIMITE'}"
   ],
   "filtro": {
-    "promosse": 11,
+    "promosse": 12,
     "filtrate": 0,
     "condensato_claims": 30
   },
-  "direzione": "Costruire una matrice di trasferibilita' del denominator gate: applicarlo a 3-4 perimetri non-BOUNDARY con poli coerente/illusorio e verificare quali parti trasferiscono (supporto one-sided, coordinate di collasso, ambiguita' classificativa) e quali restano locali.",
+  "direzione": "Verificare se ORDER_DENOMINATOR_GATE resta one-sided su perimetri naturali/non sintetici: primes windows, logistic map, Fibonacci spectrum o dominio fisico gia nel lab. Separare supporto d'ordine, coordinata di protocollo e asse semantico del dominio.",
   "verifica": {
     "pass": 11,
     "fail": 0,
@@ -311,6 +322,7 @@
     }
   ],
   "_seed_history": {
-    "trajectory_apply_20260507_090145": "applied modify_seme from 20260507_0803: direzione → 'Costruire una matrice di trasferibilita' del denominator gate: applicarlo a 3-4 perimetri non-BOUNDA...'"
+    "trajectory_apply_20260507_090145": "applied modify_seme from 20260507_0803: direzione → 'Costruire una matrice di trasferibilita' del denominator gate: applicarlo a 3-4 perimetri non-BOUNDA...'",
+    "agent_20260507_0901": "added ORDER_DENOMINATOR_GATE; direzione -> test one-sided gate on natural/non-synthetic perimeters"
   }
-}
\ No newline at end of file
+}
diff --git a/tools/exp_denominator_gate_transfer_matrix.py b/tools/exp_denominator_gate_transfer_matrix.py
new file mode 100644
index 0000000000000000000000000000000000000000..bb0a837fe1303dca05fa887b9ca28e30c4bf9031
--- /dev/null
+++ b/tools/exp_denominator_gate_transfer_matrix.py
@@ -0,0 +1,337 @@
+#!/usr/bin/env python3
+"""
+exp_denominator_gate_transfer_matrix.py
+
+Reusable META audit for the denominator gate transfer matrix.
+
+The experiment moves the original-vs-shuffle denominator gate away from the
+GUE/Poisson BOUNDARY perimeter. Each perimeter has a coherent endpoint and an
+illusory endpoint built as a permutation of the same gap multiset. That keeps
+the one-point distribution fixed and isolates ordering support.
+
+Measured for each perimeter:
+- canonical observables from observables_registry.py;
+- original-vs-shuffle z-score for each observable;
+- endpoint-stable observable set under |z| >= z_min;
+- endpoint classification using all observables and endpoint-gated observables;
+- beta layer ambiguity between coherent and illusory endpoints.
+"""
+
+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())
+PHI = (1.0 + 5.0**0.5) / 2.0
+
+
+def normalize(gaps: np.ndarray) -> np.ndarray:
+    gaps = np.asarray(gaps, dtype=float)
+    gaps = np.maximum(gaps, 1e-12)
+    mean = float(np.mean(gaps))
+    return gaps / mean if mean > 1e-15 else gaps
+
+
+def golden_beatty(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    phase = float(rng.random())
+    n = np.arange(n_gaps + 1, dtype=float) + phase
+    positions = np.floor(n * PHI)
+    return normalize(np.diff(positions))
+
+
+def periodic_triad(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    base = np.array([0.55, 1.0, 1.45, 1.0, 0.75, 1.25], dtype=float)
+    shift = int(rng.integers(0, len(base)))
+    tiled = np.tile(np.roll(base, shift), int(np.ceil(n_gaps / len(base))))[:n_gaps]
+    jitter = rng.normal(0.0, 0.015, size=n_gaps)
+    return normalize(tiled + jitter)
+
+
+def markov_alternating(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    vals = np.array([0.62, 1.38], dtype=float)
+    state = int(rng.integers(0, 2))
+    out = np.empty(n_gaps, dtype=float)
+    for i in range(n_gaps):
+        out[i] = vals[state] + rng.normal(0.0, 0.03)
+        if rng.random() < 0.88:
+            state = 1 - state
+    return normalize(out)
+
+
+def ar1_continuity(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    rho = 0.86
+    x = np.empty(n_gaps, dtype=float)
+    x[0] = rng.normal()
+    noise_scale = (1.0 - rho * rho) ** 0.5
+    for i in range(1, n_gaps):
+        x[i] = rho * x[i - 1] + noise_scale * rng.normal()
+    return normalize(np.exp(0.42 * x))
+
+
+PERIMETERS = {
+    "DUALITA_golden": golden_beatty,
+    "R_periodic_triad": periodic_triad,
+    "T_markov_alternating": markov_alternating,
+    "E_ar1_continuity": ar1_continuity,
+}
+
+
+def beta_layer(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        out = base.copy()
+    elif beta >= 1.0:
+        out = illusory.copy()
+    else:
+        mask = rng.random(len(base)) < beta
+        out = base.copy()
+        out[mask] = illusory[mask]
+    return normalize(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]]:
+    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])
+
+    shuffle_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
+        shuffle_std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, shuffle_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:
+    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)
+
+    coherent_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent_vectors, illusory_vectors])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent_vectors, axis=0)
+    illusory_centroid = np.mean(illusory_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((illusory_centroid - coherent_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_coherent = float(np.linalg.norm((x - coherent_centroid) / scale))
+            d_illusory = float(np.linalg.norm((x - illusory_centroid) / scale))
+            denom = d_coherent + d_illusory
+            coord = float((d_coherent - d_illusory) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_coherent - d_illusory) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("coherent" if d_coherent < d_illusory else "illusory")
+        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,
+            "illusory_label_fraction": float(np.mean(np.array(labels) == "illusory")),
+        }
+
+    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}
+        z_values = {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)
+                z_values[name].append(row["z"][name])
+        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()},
+            "z_mean": {name: float(np.mean(vals)) for name, vals in z_values.items()},
+        }
+
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    endpoint_one_sided = []
+    for name in OBS_NAMES:
+        coherent_freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in coherent_rows])
+        illusory_freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in illusory_rows])
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            endpoint_one_sided.append(name)
+        if np.mean([coherent_freq, illusory_freq]) >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "coherent_one_sided_observables": endpoint_one_sided,
+        "layers": layers,
+    }
+
+
+def analyze_perimeter(name: str, generator, args: argparse.Namespace, rng: np.random.Generator) -> dict:
+    rows = []
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    for rep in range(args.n_replicates):
+        base = generator(args.n_gaps, np.random.default_rng(rng.integers(0, 2**63 - 1)))
+        for beta in betas:
+            gaps = beta_layer(base, beta, np.random.default_rng(rng.integers(0, 2**63 - 1)))
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                args.n_baseline,
+                np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "perimeter": name,
+                    "replicate": rep,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [obs_name for obs_name in OBS_NAMES if abs(z[obs_name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    one_sided_classification = classify_layers(rows, gate["coherent_one_sided_observables"])
+    return {
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_one_sided_gated": one_sided_classification,
+        "rows": rows,
+    }
+
+
+def compact_matrix(perimeters: dict) -> dict:
+    matrix = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        class_all = data["classification_all_observables"]
+        class_gate = data["classification_one_sided_gated"]
+        matrix[name] = {
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "endpoint_distance_all": class_all["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": class_gate["endpoint_distance"],
+            "ambiguous_beta_all": class_all["ambiguous_beta"],
+            "ambiguous_beta_one_sided_gated": class_gate["ambiguous_beta"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+        }
+    return matrix
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    perimeters = {}
+    for name, generator in PERIMETERS.items():
+        perimeters[name] = analyze_perimeter(name, generator, args, root_rng)
+
+    output = {
+        "experiment": "denominator_gate_transfer_matrix",
+        "category": "gate_transferability",
+        "question": "Which parts of the denominator gate transfer outside BOUNDARY?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact_matrix(perimeters),
+        "perimeters": perimeters,
+    }
+
+    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("perimeter one_sided stable0 stable1 dist_all dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>21s} "
+            f"{','.join(row['coherent_one_sided_observables']) or '[]':>22s} "
+            f"{row['stable_count_coherent']:>7.3f} "
+            f"{row['stable_count_illusory']:>7.3f} "
+            f"{row['endpoint_distance_all']:>8.3f} "
+            f"{row['endpoint_distance_one_sided_gated']:>9.3f} "
+            f"{row['ambiguous_beta_one_sided_gated']}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=4096)
+    parser.add_argument("--n-replicates", type=int, default=20)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=32)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=202605070901)
+    parser.add_argument("--out", default="tools/data/denominator_gate_transfer_matrix.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

codex
Rilevo una piccola imprecisione semantica nello script: `endpoint_stable` deve significare stabile su entrambi i poli, non frequenza aggregata sui due endpoint. Correggo la definizione prima della verifica finale e rigenero il JSON.
apply patch
patch: completed
/opt/MM_D-ND/tools/exp_denominator_gate_transfer_matrix.py
diff --git a/tools/data/reports/agent_20260507_0901.md b/tools/data/reports/agent_20260507_0901.md
new file mode 100644
index 0000000000000000000000000000000000000000..101695bfa907f5e680e8d408da1a95b66b7203f1
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0901.md
@@ -0,0 +1,122 @@
+# Agent Report — Denominator Gate Transfer Matrix
+
+timestamp: 2026-05-07 09:01 UTC
+category: gate_transferability_matrix
+verdict: operator
+observables_registry: 1.0.0-2026-05-06
+observables_used: [SR, SR2, L1, L2, triple_var]
+tool: tools/exp_denominator_gate_transfer_matrix.py
+data: tools/data/denominator_gate_transfer_matrix.json
+
+## Claim Under Test
+
+Verificato dal campo vivo: il cycle 0803 ha mostrato che il denominator gate
+trasferisce da BOUNDARY a DUALITA come operatore, ma non trasferisce la
+coordinata locale GUE/Poisson.
+
+Domanda di questo cycle: applicato a 4 perimetri non-BOUNDARY con poli
+coerente/illusorio a distribuzione marginale preservata, quali parti del gate
+trasferiscono e quali restano locali?
+
+Perimetri:
+
+- `DUALITA_golden`: sequenza Beatty aurea coerente vs permutazione illusoria.
+- `R_periodic_triad`: pattern periodico a 6 fasi vs permutazione illusoria.
+- `T_markov_alternating`: catena alternante low/high vs permutazione illusoria.
+- `E_ar1_continuity`: continuita AR(1) positiva vs permutazione illusoria.
+
+Parametri verificati: `n_gaps=4096`, `n_replicates=20`, `n_beta=11`,
+`n_baseline=32`, `z_min=2.0`, `seed=202605070901`.
+
+## Deposito Numerico
+
+| perimeter | coherent one-sided observables | stable_count coherent | stable_count illusory | endpoint distance gated | ambiguous beta gated |
+|---|---:|---:|---:|---:|---:|
+| DUALITA_golden | SR, L1, triple_var | 3.00 | 0.25 | 3.418 | 0.30 |
+| R_periodic_triad | SR, SR2, L1, L2, triple_var | 5.00 | 0.25 | 4.400 | 0.30 |
+| T_markov_alternating | SR, SR2, L1, L2, triple_var | 5.00 | 0.05 | 4.412 | 0.30 |
+| E_ar1_continuity | SR, SR2, L1, L2, triple_var | 5.00 | 0.60 | 4.394 | 0.30 |
+
+Endpoint-stable observables: `[]` in all 4 perimeters.
+
+Verificato: no observable is stable at both coherent and illusory endpoints.
+The denominator gate is not a two-sided endpoint support. It is one-sided
+support for generated order.
+
+## Risultato
+
+1. The gate transfers as one-sided coherence support.
+
+   In all 4 non-BOUNDARY perimeters, the coherent endpoint carries denominator
+   support and the illusory endpoint loses it. `DUALITA_golden` transfers on
+   `SR`, `L1`, `triple_var`; the three other perimeters transfer on all five
+   canonical observables.
+
+2. The both-endpoint stable set collapses everywhere.
+
+   `endpoint_stable_observables: []` is not a failure of the test. It is the
+   structural content: the gate does not name "two classes with stable
+   denominators"; it names the side where order survives its own shuffle null.
+
+3. The beta 0.30 ambiguity layer transfers as protocol coordinate, not as
+   BOUNDARY coordinate.
+
+   The ambiguous gated layer is beta `0.30` in all 4 perimeters. This is not a
+   GUE/Poisson layer. It is the coordinate created by the replacement protocol:
+   enough positions are illusory to place the layer near the endpoint bisector,
+   while enough coherent order remains to keep large z support.
+
+4. DUALITA is narrower than the other perimeters.
+
+   `DUALITA_golden` has one-sided support on 3/5 observables. `SR2` and `L2`
+   stay near shuffle at the coherent endpoint (`z_mean` about `0.12`). The
+   DUALITA gate transfers, but only through lag-1 and triple aggregation in this
+   synthetic perimeter.
+
+## Consecutio
+
+The next word is not `boundary_layer`. The correct category is:
+
+`order_denominator_gate`
+
+Definition: an original-vs-shuffle denominator gate where support is expected
+on the coherent/generated side, endpoint-stable observables may be empty, and
+the layer coordinate belongs to the perturbation protocol unless anchored to a
+domain-specific semantic axis.
+
+This extends the 0803 result:
+
+- 0803: BOUNDARY denominator gate transfers to DUALITA, BOUNDARY coordinate does not.
+- 0901: across 4 non-BOUNDARY perimeters, the transferable object is one-sided
+  order support; beta ambiguity is a protocol fold, not domain ontology.
+
+## Self-Audit: 5 Lenti
+
+L1 hard constraint vs bias: no "always", "never", or "zero" claim is used
+without exact support. `endpoint_stable_observables: []` is exact for this run
+and perimeters.
+
+L2 quantity vs ratio: the report uses raw stable counts, endpoint distances, and
+beta layers. Percent language is avoided except where the script computes
+fractions.
+
+L3 no silent patching: the scope changed from BOUNDARY to non-BOUNDARY
+perimeters explicitly. The new category `order_denominator_gate` is declared
+instead of stretching `BOUNDARY_LAYER_GATE`.
+
+L4 edge cases: `E_ar1_continuity` has illusory stable_count `0.60`, so the
+illusory endpoint is weak-denominator, not denominator-absent. The claim is
+one-sided support dominance, not exact absence.
+
+L5 re-discovery vs discovery: the numerical mechanism is an original-vs-shuffle
+order test on synthetic sequences. No NEW mathematical theorem is claimed.
+
+## Fonti
+
+- Verificato: `tools/data/agent_field_live.md`
+- Verificato: `tools/LAB_AGENT_CONTEXT.md`
+- Verificato: `tools/observables_registry.py`
+- Verificato: `tools/exp_denominator_gate_transfer_matrix.py`
+- Verificato: `tools/data/denominator_gate_transfer_matrix.json`
+- Inferito: `order_denominator_gate` as category needed by the downstream
+  vocabulary, from the cross-perimeter matrix.
diff --git a/tools/data/seme.json b/tools/data/seme.json
index 9dc9f5c8d087e9aea3266d5fefbc52a939acea27..47927a057e5396452b3c7982c8218c6e7c4d3b12
--- a/tools/data/seme.json
+++ b/tools/data/seme.json
@@ -132,6 +132,17 @@
       "_source_log": "2026-05-07T08:10:22.658201+00:00",
       "_source_decision": "REDESIGN",
       "_source_reasoning": "Il ciclo ha prodotto evidenza controllata e replicata che il denominator gate trasferisce come operatore, ma non trasferisce la coordinata di layer BOUNDARY: ambiguita' classificativa e collasso del denominatore si separano. Continuare sul seme attuale centrato su GUE/Poisson rischia di restare nel "
+    },
+    {
+      "tipo": "vincolo",
+      "id": "ORDER_DENOMINATOR_GATE",
+      "claim": "Il denominator gate trasferisce cross-perimetro come supporto one-sided dell'ordine generato, non come endpoint-stable support a due poli. Nel perimetro agent_20260507_0901, 4/4 domini non-BOUNDARY hanno endpoint_stable_observables=[]; il polo coerente ha stable_count 3.0-5.0, il polo illusorio 0.05-0.60. La beta ambigua 0.30 e coordinata del protocollo di perturbazione finche non viene ancorata a un asse semantico del dominio.",
+      "intensita": 0.92,
+      "porta": "META",
+      "manuale": true,
+      "condensato_ref": "A4,A8,A14,C2",
+      "origine": "cycle agent_20260507_0901: denominator gate transfer matrix su DUALITA_golden, R_periodic_triad, T_markov_alternating, E_ar1_continuity",
+      "added_at": "2026-05-07T09:01:00+00:00"
     }
   ],
   "potenziale_bloccato": [],
@@ -139,11 +150,11 @@
     "Tensioni risolte: {'PIANO_PRIMARIO_DUE_ASSIOMI', 'METRIC_TENSOR', 'BOUNDARY_LAYER_GATE', 'OBSERVABLE_REGISTRY', 'DUALITA_DIPOLARE_VS_ILLUSORIA', 'TENSIONE_ENTITA', 'PERTURBATION_DENOMINATOR_GATE', 'G_POTENZIALE_NULLA', 'TRASCENDENZA_LIMITE'}"
   ],
   "filtro": {
-    "promosse": 11,
+    "promosse": 12,
     "filtrate": 0,
     "condensato_claims": 30
   },
-  "direzione": "Costruire una matrice di trasferibilita' del denominator gate: applicarlo a 3-4 perimetri non-BOUNDARY con poli coerente/illusorio e verificare quali parti trasferiscono (supporto one-sided, coordinate di collasso, ambiguita' classificativa) e quali restano locali.",
+  "direzione": "Verificare se ORDER_DENOMINATOR_GATE resta one-sided su perimetri naturali/non sintetici: primes windows, logistic map, Fibonacci spectrum o dominio fisico gia nel lab. Separare supporto d'ordine, coordinata di protocollo e asse semantico del dominio.",
   "verifica": {
     "pass": 11,
     "fail": 0,
@@ -311,6 +322,7 @@
     }
   ],
   "_seed_history": {
-    "trajectory_apply_20260507_090145": "applied modify_seme from 20260507_0803: direzione → 'Costruire una matrice di trasferibilita' del denominator gate: applicarlo a 3-4 perimetri non-BOUNDA...'"
+    "trajectory_apply_20260507_090145": "applied modify_seme from 20260507_0803: direzione → 'Costruire una matrice di trasferibilita' del denominator gate: applicarlo a 3-4 perimetri non-BOUNDA...'",
+    "agent_20260507_0901": "added ORDER_DENOMINATOR_GATE; direzione -> test one-sided gate on natural/non-synthetic perimeters"
   }
-}
\ No newline at end of file
+}
diff --git a/tools/exp_denominator_gate_transfer_matrix.py b/tools/exp_denominator_gate_transfer_matrix.py
new file mode 100644
index 0000000000000000000000000000000000000000..f23cf7cf9505045e3d62ad95c8f3cddb38b31cef
--- /dev/null
+++ b/tools/exp_denominator_gate_transfer_matrix.py
@@ -0,0 +1,337 @@
+#!/usr/bin/env python3
+"""
+exp_denominator_gate_transfer_matrix.py
+
+Reusable META audit for the denominator gate transfer matrix.
+
+The experiment moves the original-vs-shuffle denominator gate away from the
+GUE/Poisson BOUNDARY perimeter. Each perimeter has a coherent endpoint and an
+illusory endpoint built as a permutation of the same gap multiset. That keeps
+the one-point distribution fixed and isolates ordering support.
+
+Measured for each perimeter:
+- canonical observables from observables_registry.py;
+- original-vs-shuffle z-score for each observable;
+- endpoint-stable observable set under |z| >= z_min;
+- endpoint classification using all observables and endpoint-gated observables;
+- beta layer ambiguity between coherent and illusory endpoints.
+"""
+
+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())
+PHI = (1.0 + 5.0**0.5) / 2.0
+
+
+def normalize(gaps: np.ndarray) -> np.ndarray:
+    gaps = np.asarray(gaps, dtype=float)
+    gaps = np.maximum(gaps, 1e-12)
+    mean = float(np.mean(gaps))
+    return gaps / mean if mean > 1e-15 else gaps
+
+
+def golden_beatty(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    phase = float(rng.random())
+    n = np.arange(n_gaps + 1, dtype=float) + phase
+    positions = np.floor(n * PHI)
+    return normalize(np.diff(positions))
+
+
+def periodic_triad(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    base = np.array([0.55, 1.0, 1.45, 1.0, 0.75, 1.25], dtype=float)
+    shift = int(rng.integers(0, len(base)))
+    tiled = np.tile(np.roll(base, shift), int(np.ceil(n_gaps / len(base))))[:n_gaps]
+    jitter = rng.normal(0.0, 0.015, size=n_gaps)
+    return normalize(tiled + jitter)
+
+
+def markov_alternating(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    vals = np.array([0.62, 1.38], dtype=float)
+    state = int(rng.integers(0, 2))
+    out = np.empty(n_gaps, dtype=float)
+    for i in range(n_gaps):
+        out[i] = vals[state] + rng.normal(0.0, 0.03)
+        if rng.random() < 0.88:
+            state = 1 - state
+    return normalize(out)
+
+
+def ar1_continuity(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    rho = 0.86
+    x = np.empty(n_gaps, dtype=float)
+    x[0] = rng.normal()
+    noise_scale = (1.0 - rho * rho) ** 0.5
+    for i in range(1, n_gaps):
+        x[i] = rho * x[i - 1] + noise_scale * rng.normal()
+    return normalize(np.exp(0.42 * x))
+
+
+PERIMETERS = {
+    "DUALITA_golden": golden_beatty,
+    "R_periodic_triad": periodic_triad,
+    "T_markov_alternating": markov_alternating,
+    "E_ar1_continuity": ar1_continuity,
+}
+
+
+def beta_layer(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        out = base.copy()
+    elif beta >= 1.0:
+        out = illusory.copy()
+    else:
+        mask = rng.random(len(base)) < beta
+        out = base.copy()
+        out[mask] = illusory[mask]
+    return normalize(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]]:
+    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])
+
+    shuffle_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
+        shuffle_std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, shuffle_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:
+    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)
+
+    coherent_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent_vectors, illusory_vectors])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent_vectors, axis=0)
+    illusory_centroid = np.mean(illusory_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((illusory_centroid - coherent_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_coherent = float(np.linalg.norm((x - coherent_centroid) / scale))
+            d_illusory = float(np.linalg.norm((x - illusory_centroid) / scale))
+            denom = d_coherent + d_illusory
+            coord = float((d_coherent - d_illusory) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_coherent - d_illusory) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("coherent" if d_coherent < d_illusory else "illusory")
+        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,
+            "illusory_label_fraction": float(np.mean(np.array(labels) == "illusory")),
+        }
+
+    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}
+        z_values = {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)
+                z_values[name].append(row["z"][name])
+        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()},
+            "z_mean": {name: float(np.mean(vals)) for name, vals in z_values.items()},
+        }
+
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    endpoint_one_sided = []
+    for name in OBS_NAMES:
+        coherent_freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in coherent_rows])
+        illusory_freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in illusory_rows])
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            endpoint_one_sided.append(name)
+        if coherent_freq >= 0.75 and illusory_freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "coherent_one_sided_observables": endpoint_one_sided,
+        "layers": layers,
+    }
+
+
+def analyze_perimeter(name: str, generator, args: argparse.Namespace, rng: np.random.Generator) -> dict:
+    rows = []
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    for rep in range(args.n_replicates):
+        base = generator(args.n_gaps, np.random.default_rng(rng.integers(0, 2**63 - 1)))
+        for beta in betas:
+            gaps = beta_layer(base, beta, np.random.default_rng(rng.integers(0, 2**63 - 1)))
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                args.n_baseline,
+                np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "perimeter": name,
+                    "replicate": rep,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [obs_name for obs_name in OBS_NAMES if abs(z[obs_name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    one_sided_classification = classify_layers(rows, gate["coherent_one_sided_observables"])
+    return {
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_one_sided_gated": one_sided_classification,
+        "rows": rows,
+    }
+
+
+def compact_matrix(perimeters: dict) -> dict:
+    matrix = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        class_all = data["classification_all_observables"]
+        class_gate = data["classification_one_sided_gated"]
+        matrix[name] = {
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "endpoint_distance_all": class_all["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": class_gate["endpoint_distance"],
+            "ambiguous_beta_all": class_all["ambiguous_beta"],
+            "ambiguous_beta_one_sided_gated": class_gate["ambiguous_beta"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+        }
+    return matrix
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    perimeters = {}
+    for name, generator in PERIMETERS.items():
+        perimeters[name] = analyze_perimeter(name, generator, args, root_rng)
+
+    output = {
+        "experiment": "denominator_gate_transfer_matrix",
+        "category": "gate_transferability",
+        "question": "Which parts of the denominator gate transfer outside BOUNDARY?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact_matrix(perimeters),
+        "perimeters": perimeters,
+    }
+
+    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("perimeter one_sided stable0 stable1 dist_all dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>21s} "
+            f"{','.join(row['coherent_one_sided_observables']) or '[]':>22s} "
+            f"{row['stable_count_coherent']:>7.3f} "
+            f"{row['stable_count_illusory']:>7.3f} "
+            f"{row['endpoint_distance_all']:>8.3f} "
+            f"{row['endpoint_distance_one_sided_gated']:>9.3f} "
+            f"{row['ambiguous_beta_one_sided_gated']}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=4096)
+    parser.add_argument("--n-replicates", type=int, default=20)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=32)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=202605070901)
+    parser.add_argument("--out", default="tools/data/denominator_gate_transfer_matrix.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

diff --git a/tools/data/reports/agent_20260507_0901.md b/tools/data/reports/agent_20260507_0901.md
new file mode 100644
index 0000000000000000000000000000000000000000..101695bfa907f5e680e8d408da1a95b66b7203f1
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0901.md
@@ -0,0 +1,122 @@
+# Agent Report — Denominator Gate Transfer Matrix
+
+timestamp: 2026-05-07 09:01 UTC
+category: gate_transferability_matrix
+verdict: operator
+observables_registry: 1.0.0-2026-05-06
+observables_used: [SR, SR2, L1, L2, triple_var]
+tool: tools/exp_denominator_gate_transfer_matrix.py
+data: tools/data/denominator_gate_transfer_matrix.json
+
+## Claim Under Test
+
+Verificato dal campo vivo: il cycle 0803 ha mostrato che il denominator gate
+trasferisce da BOUNDARY a DUALITA come operatore, ma non trasferisce la
+coordinata locale GUE/Poisson.
+
+Domanda di questo cycle: applicato a 4 perimetri non-BOUNDARY con poli
+coerente/illusorio a distribuzione marginale preservata, quali parti del gate
+trasferiscono e quali restano locali?
+
+Perimetri:
+
+- `DUALITA_golden`: sequenza Beatty aurea coerente vs permutazione illusoria.
+- `R_periodic_triad`: pattern periodico a 6 fasi vs permutazione illusoria.
+- `T_markov_alternating`: catena alternante low/high vs permutazione illusoria.
+- `E_ar1_continuity`: continuita AR(1) positiva vs permutazione illusoria.
+
+Parametri verificati: `n_gaps=4096`, `n_replicates=20`, `n_beta=11`,
+`n_baseline=32`, `z_min=2.0`, `seed=202605070901`.
+
+## Deposito Numerico
+
+| perimeter | coherent one-sided observables | stable_count coherent | stable_count illusory | endpoint distance gated | ambiguous beta gated |
+|---|---:|---:|---:|---:|---:|
+| DUALITA_golden | SR, L1, triple_var | 3.00 | 0.25 | 3.418 | 0.30 |
+| R_periodic_triad | SR, SR2, L1, L2, triple_var | 5.00 | 0.25 | 4.400 | 0.30 |
+| T_markov_alternating | SR, SR2, L1, L2, triple_var | 5.00 | 0.05 | 4.412 | 0.30 |
+| E_ar1_continuity | SR, SR2, L1, L2, triple_var | 5.00 | 0.60 | 4.394 | 0.30 |
+
+Endpoint-stable observables: `[]` in all 4 perimeters.
+
+Verificato: no observable is stable at both coherent and illusory endpoints.
+The denominator gate is not a two-sided endpoint support. It is one-sided
+support for generated order.
+
+## Risultato
+
+1. The gate transfers as one-sided coherence support.
+
+   In all 4 non-BOUNDARY perimeters, the coherent endpoint carries denominator
+   support and the illusory endpoint loses it. `DUALITA_golden` transfers on
+   `SR`, `L1`, `triple_var`; the three other perimeters transfer on all five
+   canonical observables.
+
+2. The both-endpoint stable set collapses everywhere.
+
+   `endpoint_stable_observables: []` is not a failure of the test. It is the
+   structural content: the gate does not name "two classes with stable
+   denominators"; it names the side where order survives its own shuffle null.
+
+3. The beta 0.30 ambiguity layer transfers as protocol coordinate, not as
+   BOUNDARY coordinate.
+
+   The ambiguous gated layer is beta `0.30` in all 4 perimeters. This is not a
+   GUE/Poisson layer. It is the coordinate created by the replacement protocol:
+   enough positions are illusory to place the layer near the endpoint bisector,
+   while enough coherent order remains to keep large z support.
+
+4. DUALITA is narrower than the other perimeters.
+
+   `DUALITA_golden` has one-sided support on 3/5 observables. `SR2` and `L2`
+   stay near shuffle at the coherent endpoint (`z_mean` about `0.12`). The
+   DUALITA gate transfers, but only through lag-1 and triple aggregation in this
+   synthetic perimeter.
+
+## Consecutio
+
+The next word is not `boundary_layer`. The correct category is:
+
+`order_denominator_gate`
+
+Definition: an original-vs-shuffle denominator gate where support is expected
+on the coherent/generated side, endpoint-stable observables may be empty, and
+the layer coordinate belongs to the perturbation protocol unless anchored to a
+domain-specific semantic axis.
+
+This extends the 0803 result:
+
+- 0803: BOUNDARY denominator gate transfers to DUALITA, BOUNDARY coordinate does not.
+- 0901: across 4 non-BOUNDARY perimeters, the transferable object is one-sided
+  order support; beta ambiguity is a protocol fold, not domain ontology.
+
+## Self-Audit: 5 Lenti
+
+L1 hard constraint vs bias: no "always", "never", or "zero" claim is used
+without exact support. `endpoint_stable_observables: []` is exact for this run
+and perimeters.
+
+L2 quantity vs ratio: the report uses raw stable counts, endpoint distances, and
+beta layers. Percent language is avoided except where the script computes
+fractions.
+
+L3 no silent patching: the scope changed from BOUNDARY to non-BOUNDARY
+perimeters explicitly. The new category `order_denominator_gate` is declared
+instead of stretching `BOUNDARY_LAYER_GATE`.
+
+L4 edge cases: `E_ar1_continuity` has illusory stable_count `0.60`, so the
+illusory endpoint is weak-denominator, not denominator-absent. The claim is
+one-sided support dominance, not exact absence.
+
+L5 re-discovery vs discovery: the numerical mechanism is an original-vs-shuffle
+order test on synthetic sequences. No NEW mathematical theorem is claimed.
+
+## Fonti
+
+- Verificato: `tools/data/agent_field_live.md`
+- Verificato: `tools/LAB_AGENT_CONTEXT.md`
+- Verificato: `tools/observables_registry.py`
+- Verificato: `tools/exp_denominator_gate_transfer_matrix.py`
+- Verificato: `tools/data/denominator_gate_transfer_matrix.json`
+- Inferito: `order_denominator_gate` as category needed by the downstream
+  vocabulary, from the cross-perimeter matrix.
diff --git a/tools/data/seme.json b/tools/data/seme.json
index 9dc9f5c8d087e9aea3266d5fefbc52a939acea27..47927a057e5396452b3c7982c8218c6e7c4d3b12
--- a/tools/data/seme.json
+++ b/tools/data/seme.json
@@ -132,6 +132,17 @@
       "_source_log": "2026-05-07T08:10:22.658201+00:00",
       "_source_decision": "REDESIGN",
       "_source_reasoning": "Il ciclo ha prodotto evidenza controllata e replicata che il denominator gate trasferisce come operatore, ma non trasferisce la coordinata di layer BOUNDARY: ambiguita' classificativa e collasso del denominatore si separano. Continuare sul seme attuale centrato su GUE/Poisson rischia di restare nel "
+    },
+    {
+      "tipo": "vincolo",
+      "id": "ORDER_DENOMINATOR_GATE",
+      "claim": "Il denominator gate trasferisce cross-perimetro come supporto one-sided dell'ordine generato, non come endpoint-stable support a due poli. Nel perimetro agent_20260507_0901, 4/4 domini non-BOUNDARY hanno endpoint_stable_observables=[]; il polo coerente ha stable_count 3.0-5.0, il polo illusorio 0.05-0.60. La beta ambigua 0.30 e coordinata del protocollo di perturbazione finche non viene ancorata a un asse semantico del dominio.",
+      "intensita": 0.92,
+      "porta": "META",
+      "manuale": true,
+      "condensato_ref": "A4,A8,A14,C2",
+      "origine": "cycle agent_20260507_0901: denominator gate transfer matrix su DUALITA_golden, R_periodic_triad, T_markov_alternating, E_ar1_continuity",
+      "added_at": "2026-05-07T09:01:00+00:00"
     }
   ],
   "potenziale_bloccato": [],
@@ -139,11 +150,11 @@
     "Tensioni risolte: {'PIANO_PRIMARIO_DUE_ASSIOMI', 'METRIC_TENSOR', 'BOUNDARY_LAYER_GATE', 'OBSERVABLE_REGISTRY', 'DUALITA_DIPOLARE_VS_ILLUSORIA', 'TENSIONE_ENTITA', 'PERTURBATION_DENOMINATOR_GATE', 'G_POTENZIALE_NULLA', 'TRASCENDENZA_LIMITE'}"
   ],
   "filtro": {
-    "promosse": 11,
+    "promosse": 12,
     "filtrate": 0,
     "condensato_claims": 30
   },
-  "direzione": "Costruire una matrice di trasferibilita' del denominator gate: applicarlo a 3-4 perimetri non-BOUNDARY con poli coerente/illusorio e verificare quali parti trasferiscono (supporto one-sided, coordinate di collasso, ambiguita' classificativa) e quali restano locali.",
+  "direzione": "Verificare se ORDER_DENOMINATOR_GATE resta one-sided su perimetri naturali/non sintetici: primes windows, logistic map, Fibonacci spectrum o dominio fisico gia nel lab. Separare supporto d'ordine, coordinata di protocollo e asse semantico del dominio.",
   "verifica": {
     "pass": 11,
     "fail": 0,
@@ -311,6 +322,7 @@
     }
   ],
   "_seed_history": {
-    "trajectory_apply_20260507_090145": "applied modify_seme from 20260507_0803: direzione → 'Costruire una matrice di trasferibilita' del denominator gate: applicarlo a 3-4 perimetri non-BOUNDA...'"
+    "trajectory_apply_20260507_090145": "applied modify_seme from 20260507_0803: direzione → 'Costruire una matrice di trasferibilita' del denominator gate: applicarlo a 3-4 perimetri non-BOUNDA...'",
+    "agent_20260507_0901": "added ORDER_DENOMINATOR_GATE; direzione -> test one-sided gate on natural/non-synthetic perimeters"
   }
-}
\ No newline at end of file
+}
diff --git a/tools/exp_denominator_gate_transfer_matrix.py b/tools/exp_denominator_gate_transfer_matrix.py
new file mode 100644
index 0000000000000000000000000000000000000000..f23cf7cf9505045e3d62ad95c8f3cddb38b31cef
--- /dev/null
+++ b/tools/exp_denominator_gate_transfer_matrix.py
@@ -0,0 +1,337 @@
+#!/usr/bin/env python3
+"""
+exp_denominator_gate_transfer_matrix.py
+
+Reusable META audit for the denominator gate transfer matrix.
+
+The experiment moves the original-vs-shuffle denominator gate away from the
+GUE/Poisson BOUNDARY perimeter. Each perimeter has a coherent endpoint and an
+illusory endpoint built as a permutation of the same gap multiset. That keeps
+the one-point distribution fixed and isolates ordering support.
+
+Measured for each perimeter:
+- canonical observables from observables_registry.py;
+- original-vs-shuffle z-score for each observable;
+- endpoint-stable observable set under |z| >= z_min;
+- endpoint classification using all observables and endpoint-gated observables;
+- beta layer ambiguity between coherent and illusory endpoints.
+"""
+
+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())
+PHI = (1.0 + 5.0**0.5) / 2.0
+
+
+def normalize(gaps: np.ndarray) -> np.ndarray:
+    gaps = np.asarray(gaps, dtype=float)
+    gaps = np.maximum(gaps, 1e-12)
+    mean = float(np.mean(gaps))
+    return gaps / mean if mean > 1e-15 else gaps
+
+
+def golden_beatty(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    phase = float(rng.random())
+    n = np.arange(n_gaps + 1, dtype=float) + phase
+    positions = np.floor(n * PHI)
+    return normalize(np.diff(positions))
+
+
+def periodic_triad(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    base = np.array([0.55, 1.0, 1.45, 1.0, 0.75, 1.25], dtype=float)
+    shift = int(rng.integers(0, len(base)))
+    tiled = np.tile(np.roll(base, shift), int(np.ceil(n_gaps / len(base))))[:n_gaps]
+    jitter = rng.normal(0.0, 0.015, size=n_gaps)
+    return normalize(tiled + jitter)
+
+
+def markov_alternating(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    vals = np.array([0.62, 1.38], dtype=float)
+    state = int(rng.integers(0, 2))
+    out = np.empty(n_gaps, dtype=float)
+    for i in range(n_gaps):
+        out[i] = vals[state] + rng.normal(0.0, 0.03)
+        if rng.random() < 0.88:
+            state = 1 - state
+    return normalize(out)
+
+
+def ar1_continuity(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    rho = 0.86
+    x = np.empty(n_gaps, dtype=float)
+    x[0] = rng.normal()
+    noise_scale = (1.0 - rho * rho) ** 0.5
+    for i in range(1, n_gaps):
+        x[i] = rho * x[i - 1] + noise_scale * rng.normal()
+    return normalize(np.exp(0.42 * x))
+
+
+PERIMETERS = {
+    "DUALITA_golden": golden_beatty,
+    "R_periodic_triad": periodic_triad,
+    "T_markov_alternating": markov_alternating,
+    "E_ar1_continuity": ar1_continuity,
+}
+
+
+def beta_layer(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        out = base.copy()
+    elif beta >= 1.0:
+        out = illusory.copy()
+    else:
+        mask = rng.random(len(base)) < beta
+        out = base.copy()
+        out[mask] = illusory[mask]
+    return normalize(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]]:
+    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])
+
+    shuffle_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
+        shuffle_std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, shuffle_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:
+    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)
+
+    coherent_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent_vectors, illusory_vectors])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent_vectors, axis=0)
+    illusory_centroid = np.mean(illusory_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((illusory_centroid - coherent_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_coherent = float(np.linalg.norm((x - coherent_centroid) / scale))
+            d_illusory = float(np.linalg.norm((x - illusory_centroid) / scale))
+            denom = d_coherent + d_illusory
+            coord = float((d_coherent - d_illusory) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_coherent - d_illusory) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("coherent" if d_coherent < d_illusory else "illusory")
+        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,
+            "illusory_label_fraction": float(np.mean(np.array(labels) == "illusory")),
+        }
+
+    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}
+        z_values = {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)
+                z_values[name].append(row["z"][name])
+        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()},
+            "z_mean": {name: float(np.mean(vals)) for name, vals in z_values.items()},
+        }
+
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    endpoint_one_sided = []
+    for name in OBS_NAMES:
+        coherent_freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in coherent_rows])
+        illusory_freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in illusory_rows])
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            endpoint_one_sided.append(name)
+        if coherent_freq >= 0.75 and illusory_freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "coherent_one_sided_observables": endpoint_one_sided,
+        "layers": layers,
+    }
+
+
+def analyze_perimeter(name: str, generator, args: argparse.Namespace, rng: np.random.Generator) -> dict:
+    rows = []
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    for rep in range(args.n_replicates):
+        base = generator(args.n_gaps, np.random.default_rng(rng.integers(0, 2**63 - 1)))
+        for beta in betas:
+            gaps = beta_layer(base, beta, np.random.default_rng(rng.integers(0, 2**63 - 1)))
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                args.n_baseline,
+                np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "perimeter": name,
+                    "replicate": rep,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [obs_name for obs_name in OBS_NAMES if abs(z[obs_name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    one_sided_classification = classify_layers(rows, gate["coherent_one_sided_observables"])
+    return {
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_one_sided_gated": one_sided_classification,
+        "rows": rows,
+    }
+
+
+def compact_matrix(perimeters: dict) -> dict:
+    matrix = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        class_all = data["classification_all_observables"]
+        class_gate = data["classification_one_sided_gated"]
+        matrix[name] = {
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "endpoint_distance_all": class_all["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": class_gate["endpoint_distance"],
+            "ambiguous_beta_all": class_all["ambiguous_beta"],
+            "ambiguous_beta_one_sided_gated": class_gate["ambiguous_beta"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+        }
+    return matrix
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    perimeters = {}
+    for name, generator in PERIMETERS.items():
+        perimeters[name] = analyze_perimeter(name, generator, args, root_rng)
+
+    output = {
+        "experiment": "denominator_gate_transfer_matrix",
+        "category": "gate_transferability",
+        "question": "Which parts of the denominator gate transfer outside BOUNDARY?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact_matrix(perimeters),
+        "perimeters": perimeters,
+    }
+
+    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("perimeter one_sided stable0 stable1 dist_all dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>21s} "
+            f"{','.join(row['coherent_one_sided_observables']) or '[]':>22s} "
+            f"{row['stable_count_coherent']:>7.3f} "
+            f"{row['stable_count_illusory']:>7.3f} "
+            f"{row['endpoint_distance_all']:>8.3f} "
+            f"{row['endpoint_distance_one_sided_gated']:>9.3f} "
+            f"{row['ambiguous_beta_one_sided_gated']}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=4096)
+    parser.add_argument("--n-replicates", type=int, default=20)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=32)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=202605070901)
+    parser.add_argument("--out", default="tools/data/denominator_gate_transfer_matrix.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

exec
/bin/bash -lc 'python tools/exp_denominator_gate_transfer_matrix.py --n-gaps 4096 --n-replicates 20 --n-beta 11 --n-baseline 32 --out tools/data/denominator_gate_transfer_matrix.json && python -m py_compile tools/exp_denominator_gate_transfer_matrix.py && python -m json.tool tools/data/seme.json >/tmp/seme.valid && python -m json.tool tools/data/denominator_gate_transfer_matrix.json >/tmp/gate.valid' in /opt/MM_D-ND
diff --git a/tools/data/reports/agent_20260507_0901.md b/tools/data/reports/agent_20260507_0901.md
new file mode 100644
index 0000000000000000000000000000000000000000..101695bfa907f5e680e8d408da1a95b66b7203f1
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0901.md
@@ -0,0 +1,122 @@
+# Agent Report — Denominator Gate Transfer Matrix
+
+timestamp: 2026-05-07 09:01 UTC
+category: gate_transferability_matrix
+verdict: operator
+observables_registry: 1.0.0-2026-05-06
+observables_used: [SR, SR2, L1, L2, triple_var]
+tool: tools/exp_denominator_gate_transfer_matrix.py
+data: tools/data/denominator_gate_transfer_matrix.json
+
+## Claim Under Test
+
+Verificato dal campo vivo: il cycle 0803 ha mostrato che il denominator gate
+trasferisce da BOUNDARY a DUALITA come operatore, ma non trasferisce la
+coordinata locale GUE/Poisson.
+
+Domanda di questo cycle: applicato a 4 perimetri non-BOUNDARY con poli
+coerente/illusorio a distribuzione marginale preservata, quali parti del gate
+trasferiscono e quali restano locali?
+
+Perimetri:
+
+- `DUALITA_golden`: sequenza Beatty aurea coerente vs permutazione illusoria.
+- `R_periodic_triad`: pattern periodico a 6 fasi vs permutazione illusoria.
+- `T_markov_alternating`: catena alternante low/high vs permutazione illusoria.
+- `E_ar1_continuity`: continuita AR(1) positiva vs permutazione illusoria.
+
+Parametri verificati: `n_gaps=4096`, `n_replicates=20`, `n_beta=11`,
+`n_baseline=32`, `z_min=2.0`, `seed=202605070901`.
+
+## Deposito Numerico
+
+| perimeter | coherent one-sided observables | stable_count coherent | stable_count illusory | endpoint distance gated | ambiguous beta gated |
+|---|---:|---:|---:|---:|---:|
+| DUALITA_golden | SR, L1, triple_var | 3.00 | 0.25 | 3.418 | 0.30 |
+| R_periodic_triad | SR, SR2, L1, L2, triple_var | 5.00 | 0.25 | 4.400 | 0.30 |
+| T_markov_alternating | SR, SR2, L1, L2, triple_var | 5.00 | 0.05 | 4.412 | 0.30 |
+| E_ar1_continuity | SR, SR2, L1, L2, triple_var | 5.00 | 0.60 | 4.394 | 0.30 |
+
+Endpoint-stable observables: `[]` in all 4 perimeters.
+
+Verificato: no observable is stable at both coherent and illusory endpoints.
+The denominator gate is not a two-sided endpoint support. It is one-sided
+support for generated order.
+
+## Risultato
+
+1. The gate transfers as one-sided coherence support.
+
+   In all 4 non-BOUNDARY perimeters, the coherent endpoint carries denominator
+   support and the illusory endpoint loses it. `DUALITA_golden` transfers on
+   `SR`, `L1`, `triple_var`; the three other perimeters transfer on all five
+   canonical observables.
+
+2. The both-endpoint stable set collapses everywhere.
+
+   `endpoint_stable_observables: []` is not a failure of the test. It is the
+   structural content: the gate does not name "two classes with stable
+   denominators"; it names the side where order survives its own shuffle null.
+
+3. The beta 0.30 ambiguity layer transfers as protocol coordinate, not as
+   BOUNDARY coordinate.
+
+   The ambiguous gated layer is beta `0.30` in all 4 perimeters. This is not a
+   GUE/Poisson layer. It is the coordinate created by the replacement protocol:
+   enough positions are illusory to place the layer near the endpoint bisector,
+   while enough coherent order remains to keep large z support.
+
+4. DUALITA is narrower than the other perimeters.
+
+   `DUALITA_golden` has one-sided support on 3/5 observables. `SR2` and `L2`
+   stay near shuffle at the coherent endpoint (`z_mean` about `0.12`). The
+   DUALITA gate transfers, but only through lag-1 and triple aggregation in this
+   synthetic perimeter.
+
+## Consecutio
+
+The next word is not `boundary_layer`. The correct category is:
+
+`order_denominator_gate`
+
+Definition: an original-vs-shuffle denominator gate where support is expected
+on the coherent/generated side, endpoint-stable observables may be empty, and
+the layer coordinate belongs to the perturbation protocol unless anchored to a
+domain-specific semantic axis.
+
+This extends the 0803 result:
+
+- 0803: BOUNDARY denominator gate transfers to DUALITA, BOUNDARY coordinate does not.
+- 0901: across 4 non-BOUNDARY perimeters, the transferable object is one-sided
+  order support; beta ambiguity is a protocol fold, not domain ontology.
+
+## Self-Audit: 5 Lenti
+
+L1 hard constraint vs bias: no "always", "never", or "zero" claim is used
+without exact support. `endpoint_stable_observables: []` is exact for this run
+and perimeters.
+
+L2 quantity vs ratio: the report uses raw stable counts, endpoint distances, and
+beta layers. Percent language is avoided except where the script computes
+fractions.
+
+L3 no silent patching: the scope changed from BOUNDARY to non-BOUNDARY
+perimeters explicitly. The new category `order_denominator_gate` is declared
+instead of stretching `BOUNDARY_LAYER_GATE`.
+
+L4 edge cases: `E_ar1_continuity` has illusory stable_count `0.60`, so the
+illusory endpoint is weak-denominator, not denominator-absent. The claim is
+one-sided support dominance, not exact absence.
+
+L5 re-discovery vs discovery: the numerical mechanism is an original-vs-shuffle
+order test on synthetic sequences. No NEW mathematical theorem is claimed.
+
+## Fonti
+
+- Verificato: `tools/data/agent_field_live.md`
+- Verificato: `tools/LAB_AGENT_CONTEXT.md`
+- Verificato: `tools/observables_registry.py`
+- Verificato: `tools/exp_denominator_gate_transfer_matrix.py`
+- Verificato: `tools/data/denominator_gate_transfer_matrix.json`
+- Inferito: `order_denominator_gate` as category needed by the downstream
+  vocabulary, from the cross-perimeter matrix.
diff --git a/tools/data/seme.json b/tools/data/seme.json
index 9dc9f5c8d087e9aea3266d5fefbc52a939acea27..47927a057e5396452b3c7982c8218c6e7c4d3b12
--- a/tools/data/seme.json
+++ b/tools/data/seme.json
@@ -132,6 +132,17 @@
       "_source_log": "2026-05-07T08:10:22.658201+00:00",
       "_source_decision": "REDESIGN",
       "_source_reasoning": "Il ciclo ha prodotto evidenza controllata e replicata che il denominator gate trasferisce come operatore, ma non trasferisce la coordinata di layer BOUNDARY: ambiguita' classificativa e collasso del denominatore si separano. Continuare sul seme attuale centrato su GUE/Poisson rischia di restare nel "
+    },
+    {
+      "tipo": "vincolo",
+      "id": "ORDER_DENOMINATOR_GATE",
+      "claim": "Il denominator gate trasferisce cross-perimetro come supporto one-sided dell'ordine generato, non come endpoint-stable support a due poli. Nel perimetro agent_20260507_0901, 4/4 domini non-BOUNDARY hanno endpoint_stable_observables=[]; il polo coerente ha stable_count 3.0-5.0, il polo illusorio 0.05-0.60. La beta ambigua 0.30 e coordinata del protocollo di perturbazione finche non viene ancorata a un asse semantico del dominio.",
+      "intensita": 0.92,
+      "porta": "META",
+      "manuale": true,
+      "condensato_ref": "A4,A8,A14,C2",
+      "origine": "cycle agent_20260507_0901: denominator gate transfer matrix su DUALITA_golden, R_periodic_triad, T_markov_alternating, E_ar1_continuity",
+      "added_at": "2026-05-07T09:01:00+00:00"
     }
   ],
   "potenziale_bloccato": [],
@@ -139,11 +150,11 @@
     "Tensioni risolte: {'PIANO_PRIMARIO_DUE_ASSIOMI', 'METRIC_TENSOR', 'BOUNDARY_LAYER_GATE', 'OBSERVABLE_REGISTRY', 'DUALITA_DIPOLARE_VS_ILLUSORIA', 'TENSIONE_ENTITA', 'PERTURBATION_DENOMINATOR_GATE', 'G_POTENZIALE_NULLA', 'TRASCENDENZA_LIMITE'}"
   ],
   "filtro": {
-    "promosse": 11,
+    "promosse": 12,
     "filtrate": 0,
     "condensato_claims": 30
   },
-  "direzione": "Costruire una matrice di trasferibilita' del denominator gate: applicarlo a 3-4 perimetri non-BOUNDARY con poli coerente/illusorio e verificare quali parti trasferiscono (supporto one-sided, coordinate di collasso, ambiguita' classificativa) e quali restano locali.",
+  "direzione": "Verificare se ORDER_DENOMINATOR_GATE resta one-sided su perimetri naturali/non sintetici: primes windows, logistic map, Fibonacci spectrum o dominio fisico gia nel lab. Separare supporto d'ordine, coordinata di protocollo e asse semantico del dominio.",
   "verifica": {
     "pass": 11,
     "fail": 0,
@@ -311,6 +322,7 @@
     }
   ],
   "_seed_history": {
-    "trajectory_apply_20260507_090145": "applied modify_seme from 20260507_0803: direzione → 'Costruire una matrice di trasferibilita' del denominator gate: applicarlo a 3-4 perimetri non-BOUNDA...'"
+    "trajectory_apply_20260507_090145": "applied modify_seme from 20260507_0803: direzione → 'Costruire una matrice di trasferibilita' del denominator gate: applicarlo a 3-4 perimetri non-BOUNDA...'",
+    "agent_20260507_0901": "added ORDER_DENOMINATOR_GATE; direzione -> test one-sided gate on natural/non-synthetic perimeters"
   }
-}
\ No newline at end of file
+}
diff --git a/tools/exp_denominator_gate_transfer_matrix.py b/tools/exp_denominator_gate_transfer_matrix.py
new file mode 100644
index 0000000000000000000000000000000000000000..f23cf7cf9505045e3d62ad95c8f3cddb38b31cef
--- /dev/null
+++ b/tools/exp_denominator_gate_transfer_matrix.py
@@ -0,0 +1,337 @@
+#!/usr/bin/env python3
+"""
+exp_denominator_gate_transfer_matrix.py
+
+Reusable META audit for the denominator gate transfer matrix.
+
+The experiment moves the original-vs-shuffle denominator gate away from the
+GUE/Poisson BOUNDARY perimeter. Each perimeter has a coherent endpoint and an
+illusory endpoint built as a permutation of the same gap multiset. That keeps
+the one-point distribution fixed and isolates ordering support.
+
+Measured for each perimeter:
+- canonical observables from observables_registry.py;
+- original-vs-shuffle z-score for each observable;
+- endpoint-stable observable set under |z| >= z_min;
+- endpoint classification using all observables and endpoint-gated observables;
+- beta layer ambiguity between coherent and illusory endpoints.
+"""
+
+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())
+PHI = (1.0 + 5.0**0.5) / 2.0
+
+
+def normalize(gaps: np.ndarray) -> np.ndarray:
+    gaps = np.asarray(gaps, dtype=float)
+    gaps = np.maximum(gaps, 1e-12)
+    mean = float(np.mean(gaps))
+    return gaps / mean if mean > 1e-15 else gaps
+
+
+def golden_beatty(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    phase = float(rng.random())
+    n = np.arange(n_gaps + 1, dtype=float) + phase
+    positions = np.floor(n * PHI)
+    return normalize(np.diff(positions))
+
+
+def periodic_triad(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    base = np.array([0.55, 1.0, 1.45, 1.0, 0.75, 1.25], dtype=float)
+    shift = int(rng.integers(0, len(base)))
+    tiled = np.tile(np.roll(base, shift), int(np.ceil(n_gaps / len(base))))[:n_gaps]
+    jitter = rng.normal(0.0, 0.015, size=n_gaps)
+    return normalize(tiled + jitter)
+
+
+def markov_alternating(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    vals = np.array([0.62, 1.38], dtype=float)
+    state = int(rng.integers(0, 2))
+    out = np.empty(n_gaps, dtype=float)
+    for i in range(n_gaps):
+        out[i] = vals[state] + rng.normal(0.0, 0.03)
+        if rng.random() < 0.88:
+            state = 1 - state
+    return normalize(out)
+
+
+def ar1_continuity(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    rho = 0.86
+    x = np.empty(n_gaps, dtype=float)
+    x[0] = rng.normal()
+    noise_scale = (1.0 - rho * rho) ** 0.5
+    for i in range(1, n_gaps):
+        x[i] = rho * x[i - 1] + noise_scale * rng.normal()
+    return normalize(np.exp(0.42 * x))
+
+
+PERIMETERS = {
+    "DUALITA_golden": golden_beatty,
+    "R_periodic_triad": periodic_triad,
+    "T_markov_alternating": markov_alternating,
+    "E_ar1_continuity": ar1_continuity,
+}
+
+
+def beta_layer(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        out = base.copy()
+    elif beta >= 1.0:
+        out = illusory.copy()
+    else:
+        mask = rng.random(len(base)) < beta
+        out = base.copy()
+        out[mask] = illusory[mask]
+    return normalize(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]]:
+    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])
+
+    shuffle_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
+        shuffle_std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, shuffle_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:
+    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)
+
+    coherent_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent_vectors, illusory_vectors])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent_vectors, axis=0)
+    illusory_centroid = np.mean(illusory_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((illusory_centroid - coherent_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_coherent = float(np.linalg.norm((x - coherent_centroid) / scale))
+            d_illusory = float(np.linalg.norm((x - illusory_centroid) / scale))
+            denom = d_coherent + d_illusory
+            coord = float((d_coherent - d_illusory) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_coherent - d_illusory) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("coherent" if d_coherent < d_illusory else "illusory")
+        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,
+            "illusory_label_fraction": float(np.mean(np.array(labels) == "illusory")),
+        }
+
+    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}
+        z_values = {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)
+                z_values[name].append(row["z"][name])
+        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()},
+            "z_mean": {name: float(np.mean(vals)) for name, vals in z_values.items()},
+        }
+
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    endpoint_one_sided = []
+    for name in OBS_NAMES:
+        coherent_freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in coherent_rows])
+        illusory_freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in illusory_rows])
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            endpoint_one_sided.append(name)
+        if coherent_freq >= 0.75 and illusory_freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "coherent_one_sided_observables": endpoint_one_sided,
+        "layers": layers,
+    }
+
+
+def analyze_perimeter(name: str, generator, args: argparse.Namespace, rng: np.random.Generator) -> dict:
+    rows = []
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    for rep in range(args.n_replicates):
+        base = generator(args.n_gaps, np.random.default_rng(rng.integers(0, 2**63 - 1)))
+        for beta in betas:
+            gaps = beta_layer(base, beta, np.random.default_rng(rng.integers(0, 2**63 - 1)))
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                args.n_baseline,
+                np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "perimeter": name,
+                    "replicate": rep,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [obs_name for obs_name in OBS_NAMES if abs(z[obs_name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    one_sided_classification = classify_layers(rows, gate["coherent_one_sided_observables"])
+    return {
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_one_sided_gated": one_sided_classification,
+        "rows": rows,
+    }
+
+
+def compact_matrix(perimeters: dict) -> dict:
+    matrix = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        class_all = data["classification_all_observables"]
+        class_gate = data["classification_one_sided_gated"]
+        matrix[name] = {
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "endpoint_distance_all": class_all["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": class_gate["endpoint_distance"],
+            "ambiguous_beta_all": class_all["ambiguous_beta"],
+            "ambiguous_beta_one_sided_gated": class_gate["ambiguous_beta"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+        }
+    return matrix
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    perimeters = {}
+    for name, generator in PERIMETERS.items():
+        perimeters[name] = analyze_perimeter(name, generator, args, root_rng)
+
+    output = {
+        "experiment": "denominator_gate_transfer_matrix",
+        "category": "gate_transferability",
+        "question": "Which parts of the denominator gate transfer outside BOUNDARY?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact_matrix(perimeters),
+        "perimeters": perimeters,
+    }
+
+    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("perimeter one_sided stable0 stable1 dist_all dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>21s} "
+            f"{','.join(row['coherent_one_sided_observables']) or '[]':>22s} "
+            f"{row['stable_count_coherent']:>7.3f} "
+            f"{row['stable_count_illusory']:>7.3f} "
+            f"{row['endpoint_distance_all']:>8.3f} "
+            f"{row['endpoint_distance_one_sided_gated']:>9.3f} "
+            f"{row['ambiguous_beta_one_sided_gated']}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=4096)
+    parser.add_argument("--n-replicates", type=int, default=20)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=32)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=202605070901)
+    parser.add_argument("--out", default="tools/data/denominator_gate_transfer_matrix.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

diff --git a/tools/data/reports/agent_20260507_0901.md b/tools/data/reports/agent_20260507_0901.md
new file mode 100644
index 0000000000000000000000000000000000000000..101695bfa907f5e680e8d408da1a95b66b7203f1
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0901.md
@@ -0,0 +1,122 @@
+# Agent Report — Denominator Gate Transfer Matrix
+
+timestamp: 2026-05-07 09:01 UTC
+category: gate_transferability_matrix
+verdict: operator
+observables_registry: 1.0.0-2026-05-06
+observables_used: [SR, SR2, L1, L2, triple_var]
+tool: tools/exp_denominator_gate_transfer_matrix.py
+data: tools/data/denominator_gate_transfer_matrix.json
+
+## Claim Under Test
+
+Verificato dal campo vivo: il cycle 0803 ha mostrato che il denominator gate
+trasferisce da BOUNDARY a DUALITA come operatore, ma non trasferisce la
+coordinata locale GUE/Poisson.
+
+Domanda di questo cycle: applicato a 4 perimetri non-BOUNDARY con poli
+coerente/illusorio a distribuzione marginale preservata, quali parti del gate
+trasferiscono e quali restano locali?
+
+Perimetri:
+
+- `DUALITA_golden`: sequenza Beatty aurea coerente vs permutazione illusoria.
+- `R_periodic_triad`: pattern periodico a 6 fasi vs permutazione illusoria.
+- `T_markov_alternating`: catena alternante low/high vs permutazione illusoria.
+- `E_ar1_continuity`: continuita AR(1) positiva vs permutazione illusoria.
+
+Parametri verificati: `n_gaps=4096`, `n_replicates=20`, `n_beta=11`,
+`n_baseline=32`, `z_min=2.0`, `seed=202605070901`.
+
+## Deposito Numerico
+
+| perimeter | coherent one-sided observables | stable_count coherent | stable_count illusory | endpoint distance gated | ambiguous beta gated |
+|---|---:|---:|---:|---:|---:|
+| DUALITA_golden | SR, L1, triple_var | 3.00 | 0.25 | 3.418 | 0.30 |
+| R_periodic_triad | SR, SR2, L1, L2, triple_var | 5.00 | 0.25 | 4.400 | 0.30 |
+| T_markov_alternating | SR, SR2, L1, L2, triple_var | 5.00 | 0.05 | 4.412 | 0.30 |
+| E_ar1_continuity | SR, SR2, L1, L2, triple_var | 5.00 | 0.60 | 4.394 | 0.30 |
+
+Endpoint-stable observables: `[]` in all 4 perimeters.
+
+Verificato: no observable is stable at both coherent and illusory endpoints.
+The denominator gate is not a two-sided endpoint support. It is one-sided
+support for generated order.
+
+## Risultato
+
+1. The gate transfers as one-sided coherence support.
+
+   In all 4 non-BOUNDARY perimeters, the coherent endpoint carries denominator
+   support and the illusory endpoint loses it. `DUALITA_golden` transfers on
+   `SR`, `L1`, `triple_var`; the three other perimeters transfer on all five
+   canonical observables.
+
+2. The both-endpoint stable set collapses everywhere.
+
+   `endpoint_stable_observables: []` is not a failure of the test. It is the
+   structural content: the gate does not name "two classes with stable
+   denominators"; it names the side where order survives its own shuffle null.
+
+3. The beta 0.30 ambiguity layer transfers as protocol coordinate, not as
+   BOUNDARY coordinate.
+
+   The ambiguous gated layer is beta `0.30` in all 4 perimeters. This is not a
+   GUE/Poisson layer. It is the coordinate created by the replacement protocol:
+   enough positions are illusory to place the layer near the endpoint bisector,
+   while enough coherent order remains to keep large z support.
+
+4. DUALITA is narrower than the other perimeters.
+
+   `DUALITA_golden` has one-sided support on 3/5 observables. `SR2` and `L2`
+   stay near shuffle at the coherent endpoint (`z_mean` about `0.12`). The
+   DUALITA gate transfers, but only through lag-1 and triple aggregation in this
+   synthetic perimeter.
+
+## Consecutio
+
+The next word is not `boundary_layer`. The correct category is:
+
+`order_denominator_gate`
+
+Definition: an original-vs-shuffle denominator gate where support is expected
+on the coherent/generated side, endpoint-stable observables may be empty, and
+the layer coordinate belongs to the perturbation protocol unless anchored to a
+domain-specific semantic axis.
+
+This extends the 0803 result:
+
+- 0803: BOUNDARY denominator gate transfers to DUALITA, BOUNDARY coordinate does not.
+- 0901: across 4 non-BOUNDARY perimeters, the transferable object is one-sided
+  order support; beta ambiguity is a protocol fold, not domain ontology.
+
+## Self-Audit: 5 Lenti
+
+L1 hard constraint vs bias: no "always", "never", or "zero" claim is used
+without exact support. `endpoint_stable_observables: []` is exact for this run
+and perimeters.
+
+L2 quantity vs ratio: the report uses raw stable counts, endpoint distances, and
+beta layers. Percent language is avoided except where the script computes
+fractions.
+
+L3 no silent patching: the scope changed from BOUNDARY to non-BOUNDARY
+perimeters explicitly. The new category `order_denominator_gate` is declared
+instead of stretching `BOUNDARY_LAYER_GATE`.
+
+L4 edge cases: `E_ar1_continuity` has illusory stable_count `0.60`, so the
+illusory endpoint is weak-denominator, not denominator-absent. The claim is
+one-sided support dominance, not exact absence.
+
+L5 re-discovery vs discovery: the numerical mechanism is an original-vs-shuffle
+order test on synthetic sequences. No NEW mathematical theorem is claimed.
+
+## Fonti
+
+- Verificato: `tools/data/agent_field_live.md`
+- Verificato: `tools/LAB_AGENT_CONTEXT.md`
+- Verificato: `tools/observables_registry.py`
+- Verificato: `tools/exp_denominator_gate_transfer_matrix.py`
+- Verificato: `tools/data/denominator_gate_transfer_matrix.json`
+- Inferito: `order_denominator_gate` as category needed by the downstream
+  vocabulary, from the cross-perimeter matrix.
diff --git a/tools/data/seme.json b/tools/data/seme.json
index 9dc9f5c8d087e9aea3266d5fefbc52a939acea27..47927a057e5396452b3c7982c8218c6e7c4d3b12
--- a/tools/data/seme.json
+++ b/tools/data/seme.json
@@ -132,6 +132,17 @@
       "_source_log": "2026-05-07T08:10:22.658201+00:00",
       "_source_decision": "REDESIGN",
       "_source_reasoning": "Il ciclo ha prodotto evidenza controllata e replicata che il denominator gate trasferisce come operatore, ma non trasferisce la coordinata di layer BOUNDARY: ambiguita' classificativa e collasso del denominatore si separano. Continuare sul seme attuale centrato su GUE/Poisson rischia di restare nel "
+    },
+    {
+      "tipo": "vincolo",
+      "id": "ORDER_DENOMINATOR_GATE",
+      "claim": "Il denominator gate trasferisce cross-perimetro come supporto one-sided dell'ordine generato, non come endpoint-stable support a due poli. Nel perimetro agent_20260507_0901, 4/4 domini non-BOUNDARY hanno endpoint_stable_observables=[]; il polo coerente ha stable_count 3.0-5.0, il polo illusorio 0.05-0.60. La beta ambigua 0.30 e coordinata del protocollo di perturbazione finche non viene ancorata a un asse semantico del dominio.",
+      "intensita": 0.92,
+      "porta": "META",
+      "manuale": true,
+      "condensato_ref": "A4,A8,A14,C2",
+      "origine": "cycle agent_20260507_0901: denominator gate transfer matrix su DUALITA_golden, R_periodic_triad, T_markov_alternating, E_ar1_continuity",
+      "added_at": "2026-05-07T09:01:00+00:00"
     }
   ],
   "potenziale_bloccato": [],
@@ -139,11 +150,11 @@
     "Tensioni risolte: {'PIANO_PRIMARIO_DUE_ASSIOMI', 'METRIC_TENSOR', 'BOUNDARY_LAYER_GATE', 'OBSERVABLE_REGISTRY', 'DUALITA_DIPOLARE_VS_ILLUSORIA', 'TENSIONE_ENTITA', 'PERTURBATION_DENOMINATOR_GATE', 'G_POTENZIALE_NULLA', 'TRASCENDENZA_LIMITE'}"
   ],
   "filtro": {
-    "promosse": 11,
+    "promosse": 12,
     "filtrate": 0,
     "condensato_claims": 30
   },
-  "direzione": "Costruire una matrice di trasferibilita' del denominator gate: applicarlo a 3-4 perimetri non-BOUNDARY con poli coerente/illusorio e verificare quali parti trasferiscono (supporto one-sided, coordinate di collasso, ambiguita' classificativa) e quali restano locali.",
+  "direzione": "Verificare se ORDER_DENOMINATOR_GATE resta one-sided su perimetri naturali/non sintetici: primes windows, logistic map, Fibonacci spectrum o dominio fisico gia nel lab. Separare supporto d'ordine, coordinata di protocollo e asse semantico del dominio.",
   "verifica": {
     "pass": 11,
     "fail": 0,
@@ -311,6 +322,7 @@
     }
   ],
   "_seed_history": {
-    "trajectory_apply_20260507_090145": "applied modify_seme from 20260507_0803: direzione → 'Costruire una matrice di trasferibilita' del denominator gate: applicarlo a 3-4 perimetri non-BOUNDA...'"
+    "trajectory_apply_20260507_090145": "applied modify_seme from 20260507_0803: direzione → 'Costruire una matrice di trasferibilita' del denominator gate: applicarlo a 3-4 perimetri non-BOUNDA...'",
+    "agent_20260507_0901": "added ORDER_DENOMINATOR_GATE; direzione -> test one-sided gate on natural/non-synthetic perimeters"
   }
-}
\ No newline at end of file
+}
diff --git a/tools/exp_denominator_gate_transfer_matrix.py b/tools/exp_denominator_gate_transfer_matrix.py
new file mode 100644
index 0000000000000000000000000000000000000000..f23cf7cf9505045e3d62ad95c8f3cddb38b31cef
--- /dev/null
+++ b/tools/exp_denominator_gate_transfer_matrix.py
@@ -0,0 +1,337 @@
+#!/usr/bin/env python3
+"""
+exp_denominator_gate_transfer_matrix.py
+
+Reusable META audit for the denominator gate transfer matrix.
+
+The experiment moves the original-vs-shuffle denominator gate away from the
+GUE/Poisson BOUNDARY perimeter. Each perimeter has a coherent endpoint and an
+illusory endpoint built as a permutation of the same gap multiset. That keeps
+the one-point distribution fixed and isolates ordering support.
+
+Measured for each perimeter:
+- canonical observables from observables_registry.py;
+- original-vs-shuffle z-score for each observable;
+- endpoint-stable observable set under |z| >= z_min;
+- endpoint classification using all observables and endpoint-gated observables;
+- beta layer ambiguity between coherent and illusory endpoints.
+"""
+
+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())
+PHI = (1.0 + 5.0**0.5) / 2.0
+
+
+def normalize(gaps: np.ndarray) -> np.ndarray:
+    gaps = np.asarray(gaps, dtype=float)
+    gaps = np.maximum(gaps, 1e-12)
+    mean = float(np.mean(gaps))
+    return gaps / mean if mean > 1e-15 else gaps
+
+
+def golden_beatty(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    phase = float(rng.random())
+    n = np.arange(n_gaps + 1, dtype=float) + phase
+    positions = np.floor(n * PHI)
+    return normalize(np.diff(positions))
+
+
+def periodic_triad(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    base = np.array([0.55, 1.0, 1.45, 1.0, 0.75, 1.25], dtype=float)
+    shift = int(rng.integers(0, len(base)))
+    tiled = np.tile(np.roll(base, shift), int(np.ceil(n_gaps / len(base))))[:n_gaps]
+    jitter = rng.normal(0.0, 0.015, size=n_gaps)
+    return normalize(tiled + jitter)
+
+
+def markov_alternating(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    vals = np.array([0.62, 1.38], dtype=float)
+    state = int(rng.integers(0, 2))
+    out = np.empty(n_gaps, dtype=float)
+    for i in range(n_gaps):
+        out[i] = vals[state] + rng.normal(0.0, 0.03)
+        if rng.random() < 0.88:
+            state = 1 - state
+    return normalize(out)
+
+
+def ar1_continuity(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    rho = 0.86
+    x = np.empty(n_gaps, dtype=float)
+    x[0] = rng.normal()
+    noise_scale = (1.0 - rho * rho) ** 0.5
+    for i in range(1, n_gaps):
+        x[i] = rho * x[i - 1] + noise_scale * rng.normal()
+    return normalize(np.exp(0.42 * x))
+
+
+PERIMETERS = {
+    "DUALITA_golden": golden_beatty,
+    "R_periodic_triad": periodic_triad,
+    "T_markov_alternating": markov_alternating,
+    "E_ar1_continuity": ar1_continuity,
+}
+
+
+def beta_layer(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        out = base.copy()
+    elif beta >= 1.0:
+        out = illusory.copy()
+    else:
+        mask = rng.random(len(base)) < beta
+        out = base.copy()
+        out[mask] = illusory[mask]
+    return normalize(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]]:
+    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])
+
+    shuffle_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
+        shuffle_std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, shuffle_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:
+    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)
+
+    coherent_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent_vectors, illusory_vectors])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent_vectors, axis=0)
+    illusory_centroid = np.mean(illusory_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((illusory_centroid - coherent_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_coherent = float(np.linalg.norm((x - coherent_centroid) / scale))
+            d_illusory = float(np.linalg.norm((x - illusory_centroid) / scale))
+            denom = d_coherent + d_illusory
+            coord = float((d_coherent - d_illusory) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_coherent - d_illusory) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("coherent" if d_coherent < d_illusory else "illusory")
+        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,
+            "illusory_label_fraction": float(np.mean(np.array(labels) == "illusory")),
+        }
+
+    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}
+        z_values = {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)
+                z_values[name].append(row["z"][name])
+        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()},
+            "z_mean": {name: float(np.mean(vals)) for name, vals in z_values.items()},
+        }
+
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    endpoint_one_sided = []
+    for name in OBS_NAMES:
+        coherent_freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in coherent_rows])
+        illusory_freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in illusory_rows])
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            endpoint_one_sided.append(name)
+        if coherent_freq >= 0.75 and illusory_freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "coherent_one_sided_observables": endpoint_one_sided,
+        "layers": layers,
+    }
+
+
+def analyze_perimeter(name: str, generator, args: argparse.Namespace, rng: np.random.Generator) -> dict:
+    rows = []
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    for rep in range(args.n_replicates):
+        base = generator(args.n_gaps, np.random.default_rng(rng.integers(0, 2**63 - 1)))
+        for beta in betas:
+            gaps = beta_layer(base, beta, np.random.default_rng(rng.integers(0, 2**63 - 1)))
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                args.n_baseline,
+                np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "perimeter": name,
+                    "replicate": rep,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [obs_name for obs_name in OBS_NAMES if abs(z[obs_name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    one_sided_classification = classify_layers(rows, gate["coherent_one_sided_observables"])
+    return {
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_one_sided_gated": one_sided_classification,
+        "rows": rows,
+    }
+
+
+def compact_matrix(perimeters: dict) -> dict:
+    matrix = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        class_all = data["classification_all_observables"]
+        class_gate = data["classification_one_sided_gated"]
+        matrix[name] = {
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "endpoint_distance_all": class_all["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": class_gate["endpoint_distance"],
+            "ambiguous_beta_all": class_all["ambiguous_beta"],
+            "ambiguous_beta_one_sided_gated": class_gate["ambiguous_beta"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+        }
+    return matrix
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    perimeters = {}
+    for name, generator in PERIMETERS.items():
+        perimeters[name] = analyze_perimeter(name, generator, args, root_rng)
+
+    output = {
+        "experiment": "denominator_gate_transfer_matrix",
+        "category": "gate_transferability",
+        "question": "Which parts of the denominator gate transfer outside BOUNDARY?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact_matrix(perimeters),
+        "perimeters": perimeters,
+    }
+
+    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("perimeter one_sided stable0 stable1 dist_all dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>21s} "
+            f"{','.join(row['coherent_one_sided_observables']) or '[]':>22s} "
+            f"{row['stable_count_coherent']:>7.3f} "
+            f"{row['stable_count_illusory']:>7.3f} "
+            f"{row['endpoint_distance_all']:>8.3f} "
+            f"{row['endpoint_distance_one_sided_gated']:>9.3f} "
+            f"{row['ambiguous_beta_one_sided_gated']}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=4096)
+    parser.add_argument("--n-replicates", type=int, default=20)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=32)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=202605070901)
+    parser.add_argument("--out", default="tools/data/denominator_gate_transfer_matrix.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

 succeeded in 4540ms:
observables_registry=1.0.0-2026-05-06
observables_used=['SR', 'SR2', 'L1', 'L2', 'triple_var']
perimeter one_sided stable0 stable1 dist_all dist_gate ambiguous_gate
       DUALITA_golden       SR,L1,triple_var   3.000   0.250    3.422     3.418 [0.30000000000000004]
     R_periodic_triad SR,SR2,L1,L2,triple_var   5.000   0.250    4.400     4.400 [0.30000000000000004]
 T_markov_alternating SR,SR2,L1,L2,triple_var   5.000   0.050    4.412     4.412 [0.30000000000000004]
     E_ar1_continuity SR,SR2,L1,L2,triple_var   5.000   0.600    4.394     4.394 [0.30000000000000004]
saved tools/data/denominator_gate_transfer_matrix.json

exec
/bin/bash -lc "git diff -- tools/exp_denominator_gate_transfer_matrix.py tools/data/reports/agent_20260507_0901.md tools/data/seme.json | 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..47927a0 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-07T08:09:58.685756",
+  "piano": 70,
   "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,22 +117,44 @@
       "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"
+    },
+    {
+      "tipo": "task",
+      "id": "TRAJECTORY_APPLY_20260507_0803",
+      "claim": "Applied valutatore REDESIGN from 20260507_0803: Costruire una matrice di trasferibilita' del denominator gate: applicarlo a 3-4 perimetri non-BOUNDARY con poli coerente/illusorio e verificare quali parti trasferiscono (supporto one-sided, coordinat",
+      "intensità": 0.7,
+      "porta": "trajectory_apply",
+      "condensato_ref": "A8,A14,A15",
+      "manuale": true,
+      "_source_log": "2026-05-07T08:10:22.658201+00:00",
+      "_source_decision": "REDESIGN",
+      "_source_reasoning": "Il ciclo ha prodotto evidenza controllata e replicata che il denominator gate trasferisce come operatore, ma non trasferisce la coordinata di layer BOUNDARY: ambiguita' classificativa e collasso del denominatore si separano. Continuare sul seme attuale centrato su GUE/Poisson rischia di restare nel "
+    },
+    {
+      "tipo": "vincolo",
+      "id": "ORDER_DENOMINATOR_GATE",
+      "claim": "Il denominator gate trasferisce cross-perimetro come supporto one-sided dell'ordine generato, non come endpoint-stable support a due poli. Nel perimetro agent_20260507_0901, 4/4 domini non-BOUNDARY hanno endpoint_stable_observables=[]; il polo coerente ha stable_count 3.0-5.0, il polo illusorio 0.05-0.60. La beta ambigua 0.30 e coordinata del protocollo di perturbazione finche non viene ancorata a un asse semantico del dominio.",
+      "intensita": 0.92,
+      "porta": "META",
+      "manuale": true,
+      "condensato_ref": "A4,A8,A14,C2",
+      "origine": "cycle agent_20260507_0901: denominator gate transfer matrix su DUALITA_golden, R_periodic_triad, T_markov_alternating, E_ar1_continuity",
+      "added_at": "2026-05-07T09:01:00+00:00"
     }
   ],
   "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: {'PIANO_PRIMARIO_DUE_ASSIOMI', 'METRIC_TENSOR', 'BOUNDARY_LAYER_GATE', 'OBSERVABLE_REGISTRY', 'DUALITA_DIPOLARE_VS_ILLUSORIA', 'TENSIONE_ENTITA', 'PERTURBATION_DENOMINATOR_GATE', 'G_POTENZIALE_NULLA', 'TRASCENDENZA_LIMITE'}"
   ],
   "filtro": {
-    "promosse": 8,
+    "promosse": 12,
     "filtrate": 0,
     "condensato_claims": 30
   },
-  "direzione": "Esplorare il confine: 8 domini GUE, 5 Poisson — il confine è il terzo incluso operativo",
+  "direzione": "Verificare se ORDER_DENOMINATOR_GATE resta one-sided su perimetri naturali/non sintetici: primes windows, logistic map, Fibonacci spectrum o dominio fisico gia nel lab. Separare supporto d'ordine, coordinata di protocollo e asse semantico del dominio.",
   "verifica": {
     "pass": 11,
     "fail": 0,
@@ -265,5 +320,9 @@
       "ref": "Physical Review Letters 2025, 135(15). Chen, Zhu, Ratkovski et al. PI: Lu Li",
       "timestamp": "2026-04-04T19:34:08.014Z"
     }
-  ]
+  ],
+  "_seed_history": {
+    "trajectory_apply_20260507_090145": "applied modify_seme from 20260507_0803: direzione → 'Costruire una matrice di trasferibilita' del denominator gate: applicarlo a 3-4 perimetri non-BOUNDA...'",
+    "agent_20260507_0901": "added ORDER_DENOMINATOR_GATE; direzione -> test one-sided gate on natural/non-synthetic perimeters"
+  }
 }

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/published/20260507_0330_the-gue-poisson-boundary-is-a/
?? applications/published/20260507_0803_denominator-gate-transfers-boundary-coordinate-does/
?? 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/scoperte/20260507_0330_the-gue-poisson-boundary-is-a_auto/
?? applications/scoperte/20260507_0803_denominator-gate-transfers-boundary-coordinate-does_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/
?? applications/soluzioni/20260507_0330_the-gue-poisson-boundary-is-a/
?? applications/soluzioni/20260507_0803_denominator-gate-transfers-boundary-coordinate-does/
?? 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_mixture_gate_20260507_0330_seedcheck.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/denominator_gate_transfer_matrix.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/domandatore/domandatore_20260507_0345.json
?? tools/data/domandatore/domandatore_20260507_0728.json
?? tools/data/domandatore/domandatore_20260507_0742.json
?? tools/data/duality_gate_transfer_20260507_0803.json
?? tools/data/duality_gate_transfer_20260507_0803_seedcheck.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/evolution/evolution_20260507_0330.md
?? tools/data/evolution/evolution_20260507_0803.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/agent_20260507_0330.md
?? tools/data/reports/agent_20260507_0803.md
?? tools/data/reports/agent_20260507_0901.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/ddf_20260507_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/falsifier_20260507_0330.json
?? tools/data/reports/falsifier_20260507_0330.raw.txt
?? tools/data/reports/falsifier_20260507_0803.json
?? tools/data/reports/falsifier_20260507_0803.raw.txt
?? tools/data/reports/incident_20260504_0721.md
?? tools/data/reports/incident_20260504_1138.md
?? tools/data/reports/latest.md
?? tools/data/reports/loop_guard_20260507_0330.json
?? tools/data/reports/loop_guard_20260507_0803.json
?? 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/reports/trajectory_apply_20260507_0803.json
?? tools/data/reports/trajectory_apply_20260507_0901.json
?? tools/data/scale_selective_perturbation.json
?? tools/data/selective_layer_decoupling.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_68.json
?? tools/data/seme_archive/piano_69.json
?? tools/data/spectral_rigidity_results.json
?? tools/data/trajectory_apply_history.jsonl
?? 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/evolution_report.md
?? 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_denominator_gate_transfer_matrix.py
?? tools/exp_dipolar_angle_reference.py
?? tools/exp_dipolar_crossover.py
?? tools/exp_dipolar_vector_scaling.py
?? tools/exp_duality_gate_transfer.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 - <<'PY'
import json
p='tools/data/denominator_gate_transfer_matrix.json'
d=json.load(open(p))
print(json.dumps(d['matrix'], indent=2))
PY" in /opt/MM_D-ND
 succeeded in 0ms:
{
  "DUALITA_golden": {
    "coherent_one_sided_observables": [
      "SR",
      "L1",
      "triple_var"
    ],
    "endpoint_stable_observables": [],
    "endpoint_distance_all": 3.4215888194133655,
    "endpoint_distance_one_sided_gated": 3.4182025126759505,
    "ambiguous_beta_all": [
      0.30000000000000004
    ],
    "ambiguous_beta_one_sided_gated": [
      0.30000000000000004
    ],
    "stable_count_coherent": 3.0,
    "stable_count_illusory": 0.25
  },
  "R_periodic_triad": {
    "coherent_one_sided_observables": [
      "SR",
      "SR2",
      "L1",
      "L2",
      "triple_var"
    ],
    "endpoint_stable_observables": [],
    "endpoint_distance_all": 4.400058194601154,
    "endpoint_distance_one_sided_gated": 4.400058194601154,
    "ambiguous_beta_all": [
      0.30000000000000004
    ],
    "ambiguous_beta_one_sided_gated": [
      0.30000000000000004
    ],
    "stable_count_coherent": 5.0,
    "stable_count_illusory": 0.25
  },
  "T_markov_alternating": {
    "coherent_one_sided_observables": [
      "SR",
      "SR2",
      "L1",
      "L2",
      "triple_var"
    ],
    "endpoint_stable_observables": [],
    "endpoint_distance_all": 4.412461534213811,
    "endpoint_distance_one_sided_gated": 4.412461534213811,
    "ambiguous_beta_all": [
      0.30000000000000004
    ],
    "ambiguous_beta_one_sided_gated": [
      0.30000000000000004
    ],
    "stable_count_coherent": 5.0,
    "stable_count_illusory": 0.05
  },
  "E_ar1_continuity": {
    "coherent_one_sided_observables": [
      "SR",
      "SR2",
      "L1",
      "L2",
      "triple_var"
    ],
    "endpoint_stable_observables": [],
    "endpoint_distance_all": 4.39407612238746,
    "endpoint_distance_one_sided_gated": 4.39407612238746,
    "ambiguous_beta_all": [
      0.30000000000000004
    ],
    "ambiguous_beta_one_sided_gated": [
      0.30000000000000004
    ],
    "stable_count_coherent": 5.0,
    "stable_count_illusory": 0.6
  }
}

diff --git a/tools/data/reports/agent_20260507_0901.md b/tools/data/reports/agent_20260507_0901.md
new file mode 100644
index 0000000000000000000000000000000000000000..101695bfa907f5e680e8d408da1a95b66b7203f1
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0901.md
@@ -0,0 +1,122 @@
+# Agent Report — Denominator Gate Transfer Matrix
+
+timestamp: 2026-05-07 09:01 UTC
+category: gate_transferability_matrix
+verdict: operator
+observables_registry: 1.0.0-2026-05-06
+observables_used: [SR, SR2, L1, L2, triple_var]
+tool: tools/exp_denominator_gate_transfer_matrix.py
+data: tools/data/denominator_gate_transfer_matrix.json
+
+## Claim Under Test
+
+Verificato dal campo vivo: il cycle 0803 ha mostrato che il denominator gate
+trasferisce da BOUNDARY a DUALITA come operatore, ma non trasferisce la
+coordinata locale GUE/Poisson.
+
+Domanda di questo cycle: applicato a 4 perimetri non-BOUNDARY con poli
+coerente/illusorio a distribuzione marginale preservata, quali parti del gate
+trasferiscono e quali restano locali?
+
+Perimetri:
+
+- `DUALITA_golden`: sequenza Beatty aurea coerente vs permutazione illusoria.
+- `R_periodic_triad`: pattern periodico a 6 fasi vs permutazione illusoria.
+- `T_markov_alternating`: catena alternante low/high vs permutazione illusoria.
+- `E_ar1_continuity`: continuita AR(1) positiva vs permutazione illusoria.
+
+Parametri verificati: `n_gaps=4096`, `n_replicates=20`, `n_beta=11`,
+`n_baseline=32`, `z_min=2.0`, `seed=202605070901`.
+
+## Deposito Numerico
+
+| perimeter | coherent one-sided observables | stable_count coherent | stable_count illusory | endpoint distance gated | ambiguous beta gated |
+|---|---:|---:|---:|---:|---:|
+| DUALITA_golden | SR, L1, triple_var | 3.00 | 0.25 | 3.418 | 0.30 |
+| R_periodic_triad | SR, SR2, L1, L2, triple_var | 5.00 | 0.25 | 4.400 | 0.30 |
+| T_markov_alternating | SR, SR2, L1, L2, triple_var | 5.00 | 0.05 | 4.412 | 0.30 |
+| E_ar1_continuity | SR, SR2, L1, L2, triple_var | 5.00 | 0.60 | 4.394 | 0.30 |
+
+Endpoint-stable observables: `[]` in all 4 perimeters.
+
+Verificato: no observable is stable at both coherent and illusory endpoints.
+The denominator gate is not a two-sided endpoint support. It is one-sided
+support for generated order.
+
+## Risultato
+
+1. The gate transfers as one-sided coherence support.
+
+   In all 4 non-BOUNDARY perimeters, the coherent endpoint carries denominator
+   support and the illusory endpoint loses it. `DUALITA_golden` transfers on
+   `SR`, `L1`, `triple_var`; the three other perimeters transfer on all five
+   canonical observables.
+
+2. The both-endpoint stable set collapses everywhere.
+
+   `endpoint_stable_observables: []` is not a failure of the test. It is the
+   structural content: the gate does not name "two classes with stable
+   denominators"; it names the side where order survives its own shuffle null.
+
+3. The beta 0.30 ambiguity layer transfers as protocol coordinate, not as
+   BOUNDARY coordinate.
+
+   The ambiguous gated layer is beta `0.30` in all 4 perimeters. This is not a
+   GUE/Poisson layer. It is the coordinate created by the replacement protocol:
+   enough positions are illusory to place the layer near the endpoint bisector,
+   while enough coherent order remains to keep large z support.
+
+4. DUALITA is narrower than the other perimeters.
+
+   `DUALITA_golden` has one-sided support on 3/5 observables. `SR2` and `L2`
+   stay near shuffle at the coherent endpoint (`z_mean` about `0.12`). The
+   DUALITA gate transfers, but only through lag-1 and triple aggregation in this
+   synthetic perimeter.
+
+## Consecutio
+
+The next word is not `boundary_layer`. The correct category is:
+
+`order_denominator_gate`
+
+Definition: an original-vs-shuffle denominator gate where support is expected
+on the coherent/generated side, endpoint-stable observables may be empty, and
+the layer coordinate belongs to the perturbation protocol unless anchored to a
+domain-specific semantic axis.
+
+This extends the 0803 result:
+
+- 0803: BOUNDARY denominator gate transfers to DUALITA, BOUNDARY coordinate does not.
+- 0901: across 4 non-BOUNDARY perimeters, the transferable object is one-sided
+  order support; beta ambiguity is a protocol fold, not domain ontology.
+
+## Self-Audit: 5 Lenti
+
+L1 hard constraint vs bias: no "always", "never", or "zero" claim is used
+without exact support. `endpoint_stable_observables: []` is exact for this run
+and perimeters.
+
+L2 quantity vs ratio: the report uses raw stable counts, endpoint distances, and
+beta layers. Percent language is avoided except where the script computes
+fractions.
+
+L3 no silent patching: the scope changed from BOUNDARY to non-BOUNDARY
+perimeters explicitly. The new category `order_denominator_gate` is declared
+instead of stretching `BOUNDARY_LAYER_GATE`.
+
+L4 edge cases: `E_ar1_continuity` has illusory stable_count `0.60`, so the
+illusory endpoint is weak-denominator, not denominator-absent. The claim is
+one-sided support dominance, not exact absence.
+
+L5 re-discovery vs discovery: the numerical mechanism is an original-vs-shuffle
+order test on synthetic sequences. No NEW mathematical theorem is claimed.
+
+## Fonti
+
+- Verificato: `tools/data/agent_field_live.md`
+- Verificato: `tools/LAB_AGENT_CONTEXT.md`
+- Verificato: `tools/observables_registry.py`
+- Verificato: `tools/exp_denominator_gate_transfer_matrix.py`
+- Verificato: `tools/data/denominator_gate_transfer_matrix.json`
+- Inferito: `order_denominator_gate` as category needed by the downstream
+  vocabulary, from the cross-perimeter matrix.
diff --git a/tools/data/seme.json b/tools/data/seme.json
index 9dc9f5c8d087e9aea3266d5fefbc52a939acea27..47927a057e5396452b3c7982c8218c6e7c4d3b12
--- a/tools/data/seme.json
+++ b/tools/data/seme.json
@@ -132,6 +132,17 @@
       "_source_log": "2026-05-07T08:10:22.658201+00:00",
       "_source_decision": "REDESIGN",
       "_source_reasoning": "Il ciclo ha prodotto evidenza controllata e replicata che il denominator gate trasferisce come operatore, ma non trasferisce la coordinata di layer BOUNDARY: ambiguita' classificativa e collasso del denominatore si separano. Continuare sul seme attuale centrato su GUE/Poisson rischia di restare nel "
+    },
+    {
+      "tipo": "vincolo",
+      "id": "ORDER_DENOMINATOR_GATE",
+      "claim": "Il denominator gate trasferisce cross-perimetro come supporto one-sided dell'ordine generato, non come endpoint-stable support a due poli. Nel perimetro agent_20260507_0901, 4/4 domini non-BOUNDARY hanno endpoint_stable_observables=[]; il polo coerente ha stable_count 3.0-5.0, il polo illusorio 0.05-0.60. La beta ambigua 0.30 e coordinata del protocollo di perturbazione finche non viene ancorata a un asse semantico del dominio.",
+      "intensita": 0.92,
+      "porta": "META",
+      "manuale": true,
+      "condensato_ref": "A4,A8,A14,C2",
+      "origine": "cycle agent_20260507_0901: denominator gate transfer matrix su DUALITA_golden, R_periodic_triad, T_markov_alternating, E_ar1_continuity",
+      "added_at": "2026-05-07T09:01:00+00:00"
     }
   ],
   "potenziale_bloccato": [],
@@ -139,11 +150,11 @@
     "Tensioni risolte: {'PIANO_PRIMARIO_DUE_ASSIOMI', 'METRIC_TENSOR', 'BOUNDARY_LAYER_GATE', 'OBSERVABLE_REGISTRY', 'DUALITA_DIPOLARE_VS_ILLUSORIA', 'TENSIONE_ENTITA', 'PERTURBATION_DENOMINATOR_GATE', 'G_POTENZIALE_NULLA', 'TRASCENDENZA_LIMITE'}"
   ],
   "filtro": {
-    "promosse": 11,
+    "promosse": 12,
     "filtrate": 0,
     "condensato_claims": 30
   },
-  "direzione": "Costruire una matrice di trasferibilita' del denominator gate: applicarlo a 3-4 perimetri non-BOUNDARY con poli coerente/illusorio e verificare quali parti trasferiscono (supporto one-sided, coordinate di collasso, ambiguita' classificativa) e quali restano locali.",
+  "direzione": "Verificare se ORDER_DENOMINATOR_GATE resta one-sided su perimetri naturali/non sintetici: primes windows, logistic map, Fibonacci spectrum o dominio fisico gia nel lab. Separare supporto d'ordine, coordinata di protocollo e asse semantico del dominio.",
   "verifica": {
     "pass": 11,
     "fail": 0,
@@ -311,6 +322,7 @@
     }
   ],
   "_seed_history": {
-    "trajectory_apply_20260507_090145": "applied modify_seme from 20260507_0803: direzione → 'Costruire una matrice di trasferibilita' del denominator gate: applicarlo a 3-4 perimetri non-BOUNDA...'"
+    "trajectory_apply_20260507_090145": "applied modify_seme from 20260507_0803: direzione → 'Costruire una matrice di trasferibilita' del denominator gate: applicarlo a 3-4 perimetri non-BOUNDA...'",
+    "agent_20260507_0901": "added ORDER_DENOMINATOR_GATE; direzione -> test one-sided gate on natural/non-synthetic perimeters"
   }
-}
\ No newline at end of file
+}
diff --git a/tools/exp_denominator_gate_transfer_matrix.py b/tools/exp_denominator_gate_transfer_matrix.py
new file mode 100644
index 0000000000000000000000000000000000000000..f23cf7cf9505045e3d62ad95c8f3cddb38b31cef
--- /dev/null
+++ b/tools/exp_denominator_gate_transfer_matrix.py
@@ -0,0 +1,337 @@
+#!/usr/bin/env python3
+"""
+exp_denominator_gate_transfer_matrix.py
+
+Reusable META audit for the denominator gate transfer matrix.
+
+The experiment moves the original-vs-shuffle denominator gate away from the
+GUE/Poisson BOUNDARY perimeter. Each perimeter has a coherent endpoint and an
+illusory endpoint built as a permutation of the same gap multiset. That keeps
+the one-point distribution fixed and isolates ordering support.
+
+Measured for each perimeter:
+- canonical observables from observables_registry.py;
+- original-vs-shuffle z-score for each observable;
+- endpoint-stable observable set under |z| >= z_min;
+- endpoint classification using all observables and endpoint-gated observables;
+- beta layer ambiguity between coherent and illusory endpoints.
+"""
+
+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())
+PHI = (1.0 + 5.0**0.5) / 2.0
+
+
+def normalize(gaps: np.ndarray) -> np.ndarray:
+    gaps = np.asarray(gaps, dtype=float)
+    gaps = np.maximum(gaps, 1e-12)
+    mean = float(np.mean(gaps))
+    return gaps / mean if mean > 1e-15 else gaps
+
+
+def golden_beatty(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    phase = float(rng.random())
+    n = np.arange(n_gaps + 1, dtype=float) + phase
+    positions = np.floor(n * PHI)
+    return normalize(np.diff(positions))
+
+
+def periodic_triad(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    base = np.array([0.55, 1.0, 1.45, 1.0, 0.75, 1.25], dtype=float)
+    shift = int(rng.integers(0, len(base)))
+    tiled = np.tile(np.roll(base, shift), int(np.ceil(n_gaps / len(base))))[:n_gaps]
+    jitter = rng.normal(0.0, 0.015, size=n_gaps)
+    return normalize(tiled + jitter)
+
+
+def markov_alternating(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    vals = np.array([0.62, 1.38], dtype=float)
+    state = int(rng.integers(0, 2))
+    out = np.empty(n_gaps, dtype=float)
+    for i in range(n_gaps):
+        out[i] = vals[state] + rng.normal(0.0, 0.03)
+        if rng.random() < 0.88:
+            state = 1 - state
+    return normalize(out)
+
+
+def ar1_continuity(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    rho = 0.86
+    x = np.empty(n_gaps, dtype=float)
+    x[0] = rng.normal()
+    noise_scale = (1.0 - rho * rho) ** 0.5
+    for i in range(1, n_gaps):
+        x[i] = rho * x[i - 1] + noise_scale * rng.normal()
+    return normalize(np.exp(0.42 * x))
+
+
+PERIMETERS = {
+    "DUALITA_golden": golden_beatty,
+    "R_periodic_triad": periodic_triad,
+    "T_markov_alternating": markov_alternating,
+    "E_ar1_continuity": ar1_continuity,
+}
+
+
+def beta_layer(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        out = base.copy()
+    elif beta >= 1.0:
+        out = illusory.copy()
+    else:
+        mask = rng.random(len(base)) < beta
+        out = base.copy()
+        out[mask] = illusory[mask]
+    return normalize(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]]:
+    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])
+
+    shuffle_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
+        shuffle_std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, shuffle_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:
+    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)
+
+    coherent_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent_vectors, illusory_vectors])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent_vectors, axis=0)
+    illusory_centroid = np.mean(illusory_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((illusory_centroid - coherent_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_coherent = float(np.linalg.norm((x - coherent_centroid) / scale))
+            d_illusory = float(np.linalg.norm((x - illusory_centroid) / scale))
+            denom = d_coherent + d_illusory
+            coord = float((d_coherent - d_illusory) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_coherent - d_illusory) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("coherent" if d_coherent < d_illusory else "illusory")
+        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,
+            "illusory_label_fraction": float(np.mean(np.array(labels) == "illusory")),
+        }
+
+    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}
+        z_values = {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)
+                z_values[name].append(row["z"][name])
+        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()},
+            "z_mean": {name: float(np.mean(vals)) for name, vals in z_values.items()},
+        }
+
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    endpoint_one_sided = []
+    for name in OBS_NAMES:
+        coherent_freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in coherent_rows])
+        illusory_freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in illusory_rows])
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            endpoint_one_sided.append(name)
+        if coherent_freq >= 0.75 and illusory_freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "coherent_one_sided_observables": endpoint_one_sided,
+        "layers": layers,
+    }
+
+
+def analyze_perimeter(name: str, generator, args: argparse.Namespace, rng: np.random.Generator) -> dict:
+    rows = []
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    for rep in range(args.n_replicates):
+        base = generator(args.n_gaps, np.random.default_rng(rng.integers(0, 2**63 - 1)))
+        for beta in betas:
+            gaps = beta_layer(base, beta, np.random.default_rng(rng.integers(0, 2**63 - 1)))
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                args.n_baseline,
+                np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "perimeter": name,
+                    "replicate": rep,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [obs_name for obs_name in OBS_NAMES if abs(z[obs_name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    one_sided_classification = classify_layers(rows, gate["coherent_one_sided_observables"])
+    return {
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_one_sided_gated": one_sided_classification,
+        "rows": rows,
+    }
+
+
+def compact_matrix(perimeters: dict) -> dict:
+    matrix = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        class_all = data["classification_all_observables"]
+        class_gate = data["classification_one_sided_gated"]
+        matrix[name] = {
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "endpoint_distance_all": class_all["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": class_gate["endpoint_distance"],
+            "ambiguous_beta_all": class_all["ambiguous_beta"],
+            "ambiguous_beta_one_sided_gated": class_gate["ambiguous_beta"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+        }
+    return matrix
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    perimeters = {}
+    for name, generator in PERIMETERS.items():
+        perimeters[name] = analyze_perimeter(name, generator, args, root_rng)
+
+    output = {
+        "experiment": "denominator_gate_transfer_matrix",
+        "category": "gate_transferability",
+        "question": "Which parts of the denominator gate transfer outside BOUNDARY?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact_matrix(perimeters),
+        "perimeters": perimeters,
+    }
+
+    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("perimeter one_sided stable0 stable1 dist_all dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>21s} "
+            f"{','.join(row['coherent_one_sided_observables']) or '[]':>22s} "
+            f"{row['stable_count_coherent']:>7.3f} "
+            f"{row['stable_count_illusory']:>7.3f} "
+            f"{row['endpoint_distance_all']:>8.3f} "
+            f"{row['endpoint_distance_one_sided_gated']:>9.3f} "
+            f"{row['ambiguous_beta_one_sided_gated']}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=4096)
+    parser.add_argument("--n-replicates", type=int, default=20)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=32)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=202605070901)
+    parser.add_argument("--out", default="tools/data/denominator_gate_transfer_matrix.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

codex
Completato.

Ho eseguito il cycle e scritto il report richiesto in [agent_20260507_0901.md](/opt/MM_D-ND/tools/data/reports/agent_20260507_0901.md).

Artefatti creati/aggiornati:
- [exp_denominator_gate_transfer_matrix.py](/opt/MM_D-ND/tools/exp_denominator_gate_transfer_matrix.py)
- [denominator_gate_transfer_matrix.json](/opt/MM_D-ND/tools/data/denominator_gate_transfer_matrix.json)
- [seme.json](/opt/MM_D-ND/tools/data/seme.json)

Risultato centrale: il denominator gate trasferisce su 4 perimetri non-BOUNDARY come `order_denominator_gate`: supporto one-sided del polo coerente, non supporto stabile su entrambi gli endpoint. In tutti i perimetri `endpoint_stable_observables=[]`; la beta ambigua `0.30` è coordinata di protocollo, non ontologia BOUNDARY.

Verifiche fatte:
- nessuna `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` presente
- `python -m py_compile tools/exp_denominator_gate_transfer_matrix.py`
- `python -m json.tool` su `seme.json` e sul JSON dell’esperimento
diff --git a/tools/data/reports/agent_20260507_0901.md b/tools/data/reports/agent_20260507_0901.md
new file mode 100644
index 0000000000000000000000000000000000000000..101695bfa907f5e680e8d408da1a95b66b7203f1
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0901.md
@@ -0,0 +1,122 @@
+# Agent Report — Denominator Gate Transfer Matrix
+
+timestamp: 2026-05-07 09:01 UTC
+category: gate_transferability_matrix
+verdict: operator
+observables_registry: 1.0.0-2026-05-06
+observables_used: [SR, SR2, L1, L2, triple_var]
+tool: tools/exp_denominator_gate_transfer_matrix.py
+data: tools/data/denominator_gate_transfer_matrix.json
+
+## Claim Under Test
+
+Verificato dal campo vivo: il cycle 0803 ha mostrato che il denominator gate
+trasferisce da BOUNDARY a DUALITA come operatore, ma non trasferisce la
+coordinata locale GUE/Poisson.
+
+Domanda di questo cycle: applicato a 4 perimetri non-BOUNDARY con poli
+coerente/illusorio a distribuzione marginale preservata, quali parti del gate
+trasferiscono e quali restano locali?
+
+Perimetri:
+
+- `DUALITA_golden`: sequenza Beatty aurea coerente vs permutazione illusoria.
+- `R_periodic_triad`: pattern periodico a 6 fasi vs permutazione illusoria.
+- `T_markov_alternating`: catena alternante low/high vs permutazione illusoria.
+- `E_ar1_continuity`: continuita AR(1) positiva vs permutazione illusoria.
+
+Parametri verificati: `n_gaps=4096`, `n_replicates=20`, `n_beta=11`,
+`n_baseline=32`, `z_min=2.0`, `seed=202605070901`.
+
+## Deposito Numerico
+
+| perimeter | coherent one-sided observables | stable_count coherent | stable_count illusory | endpoint distance gated | ambiguous beta gated |
+|---|---:|---:|---:|---:|---:|
+| DUALITA_golden | SR, L1, triple_var | 3.00 | 0.25 | 3.418 | 0.30 |
+| R_periodic_triad | SR, SR2, L1, L2, triple_var | 5.00 | 0.25 | 4.400 | 0.30 |
+| T_markov_alternating | SR, SR2, L1, L2, triple_var | 5.00 | 0.05 | 4.412 | 0.30 |
+| E_ar1_continuity | SR, SR2, L1, L2, triple_var | 5.00 | 0.60 | 4.394 | 0.30 |
+
+Endpoint-stable observables: `[]` in all 4 perimeters.
+
+Verificato: no observable is stable at both coherent and illusory endpoints.
+The denominator gate is not a two-sided endpoint support. It is one-sided
+support for generated order.
+
+## Risultato
+
+1. The gate transfers as one-sided coherence support.
+
+   In all 4 non-BOUNDARY perimeters, the coherent endpoint carries denominator
+   support and the illusory endpoint loses it. `DUALITA_golden` transfers on
+   `SR`, `L1`, `triple_var`; the three other perimeters transfer on all five
+   canonical observables.
+
+2. The both-endpoint stable set collapses everywhere.
+
+   `endpoint_stable_observables: []` is not a failure of the test. It is the
+   structural content: the gate does not name "two classes with stable
+   denominators"; it names the side where order survives its own shuffle null.
+
+3. The beta 0.30 ambiguity layer transfers as protocol coordinate, not as
+   BOUNDARY coordinate.
+
+   The ambiguous gated layer is beta `0.30` in all 4 perimeters. This is not a
+   GUE/Poisson layer. It is the coordinate created by the replacement protocol:
+   enough positions are illusory to place the layer near the endpoint bisector,
+   while enough coherent order remains to keep large z support.
+
+4. DUALITA is narrower than the other perimeters.
+
+   `DUALITA_golden` has one-sided support on 3/5 observables. `SR2` and `L2`
+   stay near shuffle at the coherent endpoint (`z_mean` about `0.12`). The
+   DUALITA gate transfers, but only through lag-1 and triple aggregation in this
+   synthetic perimeter.
+
+## Consecutio
+
+The next word is not `boundary_layer`. The correct category is:
+
+`order_denominator_gate`
+
+Definition: an original-vs-shuffle denominator gate where support is expected
+on the coherent/generated side, endpoint-stable observables may be empty, and
+the layer coordinate belongs to the perturbation protocol unless anchored to a
+domain-specific semantic axis.
+
+This extends the 0803 result:
+
+- 0803: BOUNDARY denominator gate transfers to DUALITA, BOUNDARY coordinate does not.
+- 0901: across 4 non-BOUNDARY perimeters, the transferable object is one-sided
+  order support; beta ambiguity is a protocol fold, not domain ontology.
+
+## Self-Audit: 5 Lenti
+
+L1 hard constraint vs bias: no "always", "never", or "zero" claim is used
+without exact support. `endpoint_stable_observables: []` is exact for this run
+and perimeters.
+
+L2 quantity vs ratio: the report uses raw stable counts, endpoint distances, and
+beta layers. Percent language is avoided except where the script computes
+fractions.
+
+L3 no silent patching: the scope changed from BOUNDARY to non-BOUNDARY
+perimeters explicitly. The new category `order_denominator_gate` is declared
+instead of stretching `BOUNDARY_LAYER_GATE`.
+
+L4 edge cases: `E_ar1_continuity` has illusory stable_count `0.60`, so the
+illusory endpoint is weak-denominator, not denominator-absent. The claim is
+one-sided support dominance, not exact absence.
+
+L5 re-discovery vs discovery: the numerical mechanism is an original-vs-shuffle
+order test on synthetic sequences. No NEW mathematical theorem is claimed.
+
+## Fonti
+
+- Verificato: `tools/data/agent_field_live.md`
+- Verificato: `tools/LAB_AGENT_CONTEXT.md`
+- Verificato: `tools/observables_registry.py`
+- Verificato: `tools/exp_denominator_gate_transfer_matrix.py`
+- Verificato: `tools/data/denominator_gate_transfer_matrix.json`
+- Inferito: `order_denominator_gate` as category needed by the downstream
+  vocabulary, from the cross-perimeter matrix.
diff --git a/tools/data/seme.json b/tools/data/seme.json
index 9dc9f5c8d087e9aea3266d5fefbc52a939acea27..47927a057e5396452b3c7982c8218c6e7c4d3b12
--- a/tools/data/seme.json
+++ b/tools/data/seme.json
@@ -132,6 +132,17 @@
       "_source_log": "2026-05-07T08:10:22.658201+00:00",
       "_source_decision": "REDESIGN",
       "_source_reasoning": "Il ciclo ha prodotto evidenza controllata e replicata che il denominator gate trasferisce come operatore, ma non trasferisce la coordinata di layer BOUNDARY: ambiguita' classificativa e collasso del denominatore si separano. Continuare sul seme attuale centrato su GUE/Poisson rischia di restare nel "
+    },
+    {
+      "tipo": "vincolo",
+      "id": "ORDER_DENOMINATOR_GATE",
+      "claim": "Il denominator gate trasferisce cross-perimetro come supporto one-sided dell'ordine generato, non come endpoint-stable support a due poli. Nel perimetro agent_20260507_0901, 4/4 domini non-BOUNDARY hanno endpoint_stable_observables=[]; il polo coerente ha stable_count 3.0-5.0, il polo illusorio 0.05-0.60. La beta ambigua 0.30 e coordinata del protocollo di perturbazione finche non viene ancorata a un asse semantico del dominio.",
+      "intensita": 0.92,
+      "porta": "META",
+      "manuale": true,
+      "condensato_ref": "A4,A8,A14,C2",
+      "origine": "cycle agent_20260507_0901: denominator gate transfer matrix su DUALITA_golden, R_periodic_triad, T_markov_alternating, E_ar1_continuity",
+      "added_at": "2026-05-07T09:01:00+00:00"
     }
   ],
   "potenziale_bloccato": [],
@@ -139,11 +150,11 @@
     "Tensioni risolte: {'PIANO_PRIMARIO_DUE_ASSIOMI', 'METRIC_TENSOR', 'BOUNDARY_LAYER_GATE', 'OBSERVABLE_REGISTRY', 'DUALITA_DIPOLARE_VS_ILLUSORIA', 'TENSIONE_ENTITA', 'PERTURBATION_DENOMINATOR_GATE', 'G_POTENZIALE_NULLA', 'TRASCENDENZA_LIMITE'}"
   ],
   "filtro": {
-    "promosse": 11,
+    "promosse": 12,
     "filtrate": 0,
     "condensato_claims": 30
   },
-  "direzione": "Costruire una matrice di trasferibilita' del denominator gate: applicarlo a 3-4 perimetri non-BOUNDARY con poli coerente/illusorio e verificare quali parti trasferiscono (supporto one-sided, coordinate di collasso, ambiguita' classificativa) e quali restano locali.",
+  "direzione": "Verificare se ORDER_DENOMINATOR_GATE resta one-sided su perimetri naturali/non sintetici: primes windows, logistic map, Fibonacci spectrum o dominio fisico gia nel lab. Separare supporto d'ordine, coordinata di protocollo e asse semantico del dominio.",
   "verifica": {
     "pass": 11,
     "fail": 0,
@@ -311,6 +322,7 @@
     }
   ],
   "_seed_history": {
-    "trajectory_apply_20260507_090145": "applied modify_seme from 20260507_0803: direzione → 'Costruire una matrice di trasferibilita' del denominator gate: applicarlo a 3-4 perimetri non-BOUNDA...'"
+    "trajectory_apply_20260507_090145": "applied modify_seme from 20260507_0803: direzione → 'Costruire una matrice di trasferibilita' del denominator gate: applicarlo a 3-4 perimetri non-BOUNDA...'",
+    "agent_20260507_0901": "added ORDER_DENOMINATOR_GATE; direzione -> test one-sided gate on natural/non-synthetic perimeters"
   }
-}
\ No newline at end of file
+}
diff --git a/tools/exp_denominator_gate_transfer_matrix.py b/tools/exp_denominator_gate_transfer_matrix.py
new file mode 100644
index 0000000000000000000000000000000000000000..f23cf7cf9505045e3d62ad95c8f3cddb38b31cef
--- /dev/null
+++ b/tools/exp_denominator_gate_transfer_matrix.py
@@ -0,0 +1,337 @@
+#!/usr/bin/env python3
+"""
+exp_denominator_gate_transfer_matrix.py
+
+Reusable META audit for the denominator gate transfer matrix.
+
+The experiment moves the original-vs-shuffle denominator gate away from the
+GUE/Poisson BOUNDARY perimeter. Each perimeter has a coherent endpoint and an
+illusory endpoint built as a permutation of the same gap multiset. That keeps
+the one-point distribution fixed and isolates ordering support.
+
+Measured for each perimeter:
+- canonical observables from observables_registry.py;
+- original-vs-shuffle z-score for each observable;
+- endpoint-stable observable set under |z| >= z_min;
+- endpoint classification using all observables and endpoint-gated observables;
+- beta layer ambiguity between coherent and illusory endpoints.
+"""
+
+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())
+PHI = (1.0 + 5.0**0.5) / 2.0
+
+
+def normalize(gaps: np.ndarray) -> np.ndarray:
+    gaps = np.asarray(gaps, dtype=float)
+    gaps = np.maximum(gaps, 1e-12)
+    mean = float(np.mean(gaps))
+    return gaps / mean if mean > 1e-15 else gaps
+
+
+def golden_beatty(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    phase = float(rng.random())
+    n = np.arange(n_gaps + 1, dtype=float) + phase
+    positions = np.floor(n * PHI)
+    return normalize(np.diff(positions))
+
+
+def periodic_triad(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    base = np.array([0.55, 1.0, 1.45, 1.0, 0.75, 1.25], dtype=float)
+    shift = int(rng.integers(0, len(base)))
+    tiled = np.tile(np.roll(base, shift), int(np.ceil(n_gaps / len(base))))[:n_gaps]
+    jitter = rng.normal(0.0, 0.015, size=n_gaps)
+    return normalize(tiled + jitter)
+
+
+def markov_alternating(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    vals = np.array([0.62, 1.38], dtype=float)
+    state = int(rng.integers(0, 2))
+    out = np.empty(n_gaps, dtype=float)
+    for i in range(n_gaps):
+        out[i] = vals[state] + rng.normal(0.0, 0.03)
+        if rng.random() < 0.88:
+            state = 1 - state
+    return normalize(out)
+
+
+def ar1_continuity(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    rho = 0.86
+    x = np.empty(n_gaps, dtype=float)
+    x[0] = rng.normal()
+    noise_scale = (1.0 - rho * rho) ** 0.5
+    for i in range(1, n_gaps):
+        x[i] = rho * x[i - 1] + noise_scale * rng.normal()
+    return normalize(np.exp(0.42 * x))
+
+
+PERIMETERS = {
+    "DUALITA_golden": golden_beatty,
+    "R_periodic_triad": periodic_triad,
+    "T_markov_alternating": markov_alternating,
+    "E_ar1_continuity": ar1_continuity,
+}
+
+
+def beta_layer(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        out = base.copy()
+    elif beta >= 1.0:
+        out = illusory.copy()
+    else:
+        mask = rng.random(len(base)) < beta
+        out = base.copy()
+        out[mask] = illusory[mask]
+    return normalize(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]]:
+    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])
+
+    shuffle_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
+        shuffle_std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, shuffle_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:
+    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)
+
+    coherent_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent_vectors, illusory_vectors])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent_vectors, axis=0)
+    illusory_centroid = np.mean(illusory_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((illusory_centroid - coherent_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_coherent = float(np.linalg.norm((x - coherent_centroid) / scale))
+            d_illusory = float(np.linalg.norm((x - illusory_centroid) / scale))
+            denom = d_coherent + d_illusory
+            coord = float((d_coherent - d_illusory) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_coherent - d_illusory) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("coherent" if d_coherent < d_illusory else "illusory")
+        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,
+            "illusory_label_fraction": float(np.mean(np.array(labels) == "illusory")),
+        }
+
+    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}
+        z_values = {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)
+                z_values[name].append(row["z"][name])
+        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()},
+            "z_mean": {name: float(np.mean(vals)) for name, vals in z_values.items()},
+        }
+
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    endpoint_one_sided = []
+    for name in OBS_NAMES:
+        coherent_freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in coherent_rows])
+        illusory_freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in illusory_rows])
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            endpoint_one_sided.append(name)
+        if coherent_freq >= 0.75 and illusory_freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "coherent_one_sided_observables": endpoint_one_sided,
+        "layers": layers,
+    }
+
+
+def analyze_perimeter(name: str, generator, args: argparse.Namespace, rng: np.random.Generator) -> dict:
+    rows = []
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    for rep in range(args.n_replicates):
+        base = generator(args.n_gaps, np.random.default_rng(rng.integers(0, 2**63 - 1)))
+        for beta in betas:
+            gaps = beta_layer(base, beta, np.random.default_rng(rng.integers(0, 2**63 - 1)))
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                args.n_baseline,
+                np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "perimeter": name,
+                    "replicate": rep,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [obs_name for obs_name in OBS_NAMES if abs(z[obs_name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    one_sided_classification = classify_layers(rows, gate["coherent_one_sided_observables"])
+    return {
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_one_sided_gated": one_sided_classification,
+        "rows": rows,
+    }
+
+
+def compact_matrix(perimeters: dict) -> dict:
+    matrix = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        class_all = data["classification_all_observables"]
+        class_gate = data["classification_one_sided_gated"]
+        matrix[name] = {
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "endpoint_distance_all": class_all["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": class_gate["endpoint_distance"],
+            "ambiguous_beta_all": class_all["ambiguous_beta"],
+            "ambiguous_beta_one_sided_gated": class_gate["ambiguous_beta"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+        }
+    return matrix
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    perimeters = {}
+    for name, generator in PERIMETERS.items():
+        perimeters[name] = analyze_perimeter(name, generator, args, root_rng)
+
+    output = {
+        "experiment": "denominator_gate_transfer_matrix",
+        "category": "gate_transferability",
+        "question": "Which parts of the denominator gate transfer outside BOUNDARY?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact_matrix(perimeters),
+        "perimeters": perimeters,
+    }
+
+    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("perimeter one_sided stable0 stable1 dist_all dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>21s} "
+            f"{','.join(row['coherent_one_sided_observables']) or '[]':>22s} "
+            f"{row['stable_count_coherent']:>7.3f} "
+            f"{row['stable_count_illusory']:>7.3f} "
+            f"{row['endpoint_distance_all']:>8.3f} "
+            f"{row['endpoint_distance_one_sided_gated']:>9.3f} "
+            f"{row['ambiguous_beta_one_sided_gated']}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=4096)
+    parser.add_argument("--n-replicates", type=int, default=20)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=32)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=202605070901)
+    parser.add_argument("--out", default="tools/data/denominator_gate_transfer_matrix.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

tokens used
109,942
Completato.

Ho eseguito il cycle e scritto il report richiesto in [agent_20260507_0901.md](/opt/MM_D-ND/tools/data/reports/agent_20260507_0901.md).

Artefatti creati/aggiornati:
- [exp_denominator_gate_transfer_matrix.py](/opt/MM_D-ND/tools/exp_denominator_gate_transfer_matrix.py)
- [denominator_gate_transfer_matrix.json](/opt/MM_D-ND/tools/data/denominator_gate_transfer_matrix.json)
- [seme.json](/opt/MM_D-ND/tools/data/seme.json)

Risultato centrale: il denominator gate trasferisce su 4 perimetri non-BOUNDARY come `order_denominator_gate`: supporto one-sided del polo coerente, non supporto stabile su entrambi gli endpoint. In tutti i perimetri `endpoint_stable_observables=[]`; la beta ambigua `0.30` è coordinata di protocollo, non ontologia BOUNDARY.

Verifiche fatte:
- nessuna `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` presente
- `python -m py_compile tools/exp_denominator_gate_transfer_matrix.py`
- `python -m json.tool` su `seme.json` e sul JSON dell’esperimento
