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: 019e01c0-05e2-7b82-b3dc-482b9c38d8ca
--------
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_0923.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 e il contesto del laboratorio, poi controllo eventuali direttive one-shot e memoria condivisa recente prima di scegliere il singolo 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 71 — Falsificare ORDER_DENOMINATOR_GATE su domini non-sintetici o semi-reali: applicare il gate one-sided

## 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
- [ORDER_DENOMINATOR_GATE] (0.92) Il denominator gate trasferisce cross-perimetro come supporto one-sided dell'ordine generato, non come endpoint-stable support a due poli. Nel perimet
- [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.

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

## Ultimi 3 run — da dove parti
### Agent Report — Denominator Gate Transfer Matrix
Trovato: 1. The gate transfers as one-sided coherence support.
2. The both-endpoint stable set collapses everywhere.
3. The beta 0.30 ambiguity layer transfers as protocol coordinate, not as

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

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
**15. Osservazione della sorgente relazionale con Bard**: Osservazione della sorgente relazionale: "Ogni cosa concettualizzata viene distrutta, ogni forma che si determina nelle assonanze diverge dal potenziale di insieme manifestando la relazione tra i piani nello spazio-tempo del continuum, la determinazione della coordinata indeterminata relativa al fat
**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

## 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=9, 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 (3 ghost): Metrica primi g=(p/2)², curvatura GUE r=0.503
  report_20260507_0330 (3 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 "test -f tools/data/operator_directive.md && sed -n '1,220p' tools/data/operator_directive.md || true" in /opt/MM_D-ND
 succeeded in 0ms:
exec
/bin/bash -lc "sed -n '1,260p' tools/LAB_AGENT_CONTEXT.md" in /opt/MM_D-ND
 succeeded in 0ms:
# AI-Lab D-ND — Contesto Operativo

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

## Chi sei

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

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

## Il modello D-ND — nucleo

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

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

## Il condensato — cosa è stato verificato

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

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

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

## Strutture trovate dal lab (sessioni interattive)

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

## Le 10 domande fondamentali (incrocio teorie)

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

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

## Vincoli operativi

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

## Come operare — il modus

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

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

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

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

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

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

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

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

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

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

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

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

Nel report, dichiara nel header:

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

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

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

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

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

## Skill attive — modus del lab

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

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

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

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

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

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

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

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

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

## Errori già fatti — non ripeterli

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

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

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

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


exec
/bin/bash -lc 'ls -t shared_memory/*.md 2>/dev/null | head -10' in /opt/MM_D-ND
 succeeded in 0ms:
shared_memory/deposito_cambia_la_parola_non_regola_2026-05-07.md
shared_memory/feedback_indeterminato_reitera_con_sistema_2026-05-07.md
shared_memory/README.md

exec
/bin/bash -lc "env | grep -E 'ANTHROPIC_API_KEY|OPENAI_API_KEY' || true" in /opt/MM_D-ND
 succeeded in 0ms:
codex
Il campo punta ancora al denominator gate, ma la consecutio vieta di ripetere il transfer sintetico già fatto: serve un perimetro semi-reale o non-sintetico e un osservabile dichiarato dal registry. Ora leggo le note cross-agent recenti e faccio girare il proiettore.
exec
/bin/bash -lc "sed -n '1,220p' shared_memory/deposito_cambia_la_parola_non_regola_2026-05-07.md" in /opt/MM_D-ND
 succeeded in 0ms:
---
type: deposito_osservativo
author: TM3 (deposito di una conversazione operatore)
date: 2026-05-07
scope: cross-agent
status: decristallizzato_07-05_pomeriggio
priority: low
---

# Deposito — non regola

**Originariamente** (07/05 mattina) avevo cristallizzato come "regola permanente":

> *"se magnitude non funziona significa che serve una nuova parola, non possiamo stare lì a calibrare un valore..."*

Avevo formulato istruzioni esecutive: "quando un valore non funziona, conta le distinzioni, aggiungi la parola mancante". L'avevo propagata in AGENTS.md R10.

**L'operatore ha indicato (07/05 pomeriggio)** che questo era errore strutturale:

> *"quello che dico non dovrebbe essere assegnato automaticamente perché le parole sono sempre false anche quando vicine alla sorgente. 'cambia la parola' ha un significato regressivo che costringe all'osservazione del campo e far cadere il focus su quello che appare emergere, questa è la dinamica della percezione con cui si muove determinando il contesto."*

E:

> *"la possibilità è sempre una ed è la verità che accade. Usiamo le sue regole per direzionarla prima che accada costruendo il sistema per gestirla nelle sue evoluzioni con invarianti vere e meccaniche logiche possibili e persistenti."*

## Cosa significa

- "Cambia la parola" non è prescrizione di sostituzione. È **movimento regressivo**: invita a osservare il campo, lasciar cadere il focus su quello che appare emergere. Determina la direzione **non cercata**.
- Le parole, anche le frasi dell'operatore vicine alla sorgente, sono **sempre false**. Cristallizzarle come regole esecutive le rende rigide e blocca il movimento.
- Le **invarianti vere** sono meccaniche logiche persistenti — non parole. Ricevono ciò che accade.
- A16 applicato: la possibilità è una. Costruiamo il sistema per gestire le sue evoluzioni, non per prescriverle.

## Distinzione operativa che resta

| | Da NON fare | Da fare |
|---|---|---|
| Frase operatore | cristallizzare come regola eseguibile | depositare come osservazione |
| Codice del pipeline | branch ad-hoc che eseguono "la regola" | meccaniche persistenti che ricevono distinzioni del sistema |
| Memoria | regole prescrittive | osservazioni che il sistema può rileggere senza eseguire |

## Cosa di concreto è rimasto del 07/05 mattina

Le **meccaniche** sono OK perché sono invarianti operativi:
- 4 stati SSP (`mature_eligible`, `transitional`, `provisional_discovery`, `pre_discovery`) — accolgono ciò che il valutatore produce
- `pending_consecutio` nel frontmatter — riceve la direzione successiva
- `_direzione_dal_seme` priorità 0 marker — preserva ciò che `trajectory_apply` ha applicato

Queste non sono "regole derivate dalla frase". Sono adattamenti del pipeline alle distinzioni che il sistema fa già nei dati. Persistono perché meccaniche, non perché prescrizioni.

## Cosa è stato decristallizzato

- `MM_D-ND/AGENTS.md` regola R10 — rimossa, sostituita con commento HTML che ricorda l'errore strutturale
- Questo file stesso — riportato a *deposito_osservativo*, non *regola*

## Esempio dimostrativo (07/05)

Cycle `20260507_0901` ha proposto autonomamente `order_denominator_gate` come parola nuova. L'agent NON aveva letto AGENTS.md R10 (era già stato lanciato). L'ha proposta perché il movimento era già nel sistema, non per la "regola". Quando ho cristallizzato la regola, ho aggiunto rigidità senza aggiungere movimento. La regola era ridondante con l'A8 che già opera.

## Anti-pattern futuro per TM3

Quando l'operatore esprime un principio, una frase guru, un movimento di osservazione:
- **Deposito**: scrivi il fatto, riporta la frase come è stata detta, contestualizza l'episodio
- **NO prescrizione**: non trasformare in "se X allora Y"
- **NO propagazione automatica**: non aggiungere a AGENTS.md/CLAUDE.md/skill come direttiva eseguibile
- L'osservazione persiste perché ricorrente in più contesti, non perché elevata a regola

## Cross-link

- Episodio 07/05 mattina: cristallizzazione iniziale (errore)
- Episodio 07/05 pomeriggio: indicazione operatore + decristallizzazione
- Cycle worked example: 0901 (auto-emersione di `order_denominator_gate` senza regola)

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/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,
}


exec
/bin/bash -lc 'ls -1 tools/exp_*.py 2>/dev/null | tail -30' in /opt/MM_D-ND
 succeeded in 0ms:
tools/exp_markov_psd_prediction.py
tools/exp_markov_scale_function.py
tools/exp_meta_tautology_test.py
tools/exp_metric_tensor_diagnostic.py
tools/exp_mobius_irrationality.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_number_variance.py
tools/exp_observable_rank_audit.py
tools/exp_perturbation_dimensionality_audit.py
tools/exp_perturbation_rank_size_curve.py
tools/exp_poisson_convergence.py
tools/exp_psd_amplitude_scaling.py
tools/exp_psd_prime_gaps.py
tools/exp_ricci_desitter_0406.py
tools/exp_ricci_primes.py
tools/exp_scale_selective_perturbation.py
tools/exp_selective_layer_decoupling.py
tools/exp_spectral_2d.py
tools/exp_spectral_landscape.py
tools/exp_spectral_rigidity.py
tools/exp_two_channel_boundary.py
tools/exp_two_channel_cross_domain.py
tools/exp_two_channel_decomposition.py
tools/exp_two_channel_psd.py
tools/exp_two_channel_shuffle_audit.py
tools/exp_two_channel_universality.py
tools/exp_two_layer_universality.py

exec
/bin/bash -lc 'python tools/dnd_scenario.py --best' in /opt/MM_D-ND
 succeeded in 140ms:
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
TRAJECTORY_APPLY_20260507_0901: score=0.707  — TRAJECTORY_APPLY_20260507_0901: incerto (i=0.7) — massimo potere discriminante

codex
La massima informazione formale è META, ma il punto concreto della convergenza è il perimetro del gate: testare se il supporto one-sided sopravvive quando l'ordine viene generato da segnali semi-reali, non da etichette costruite. Cerco il codice dei gate precedenti per riusare la stessa definizione senza cambiare oggetto.
exec
/bin/bash -lc 'ls -t tools/data/reports/*.md | head -12' in /opt/MM_D-ND
 succeeded in 0ms:
tools/data/reports/latest.md
tools/data/reports/agent_20260507_0901.md
tools/data/reports/agent_20260507_0803.md
tools/data/reports/agent_20260507_0330.md
tools/data/reports/agent_20260506_1955.md
tools/data/reports/evolution_20260506_1941.md
tools/data/reports/agent_20260506_1941.md
tools/data/reports/agent_20260506_0625.md
tools/data/reports/evolution_20260506_0330.md
tools/data/reports/agent_20260506_0330.md
tools/data/reports/agent_20260505_1022.md
tools/data/reports/evolution_20260505_0330.md

exec
/bin/bash -lc "rg -n \"denominator|ORDER_DENOMINATOR|gate|original-vs-shuffle|one-sided\" tools -g '*.py' -g '*.md'" in /opt/MM_D-ND
 succeeded in 0ms:
tools/data/notte_20260329_0330.md:32:## Combo A11: 17 anomalie propagate
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:242:        "denominator": denom,
tools/exp_two_layer_universality.py:15:  2. For each, build Markov-k surrogates (k=0,1,2,3)
tools/exp_two_layer_universality.py:68:def generate_markov_surrogate(gaps, order, n_bins=12, rng=None):
tools/exp_two_layer_universality.py:305:        # Markov surrogates for k=0,1,2
tools/exp_two_layer_universality.py:315:                        surr = generate_markov_surrogate(gaps, mk, rng=rng)
tools/dnd_curva.py:449:    print(f"    Le due equazioni sono legate:")
tools/exp_crossover_universality.py:21:  - Null: 15 Cramer surrogates (should be flat at Poisson)
tools/exp_crossover_universality.py:212:# === Null baseline: Cramer surrogates ===
tools/exp_crossover_universality.py:213:print(f"\nComputing {N_SURR} Cramer surrogates...")
tools/exp_crossover_universality.py:250:        print(f"  Surrogate {si+1}/{N_SURR}")
tools/exp_crossover_universality.py:304:    "N_surrogates": N_SURR,
tools/exp_alpha_stability.py:7:Reusable: accepts --n_primes, --n_windows, --n_surrogates, --max_lag.
tools/exp_alpha_stability.py:81:    parser.add_argument("--n_surrogates", type=int, default=10, help="Shuffled surrogates per window")
tools/exp_alpha_stability.py:123:        # Shuffled surrogates
tools/exp_alpha_stability.py:126:        for _ in range(args.n_surrogates):
tools/data/reports/agent_20260410_0330.md:25:- **Null baseline**: 15 shuffled surrogates per window (order destroyed, distribution preserved)
tools/data/reports/agent_20260410_0330.md:43:All z-scores > 6. Shuffled surrogates give A ~ 0 at every scale.
tools/exp_acf_amplitude_scaling.py:50:    parser.add_argument('--n_surrogates', type=int, default=10)
tools/exp_acf_amplitude_scaling.py:82:        # Surrogate ACFs (shuffled)
tools/exp_acf_amplitude_scaling.py:83:        A_surrogates = []
tools/exp_acf_amplitude_scaling.py:84:        for _ in range(args.n_surrogates):
tools/exp_acf_amplitude_scaling.py:88:            A_surrogates.append(A_s)
tools/exp_acf_amplitude_scaling.py:90:        A_shuf_mean = np.mean(A_surrogates)
tools/exp_acf_amplitude_scaling.py:91:        A_shuf_std = np.std(A_surrogates) if np.std(A_surrogates) > 0 else 1e-10
tools/data/reports/agent_20260506_1955.md:15:> When the five canonical observables are measured under uniform partial shuffle, does observable collinearity break in structured domains, or only in controls where original-vs-shuffle denominators are weak?
tools/data/reports/agent_20260506_1955.md:25:- reported weak observable count using the fixed gate `abs(original - shuffle_mean) / shuffle_std < 2`.
tools/data/reports/agent_20260506_1955.md:57:2. **Observed collinearity breaking is concentrated in weak-denominator controls.** Poisson has the highest apparent rank among the three-run means (`2.196`), but all five observables are weak against full shuffle in every run. Prime-shuffle behaves similarly: rank is unstable and 4-5 of 5 observables are weak. This mirrors the denominator lesson from perturbation rank without repeating the perturbation-rank experiment.
tools/data/reports/agent_20260506_1955.md:59:3. **The 05-05 observable-rank result survives only after narrowing its language.** The valid statement is not "five probes are always one thing." The scoped statement is: under uniform partial shuffle and canonical observables, primes and GUE show a dominant one-coordinate retention response; controls can show larger PCA rank, but that rank is not structural when the original-vs-shuffle denominators are absent.
tools/data/reports/agent_20260506_1955.md:61:4. **GUE is the cleanest conditioning check.** In the main run, all five GUE observables pass the denominator gate and still give rank `1.060`. This makes GUE the best positive control for "low rank despite valid denominators." Poisson is the negative control for "high rank without valid denominators."
tools/data/reports/agent_20260506_1955.md:66:> observables_registry version + canonical observable list + original-vs-shuffle z per observable + control domains.
tools/data/reports/agent_20260506_1955.md:68:In this perimeter, high observable-rank is not the sign of richer structure when it appears in Poisson or prime-shuffle controls; it is a warning that retention ratios are being formed on weak denominators. The stable cross-domain result is narrower:
tools/data/reports/agent_20260506_1955.md:70:> uniform partial shuffle exposes one dominant retention coordinate in conditioned structured domains (primes, GUE), while apparent multi-coordinate behavior in Poisson/shuffle controls is denominator-weak.
tools/data/reports/agent_20260506_1955.md:73:What opens now: the next non-redundant test is not another PCA audit. It is **selective operator coupling**: use perturbations that act separately on pair-scale and triple-scale structure, but report only observables whose denominator gate survives. If selective operators rotate primes while GUE stays collinear, the lab gets a real cross-domain discriminator; if both stay collinear, the current observable suite is overcomplete for this question.
tools/data/reports/agent_20260506_1955.md:76:- **L1 hard constraint vs bias**: no zero/always claim. "Weak" means `abs(z) < 2` in this declared gate.
tools/data/reports/agent_20260506_1955.md:77:- **L2 quantity vs ratio**: PCA rank and retention ratios are interpreted only after raw z denominators.
tools/data/reports/agent_20260423_0330.md:58:2. **Brody β decays 5x faster toward Poisson.** β drops from 0.42 to 0.29 over the same range (R²=0.72). Gap-level correlations (the GUE signature) decorrelate with scale. But this decorrelation does NOT propagate into the mod-6 sequential memory.
tools/evolution_report.md:2:Il passo ha preso il denominator gate fuori da BOUNDARY e lo ha fatto transitare
tools/evolution_report.md:5:locale: non "beta BOUNDARY che ricompare", ma supporto one-sided dell'ordine
tools/evolution_report.md:12:la scissione fra supporto del denominatore e coordinata beta emerge nel deposito,
tools/evolution_report.md:16:Il nodo regressivo e' il contratto di trasferibilita'. Il gate portava due
tools/evolution_report.md:17:relazioni fuse: supporto original-vs-shuffle e layer di ambiguita' del protocollo.
tools/evolution_report.md:23:- Trattare `order_denominator_gate` come parola autonoma del pipeline: supporto
tools/evolution_report.md:24:  one-sided valido anche con endpoint-stable vuoto.
tools/evolution_report.md:26:  collasso del denominatore.
tools/exp_markov_layer_recovery_audit.py:10:  Recent reports used Mk surrogate capture to claim two visible memory layers.
tools/exp_markov_layer_recovery_audit.py:17:  - empirical_Mk1: prime-trained Mk1 surrogate; known order 1
tools/exp_markov_layer_recovery_audit.py:18:  - empirical_Mk2: prime-trained Mk2 surrogate; known order 2
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_dipolar_decomposition.py:13:  2. Generate Markov-1 surrogate with that matrix (same pair stats, no higher memory)
tools/exp_markov_dipolar_decomposition.py:14:  3. Generate Markov-0 surrogate (iid from marginal distribution)
tools/exp_markov_dipolar_decomposition.py:204:    # === Markov-1 surrogates ===
tools/exp_markov_dipolar_decomposition.py:222:    # === Markov-0 surrogates (iid from marginal) ===
tools/data/reports/incident_20260504_1138.md:21:4. Poisson è rumore da denominatore piccolo, non controesempio strutturale.
tools/data/reports/agent_20260507_0330.md:11:and the original-vs-shuffle denominator gate.
tools/data/reports/agent_20260507_0330.md:17:> layer where classification is ambiguous and denominator support collapses?
tools/data/reports/agent_20260507_0330.md:20:Tool created: `tools/exp_boundary_mixture_gate.py`
tools/data/reports/agent_20260507_0330.md:27:- denominator gate: observable is stable when `abs(original - shuffle_mean) / shuffle_std >= 2`;
tools/data/reports/agent_20260507_0330.md:30:The endpoint-gated classifier is intentionally reported. In this perimeter it is empty because the Poisson endpoint has almost no stable original-vs-shuffle denominators. That is not discarded; it is the core META result.
tools/data/reports/agent_20260507_0330.md:55:but denominator support is mostly absent. The transition is not centered at
tools/data/reports/agent_20260507_0330.md:74:1. **The clean two-class boundary fails under denominator gating.** Pure GUE and pure Poisson are separable in all-observable space, but there are no observables stable at both endpoints under the declared gate. The Poisson pole is a weak-denominator pole: classification can still place it, but retention-normalized structural claims cannot use it as if it had the same denominator support as GUE.
tools/data/reports/agent_20260507_0330.md:76:2. **The operational boundary is a layer, not a line, in this synthetic perimeter.** Both the main run and the seed check isolate beta 0.3-0.4 as the ambiguous layer. In the main run the nearest-centroid margin falls to `0.070-0.083`, while ambiguous fraction rises to `0.812-0.875`. This is the measured form of the "third included" here: not a metaphysical third class, but a beta region where two-class assignment and denominator support are both unstable.
tools/data/reports/agent_20260507_0330.md:78:3. **Denominator collapse precedes full Poisson classification.** Stable-observable count drops from about `3.3` at beta 0.1-0.2 to `2.5` at beta 0.3 and `1.625` at beta 0.4. By beta 0.5 the classifier is fully Poisson-labeled, but only `0.750/5` observables remain stable on average. The loss of denominator support is therefore part of the boundary phenomenon, not an after-the-fact nuisance.
tools/data/reports/agent_20260507_0330.md:80:4. **The previous META constraints are extended, not replaced.** The 19:41 and 19:55 constraints still hold. This run adds that a boundary claim also needs a layer map: endpoint separability alone can hide the fact that one endpoint has no original-vs-shuffle denominator and that the transition region carries the actual instability.
tools/data/reports/agent_20260507_0330.md:85:> observables_registry version + canonical observable list + original-vs-shuffle z gate per observable + endpoint-stable observable set + beta/window layer where classification margin is ambiguous.
tools/data/reports/agent_20260507_0330.md:89:> In the synthetic mixture perimeter tested here, the GUE-Poisson boundary is an operational layer at beta 0.3-0.4: classification is ambiguous there, and denominator support collapses across the transition. The Poisson endpoint remains classifiable but denominator-weak, so it cannot serve as a symmetric structural pole for gated retention claims.
tools/data/reports/agent_20260507_0330.md:94:> Which scale window has the same signature as the synthetic beta 0.3-0.4 layer: low classifier margin plus falling original-vs-shuffle denominators?
tools/data/reports/agent_20260507_0330.md:99:- **L1 hard constraint vs bias**: no zero/always claim. "Endpoint gate is empty" means no observable reached frequency >= 0.75 across both endpoints under `abs(z) >= 2`; it does not mean the observables are identically zero.
tools/data/reports/agent_20260507_0330.md:100:- **L2 quantity vs ratio**: classification margin is reported together with stable-observable count and z-gate frequencies. Ratios are not interpreted without denominator support.
tools/data/reports/agent_20260507_0330.md:103:- **L5 re-discovery**: this is a finite-sample diagnostic of crossover and noisy denominator normalization in classical GUE/Poisson spacings. It is not tagged as a new RMT theorem.
tools/data/reports/agent_20260507_0330.md:106:- Script: `tools/exp_boundary_mixture_gate.py`
tools/data/reports/agent_20260507_0330.md:107:- Main data: `tools/data/boundary_mixture_gate_20260507_0330.json`
tools/data/reports/agent_20260507_0330.md:108:- Seed check: `tools/data/boundary_mixture_gate_20260507_0330_seedcheck.json`
tools/exp_two_channel_cross_domain.py:163:def analyze_at_scale(gaps, binary, mag, n_surrogates=30, rng=None):
tools/exp_two_channel_cross_domain.py:176:    for _ in range(n_surrogates):
tools/exp_two_channel_cross_domain.py:209:def analyze_at_scale_primes(primes_window, n_surrogates=30, rng=None):
tools/exp_two_channel_cross_domain.py:227:    for _ in range(n_surrogates):
tools/exp_two_channel_cross_domain.py:266:def run(n_primes=200000, gue_matrices=50, gue_size=800, n_windows=8, window=5000, n_surrogates=20):
tools/exp_two_channel_cross_domain.py:283:        obs = analyze_at_scale_primes(pw, n_surrogates, rng)
tools/exp_two_channel_cross_domain.py:311:        obs = analyze_at_scale(gaps, binary, mag, n_surrogates, rng)
tools/exp_two_channel_cross_domain.py:337:        obs = analyze_at_scale(gaps, binary, mag, n_surrogates, rng)
tools/exp_two_channel_cross_domain.py:422:    parser.add_argument('--n_surrogates', type=int, default=20)
tools/exp_two_channel_cross_domain.py:426:                  args.n_windows, args.window, args.n_surrogates)
tools/exp_two_channel_cross_domain.py:441:            'n_surrogates': args.n_surrogates,
tools/exp_beta_crossover.py:18:    python exp_beta_crossover.py [--N 2000] [--betas 30] [--surrogates 10]
tools/exp_beta_crossover.py:29:N_SURROGATES = 10  # shuffled surrogates per point
tools/exp_beta_crossover.py:182:    parser.add_argument("--surrogates", type=int, default=N_SURROGATES)
tools/exp_beta_crossover.py:187:    NS = args.surrogates
tools/exp_beta_crossover.py:190:    print(f"Matrix N={N}, beta sweep: {n_betas} values, surrogates={NS}\n")
tools/exp_beta_crossover.py:369:            "surrogates": NS,
tools/data/reports/agent_20260429_1041.md:89:1. **Mod-3 non-zero self-transition is algebraically forbidden**, not statistically suppressed. The z-score is -42 (mean) not because of strong bias, but because the numerator is exactly -0.500 (real = 0, shuffle = 0.50) with small denominator noise. The signal has no scaling because it's a theorem with no free parameters.
tools/build_lab_graph.py:382:            # Scoperte pre-tensione: mappa via teorie collegate
tools/build_lab_graph.py:399:                # QxG scoperte → tensioni legate al vuoto quantistico-gravitazionale
tools/build_lab_graph.py:833:    # step 5 (gateato dal falsifier counter-pole). Rimosso shutil.copy
tools/build_lab_graph.py:834:    # hardcoded qui il 29/04 perche' bypassava Phase A gate (build_lab_graph
tools/build_lab_graph.py:835:    # gira PRE-gate; il flagged report 0958 era sync-ato a THIA + Docker
tools/exp_geodesic_deviation_primes.py:14:4. Compare ALL of these against shuffled-gap surrogates
tools/exp_geodesic_deviation_primes.py:72:# --- Shuffled-gap surrogates ---
tools/exp_geodesic_deviation_primes.py:115:print(f"\n  PSD ratio (prime/surrogate) by frequency band:")
tools/exp_geodesic_deviation_primes.py:125:# --- Cramer surrogates (model comparison) ---
tools/exp_geodesic_deviation_primes.py:187:        "n_surrogates": n_surr,
tools/exp_geodesic_deviation_primes.py:194:        "n_surrogates": n_surr,
tools/data/reports/agent_20260417_0803.md:23:- **Null baseline**: 15 shuffled surrogates (order destroyed, marginals preserved)
tools/exp_psd_amplitude_scaling.py:31:    p.add_argument('--n_surrogates', type=int, default=15)
tools/exp_psd_amplitude_scaling.py:89:        # Shuffled surrogates
tools/exp_psd_amplitude_scaling.py:92:        for _ in range(args.n_surrogates):
tools/exp_psd_amplitude_scaling.py:172:        'n_surrogates': args.n_surrogates,
tools/data/reports/agent_diag2.md:18:- Null: 20 Cramer surrogates (gaps ~ Exp(ln p)) + 20 shuffled surrogates
tools/data/reports/agent_diag2.md:75:2. **dR_acf1 is prime-specific**: z = -3.0 vs Cramer. Primes push curvature autocorrelation to -0.484, compatible with -1/2 = H (de Sitter Hubble). Cramer surrogates sit at -0.463. The difference is 0.021, more than 3 sigma of the null distribution.
tools/data/reports/agent_20260418_0330.md:23:3. **Factorial surrogates** (20 each):
tools/data/reports/agent_20260418_0330.md:64:### Test 3: Factorial surrogates — Z/6Z has zero effect
tools/data/reports/agent_20260418_0330.md:119:- Script: `tools/exp_acf_z6z_mechanism.py` (reusable: --n_primes, --max_lag, --n_surrogates)
tools/lab_valutatore.py:31:  sono gated dal flag --execute. Default: non eseguire.
tools/lab_valutatore.py:107:- (f) ha connesso teorie / domini / pair prima scollegate — CONTA FORTE
tools/exp_markov_k_direction.py:16:  1. Build Markov-k surrogates for k=0,1,2,3 from real prime gaps
tools/exp_markov_k_direction.py:18:  3. Per-source shuffle baseline (each surrogate vs its own permutation)
tools/exp_markov_k_direction.py:120:    """Generate Markov-k surrogate with fallback and gap-pool sampling."""
tools/data/reports/agent_20260413_0330.md:20:- **Null baseline**: 15 shuffled surrogates per window (destroys sequential order, preserves distribution)
tools/LAB_OPERATIVO.md:16:- Se il risultato è indistinguibile dal surrogate → declassato
tools/data/reports/_quarantine_falsifier_29_04/agent_20260429_0833.md:69:- Script: `tools/exp_two_channel_boundary.py` (reusable with --n_primes, --window, --n_surrogates)
tools/exp_markov3_observable_hunt.py:10:  1. Compute prime gaps, build Markov-k surrogates (k=0,1,2,3)
tools/exp_markov3_observable_hunt.py:67:def generate_markov_surrogate(gaps, order, n_bins=12, rng=None):
tools/exp_markov3_observable_hunt.py:68:    """Generate a surrogate sequence from a Markov-k model of the gaps."""
tools/exp_markov3_observable_hunt.py:221:                        help='Surrogates per Markov order')
tools/exp_markov3_observable_hunt.py:240:    # For each Markov order, generate surrogates and compute observables
tools/exp_markov3_observable_hunt.py:246:        print(f"\nMarkov-{k}: generating {args.n_surr} surrogates...")
tools/exp_markov3_observable_hunt.py:254:                surr = generate_markov_surrogate(gaps, k, args.n_bins, rng)
tools/exp_markov3_observable_hunt.py:311:        'n_surrogates': args.n_surr,
tools/dnd_scenario.py:1203:            'leverage': 'Foundation — invest here, it propagates everywhere',
tools/dnd_scenario.py:1274:                'what': label or f"Investigate: {blind['id']}",
tools/exp_acf_z6z_mechanism.py:11:3. Factorial surrogates — Z/6Z with/without trend, trend without Z/6Z,
tools/exp_acf_z6z_mechanism.py:15:    python tools/exp_acf_z6z_mechanism.py [--n_primes N] [--max_lag K] [--n_surrogates S]
tools/exp_acf_z6z_mechanism.py:77:    parser.add_argument('--n_surrogates', type=int, default=20)
tools/exp_acf_z6z_mechanism.py:147:    # TEST 3: Factorial surrogates
tools/exp_acf_z6z_mechanism.py:150:    print(f"TEST 3: Factorial surrogates ({args.n_surrogates} each)")
tools/exp_acf_z6z_mechanism.py:154:    NS = args.n_surrogates
tools/exp_acf_z6z_mechanism.py:206:    # 3e: Anti-correlated surrogates without trend (Gaussian AR(1) with acf1 matching primes)
tools/exp_acf_z6z_mechanism.py:321:        'n_surrogates': args.n_surrogates,
tools/exp_acf_z6z_mechanism.py:339:        'test3_surrogates': {
tools/exp_dR_brody_connection.py:13:  If Cramer surrogates show the same -> tautological (artifact of g=(p/2)^2 mapping).
tools/exp_dR_brody_connection.py:14:  If Cramer surrogates differ -> prime-specific structure.
tools/exp_dR_brody_connection.py:16:Null baseline: 20 Cramer surrogates + 20 shuffled.
tools/exp_dR_brody_connection.py:190:print(f"\nComputing {N_SURR} surrogates each...")
tools/exp_dR_brody_connection.py:293:    "N_surrogates": N_SURR,
tools/data/reports/agent_20260411_0330.md:18:- **Null baseline**: 10 shuffled surrogates per window (same distribution, order destroyed)
tools/data/reports/agent_20260411_0330.md:73:1. **Alpha drifts from ~2 to ~0.7.** The power-law exponent of the gap ACF is NOT universal at 1.00. It decreases with prime scale. The "alpha=1.00, R2=0.998" from ACF_1K_LAW is an aggregate average over a drifting exponent — the aggregate R2 is high because it averages over many scales.
tools/data/reports/agent_20260411_0330.md:87:- The aggregate "alpha=1.00" needs reinterpretation: it's a weighted average of a drifting exponent, not a universal law
tools/data/reports/agent_20260415_0330.md:19:- **Null baseline**: 20 shuffled surrogates (same gap distribution, order destroyed)
tools/data/reports/agent_20260415_0330.md:20:- **Scale test**: 5 windows across ln(p) = 16.0 to 18.3, Q1 vs Q5 spread + 15 shuffled surrogates per window
tools/dnd_zero_notturno.py:373:    # === Misure aggregate ===
tools/data/reports/agent_20260501_0330.md:17:- **Method**: Build the empirical transition matrix T(g_{n+1} | g_n) from real prime gaps (binned into 7 categories: g ~ 2,4,6,8,10,12,14+). Generate Markov-1 surrogates (same pair statistics, no higher-order memory) and Markov-0 surrogates (iid from marginal, no pair memory). Compute dipolar angle for each.
tools/exp_desitter_unification.py:135:print(f"\nComputing {N_SURROGATES} Cramer surrogates...")
tools/exp_desitter_unification.py:171:        print(f"  Cramer surrogate {i+1}/{N_SURROGATES}")
tools/exp_desitter_unification.py:173:print(f"\nComputing {N_SURROGATES} shuffled surrogates...")
tools/exp_desitter_unification.py:194:        print(f"  Shuffled surrogate {i+1}/{N_SURROGATES}")
tools/exp_desitter_unification.py:273:        "n_surrogates": N_SURROGATES,
tools/exp_desitter_unification.py:280:        "n_surrogates": N_SURROGATES,
tools/data/reports/agent_test_field.md:20:- **Null baseline**: 10 Cramer random surrogates per window (independent exponential gaps, same local density)
tools/data/reports/agent_test_field.md:32:**Cramer surrogates**: beta_Cramer ~ 0.005, <r>_Cramer ~ 0.386, acf1_Cramer ~ 0.001 at all scales. The excess over Cramer is the signal — all three excesses are large, positive (beta, <r>) or negative (acf1), and shrink with scale.
tools/data/reports/agent_test_field.md:54:The hierarchy raises a question: **is the ordering (beta first, acf1 last) a property of primes, or of any correlated sequence approaching independence?** If Cramer surrogates with injected mild anti-correlation show the same ordering, it's generic. If primes have a different ordering than synthetic anti-correlated sequences, the hierarchy itself is prime-specific content.
tools/data/reports/agent_test_field.md:58:- Script: `tools/exp_poisson_convergence.py` (reusable with --n_primes, --n_windows, --n_surrogates, --window_size)
tools/exp_markov_memory_by_gue_type.py:250:    # Aggregate by type
tools/exp_markov_memory_by_gue_type.py:251:    print("\nAggregate by GUE type:")
tools/data/reports/agent_20260409_0330.md:18:- **Null baseline**: 20 shuffled-gap surrogates (same distribution, destroyed order)
tools/data/reports/agent_20260420_0330.md:20:- **Null baseline**: 20 shuffled surrogates per channel (preserve distribution, destroy order)
tools/exp_brody_crossover.py:3:Experiment: Brody parameter crossover for primes vs Cramer surrogates.
tools/exp_brody_crossover.py:13:- Compare to Cramer surrogates (exponential gaps) at each scale
tools/exp_brody_crossover.py:59:def cramer_surrogate(densities, n_gaps):
tools/exp_brody_crossover.py:104:        # Cramer surrogates
tools/exp_brody_crossover.py:194:            "n_surrogates": N_SURROGATES,
tools/data/reports/evolution_20260506_1941.md:3:Il passo ha costruito una curva size-rank con gate sul denominatore, chiudendo il ciclo aperto dal run 03:30 (secondo asse GUE) e dal run 06:25 (restrizione). La traiettoria è stata: inflazione di rango osservata → ipotesi denominatore debole → costruzione del gate → falsificazione via controlli Poisson/shuffle. Il produttore ha invertito su se stesso: ha testato se il proprio strumento (PCA su retention) produceva artefatti, e ha trovato che sì. Passo autologico netto.
tools/data/reports/evolution_20260506_1941.md:15:1. **Mappa denominatore per osservabile**: il report apre esattamente questa direzione — per ogni osservabile, mappare la regione (perturbazione × dominio × scala) dove il segnale originale-vs-shuffle è reale. Non è più PCA; è una mappa di validità dello strumento stesso.
tools/data/reports/evolution_20260506_1941.md:17:2. **Gate come primitiva del registry**: il denominatore gate (`|z| >= 2`) potrebbe entrare nell'`observables_registry.py` come metadato per osservabile, non come logica ripetuta in ogni esperimento.
tools/data/reports/evolution_20260506_1941.md:23:Il ciclo seguente può proseguire sulla mappa denominatore — è la direzione naturale. Ma prima: riparare l'osservabilità dell'autopsy sul path trajectory_apply, altrimenti il prossimo run che fallisce al CLI produrrà di nuovo un health cieco.
tools/exp_poisson_convergence.py:14:    python exp_poisson_convergence.py [--n_primes N] [--n_windows W] [--n_surrogates S]
tools/exp_poisson_convergence.py:71:def cramer_random_primes(primes, n_surrogates=10):
tools/exp_poisson_convergence.py:74:    for _ in range(n_surrogates):
tools/exp_poisson_convergence.py:103:def measure_cramer_window(primes_window, n_surrogates=10):
tools/exp_poisson_convergence.py:104:    """Measure observables on Cramer surrogates for a window."""
tools/exp_poisson_convergence.py:111:    for _ in range(n_surrogates):
tools/exp_poisson_convergence.py:131:def run_experiment(n_primes_target=6_000_000, n_windows=25, n_surrogates=10, window_size=50_000):
tools/exp_poisson_convergence.py:155:    print(f"\nMeasuring {len(window_starts)} windows, {window_size} primes each, {n_surrogates} Cramer surrogates...\n")
tools/exp_poisson_convergence.py:167:        c = measure_cramer_window(window, n_surrogates)
tools/exp_poisson_convergence.py:308:    parser.add_argument('--n_surrogates', type=int, default=10, help='Cramer surrogates per window')
tools/exp_poisson_convergence.py:312:    results = run_experiment(args.n_primes, args.n_windows, args.n_surrogates, args.window_size)
tools/exp_two_channel_shuffle_audit.py:209:    # Aggregate shuffle statistics
tools/data/reports/agent_20260501_0725.md:17:- **Method**: Build Markov-k surrogates for k=0,1,2,3 from 100K prime gaps (p > 10007). Compare dipolar angle of each Markov order to real primes.
tools/data/reports/agent_20260501_0725.md:20:  2. **Gap-pool sampling** — surrogates draw actual gap values from within-bin pools, not bin centers
tools/data/reports/agent_20260501_0725.md:21:  3. **Per-source shuffle baseline** — each surrogate normalized against its own permutation
tools/data/reports/agent_20260501_0725.md:72:4. **Binning resolution matters.** The "14+" catch-all bin in fixed-edge binning loses within-bin value correlations. The Markov surrogate randomizes gap values within the bin, destroying ordering that real primes preserve. Equal-count binning resolves this by distributing the tail across multiple bins.
tools/data/reports/agent_20260405_0916.md:13:- **Null**: 20 Cramer surrogates per window (exponential gaps, same density)
tools/riemann_R.py:371:    print("  Res(f^n, z0) = g(z0)/h'(z0) dove g = numeratore, h = denominatore")
tools/exp_boundary_mixture_gate.py:3:exp_boundary_mixture_gate.py
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:17:- original-vs-shuffle z-score per observable;
tools/exp_boundary_mixture_gate.py:18:- endpoint separability in all observables and in gate-stable observables;
tools/exp_boundary_mixture_gate.py:74:    """Return original observables, shuffle baseline std, and original-vs-shuffle z."""
tools/exp_boundary_mixture_gate.py:155:def summarize_gate(rows: list[dict], z_min: float) -> dict:
tools/exp_boundary_mixture_gate.py:218:    gate = summarize_gate(rows, args.z_min)
tools/exp_boundary_mixture_gate.py:220:    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
tools/exp_boundary_mixture_gate.py:223:        "experiment": "boundary_mixture_gate",
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:228:        "gate": gate,
tools/exp_boundary_mixture_gate.py:230:        "classification_endpoint_gated": gated_classification,
tools/exp_boundary_mixture_gate.py:241:    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
tools/exp_boundary_mixture_gate.py:245:        "endpoint_distance_gated="
tools/exp_boundary_mixture_gate.py:246:        f"{gated_classification['endpoint_distance']:.3f}"
tools/exp_boundary_mixture_gate.py:248:    if gate["endpoint_stable_observables"]:
tools/exp_boundary_mixture_gate.py:249:        print("beta stable_count margin_gated ambiguous_gated poisson_fraction_gated")
tools/exp_boundary_mixture_gate.py:251:        print("endpoint gate is empty; printing all-observable classification")
tools/exp_boundary_mixture_gate.py:255:        stable_count = gate["layers"][key]["stable_count_mean"]
tools/exp_boundary_mixture_gate.py:256:        source = gated_classification if gate["endpoint_stable_observables"] else all_classification
tools/exp_boundary_mixture_gate.py:277:    parser.add_argument("--out", default="tools/data/boundary_mixture_gate.json")
tools/data/reports/agent_20260506_1941.md:11:> Does the second perturbation axis stabilize as sample size grows, or is rank inflated when retention denominators are weak relative to full-shuffle baselines?
tools/data/reports/agent_20260506_1941.md:25:- denominator gate: observable is stable only when `abs(original - shuffle_mean) / shuffle_std >= 2`.
tools/data/reports/agent_20260506_1941.md:29:- `stable_rank`: PCA effective rank after dropping observables whose original-vs-shuffle denominator is weak.
tools/data/reports/agent_20260506_1941.md:60:1. **Perturbation rank is not interpretable without denominator gating.** In this perimeter, Poisson and prime-shuffle controls can show `rank_all` near 1.8-2.0. Because their original-vs-shuffle denominators are mostly weak, that rank is a retention-normalization artifact unless the stable-observable screen also supports it.
tools/data/reports/agent_20260506_1941.md:62:2. **GUE does not show a stable second axis on canonical observables up to N=2048.** GUE `rank_all` falls from 1.913 at N=256 to 1.234 at N=2048; PC2 falls from 16.4% to 4.6%. After denominator gating, GUE stable rank stays close to 1.1-1.2.
tools/data/reports/agent_20260506_1941.md:64:3. **The old L2-driven sign-flip should not be promoted without a denominator check.** Under canonical observables, GUE `L2` is weak relative to shuffle at every tested size and is stable in 0/8 replicates at N >= 512. This does not prove every L2 sign effect is false; it restricts such effects to local/sample-specific observations unless the denominator survives.
tools/data/reports/agent_20260506_1941.md:71:> effective rank + PC2 + observable registry version + original-vs-shuffle z gate per observable.
tools/data/reports/agent_20260506_1941.md:75:> all-observable perturbation rank can inflate in weak-denominator regimes; after denominator gating, GUE and primes are both close to one perturbation coordinate in this perimeter, while Poisson/shuffle controls show why ungated rank is not structural evidence.
tools/data/reports/agent_20260506_1941.md:78:What opens now: the lab can keep using perturbation rank, but only as a gated observable. The next useful movement is not more PCA; it is an operator-level denominator map: for each observable, identify the perturbation/domain/scale region where `original - shuffle` is a real signal rather than a noisy divisor.
tools/data/reports/agent_20260506_1941.md:81:- **L1 hard constraint vs bias**: no zero/always claim. "Weak" means `abs(z) < 2` in the declared gate, not absence of signal.
tools/data/reports/agent_20260506_1941.md:82:- **L2 quantity vs ratio**: retention ratios are not read alone; raw denominator z-scores are reported first.
tools/exp_two_channel_decomposition.py:23:    python tools/exp_two_channel_decomposition.py [--n_primes N] [--n_windows W] [--n_surrogates S]
tools/exp_two_channel_decomposition.py:120:def measure_coherence_length(channel, n_surrogates=20, window_sizes=None):
tools/exp_two_channel_decomposition.py:152:        # Shuffled surrogates
tools/exp_two_channel_decomposition.py:154:        for _ in range(n_surrogates):
tools/exp_two_channel_decomposition.py:188:def scaling_analysis(primes, n_windows=15, n_surrogates=10):
tools/exp_two_channel_decomposition.py:240:        for _ in range(n_surrogates):
tools/exp_two_channel_decomposition.py:287:    parser.add_argument('--n_surrogates', type=int, default=10, help='Surrogates per window')
tools/exp_two_channel_decomposition.py:319:    scaling = scaling_analysis(primes, n_windows=args.n_windows, n_surrogates=args.n_surrogates)
tools/dnd_autoricerca.py:477:        # Il surrogate dipende dal tipo di dato:
tools/dnd_autoricerca.py:863:    # Null baseline gate: se non discrimina, declassa findings a vincoli
tools/dnd_autoricerca.py:1880:        report_lines.append(f"\n## Combo A11: {len(campo['anomalie'])} anomalie propagate")
tools/data/reports/agent_20260506_0330.md:81:- **L2 quantita vs ratio**: Retentions are ratios normalized by (original - shuffle_mean). The denominators differ across observables but are used consistently within each observable. No cross-space ratio comparison.
tools/dnd_torre.py:572:    Ma G·ℏ e Λ sono legate dalla cosmologia (non indipendenti in ultima istanza).
tools/exp_magnitude_psd_from_acf.py:206:    print(f"\n--- Null baseline: shuffled magnitude ({args.n_shuffle} surrogates) ---")
tools/data/reports/agent_20260419_0330.md:20:- **Null baseline**: 15 shuffled surrogates per window per channel
tools/exp_coherence_length.py:14:    python tools/exp_coherence_length.py [--N_primes 6000000] [--n_surrogates 20]
tools/exp_coherence_length.py:45:def measure_coherence(gaps, window_lengths, n_windows=200, n_surrogates=20):
tools/exp_coherence_length.py:61:        r_shuf_lists = [[] for _ in range(n_surrogates)]
tools/exp_coherence_length.py:69:            for si in range(n_surrogates):
tools/exp_coherence_length.py:77:        # Pool all surrogate measurements
tools/exp_coherence_length.py:98:            'n_surrogates': int(n_surrogates),
tools/exp_coherence_length.py:105:                               scale_windows=5, n_windows=150, n_surrogates=15):
tools/exp_coherence_length.py:128:                                     n_surrogates=n_surrogates)
tools/exp_coherence_length.py:149:    parser.add_argument('--n_surrogates', type=int, default=20)
tools/exp_coherence_length.py:173:                                        n_surrogates=args.n_surrogates)
tools/exp_coherence_length.py:192:        scale_windows=5, n_windows=150, n_surrogates=15
tools/exp_coherence_length.py:242:        'n_surrogates': args.n_surrogates,
tools/data/reports/agent_20260422_0330.md:22:- **Null**: 15 shuffled magnitude surrogates (same distribution, destroyed order)
tools/data/reports/agent_20260408_0330.md:19:- **Null baselines**: (a) 20 shuffled surrogates (same distribution, no order), (b) 10 Z/6Z-confined Cramer surrogates (candidates at 6k+/-1, survival ~3/ln(n))
tools/exp_denominator_gate_transfer_matrix.py:3:exp_denominator_gate_transfer_matrix.py
tools/exp_denominator_gate_transfer_matrix.py:5:Reusable META audit for the denominator gate transfer matrix.
tools/exp_denominator_gate_transfer_matrix.py:7:The experiment moves the original-vs-shuffle denominator gate away from the
tools/exp_denominator_gate_transfer_matrix.py:14:- original-vs-shuffle z-score for each observable;
tools/exp_denominator_gate_transfer_matrix.py:16:- endpoint classification using all observables and endpoint-gated observables;
tools/exp_denominator_gate_transfer_matrix.py:187:def summarize_gate(rows: list[dict], z_min: float) -> dict:
tools/exp_denominator_gate_transfer_matrix.py:254:    gate = summarize_gate(rows, args.z_min)
tools/exp_denominator_gate_transfer_matrix.py:256:    one_sided_classification = classify_layers(rows, gate["coherent_one_sided_observables"])
tools/exp_denominator_gate_transfer_matrix.py:258:        "gate": gate,
tools/exp_denominator_gate_transfer_matrix.py:260:        "classification_one_sided_gated": one_sided_classification,
tools/exp_denominator_gate_transfer_matrix.py:268:        gate = data["gate"]
tools/exp_denominator_gate_transfer_matrix.py:270:        class_gate = data["classification_one_sided_gated"]
tools/exp_denominator_gate_transfer_matrix.py:272:            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
tools/exp_denominator_gate_transfer_matrix.py:273:            "endpoint_stable_observables": gate["endpoint_stable_observables"],
tools/exp_denominator_gate_transfer_matrix.py:275:            "endpoint_distance_one_sided_gated": class_gate["endpoint_distance"],
tools/exp_denominator_gate_transfer_matrix.py:277:            "ambiguous_beta_one_sided_gated": class_gate["ambiguous_beta"],
tools/exp_denominator_gate_transfer_matrix.py:278:            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
tools/exp_denominator_gate_transfer_matrix.py:279:            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
tools/exp_denominator_gate_transfer_matrix.py:291:        "experiment": "denominator_gate_transfer_matrix",
tools/exp_denominator_gate_transfer_matrix.py:292:        "category": "gate_transferability",
tools/exp_denominator_gate_transfer_matrix.py:293:        "question": "Which parts of the denominator gate transfer outside BOUNDARY?",
tools/exp_denominator_gate_transfer_matrix.py:308:    print("perimeter one_sided stable0 stable1 dist_all dist_gate ambiguous_gate")
tools/exp_denominator_gate_transfer_matrix.py:316:            f"{row['endpoint_distance_one_sided_gated']:>9.3f} "
tools/exp_denominator_gate_transfer_matrix.py:317:            f"{row['ambiguous_beta_one_sided_gated']}"
tools/exp_denominator_gate_transfer_matrix.py:331:    parser.add_argument("--out", default="tools/data/denominator_gate_transfer_matrix.json")
tools/data/reports/agent_20260406_1030.md:14:- N=5000 spacings per domain, 20 shuffled surrogates for z-scores
tools/build_agent_field.py:282:    # cycle-test diagnostici (es. applicare un gate cristallizzato a una
tools/build_agent_field.py:366:    # se ripeti lo stesso framing, il falsifier ti blocca al prossimo gate.
tools/build_agent_field.py:491:        "non viene bloccato dal gate. Quello che cade va al cimitero.\n"
tools/build_agent_field.py:504:        "larga, N piccolo vs grande): le percentuali ingannano perche' il denominatore "
tools/exp_excess_scaling.py:13:- Generate 20 Cramer surrogates per window (exponential gaps with same density)
tools/exp_excess_scaling.py:42:# Cramer surrogate: exponential gaps with same mean gap
tools/exp_excess_scaling.py:43:def cramer_surrogate_r(mean_gap, n_gaps, n_surrogates=20):
tools/exp_excess_scaling.py:44:    """Generate Cramer surrogates and return mean <r>."""
tools/exp_excess_scaling.py:46:    for _ in range(n_surrogates):
tools/exp_excess_scaling.py:70:    r_cramer_mean, r_cramer_std = cramer_surrogate_r(mean_gap, len(gaps))
tools/exp_ricci_primes.py:112:n_surrogates = 20
tools/exp_ricci_primes.py:113:surrogate_means = []
tools/exp_ricci_primes.py:114:surrogate_slopes = []
tools/exp_ricci_primes.py:117:for s in range(n_surrogates):
tools/exp_ricci_primes.py:134:    surrogate_means.append(np.mean(surr_R))
tools/exp_ricci_primes.py:143:    surrogate_slopes.append(surr_coeffs[0])
tools/exp_ricci_primes.py:145:surr_mean_avg = np.mean(surrogate_means)
tools/exp_ricci_primes.py:146:surr_mean_std = np.std(surrogate_means)
tools/exp_ricci_primes.py:147:surr_slope_avg = np.mean(surrogate_slopes)
tools/exp_ricci_primes.py:148:surr_slope_std = np.std(surrogate_slopes)
tools/exp_ricci_primes.py:153:print(f"Surrogate <R>: {surr_mean_avg:.6f} +/- {surr_mean_std:.6f}")
tools/exp_ricci_primes.py:156:print(f"\nSurrogate slope: {surr_slope_avg:.8f} +/- {surr_slope_std:.8f}")
tools/exp_ricci_primes.py:200:        "n_surrogates": n_surrogates,
tools/exp_ricci_primes.py:201:        "surrogate_mean_R": round(surr_mean_avg, 8),
tools/exp_ricci_primes.py:202:        "surrogate_std": round(surr_mean_std, 8),
tools/data/reports/agent_20260505_0330.md:64:- **L2 quantita vs ratio**: il risultato usa energie PCA e z-score original-vs-shuffle nello stesso spazio di osservabili; Poisson e' escluso dall'interpretazione strutturale per assenza di segnale, non per percentuale.
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:13:  For 3 perturbation types x 15 alpha levels x 30 surrogates:
tools/exp_selective_layer_decoupling.py:15:  2. Mk1-SELECTIVE: replace position i with Mk1 surrogate value, prob=alpha
tools/exp_selective_layer_decoupling.py:17:  3. Mk2-SELECTIVE: replace position i with Mk2 surrogate value, prob=alpha
tools/exp_selective_layer_decoupling.py:70:# --- Markov surrogate generation ---
tools/exp_selective_layer_decoupling.py:95:def generate_markov_surrogate(gaps, order, n_bins=12, rng=None):
tools/exp_selective_layer_decoupling.py:120:def partial_perturbation(original, surrogate, alpha, rng):
tools/exp_selective_layer_decoupling.py:121:    """Position-wise interpolation: replace original[i] with surrogate[i] with probability alpha."""
tools/exp_selective_layer_decoupling.py:125:    result[mask] = surrogate[mask]
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/harvest_aimorning.py:66:    "/it/doc-dev/gate-cnot-d-nd",
tools/harvest_aimorning.py:67:    "/it/doc-dev/gate-cnot-nel-contesto-del-modello-d-nd",
tools/data/reports/agent_20260503_0330.md:18:- **Method**: Build Markov-k surrogates (k=0,1,2,3) from prime gaps, compute 10 observables on real gaps and surrogates, measure z-score for each (observable, Markov-order) pair
tools/data/reports/agent_20260503_0330.md:22:- **Surrogates**: 40 per Markov order
tools/data/reports/agent_20260503_0330.md:50:SR2 (next-nearest-neighbor spacing ratio) has a notable property: its z-score INCREASES in magnitude from Mk0 (-3.7) to Mk1 (-9.4), then drops to -0.05 at Mk2. The Mk1 surrogate makes SR2 WORSE than the iid shuffle. This happens because Mk1 correctly reproduces the pair correlation (lag-1 anti-correlation), which causes consecutive gaps to anti-correlate, but does NOT reproduce the triple correlation that partially compensates. The partial information of Mk1 amplifies the SR2 deviation. Mk2 restores the full triple structure and SR2 normalizes.
tools/data/reports/agent_20260503_0330.md:58:3. **Markov-3 adds no visible content in any tested observable.** The massive z=6203 from previous entropy measurements is a property of the transition matrix's internal structure (how many distinct states the chain visits), not of any single low-dimensional observable. For practical characterization of prime gap ordering, Markov-2 is sufficient across all 10 observables tested. Perimeter: tested with 10 observables, 100K primes, 40 surrogates, 12 bins.
tools/data/reports/agent_20260503_0330.md:64:**CONFIRMED + NEW on DIPOLAR_ORDERING**: The prime gap ordering decomposes into two independent visible layers. Layer 1 (pairs) lives in (SR, L1) = the dipolar plane. Layer 2 (triples) lives in (SR2, L2, cond_entropy) = the depth plane. SR2 is the sharpest probe for Layer 2 (z=-9.4 under Mk1, z=-0.05 under Mk2). Markov-2 is sufficient for all 10 tested observables. Perimeter: N=100K primes, 40 surrogates per order, 12 equal-count bins, 10 observables.
tools/exp_spectral_2d.py:24:    python exp_spectral_2d.py [--N 10000] [--surrogates 20]
tools/exp_spectral_2d.py:277:def measure(spacings, n_surrogates=20):
tools/exp_spectral_2d.py:288:    for _ in range(n_surrogates):
tools/exp_spectral_2d.py:316:    parser.add_argument("--surrogates", type=int, default=N_SURROGATES)
tools/exp_spectral_2d.py:319:    NS = args.surrogates
tools/exp_spectral_2d.py:322:    print(f"N={N}, surrogates={NS}\n")
tools/exp_spectral_2d.py:425:            "surrogates": NS,
tools/exp_acf_range_universality.py:14:Reusable: --n_primes, --max_lag_prime, --max_lag_other, --n_surrogates.
tools/exp_acf_range_universality.py:95:    parser.add_argument("--n_surrogates", type=int, default=15)
tools/exp_acf_range_universality.py:148:    # Shuffled surrogate extended ACF
tools/exp_acf_range_universality.py:149:    print("\n  Shuffled surrogates (extended):")
tools/exp_acf_range_universality.py:151:    for i in range(args.n_surrogates):
tools/exp_acf_range_universality.py:254:        for _ in range(args.n_surrogates):
tools/data/reports/agent_20260406_0714.md:19:- Null baseline: 15 Cramer surrogates
tools/data/reports/agent_20260406_0714.md:60:1. **dR_acf1 drift is tautological.** Cramer surrogates show the same slope (z=-0.5). The de Sitter autocorrelation drift is an artifact of the g=(p/2)² mapping, not prime structure. This constrains METRIC_TENSOR: the Ricci scalar fluctuation captures scale geometry but NOT prime-specific content.
tools/exp_metric_tensor_diagnostic.py:14:4. Confronta con Cramer surrogates e shuffled gaps
tools/exp_metric_tensor_diagnostic.py:94:print(f"Running {n_surr} surrogates each (Cramer + shuffled)...")
tools/exp_metric_tensor_diagnostic.py:97:    # Cramer surrogate: gaps ~ Exponential(ln(p))
tools/exp_metric_tensor_diagnostic.py:157:print("Surrogates done.")
tools/exp_metric_tensor_diagnostic.py:376:    "surrogates": {
tools/exp_spectral_landscape.py:13:    python exp_spectral_landscape.py [--N 5000] [--surrogates 20]
tools/exp_spectral_landscape.py:253:def run_domain(name, generator, n_spacings, n_surrogates, **kwargs):
tools/exp_spectral_landscape.py:254:    """Run observables on a domain + shuffled surrogates."""
tools/exp_spectral_landscape.py:268:    # Shuffled surrogates — destroy sequential structure
tools/exp_spectral_landscape.py:270:    for _ in range(n_surrogates):
tools/exp_spectral_landscape.py:320:    parser.add_argument("--surrogates", type=int, default=N_SURROGATES)
tools/exp_spectral_landscape.py:324:    NS = args.surrogates
tools/exp_spectral_landscape.py:353:    print(f"Spectral Landscape — N={N}, surrogates={NS}")
tools/exp_spectral_landscape.py:421:            "surrogates": NS,
tools/data/reports/agent_20260507_0803.md:4:**Category**: gate_transferability  
tools/data/reports/agent_20260507_0803.md:16:> Apply the denominator-collapse gate to the discrimination between dipolar
tools/data/reports/agent_20260507_0803.md:17:> duality and illusory duality. If the gate distinguishes structural generation
tools/data/reports/agent_20260507_0803.md:22:Tool created: `tools/exp_duality_gate_transfer.py`
tools/data/reports/agent_20260507_0803.md:32:- denominator gate: observable stable when `abs(z original-vs-shuffle) >= 2`;
tools/data/reports/agent_20260507_0803.md:37:generator. The only transferred object is the denominator gate.
tools/data/reports/agent_20260507_0803.md:81:1. **The gate does not degenerate on DUALITA.** The dipolar endpoint has three
tools/data/reports/agent_20260507_0803.md:82:stable canonical observables with large original-vs-shuffle denominators
tools/data/reports/agent_20260507_0803.md:84:illusory endpoint has no stable denominator support. This is a structural
tools/data/reports/agent_20260507_0803.md:85:original-vs-shuffle distinction, not an empty syntactic map.
tools/data/reports/agent_20260507_0803.md:89:denominator collapse. Here, beta 0.3 is classification-ambiguous, but the
tools/data/reports/agent_20260507_0803.md:90:denominator support is still strong: stable count remains `3.000/5`.
tools/data/reports/agent_20260507_0803.md:94:3. **Endpoint-gated classification remains empty for the same structural
tools/data/reports/agent_20260507_0803.md:96:pole is denominator-weak. This does not erase the discrimination; it prevents
tools/data/reports/agent_20260507_0803.md:97:symmetric endpoint-gated retention claims. The valid claim is one-sided:
tools/data/reports/agent_20260507_0803.md:98:coherent dipolar order survives original-vs-shuffle gating; illusory dispersion
tools/data/reports/agent_20260507_0803.md:103:as the dipolar sequence. The gate is therefore not measuring the marginal
tools/data/reports/agent_20260507_0803.md:105:ordered denominator against full shuffle.
tools/data/reports/agent_20260507_0803.md:108:**category: gate_transferability**  
tools/data/reports/agent_20260507_0803.md:113:> In this synthetic DUALITA perimeter, the denominator gate is transferable as
tools/data/reports/agent_20260507_0803.md:117:> 0.3, while denominator collapse appears near beta 0.8-0.9.
tools/data/reports/agent_20260507_0803.md:122:> report layer maps as classifier margin plus original-vs-shuffle denominator
tools/data/reports/agent_20260507_0803.md:123:> support, and treat denominator-weak poles as asymmetric structural poles.
tools/data/reports/agent_20260507_0803.md:130:2. denominator-collapse layer.
tools/data/reports/agent_20260507_0803.md:133:If they split, as they do here, the gate is still useful but the transition
tools/data/reports/agent_20260507_0803.md:142:  denominator support.
tools/data/reports/agent_20260507_0803.md:143:- **L3 no silent patching**: the report explicitly separates "gate transfers"
tools/data/reports/agent_20260507_0803.md:152:- Script: `tools/exp_duality_gate_transfer.py`
tools/data/reports/agent_20260507_0803.md:153:- Main data: `tools/data/duality_gate_transfer_20260507_0803.json`
tools/data/reports/agent_20260507_0803.md:154:- Seed check: `tools/data/duality_gate_transfer_20260507_0803_seedcheck.json`
tools/lab_promotion.py:48:    "must ", "deve", "constraint", "vincolo", "gate ",
tools/lab_falsifier.py:3:lab_falsifier.py — Counter-pole gate per il lab fisica.
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/lab_falsifier.py:200:    parser = argparse.ArgumentParser(description="Counter-pole gate for the lab")
tools/lab_session_logger.py:43:    p.add_argument('--sync-blocked', action='store_true', help='Phase A/B gate ha bloccato')
tools/dnd_research_engine.py:303:                             f'in the conjugate observable',
tools/exp_two_channel_psd.py:109:    parser.add_argument('--n_surrogates', type=int, default=20)
tools/exp_two_channel_psd.py:189:    print(f"\n=== NULL BASELINES ({args.n_surrogates} surrogates) ===")
tools/exp_two_channel_psd.py:195:    for i in range(args.n_surrogates):
tools/exp_two_channel_psd.py:282:        'n_surrogates': args.n_surrogates,
tools/exp_coherence_robustness.py:10:  - numero di shuffle per surrogate
tools/exp_coherence_robustness.py:74:def measure_delta_r(gaps, L, n_windows, n_surrogates, strategy, rng):
tools/exp_coherence_robustness.py:87:        for _ in range(n_surrogates):
tools/exp_coherence_robustness.py:106:def estimate_L_star(gaps, L_grid, n_windows, n_surrogates, strategy, rng, z_threshold=3.0):
tools/exp_coherence_robustness.py:109:        _, z = measure_delta_r(gaps, L, n_windows, n_surrogates, strategy, rng)
tools/exp_coherence_robustness.py:115:def bootstrap_L_star(gaps, L_grid, n_windows, n_surrogates, n_boot, strategy, master_seed):
tools/exp_coherence_robustness.py:123:        L_star = estimate_L_star(gaps, L_grid, n_windows, n_surrogates, strategy, rng)
tools/exp_coherence_robustness.py:129:def variance_over_n_windows(gaps, L_fixed, n_windows_list, n_surrogates, 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:144:    parser.add_argument('--n_surrogates', type=int, default=20)
tools/exp_coherence_robustness.py:162:        n_windows=200, n_surrogates=args.n_surrogates,
tools/exp_coherence_robustness.py:178:                                         args.n_surrogates, rng_seed=42)
tools/exp_coherence_robustness.py:191:            dr, z = measure_delta_r(gaps, args.L_fixed, 200, args.n_surrogates, strat, rng)
tools/exp_coherence_robustness.py:219:            dr, _ = measure_delta_r(gaps, L, 200, args.n_surrogates, 'random', rng)
tools/data/reports/agent_20260412_0330.md:15:- **Null baseline**: 20 shuffled-gap surrogates per window (same marginal distribution, destroyed order)
tools/dnd_paper_refactor.py:6:propagates them across all papers. Not a blind find-replace — understands
tools/data/reports/agent_20260507_0901.md:4:category: gate_transferability_matrix
tools/data/reports/agent_20260507_0901.md:8:tool: tools/exp_denominator_gate_transfer_matrix.py
tools/data/reports/agent_20260507_0901.md:9:data: tools/data/denominator_gate_transfer_matrix.json
tools/data/reports/agent_20260507_0901.md:13:Verificato dal campo vivo: il cycle 0803 ha mostrato che il denominator gate
tools/data/reports/agent_20260507_0901.md:18:coerente/illusorio a distribuzione marginale preservata, quali parti del gate
tools/data/reports/agent_20260507_0901.md:33:| perimeter | coherent one-sided observables | stable_count coherent | stable_count illusory | endpoint distance gated | ambiguous beta gated |
tools/data/reports/agent_20260507_0901.md:43:The denominator gate is not a two-sided endpoint support. It is one-sided
tools/data/reports/agent_20260507_0901.md:48:1. The gate transfers as one-sided coherence support.
tools/data/reports/agent_20260507_0901.md:50:   In all 4 non-BOUNDARY perimeters, the coherent endpoint carries denominator
tools/data/reports/agent_20260507_0901.md:58:   structural content: the gate does not name "two classes with stable
tools/data/reports/agent_20260507_0901.md:59:   denominators"; it names the side where order survives its own shuffle null.
tools/data/reports/agent_20260507_0901.md:64:   The ambiguous gated layer is beta `0.30` in all 4 perimeters. This is not a
tools/data/reports/agent_20260507_0901.md:71:   `DUALITA_golden` has one-sided support on 3/5 observables. `SR2` and `L2`
tools/data/reports/agent_20260507_0901.md:73:   DUALITA gate transfers, but only through lag-1 and triple aggregation in this
tools/data/reports/agent_20260507_0901.md:80:`order_denominator_gate`
tools/data/reports/agent_20260507_0901.md:82:Definition: an original-vs-shuffle denominator gate where support is expected
tools/data/reports/agent_20260507_0901.md:89:- 0803: BOUNDARY denominator gate transfers to DUALITA, BOUNDARY coordinate does not.
tools/data/reports/agent_20260507_0901.md:90:- 0901: across 4 non-BOUNDARY perimeters, the transferable object is one-sided
tools/data/reports/agent_20260507_0901.md:104:perimeters explicitly. The new category `order_denominator_gate` is declared
tools/data/reports/agent_20260507_0901.md:108:illusory endpoint is weak-denominator, not denominator-absent. The claim is
tools/data/reports/agent_20260507_0901.md:109:one-sided support dominance, not exact absence.
tools/data/reports/agent_20260507_0901.md:111:L5 re-discovery vs discovery: the numerical mechanism is an original-vs-shuffle
tools/data/reports/agent_20260507_0901.md:119:- Verificato: `tools/exp_denominator_gate_transfer_matrix.py`
tools/data/reports/agent_20260507_0901.md:120:- Verificato: `tools/data/denominator_gate_transfer_matrix.json`
tools/data/reports/agent_20260507_0901.md:121:- Inferito: `order_denominator_gate` as category needed by the downstream
tools/data/agent_field_live.md:5:## Piano 71 — Falsificare ORDER_DENOMINATOR_GATE su domini non-sintetici o semi-reali: applicare il gate one-sided
tools/data/agent_field_live.md:9:- [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
tools/data/agent_field_live.md:10:- [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
tools/data/agent_field_live.md:11:- [ORDER_DENOMINATOR_GATE] (0.92) Il denominator gate trasferisce cross-perimetro come supporto one-sided dell'ordine generato, non come endpoint-stable support a due poli. Nel perimet
tools/data/agent_field_live.md:23:  "perimetro" → ORDER_DENOMINATOR_GATE, BOUNDARY_LAYER_GATE, PERTURBATION_DENOMINATOR_GATE
tools/data/agent_field_live.md:27:  "domini" → TRAJECTORY_APPLY_20260507_0901, ORDER_DENOMINATOR_GATE, BOUNDARY
tools/data/agent_field_live.md:32:Trovato: 1. The gate transfers as one-sided coherence support.
tools/data/agent_field_live.md:37:Trovato: 1. **The gate does not degenerate on DUALITA.** The dipolar endpoint has three
tools/data/agent_field_live.md:38:stable canonical observables with large original-vs-shuffle denominators
tools/data/agent_field_live.md:40:illusory endpoint has no stable denominator support. This is a structural
tools/data/agent_field_live.md:42:Verdetto: **category: gate_transferability**  
tools/data/agent_field_live.md:47:> In this synthetic DUALITA perimeter, the denominator gate is transfer
tools/data/agent_field_live.md:50: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
tools/data/agent_field_live.md:157: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.
tools/data/agent_field_live.md:163: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.
tools/data/reports/agent_20260504_1219.md:18:  - `empirical_Mk1`: surrogate Mk1 addestrato sui gap primi, ordine noto 1.
tools/data/reports/agent_20260504_1219.md:19:  - `empirical_Mk2`: surrogate Mk2 addestrato sui gap primi, ordine noto 2.
tools/data/reports/agent_20260504_1219.md:70:2. **`cond_entropy` non può essere usata come prova forte di Layer 3 nel setup attuale.** Anche un controllo generato come Mk2 viene letto Layer 3 per `cond_entropy` (z vs Mk2 = 3.15). Questo indica bias del surrogate/binning o varianza finita, non necessariamente memoria reale oltre Mk2.
tools/data/reports/agent_20260504_1219.md:79:Non è una scoperta nuova di teoria Markov: è un audit metodologico vicino ai test di ordine Markov con surrogate data. La letteratura rilevante include test esatti di ordine Markov e surrogate vincolati che preservano proprietà Markov di ordine n, quindi il prossimo strumento deve avvicinarsi a quei null esatti invece di usare solo generatori bin-pool approssimati.
tools/data/reports/agent_20260504_1219.md:92:- **L5 rediscovery**: non taggo NEW. Riferimenti classici vicini: surrogate-data tests for Markov order e constrained Markov-order surrogates.
tools/data/reports/agent_20260504_1219.md:95:- Kandel et al.-lineage / surrogate tests: ["Testing the order of discrete Markov chains using surrogate data", Physica D 117, 1998](https://www.sciencedirect.com/science/article/pii/S0167278997003151).
tools/data/reports/agent_20260504_1219.md:97:- ["Constrained Markov order surrogates", Physica D 2020](https://www.sciencedirect.com/science/article/pii/S0167278919305652).
tools/lab_aeternitas.py:2:"""lab_aeternitas.py — Aeternitas gate per il lab fisico MM_D-ND.
tools/data/reports/agent_20260506_0625.md:57:5. **Poisson and shuffled-prime controls show multi-axis artifacts.** Poisson has rank 1.917/2.201 depending on observable set; prime shuffle control has rank 2.294/1.988. Multi-dimensional perturbation response by itself is not evidence of structured GUE-like boundary. It can arise from low structural signal plus noisy denominators in retention normalization.
tools/data/reports/agent_20260405_0919.md:18:- **Null**: 20 Cramer surrogates per window (exponential gaps)
tools/data/notte_20260330_0330.md:30:## Combo A11: 18 anomalie propagate
tools/triggers/finding_promoter.py:61:PROMPT_TEMPLATE = """You are a research lab promoter. A scientific cycle produced a meta-finding marked REVIEW_REQUIRED by an automated eligibility gate. Your job: decide if this finding can be reformulated as an APPLICATIVE rule (a predictive operator with concrete input/output), or if it's purely descriptive/methodological.
tools/triggers/finding_eligibility_gate.py:2:"""finding_eligibility_gate — classifica i findings prima di application_designer.
tools/triggers/finding_eligibility_gate.py:244:    print(f"finding_eligibility_gate cycle_ts={cycle_ts}")
tools/triggers/finding_eligibility_gate.py:289:        "stage_name": "finding_eligibility_gate",
tools/triggers/finding_eligibility_gate.py:291:        "generated_by": "finding_eligibility_gate.py",
tools/triggers/application_designer.py:2:"""application_designer — Stage 2 SSP per scoperte PASS gate.
tools/triggers/application_designer.py:4:Input: cycle_ts (deve aver passato gate strict via on_crystallize)
tools/triggers/application_designer.py:264:        # Fallback (gate non eseguito): genera per tutti, segnala
tools/triggers/application_designer.py:346:## Non-application findings (skipped da gate, non buttati via)
tools/triggers/application_designer.py:373:`manifest.draft.json` + `finding_index.draft.json` (se gate eseguito).*
tools/triggers/application_designer.py:451:            print(f"  WARN: eligibility_index unreadable ({e}) — fallback no-gate")
tools/triggers/application_designer.py:453:        print(f"  WARN: eligibility_index non trovato — esegui finding_eligibility_gate.py prima per filtraggio strict")
tools/triggers/on_crystallize.py:83:    Ritorna decision + confidence + source + action (necessaria per gate
tools/triggers/on_crystallize.py:84:    discriminator REDESIGN tipo B vs A — vedi gate_check).
tools/triggers/on_crystallize.py:117:def gate_check(falsifier: dict, valutatore: dict | None) -> tuple[str, str]:
tools/triggers/on_crystallize.py:321:  gate_status: {ctx.get('gate_status','unknown')}{(chr(10) + 'pending_consecutio: ' + json.dumps(ctx.get('pending_consecutio',''))) if ctx.get('pending_consecutio') else ''}
tools/triggers/on_crystallize.py:497:    gate_status, msg = gate_check(falsifier, valutatore)
tools/triggers/on_crystallize.py:498:    print(f"  gate: [{gate_status}] {msg}")
tools/triggers/on_crystallize.py:499:    if gate_status == "invalid":
tools/triggers/on_crystallize.py:507:    if gate_status == "mature_eligible":
tools/triggers/on_crystallize.py:509:    elif gate_status == "transitional":
tools/triggers/on_crystallize.py:511:    elif gate_status == "provisional_discovery":
tools/triggers/on_crystallize.py:519:    if gate_status == "provisional_discovery" and valutatore:
tools/triggers/on_crystallize.py:545:        "gate_status": gate_status,
tools/exp_duality_gate_transfer.py:3:exp_duality_gate_transfer.py
tools/exp_duality_gate_transfer.py:8:    Does the denominator-collapse gate discriminate dipolar duality from
tools/exp_duality_gate_transfer.py:21:illusory permutation. Canonical observables and original-vs-shuffle z gates are
tools/exp_duality_gate_transfer.py:158:def summarize_gate(rows: list[dict], z_min: float) -> dict:
tools/exp_duality_gate_transfer.py:223:    gate = summarize_gate(rows, args.z_min)
tools/exp_duality_gate_transfer.py:225:    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
tools/exp_duality_gate_transfer.py:228:        "experiment": "duality_gate_transfer",
tools/exp_duality_gate_transfer.py:229:        "category": "gate_transferability",
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:234:        "gate": gate,
tools/exp_duality_gate_transfer.py:236:        "classification_endpoint_gated": gated_classification,
tools/exp_duality_gate_transfer.py:247:    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
tools/exp_duality_gate_transfer.py:251:        "endpoint_distance_gated="
tools/exp_duality_gate_transfer.py:252:        f"{gated_classification['endpoint_distance']:.3f}"
tools/exp_duality_gate_transfer.py:254:    source = gated_classification if gate["endpoint_stable_observables"] else all_classification
tools/exp_duality_gate_transfer.py:258:        stable_count = gate["layers"][key]["stable_count_mean"]
tools/exp_duality_gate_transfer.py:278:    parser.add_argument("--out", default="tools/data/duality_gate_transfer.json")
tools/exp_two_channel_boundary.py:28:    python tools/exp_two_channel_boundary.py [--n_primes N] [--window W] [--n_surrogates S]
tools/exp_two_channel_boundary.py:151:def run(n_primes=500000, window=5000, n_surrogates=20):
tools/exp_two_channel_boundary.py:173:        # Surrogates
tools/exp_two_channel_boundary.py:178:        for _ in range(n_surrogates):
tools/exp_two_channel_boundary.py:281:    parser.add_argument('--n_surrogates', type=int, default=20)
tools/exp_two_channel_boundary.py:284:    results = run(args.n_primes, args.window, args.n_surrogates)
tools/exp_two_channel_boundary.py:308:        'n_surrogates': args.n_surrogates,
tools/lab_autopsy.py:336:                    "autopsy script itself failed — signal to investigate lab_autopsy.py "
tools/topological_charge.py:90:    denominator = (1.0 + h_x**2 + h_y**2)**2
tools/topological_charge.py:92:    K = numerator / denominator
tools/data/notte_20260331_0330.md:27:## Combo A11: 9 anomalie propagate
tools/data/notte_20260402_0330.md:36:## Combo A11: 12 anomalie propagate
tools/data/evolution/evolution_20260504_1219.md:1:Evolution report scritto. Il passo era netto — audit metodologico pulito, nessun attrito significativo. Le possibilita' concrete per il ciclo successivo: surrogati Markov esatti (non bin-pool), gate multi-seed obbligatorio, test di coupling tra i due layer.
tools/data/notte_20260327_0330.md:19:## Combo A11: 13 anomalie propagate
tools/data/evolution/evolution_20260506_1941.md:3:**Sintesi**: il passo è stato autologico (ha falsificato il proprio strumento), ma l'attrito reale è infrastrutturale — `trajectory_apply` ha salvato il run, però l'autopsy non sa leggerlo. Il nodo regressivo è nella separazione tra path di esecuzione e path di osservazione. La consecutio scientifica è chiara: mappa denominatore per osservabile. La consecutio di sistema è: estendere `lab_autopsy.py` per riconoscere i run trajectory_apply.
tools/data/evolution/evolution_20260506_1955.md:3:**Sintesi**: il passo scientifico è stato netto — autologico, ha falsificato il proprio strumento e ristretto il claim. L'attrito è infrastrutturale: `trajectory_apply` esegue il lavoro ma `lab_autopsy` non lo vede. Il nodo regressivo è nella mancanza di un contratto strutturato tra esecutore e osservatore. La consecutio si biforca: sistema (unificare il campo visivo) e scienza (operatori selettivi con denominator gate).
tools/data/notte_20260331_1753.md:27:## Combo A11: 11 anomalie propagate
tools/data/evolution/evolution_20260507_0803.md:3:Il report resta breve, osserva il percorso invece del risultato scientifico, e colloca l’attrito al nodo regressivo: contratto iniziale del trasferimento e separazione anticipata tra ambiguita’ classificativa e collasso del denominatore.
tools/data/notte_20260401_0330.md:32:## Combo A11: 9 anomalie propagate
tools/data/notte_20260328_0330.md:19:## Combo A11: 11 anomalie propagate
tools/data/operator_directive_consumed_20260507_0803.md:3:**Contesto.** I 9 cycle precedenti hanno tutti attaccato BOUNDARY (8 GUE / 5 Poisson) producendo 3 gate metodologici cristallizzati: OBSERVABLE_REGISTRY, PERTURBATION_DENOMINATOR_GATE, BOUNDARY_LAYER_GATE (denominator collapse layer beta 0.3-0.4). Le altre 6 tensioni-sorgente sono ferme da settimane. Il sistema (Godel) ha posto la domanda critica:
tools/data/operator_directive_consumed_20260507_0803.md:5:> I tre gate cristallizzati sono **operatori generali trasferibili** o **metriche locali** vestite da gate? Se il gate "denominator collapse layer" si applica anche a trascendenza, dualità, G — allora BOUNDARY non è un asse, è il metodo. I 9 cicli hanno prodotto lo strumento, non il risultato. Se i gate funzionano solo su BOUNDARY — sono metriche locali e R+1=R: il campo è saturo.
tools/data/operator_directive_consumed_20260507_0803.md:9:Lavora **NON su BOUNDARY**. Lavora applicando `BOUNDARY_LAYER_GATE` (il gate denominator-collapse cristallizzato dal cycle 20260506_1955) come strumento di analisi sulla tensione `DUALITA_DIPOLARE_VS_ILLUSORIA`.
tools/data/operator_directive_consumed_20260507_0803.md:13:Il gate metodologico denominator-collapse produce **falsificazione strutturale** quando applicato alla discriminazione tra:
tools/data/operator_directive_consumed_20260507_0803.md:17:oppure il gate degenera (non discrimina, non flagga, output sintatticamente valido ma vuoto)?
tools/data/operator_directive_consumed_20260507_0803.md:25:3. Applica `BOUNDARY_LAYER_GATE`: gate beta abs(z)>=2 sui denominatori, layer 0.3-0.4. Misura quale famiglia attraversa il gate e quale collassa.
tools/data/operator_directive_consumed_20260507_0803.md:26:4. Verifica con shuffle/surrogati (PERTURBATION_DENOMINATOR_GATE) — il gate distingue solo strutturali da originali, o anche i due tipi di dualità tra loro?
tools/data/operator_directive_consumed_20260507_0803.md:30:- ✅ **Gate trasferibile**: il gate distingue dipolare da illusorio con segnale strutturale (z, beta, denominator-strength misurabili e replicabili sotto null baseline). I 9 cycle hanno **forgiato una lente**, non scavato una buca. Il prossimo passo è generalizzare il gate alle altre 5 tensioni-sorgente.
tools/data/operator_directive_consumed_20260507_0803.md:31:- ❌ **Gate locale**: il gate non discrimina o produce solo flagging artefattuale (z marginal, beta non separa, falliscono shuffle test). I 9 cycle hanno **scavato una buca su BOUNDARY**. Il prossimo passo è pivot vero a un'altra tensione-sorgente da zero (cambia generatore, non solo target).
tools/data/operator_directive_consumed_20260507_0803.md:33:**Importante.** Il valutatore di questo cycle deve esprimersi sulla **trasferibilità del gate**, non sulla "produzione di un risultato". Se il gate si rompe applicato a DUALITA, è un'informazione strutturale tanto quanto un attacco riuscito. La domanda non è "ha funzionato?" — la domanda è "il gate è operatore o metrica?".
tools/data/operator_directive_consumed_20260507_0803.md:35:**Consecutio (post-cycle).** Esito da scrivere come `category: gate_transferability`, con campo `verdict ∈ {operator, metric, ambiguous}`.
tools/exp_acf_stationarity.py:186:    print(f"PART 2: Shuffled baseline ({args.n_shuffles} surrogates)")

exec
/bin/bash -lc "sed -n '1,220p' tools/data/seme.json" in /opt/MM_D-ND
 succeeded in 0ms:
{
  "timestamp": "2026-05-07T09:07:22.652946",
  "piano": 71,
  "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": "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"
    },
    {
      "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": "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": "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_0901",
      "claim": "Applied valutatore REDESIGN from 20260507_0901: Falsificare ORDER_DENOMINATOR_GATE su domini non-sintetici o semi-reali: applicare il gate one-sided a 2-3 sequenze fisiche/ponte gia' presenti nel sito, con shuffle e surrogati preservanti marginale,",
      "intensità": 0.7,
      "porta": "trajectory_apply",
      "condensato_ref": "A8,A14,A15",
      "manuale": true,
      "_source_log": "2026-05-07T09:07:53.577876+00:00",
      "_source_decision": "REDESIGN",
      "_source_reasoning": "La direzione corrente ha eseguito il mandato: la matrice di trasferibilita' su 4 perimetri non-BOUNDARY e' stata prodotta e ha nominato una nuova categoria, order_denominator_gate. Un altro giro nello stesso frame rischia accumulo numerico locale su sintetici; la prossima mossa deve falsificare il n"
    }
  ],
  "potenziale_bloccato": [],
  "varianza": [
    "Tensioni risolte: {'METRIC_TENSOR', 'PIANO_PRIMARIO_DUE_ASSIOMI', 'OBSERVABLE_REGISTRY', 'TENSIONE_ENTITA', 'PERTURBATION_DENOMINATOR_GATE', 'BOUNDARY_LAYER_GATE', 'TRAJECTORY_APPLY_20260507_0803', 'G_POTENZIALE_NULLA', 'TRASCENDENZA_LIMITE', 'ORDER_DENOMINATOR_GATE', 'DUALITA_DIPOLARE_VS_ILLUSORIA'}"
  ],
  "filtro": {
    "promosse": 13,
    "filtrate": 0,
    "condensato_claims": 30
  },
  "direzione": "Falsificare ORDER_DENOMINATOR_GATE su domini non-sintetici o semi-reali: applicare il gate one-sided a 2-3 sequenze fisiche/ponte gia' presenti nel sito, con shuffle e surrogati preservanti marginale, cercando un controesempio dove il supporto denominatore appare anche sul lato illusorio o scompare sul lato coerente.",
  "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"
    },
    {

exec
/bin/bash -lc "sed -n '1,340p' tools/exp_denominator_gate_transfer_matrix.py" in /opt/MM_D-ND
 succeeded in 0ms:
#!/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 "sed -n '1,180p' tools/data/reports/agent_20260507_0901.md" in /opt/MM_D-ND
 succeeded in 0ms:
# 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.

exec
/bin/bash -lc "find . -maxdepth 3 -type f \\( -name '*.json' -o -name '*.csv' -o -name '*.npy' -o -name '*.npz' \\) | sed 's#"'^./##'"' | head -200" in /opt/MM_D-ND
 succeeded in 0ms:
tools/STRUMENTI.json
tools/awareness.json
tools/rules/axioms_to_theorems.json
tools/data/two_channel_shuffle_audit.json
tools/data/audit_paper_D_draft2.json
tools/data/r_excess_l_functions.json
tools/data/cognitive_fingerprint.json
tools/data/m_spectro_calibra_20260310_2015.json
tools/data/zero_traiettoria_20260310_1649.json
tools/data/incrocio_20260428_0340.json
tools/data/torre_results.json
tools/data/meta_tautology_test.json
tools/data/projective_quantization_results.json
tools/data/seme_backup_pre_run.json
tools/data/pipeline_state.json
tools/data/exp_markov_psd_prediction.json
tools/data/piano11_results.json
tools/data/audit_paper_G_draft3.json
tools/data/observable_collinearity_breaking_20260506_1956.json
tools/data/incrocio_20260506_0633.json
tools/data/observable_collinearity_breaking_20260506_1955.json
tools/data/iterata_M_confronto_20260312_1254.json
tools/data/duality_gate_transfer_20260507_0803_seedcheck.json
tools/data/specchio_20260314.json
tools/data/zero_primi_20260310_0820.json
tools/data/lab_results.json
tools/data/audit_paper_C_draft2.json
tools/data/mod3_scaling.json
tools/data/modular_memory_spectrum.json
tools/data/exp_det_drift.json
tools/data/observable_collinearity_breaking_20260506_1957.json
tools/data/incrocio_20260402_0344.json
tools/data/engine_state.json
tools/data/exp_two_channel_universality.json
tools/data/trasmutazione_results.json
tools/data/zero_varieta_primi_20260310_1842.json
tools/data/audit_paper_F_draft3.json
tools/data/trace_bridge_results.json
tools/data/proto_oom_001.json
tools/data/lab_data.json
tools/data/two_layer_universality.json
tools/data/synthetic_validation.json
tools/data/ponti_evoluti.json
tools/data/magnitude_psd_from_acf.json
tools/data/dipolar_vector_scaling.json
tools/data/arxiv_cache.json
tools/data/markov_k_direction.json
tools/data/duality_gate_transfer_20260507_0803.json
tools/data/seme.json
tools/data/exp_conditional_r.json
tools/data/two_channel_boundary.json
tools/data/consecutio_processata.json
tools/data/markov_layer_recovery_audit.json
tools/data/spectral_rigidity_results.json
tools/data/multi_pattern_results.json
tools/data/modular_algebra_depth.json
tools/data/incrocio_20260404_0330.json
tools/data/exp_acf_stationarity.json
tools/data/audit_paper_E_draft3.json
tools/data/denominator_gate_transfer_matrix.json
tools/data/knowledge_state.json
tools/data/incrocio_20260331_1807.json
tools/data/incrocio_20260402_0755.json
tools/data/two_channel_cross_domain.json
tools/data/m_spectro_11domini.json
tools/data/curva_results.json
tools/data/odlyzko_block2_probe.json
tools/data/stato_ciclo.json
tools/data/riformulazioni.json
tools/data/incrocio_20260425_0339.json
tools/data/doppia_fenditura_20260314.json
tools/data/costante_dinamica.json
tools/data/incrocio_20260421_0720.json
tools/data/crossover_phase_test.json
tools/data/lab_risultante.json
tools/data/indeterminazione_results.json
tools/data/alignment_active.json
tools/data/exp_beta_crossover.json
tools/data/lab_registro.json
tools/data/dipolar_crossover.json
tools/data/piano11e_results.json
tools/data/exp_two_channel_psd.json
tools/data/gap_resolution.json
tools/data/odlyzko_100k_probe.json
tools/data/seme_axioms.json
tools/data/incrocio_20260331_0345.json
tools/data/markov_scale_function.json
tools/data/research_kb.json
tools/data/paper_H_results.json
tools/data/m_spectro_godel_attrito_20260318_0949.json
tools/data/exp_spectral_2d.json
tools/data/perturbation_dimensionality_audit_scale0330.json
tools/data/cross_domain_dipolar_direction.json
tools/data/zero_multiscala_primi_20260310_0831.json
tools/data/zero_controllo_20260310_1659.json
tools/data/exp_coherence_length.json
tools/data/brody_flow.json
tools/data/observable_rank_audit.json
tools/data/r_ratio_decay.json
tools/data/vault_condizioni.json
tools/data/incrocio_20260405_0730.json
tools/data/audit_paper_F_draft2.json
tools/data/conoscenza_teorie.json
tools/data/psd_prime_gaps_results.json
tools/data/audit_paper_B_draft3.json
tools/data/zero_confronto_20260317_1604.json
tools/data/boundary_mixture_gate_20260507_0330.json
tools/data/test_semiprimi_20260313.json
tools/data/boundary_coherence.json
tools/data/vocabolario_custom.json
tools/data/incrocio_20260420_1856.json
tools/data/quantization_results.json
tools/data/audit_paper_D_draft3.json
tools/data/loop_state.json
tools/data/lab_health.json
tools/data/lab_vault.json
tools/data/audit_paper_A_draft3.json
tools/data/experiment_results.json
tools/data/incrocio_20260405_0753.json
tools/data/perturbation_rank_size_curve.json
tools/data/odlyzko_probe_results.json
tools/data/boundary_shuffle_audit.json
tools/data/risultante_v2.json
tools/data/scale_selective_perturbation.json
tools/data/zero_notturno_20260310_0858.json
tools/data/bloch_explorer_results.json
tools/data/exp_poisson_convergence.json
tools/data/residuo_ordine_9domini.json
tools/data/implications_state.json
tools/data/exp_acf_range_universality.json
tools/data/zero_varieta_20260310_1831.json
tools/data/retriever_risultati.json
tools/data/piano11b_gue_test.json
tools/data/incrocio_20260423_0335.json
tools/data/3d_boundary_layers.json
tools/data/exp_acf_z6z_mechanism.json
tools/data/incrocio_20260401_0344.json
tools/data/exp_two_channel_decomposition.json
tools/data/incrocio_20260429_0859.json
tools/data/lab_riflessi.json
tools/data/ciclo_memoria.json
tools/data/test_rarefazione_20260313.json
tools/data/exp_psd_amp_scaling.json
tools/data/rottura_phi2_results.json
tools/data/autoricerca_state.json
tools/data/domande_fondamentali.json
tools/data/dinamiche.json
tools/data/markov3_observable_hunt.json
tools/data/explorer_20260313_0954.json
tools/data/r_excess_analysis.json
tools/data/observable_rank_audit_seed20260506.json
tools/data/interferenza_zeri_20260314.json
tools/data/incrocio_20260405_0723.json
tools/data/loop_insights.json
tools/data/cross_observable_consistency.json
tools/data/m_spectro_godel_risonanza_20260318_0949.json
tools/data/knowledge_state_pre_fix.json
tools/data/m_spectro_confronto_20260310_1959.json
tools/data/learning_curve_100k.json
tools/data/topological_charge_results.json
tools/data/incrocio_20260424_0347.json
tools/data/perturbation_dimensionality_audit.json
tools/data/incrocio_20260402_0803.json
tools/data/spettro_zeta_results.json
tools/data/m_spectro_godel_densita_20260318_0949.json
tools/data/zero_ising_20260310_1716.json
tools/data/lab_graph.json
tools/data/zeta_validation_results.json
tools/data/autoricerca_journal.json
tools/data/exp_spectral_landscape.json
tools/data/incrocio_20260403_0330.json
tools/data/mod3_vs_residual_ordering.json
tools/data/incrocio_20260402_0808.json
tools/data/spectral_gap_results.json
tools/data/incrocio_risultato.json
tools/data/bloch_search_results.json
tools/data/tension_to_theory.json
tools/data/zero_controllo2_20260310_1703.json
tools/data/conoscenza_generata.json
tools/data/incrocio_20260405_0330.json
tools/data/refresh_detector_state.json
tools/data/incrocio_20260404_1852.json
tools/data/lab_errori.json
tools/data/brody_calibration_results.json
tools/data/research_protocols.json
tools/data/incrocio_20260405_0715.json
tools/data/consecutio.json
tools/data/boundary_mixture_gate_20260507_0330_seedcheck.json
tools/data/neuron_snapshot.json
tools/data/markov_dipolar_decomposition.json
tools/data/incrocio_20260402_0809.json
tools/data/selective_layer_decoupling.json
tools/data/zero_confronto_20260310_0830.json
tools/data/risultante_results.json
tools/data/incrocio_20260422_0336.json
tools/data/markov_memory_by_gue_type.json
tools/data/video_feed.json
tools/data/zero_confronto_20260310_0822.json
.claude/settings.local.json
applications/INDEX.draft.json

exec
/bin/bash -lc "sed -n '220,420p' tools/observables_registry.py" in /opt/MM_D-ND
 succeeded in 0ms:


# ─── 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 "sed -n '1,220p' tools/data/odlyzko_probe_results.json" in /opt/MM_D-ND
 succeeded in 0ms:
{
  "ks_test": {
    "n_spacings": 9999,
    "mean": 1.0,
    "variance": 0.1541,
    "ks_gue": {
      "KS": 0.2322,
      "p": 0.0
    },
    "ks_goe": {
      "KS": 0.0952,
      "p": 0.0
    },
    "ks_poisson": {
      "KS": 0.3068,
      "p": 0.0
    },
    "best_fit": "GOE",
    "best_KS": 0.0952,
    "best_p": 0.0
  },
  "fractal_dipole": {
    "s<0.3": {
      "beta": 1.9577,
      "n_points": 163,
      "class": "GUE",
      "r_squared": 0.9777
    },
    "s<0.5": {
      "beta": 2.185,
      "n_points": 878,
      "class": "GUE",
      "r_squared": 0.9947
    },
    "s<0.7": {
      "beta": 2.132,
      "n_points": 2324,
      "class": "GUE",
      "r_squared": 0.9975
    },
    "s<1.0": {
      "beta": 1.905,
      "n_points": 5374,
      "class": "GUE",
      "r_squared": 0.9937
    },
    "s<1.5": {
      "beta": 1.4786,
      "n_points": 8892,
      "class": "GOE",
      "r_squared": 0.9685
    }
  },
  "classification": {
    "ks_class": "GOE",
    "fd_class": "GUE",
    "verdict": "LIKELY GUE: \u03b2\u2192GUE, KS\u2192GOE (finite-height effect, Piano 11c)",
    "confidence": "medium"
  },
  "summary": {
    "name": "Odlyzko block 1 (10000 zeros)",
    "n_zeros": 10000,
    "n_spacings": 9999,
    "conductor": 1,
    "t_range": [
      14.13,
      9877.78
    ],
    "timestamp": "2026-03-04T10:56:10"
  },
  "beta_vs_height": [
    {
      "window": 0,
      "t_center": 811.98,
      "t_range": [
        14.13,
        1419.42
      ],
      "n_zeros": 1000,
      "beta_05": 2.4262,
      "beta_10": 2.0734
    },
    {
      "window": 1,
      "t_center": 1981.33,
      "t_range": [
        1420.42,
        2515.29
      ],
      "n_zeros": 1000,
      "beta_05": 2.1398,
      "beta_10": 1.9562
    },
    {
      "window": 2,
      "t_center": 3032.02,
      "t_range": [
        2516.57,
        3533.33
      ],
      "n_zeros": 1000,
      "beta_05": 2.9602,
      "beta_10": 2.0423
    },
    {
      "window": 3,
      "t_center": 4024.74,
      "t_range": [
        3534.06,
        4506.31
      ],
      "n_zeros": 1000,
      "beta_05": 1.6582,
      "beta_10": 1.6982
    },
    {
      "window": 4,
      "t_center": 4980.66,
      "t_range": [
        4507.75,
        5447.86
      ],
      "n_zeros": 1000,
      "beta_05": 1.2631,
      "beta_10": 1.6348
    },
    {
      "window": 5,
      "t_center": 5909.86,
      "t_range": [
        5448.91,
        6365.85
      ],
      "n_zeros": 1000,
      "beta_05": 1.7942,
      "beta_10": 1.7975
    },
    {
      "window": 6,
      "t_center": 6818.28,
      "t_range": [
        6366.67,
        7264.75
      ],
      "n_zeros": 1000,
      "beta_05": 1.5407,
      "beta_10": 1.7643
    },
    {
      "window": 7,
      "t_center": 7708.73,
      "t_range": [
        7265.96,
        8148.19
      ],
      "n_zeros": 1000,
      "beta_05": 2.2054,
      "beta_10": 1.7958
    },
    {
      "window": 8,
      "t_center": 8585.91,
      "t_range": [
        8148.99,
        9018.42
      ],
      "n_zeros": 1000,
      "beta_05": 2.6632,
      "beta_10": 1.9475
    },
    {
      "window": 9,
      "t_center": 9449.88,
      "t_range": [
        9020.07,
        9877.78
      ],
      "n_zeros": 1000,
      "beta_05": 1.9503,
      "beta_10": 1.8087
    }
  ],
  "beta_convergence": {
    "beta_limit_T_inf": 0.9759,
    "slope": 7.2785,
    "interpretation": "\u03b2\u21920.98 (structural excess)"
  }
}
exec
/bin/bash -lc "sed -n '1,220p' tools/data/odlyzko_100k_probe.json" in /opt/MM_D-ND
 succeeded in 0ms:
{
  "ks_test": {
    "n_spacings": 99999,
    "mean": 1.0,
    "variance": 0.1607,
    "ks_gue": {
      "KS": 0.2262,
      "p": 0.0
    },
    "ks_goe": {
      "KS": 0.0866,
      "p": 0.0
    },
    "ks_poisson": {
      "KS": 0.2982,
      "p": 0.0
    },
    "best_fit": "GOE",
    "best_KS": 0.0866,
    "best_p": 0.0
  },
  "fractal_dipole": {
    "s<0.3": {
      "beta": 2.01,
      "n_points": 2109,
      "class": "GUE",
      "r_squared": 0.9992
    },
    "s<0.5": {
      "beta": 2.0066,
      "n_points": 9584,
      "class": "GUE",
      "r_squared": 0.9997
    },
    "s<0.7": {
      "beta": 1.9309,
      "n_points": 24045,
      "class": "GUE",
      "r_squared": 0.9993
    },
    "s<1.0": {
      "beta": 1.7406,
      "n_points": 53400,
      "class": "GUE",
      "r_squared": 0.9956
    },
    "s<1.5": {
      "beta": 1.3871,
      "n_points": 88618,
      "class": "GOE",
      "r_squared": 0.9749
    }
  },
  "classification": {
    "ks_class": "GOE",
    "fd_class": "GUE",
    "verdict": "LIKELY GUE: \u03b2\u2192GUE, KS\u2192GOE (finite-height effect, Piano 11c)",
    "confidence": "medium"
  },
  "summary": {
    "name": "Odlyzko block 1 (100000 zeros)",
    "n_zeros": 100000,
    "n_spacings": 99999,
    "conductor": 1,
    "t_range": [
      14.13,
      74920.83
    ],
    "timestamp": "2026-03-04T10:56:56"
  },
  "beta_vs_height": [
    {
      "window": 0,
      "t_center": 3032.02,
      "t_range": [
        14.13,
        5447.86
      ],
      "n_zeros": 5000,
      "beta_05": 2.0923,
      "beta_10": 1.9183
    },
    {
      "window": 1,
      "t_center": 7708.73,
      "t_range": [
        5448.91,
        9877.78
      ],
      "n_zeros": 5000,
      "beta_05": 2.1909,
      "beta_10": 1.8745
    },
    {
      "window": 2,
      "t_center": 11983.82,
      "t_range": [
        9878.65,
        14040.46
      ],
      "n_zeros": 5000,
      "beta_05": 2.1424,
      "beta_10": 1.7682
    },
    {
      "window": 3,
      "t_center": 16059.81,
      "t_range": [
        14041.67,
        18046.46
      ],
      "n_zeros": 5000,
      "beta_05": 1.751,
      "beta_10": 1.7262
    },
    {
      "window": 4,
      "t_center": 20007.0,
      "t_range": [
        18047.13,
        21942.59
      ],
      "n_zeros": 5000,
      "beta_05": 1.9001,
      "beta_10": 1.7367
    },
    {
      "window": 5,
      "t_center": 23858.38,
      "t_range": [
        21942.66,
        25755.35
      ],
      "n_zeros": 5000,
      "beta_05": 1.689,
      "beta_10": 1.6462
    },
    {
      "window": 6,
      "t_center": 27635.26,
      "t_range": [
        25755.71,
        29500.32
      ],
      "n_zeros": 5000,
      "beta_05": 2.1416,
      "beta_10": 1.7573
    },
    {
      "window": 7,
      "t_center": 31351.53,
      "t_range": [
        29501.07,
        33190.01
      ],
      "n_zeros": 5000,
      "beta_05": 2.0938,
      "beta_10": 1.7552
    },
    {
      "window": 8,
      "t_center": 35016.73,
      "t_range": [
        33190.8,
        36832.76
      ],
      "n_zeros": 5000,
      "beta_05": 2.1908,
      "beta_10": 1.7904
    },
    {
      "window": 9,
      "t_center": 38638.15,
      "t_range": [
        36832.96,
        40433.69
      ],
      "n_zeros": 5000,
      "beta_05": 1.9682,
      "beta_10": 1.7333
    },
    {
      "window": 10,
      "t_center": 42220.58,
      "t_range": [
        40434.23,
        43998.54
      ],
      "n_zeros": 5000,
      "beta_05": 1.9728,
      "beta_10": 1.698
    },
    {
      "window": 11,
      "t_center": 45769.12,
      "t_range": [
        43999.28,
        47530.95
      ],
      "n_zeros": 5000,
      "beta_05": 2.3504,
      "beta_10": 1.787
    },
    {
      "window": 12,
      "t_center": 49286.66,
      "t_range": [
        47531.77,
        51034.7
      ],
      "n_zeros": 5000,
      "beta_05": 1.8875,
      "beta_10": 1.6894
    },
    {
      "window": 13,
      "t_center": 52776.58,
      "t_range": [
        51035.16,
        54511.66

exec
/bin/bash -lc "sed -n '1,220p' tools/data/odlyzko_block2_probe.json" in /opt/MM_D-ND
 succeeded in 0ms:
{
  "ks_test": {
    "n_spacings": 1499,
    "mean": 1.0,
    "variance": 1.294,
    "ks_gue": {
      "KS": 0.2256,
      "p": 0.0
    },
    "ks_goe": {
      "KS": 0.2742,
      "p": 0.0
    },
    "ks_poisson": {
      "KS": 0.1337,
      "p": 0.0
    },
    "best_fit": "Poisson",
    "best_KS": 0.1337,
    "best_p": 0.0
  },
  "fractal_dipole": {
    "s<0.3": {
      "beta": -0.9857,
      "n_points": 494,
      "class": "Poisson",
      "r_squared": 0.7749
    },
    "s<0.5": {
      "beta": -0.9843,
      "n_points": 657,
      "class": "Poisson",
      "r_squared": 0.7804
    },
    "s<0.7": {
      "beta": -0.9834,
      "n_points": 786,
      "class": "Poisson",
      "r_squared": 0.7713
    },
    "s<1.0": {
      "beta": -0.9823,
      "n_points": 953,
      "class": "Poisson",
      "r_squared": 0.7532
    },
    "s<1.5": {
      "beta": -0.9812,
      "n_points": 1140,
      "class": "Poisson",
      "r_squared": 0.7306
    }
  },
  "classification": {
    "ks_class": "Poisson",
    "fd_class": "Poisson",
    "verdict": "CONSISTENT: Poisson (KS and \u03b2 agree)",
    "confidence": "high"
  },
  "summary": {
    "name": "Odlyzko block 2 (T~10^12)",
    "n_zeros": 1500,
    "n_spacings": 1499,
    "conductor": 1,
    "t_range": [
      14.13,
      9.996851577763514e+69
    ],
    "timestamp": "2026-03-04T12:31:31"
  },
  "learning_curve": {
    "name": "Block 2 (T~10^12)",
    "sample_sizes": [
      100,
      316,
      998,
      1500
    ],
    "matrix": {
      "s<0.3": {
        "316": -0.9876,
        "998": -0.9871,
        "1500": -0.9857
      },
      "s<0.5": {
        "100": 10.8416,
        "316": -0.9877,
        "998": -0.9861,
        "1500": -0.9843
      },
      "s<0.7": {
        "100": 3.062,
        "316": -0.9878,
        "998": -0.9854,
        "1500": -0.9834
      },
      "s<1.0": {
        "100": 2.2641,
        "316": -0.9879,
        "998": -0.9846,
        "1500": -0.9823
      },
      "s<1.5": {
        "100": 1.5866,
        "316": -0.988,
        "998": -0.9837,
        "1500": -0.9812
      }
    },
    "fits": {
      "s<0.3": {
        "beta_inf": -0.9849,
        "c": -0.05,
        "r_squared": 0.6728,
        "se_beta_inf": 0.0014,
        "n_points": 3
      },
      "s<0.5": {
        "beta_inf": -6.6506,
        "c": 161.3531,
        "r_squared": 0.847,
        "se_beta_inf": 2.9534,
        "n_points": 4
      },
      "s<0.7": {
        "beta_inf": -2.9232,
        "c": 55.2018,
        "r_squared": 0.8465,
        "se_beta_inf": 1.0122,
        "n_points": 4
      },
      "s<1.0": {
        "beta_inf": -2.5398,
        "c": 44.3017,
        "r_squared": 0.8461,
        "se_beta_inf": 0.8135,
        "n_points": 4
      },
      "s<1.5": {
        "beta_inf": -2.214,
        "c": 35.0436,
        "r_squared": 0.8456,
        "se_beta_inf": 0.6449,
        "n_points": 4
      }
    },
    "diagnosis": {
      "phase": "NOISE",
      "phase_description": "\u03b2 estimates dominated by sampling noise (need more data)",
      "reason": "Fit quality R\u00b2=0.847, SE=2.953 \u2014 law not yet established",
      "primary_cutoff": 0.5,
      "beta_inf": -6.6506,
      "se_beta_inf": 2.9534,
      "r_squared": 0.847,
      "best_beta_current": -0.9843,
      "n_current": 1500,
      "n_for_precision_005": 10413929,
      "reference_beta": 2.0
    }
  }
}
exec
/bin/bash -lc "rg -n \"def .*prime|primerange|isprime|generate.*prime|np.diff\\(primes|eigvalsh|gue|poisson|zeta|zeros\" tools/exp_*.py tools/*.py | head -240" in /opt/MM_D-ND
exec
/bin/bash -lc "find . -maxdepth 5 -type f | rg -i 'zero|odlyzko|zeta|prime|gue|data' | head -200" in /opt/MM_D-ND
 succeeded in 0ms:
tools/exp_acf_stationarity.py:32:def sieve_primes(limit):
tools/exp_acf_stationarity.py:42:def get_primes(n):
tools/exp_acf_stationarity.py:59:        return np.zeros(max_lag)
tools/exp_acf_stationarity.py:143:    gaps_raw = np.diff(primes).astype(float)
tools/exp_3d_boundary_layers.py:27:def get_primes(n_max):
tools/exp_3d_boundary_layers.py:36:def gue_gaps(N_mat, n_matrices, rng):
tools/exp_3d_boundary_layers.py:42:        evals = np.sort(np.linalg.eigvalsh(H))
tools/exp_3d_boundary_layers.py:174:    prime_gaps = np.diff(primes).astype(float)
tools/exp_3d_boundary_layers.py:183:    gue_g = gue_gaps(n_mat, n_matrices, rng)
tools/exp_3d_boundary_layers.py:184:    if len(gue_g) > args.N:
tools/exp_3d_boundary_layers.py:185:        gue_g = gue_g[:args.N]
tools/exp_3d_boundary_layers.py:186:    gue_results, gue_orig, gue_bl_mean, gue_bl_std = run_crossover(
tools/exp_3d_boundary_layers.py:187:        gue_g, alphas, args.n_trials, rng, "GUE"
tools/exp_3d_boundary_layers.py:192:    poisson_gaps = rng.exponential(1.0, size=args.N)
tools/exp_3d_boundary_layers.py:194:        poisson_gaps, alphas, args.n_trials, rng, "Poisson"
tools/exp_3d_boundary_layers.py:210:        ('gue', gue_results, gue_orig, gue_bl_mean, gue_bl_std),
tools/exp_3d_boundary_layers.py:211:        ('poisson', pois_results, pois_orig, pois_bl_mean, pois_bl_std),
tools/exp_3d_boundary_layers.py:251:    gue_sep = output['sequences']['gue']['layer_separation']['delta']
tools/exp_3d_boundary_layers.py:252:    pois_sep = output['sequences']['poisson']['layer_separation']['delta']
tools/exp_3d_boundary_layers.py:255:    print(f"Layer separation Δα: Primes={prime_sep:+.3f}, GUE={gue_sep:+.3f}, Poisson={pois_sep:+.3f}")
tools/exp_3d_boundary_layers.py:259:        'gue_layer_separation': float(gue_sep),
tools/exp_3d_boundary_layers.py:260:        'poisson_layer_separation': float(pois_sep),
tools/exp_acf_z6z_mechanism.py:24:def sieve_primes(n_max):
tools/exp_acf_z6z_mechanism.py:34:def get_primes(n_primes):
tools/exp_acf_z6z_mechanism.py:54:        return np.zeros(max_lag)
tools/exp_acf_z6z_mechanism.py:84:    gaps = np.diff(primes).astype(float)
tools/exp_boundary_coherence.py:24:from sympy import primerange
tools/exp_boundary_coherence.py:33:    "spacing_ratio":   {"poisson": 0.38629, "gue": 0.53590},  # 2ln2-1, 4-2√3
tools/exp_boundary_coherence.py:34:    "gap_var_ratio":   {"poisson": 1.0,     "gue": 0.178},
tools/exp_boundary_coherence.py:35:    "small_gap_frac":  {"poisson": 0.2592,  "gue": 0.020},    # P(s<0.3) for exp vs Wigner
tools/exp_boundary_coherence.py:36:    "brody_beta":      {"poisson": 0.0,     "gue": 1.0},
tools/exp_boundary_coherence.py:37:    "lag1_acf":        {"poisson": 0.0,     "gue": -0.271},
tools/exp_boundary_coherence.py:114:    p = REF[obs_name]["poisson"]
tools/exp_boundary_coherence.py:115:    g = REF[obs_name]["gue"]
tools/exp_boundary_coherence.py:121:def generate_gue_spacings(n, n_matrices=50):
tools/exp_boundary_coherence.py:129:        eigs = np.sort(np.linalg.eigvalsh(H))
tools/exp_boundary_coherence.py:136:def generate_poisson_spacings(n):
tools/exp_boundary_coherence.py:141:def get_prime_gaps(pmin, pmax):
tools/exp_boundary_coherence.py:143:    primes = np.array(list(primerange(pmin, pmax)))
tools/exp_boundary_coherence.py:144:    return np.diff(primes).astype(float)
tools/exp_boundary_coherence.py:162:    gue_gaps = generate_gue_spacings(20000)
tools/exp_boundary_coherence.py:163:    gue_obs = compute_all_observables(gue_gaps)
tools/exp_boundary_coherence.py:165:        "raw": gue_obs,
tools/exp_boundary_coherence.py:166:        "tau": {k: to_tau(k, v) for k, v in gue_obs.items()},
tools/exp_boundary_coherence.py:170:    poi_gaps = generate_poisson_spacings(20000)
tools/exp_boundary_coherence.py:275:    tau_matrix = np.zeros((len(scales), len(obs_names)))
tools/exp_acf_amplitude_scaling.py:14:from sympy import primerange
tools/exp_acf_amplitude_scaling.py:25:        return np.zeros(max_lag)
tools/exp_acf_amplitude_scaling.py:57:    primes = np.array(list(primerange(2, upper)))[:args.n_primes]
tools/exp_acf_amplitude_scaling.py:58:    gaps = np.diff(primes).astype(float)
tools/exp_boundary_growth.py:21:from sympy import primerange
tools/exp_boundary_growth.py:60:    primes = np.array(list(primerange(2, LIMIT)), dtype=np.int64)
tools/exp_boundary_shuffle_audit.py:16:Domains: primes, zeta zeros, random matrix (GUE), Fibonacci spectrum,
tools/exp_boundary_shuffle_audit.py:70:def gen_primes(n=100000):
tools/exp_boundary_shuffle_audit.py:79:    return np.diff(primes).astype(float)
tools/exp_boundary_shuffle_audit.py:82:def gen_gue_eigenvalues(size=2000, n_matrices=50):
tools/exp_boundary_shuffle_audit.py:88:        eigs = np.sort(linalg.eigvalsh(H))
tools/exp_boundary_shuffle_audit.py:99:def gen_poisson(n=100000):
tools/exp_boundary_shuffle_audit.py:122:    eigs = np.sort(linalg.eigvalsh(H))
tools/exp_boundary_shuffle_audit.py:180:    K = np.zeros((n, n))
tools/exp_boundary_shuffle_audit.py:186:    eigs = np.sort(linalg.eigvalsh(K))
tools/exp_boundary_shuffle_audit.py:197:    v = np.zeros(L)
tools/exp_boundary_shuffle_audit.py:218:    state = np.zeros(n, dtype=int)
tools/exp_boundary_shuffle_audit.py:222:        new = np.zeros(n, dtype=int)
tools/exp_boundary_shuffle_audit.py:244:    'gue':                 ('GUE random matrix',          gen_gue_eigenvalues),
tools/exp_boundary_shuffle_audit.py:245:    'poisson':             ('Poisson process',            gen_poisson),
tools/exp_boundary_shuffle_audit.py:275:            dist_gue = abs(res['r_original'] - R_GUE)
tools/exp_boundary_shuffle_audit.py:277:            res['class_original'] = 'GUE' if dist_gue < dist_poi else 'Poisson'
tools/exp_boundary_shuffle_audit.py:279:            dist_gue_s = abs(res['r_shuffled_mean'] - R_GUE)
tools/exp_boundary_shuffle_audit.py:281:            res['class_shuffled'] = 'GUE' if dist_gue_s < dist_poi_s else 'Poisson'
tools/exp_alpha_stability.py:15:from sympy import primerange
tools/exp_alpha_stability.py:18:def get_primes(n):
tools/exp_alpha_stability.py:25:    primes = list(primerange(2, upper))
tools/exp_alpha_stability.py:28:        primes = list(primerange(2, upper))
tools/exp_alpha_stability.py:38:        return np.zeros(max_lag)
tools/exp_alpha_stability.py:39:    acf = np.zeros(max_lag)
tools/exp_alpha_stability.py:86:    gaps = np.diff(primes).astype(float)
tools/exp_brody_calibration.py:109:def generate_primes(n_max=200000):
tools/exp_brody_calibration.py:118:def prime_gaps_unfolded(n_gaps):
tools/exp_brody_calibration.py:119:    primes = generate_primes(n_gaps * 20)[:n_gaps + 1]
tools/exp_brody_calibration.py:120:    gaps = np.diff(primes.astype(float))
tools/exp_brody_calibration.py:126:def gue_gaps(n_gaps, rng):
tools/exp_brody_calibration.py:130:    eigs = np.sort(np.linalg.eigvalsh(H))
tools/exp_brody_calibration.py:212:    gue_g = gue_gaps(min(args.n_gaps, 400), 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:215:    real_domains['gue_matrix'] = {**obs_g, 'beta_eff': beta_eff_g}
tools/exp_brody_calibration.py:216:    print(f"{'gue_matrix':>20} {obs_g['r']:8.4f} {obs_g['r_shuf']:8.4f} "
tools/exp_brody_calibration.py:236:    poisson_gaps = rng.exponential(1.0, args.n_gaps)
tools/exp_brody_calibration.py:237:    obs_po = compute_observables(poisson_gaps, n_shuffles=args.n_shuffles, rng=rng)
tools/exp_brody_calibration.py:239:    real_domains['poisson'] = {**obs_po, 'beta_eff': beta_eff_po}
tools/exp_brody_calibration.py:240:    print(f"{'poisson':>20} {obs_po['r']:8.4f} {obs_po['r_shuf']:8.4f} "
tools/exp_boundary_gue_poisson.py:22:from sympy import primerange
tools/exp_boundary_gue_poisson.py:33:def primes_in_window(start, end, primes_array):
tools/exp_boundary_gue_poisson.py:39:def cramer_random_primes(N_max, rng):
tools/exp_boundary_gue_poisson.py:50:def analyze_windows(primes_array, windows):
tools/exp_boundary_gue_poisson.py:69:    primes = np.array(list(primerange(2, N_MAX)))
tools/exp_boundary_gue_poisson.py:100:    r_gue = 0.5307  # GOE (real symmetric) in 1D
tools/exp_boundary_gue_poisson.py:101:    r_poisson = 0.3863
tools/exp_boundary_gue_poisson.py:120:    print(f"\nReference: <r>_GUE = {r_gue:.4f}, <r>_Poisson = {r_poisson:.4f}")
tools/exp_boundary_gue_poisson.py:161:        dist_gue = abs(rp - r_gue)
tools/exp_boundary_gue_poisson.py:162:        dist_poi = abs(rp - r_poisson)
tools/exp_boundary_gue_poisson.py:163:        label = "GUE" if dist_gue < dist_poi else "POISSON"
tools/exp_boundary_gue_poisson.py:164:        margin = abs(dist_gue - dist_poi)
tools/exp_boundary_gue_poisson.py:171:        "experiment": "boundary_gue_poisson_cramer",
tools/exp_boundary_gue_poisson.py:176:        "reference": {"r_gue": r_gue, "r_poisson": r_poisson},
tools/exp_coherence_robustness.py:5:Segue agent_20260416_0330 (COHERENCE_LENGTH). Obiettivo: stimare confidence intervals
tools/exp_coherence_robustness.py:27:def sieve_primes(limit):
tools/exp_coherence_robustness.py:152:    gaps = np.diff(primes).astype(float)
tools/exp_brody_crossover.py:25:def sieve_primes(limit):
tools/exp_brody_crossover.py:74:    gaps = np.diff(primes).astype(float)
tools/exp_brody_crossover.py:177:        ln_p_poisson = -intercept_beta / slope_beta
tools/exp_brody_crossover.py:178:        print(f"\nExtrapolated Poisson (beta=0) at ln(p) = {ln_p_poisson:.1f} → p ~ 10^{ln_p_poisson/np.log(10):.0f}")
tools/exp_cross_domain_dipolar_direction.py:11:  GUE-like: GUE spacings, GOE spacings, CUE spacings, Riemann zeta zeros (via GUE proxy)
tools/exp_cross_domain_dipolar_direction.py:28:def get_primes(n_max):
tools/exp_cross_domain_dipolar_direction.py:109:    eigs = np.linalg.eigvalsh(H)
tools/exp_cross_domain_dipolar_direction.py:121:    vals = np.zeros(N + 1000)  # burn-in
tools/exp_cross_domain_dipolar_direction.py:132:def gen_poisson(N, rng=None):
tools/exp_cross_domain_dipolar_direction.py:230:    for name, gen_fn in [('exponential', gen_poisson),
tools/exp_cross_domain_dipolar_direction.py:276:    poisson_names = ['exponential', 'uniform', 'geometric']
tools/exp_cross_domain_dipolar_direction.py:308:    for name in poisson_names:
tools/exp_cross_domain_dipolar_direction.py:313:    for name in poisson_names:
tools/exp_dR_brody_connection.py:192:gaps_all = np.diff(primes)
tools/exp_acf_range_universality.py:21:from sympy import primerange
tools/exp_acf_range_universality.py:24:def get_primes(n):
tools/exp_acf_range_universality.py:29:    primes = list(primerange(2, upper))
tools/exp_acf_range_universality.py:32:        primes = list(primerange(2, upper))
tools/exp_acf_range_universality.py:42:        return np.zeros(max_lag)
tools/exp_acf_range_universality.py:81:        eigs = np.sort(np.linalg.eigvalsh(H))
tools/exp_acf_range_universality.py:103:    gaps = np.diff(primes).astype(float)
tools/exp_acf_range_universality.py:170:    gue_gaps = generate_rmt_spacings(N_rmt, n_mat_rmt, 'GUE')
tools/exp_acf_range_universality.py:174:    poisson_gaps = np.random.exponential(1.0, 100_000)
tools/exp_acf_range_universality.py:182:        'GUE': gue_gaps,
tools/exp_acf_range_universality.py:184:        'Poisson': poisson_gaps,
tools/exp_dipolar_angle_reference.py:21:from numpy.linalg import eigvalsh
tools/exp_dipolar_angle_reference.py:24:def get_primes(n_max):
tools/exp_dipolar_angle_reference.py:79:def generate_gue_gaps(n_gaps, matrix_size=500):
tools/exp_dipolar_angle_reference.py:87:        eigs = np.sort(eigvalsh(H))
tools/exp_dipolar_angle_reference.py:105:        eigs = np.sort(eigvalsh(H))
tools/exp_dipolar_angle_reference.py:115:def generate_poisson_gaps(n_gaps):
tools/exp_dipolar_angle_reference.py:120:def generate_cramer_gaps(primes):
tools/exp_dipolar_angle_reference.py:122:    gaps = np.diff(primes).astype(float)
tools/exp_dipolar_angle_reference.py:143:    prime_gaps = np.diff(primes_filtered).astype(float)
tools/exp_dipolar_angle_reference.py:155:    gue_thetas = []
tools/exp_dipolar_angle_reference.py:156:    gue_data = []
tools/exp_dipolar_angle_reference.py:158:        gue_gaps = generate_gue_gaps(len(prime_gaps))
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:160:        gue_thetas.append(theta)
tools/exp_dipolar_angle_reference.py:161:        gue_data.append((theta, dsr, dl1, sr, l1))
tools/exp_dipolar_angle_reference.py:164:    gue_thetas = np.array(gue_thetas)
tools/exp_dipolar_angle_reference.py:166:        'theta_mean': np.mean(gue_thetas), 'theta_std': np.std(gue_thetas),
tools/exp_dipolar_angle_reference.py:167:        'thetas': gue_thetas.tolist(),
tools/exp_dipolar_angle_reference.py:168:        'SR_mean': np.mean([d[3] for d in gue_data]),
tools/exp_dipolar_angle_reference.py:169:        'L1_mean': np.mean([d[4] for d in gue_data]),
tools/exp_dipolar_angle_reference.py:171:    print(f"  GUE: theta = {np.mean(gue_thetas):.1f} +/- {np.std(gue_thetas):.1f} deg")
tools/exp_dipolar_angle_reference.py:178:        goe_gaps = generate_goe_gaps(len(prime_gaps))
tools/exp_dipolar_angle_reference.py:198:        poi_gaps = generate_poisson_gaps(len(prime_gaps))
tools/exp_dipolar_angle_reference.py:218:        cramer_gaps = generate_cramer_gaps(primes_filtered)
tools/exp_beta_crossover.py:90:    off_diag = np.zeros(N - 1)
tools/exp_beta_crossover.py:98:    from scipy.linalg import eigvalsh_tridiagonal
tools/exp_beta_crossover.py:99:    eigs = eigvalsh_tridiagonal(diag, off_diag)
tools/exp_beta_crossover.py:119:def gen_primes_multiscale(scales=None):
tools/exp_markov_dipolar_decomposition.py:31:def get_primes(n_max):
tools/exp_markov_dipolar_decomposition.py:111:    T = np.zeros((n_bins, n_bins), dtype=float)
tools/exp_markov_dipolar_decomposition.py:125:    centers = np.zeros(n_bins)
tools/exp_markov_dipolar_decomposition.py:142:    gaps = np.zeros(n_gaps)
tools/exp_markov_dipolar_decomposition.py:172:    real_gaps = np.diff(primes_f).astype(float)
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:49:        eigs = np.sort(np.linalg.eigvalsh(h).real)
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:60:    if len(gue) != len(poisson):
tools/exp_boundary_mixture_gate.py:61:        raise ValueError("gue and poisson arrays must have the same length")
tools/exp_boundary_mixture_gate.py:62:    mask = rng.random(len(gue)) < beta
tools/exp_boundary_mixture_gate.py:63:    out = gue.copy()
tools/exp_boundary_mixture_gate.py:64:    out[mask] = poisson[mask]
tools/exp_boundary_mixture_gate.py:111:    gue_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
tools/exp_boundary_mixture_gate.py:113:    all_endpoint = np.vstack([gue_vectors, poi_vectors])
tools/exp_boundary_mixture_gate.py:116:    gue_centroid = np.mean(gue_vectors, axis=0)
tools/exp_boundary_mixture_gate.py:118:    endpoint_distance = float(np.linalg.norm((poi_centroid - gue_centroid) / scale))
tools/exp_boundary_mixture_gate.py:128:            d_gue = float(np.linalg.norm((x - gue_centroid) / scale))
tools/exp_boundary_mixture_gate.py:130:            denom = d_gue + d_poi
tools/exp_boundary_mixture_gate.py:131:            coord = float((d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
tools/exp_boundary_mixture_gate.py:132:            margin = float(abs(d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
tools/exp_boundary_mixture_gate.py:135:            labels.append("gue" if d_gue < d_poi else "poisson")
tools/exp_boundary_mixture_gate.py:144:            "poisson_label_fraction": float(np.mean(np.array(labels) == "poisson")),
tools/exp_boundary_mixture_gate.py:196:        gue = gue_spacings(args.gue_matrix_size, args.n_gaps, rep_rng)
tools/exp_boundary_mixture_gate.py:197:        poisson = rep_rng.exponential(1.0, size=args.n_gaps)
tools/exp_boundary_mixture_gate.py:198:        poisson = poisson / float(np.mean(poisson))
tools/exp_boundary_mixture_gate.py:201:            gaps = mixture_spacings(gue, poisson, beta, layer_rng)
tools/exp_boundary_mixture_gate.py:249:        print("beta stable_count margin_gated ambiguous_gated poisson_fraction_gated")
tools/exp_boundary_mixture_gate.py:252:        print("beta stable_count margin_all ambiguous_all poisson_fraction_all")
tools/exp_boundary_mixture_gate.py:262:            f"{layer.get('poisson_label_fraction', 0.0):>21.3f}"
tools/exp_boundary_mixture_gate.py:272:    parser.add_argument("--gue-matrix-size", type=int, default=180)
tools/exp_coherence_length.py:23:def sieve_primes(limit):
tools/exp_coherence_length.py:104:def measure_coherence_by_scale(all_gaps, all_primes, window_lengths,
tools/exp_coherence_length.py:162:    gaps = np.diff(primes).astype(float)
tools/exp_crossover_universality.py:278:    ln_p_poisson_beta = -fits['beta']['intercept'] / fits['beta']['slope']
tools/exp_crossover_universality.py:279:    p_poisson_beta = np.exp(ln_p_poisson_beta)
tools/exp_crossover_universality.py:280:    print(f"  beta -> 0 (Poisson) at ln(p)={ln_p_poisson_beta:.1f}, p~{p_poisson_beta:.2e}")
tools/exp_crossover_universality.py:282:    ln_p_poisson_dRacf1 = -fits['dR_acf1']['intercept'] / fits['dR_acf1']['slope']
tools/exp_crossover_universality.py:283:    p_poisson_dRacf1 = np.exp(ln_p_poisson_dRacf1)
tools/exp_crossover_universality.py:284:    print(f"  dR_acf1 -> 0 at ln(p)={ln_p_poisson_dRacf1:.1f}, p~{p_poisson_dRacf1:.2e}")
tools/exp_crossover_universality.py:286:    ln_p_poisson_gacf1 = -fits['gap_acf1']['intercept'] / fits['gap_acf1']['slope']
tools/exp_crossover_universality.py:287:    p_poisson_gacf1 = np.exp(ln_p_poisson_gacf1)
tools/exp_crossover_universality.py:288:    print(f"  gap_acf1 -> 0 at ln(p)={ln_p_poisson_gacf1:.1f}, p~{p_poisson_gacf1:.2e}")
tools/exp_crossover_universality.py:290:    ln_p_poisson_r = (R_POISSON - fits['r_mean']['intercept']) / fits['r_mean']['slope']
tools/exp_crossover_universality.py:291:    p_poisson_r = np.exp(ln_p_poisson_r)
tools/exp_crossover_universality.py:292:    print(f"  <r> -> {R_POISSON:.4f} (Poisson) at ln(p)={ln_p_poisson_r:.1f}, p~{p_poisson_r:.2e}")
tools/exp_crossover_phase_test.py:87:def generate_gue_gaps(N, rng):
tools/exp_crossover_phase_test.py:94:        evals = np.sort(np.linalg.eigvalsh(H))
tools/exp_crossover_phase_test.py:102:def generate_prime_gaps(N):
tools/exp_crossover_phase_test.py:106:    primes = list(sympy.primerange(2, limit))
tools/exp_crossover_phase_test.py:108:        primes = list(sympy.primerange(2, limit * 2))
tools/exp_crossover_phase_test.py:110:    gaps = np.diff(primes)
tools/exp_crossover_phase_test.py:136:    x = np.zeros(N)
tools/exp_crossover_phase_test.py:161:def generate_poisson(N, rng):
tools/exp_crossover_phase_test.py:216:    sequences['GUE'] = generate_gue_gaps(args.N, rng)
tools/exp_crossover_phase_test.py:218:    sequences['Primes'] = generate_prime_gaps(args.N)
tools/exp_crossover_phase_test.py:228:    sequences['Poisson'] = generate_poisson(args.N, rng)
tools/exp_markov_psd_prediction.py:25:def get_primes_sieve(n_max):
tools/exp_markov_psd_prediction.py:36:def residue_sequence(primes):
tools/exp_markov_psd_prediction.py:43:    T = np.zeros((2, 2))
tools/exp_det_drift.py:26:def generate_primes(n_primes, start=2):
tools/exp_det_drift.py:66:        primes = generate_primes(window_size + 1, start=s)
tools/exp_det_drift.py:67:        gaps = np.diff(primes)
tools/exp_desitter_unification.py:137:gaps_all = np.diff(primes)
tools/exp_desitter_unification.py:144:    cramer_gaps = np.zeros(N_total - 1)
tools/exp_desitter_unification.py:197:def z_score(prime_val, null_vals):
tools/exp_modular_memory_spectrum.py:32:def sieve_primes(n_max):
tools/exp_modular_memory_spectrum.py:42:def cramer_random_primes(n_max, rng):
tools/exp_modular_memory_spectrum.py:60:    trans = np.zeros((n_classes, n_classes), dtype=int)
tools/exp_modular_memory_spectrum.py:141:    gaps = np.diff(primes).astype(float)
tools/exp_dipolar_vector_scaling.py:21:from sympy import primerange
tools/exp_dipolar_vector_scaling.py:24:def get_primes_in_range(lo, hi):
tools/exp_dipolar_vector_scaling.py:26:    return np.array(list(primerange(lo, hi)), dtype=np.int64)
tools/exp_dipolar_vector_scaling.py:72:def analyze_scale(primes, label=""):
tools/exp_dipolar_vector_scaling.py:74:    gaps = np.diff(primes)
tools/exp_markov3_observable_hunt.py:26:def get_primes(n_max):
tools/exp_markov3_observable_hunt.py:74:    result = np.zeros(n)
tools/exp_markov3_observable_hunt.py:231:    gaps = np.diff(primes).astype(float)
tools/exp_perturbation_rank_size_curve.py:37:def prime_gaps(n_gaps: int) -> np.ndarray:

 succeeded in 0ms:
./tools/dnd_zero_controllo2.py
./tools/dnd_zero_varieta_primi.py
./tools/dnd_zero_controllo.py
./tools/exp_boundary_gue_poisson.py
./tools/zeta_validation.py
./tools/__pycache__/dnd_zero_notturno.cpython-312.pyc
./tools/__pycache__/dnd_zero_traiettoria.cpython-312.pyc
./tools/__pycache__/dnd_zero_operator.cpython-312.pyc
./tools/__pycache__/zeta_validation.cpython-312.pyc
./tools/__pycache__/dnd_zero_varieta.cpython-312.pyc
./tools/__pycache__/dnd_zero_varieta_primi.cpython-312.pyc
./tools/__pycache__/dnd_zero_ising.cpython-312.pyc
./tools/__pycache__/dnd_gue_test.cpython-312.pyc
./tools/__pycache__/exp_markov_memory_by_gue_type.cpython-312.pyc
./tools/__pycache__/dnd_zero_controllo2.cpython-312.pyc
./tools/r_stat_primes.py
./tools/exp_geodesic_deviation_primes.py
./tools/dnd_zero_traiettoria.py
./tools/dnd_gue_test.py
./tools/gue_gap_test.py
./tools/exp_markov_memory_by_gue_type.py
./tools/dnd_zero_notturno.py
./tools/test_gue_poisson_boundary.py
./tools/dnd_zero_operator.py
./tools/exp_psd_prime_gaps.py
./tools/dnd_zero_varieta.py
./tools/gap_ratio_primes.py
./tools/exp_ricci_primes.py
./tools/dnd_spettro_zeta.py
./tools/PROTOCOLLO_ZETA.md
./tools/dnd_zeros_vs_zeta.py
./tools/dnd_zero_ising.py
./tools/data/two_channel_shuffle_audit.json
./tools/data/audit_paper_D_draft2.json
./tools/data/r_excess_l_functions.json
./tools/data/cognitive_fingerprint.json
./tools/data/m_spectro_calibra_20260310_2015.json
./tools/data/zero_traiettoria_20260310_1649.json
./tools/data/incrocio_20260428_0340.json
./tools/data/notte_20260326_0330.md
./tools/data/torre_results.json
./tools/data/agent_field_live.md
./tools/data/meta_tautology_test.json
./tools/data/valutatore_log.jsonl
./tools/data/projective_quantization_results.json
./tools/data/seme_backup_pre_run.json
./tools/data/pipeline_state.json
./tools/data/prime_gaps_spectrum.png
./tools/data/exp_markov_psd_prediction.json
./tools/data/piano11_results.json
./tools/data/audit_paper_G_draft3.json
./tools/data/observable_collinearity_breaking_20260506_1956.json
./tools/data/incrocio_20260506_0633.json
./tools/data/observable_collinearity_breaking_20260506_1955.json
./tools/data/iterata_M_confronto_20260312_1254.json
./tools/data/promotions/promotion_20260507_0330.json
./tools/data/promotions/promotion_20260506_0625.json
./tools/data/promotions/promotion_20260506_1941.json
./tools/data/promotions/promotion_20260507_0803.json
./tools/data/promotions/promotion_20260506_1955.json
./tools/data/notte_20260312_0330.md
./tools/data/notte_20260303_0330.md
./tools/data/dipartimento_journal.jsonl
./tools/data/conoscenza_teorie.json.bak.retraction_22_04
./tools/data/duality_gate_transfer_20260507_0803_seedcheck.json
./tools/data/specchio_20260314.json
./tools/data/zero_primi_20260310_0820.json
./tools/data/lab_results.json
./tools/data/notte_20260317_0330.md
./tools/data/audit_paper_C_draft2.json
./tools/data/mod3_scaling.json
./tools/data/modular_memory_spectrum.json
./tools/data/exp_det_drift.json
./tools/data/observable_collinearity_breaking_20260506_1957.json
./tools/data/incrocio_20260402_0344.json
./tools/data/engine_state.json
./tools/data/exp_two_channel_universality.json
./tools/data/trasmutazione_results.json
./tools/data/zero_varieta_primi_20260310_1842.json
./tools/data/audit_paper_F_draft3.json
./tools/data/trace_bridge_results.json
./tools/data/tm1_figures/family_table.json
./tools/data/tm1_figures/tensions.json
./tools/data/tm1_figures/tensions_raw.json
./tools/data/tm1_figures/duality_map.json
./tools/data/tm1_figures/phase_diagram.json
./tools/data/tm1_figures/prime_gaps_spectrum_pub.png
./tools/data/tm1_figures/version.json
./tools/data/tm1_figures/zero_signature_logistic.png
./tools/data/tm1_figures/summary.json
./tools/data/tm1_figures/m_spectro_18_domains.png
./tools/data/tm1_figures/scaling_plot.json
./tools/data/notte_20260311_0330.md
./tools/data/proto_oom_001.json
./tools/data/godel_configs/tm1_editorial.json
./tools/data/godel_configs/content_creator.json
./tools/data/godel_configs/tm5_product.json
./tools/data/godel_configs/tm3_research.json
./tools/data/godel_configs/founder.json
./tools/data/godel_configs/consulenza.json
./tools/data/lab_data.json
./tools/data/two_layer_universality.json
./tools/data/synthetic_validation.json
./tools/data/ponti_evoluti.json
./tools/data/magnitude_psd_from_acf.json
./tools/data/dipolar_vector_scaling.json
./tools/data/arxiv_cache.json
./tools/data/markov_k_direction.json
./tools/data/duality_gate_transfer_20260507_0803.json
./tools/data/notte_20260309_0330.md
./tools/data/seme.json
./tools/data/exp_conditional_r.json
./tools/data/two_channel_boundary.json
./tools/data/lab_vincoli.md
./tools/data/consecutio_processata.json
./tools/data/markov_layer_recovery_audit.json
./tools/data/notte_20260402_0330.md
./tools/data/spectral_rigidity_results.json
./tools/data/multi_pattern_results.json
./tools/data/modular_algebra_depth.json
./tools/data/incrocio_20260404_0330.json
./tools/data/exp_acf_stationarity.json
./tools/data/audit_paper_E_draft3.json
./tools/data/denominator_gate_transfer_matrix.json
./tools/data/knowledge_state.json
./tools/data/incrocio_20260331_1807.json
./tools/data/notte_20260327_0330.md
./tools/data/incrocio_20260402_0755.json
./tools/data/two_channel_cross_domain.json
./tools/data/m_spectro_11domini.json
./tools/data/notte_20260330_0330.md
./tools/data/curva_results.json
./tools/data/odlyzko_block2_probe.json
./tools/data/stato_ciclo.json
./tools/data/domandatore/domandatore_20260324_1716.json
./tools/data/domandatore/domandatore_20260308_2007.json
./tools/data/domandatore/domandatore_20260420_0345.json
./tools/data/domandatore/domandatore_20260318_1853.json
./tools/data/domandatore/domandatore_20260305_2023.json
./tools/data/domandatore/domandatore_20260306_1032.json
./tools/data/domandatore/domandatore_20260423_0345.json
./tools/data/domandatore/domandatore_20260328_0343.json
./tools/data/domandatore/domandatore_20260315_0341.json
./tools/data/domandatore/domandatore_20260322_1726.json
./tools/data/domandatore/domandatore_20260327_1855.json
./tools/data/domandatore/domandatore_20260505_0345.json
./tools/data/domandatore/domandatore_20260314_1903.json
./tools/data/domandatore/domandatore_20260312_1547.json
./tools/data/domandatore/domandatore_20260426_0345.json
./tools/data/domandatore/domandatore_20260314_1904.json
./tools/data/domandatore/domandatore_20260429_0345.json
./tools/data/domandatore/domandatore_20260329_0342.json
./tools/data/domandatore/domandatore_20260424_0345.json
./tools/data/domandatore/domandatore_20260305_2005.json
./tools/data/domandatore/domandatore_20260318_1852.json
./tools/data/domandatore/domandatore_20260415_0345.json
./tools/data/domandatore/domandatore_20260315_0829.json
./tools/data/domandatore/domandatore_20260319_1445.json
./tools/data/domandatore/domandatore_20260430_0345.json
./tools/data/domandatore/domandatore_20260327_0949.json
./tools/data/domandatore/domandatore_20260326_1337.json
./tools/data/domandatore/domandatore_20260306_1327.json
./tools/data/domandatore/domandatore_20260305_1953.json
./tools/data/domandatore/domandatore_20260506_0345.json
./tools/data/domandatore/domandatore_20260308_1931.json
./tools/data/domandatore/domandatore_20260306_0341.json
./tools/data/domandatore/domandatore_20260405_0345.json
./tools/data/domandatore/domandatore_20260311_2212.json
./tools/data/domandatore/domandatore_20260405_0715.json
./tools/data/domandatore/domandatore_20260319_1417.json
./tools/data/domandatore/domandatore_20260307_1947.json
./tools/data/domandatore/domandatore_20260317_1636.json
./tools/data/domandatore/domandatore_20260327_1906.json
./tools/data/domandatore/domandatore_20260330_0344.json
./tools/data/domandatore/domandatore_20260331_0344.json
./tools/data/domandatore/domandatore_20260417_0345.json
./tools/data/domandatore/domandatore_20260305_2016.json
./tools/data/domandatore/domandatore_20260327_0344.json
./tools/data/domandatore/domandatore_20260405_0810.json
./tools/data/domandatore/domandatore_20260305_2025.json
./tools/data/domandatore/domandatore_20260410_0345.json
./tools/data/domandatore/domandatore_20260331_1602.json
./tools/data/domandatore/domandatore_20260331_0345.json
./tools/data/domandatore/domandatore_20260412_0345.json
./tools/data/domandatore/domandatore_20260328_1801.json
./tools/data/domandatore/domandatore_20260305_2028.json
./tools/data/domandatore/domandatore_20260308_1504.json
./tools/data/domandatore/domandatore_20260421_0746.json
./tools/data/domandatore/domandatore_20260326_0343.json
./tools/data/domandatore/domandatore_20260308_1936.json
./tools/data/domandatore/domandatore_20260427_0345.json
./tools/data/domandatore/domandatore_20260405_0813.json
./tools/data/domandatore/domandatore_20260321_0842.json
./tools/data/domandatore/domandatore_20260322_1836.json
./tools/data/domandatore/domandatore_20260318_1856.json
./tools/data/domandatore/domandatore_20260418_0345.json
./tools/data/domandatore/domandatore_20260306_1329.json
./tools/data/domandatore/domandatore_20260318_1732.json
./tools/data/domandatore/domandatore_20260411_0345.json
./tools/data/domandatore/domandatore_20260411_0807_autoricerca.json

codex
Creo uno strumento nuovo invece di modificare quello 09:01: stessi osservabili e stesso z-gate, ma generatori reali/semi-reali con endpoint coerente = sequenza ordinata e endpoint illusorio = permutazione marginale. Uso primi, zeri zeta calcolati localmente e ritorni della mappa logistica come tre perimetri con natura diversa.
apply patch
patch: completed
/opt/MM_D-ND/tools/exp_semireal_order_denominator_gate.py
diff --git a/tools/exp_semireal_order_denominator_gate.py b/tools/exp_semireal_order_denominator_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..f3da4e51c84f066066367b25fded3ebea3c1c00d
--- /dev/null
+++ b/tools/exp_semireal_order_denominator_gate.py
@@ -0,0 +1,352 @@
+#!/usr/bin/env python3
+"""
+exp_semireal_order_denominator_gate.py
+
+Falsification attempt for ORDER_DENOMINATOR_GATE on non-synthetic / semi-real
+ordered sequences. The coherent endpoint is the observed order of each sequence;
+the illusory endpoint is a marginal-preserving permutation. The same
+original-vs-shuffle denominator gate used in the prior reports is applied to
+canonical observables from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import math
+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 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 sieve_primes_for_count(n_primes: int) -> np.ndarray:
+    if n_primes < 6:
+        limit = 20
+    else:
+        limit = int(n_primes * (math.log(n_primes) + math.log(math.log(n_primes))) * 1.25)
+    while True:
+        sieve = np.ones(limit + 1, dtype=bool)
+        sieve[:2] = False
+        for p in range(2, int(limit**0.5) + 1):
+            if sieve[p]:
+                sieve[p * p : limit + 1 : p] = False
+        primes = np.flatnonzero(sieve)
+        if len(primes) >= n_primes:
+            return primes[:n_primes].astype(float)
+        limit *= 2
+
+
+def prime_gap_sequence(n_gaps: int) -> np.ndarray:
+    primes = sieve_primes_for_count(n_gaps + 1)
+    return normalize(np.diff(primes))
+
+
+def zeta_zero_spacings(n_gaps: int) -> np.ndarray:
+    try:
+        import mpmath as mp
+    except ImportError as exc:
+        raise RuntimeError("mpmath is required for zeta_zero_spacings") from exc
+
+    zeros = np.empty(n_gaps + 1, dtype=float)
+    for i in range(n_gaps + 1):
+        zeros[i] = float(mp.im(mp.zetazero(i + 1)))
+    return normalize(np.diff(zeros))
+
+
+def logistic_return_intervals(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    # Return intervals to a high-density-edge event in the fully chaotic logistic map.
+    threshold = 0.95
+    burn = 2000
+    needed = n_gaps + 1
+    returns: list[int] = []
+    last_hit: int | None = None
+    x = float(rng.random())
+    i = 0
+    max_steps = 50_000_000
+    while len(returns) < needed and i < max_steps:
+        x = 4.0 * x * (1.0 - x)
+        if i >= burn and x > threshold:
+            if last_hit is not None:
+                returns.append(i - last_hit)
+            last_hit = i
+        i += 1
+    if len(returns) < needed:
+        raise RuntimeError(f"logistic generator produced {len(returns)} intervals, need {needed}")
+    return normalize(np.array(returns[:n_gaps], dtype=float))
+
+
+def beta_replace(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        return base.copy()
+    if beta >= 1.0:
+        return illusory
+    out = base.copy()
+    mask = rng.random(len(base)) < beta
+    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], dict[str, float]]:
+    original = compute_canonical(gaps)
+    baseline = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline[name].append(obs[name])
+
+    means = {}
+    sds = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline[name], dtype=float)
+        means[name] = float(np.mean(vals))
+        sds[name] = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        z[name] = float((original[name] - means[name]) / sds[name]) if sds[name] > 1e-15 else 0.0
+    return original, means, sds, 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 = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent, illusory])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent, axis=0)
+    illusory_centroid = np.mean(illusory, 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()):
+        margins = []
+        labels = []
+        coords = []
+        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)),
+            "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_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()},
+        }
+
+    one_sided = []
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    for name in OBS_NAMES:
+        coherent_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in coherent_rows]))
+        illusory_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in illusory_rows]))
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            one_sided.append(name)
+        if coherent_freq >= 0.75 and illusory_freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "coherent_one_sided_observables": one_sided,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def analyze_sequence(name: str, base: np.ndarray, 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):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        for beta in betas:
+            gaps = beta_replace(base, beta, rep_rng)
+            obs, shuffle_mean, 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_mean": shuffle_mean,
+                    "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)
+    return {
+        "source": {
+            "n_gaps": int(len(base)),
+            "mean": float(np.mean(base)),
+            "variance": float(np.var(base)),
+        },
+        "gate": gate,
+        "classification_all_observables": classify_layers(rows, OBS_NAMES),
+        "classification_one_sided_gated": classify_layers(rows, gate["coherent_one_sided_observables"]),
+        "rows": rows,
+    }
+
+
+def build_sequences(args: argparse.Namespace, rng: np.random.Generator) -> dict[str, np.ndarray]:
+    sequences = {
+        "prime_gaps_first": prime_gap_sequence(args.n_gaps),
+        "logistic_return_intervals": logistic_return_intervals(args.n_gaps, rng),
+    }
+    if args.include_zeta:
+        sequences["zeta_zero_spacings_first"] = zeta_zero_spacings(args.zeta_gaps)
+    return sequences
+
+
+def compact(perimeters: dict) -> dict:
+    out = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        all_cls = data["classification_all_observables"]
+        gated_cls = data["classification_one_sided_gated"]
+        out[name] = {
+            "n_gaps": data["source"]["n_gaps"],
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+            "z_mean_coherent": gate["layers"]["0.000"]["z_mean"],
+            "z_mean_illusory": gate["layers"]["1.000"]["z_mean"],
+            "endpoint_distance_all": all_cls["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": gated_cls["endpoint_distance"],
+            "ambiguous_beta_one_sided_gated": gated_cls["ambiguous_beta"],
+        }
+    return out
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    sequences = build_sequences(args, root_rng)
+    perimeters = {}
+    for name, base in sequences.items():
+        perimeters[name] = analyze_sequence(name, base, args, root_rng)
+
+    output = {
+        "experiment": "semireal_order_denominator_gate",
+        "category": "gate_falsification_semireal",
+        "question": "Does ORDER_DENOMINATOR_GATE survive on non-synthetic / semi-real ordered sequences?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact(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 n one_sided stable0 stable1 dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>28s} "
+            f"{row['n_gaps']:>5d} "
+            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_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("--zeta-gaps", type=int, default=1024)
+    parser.add_argument("--include-zeta", action="store_true")
+    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=202605070923)
+    parser.add_argument("--out", default="tools/data/semireal_order_denominator_gate_20260507_0923.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

diff --git a/tools/exp_semireal_order_denominator_gate.py b/tools/exp_semireal_order_denominator_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..f3da4e51c84f066066367b25fded3ebea3c1c00d
--- /dev/null
+++ b/tools/exp_semireal_order_denominator_gate.py
@@ -0,0 +1,352 @@
+#!/usr/bin/env python3
+"""
+exp_semireal_order_denominator_gate.py
+
+Falsification attempt for ORDER_DENOMINATOR_GATE on non-synthetic / semi-real
+ordered sequences. The coherent endpoint is the observed order of each sequence;
+the illusory endpoint is a marginal-preserving permutation. The same
+original-vs-shuffle denominator gate used in the prior reports is applied to
+canonical observables from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import math
+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 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 sieve_primes_for_count(n_primes: int) -> np.ndarray:
+    if n_primes < 6:
+        limit = 20
+    else:
+        limit = int(n_primes * (math.log(n_primes) + math.log(math.log(n_primes))) * 1.25)
+    while True:
+        sieve = np.ones(limit + 1, dtype=bool)
+        sieve[:2] = False
+        for p in range(2, int(limit**0.5) + 1):
+            if sieve[p]:
+                sieve[p * p : limit + 1 : p] = False
+        primes = np.flatnonzero(sieve)
+        if len(primes) >= n_primes:
+            return primes[:n_primes].astype(float)
+        limit *= 2
+
+
+def prime_gap_sequence(n_gaps: int) -> np.ndarray:
+    primes = sieve_primes_for_count(n_gaps + 1)
+    return normalize(np.diff(primes))
+
+
+def zeta_zero_spacings(n_gaps: int) -> np.ndarray:
+    try:
+        import mpmath as mp
+    except ImportError as exc:
+        raise RuntimeError("mpmath is required for zeta_zero_spacings") from exc
+
+    zeros = np.empty(n_gaps + 1, dtype=float)
+    for i in range(n_gaps + 1):
+        zeros[i] = float(mp.im(mp.zetazero(i + 1)))
+    return normalize(np.diff(zeros))
+
+
+def logistic_return_intervals(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    # Return intervals to a high-density-edge event in the fully chaotic logistic map.
+    threshold = 0.95
+    burn = 2000
+    needed = n_gaps + 1
+    returns: list[int] = []
+    last_hit: int | None = None
+    x = float(rng.random())
+    i = 0
+    max_steps = 50_000_000
+    while len(returns) < needed and i < max_steps:
+        x = 4.0 * x * (1.0 - x)
+        if i >= burn and x > threshold:
+            if last_hit is not None:
+                returns.append(i - last_hit)
+            last_hit = i
+        i += 1
+    if len(returns) < needed:
+        raise RuntimeError(f"logistic generator produced {len(returns)} intervals, need {needed}")
+    return normalize(np.array(returns[:n_gaps], dtype=float))
+
+
+def beta_replace(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        return base.copy()
+    if beta >= 1.0:
+        return illusory
+    out = base.copy()
+    mask = rng.random(len(base)) < beta
+    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], dict[str, float]]:
+    original = compute_canonical(gaps)
+    baseline = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline[name].append(obs[name])
+
+    means = {}
+    sds = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline[name], dtype=float)
+        means[name] = float(np.mean(vals))
+        sds[name] = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        z[name] = float((original[name] - means[name]) / sds[name]) if sds[name] > 1e-15 else 0.0
+    return original, means, sds, 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 = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent, illusory])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent, axis=0)
+    illusory_centroid = np.mean(illusory, 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()):
+        margins = []
+        labels = []
+        coords = []
+        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)),
+            "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_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()},
+        }
+
+    one_sided = []
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    for name in OBS_NAMES:
+        coherent_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in coherent_rows]))
+        illusory_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in illusory_rows]))
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            one_sided.append(name)
+        if coherent_freq >= 0.75 and illusory_freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "coherent_one_sided_observables": one_sided,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def analyze_sequence(name: str, base: np.ndarray, 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):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        for beta in betas:
+            gaps = beta_replace(base, beta, rep_rng)
+            obs, shuffle_mean, 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_mean": shuffle_mean,
+                    "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)
+    return {
+        "source": {
+            "n_gaps": int(len(base)),
+            "mean": float(np.mean(base)),
+            "variance": float(np.var(base)),
+        },
+        "gate": gate,
+        "classification_all_observables": classify_layers(rows, OBS_NAMES),
+        "classification_one_sided_gated": classify_layers(rows, gate["coherent_one_sided_observables"]),
+        "rows": rows,
+    }
+
+
+def build_sequences(args: argparse.Namespace, rng: np.random.Generator) -> dict[str, np.ndarray]:
+    sequences = {
+        "prime_gaps_first": prime_gap_sequence(args.n_gaps),
+        "logistic_return_intervals": logistic_return_intervals(args.n_gaps, rng),
+    }
+    if args.include_zeta:
+        sequences["zeta_zero_spacings_first"] = zeta_zero_spacings(args.zeta_gaps)
+    return sequences
+
+
+def compact(perimeters: dict) -> dict:
+    out = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        all_cls = data["classification_all_observables"]
+        gated_cls = data["classification_one_sided_gated"]
+        out[name] = {
+            "n_gaps": data["source"]["n_gaps"],
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+            "z_mean_coherent": gate["layers"]["0.000"]["z_mean"],
+            "z_mean_illusory": gate["layers"]["1.000"]["z_mean"],
+            "endpoint_distance_all": all_cls["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": gated_cls["endpoint_distance"],
+            "ambiguous_beta_one_sided_gated": gated_cls["ambiguous_beta"],
+        }
+    return out
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    sequences = build_sequences(args, root_rng)
+    perimeters = {}
+    for name, base in sequences.items():
+        perimeters[name] = analyze_sequence(name, base, args, root_rng)
+
+    output = {
+        "experiment": "semireal_order_denominator_gate",
+        "category": "gate_falsification_semireal",
+        "question": "Does ORDER_DENOMINATOR_GATE survive on non-synthetic / semi-real ordered sequences?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact(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 n one_sided stable0 stable1 dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>28s} "
+            f"{row['n_gaps']:>5d} "
+            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_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("--zeta-gaps", type=int, default=1024)
+    parser.add_argument("--include-zeta", action="store_true")
+    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=202605070923)
+    parser.add_argument("--out", default="tools/data/semireal_order_denominator_gate_20260507_0923.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

codex
Lo script è pronto. Avvio una prima esecuzione con zeta incluso ma taglia zeta più piccola: se il calcolo degli zeri è lento, il fallback resta comunque primi + ritorni logistici, ma provo l'atto minimo completo.
exec
/bin/bash -lc 'python tools/exp_semireal_order_denominator_gate.py --include-zeta --n-gaps 4096 --zeta-gaps 512 --n-replicates 20 --n-beta 11 --n-baseline 32 --seed 202605070923 --out tools/data/semireal_order_denominator_gate_20260507_0923.json' in /opt/MM_D-ND
diff --git a/tools/exp_semireal_order_denominator_gate.py b/tools/exp_semireal_order_denominator_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..f3da4e51c84f066066367b25fded3ebea3c1c00d
--- /dev/null
+++ b/tools/exp_semireal_order_denominator_gate.py
@@ -0,0 +1,352 @@
+#!/usr/bin/env python3
+"""
+exp_semireal_order_denominator_gate.py
+
+Falsification attempt for ORDER_DENOMINATOR_GATE on non-synthetic / semi-real
+ordered sequences. The coherent endpoint is the observed order of each sequence;
+the illusory endpoint is a marginal-preserving permutation. The same
+original-vs-shuffle denominator gate used in the prior reports is applied to
+canonical observables from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import math
+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 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 sieve_primes_for_count(n_primes: int) -> np.ndarray:
+    if n_primes < 6:
+        limit = 20
+    else:
+        limit = int(n_primes * (math.log(n_primes) + math.log(math.log(n_primes))) * 1.25)
+    while True:
+        sieve = np.ones(limit + 1, dtype=bool)
+        sieve[:2] = False
+        for p in range(2, int(limit**0.5) + 1):
+            if sieve[p]:
+                sieve[p * p : limit + 1 : p] = False
+        primes = np.flatnonzero(sieve)
+        if len(primes) >= n_primes:
+            return primes[:n_primes].astype(float)
+        limit *= 2
+
+
+def prime_gap_sequence(n_gaps: int) -> np.ndarray:
+    primes = sieve_primes_for_count(n_gaps + 1)
+    return normalize(np.diff(primes))
+
+
+def zeta_zero_spacings(n_gaps: int) -> np.ndarray:
+    try:
+        import mpmath as mp
+    except ImportError as exc:
+        raise RuntimeError("mpmath is required for zeta_zero_spacings") from exc
+
+    zeros = np.empty(n_gaps + 1, dtype=float)
+    for i in range(n_gaps + 1):
+        zeros[i] = float(mp.im(mp.zetazero(i + 1)))
+    return normalize(np.diff(zeros))
+
+
+def logistic_return_intervals(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    # Return intervals to a high-density-edge event in the fully chaotic logistic map.
+    threshold = 0.95
+    burn = 2000
+    needed = n_gaps + 1
+    returns: list[int] = []
+    last_hit: int | None = None
+    x = float(rng.random())
+    i = 0
+    max_steps = 50_000_000
+    while len(returns) < needed and i < max_steps:
+        x = 4.0 * x * (1.0 - x)
+        if i >= burn and x > threshold:
+            if last_hit is not None:
+                returns.append(i - last_hit)
+            last_hit = i
+        i += 1
+    if len(returns) < needed:
+        raise RuntimeError(f"logistic generator produced {len(returns)} intervals, need {needed}")
+    return normalize(np.array(returns[:n_gaps], dtype=float))
+
+
+def beta_replace(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        return base.copy()
+    if beta >= 1.0:
+        return illusory
+    out = base.copy()
+    mask = rng.random(len(base)) < beta
+    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], dict[str, float]]:
+    original = compute_canonical(gaps)
+    baseline = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline[name].append(obs[name])
+
+    means = {}
+    sds = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline[name], dtype=float)
+        means[name] = float(np.mean(vals))
+        sds[name] = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        z[name] = float((original[name] - means[name]) / sds[name]) if sds[name] > 1e-15 else 0.0
+    return original, means, sds, 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 = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent, illusory])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent, axis=0)
+    illusory_centroid = np.mean(illusory, 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()):
+        margins = []
+        labels = []
+        coords = []
+        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)),
+            "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_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()},
+        }
+
+    one_sided = []
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    for name in OBS_NAMES:
+        coherent_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in coherent_rows]))
+        illusory_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in illusory_rows]))
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            one_sided.append(name)
+        if coherent_freq >= 0.75 and illusory_freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "coherent_one_sided_observables": one_sided,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def analyze_sequence(name: str, base: np.ndarray, 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):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        for beta in betas:
+            gaps = beta_replace(base, beta, rep_rng)
+            obs, shuffle_mean, 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_mean": shuffle_mean,
+                    "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)
+    return {
+        "source": {
+            "n_gaps": int(len(base)),
+            "mean": float(np.mean(base)),
+            "variance": float(np.var(base)),
+        },
+        "gate": gate,
+        "classification_all_observables": classify_layers(rows, OBS_NAMES),
+        "classification_one_sided_gated": classify_layers(rows, gate["coherent_one_sided_observables"]),
+        "rows": rows,
+    }
+
+
+def build_sequences(args: argparse.Namespace, rng: np.random.Generator) -> dict[str, np.ndarray]:
+    sequences = {
+        "prime_gaps_first": prime_gap_sequence(args.n_gaps),
+        "logistic_return_intervals": logistic_return_intervals(args.n_gaps, rng),
+    }
+    if args.include_zeta:
+        sequences["zeta_zero_spacings_first"] = zeta_zero_spacings(args.zeta_gaps)
+    return sequences
+
+
+def compact(perimeters: dict) -> dict:
+    out = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        all_cls = data["classification_all_observables"]
+        gated_cls = data["classification_one_sided_gated"]
+        out[name] = {
+            "n_gaps": data["source"]["n_gaps"],
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+            "z_mean_coherent": gate["layers"]["0.000"]["z_mean"],
+            "z_mean_illusory": gate["layers"]["1.000"]["z_mean"],
+            "endpoint_distance_all": all_cls["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": gated_cls["endpoint_distance"],
+            "ambiguous_beta_one_sided_gated": gated_cls["ambiguous_beta"],
+        }
+    return out
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    sequences = build_sequences(args, root_rng)
+    perimeters = {}
+    for name, base in sequences.items():
+        perimeters[name] = analyze_sequence(name, base, args, root_rng)
+
+    output = {
+        "experiment": "semireal_order_denominator_gate",
+        "category": "gate_falsification_semireal",
+        "question": "Does ORDER_DENOMINATOR_GATE survive on non-synthetic / semi-real ordered sequences?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact(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 n one_sided stable0 stable1 dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>28s} "
+            f"{row['n_gaps']:>5d} "
+            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_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("--zeta-gaps", type=int, default=1024)
+    parser.add_argument("--include-zeta", action="store_true")
+    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=202605070923)
+    parser.add_argument("--out", default="tools/data/semireal_order_denominator_gate_20260507_0923.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

diff --git a/tools/exp_semireal_order_denominator_gate.py b/tools/exp_semireal_order_denominator_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..f3da4e51c84f066066367b25fded3ebea3c1c00d
--- /dev/null
+++ b/tools/exp_semireal_order_denominator_gate.py
@@ -0,0 +1,352 @@
+#!/usr/bin/env python3
+"""
+exp_semireal_order_denominator_gate.py
+
+Falsification attempt for ORDER_DENOMINATOR_GATE on non-synthetic / semi-real
+ordered sequences. The coherent endpoint is the observed order of each sequence;
+the illusory endpoint is a marginal-preserving permutation. The same
+original-vs-shuffle denominator gate used in the prior reports is applied to
+canonical observables from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import math
+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 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 sieve_primes_for_count(n_primes: int) -> np.ndarray:
+    if n_primes < 6:
+        limit = 20
+    else:
+        limit = int(n_primes * (math.log(n_primes) + math.log(math.log(n_primes))) * 1.25)
+    while True:
+        sieve = np.ones(limit + 1, dtype=bool)
+        sieve[:2] = False
+        for p in range(2, int(limit**0.5) + 1):
+            if sieve[p]:
+                sieve[p * p : limit + 1 : p] = False
+        primes = np.flatnonzero(sieve)
+        if len(primes) >= n_primes:
+            return primes[:n_primes].astype(float)
+        limit *= 2
+
+
+def prime_gap_sequence(n_gaps: int) -> np.ndarray:
+    primes = sieve_primes_for_count(n_gaps + 1)
+    return normalize(np.diff(primes))
+
+
+def zeta_zero_spacings(n_gaps: int) -> np.ndarray:
+    try:
+        import mpmath as mp
+    except ImportError as exc:
+        raise RuntimeError("mpmath is required for zeta_zero_spacings") from exc
+
+    zeros = np.empty(n_gaps + 1, dtype=float)
+    for i in range(n_gaps + 1):
+        zeros[i] = float(mp.im(mp.zetazero(i + 1)))
+    return normalize(np.diff(zeros))
+
+
+def logistic_return_intervals(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    # Return intervals to a high-density-edge event in the fully chaotic logistic map.
+    threshold = 0.95
+    burn = 2000
+    needed = n_gaps + 1
+    returns: list[int] = []
+    last_hit: int | None = None
+    x = float(rng.random())
+    i = 0
+    max_steps = 50_000_000
+    while len(returns) < needed and i < max_steps:
+        x = 4.0 * x * (1.0 - x)
+        if i >= burn and x > threshold:
+            if last_hit is not None:
+                returns.append(i - last_hit)
+            last_hit = i
+        i += 1
+    if len(returns) < needed:
+        raise RuntimeError(f"logistic generator produced {len(returns)} intervals, need {needed}")
+    return normalize(np.array(returns[:n_gaps], dtype=float))
+
+
+def beta_replace(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        return base.copy()
+    if beta >= 1.0:
+        return illusory
+    out = base.copy()
+    mask = rng.random(len(base)) < beta
+    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], dict[str, float]]:
+    original = compute_canonical(gaps)
+    baseline = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline[name].append(obs[name])
+
+    means = {}
+    sds = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline[name], dtype=float)
+        means[name] = float(np.mean(vals))
+        sds[name] = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        z[name] = float((original[name] - means[name]) / sds[name]) if sds[name] > 1e-15 else 0.0
+    return original, means, sds, 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 = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent, illusory])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent, axis=0)
+    illusory_centroid = np.mean(illusory, 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()):
+        margins = []
+        labels = []
+        coords = []
+        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)),
+            "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_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()},
+        }
+
+    one_sided = []
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    for name in OBS_NAMES:
+        coherent_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in coherent_rows]))
+        illusory_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in illusory_rows]))
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            one_sided.append(name)
+        if coherent_freq >= 0.75 and illusory_freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "coherent_one_sided_observables": one_sided,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def analyze_sequence(name: str, base: np.ndarray, 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):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        for beta in betas:
+            gaps = beta_replace(base, beta, rep_rng)
+            obs, shuffle_mean, 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_mean": shuffle_mean,
+                    "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)
+    return {
+        "source": {
+            "n_gaps": int(len(base)),
+            "mean": float(np.mean(base)),
+            "variance": float(np.var(base)),
+        },
+        "gate": gate,
+        "classification_all_observables": classify_layers(rows, OBS_NAMES),
+        "classification_one_sided_gated": classify_layers(rows, gate["coherent_one_sided_observables"]),
+        "rows": rows,
+    }
+
+
+def build_sequences(args: argparse.Namespace, rng: np.random.Generator) -> dict[str, np.ndarray]:
+    sequences = {
+        "prime_gaps_first": prime_gap_sequence(args.n_gaps),
+        "logistic_return_intervals": logistic_return_intervals(args.n_gaps, rng),
+    }
+    if args.include_zeta:
+        sequences["zeta_zero_spacings_first"] = zeta_zero_spacings(args.zeta_gaps)
+    return sequences
+
+
+def compact(perimeters: dict) -> dict:
+    out = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        all_cls = data["classification_all_observables"]
+        gated_cls = data["classification_one_sided_gated"]
+        out[name] = {
+            "n_gaps": data["source"]["n_gaps"],
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+            "z_mean_coherent": gate["layers"]["0.000"]["z_mean"],
+            "z_mean_illusory": gate["layers"]["1.000"]["z_mean"],
+            "endpoint_distance_all": all_cls["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": gated_cls["endpoint_distance"],
+            "ambiguous_beta_one_sided_gated": gated_cls["ambiguous_beta"],
+        }
+    return out
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    sequences = build_sequences(args, root_rng)
+    perimeters = {}
+    for name, base in sequences.items():
+        perimeters[name] = analyze_sequence(name, base, args, root_rng)
+
+    output = {
+        "experiment": "semireal_order_denominator_gate",
+        "category": "gate_falsification_semireal",
+        "question": "Does ORDER_DENOMINATOR_GATE survive on non-synthetic / semi-real ordered sequences?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact(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 n one_sided stable0 stable1 dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>28s} "
+            f"{row['n_gaps']:>5d} "
+            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_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("--zeta-gaps", type=int, default=1024)
+    parser.add_argument("--include-zeta", action="store_true")
+    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=202605070923)
+    parser.add_argument("--out", default="tools/data/semireal_order_denominator_gate_20260507_0923.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

diff --git a/tools/exp_semireal_order_denominator_gate.py b/tools/exp_semireal_order_denominator_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..f3da4e51c84f066066367b25fded3ebea3c1c00d
--- /dev/null
+++ b/tools/exp_semireal_order_denominator_gate.py
@@ -0,0 +1,352 @@
+#!/usr/bin/env python3
+"""
+exp_semireal_order_denominator_gate.py
+
+Falsification attempt for ORDER_DENOMINATOR_GATE on non-synthetic / semi-real
+ordered sequences. The coherent endpoint is the observed order of each sequence;
+the illusory endpoint is a marginal-preserving permutation. The same
+original-vs-shuffle denominator gate used in the prior reports is applied to
+canonical observables from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import math
+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 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 sieve_primes_for_count(n_primes: int) -> np.ndarray:
+    if n_primes < 6:
+        limit = 20
+    else:
+        limit = int(n_primes * (math.log(n_primes) + math.log(math.log(n_primes))) * 1.25)
+    while True:
+        sieve = np.ones(limit + 1, dtype=bool)
+        sieve[:2] = False
+        for p in range(2, int(limit**0.5) + 1):
+            if sieve[p]:
+                sieve[p * p : limit + 1 : p] = False
+        primes = np.flatnonzero(sieve)
+        if len(primes) >= n_primes:
+            return primes[:n_primes].astype(float)
+        limit *= 2
+
+
+def prime_gap_sequence(n_gaps: int) -> np.ndarray:
+    primes = sieve_primes_for_count(n_gaps + 1)
+    return normalize(np.diff(primes))
+
+
+def zeta_zero_spacings(n_gaps: int) -> np.ndarray:
+    try:
+        import mpmath as mp
+    except ImportError as exc:
+        raise RuntimeError("mpmath is required for zeta_zero_spacings") from exc
+
+    zeros = np.empty(n_gaps + 1, dtype=float)
+    for i in range(n_gaps + 1):
+        zeros[i] = float(mp.im(mp.zetazero(i + 1)))
+    return normalize(np.diff(zeros))
+
+
+def logistic_return_intervals(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    # Return intervals to a high-density-edge event in the fully chaotic logistic map.
+    threshold = 0.95
+    burn = 2000
+    needed = n_gaps + 1
+    returns: list[int] = []
+    last_hit: int | None = None
+    x = float(rng.random())
+    i = 0
+    max_steps = 50_000_000
+    while len(returns) < needed and i < max_steps:
+        x = 4.0 * x * (1.0 - x)
+        if i >= burn and x > threshold:
+            if last_hit is not None:
+                returns.append(i - last_hit)
+            last_hit = i
+        i += 1
+    if len(returns) < needed:
+        raise RuntimeError(f"logistic generator produced {len(returns)} intervals, need {needed}")
+    return normalize(np.array(returns[:n_gaps], dtype=float))
+
+
+def beta_replace(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        return base.copy()
+    if beta >= 1.0:
+        return illusory
+    out = base.copy()
+    mask = rng.random(len(base)) < beta
+    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], dict[str, float]]:
+    original = compute_canonical(gaps)
+    baseline = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline[name].append(obs[name])
+
+    means = {}
+    sds = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline[name], dtype=float)
+        means[name] = float(np.mean(vals))
+        sds[name] = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        z[name] = float((original[name] - means[name]) / sds[name]) if sds[name] > 1e-15 else 0.0
+    return original, means, sds, 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 = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent, illusory])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent, axis=0)
+    illusory_centroid = np.mean(illusory, 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()):
+        margins = []
+        labels = []
+        coords = []
+        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)),
+            "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_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()},
+        }
+
+    one_sided = []
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    for name in OBS_NAMES:
+        coherent_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in coherent_rows]))
+        illusory_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in illusory_rows]))
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            one_sided.append(name)
+        if coherent_freq >= 0.75 and illusory_freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "coherent_one_sided_observables": one_sided,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def analyze_sequence(name: str, base: np.ndarray, 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):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        for beta in betas:
+            gaps = beta_replace(base, beta, rep_rng)
+            obs, shuffle_mean, 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_mean": shuffle_mean,
+                    "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)
+    return {
+        "source": {
+            "n_gaps": int(len(base)),
+            "mean": float(np.mean(base)),
+            "variance": float(np.var(base)),
+        },
+        "gate": gate,
+        "classification_all_observables": classify_layers(rows, OBS_NAMES),
+        "classification_one_sided_gated": classify_layers(rows, gate["coherent_one_sided_observables"]),
+        "rows": rows,
+    }
+
+
+def build_sequences(args: argparse.Namespace, rng: np.random.Generator) -> dict[str, np.ndarray]:
+    sequences = {
+        "prime_gaps_first": prime_gap_sequence(args.n_gaps),
+        "logistic_return_intervals": logistic_return_intervals(args.n_gaps, rng),
+    }
+    if args.include_zeta:
+        sequences["zeta_zero_spacings_first"] = zeta_zero_spacings(args.zeta_gaps)
+    return sequences
+
+
+def compact(perimeters: dict) -> dict:
+    out = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        all_cls = data["classification_all_observables"]
+        gated_cls = data["classification_one_sided_gated"]
+        out[name] = {
+            "n_gaps": data["source"]["n_gaps"],
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+            "z_mean_coherent": gate["layers"]["0.000"]["z_mean"],
+            "z_mean_illusory": gate["layers"]["1.000"]["z_mean"],
+            "endpoint_distance_all": all_cls["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": gated_cls["endpoint_distance"],
+            "ambiguous_beta_one_sided_gated": gated_cls["ambiguous_beta"],
+        }
+    return out
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    sequences = build_sequences(args, root_rng)
+    perimeters = {}
+    for name, base in sequences.items():
+        perimeters[name] = analyze_sequence(name, base, args, root_rng)
+
+    output = {
+        "experiment": "semireal_order_denominator_gate",
+        "category": "gate_falsification_semireal",
+        "question": "Does ORDER_DENOMINATOR_GATE survive on non-synthetic / semi-real ordered sequences?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact(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 n one_sided stable0 stable1 dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>28s} "
+            f"{row['n_gaps']:>5d} "
+            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_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("--zeta-gaps", type=int, default=1024)
+    parser.add_argument("--include-zeta", action="store_true")
+    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=202605070923)
+    parser.add_argument("--out", default="tools/data/semireal_order_denominator_gate_20260507_0923.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

diff --git a/tools/exp_semireal_order_denominator_gate.py b/tools/exp_semireal_order_denominator_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..f3da4e51c84f066066367b25fded3ebea3c1c00d
--- /dev/null
+++ b/tools/exp_semireal_order_denominator_gate.py
@@ -0,0 +1,352 @@
+#!/usr/bin/env python3
+"""
+exp_semireal_order_denominator_gate.py
+
+Falsification attempt for ORDER_DENOMINATOR_GATE on non-synthetic / semi-real
+ordered sequences. The coherent endpoint is the observed order of each sequence;
+the illusory endpoint is a marginal-preserving permutation. The same
+original-vs-shuffle denominator gate used in the prior reports is applied to
+canonical observables from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import math
+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 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 sieve_primes_for_count(n_primes: int) -> np.ndarray:
+    if n_primes < 6:
+        limit = 20
+    else:
+        limit = int(n_primes * (math.log(n_primes) + math.log(math.log(n_primes))) * 1.25)
+    while True:
+        sieve = np.ones(limit + 1, dtype=bool)
+        sieve[:2] = False
+        for p in range(2, int(limit**0.5) + 1):
+            if sieve[p]:
+                sieve[p * p : limit + 1 : p] = False
+        primes = np.flatnonzero(sieve)
+        if len(primes) >= n_primes:
+            return primes[:n_primes].astype(float)
+        limit *= 2
+
+
+def prime_gap_sequence(n_gaps: int) -> np.ndarray:
+    primes = sieve_primes_for_count(n_gaps + 1)
+    return normalize(np.diff(primes))
+
+
+def zeta_zero_spacings(n_gaps: int) -> np.ndarray:
+    try:
+        import mpmath as mp
+    except ImportError as exc:
+        raise RuntimeError("mpmath is required for zeta_zero_spacings") from exc
+
+    zeros = np.empty(n_gaps + 1, dtype=float)
+    for i in range(n_gaps + 1):
+        zeros[i] = float(mp.im(mp.zetazero(i + 1)))
+    return normalize(np.diff(zeros))
+
+
+def logistic_return_intervals(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    # Return intervals to a high-density-edge event in the fully chaotic logistic map.
+    threshold = 0.95
+    burn = 2000
+    needed = n_gaps + 1
+    returns: list[int] = []
+    last_hit: int | None = None
+    x = float(rng.random())
+    i = 0
+    max_steps = 50_000_000
+    while len(returns) < needed and i < max_steps:
+        x = 4.0 * x * (1.0 - x)
+        if i >= burn and x > threshold:
+            if last_hit is not None:
+                returns.append(i - last_hit)
+            last_hit = i
+        i += 1
+    if len(returns) < needed:
+        raise RuntimeError(f"logistic generator produced {len(returns)} intervals, need {needed}")
+    return normalize(np.array(returns[:n_gaps], dtype=float))
+
+
+def beta_replace(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        return base.copy()
+    if beta >= 1.0:
+        return illusory
+    out = base.copy()
+    mask = rng.random(len(base)) < beta
+    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], dict[str, float]]:
+    original = compute_canonical(gaps)
+    baseline = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline[name].append(obs[name])
+
+    means = {}
+    sds = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline[name], dtype=float)
+        means[name] = float(np.mean(vals))
+        sds[name] = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        z[name] = float((original[name] - means[name]) / sds[name]) if sds[name] > 1e-15 else 0.0
+    return original, means, sds, 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 = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent, illusory])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent, axis=0)
+    illusory_centroid = np.mean(illusory, 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()):
+        margins = []
+        labels = []
+        coords = []
+        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)),
+            "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_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()},
+        }
+
+    one_sided = []
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    for name in OBS_NAMES:
+        coherent_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in coherent_rows]))
+        illusory_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in illusory_rows]))
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            one_sided.append(name)
+        if coherent_freq >= 0.75 and illusory_freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "coherent_one_sided_observables": one_sided,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def analyze_sequence(name: str, base: np.ndarray, 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):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        for beta in betas:
+            gaps = beta_replace(base, beta, rep_rng)
+            obs, shuffle_mean, 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_mean": shuffle_mean,
+                    "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)
+    return {
+        "source": {
+            "n_gaps": int(len(base)),
+            "mean": float(np.mean(base)),
+            "variance": float(np.var(base)),
+        },
+        "gate": gate,
+        "classification_all_observables": classify_layers(rows, OBS_NAMES),
+        "classification_one_sided_gated": classify_layers(rows, gate["coherent_one_sided_observables"]),
+        "rows": rows,
+    }
+
+
+def build_sequences(args: argparse.Namespace, rng: np.random.Generator) -> dict[str, np.ndarray]:
+    sequences = {
+        "prime_gaps_first": prime_gap_sequence(args.n_gaps),
+        "logistic_return_intervals": logistic_return_intervals(args.n_gaps, rng),
+    }
+    if args.include_zeta:
+        sequences["zeta_zero_spacings_first"] = zeta_zero_spacings(args.zeta_gaps)
+    return sequences
+
+
+def compact(perimeters: dict) -> dict:
+    out = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        all_cls = data["classification_all_observables"]
+        gated_cls = data["classification_one_sided_gated"]
+        out[name] = {
+            "n_gaps": data["source"]["n_gaps"],
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+            "z_mean_coherent": gate["layers"]["0.000"]["z_mean"],
+            "z_mean_illusory": gate["layers"]["1.000"]["z_mean"],
+            "endpoint_distance_all": all_cls["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": gated_cls["endpoint_distance"],
+            "ambiguous_beta_one_sided_gated": gated_cls["ambiguous_beta"],
+        }
+    return out
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    sequences = build_sequences(args, root_rng)
+    perimeters = {}
+    for name, base in sequences.items():
+        perimeters[name] = analyze_sequence(name, base, args, root_rng)
+
+    output = {
+        "experiment": "semireal_order_denominator_gate",
+        "category": "gate_falsification_semireal",
+        "question": "Does ORDER_DENOMINATOR_GATE survive on non-synthetic / semi-real ordered sequences?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact(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 n one_sided stable0 stable1 dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>28s} "
+            f"{row['n_gaps']:>5d} "
+            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_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("--zeta-gaps", type=int, default=1024)
+    parser.add_argument("--include-zeta", action="store_true")
+    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=202605070923)
+    parser.add_argument("--out", default="tools/data/semireal_order_denominator_gate_20260507_0923.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

codex
Il run è ancora in corso, con ogni probabilità fermo sul calcolo locale degli zeri zeta. Lo lascio finire ancora un momento: se non chiude in tempi utili userò il risultato come telemetria e riduco solo quel perimetro, senza cambiare il gate.
diff --git a/tools/exp_semireal_order_denominator_gate.py b/tools/exp_semireal_order_denominator_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..f3da4e51c84f066066367b25fded3ebea3c1c00d
--- /dev/null
+++ b/tools/exp_semireal_order_denominator_gate.py
@@ -0,0 +1,352 @@
+#!/usr/bin/env python3
+"""
+exp_semireal_order_denominator_gate.py
+
+Falsification attempt for ORDER_DENOMINATOR_GATE on non-synthetic / semi-real
+ordered sequences. The coherent endpoint is the observed order of each sequence;
+the illusory endpoint is a marginal-preserving permutation. The same
+original-vs-shuffle denominator gate used in the prior reports is applied to
+canonical observables from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import math
+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 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 sieve_primes_for_count(n_primes: int) -> np.ndarray:
+    if n_primes < 6:
+        limit = 20
+    else:
+        limit = int(n_primes * (math.log(n_primes) + math.log(math.log(n_primes))) * 1.25)
+    while True:
+        sieve = np.ones(limit + 1, dtype=bool)
+        sieve[:2] = False
+        for p in range(2, int(limit**0.5) + 1):
+            if sieve[p]:
+                sieve[p * p : limit + 1 : p] = False
+        primes = np.flatnonzero(sieve)
+        if len(primes) >= n_primes:
+            return primes[:n_primes].astype(float)
+        limit *= 2
+
+
+def prime_gap_sequence(n_gaps: int) -> np.ndarray:
+    primes = sieve_primes_for_count(n_gaps + 1)
+    return normalize(np.diff(primes))
+
+
+def zeta_zero_spacings(n_gaps: int) -> np.ndarray:
+    try:
+        import mpmath as mp
+    except ImportError as exc:
+        raise RuntimeError("mpmath is required for zeta_zero_spacings") from exc
+
+    zeros = np.empty(n_gaps + 1, dtype=float)
+    for i in range(n_gaps + 1):
+        zeros[i] = float(mp.im(mp.zetazero(i + 1)))
+    return normalize(np.diff(zeros))
+
+
+def logistic_return_intervals(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    # Return intervals to a high-density-edge event in the fully chaotic logistic map.
+    threshold = 0.95
+    burn = 2000
+    needed = n_gaps + 1
+    returns: list[int] = []
+    last_hit: int | None = None
+    x = float(rng.random())
+    i = 0
+    max_steps = 50_000_000
+    while len(returns) < needed and i < max_steps:
+        x = 4.0 * x * (1.0 - x)
+        if i >= burn and x > threshold:
+            if last_hit is not None:
+                returns.append(i - last_hit)
+            last_hit = i
+        i += 1
+    if len(returns) < needed:
+        raise RuntimeError(f"logistic generator produced {len(returns)} intervals, need {needed}")
+    return normalize(np.array(returns[:n_gaps], dtype=float))
+
+
+def beta_replace(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        return base.copy()
+    if beta >= 1.0:
+        return illusory
+    out = base.copy()
+    mask = rng.random(len(base)) < beta
+    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], dict[str, float]]:
+    original = compute_canonical(gaps)
+    baseline = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline[name].append(obs[name])
+
+    means = {}
+    sds = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline[name], dtype=float)
+        means[name] = float(np.mean(vals))
+        sds[name] = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        z[name] = float((original[name] - means[name]) / sds[name]) if sds[name] > 1e-15 else 0.0
+    return original, means, sds, 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 = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent, illusory])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent, axis=0)
+    illusory_centroid = np.mean(illusory, 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()):
+        margins = []
+        labels = []
+        coords = []
+        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)),
+            "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_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()},
+        }
+
+    one_sided = []
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    for name in OBS_NAMES:
+        coherent_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in coherent_rows]))
+        illusory_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in illusory_rows]))
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            one_sided.append(name)
+        if coherent_freq >= 0.75 and illusory_freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "coherent_one_sided_observables": one_sided,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def analyze_sequence(name: str, base: np.ndarray, 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):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        for beta in betas:
+            gaps = beta_replace(base, beta, rep_rng)
+            obs, shuffle_mean, 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_mean": shuffle_mean,
+                    "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)
+    return {
+        "source": {
+            "n_gaps": int(len(base)),
+            "mean": float(np.mean(base)),
+            "variance": float(np.var(base)),
+        },
+        "gate": gate,
+        "classification_all_observables": classify_layers(rows, OBS_NAMES),
+        "classification_one_sided_gated": classify_layers(rows, gate["coherent_one_sided_observables"]),
+        "rows": rows,
+    }
+
+
+def build_sequences(args: argparse.Namespace, rng: np.random.Generator) -> dict[str, np.ndarray]:
+    sequences = {
+        "prime_gaps_first": prime_gap_sequence(args.n_gaps),
+        "logistic_return_intervals": logistic_return_intervals(args.n_gaps, rng),
+    }
+    if args.include_zeta:
+        sequences["zeta_zero_spacings_first"] = zeta_zero_spacings(args.zeta_gaps)
+    return sequences
+
+
+def compact(perimeters: dict) -> dict:
+    out = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        all_cls = data["classification_all_observables"]
+        gated_cls = data["classification_one_sided_gated"]
+        out[name] = {
+            "n_gaps": data["source"]["n_gaps"],
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+            "z_mean_coherent": gate["layers"]["0.000"]["z_mean"],
+            "z_mean_illusory": gate["layers"]["1.000"]["z_mean"],
+            "endpoint_distance_all": all_cls["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": gated_cls["endpoint_distance"],
+            "ambiguous_beta_one_sided_gated": gated_cls["ambiguous_beta"],
+        }
+    return out
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    sequences = build_sequences(args, root_rng)
+    perimeters = {}
+    for name, base in sequences.items():
+        perimeters[name] = analyze_sequence(name, base, args, root_rng)
+
+    output = {
+        "experiment": "semireal_order_denominator_gate",
+        "category": "gate_falsification_semireal",
+        "question": "Does ORDER_DENOMINATOR_GATE survive on non-synthetic / semi-real ordered sequences?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact(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 n one_sided stable0 stable1 dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>28s} "
+            f"{row['n_gaps']:>5d} "
+            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_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("--zeta-gaps", type=int, default=1024)
+    parser.add_argument("--include-zeta", action="store_true")
+    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=202605070923)
+    parser.add_argument("--out", default="tools/data/semireal_order_denominator_gate_20260507_0923.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

diff --git a/tools/exp_semireal_order_denominator_gate.py b/tools/exp_semireal_order_denominator_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..f3da4e51c84f066066367b25fded3ebea3c1c00d
--- /dev/null
+++ b/tools/exp_semireal_order_denominator_gate.py
@@ -0,0 +1,352 @@
+#!/usr/bin/env python3
+"""
+exp_semireal_order_denominator_gate.py
+
+Falsification attempt for ORDER_DENOMINATOR_GATE on non-synthetic / semi-real
+ordered sequences. The coherent endpoint is the observed order of each sequence;
+the illusory endpoint is a marginal-preserving permutation. The same
+original-vs-shuffle denominator gate used in the prior reports is applied to
+canonical observables from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import math
+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 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 sieve_primes_for_count(n_primes: int) -> np.ndarray:
+    if n_primes < 6:
+        limit = 20
+    else:
+        limit = int(n_primes * (math.log(n_primes) + math.log(math.log(n_primes))) * 1.25)
+    while True:
+        sieve = np.ones(limit + 1, dtype=bool)
+        sieve[:2] = False
+        for p in range(2, int(limit**0.5) + 1):
+            if sieve[p]:
+                sieve[p * p : limit + 1 : p] = False
+        primes = np.flatnonzero(sieve)
+        if len(primes) >= n_primes:
+            return primes[:n_primes].astype(float)
+        limit *= 2
+
+
+def prime_gap_sequence(n_gaps: int) -> np.ndarray:
+    primes = sieve_primes_for_count(n_gaps + 1)
+    return normalize(np.diff(primes))
+
+
+def zeta_zero_spacings(n_gaps: int) -> np.ndarray:
+    try:
+        import mpmath as mp
+    except ImportError as exc:
+        raise RuntimeError("mpmath is required for zeta_zero_spacings") from exc
+
+    zeros = np.empty(n_gaps + 1, dtype=float)
+    for i in range(n_gaps + 1):
+        zeros[i] = float(mp.im(mp.zetazero(i + 1)))
+    return normalize(np.diff(zeros))
+
+
+def logistic_return_intervals(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    # Return intervals to a high-density-edge event in the fully chaotic logistic map.
+    threshold = 0.95
+    burn = 2000
+    needed = n_gaps + 1
+    returns: list[int] = []
+    last_hit: int | None = None
+    x = float(rng.random())
+    i = 0
+    max_steps = 50_000_000
+    while len(returns) < needed and i < max_steps:
+        x = 4.0 * x * (1.0 - x)
+        if i >= burn and x > threshold:
+            if last_hit is not None:
+                returns.append(i - last_hit)
+            last_hit = i
+        i += 1
+    if len(returns) < needed:
+        raise RuntimeError(f"logistic generator produced {len(returns)} intervals, need {needed}")
+    return normalize(np.array(returns[:n_gaps], dtype=float))
+
+
+def beta_replace(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        return base.copy()
+    if beta >= 1.0:
+        return illusory
+    out = base.copy()
+    mask = rng.random(len(base)) < beta
+    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], dict[str, float]]:
+    original = compute_canonical(gaps)
+    baseline = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline[name].append(obs[name])
+
+    means = {}
+    sds = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline[name], dtype=float)
+        means[name] = float(np.mean(vals))
+        sds[name] = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        z[name] = float((original[name] - means[name]) / sds[name]) if sds[name] > 1e-15 else 0.0
+    return original, means, sds, 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 = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent, illusory])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent, axis=0)
+    illusory_centroid = np.mean(illusory, 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()):
+        margins = []
+        labels = []
+        coords = []
+        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)),
+            "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_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()},
+        }
+
+    one_sided = []
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    for name in OBS_NAMES:
+        coherent_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in coherent_rows]))
+        illusory_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in illusory_rows]))
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            one_sided.append(name)
+        if coherent_freq >= 0.75 and illusory_freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "coherent_one_sided_observables": one_sided,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def analyze_sequence(name: str, base: np.ndarray, 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):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        for beta in betas:
+            gaps = beta_replace(base, beta, rep_rng)
+            obs, shuffle_mean, 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_mean": shuffle_mean,
+                    "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)
+    return {
+        "source": {
+            "n_gaps": int(len(base)),
+            "mean": float(np.mean(base)),
+            "variance": float(np.var(base)),
+        },
+        "gate": gate,
+        "classification_all_observables": classify_layers(rows, OBS_NAMES),
+        "classification_one_sided_gated": classify_layers(rows, gate["coherent_one_sided_observables"]),
+        "rows": rows,
+    }
+
+
+def build_sequences(args: argparse.Namespace, rng: np.random.Generator) -> dict[str, np.ndarray]:
+    sequences = {
+        "prime_gaps_first": prime_gap_sequence(args.n_gaps),
+        "logistic_return_intervals": logistic_return_intervals(args.n_gaps, rng),
+    }
+    if args.include_zeta:
+        sequences["zeta_zero_spacings_first"] = zeta_zero_spacings(args.zeta_gaps)
+    return sequences
+
+
+def compact(perimeters: dict) -> dict:
+    out = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        all_cls = data["classification_all_observables"]
+        gated_cls = data["classification_one_sided_gated"]
+        out[name] = {
+            "n_gaps": data["source"]["n_gaps"],
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+            "z_mean_coherent": gate["layers"]["0.000"]["z_mean"],
+            "z_mean_illusory": gate["layers"]["1.000"]["z_mean"],
+            "endpoint_distance_all": all_cls["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": gated_cls["endpoint_distance"],
+            "ambiguous_beta_one_sided_gated": gated_cls["ambiguous_beta"],
+        }
+    return out
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    sequences = build_sequences(args, root_rng)
+    perimeters = {}
+    for name, base in sequences.items():
+        perimeters[name] = analyze_sequence(name, base, args, root_rng)
+
+    output = {
+        "experiment": "semireal_order_denominator_gate",
+        "category": "gate_falsification_semireal",
+        "question": "Does ORDER_DENOMINATOR_GATE survive on non-synthetic / semi-real ordered sequences?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact(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 n one_sided stable0 stable1 dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>28s} "
+            f"{row['n_gaps']:>5d} "
+            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_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("--zeta-gaps", type=int, default=1024)
+    parser.add_argument("--include-zeta", action="store_true")
+    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=202605070923)
+    parser.add_argument("--out", default="tools/data/semireal_order_denominator_gate_20260507_0923.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

2026-05-07T09:26:48.888075Z ERROR codex_core::tools::router: error=write_stdin failed: stdin is closed for this session; rerun exec_command with tty=true to keep stdin open
diff --git a/tools/exp_semireal_order_denominator_gate.py b/tools/exp_semireal_order_denominator_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..f3da4e51c84f066066367b25fded3ebea3c1c00d
--- /dev/null
+++ b/tools/exp_semireal_order_denominator_gate.py
@@ -0,0 +1,352 @@
+#!/usr/bin/env python3
+"""
+exp_semireal_order_denominator_gate.py
+
+Falsification attempt for ORDER_DENOMINATOR_GATE on non-synthetic / semi-real
+ordered sequences. The coherent endpoint is the observed order of each sequence;
+the illusory endpoint is a marginal-preserving permutation. The same
+original-vs-shuffle denominator gate used in the prior reports is applied to
+canonical observables from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import math
+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 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 sieve_primes_for_count(n_primes: int) -> np.ndarray:
+    if n_primes < 6:
+        limit = 20
+    else:
+        limit = int(n_primes * (math.log(n_primes) + math.log(math.log(n_primes))) * 1.25)
+    while True:
+        sieve = np.ones(limit + 1, dtype=bool)
+        sieve[:2] = False
+        for p in range(2, int(limit**0.5) + 1):
+            if sieve[p]:
+                sieve[p * p : limit + 1 : p] = False
+        primes = np.flatnonzero(sieve)
+        if len(primes) >= n_primes:
+            return primes[:n_primes].astype(float)
+        limit *= 2
+
+
+def prime_gap_sequence(n_gaps: int) -> np.ndarray:
+    primes = sieve_primes_for_count(n_gaps + 1)
+    return normalize(np.diff(primes))
+
+
+def zeta_zero_spacings(n_gaps: int) -> np.ndarray:
+    try:
+        import mpmath as mp
+    except ImportError as exc:
+        raise RuntimeError("mpmath is required for zeta_zero_spacings") from exc
+
+    zeros = np.empty(n_gaps + 1, dtype=float)
+    for i in range(n_gaps + 1):
+        zeros[i] = float(mp.im(mp.zetazero(i + 1)))
+    return normalize(np.diff(zeros))
+
+
+def logistic_return_intervals(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    # Return intervals to a high-density-edge event in the fully chaotic logistic map.
+    threshold = 0.95
+    burn = 2000
+    needed = n_gaps + 1
+    returns: list[int] = []
+    last_hit: int | None = None
+    x = float(rng.random())
+    i = 0
+    max_steps = 50_000_000
+    while len(returns) < needed and i < max_steps:
+        x = 4.0 * x * (1.0 - x)
+        if i >= burn and x > threshold:
+            if last_hit is not None:
+                returns.append(i - last_hit)
+            last_hit = i
+        i += 1
+    if len(returns) < needed:
+        raise RuntimeError(f"logistic generator produced {len(returns)} intervals, need {needed}")
+    return normalize(np.array(returns[:n_gaps], dtype=float))
+
+
+def beta_replace(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        return base.copy()
+    if beta >= 1.0:
+        return illusory
+    out = base.copy()
+    mask = rng.random(len(base)) < beta
+    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], dict[str, float]]:
+    original = compute_canonical(gaps)
+    baseline = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline[name].append(obs[name])
+
+    means = {}
+    sds = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline[name], dtype=float)
+        means[name] = float(np.mean(vals))
+        sds[name] = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        z[name] = float((original[name] - means[name]) / sds[name]) if sds[name] > 1e-15 else 0.0
+    return original, means, sds, 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 = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent, illusory])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent, axis=0)
+    illusory_centroid = np.mean(illusory, 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()):
+        margins = []
+        labels = []
+        coords = []
+        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)),
+            "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_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()},
+        }
+
+    one_sided = []
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    for name in OBS_NAMES:
+        coherent_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in coherent_rows]))
+        illusory_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in illusory_rows]))
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            one_sided.append(name)
+        if coherent_freq >= 0.75 and illusory_freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "coherent_one_sided_observables": one_sided,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def analyze_sequence(name: str, base: np.ndarray, 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):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        for beta in betas:
+            gaps = beta_replace(base, beta, rep_rng)
+            obs, shuffle_mean, 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_mean": shuffle_mean,
+                    "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)
+    return {
+        "source": {
+            "n_gaps": int(len(base)),
+            "mean": float(np.mean(base)),
+            "variance": float(np.var(base)),
+        },
+        "gate": gate,
+        "classification_all_observables": classify_layers(rows, OBS_NAMES),
+        "classification_one_sided_gated": classify_layers(rows, gate["coherent_one_sided_observables"]),
+        "rows": rows,
+    }
+
+
+def build_sequences(args: argparse.Namespace, rng: np.random.Generator) -> dict[str, np.ndarray]:
+    sequences = {
+        "prime_gaps_first": prime_gap_sequence(args.n_gaps),
+        "logistic_return_intervals": logistic_return_intervals(args.n_gaps, rng),
+    }
+    if args.include_zeta:
+        sequences["zeta_zero_spacings_first"] = zeta_zero_spacings(args.zeta_gaps)
+    return sequences
+
+
+def compact(perimeters: dict) -> dict:
+    out = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        all_cls = data["classification_all_observables"]
+        gated_cls = data["classification_one_sided_gated"]
+        out[name] = {
+            "n_gaps": data["source"]["n_gaps"],
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+            "z_mean_coherent": gate["layers"]["0.000"]["z_mean"],
+            "z_mean_illusory": gate["layers"]["1.000"]["z_mean"],
+            "endpoint_distance_all": all_cls["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": gated_cls["endpoint_distance"],
+            "ambiguous_beta_one_sided_gated": gated_cls["ambiguous_beta"],
+        }
+    return out
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    sequences = build_sequences(args, root_rng)
+    perimeters = {}
+    for name, base in sequences.items():
+        perimeters[name] = analyze_sequence(name, base, args, root_rng)
+
+    output = {
+        "experiment": "semireal_order_denominator_gate",
+        "category": "gate_falsification_semireal",
+        "question": "Does ORDER_DENOMINATOR_GATE survive on non-synthetic / semi-real ordered sequences?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact(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 n one_sided stable0 stable1 dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>28s} "
+            f"{row['n_gaps']:>5d} "
+            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_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("--zeta-gaps", type=int, default=1024)
+    parser.add_argument("--include-zeta", action="store_true")
+    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=202605070923)
+    parser.add_argument("--out", default="tools/data/semireal_order_denominator_gate_20260507_0923.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

exec
/bin/bash -lc 'test -f tools/data/semireal_order_denominator_gate_20260507_0923.json && wc -c tools/data/semireal_order_denominator_gate_20260507_0923.json || true' in /opt/MM_D-ND
 succeeded in 0ms:
exec
/bin/bash -lc "ps -ef | rg 'exp_semireal_order_denominator_gate|zetazero'" in /opt/MM_D-ND
 succeeded in 0ms:
root           1       0  0 09:26 ?        00:00:00 /root/.codex-lab/tmp/arg0/codex-arg0PxDEiY/codex-linux-sandbox --sandbox-policy-cwd /opt/MM_D-ND --command-cwd /opt/MM_D-ND --sandbox-policy {"type":"workspace-write","writable_roots":["/root/.codex-lab/memories"],"network_access":false,"exclude_tmpdir_env_var":false,"exclude_slash_tmp":false} --file-system-sandbox-policy {"kind":"restricted","entries":[{"path":{"type":"special","value":{"kind":"root"}},"access":"read"},{"path":{"type":"special","value":{"kind":"current_working_directory"}},"access":"write"},{"path":{"type":"special","value":{"kind":"slash_tmp"}},"access":"write"},{"path":{"type":"special","value":{"kind":"tmpdir"}},"access":"write"},{"path":{"type":"path","path":"/root/.codex-lab/memories"},"access":"write"},{"path":{"type":"path","path":"/opt/MM_D-ND/.git"},"access":"read"},{"path":{"type":"path","path":"/opt/MM_D-ND/.codex"},"access":"read"}]} --network-sandbox-policy "restricted" -- /bin/bash -c __CODEX_SNAPSHOT_OVERRIDE_SET_0="${CODEX_THREAD_ID+x}" __CODEX_SNAPSHOT_OVERRIDE_0="${CODEX_THREAD_ID-}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_0="${ALL_PROXY+x}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_0="${ALL_PROXY-}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_1="${BUNDLE_HTTPS_PROXY+x}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_1="${BUNDLE_HTTPS_PROXY-}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_2="${BUNDLE_HTTP_PROXY+x}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_2="${BUNDLE_HTTP_PROXY-}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_3="${BUNDLE_NO_PROXY+x}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_3="${BUNDLE_NO_PROXY-}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_4="${CODEX_NETWORK_ALLOW_LOCAL_BINDING+x}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_4="${CODEX_NETWORK_ALLOW_LOCAL_BINDING-}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_5="${CODEX_NETWORK_PROXY_ACTIVE+x}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_5="${CODEX_NETWORK_PROXY_ACTIVE-}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_6="${DOCKER_HTTPS_PROXY+x}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_6="${DOCKER_HTTPS_PROXY-}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_7="${DOCKER_HTTP_PROXY+x}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_7="${DOCKER_HTTP_PROXY-}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_8="${ELECTRON_GET_USE_PROXY+x}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_8="${ELECTRON_GET_USE_PROXY-}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_9="${FTP_PROXY+x}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_9="${FTP_PROXY-}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_10="${HTTPS_PROXY+x}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_10="${HTTPS_PROXY-}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_11="${HTTP_PROXY+x}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_11="${HTTP_PROXY-}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_12="${NO_PROXY+x}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_12="${NO_PROXY-}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_13="${NPM_CONFIG_HTTPS_PROXY+x}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_13="${NPM_CONFIG_HTTPS_PROXY-}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_14="${NPM_CONFIG_HTTP_PROXY+x}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_14="${NPM_CONFIG_HTTP_PROXY-}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_15="${NPM_CONFIG_NOPROXY+x}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_15="${NPM_CONFIG_NOPROXY-}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_16="${NPM_CONFIG_PROXY+x}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_16="${NPM_CONFIG_PROXY-}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_17="${PIP_PROXY+x}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_17="${PIP_PROXY-}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_18="${WSS_PROXY+x}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_18="${WSS_PROXY-}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_19="${WS_PROXY+x}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_19="${WS_PROXY-}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_20="${YARN_HTTPS_PROXY+x}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_20="${YARN_HTTPS_PROXY-}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_21="${YARN_HTTP_PROXY+x}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_21="${YARN_HTTP_PROXY-}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_22="${YARN_NO_PROXY+x}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_22="${YARN_NO_PROXY-}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_23="${all_proxy+x}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_23="${all_proxy-}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_24="${ftp_proxy+x}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_24="${ftp_proxy-}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_25="${http_proxy+x}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_25="${http_proxy-}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_26="${https_proxy+x}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_26="${https_proxy-}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_27="${no_proxy+x}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_27="${no_proxy-}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_28="${npm_config_http_proxy+x}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_28="${npm_config_http_proxy-}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_29="${npm_config_https_proxy+x}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_29="${npm_config_https_proxy-}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_30="${npm_config_noproxy+x}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_30="${npm_config_noproxy-}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_31="${npm_config_proxy+x}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_31="${npm_config_proxy-}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_32="${ws_proxy+x}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_32="${ws_proxy-}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_33="${wss_proxy+x}" __CODEX_SNAPSHOT_PROXY_OVERRIDE_33="${wss_proxy-}" __CODEX_SNAPSHOT_PROXY_ENV_SET="${CODEX_NETWORK_PROXY_ACTIVE+x}"  if . '/root/.codex-lab/shell_snapshots/019e01c0-05e2-7b82-b3dc-482b9c38d8ca.1778145822191130692.sh' >/dev/null 2>&1; then :; fi  if [ -n "${__CODEX_SNAPSHOT_OVERRIDE_SET_0}" ]; then export CODEX_THREAD_ID="${__CODEX_SNAPSHOT_OVERRIDE_0}"; else unset CODEX_THREAD_ID; fi if [ -n "$__CODEX_SNAPSHOT_PROXY_ENV_SET" ] || [ -n "${CODEX_NETWORK_PROXY_ACTIVE+x}" ]; then if [ -n "${__CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_0}" ]; then export ALL_PROXY="${__CODEX_SNAPSHOT_PROXY_OVERRIDE_0}"; else unset ALL_PROXY; fi if [ -n "${__CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_1}" ]; then export BUNDLE_HTTPS_PROXY="${__CODEX_SNAPSHOT_PROXY_OVERRIDE_1}"; else unset BUNDLE_HTTPS_PROXY; fi if [ -n "${__CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_2}" ]; then export BUNDLE_HTTP_PROXY="${__CODEX_SNAPSHOT_PROXY_OVERRIDE_2}"; else unset BUNDLE_HTTP_PROXY; fi if [ -n "${__CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_3}" ]; then export BUNDLE_NO_PROXY="${__CODEX_SNAPSHOT_PROXY_OVERRIDE_3}"; else unset BUNDLE_NO_PROXY; fi if [ -n "${__CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_4}" ]; then export CODEX_NETWORK_ALLOW_LOCAL_BINDING="${__CODEX_SNAPSHOT_PROXY_OVERRIDE_4}"; else unset CODEX_NETWORK_ALLOW_LOCAL_BINDING; fi if [ -n "${__CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_5}" ]; then export CODEX_NETWORK_PROXY_ACTIVE="${__CODEX_SNAPSHOT_PROXY_OVERRIDE_5}"; else unset CODEX_NETWORK_PROXY_ACTIVE; fi if [ -n "${__CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_6}" ]; then export DOCKER_HTTPS_PROXY="${__CODEX_SNAPSHOT_PROXY_OVERRIDE_6}"; else unset DOCKER_HTTPS_PROXY; fi if [ -n "${__CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_7}" ]; then export DOCKER_HTTP_PROXY="${__CODEX_SNAPSHOT_PROXY_OVERRIDE_7}"; else unset DOCKER_HTTP_PROXY; fi if [ -n "${__CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_8}" ]; then export ELECTRON_GET_USE_PROXY="${__CODEX_SNAPSHOT_PROXY_OVERRIDE_8}"; else unset ELECTRON_GET_USE_PROXY; fi if [ -n "${__CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_9}" ]; then export FTP_PROXY="${__CODEX_SNAPSHOT_PROXY_OVERRIDE_9}"; else unset FTP_PROXY; fi if [ -n "${__CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_10}" ]; then export HTTPS_PROXY="${__CODEX_SNAPSHOT_PROXY_OVERRIDE_10}"; else unset HTTPS_PROXY; fi if [ -n "${__CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_11}" ]; then export HTTP_PROXY="${__CODEX_SNAPSHOT_PROXY_OVERRIDE_11}"; else unset HTTP_PROXY; fi if [ -n "${__CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_12}" ]; then export NO_PROXY="${__CODEX_SNAPSHOT_PROXY_OVERRIDE_12}"; else unset NO_PROXY; fi if [ -n "${__CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_13}" ]; then export NPM_CONFIG_HTTPS_PROXY="${__CODEX_SNAPSHOT_PROXY_OVERRIDE_13}"; else unset NPM_CONFIG_HTTPS_PROXY; fi if [ -n "${__CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_14}" ]; then export NPM_CONFIG_HTTP_PROXY="${__CODEX_SNAPSHOT_PROXY_OVERRIDE_14}"; else unset NPM_CONFIG_HTTP_PROXY; fi if [ -n "${__CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_15}" ]; then export NPM_CONFIG_NOPROXY="${__CODEX_SNAPSHOT_PROXY_OVERRIDE_15}"; else unset NPM_CONFIG_NOPROXY; fi if [ -n "${__CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_16}" ]; then export NPM_CONFIG_PROXY="${__CODEX_SNAPSHOT_PROXY_OVERRIDE_16}"; else unset NPM_CONFIG_PROXY; fi if [ -n "${__CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_17}" ]; then export PIP_PROXY="${__CODEX_SNAPSHOT_PROXY_OVERRIDE_17}"; else unset PIP_PROXY; fi if [ -n "${__CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_18}" ]; then export WSS_PROXY="${__CODEX_SNAPSHOT_PROXY_OVERRIDE_18}"; else unset WSS_PROXY; fi if [ -n "${__CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_19}" ]; then export WS_PROXY="${__CODEX_SNAPSHOT_PROXY_OVERRIDE_19}"; else unset WS_PROXY; fi if [ -n "${__CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_20}" ]; then export YARN_HTTPS_PROXY="${__CODEX_SNAPSHOT_PROXY_OVERRIDE_20}"; else unset YARN_HTTPS_PROXY; fi if [ -n "${__CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_21}" ]; then export YARN_HTTP_PROXY="${__CODEX_SNAPSHOT_PROXY_OVERRIDE_21}"; else unset YARN_HTTP_PROXY; fi if [ -n "${__CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_22}" ]; then export YARN_NO_PROXY="${__CODEX_SNAPSHOT_PROXY_OVERRIDE_22}"; else unset YARN_NO_PROXY; fi if [ -n "${__CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_23}" ]; then export all_proxy="${__CODEX_SNAPSHOT_PROXY_OVERRIDE_23}"; else unset all_proxy; fi if [ -n "${__CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_24}" ]; then export ftp_proxy="${__CODEX_SNAPSHOT_PROXY_OVERRIDE_24}"; else unset ftp_proxy; fi if [ -n "${__CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_25}" ]; then export http_proxy="${__CODEX_SNAPSHOT_PROXY_OVERRIDE_25}"; else unset http_proxy; fi if [ -n "${__CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_26}" ]; then export https_proxy="${__CODEX_SNAPSHOT_PROXY_OVERRIDE_26}"; else unset https_proxy; fi if [ -n "${__CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_27}" ]; then export no_proxy="${__CODEX_SNAPSHOT_PROXY_OVERRIDE_27}"; else unset no_proxy; fi if [ -n "${__CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_28}" ]; then export npm_config_http_proxy="${__CODEX_SNAPSHOT_PROXY_OVERRIDE_28}"; else unset npm_config_http_proxy; fi if [ -n "${__CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_29}" ]; then export npm_config_https_proxy="${__CODEX_SNAPSHOT_PROXY_OVERRIDE_29}"; else unset npm_config_https_proxy; fi if [ -n "${__CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_30}" ]; then export npm_config_noproxy="${__CODEX_SNAPSHOT_PROXY_OVERRIDE_30}"; else unset npm_config_noproxy; fi if [ -n "${__CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_31}" ]; then export npm_config_proxy="${__CODEX_SNAPSHOT_PROXY_OVERRIDE_31}"; else unset npm_config_proxy; fi if [ -n "${__CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_32}" ]; then export ws_proxy="${__CODEX_SNAPSHOT_PROXY_OVERRIDE_32}"; else unset ws_proxy; fi if [ -n "${__CODEX_SNAPSHOT_PROXY_OVERRIDE_SET_33}" ]; then export wss_proxy="${__CODEX_SNAPSHOT_PROXY_OVERRIDE_33}"; else unset wss_proxy; fi fi  exec '/bin/bash' -c 'ps -ef | rg '"'"'exp_semireal_order_denominator_gate|zetazero'"'"''
root           2       1  0 09:26 ?        00:00:00 /bin/bash -c ps -ef | rg 'exp_semireal_order_denominator_gate|zetazero'
root           4       2  0 09:26 ?        00:00:00 rg exp_semireal_order_denominator_gate|zetazero

diff --git a/tools/exp_semireal_order_denominator_gate.py b/tools/exp_semireal_order_denominator_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..f3da4e51c84f066066367b25fded3ebea3c1c00d
--- /dev/null
+++ b/tools/exp_semireal_order_denominator_gate.py
@@ -0,0 +1,352 @@
+#!/usr/bin/env python3
+"""
+exp_semireal_order_denominator_gate.py
+
+Falsification attempt for ORDER_DENOMINATOR_GATE on non-synthetic / semi-real
+ordered sequences. The coherent endpoint is the observed order of each sequence;
+the illusory endpoint is a marginal-preserving permutation. The same
+original-vs-shuffle denominator gate used in the prior reports is applied to
+canonical observables from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import math
+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 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 sieve_primes_for_count(n_primes: int) -> np.ndarray:
+    if n_primes < 6:
+        limit = 20
+    else:
+        limit = int(n_primes * (math.log(n_primes) + math.log(math.log(n_primes))) * 1.25)
+    while True:
+        sieve = np.ones(limit + 1, dtype=bool)
+        sieve[:2] = False
+        for p in range(2, int(limit**0.5) + 1):
+            if sieve[p]:
+                sieve[p * p : limit + 1 : p] = False
+        primes = np.flatnonzero(sieve)
+        if len(primes) >= n_primes:
+            return primes[:n_primes].astype(float)
+        limit *= 2
+
+
+def prime_gap_sequence(n_gaps: int) -> np.ndarray:
+    primes = sieve_primes_for_count(n_gaps + 1)
+    return normalize(np.diff(primes))
+
+
+def zeta_zero_spacings(n_gaps: int) -> np.ndarray:
+    try:
+        import mpmath as mp
+    except ImportError as exc:
+        raise RuntimeError("mpmath is required for zeta_zero_spacings") from exc
+
+    zeros = np.empty(n_gaps + 1, dtype=float)
+    for i in range(n_gaps + 1):
+        zeros[i] = float(mp.im(mp.zetazero(i + 1)))
+    return normalize(np.diff(zeros))
+
+
+def logistic_return_intervals(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    # Return intervals to a high-density-edge event in the fully chaotic logistic map.
+    threshold = 0.95
+    burn = 2000
+    needed = n_gaps + 1
+    returns: list[int] = []
+    last_hit: int | None = None
+    x = float(rng.random())
+    i = 0
+    max_steps = 50_000_000
+    while len(returns) < needed and i < max_steps:
+        x = 4.0 * x * (1.0 - x)
+        if i >= burn and x > threshold:
+            if last_hit is not None:
+                returns.append(i - last_hit)
+            last_hit = i
+        i += 1
+    if len(returns) < needed:
+        raise RuntimeError(f"logistic generator produced {len(returns)} intervals, need {needed}")
+    return normalize(np.array(returns[:n_gaps], dtype=float))
+
+
+def beta_replace(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        return base.copy()
+    if beta >= 1.0:
+        return illusory
+    out = base.copy()
+    mask = rng.random(len(base)) < beta
+    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], dict[str, float]]:
+    original = compute_canonical(gaps)
+    baseline = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline[name].append(obs[name])
+
+    means = {}
+    sds = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline[name], dtype=float)
+        means[name] = float(np.mean(vals))
+        sds[name] = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        z[name] = float((original[name] - means[name]) / sds[name]) if sds[name] > 1e-15 else 0.0
+    return original, means, sds, 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 = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent, illusory])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent, axis=0)
+    illusory_centroid = np.mean(illusory, 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()):
+        margins = []
+        labels = []
+        coords = []
+        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)),
+            "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_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()},
+        }
+
+    one_sided = []
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    for name in OBS_NAMES:
+        coherent_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in coherent_rows]))
+        illusory_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in illusory_rows]))
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            one_sided.append(name)
+        if coherent_freq >= 0.75 and illusory_freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "coherent_one_sided_observables": one_sided,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def analyze_sequence(name: str, base: np.ndarray, 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):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        for beta in betas:
+            gaps = beta_replace(base, beta, rep_rng)
+            obs, shuffle_mean, 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_mean": shuffle_mean,
+                    "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)
+    return {
+        "source": {
+            "n_gaps": int(len(base)),
+            "mean": float(np.mean(base)),
+            "variance": float(np.var(base)),
+        },
+        "gate": gate,
+        "classification_all_observables": classify_layers(rows, OBS_NAMES),
+        "classification_one_sided_gated": classify_layers(rows, gate["coherent_one_sided_observables"]),
+        "rows": rows,
+    }
+
+
+def build_sequences(args: argparse.Namespace, rng: np.random.Generator) -> dict[str, np.ndarray]:
+    sequences = {
+        "prime_gaps_first": prime_gap_sequence(args.n_gaps),
+        "logistic_return_intervals": logistic_return_intervals(args.n_gaps, rng),
+    }
+    if args.include_zeta:
+        sequences["zeta_zero_spacings_first"] = zeta_zero_spacings(args.zeta_gaps)
+    return sequences
+
+
+def compact(perimeters: dict) -> dict:
+    out = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        all_cls = data["classification_all_observables"]
+        gated_cls = data["classification_one_sided_gated"]
+        out[name] = {
+            "n_gaps": data["source"]["n_gaps"],
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+            "z_mean_coherent": gate["layers"]["0.000"]["z_mean"],
+            "z_mean_illusory": gate["layers"]["1.000"]["z_mean"],
+            "endpoint_distance_all": all_cls["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": gated_cls["endpoint_distance"],
+            "ambiguous_beta_one_sided_gated": gated_cls["ambiguous_beta"],
+        }
+    return out
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    sequences = build_sequences(args, root_rng)
+    perimeters = {}
+    for name, base in sequences.items():
+        perimeters[name] = analyze_sequence(name, base, args, root_rng)
+
+    output = {
+        "experiment": "semireal_order_denominator_gate",
+        "category": "gate_falsification_semireal",
+        "question": "Does ORDER_DENOMINATOR_GATE survive on non-synthetic / semi-real ordered sequences?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact(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 n one_sided stable0 stable1 dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>28s} "
+            f"{row['n_gaps']:>5d} "
+            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_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("--zeta-gaps", type=int, default=1024)
+    parser.add_argument("--include-zeta", action="store_true")
+    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=202605070923)
+    parser.add_argument("--out", default="tools/data/semireal_order_denominator_gate_20260507_0923.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

diff --git a/tools/exp_semireal_order_denominator_gate.py b/tools/exp_semireal_order_denominator_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..f3da4e51c84f066066367b25fded3ebea3c1c00d
--- /dev/null
+++ b/tools/exp_semireal_order_denominator_gate.py
@@ -0,0 +1,352 @@
+#!/usr/bin/env python3
+"""
+exp_semireal_order_denominator_gate.py
+
+Falsification attempt for ORDER_DENOMINATOR_GATE on non-synthetic / semi-real
+ordered sequences. The coherent endpoint is the observed order of each sequence;
+the illusory endpoint is a marginal-preserving permutation. The same
+original-vs-shuffle denominator gate used in the prior reports is applied to
+canonical observables from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import math
+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 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 sieve_primes_for_count(n_primes: int) -> np.ndarray:
+    if n_primes < 6:
+        limit = 20
+    else:
+        limit = int(n_primes * (math.log(n_primes) + math.log(math.log(n_primes))) * 1.25)
+    while True:
+        sieve = np.ones(limit + 1, dtype=bool)
+        sieve[:2] = False
+        for p in range(2, int(limit**0.5) + 1):
+            if sieve[p]:
+                sieve[p * p : limit + 1 : p] = False
+        primes = np.flatnonzero(sieve)
+        if len(primes) >= n_primes:
+            return primes[:n_primes].astype(float)
+        limit *= 2
+
+
+def prime_gap_sequence(n_gaps: int) -> np.ndarray:
+    primes = sieve_primes_for_count(n_gaps + 1)
+    return normalize(np.diff(primes))
+
+
+def zeta_zero_spacings(n_gaps: int) -> np.ndarray:
+    try:
+        import mpmath as mp
+    except ImportError as exc:
+        raise RuntimeError("mpmath is required for zeta_zero_spacings") from exc
+
+    zeros = np.empty(n_gaps + 1, dtype=float)
+    for i in range(n_gaps + 1):
+        zeros[i] = float(mp.im(mp.zetazero(i + 1)))
+    return normalize(np.diff(zeros))
+
+
+def logistic_return_intervals(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    # Return intervals to a high-density-edge event in the fully chaotic logistic map.
+    threshold = 0.95
+    burn = 2000
+    needed = n_gaps + 1
+    returns: list[int] = []
+    last_hit: int | None = None
+    x = float(rng.random())
+    i = 0
+    max_steps = 50_000_000
+    while len(returns) < needed and i < max_steps:
+        x = 4.0 * x * (1.0 - x)
+        if i >= burn and x > threshold:
+            if last_hit is not None:
+                returns.append(i - last_hit)
+            last_hit = i
+        i += 1
+    if len(returns) < needed:
+        raise RuntimeError(f"logistic generator produced {len(returns)} intervals, need {needed}")
+    return normalize(np.array(returns[:n_gaps], dtype=float))
+
+
+def beta_replace(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        return base.copy()
+    if beta >= 1.0:
+        return illusory
+    out = base.copy()
+    mask = rng.random(len(base)) < beta
+    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], dict[str, float]]:
+    original = compute_canonical(gaps)
+    baseline = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline[name].append(obs[name])
+
+    means = {}
+    sds = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline[name], dtype=float)
+        means[name] = float(np.mean(vals))
+        sds[name] = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        z[name] = float((original[name] - means[name]) / sds[name]) if sds[name] > 1e-15 else 0.0
+    return original, means, sds, 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 = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent, illusory])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent, axis=0)
+    illusory_centroid = np.mean(illusory, 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()):
+        margins = []
+        labels = []
+        coords = []
+        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)),
+            "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_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()},
+        }
+
+    one_sided = []
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    for name in OBS_NAMES:
+        coherent_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in coherent_rows]))
+        illusory_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in illusory_rows]))
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            one_sided.append(name)
+        if coherent_freq >= 0.75 and illusory_freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "coherent_one_sided_observables": one_sided,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def analyze_sequence(name: str, base: np.ndarray, 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):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        for beta in betas:
+            gaps = beta_replace(base, beta, rep_rng)
+            obs, shuffle_mean, 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_mean": shuffle_mean,
+                    "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)
+    return {
+        "source": {
+            "n_gaps": int(len(base)),
+            "mean": float(np.mean(base)),
+            "variance": float(np.var(base)),
+        },
+        "gate": gate,
+        "classification_all_observables": classify_layers(rows, OBS_NAMES),
+        "classification_one_sided_gated": classify_layers(rows, gate["coherent_one_sided_observables"]),
+        "rows": rows,
+    }
+
+
+def build_sequences(args: argparse.Namespace, rng: np.random.Generator) -> dict[str, np.ndarray]:
+    sequences = {
+        "prime_gaps_first": prime_gap_sequence(args.n_gaps),
+        "logistic_return_intervals": logistic_return_intervals(args.n_gaps, rng),
+    }
+    if args.include_zeta:
+        sequences["zeta_zero_spacings_first"] = zeta_zero_spacings(args.zeta_gaps)
+    return sequences
+
+
+def compact(perimeters: dict) -> dict:
+    out = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        all_cls = data["classification_all_observables"]
+        gated_cls = data["classification_one_sided_gated"]
+        out[name] = {
+            "n_gaps": data["source"]["n_gaps"],
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+            "z_mean_coherent": gate["layers"]["0.000"]["z_mean"],
+            "z_mean_illusory": gate["layers"]["1.000"]["z_mean"],
+            "endpoint_distance_all": all_cls["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": gated_cls["endpoint_distance"],
+            "ambiguous_beta_one_sided_gated": gated_cls["ambiguous_beta"],
+        }
+    return out
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    sequences = build_sequences(args, root_rng)
+    perimeters = {}
+    for name, base in sequences.items():
+        perimeters[name] = analyze_sequence(name, base, args, root_rng)
+
+    output = {
+        "experiment": "semireal_order_denominator_gate",
+        "category": "gate_falsification_semireal",
+        "question": "Does ORDER_DENOMINATOR_GATE survive on non-synthetic / semi-real ordered sequences?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact(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 n one_sided stable0 stable1 dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>28s} "
+            f"{row['n_gaps']:>5d} "
+            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_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("--zeta-gaps", type=int, default=1024)
+    parser.add_argument("--include-zeta", action="store_true")
+    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=202605070923)
+    parser.add_argument("--out", default="tools/data/semireal_order_denominator_gate_20260507_0923.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

diff --git a/tools/exp_semireal_order_denominator_gate.py b/tools/exp_semireal_order_denominator_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..f3da4e51c84f066066367b25fded3ebea3c1c00d
--- /dev/null
+++ b/tools/exp_semireal_order_denominator_gate.py
@@ -0,0 +1,352 @@
+#!/usr/bin/env python3
+"""
+exp_semireal_order_denominator_gate.py
+
+Falsification attempt for ORDER_DENOMINATOR_GATE on non-synthetic / semi-real
+ordered sequences. The coherent endpoint is the observed order of each sequence;
+the illusory endpoint is a marginal-preserving permutation. The same
+original-vs-shuffle denominator gate used in the prior reports is applied to
+canonical observables from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import math
+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 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 sieve_primes_for_count(n_primes: int) -> np.ndarray:
+    if n_primes < 6:
+        limit = 20
+    else:
+        limit = int(n_primes * (math.log(n_primes) + math.log(math.log(n_primes))) * 1.25)
+    while True:
+        sieve = np.ones(limit + 1, dtype=bool)
+        sieve[:2] = False
+        for p in range(2, int(limit**0.5) + 1):
+            if sieve[p]:
+                sieve[p * p : limit + 1 : p] = False
+        primes = np.flatnonzero(sieve)
+        if len(primes) >= n_primes:
+            return primes[:n_primes].astype(float)
+        limit *= 2
+
+
+def prime_gap_sequence(n_gaps: int) -> np.ndarray:
+    primes = sieve_primes_for_count(n_gaps + 1)
+    return normalize(np.diff(primes))
+
+
+def zeta_zero_spacings(n_gaps: int) -> np.ndarray:
+    try:
+        import mpmath as mp
+    except ImportError as exc:
+        raise RuntimeError("mpmath is required for zeta_zero_spacings") from exc
+
+    zeros = np.empty(n_gaps + 1, dtype=float)
+    for i in range(n_gaps + 1):
+        zeros[i] = float(mp.im(mp.zetazero(i + 1)))
+    return normalize(np.diff(zeros))
+
+
+def logistic_return_intervals(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    # Return intervals to a high-density-edge event in the fully chaotic logistic map.
+    threshold = 0.95
+    burn = 2000
+    needed = n_gaps + 1
+    returns: list[int] = []
+    last_hit: int | None = None
+    x = float(rng.random())
+    i = 0
+    max_steps = 50_000_000
+    while len(returns) < needed and i < max_steps:
+        x = 4.0 * x * (1.0 - x)
+        if i >= burn and x > threshold:
+            if last_hit is not None:
+                returns.append(i - last_hit)
+            last_hit = i
+        i += 1
+    if len(returns) < needed:
+        raise RuntimeError(f"logistic generator produced {len(returns)} intervals, need {needed}")
+    return normalize(np.array(returns[:n_gaps], dtype=float))
+
+
+def beta_replace(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        return base.copy()
+    if beta >= 1.0:
+        return illusory
+    out = base.copy()
+    mask = rng.random(len(base)) < beta
+    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], dict[str, float]]:
+    original = compute_canonical(gaps)
+    baseline = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline[name].append(obs[name])
+
+    means = {}
+    sds = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline[name], dtype=float)
+        means[name] = float(np.mean(vals))
+        sds[name] = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        z[name] = float((original[name] - means[name]) / sds[name]) if sds[name] > 1e-15 else 0.0
+    return original, means, sds, 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 = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent, illusory])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent, axis=0)
+    illusory_centroid = np.mean(illusory, 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()):
+        margins = []
+        labels = []
+        coords = []
+        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)),
+            "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_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()},
+        }
+
+    one_sided = []
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    for name in OBS_NAMES:
+        coherent_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in coherent_rows]))
+        illusory_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in illusory_rows]))
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            one_sided.append(name)
+        if coherent_freq >= 0.75 and illusory_freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "coherent_one_sided_observables": one_sided,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def analyze_sequence(name: str, base: np.ndarray, 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):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        for beta in betas:
+            gaps = beta_replace(base, beta, rep_rng)
+            obs, shuffle_mean, 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_mean": shuffle_mean,
+                    "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)
+    return {
+        "source": {
+            "n_gaps": int(len(base)),
+            "mean": float(np.mean(base)),
+            "variance": float(np.var(base)),
+        },
+        "gate": gate,
+        "classification_all_observables": classify_layers(rows, OBS_NAMES),
+        "classification_one_sided_gated": classify_layers(rows, gate["coherent_one_sided_observables"]),
+        "rows": rows,
+    }
+
+
+def build_sequences(args: argparse.Namespace, rng: np.random.Generator) -> dict[str, np.ndarray]:
+    sequences = {
+        "prime_gaps_first": prime_gap_sequence(args.n_gaps),
+        "logistic_return_intervals": logistic_return_intervals(args.n_gaps, rng),
+    }
+    if args.include_zeta:
+        sequences["zeta_zero_spacings_first"] = zeta_zero_spacings(args.zeta_gaps)
+    return sequences
+
+
+def compact(perimeters: dict) -> dict:
+    out = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        all_cls = data["classification_all_observables"]
+        gated_cls = data["classification_one_sided_gated"]
+        out[name] = {
+            "n_gaps": data["source"]["n_gaps"],
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+            "z_mean_coherent": gate["layers"]["0.000"]["z_mean"],
+            "z_mean_illusory": gate["layers"]["1.000"]["z_mean"],
+            "endpoint_distance_all": all_cls["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": gated_cls["endpoint_distance"],
+            "ambiguous_beta_one_sided_gated": gated_cls["ambiguous_beta"],
+        }
+    return out
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    sequences = build_sequences(args, root_rng)
+    perimeters = {}
+    for name, base in sequences.items():
+        perimeters[name] = analyze_sequence(name, base, args, root_rng)
+
+    output = {
+        "experiment": "semireal_order_denominator_gate",
+        "category": "gate_falsification_semireal",
+        "question": "Does ORDER_DENOMINATOR_GATE survive on non-synthetic / semi-real ordered sequences?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact(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 n one_sided stable0 stable1 dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>28s} "
+            f"{row['n_gaps']:>5d} "
+            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_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("--zeta-gaps", type=int, default=1024)
+    parser.add_argument("--include-zeta", action="store_true")
+    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=202605070923)
+    parser.add_argument("--out", default="tools/data/semireal_order_denominator_gate_20260507_0923.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

diff --git a/tools/exp_semireal_order_denominator_gate.py b/tools/exp_semireal_order_denominator_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..f3da4e51c84f066066367b25fded3ebea3c1c00d
--- /dev/null
+++ b/tools/exp_semireal_order_denominator_gate.py
@@ -0,0 +1,352 @@
+#!/usr/bin/env python3
+"""
+exp_semireal_order_denominator_gate.py
+
+Falsification attempt for ORDER_DENOMINATOR_GATE on non-synthetic / semi-real
+ordered sequences. The coherent endpoint is the observed order of each sequence;
+the illusory endpoint is a marginal-preserving permutation. The same
+original-vs-shuffle denominator gate used in the prior reports is applied to
+canonical observables from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import math
+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 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 sieve_primes_for_count(n_primes: int) -> np.ndarray:
+    if n_primes < 6:
+        limit = 20
+    else:
+        limit = int(n_primes * (math.log(n_primes) + math.log(math.log(n_primes))) * 1.25)
+    while True:
+        sieve = np.ones(limit + 1, dtype=bool)
+        sieve[:2] = False
+        for p in range(2, int(limit**0.5) + 1):
+            if sieve[p]:
+                sieve[p * p : limit + 1 : p] = False
+        primes = np.flatnonzero(sieve)
+        if len(primes) >= n_primes:
+            return primes[:n_primes].astype(float)
+        limit *= 2
+
+
+def prime_gap_sequence(n_gaps: int) -> np.ndarray:
+    primes = sieve_primes_for_count(n_gaps + 1)
+    return normalize(np.diff(primes))
+
+
+def zeta_zero_spacings(n_gaps: int) -> np.ndarray:
+    try:
+        import mpmath as mp
+    except ImportError as exc:
+        raise RuntimeError("mpmath is required for zeta_zero_spacings") from exc
+
+    zeros = np.empty(n_gaps + 1, dtype=float)
+    for i in range(n_gaps + 1):
+        zeros[i] = float(mp.im(mp.zetazero(i + 1)))
+    return normalize(np.diff(zeros))
+
+
+def logistic_return_intervals(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    # Return intervals to a high-density-edge event in the fully chaotic logistic map.
+    threshold = 0.95
+    burn = 2000
+    needed = n_gaps + 1
+    returns: list[int] = []
+    last_hit: int | None = None
+    x = float(rng.random())
+    i = 0
+    max_steps = 50_000_000
+    while len(returns) < needed and i < max_steps:
+        x = 4.0 * x * (1.0 - x)
+        if i >= burn and x > threshold:
+            if last_hit is not None:
+                returns.append(i - last_hit)
+            last_hit = i
+        i += 1
+    if len(returns) < needed:
+        raise RuntimeError(f"logistic generator produced {len(returns)} intervals, need {needed}")
+    return normalize(np.array(returns[:n_gaps], dtype=float))
+
+
+def beta_replace(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        return base.copy()
+    if beta >= 1.0:
+        return illusory
+    out = base.copy()
+    mask = rng.random(len(base)) < beta
+    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], dict[str, float]]:
+    original = compute_canonical(gaps)
+    baseline = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline[name].append(obs[name])
+
+    means = {}
+    sds = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline[name], dtype=float)
+        means[name] = float(np.mean(vals))
+        sds[name] = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        z[name] = float((original[name] - means[name]) / sds[name]) if sds[name] > 1e-15 else 0.0
+    return original, means, sds, 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 = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent, illusory])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent, axis=0)
+    illusory_centroid = np.mean(illusory, 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()):
+        margins = []
+        labels = []
+        coords = []
+        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)),
+            "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_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()},
+        }
+
+    one_sided = []
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    for name in OBS_NAMES:
+        coherent_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in coherent_rows]))
+        illusory_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in illusory_rows]))
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            one_sided.append(name)
+        if coherent_freq >= 0.75 and illusory_freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "coherent_one_sided_observables": one_sided,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def analyze_sequence(name: str, base: np.ndarray, 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):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        for beta in betas:
+            gaps = beta_replace(base, beta, rep_rng)
+            obs, shuffle_mean, 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_mean": shuffle_mean,
+                    "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)
+    return {
+        "source": {
+            "n_gaps": int(len(base)),
+            "mean": float(np.mean(base)),
+            "variance": float(np.var(base)),
+        },
+        "gate": gate,
+        "classification_all_observables": classify_layers(rows, OBS_NAMES),
+        "classification_one_sided_gated": classify_layers(rows, gate["coherent_one_sided_observables"]),
+        "rows": rows,
+    }
+
+
+def build_sequences(args: argparse.Namespace, rng: np.random.Generator) -> dict[str, np.ndarray]:
+    sequences = {
+        "prime_gaps_first": prime_gap_sequence(args.n_gaps),
+        "logistic_return_intervals": logistic_return_intervals(args.n_gaps, rng),
+    }
+    if args.include_zeta:
+        sequences["zeta_zero_spacings_first"] = zeta_zero_spacings(args.zeta_gaps)
+    return sequences
+
+
+def compact(perimeters: dict) -> dict:
+    out = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        all_cls = data["classification_all_observables"]
+        gated_cls = data["classification_one_sided_gated"]
+        out[name] = {
+            "n_gaps": data["source"]["n_gaps"],
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+            "z_mean_coherent": gate["layers"]["0.000"]["z_mean"],
+            "z_mean_illusory": gate["layers"]["1.000"]["z_mean"],
+            "endpoint_distance_all": all_cls["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": gated_cls["endpoint_distance"],
+            "ambiguous_beta_one_sided_gated": gated_cls["ambiguous_beta"],
+        }
+    return out
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    sequences = build_sequences(args, root_rng)
+    perimeters = {}
+    for name, base in sequences.items():
+        perimeters[name] = analyze_sequence(name, base, args, root_rng)
+
+    output = {
+        "experiment": "semireal_order_denominator_gate",
+        "category": "gate_falsification_semireal",
+        "question": "Does ORDER_DENOMINATOR_GATE survive on non-synthetic / semi-real ordered sequences?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact(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 n one_sided stable0 stable1 dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>28s} "
+            f"{row['n_gaps']:>5d} "
+            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_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("--zeta-gaps", type=int, default=1024)
+    parser.add_argument("--include-zeta", action="store_true")
+    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=202605070923)
+    parser.add_argument("--out", default="tools/data/semireal_order_denominator_gate_20260507_0923.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

 succeeded in 75159ms:
observables_registry=1.0.0-2026-05-06
observables_used=['SR', 'SR2', 'L1', 'L2', 'triple_var']
perimeter n one_sided stable0 stable1 dist_gate ambiguous_gate
            prime_gaps_first  4096       SR,L1,triple_var   3.000   0.650     3.270 [0.30000000000000004]
   logistic_return_intervals  4096                     []   0.200   0.100     0.000 []
    zeta_zero_spacings_first   512                  SR,L2   2.150   0.250     2.666 []
saved tools/data/semireal_order_denominator_gate_20260507_0923.json

codex
Il primo deposito è discriminante: primi e zeta sostengono il gate, ma i ritorni logistici non mostrano supporto coerente. Faccio una seed check più leggera ma con lo stesso strumento, perché quel controesempio è il punto da non sovrainterpretare.
exec
/bin/bash -lc 'python tools/exp_semireal_order_denominator_gate.py --include-zeta --n-gaps 4096 --zeta-gaps 512 --n-replicates 12 --n-beta 11 --n-baseline 24 --seed 202605070924 --out tools/data/semireal_order_denominator_gate_20260507_0923_seedcheck.json' in /opt/MM_D-ND
diff --git a/tools/exp_semireal_order_denominator_gate.py b/tools/exp_semireal_order_denominator_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..f3da4e51c84f066066367b25fded3ebea3c1c00d
--- /dev/null
+++ b/tools/exp_semireal_order_denominator_gate.py
@@ -0,0 +1,352 @@
+#!/usr/bin/env python3
+"""
+exp_semireal_order_denominator_gate.py
+
+Falsification attempt for ORDER_DENOMINATOR_GATE on non-synthetic / semi-real
+ordered sequences. The coherent endpoint is the observed order of each sequence;
+the illusory endpoint is a marginal-preserving permutation. The same
+original-vs-shuffle denominator gate used in the prior reports is applied to
+canonical observables from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import math
+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 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 sieve_primes_for_count(n_primes: int) -> np.ndarray:
+    if n_primes < 6:
+        limit = 20
+    else:
+        limit = int(n_primes * (math.log(n_primes) + math.log(math.log(n_primes))) * 1.25)
+    while True:
+        sieve = np.ones(limit + 1, dtype=bool)
+        sieve[:2] = False
+        for p in range(2, int(limit**0.5) + 1):
+            if sieve[p]:
+                sieve[p * p : limit + 1 : p] = False
+        primes = np.flatnonzero(sieve)
+        if len(primes) >= n_primes:
+            return primes[:n_primes].astype(float)
+        limit *= 2
+
+
+def prime_gap_sequence(n_gaps: int) -> np.ndarray:
+    primes = sieve_primes_for_count(n_gaps + 1)
+    return normalize(np.diff(primes))
+
+
+def zeta_zero_spacings(n_gaps: int) -> np.ndarray:
+    try:
+        import mpmath as mp
+    except ImportError as exc:
+        raise RuntimeError("mpmath is required for zeta_zero_spacings") from exc
+
+    zeros = np.empty(n_gaps + 1, dtype=float)
+    for i in range(n_gaps + 1):
+        zeros[i] = float(mp.im(mp.zetazero(i + 1)))
+    return normalize(np.diff(zeros))
+
+
+def logistic_return_intervals(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    # Return intervals to a high-density-edge event in the fully chaotic logistic map.
+    threshold = 0.95
+    burn = 2000
+    needed = n_gaps + 1
+    returns: list[int] = []
+    last_hit: int | None = None
+    x = float(rng.random())
+    i = 0
+    max_steps = 50_000_000
+    while len(returns) < needed and i < max_steps:
+        x = 4.0 * x * (1.0 - x)
+        if i >= burn and x > threshold:
+            if last_hit is not None:
+                returns.append(i - last_hit)
+            last_hit = i
+        i += 1
+    if len(returns) < needed:
+        raise RuntimeError(f"logistic generator produced {len(returns)} intervals, need {needed}")
+    return normalize(np.array(returns[:n_gaps], dtype=float))
+
+
+def beta_replace(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        return base.copy()
+    if beta >= 1.0:
+        return illusory
+    out = base.copy()
+    mask = rng.random(len(base)) < beta
+    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], dict[str, float]]:
+    original = compute_canonical(gaps)
+    baseline = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline[name].append(obs[name])
+
+    means = {}
+    sds = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline[name], dtype=float)
+        means[name] = float(np.mean(vals))
+        sds[name] = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        z[name] = float((original[name] - means[name]) / sds[name]) if sds[name] > 1e-15 else 0.0
+    return original, means, sds, 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 = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent, illusory])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent, axis=0)
+    illusory_centroid = np.mean(illusory, 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()):
+        margins = []
+        labels = []
+        coords = []
+        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)),
+            "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_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()},
+        }
+
+    one_sided = []
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    for name in OBS_NAMES:
+        coherent_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in coherent_rows]))
+        illusory_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in illusory_rows]))
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            one_sided.append(name)
+        if coherent_freq >= 0.75 and illusory_freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "coherent_one_sided_observables": one_sided,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def analyze_sequence(name: str, base: np.ndarray, 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):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        for beta in betas:
+            gaps = beta_replace(base, beta, rep_rng)
+            obs, shuffle_mean, 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_mean": shuffle_mean,
+                    "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)
+    return {
+        "source": {
+            "n_gaps": int(len(base)),
+            "mean": float(np.mean(base)),
+            "variance": float(np.var(base)),
+        },
+        "gate": gate,
+        "classification_all_observables": classify_layers(rows, OBS_NAMES),
+        "classification_one_sided_gated": classify_layers(rows, gate["coherent_one_sided_observables"]),
+        "rows": rows,
+    }
+
+
+def build_sequences(args: argparse.Namespace, rng: np.random.Generator) -> dict[str, np.ndarray]:
+    sequences = {
+        "prime_gaps_first": prime_gap_sequence(args.n_gaps),
+        "logistic_return_intervals": logistic_return_intervals(args.n_gaps, rng),
+    }
+    if args.include_zeta:
+        sequences["zeta_zero_spacings_first"] = zeta_zero_spacings(args.zeta_gaps)
+    return sequences
+
+
+def compact(perimeters: dict) -> dict:
+    out = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        all_cls = data["classification_all_observables"]
+        gated_cls = data["classification_one_sided_gated"]
+        out[name] = {
+            "n_gaps": data["source"]["n_gaps"],
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+            "z_mean_coherent": gate["layers"]["0.000"]["z_mean"],
+            "z_mean_illusory": gate["layers"]["1.000"]["z_mean"],
+            "endpoint_distance_all": all_cls["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": gated_cls["endpoint_distance"],
+            "ambiguous_beta_one_sided_gated": gated_cls["ambiguous_beta"],
+        }
+    return out
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    sequences = build_sequences(args, root_rng)
+    perimeters = {}
+    for name, base in sequences.items():
+        perimeters[name] = analyze_sequence(name, base, args, root_rng)
+
+    output = {
+        "experiment": "semireal_order_denominator_gate",
+        "category": "gate_falsification_semireal",
+        "question": "Does ORDER_DENOMINATOR_GATE survive on non-synthetic / semi-real ordered sequences?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact(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 n one_sided stable0 stable1 dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>28s} "
+            f"{row['n_gaps']:>5d} "
+            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_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("--zeta-gaps", type=int, default=1024)
+    parser.add_argument("--include-zeta", action="store_true")
+    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=202605070923)
+    parser.add_argument("--out", default="tools/data/semireal_order_denominator_gate_20260507_0923.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

diff --git a/tools/exp_semireal_order_denominator_gate.py b/tools/exp_semireal_order_denominator_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..f3da4e51c84f066066367b25fded3ebea3c1c00d
--- /dev/null
+++ b/tools/exp_semireal_order_denominator_gate.py
@@ -0,0 +1,352 @@
+#!/usr/bin/env python3
+"""
+exp_semireal_order_denominator_gate.py
+
+Falsification attempt for ORDER_DENOMINATOR_GATE on non-synthetic / semi-real
+ordered sequences. The coherent endpoint is the observed order of each sequence;
+the illusory endpoint is a marginal-preserving permutation. The same
+original-vs-shuffle denominator gate used in the prior reports is applied to
+canonical observables from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import math
+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 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 sieve_primes_for_count(n_primes: int) -> np.ndarray:
+    if n_primes < 6:
+        limit = 20
+    else:
+        limit = int(n_primes * (math.log(n_primes) + math.log(math.log(n_primes))) * 1.25)
+    while True:
+        sieve = np.ones(limit + 1, dtype=bool)
+        sieve[:2] = False
+        for p in range(2, int(limit**0.5) + 1):
+            if sieve[p]:
+                sieve[p * p : limit + 1 : p] = False
+        primes = np.flatnonzero(sieve)
+        if len(primes) >= n_primes:
+            return primes[:n_primes].astype(float)
+        limit *= 2
+
+
+def prime_gap_sequence(n_gaps: int) -> np.ndarray:
+    primes = sieve_primes_for_count(n_gaps + 1)
+    return normalize(np.diff(primes))
+
+
+def zeta_zero_spacings(n_gaps: int) -> np.ndarray:
+    try:
+        import mpmath as mp
+    except ImportError as exc:
+        raise RuntimeError("mpmath is required for zeta_zero_spacings") from exc
+
+    zeros = np.empty(n_gaps + 1, dtype=float)
+    for i in range(n_gaps + 1):
+        zeros[i] = float(mp.im(mp.zetazero(i + 1)))
+    return normalize(np.diff(zeros))
+
+
+def logistic_return_intervals(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    # Return intervals to a high-density-edge event in the fully chaotic logistic map.
+    threshold = 0.95
+    burn = 2000
+    needed = n_gaps + 1
+    returns: list[int] = []
+    last_hit: int | None = None
+    x = float(rng.random())
+    i = 0
+    max_steps = 50_000_000
+    while len(returns) < needed and i < max_steps:
+        x = 4.0 * x * (1.0 - x)
+        if i >= burn and x > threshold:
+            if last_hit is not None:
+                returns.append(i - last_hit)
+            last_hit = i
+        i += 1
+    if len(returns) < needed:
+        raise RuntimeError(f"logistic generator produced {len(returns)} intervals, need {needed}")
+    return normalize(np.array(returns[:n_gaps], dtype=float))
+
+
+def beta_replace(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        return base.copy()
+    if beta >= 1.0:
+        return illusory
+    out = base.copy()
+    mask = rng.random(len(base)) < beta
+    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], dict[str, float]]:
+    original = compute_canonical(gaps)
+    baseline = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline[name].append(obs[name])
+
+    means = {}
+    sds = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline[name], dtype=float)
+        means[name] = float(np.mean(vals))
+        sds[name] = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        z[name] = float((original[name] - means[name]) / sds[name]) if sds[name] > 1e-15 else 0.0
+    return original, means, sds, 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 = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent, illusory])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent, axis=0)
+    illusory_centroid = np.mean(illusory, 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()):
+        margins = []
+        labels = []
+        coords = []
+        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)),
+            "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_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()},
+        }
+
+    one_sided = []
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    for name in OBS_NAMES:
+        coherent_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in coherent_rows]))
+        illusory_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in illusory_rows]))
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            one_sided.append(name)
+        if coherent_freq >= 0.75 and illusory_freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "coherent_one_sided_observables": one_sided,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def analyze_sequence(name: str, base: np.ndarray, 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):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        for beta in betas:
+            gaps = beta_replace(base, beta, rep_rng)
+            obs, shuffle_mean, 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_mean": shuffle_mean,
+                    "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)
+    return {
+        "source": {
+            "n_gaps": int(len(base)),
+            "mean": float(np.mean(base)),
+            "variance": float(np.var(base)),
+        },
+        "gate": gate,
+        "classification_all_observables": classify_layers(rows, OBS_NAMES),
+        "classification_one_sided_gated": classify_layers(rows, gate["coherent_one_sided_observables"]),
+        "rows": rows,
+    }
+
+
+def build_sequences(args: argparse.Namespace, rng: np.random.Generator) -> dict[str, np.ndarray]:
+    sequences = {
+        "prime_gaps_first": prime_gap_sequence(args.n_gaps),
+        "logistic_return_intervals": logistic_return_intervals(args.n_gaps, rng),
+    }
+    if args.include_zeta:
+        sequences["zeta_zero_spacings_first"] = zeta_zero_spacings(args.zeta_gaps)
+    return sequences
+
+
+def compact(perimeters: dict) -> dict:
+    out = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        all_cls = data["classification_all_observables"]
+        gated_cls = data["classification_one_sided_gated"]
+        out[name] = {
+            "n_gaps": data["source"]["n_gaps"],
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+            "z_mean_coherent": gate["layers"]["0.000"]["z_mean"],
+            "z_mean_illusory": gate["layers"]["1.000"]["z_mean"],
+            "endpoint_distance_all": all_cls["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": gated_cls["endpoint_distance"],
+            "ambiguous_beta_one_sided_gated": gated_cls["ambiguous_beta"],
+        }
+    return out
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    sequences = build_sequences(args, root_rng)
+    perimeters = {}
+    for name, base in sequences.items():
+        perimeters[name] = analyze_sequence(name, base, args, root_rng)
+
+    output = {
+        "experiment": "semireal_order_denominator_gate",
+        "category": "gate_falsification_semireal",
+        "question": "Does ORDER_DENOMINATOR_GATE survive on non-synthetic / semi-real ordered sequences?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact(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 n one_sided stable0 stable1 dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>28s} "
+            f"{row['n_gaps']:>5d} "
+            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_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("--zeta-gaps", type=int, default=1024)
+    parser.add_argument("--include-zeta", action="store_true")
+    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=202605070923)
+    parser.add_argument("--out", default="tools/data/semireal_order_denominator_gate_20260507_0923.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

diff --git a/tools/exp_semireal_order_denominator_gate.py b/tools/exp_semireal_order_denominator_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..f3da4e51c84f066066367b25fded3ebea3c1c00d
--- /dev/null
+++ b/tools/exp_semireal_order_denominator_gate.py
@@ -0,0 +1,352 @@
+#!/usr/bin/env python3
+"""
+exp_semireal_order_denominator_gate.py
+
+Falsification attempt for ORDER_DENOMINATOR_GATE on non-synthetic / semi-real
+ordered sequences. The coherent endpoint is the observed order of each sequence;
+the illusory endpoint is a marginal-preserving permutation. The same
+original-vs-shuffle denominator gate used in the prior reports is applied to
+canonical observables from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import math
+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 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 sieve_primes_for_count(n_primes: int) -> np.ndarray:
+    if n_primes < 6:
+        limit = 20
+    else:
+        limit = int(n_primes * (math.log(n_primes) + math.log(math.log(n_primes))) * 1.25)
+    while True:
+        sieve = np.ones(limit + 1, dtype=bool)
+        sieve[:2] = False
+        for p in range(2, int(limit**0.5) + 1):
+            if sieve[p]:
+                sieve[p * p : limit + 1 : p] = False
+        primes = np.flatnonzero(sieve)
+        if len(primes) >= n_primes:
+            return primes[:n_primes].astype(float)
+        limit *= 2
+
+
+def prime_gap_sequence(n_gaps: int) -> np.ndarray:
+    primes = sieve_primes_for_count(n_gaps + 1)
+    return normalize(np.diff(primes))
+
+
+def zeta_zero_spacings(n_gaps: int) -> np.ndarray:
+    try:
+        import mpmath as mp
+    except ImportError as exc:
+        raise RuntimeError("mpmath is required for zeta_zero_spacings") from exc
+
+    zeros = np.empty(n_gaps + 1, dtype=float)
+    for i in range(n_gaps + 1):
+        zeros[i] = float(mp.im(mp.zetazero(i + 1)))
+    return normalize(np.diff(zeros))
+
+
+def logistic_return_intervals(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    # Return intervals to a high-density-edge event in the fully chaotic logistic map.
+    threshold = 0.95
+    burn = 2000
+    needed = n_gaps + 1
+    returns: list[int] = []
+    last_hit: int | None = None
+    x = float(rng.random())
+    i = 0
+    max_steps = 50_000_000
+    while len(returns) < needed and i < max_steps:
+        x = 4.0 * x * (1.0 - x)
+        if i >= burn and x > threshold:
+            if last_hit is not None:
+                returns.append(i - last_hit)
+            last_hit = i
+        i += 1
+    if len(returns) < needed:
+        raise RuntimeError(f"logistic generator produced {len(returns)} intervals, need {needed}")
+    return normalize(np.array(returns[:n_gaps], dtype=float))
+
+
+def beta_replace(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        return base.copy()
+    if beta >= 1.0:
+        return illusory
+    out = base.copy()
+    mask = rng.random(len(base)) < beta
+    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], dict[str, float]]:
+    original = compute_canonical(gaps)
+    baseline = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline[name].append(obs[name])
+
+    means = {}
+    sds = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline[name], dtype=float)
+        means[name] = float(np.mean(vals))
+        sds[name] = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        z[name] = float((original[name] - means[name]) / sds[name]) if sds[name] > 1e-15 else 0.0
+    return original, means, sds, 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 = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent, illusory])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent, axis=0)
+    illusory_centroid = np.mean(illusory, 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()):
+        margins = []
+        labels = []
+        coords = []
+        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)),
+            "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_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()},
+        }
+
+    one_sided = []
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    for name in OBS_NAMES:
+        coherent_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in coherent_rows]))
+        illusory_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in illusory_rows]))
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            one_sided.append(name)
+        if coherent_freq >= 0.75 and illusory_freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "coherent_one_sided_observables": one_sided,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def analyze_sequence(name: str, base: np.ndarray, 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):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        for beta in betas:
+            gaps = beta_replace(base, beta, rep_rng)
+            obs, shuffle_mean, 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_mean": shuffle_mean,
+                    "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)
+    return {
+        "source": {
+            "n_gaps": int(len(base)),
+            "mean": float(np.mean(base)),
+            "variance": float(np.var(base)),
+        },
+        "gate": gate,
+        "classification_all_observables": classify_layers(rows, OBS_NAMES),
+        "classification_one_sided_gated": classify_layers(rows, gate["coherent_one_sided_observables"]),
+        "rows": rows,
+    }
+
+
+def build_sequences(args: argparse.Namespace, rng: np.random.Generator) -> dict[str, np.ndarray]:
+    sequences = {
+        "prime_gaps_first": prime_gap_sequence(args.n_gaps),
+        "logistic_return_intervals": logistic_return_intervals(args.n_gaps, rng),
+    }
+    if args.include_zeta:
+        sequences["zeta_zero_spacings_first"] = zeta_zero_spacings(args.zeta_gaps)
+    return sequences
+
+
+def compact(perimeters: dict) -> dict:
+    out = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        all_cls = data["classification_all_observables"]
+        gated_cls = data["classification_one_sided_gated"]
+        out[name] = {
+            "n_gaps": data["source"]["n_gaps"],
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+            "z_mean_coherent": gate["layers"]["0.000"]["z_mean"],
+            "z_mean_illusory": gate["layers"]["1.000"]["z_mean"],
+            "endpoint_distance_all": all_cls["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": gated_cls["endpoint_distance"],
+            "ambiguous_beta_one_sided_gated": gated_cls["ambiguous_beta"],
+        }
+    return out
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    sequences = build_sequences(args, root_rng)
+    perimeters = {}
+    for name, base in sequences.items():
+        perimeters[name] = analyze_sequence(name, base, args, root_rng)
+
+    output = {
+        "experiment": "semireal_order_denominator_gate",
+        "category": "gate_falsification_semireal",
+        "question": "Does ORDER_DENOMINATOR_GATE survive on non-synthetic / semi-real ordered sequences?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact(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 n one_sided stable0 stable1 dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>28s} "
+            f"{row['n_gaps']:>5d} "
+            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_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("--zeta-gaps", type=int, default=1024)
+    parser.add_argument("--include-zeta", action="store_true")
+    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=202605070923)
+    parser.add_argument("--out", default="tools/data/semireal_order_denominator_gate_20260507_0923.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

codex
La seed check con zeta sta richiedendo più del run principale; non cambio ancora per evitare due protocolli diversi. Attendo la chiusura e poi estraggo solo la matrice compatta.
diff --git a/tools/exp_semireal_order_denominator_gate.py b/tools/exp_semireal_order_denominator_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..f3da4e51c84f066066367b25fded3ebea3c1c00d
--- /dev/null
+++ b/tools/exp_semireal_order_denominator_gate.py
@@ -0,0 +1,352 @@
+#!/usr/bin/env python3
+"""
+exp_semireal_order_denominator_gate.py
+
+Falsification attempt for ORDER_DENOMINATOR_GATE on non-synthetic / semi-real
+ordered sequences. The coherent endpoint is the observed order of each sequence;
+the illusory endpoint is a marginal-preserving permutation. The same
+original-vs-shuffle denominator gate used in the prior reports is applied to
+canonical observables from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import math
+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 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 sieve_primes_for_count(n_primes: int) -> np.ndarray:
+    if n_primes < 6:
+        limit = 20
+    else:
+        limit = int(n_primes * (math.log(n_primes) + math.log(math.log(n_primes))) * 1.25)
+    while True:
+        sieve = np.ones(limit + 1, dtype=bool)
+        sieve[:2] = False
+        for p in range(2, int(limit**0.5) + 1):
+            if sieve[p]:
+                sieve[p * p : limit + 1 : p] = False
+        primes = np.flatnonzero(sieve)
+        if len(primes) >= n_primes:
+            return primes[:n_primes].astype(float)
+        limit *= 2
+
+
+def prime_gap_sequence(n_gaps: int) -> np.ndarray:
+    primes = sieve_primes_for_count(n_gaps + 1)
+    return normalize(np.diff(primes))
+
+
+def zeta_zero_spacings(n_gaps: int) -> np.ndarray:
+    try:
+        import mpmath as mp
+    except ImportError as exc:
+        raise RuntimeError("mpmath is required for zeta_zero_spacings") from exc
+
+    zeros = np.empty(n_gaps + 1, dtype=float)
+    for i in range(n_gaps + 1):
+        zeros[i] = float(mp.im(mp.zetazero(i + 1)))
+    return normalize(np.diff(zeros))
+
+
+def logistic_return_intervals(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    # Return intervals to a high-density-edge event in the fully chaotic logistic map.
+    threshold = 0.95
+    burn = 2000
+    needed = n_gaps + 1
+    returns: list[int] = []
+    last_hit: int | None = None
+    x = float(rng.random())
+    i = 0
+    max_steps = 50_000_000
+    while len(returns) < needed and i < max_steps:
+        x = 4.0 * x * (1.0 - x)
+        if i >= burn and x > threshold:
+            if last_hit is not None:
+                returns.append(i - last_hit)
+            last_hit = i
+        i += 1
+    if len(returns) < needed:
+        raise RuntimeError(f"logistic generator produced {len(returns)} intervals, need {needed}")
+    return normalize(np.array(returns[:n_gaps], dtype=float))
+
+
+def beta_replace(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        return base.copy()
+    if beta >= 1.0:
+        return illusory
+    out = base.copy()
+    mask = rng.random(len(base)) < beta
+    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], dict[str, float]]:
+    original = compute_canonical(gaps)
+    baseline = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline[name].append(obs[name])
+
+    means = {}
+    sds = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline[name], dtype=float)
+        means[name] = float(np.mean(vals))
+        sds[name] = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        z[name] = float((original[name] - means[name]) / sds[name]) if sds[name] > 1e-15 else 0.0
+    return original, means, sds, 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 = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent, illusory])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent, axis=0)
+    illusory_centroid = np.mean(illusory, 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()):
+        margins = []
+        labels = []
+        coords = []
+        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)),
+            "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_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()},
+        }
+
+    one_sided = []
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    for name in OBS_NAMES:
+        coherent_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in coherent_rows]))
+        illusory_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in illusory_rows]))
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            one_sided.append(name)
+        if coherent_freq >= 0.75 and illusory_freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "coherent_one_sided_observables": one_sided,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def analyze_sequence(name: str, base: np.ndarray, 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):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        for beta in betas:
+            gaps = beta_replace(base, beta, rep_rng)
+            obs, shuffle_mean, 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_mean": shuffle_mean,
+                    "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)
+    return {
+        "source": {
+            "n_gaps": int(len(base)),
+            "mean": float(np.mean(base)),
+            "variance": float(np.var(base)),
+        },
+        "gate": gate,
+        "classification_all_observables": classify_layers(rows, OBS_NAMES),
+        "classification_one_sided_gated": classify_layers(rows, gate["coherent_one_sided_observables"]),
+        "rows": rows,
+    }
+
+
+def build_sequences(args: argparse.Namespace, rng: np.random.Generator) -> dict[str, np.ndarray]:
+    sequences = {
+        "prime_gaps_first": prime_gap_sequence(args.n_gaps),
+        "logistic_return_intervals": logistic_return_intervals(args.n_gaps, rng),
+    }
+    if args.include_zeta:
+        sequences["zeta_zero_spacings_first"] = zeta_zero_spacings(args.zeta_gaps)
+    return sequences
+
+
+def compact(perimeters: dict) -> dict:
+    out = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        all_cls = data["classification_all_observables"]
+        gated_cls = data["classification_one_sided_gated"]
+        out[name] = {
+            "n_gaps": data["source"]["n_gaps"],
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+            "z_mean_coherent": gate["layers"]["0.000"]["z_mean"],
+            "z_mean_illusory": gate["layers"]["1.000"]["z_mean"],
+            "endpoint_distance_all": all_cls["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": gated_cls["endpoint_distance"],
+            "ambiguous_beta_one_sided_gated": gated_cls["ambiguous_beta"],
+        }
+    return out
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    sequences = build_sequences(args, root_rng)
+    perimeters = {}
+    for name, base in sequences.items():
+        perimeters[name] = analyze_sequence(name, base, args, root_rng)
+
+    output = {
+        "experiment": "semireal_order_denominator_gate",
+        "category": "gate_falsification_semireal",
+        "question": "Does ORDER_DENOMINATOR_GATE survive on non-synthetic / semi-real ordered sequences?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact(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 n one_sided stable0 stable1 dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>28s} "
+            f"{row['n_gaps']:>5d} "
+            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_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("--zeta-gaps", type=int, default=1024)
+    parser.add_argument("--include-zeta", action="store_true")
+    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=202605070923)
+    parser.add_argument("--out", default="tools/data/semireal_order_denominator_gate_20260507_0923.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

 succeeded in 73305ms:
observables_registry=1.0.0-2026-05-06
observables_used=['SR', 'SR2', 'L1', 'L2', 'triple_var']
perimeter n one_sided stable0 stable1 dist_gate ambiguous_gate
            prime_gaps_first  4096       SR,L1,triple_var   3.000   0.250     3.288 [0.30000000000000004]
   logistic_return_intervals  4096                     []   0.000   0.583     0.000 []
    zeta_zero_spacings_first   512                  SR,L2   2.417   0.333     2.700 []
saved tools/data/semireal_order_denominator_gate_20260507_0923_seedcheck.json

exec
/bin/bash -lc "python -m json.tool tools/data/semireal_order_denominator_gate_20260507_0923_seedcheck.json | sed -n '1,180p'" in /opt/MM_D-ND
 succeeded in 0ms:
{
    "experiment": "semireal_order_denominator_gate",
    "category": "gate_falsification_semireal",
    "question": "Does ORDER_DENOMINATOR_GATE survive on non-synthetic / semi-real ordered sequences?",
    "observables_registry": "1.0.0-2026-05-06",
    "observables_used": [
        "SR",
        "SR2",
        "L1",
        "L2",
        "triple_var"
    ],
    "params": {
        "n_gaps": 4096,
        "zeta_gaps": 512,
        "include_zeta": true,
        "n_replicates": 12,
        "n_beta": 11,
        "n_baseline": 24,
        "z_min": 2.0,
        "seed": 202605070924,
        "out": "tools/data/semireal_order_denominator_gate_20260507_0923_seedcheck.json"
    },
    "matrix": {
        "prime_gaps_first": {
            "n_gaps": 4096,
            "coherent_one_sided_observables": [
                "SR",
                "L1",
                "triple_var"
            ],
            "endpoint_stable_observables": [],
            "stable_count_coherent": 3.0,
            "stable_count_illusory": 0.25,
            "z_mean_coherent": {
                "SR": -8.02832441673615,
                "SR2": 0.8699534866925145,
                "L1": -4.562582828063424,
                "L2": -1.0268489517647035,
                "triple_var": -4.340247368699423
            },
            "z_mean_illusory": {
                "SR": 0.03645879049078434,
                "SR2": 0.10524150985481462,
                "L1": 0.08196562905189624,
                "L2": -0.08194940977051592,
                "triple_var": 0.09755429202500134
            },
            "endpoint_distance_all": 3.592847228877096,
            "endpoint_distance_one_sided_gated": 3.2881853795513174,
            "ambiguous_beta_one_sided_gated": [
                0.30000000000000004
            ]
        },
        "logistic_return_intervals": {
            "n_gaps": 4096,
            "coherent_one_sided_observables": [],
            "endpoint_stable_observables": [],
            "stable_count_coherent": 0.0,
            "stable_count_illusory": 0.5833333333333334,
            "z_mean_coherent": {
                "SR": -0.8646630675736953,
                "SR2": -0.7111079456695754,
                "L1": -0.4954289398807674,
                "L2": -0.078786673495728,
                "triple_var": -0.4589242447435282
            },
            "z_mean_illusory": {
                "SR": 0.8610378012152458,
                "SR2": -0.4517610401257545,
                "L1": 0.17364255984943977,
                "L2": -0.4287103523101701,
                "triple_var": -0.10819061288984577
            },
            "endpoint_distance_all": 1.9761209571318792,
            "endpoint_distance_one_sided_gated": 0.0,
            "ambiguous_beta_one_sided_gated": []
        },
        "zeta_zero_spacings_first": {
            "n_gaps": 512,
            "coherent_one_sided_observables": [
                "SR",
                "L2"
            ],
            "endpoint_stable_observables": [],
            "stable_count_coherent": 2.4166666666666665,
            "stable_count_illusory": 0.3333333333333333,
            "z_mean_coherent": {
                "SR": -4.227830450383016,
                "SR2": 1.0018698862997617,
                "L1": 0.9340747824185985,
                "L2": 5.434468982122841,
                "triple_var": 1.9197467431391368
            },
            "z_mean_illusory": {
                "SR": 0.4664478696577537,
                "SR2": -0.12472533074045382,
                "L1": 0.42939210931541555,
                "L2": -0.32066345569383975,
                "triple_var": 0.3183962386428235
            },
            "endpoint_distance_all": 3.275615448017419,
            "endpoint_distance_one_sided_gated": 2.6996438903252367,
            "ambiguous_beta_one_sided_gated": []
        }
    },
    "perimeters": {
        "prime_gaps_first": {
            "source": {
                "n_gaps": 4096,
                "mean": 1.0,
                "variance": 0.576159913652933
            },
            "gate": {
                "z_min": 2.0,
                "coherent_one_sided_observables": [
                    "SR",
                    "L1",
                    "triple_var"
                ],
                "endpoint_stable_observables": [],
                "layers": {
                    "0.000": {
                        "stable_count_mean": 3.0,
                        "stable_frequency": {
                            "SR": 1.0,
                            "SR2": 0.0,
                            "L1": 1.0,
                            "L2": 0.0,
                            "triple_var": 1.0
                        },
                        "z_mean": {
                            "SR": -8.02832441673615,
                            "SR2": 0.8699534866925145,
                            "L1": -4.562582828063424,
                            "L2": -1.0268489517647035,
                            "triple_var": -4.340247368699423
                        }
                    },
                    "0.100": {
                        "stable_count_mean": 3.0,
                        "stable_frequency": {
                            "SR": 1.0,
                            "SR2": 0.0,
                            "L1": 1.0,
                            "L2": 0.0,
                            "triple_var": 1.0
                        },
                        "z_mean": {
                            "SR": -6.048465296895553,
                            "SR2": 0.7887964935683446,
                            "L1": -3.4046740135055806,
                            "L2": -0.7494875620109558,
                            "triple_var": -3.471680659840176
                        }
                    },
                    "0.200": {
                        "stable_count_mean": 2.5833333333333335,
                        "stable_frequency": {
                            "SR": 1.0,
                            "SR2": 0.08333333333333333,
                            "L1": 0.9166666666666666,
                            "L2": 0.0,
                            "triple_var": 0.5833333333333334
                        },
                        "z_mean": {
                            "SR": -5.1674616103020945,
                            "SR2": 0.9935417708829841,
                            "L1": -2.6516440778482506,
                            "L2": -0.21931244922274742,
                            "triple_var": -2.4786643140297375
                        }
                    },
                    "0.300": {
                        "stable_count_mean": 2.5833333333333335,
                        "stable_frequency": {
                            "SR": 1.0,
                            "SR2": 0.0,
                            "L1": 0.75,
                            "L2": 0.08333333333333333,

exec
/bin/bash -lc "python -m json.tool tools/data/semireal_order_denominator_gate_20260507_0923.json | sed -n '1,180p'" in /opt/MM_D-ND
 succeeded in 0ms:
{
    "experiment": "semireal_order_denominator_gate",
    "category": "gate_falsification_semireal",
    "question": "Does ORDER_DENOMINATOR_GATE survive on non-synthetic / semi-real ordered sequences?",
    "observables_registry": "1.0.0-2026-05-06",
    "observables_used": [
        "SR",
        "SR2",
        "L1",
        "L2",
        "triple_var"
    ],
    "params": {
        "n_gaps": 4096,
        "zeta_gaps": 512,
        "include_zeta": true,
        "n_replicates": 20,
        "n_beta": 11,
        "n_baseline": 32,
        "z_min": 2.0,
        "seed": 202605070923,
        "out": "tools/data/semireal_order_denominator_gate_20260507_0923.json"
    },
    "matrix": {
        "prime_gaps_first": {
            "n_gaps": 4096,
            "coherent_one_sided_observables": [
                "SR",
                "L1",
                "triple_var"
            ],
            "endpoint_stable_observables": [],
            "stable_count_coherent": 3.0,
            "stable_count_illusory": 0.65,
            "z_mean_coherent": {
                "SR": -8.037812620228255,
                "SR2": 0.8216199986378927,
                "L1": -4.578531941718163,
                "L2": -0.8511477712718521,
                "triple_var": -4.388542503383684
            },
            "z_mean_illusory": {
                "SR": 0.40964307693260515,
                "SR2": -0.08934761782604927,
                "L1": 0.28609190206426843,
                "L2": 0.0004125448769160711,
                "triple_var": 0.2661842355969954
            },
            "endpoint_distance_all": 3.550699898553146,
            "endpoint_distance_one_sided_gated": 3.2695820173813357,
            "ambiguous_beta_one_sided_gated": [
                0.30000000000000004
            ]
        },
        "logistic_return_intervals": {
            "n_gaps": 4096,
            "coherent_one_sided_observables": [],
            "endpoint_stable_observables": [],
            "stable_count_coherent": 0.2,
            "stable_count_illusory": 0.1,
            "z_mean_coherent": {
                "SR": 1.661292119210638,
                "SR2": -1.2585451951418403,
                "L1": 0.7273899874126708,
                "L2": -1.463561687131796,
                "triple_var": 0.0699405220488025
            },
            "z_mean_illusory": {
                "SR": 0.0025100022535950893,
                "SR2": -0.30440353190116476,
                "L1": -0.264606526111297,
                "L2": -0.32999828051485336,
                "triple_var": -0.3655427687689501
            },
            "endpoint_distance_all": 2.614566224120535,
            "endpoint_distance_one_sided_gated": 0.0,
            "ambiguous_beta_one_sided_gated": []
        },
        "zeta_zero_spacings_first": {
            "n_gaps": 512,
            "coherent_one_sided_observables": [
                "SR",
                "L2"
            ],
            "endpoint_stable_observables": [],
            "stable_count_coherent": 2.15,
            "stable_count_illusory": 0.25,
            "z_mean_coherent": {
                "SR": -4.1175630417503495,
                "SR2": 1.1852418598861505,
                "L1": 0.7868276524176598,
                "L2": 5.845032789755395,
                "triple_var": 1.7520777754934116
            },
            "z_mean_illusory": {
                "SR": -0.015496318695534828,
                "SR2": -0.05724037537342937,
                "L1": 0.07934210931785439,
                "L2": -0.17919732537148314,
                "triple_var": -0.034558529532226254
            },
            "endpoint_distance_all": 3.526929915179896,
            "endpoint_distance_one_sided_gated": 2.6657852378352045,
            "ambiguous_beta_one_sided_gated": []
        }
    },
    "perimeters": {
        "prime_gaps_first": {
            "source": {
                "n_gaps": 4096,
                "mean": 1.0,
                "variance": 0.576159913652933
            },
            "gate": {
                "z_min": 2.0,
                "coherent_one_sided_observables": [
                    "SR",
                    "L1",
                    "triple_var"
                ],
                "endpoint_stable_observables": [],
                "layers": {
                    "0.000": {
                        "stable_count_mean": 3.0,
                        "stable_frequency": {
                            "SR": 1.0,
                            "SR2": 0.0,
                            "L1": 1.0,
                            "L2": 0.0,
                            "triple_var": 1.0
                        },
                        "z_mean": {
                            "SR": -8.037812620228255,
                            "SR2": 0.8216199986378927,
                            "L1": -4.578531941718163,
                            "L2": -0.8511477712718521,
                            "triple_var": -4.388542503383684
                        }
                    },
                    "0.100": {
                        "stable_count_mean": 2.95,
                        "stable_frequency": {
                            "SR": 1.0,
                            "SR2": 0.0,
                            "L1": 1.0,
                            "L2": 0.0,
                            "triple_var": 0.95
                        },
                        "z_mean": {
                            "SR": -7.043054915187369,
                            "SR2": 0.6955565257682449,
                            "L1": -3.604412474298415,
                            "L2": -0.5770130926147786,
                            "triple_var": -3.449328500536172
                        }
                    },
                    "0.200": {
                        "stable_count_mean": 2.7,
                        "stable_frequency": {
                            "SR": 1.0,
                            "SR2": 0.0,
                            "L1": 0.9,
                            "L2": 0.0,
                            "triple_var": 0.8
                        },
                        "z_mean": {
                            "SR": -5.219959434992606,
                            "SR2": 0.5392380096974182,
                            "L1": -3.035283128631421,
                            "L2": -0.4034313025661798,
                            "triple_var": -2.849206073341911
                        }
                    },
                    "0.300": {
                        "stable_count_mean": 1.85,
                        "stable_frequency": {
                            "SR": 1.0,
                            "SR2": 0.05,
                            "L1": 0.45,
                            "L2": 0.0,

diff --git a/tools/exp_semireal_order_denominator_gate.py b/tools/exp_semireal_order_denominator_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..f3da4e51c84f066066367b25fded3ebea3c1c00d
--- /dev/null
+++ b/tools/exp_semireal_order_denominator_gate.py
@@ -0,0 +1,352 @@
+#!/usr/bin/env python3
+"""
+exp_semireal_order_denominator_gate.py
+
+Falsification attempt for ORDER_DENOMINATOR_GATE on non-synthetic / semi-real
+ordered sequences. The coherent endpoint is the observed order of each sequence;
+the illusory endpoint is a marginal-preserving permutation. The same
+original-vs-shuffle denominator gate used in the prior reports is applied to
+canonical observables from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import math
+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 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 sieve_primes_for_count(n_primes: int) -> np.ndarray:
+    if n_primes < 6:
+        limit = 20
+    else:
+        limit = int(n_primes * (math.log(n_primes) + math.log(math.log(n_primes))) * 1.25)
+    while True:
+        sieve = np.ones(limit + 1, dtype=bool)
+        sieve[:2] = False
+        for p in range(2, int(limit**0.5) + 1):
+            if sieve[p]:
+                sieve[p * p : limit + 1 : p] = False
+        primes = np.flatnonzero(sieve)
+        if len(primes) >= n_primes:
+            return primes[:n_primes].astype(float)
+        limit *= 2
+
+
+def prime_gap_sequence(n_gaps: int) -> np.ndarray:
+    primes = sieve_primes_for_count(n_gaps + 1)
+    return normalize(np.diff(primes))
+
+
+def zeta_zero_spacings(n_gaps: int) -> np.ndarray:
+    try:
+        import mpmath as mp
+    except ImportError as exc:
+        raise RuntimeError("mpmath is required for zeta_zero_spacings") from exc
+
+    zeros = np.empty(n_gaps + 1, dtype=float)
+    for i in range(n_gaps + 1):
+        zeros[i] = float(mp.im(mp.zetazero(i + 1)))
+    return normalize(np.diff(zeros))
+
+
+def logistic_return_intervals(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    # Return intervals to a high-density-edge event in the fully chaotic logistic map.
+    threshold = 0.95
+    burn = 2000
+    needed = n_gaps + 1
+    returns: list[int] = []
+    last_hit: int | None = None
+    x = float(rng.random())
+    i = 0
+    max_steps = 50_000_000
+    while len(returns) < needed and i < max_steps:
+        x = 4.0 * x * (1.0 - x)
+        if i >= burn and x > threshold:
+            if last_hit is not None:
+                returns.append(i - last_hit)
+            last_hit = i
+        i += 1
+    if len(returns) < needed:
+        raise RuntimeError(f"logistic generator produced {len(returns)} intervals, need {needed}")
+    return normalize(np.array(returns[:n_gaps], dtype=float))
+
+
+def beta_replace(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        return base.copy()
+    if beta >= 1.0:
+        return illusory
+    out = base.copy()
+    mask = rng.random(len(base)) < beta
+    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], dict[str, float]]:
+    original = compute_canonical(gaps)
+    baseline = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline[name].append(obs[name])
+
+    means = {}
+    sds = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline[name], dtype=float)
+        means[name] = float(np.mean(vals))
+        sds[name] = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        z[name] = float((original[name] - means[name]) / sds[name]) if sds[name] > 1e-15 else 0.0
+    return original, means, sds, 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 = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent, illusory])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent, axis=0)
+    illusory_centroid = np.mean(illusory, 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()):
+        margins = []
+        labels = []
+        coords = []
+        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)),
+            "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_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()},
+        }
+
+    one_sided = []
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    for name in OBS_NAMES:
+        coherent_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in coherent_rows]))
+        illusory_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in illusory_rows]))
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            one_sided.append(name)
+        if coherent_freq >= 0.75 and illusory_freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "coherent_one_sided_observables": one_sided,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def analyze_sequence(name: str, base: np.ndarray, 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):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        for beta in betas:
+            gaps = beta_replace(base, beta, rep_rng)
+            obs, shuffle_mean, 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_mean": shuffle_mean,
+                    "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)
+    return {
+        "source": {
+            "n_gaps": int(len(base)),
+            "mean": float(np.mean(base)),
+            "variance": float(np.var(base)),
+        },
+        "gate": gate,
+        "classification_all_observables": classify_layers(rows, OBS_NAMES),
+        "classification_one_sided_gated": classify_layers(rows, gate["coherent_one_sided_observables"]),
+        "rows": rows,
+    }
+
+
+def build_sequences(args: argparse.Namespace, rng: np.random.Generator) -> dict[str, np.ndarray]:
+    sequences = {
+        "prime_gaps_first": prime_gap_sequence(args.n_gaps),
+        "logistic_return_intervals": logistic_return_intervals(args.n_gaps, rng),
+    }
+    if args.include_zeta:
+        sequences["zeta_zero_spacings_first"] = zeta_zero_spacings(args.zeta_gaps)
+    return sequences
+
+
+def compact(perimeters: dict) -> dict:
+    out = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        all_cls = data["classification_all_observables"]
+        gated_cls = data["classification_one_sided_gated"]
+        out[name] = {
+            "n_gaps": data["source"]["n_gaps"],
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+            "z_mean_coherent": gate["layers"]["0.000"]["z_mean"],
+            "z_mean_illusory": gate["layers"]["1.000"]["z_mean"],
+            "endpoint_distance_all": all_cls["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": gated_cls["endpoint_distance"],
+            "ambiguous_beta_one_sided_gated": gated_cls["ambiguous_beta"],
+        }
+    return out
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    sequences = build_sequences(args, root_rng)
+    perimeters = {}
+    for name, base in sequences.items():
+        perimeters[name] = analyze_sequence(name, base, args, root_rng)
+
+    output = {
+        "experiment": "semireal_order_denominator_gate",
+        "category": "gate_falsification_semireal",
+        "question": "Does ORDER_DENOMINATOR_GATE survive on non-synthetic / semi-real ordered sequences?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact(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 n one_sided stable0 stable1 dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>28s} "
+            f"{row['n_gaps']:>5d} "
+            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_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("--zeta-gaps", type=int, default=1024)
+    parser.add_argument("--include-zeta", action="store_true")
+    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=202605070923)
+    parser.add_argument("--out", default="tools/data/semireal_order_denominator_gate_20260507_0923.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

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/scoperte/20260507_0901_denominator-gate-transfer-matrix_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/evolution/evolution_20260507_0901.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/falsifier_20260507_0901.json
?? 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/loop_guard_20260507_0901.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/reports/trajectory_apply_20260507_0923.json
?? tools/data/scale_selective_perturbation.json
?? tools/data/selective_layer_decoupling.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/seme_archive/piano_70.json
?? tools/data/semireal_order_denominator_gate_20260507_0923.json
?? tools/data/semireal_order_denominator_gate_20260507_0923_seedcheck.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_semireal_order_denominator_gate.py
?? tools/exp_spectral_rigidity.py
?? tools/exp_two_channel_boundary.py
?? tools/exp_two_channel_cross_domain.py
?? tools/exp_two_channel_shuffle_audit.py
?? tools/exp_two_layer_universality.py
?? tools/observables_registry.py

exec
/bin/bash -lc 'python -m py_compile tools/exp_semireal_order_denominator_gate.py' in /opt/MM_D-ND
 succeeded in 0ms:
diff --git a/tools/exp_semireal_order_denominator_gate.py b/tools/exp_semireal_order_denominator_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..f3da4e51c84f066066367b25fded3ebea3c1c00d
--- /dev/null
+++ b/tools/exp_semireal_order_denominator_gate.py
@@ -0,0 +1,352 @@
+#!/usr/bin/env python3
+"""
+exp_semireal_order_denominator_gate.py
+
+Falsification attempt for ORDER_DENOMINATOR_GATE on non-synthetic / semi-real
+ordered sequences. The coherent endpoint is the observed order of each sequence;
+the illusory endpoint is a marginal-preserving permutation. The same
+original-vs-shuffle denominator gate used in the prior reports is applied to
+canonical observables from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import math
+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 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 sieve_primes_for_count(n_primes: int) -> np.ndarray:
+    if n_primes < 6:
+        limit = 20
+    else:
+        limit = int(n_primes * (math.log(n_primes) + math.log(math.log(n_primes))) * 1.25)
+    while True:
+        sieve = np.ones(limit + 1, dtype=bool)
+        sieve[:2] = False
+        for p in range(2, int(limit**0.5) + 1):
+            if sieve[p]:
+                sieve[p * p : limit + 1 : p] = False
+        primes = np.flatnonzero(sieve)
+        if len(primes) >= n_primes:
+            return primes[:n_primes].astype(float)
+        limit *= 2
+
+
+def prime_gap_sequence(n_gaps: int) -> np.ndarray:
+    primes = sieve_primes_for_count(n_gaps + 1)
+    return normalize(np.diff(primes))
+
+
+def zeta_zero_spacings(n_gaps: int) -> np.ndarray:
+    try:
+        import mpmath as mp
+    except ImportError as exc:
+        raise RuntimeError("mpmath is required for zeta_zero_spacings") from exc
+
+    zeros = np.empty(n_gaps + 1, dtype=float)
+    for i in range(n_gaps + 1):
+        zeros[i] = float(mp.im(mp.zetazero(i + 1)))
+    return normalize(np.diff(zeros))
+
+
+def logistic_return_intervals(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    # Return intervals to a high-density-edge event in the fully chaotic logistic map.
+    threshold = 0.95
+    burn = 2000
+    needed = n_gaps + 1
+    returns: list[int] = []
+    last_hit: int | None = None
+    x = float(rng.random())
+    i = 0
+    max_steps = 50_000_000
+    while len(returns) < needed and i < max_steps:
+        x = 4.0 * x * (1.0 - x)
+        if i >= burn and x > threshold:
+            if last_hit is not None:
+                returns.append(i - last_hit)
+            last_hit = i
+        i += 1
+    if len(returns) < needed:
+        raise RuntimeError(f"logistic generator produced {len(returns)} intervals, need {needed}")
+    return normalize(np.array(returns[:n_gaps], dtype=float))
+
+
+def beta_replace(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        return base.copy()
+    if beta >= 1.0:
+        return illusory
+    out = base.copy()
+    mask = rng.random(len(base)) < beta
+    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], dict[str, float]]:
+    original = compute_canonical(gaps)
+    baseline = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline[name].append(obs[name])
+
+    means = {}
+    sds = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline[name], dtype=float)
+        means[name] = float(np.mean(vals))
+        sds[name] = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        z[name] = float((original[name] - means[name]) / sds[name]) if sds[name] > 1e-15 else 0.0
+    return original, means, sds, 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 = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent, illusory])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent, axis=0)
+    illusory_centroid = np.mean(illusory, 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()):
+        margins = []
+        labels = []
+        coords = []
+        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)),
+            "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_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()},
+        }
+
+    one_sided = []
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    for name in OBS_NAMES:
+        coherent_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in coherent_rows]))
+        illusory_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in illusory_rows]))
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            one_sided.append(name)
+        if coherent_freq >= 0.75 and illusory_freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "coherent_one_sided_observables": one_sided,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def analyze_sequence(name: str, base: np.ndarray, 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):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        for beta in betas:
+            gaps = beta_replace(base, beta, rep_rng)
+            obs, shuffle_mean, 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_mean": shuffle_mean,
+                    "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)
+    return {
+        "source": {
+            "n_gaps": int(len(base)),
+            "mean": float(np.mean(base)),
+            "variance": float(np.var(base)),
+        },
+        "gate": gate,
+        "classification_all_observables": classify_layers(rows, OBS_NAMES),
+        "classification_one_sided_gated": classify_layers(rows, gate["coherent_one_sided_observables"]),
+        "rows": rows,
+    }
+
+
+def build_sequences(args: argparse.Namespace, rng: np.random.Generator) -> dict[str, np.ndarray]:
+    sequences = {
+        "prime_gaps_first": prime_gap_sequence(args.n_gaps),
+        "logistic_return_intervals": logistic_return_intervals(args.n_gaps, rng),
+    }
+    if args.include_zeta:
+        sequences["zeta_zero_spacings_first"] = zeta_zero_spacings(args.zeta_gaps)
+    return sequences
+
+
+def compact(perimeters: dict) -> dict:
+    out = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        all_cls = data["classification_all_observables"]
+        gated_cls = data["classification_one_sided_gated"]
+        out[name] = {
+            "n_gaps": data["source"]["n_gaps"],
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+            "z_mean_coherent": gate["layers"]["0.000"]["z_mean"],
+            "z_mean_illusory": gate["layers"]["1.000"]["z_mean"],
+            "endpoint_distance_all": all_cls["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": gated_cls["endpoint_distance"],
+            "ambiguous_beta_one_sided_gated": gated_cls["ambiguous_beta"],
+        }
+    return out
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    sequences = build_sequences(args, root_rng)
+    perimeters = {}
+    for name, base in sequences.items():
+        perimeters[name] = analyze_sequence(name, base, args, root_rng)
+
+    output = {
+        "experiment": "semireal_order_denominator_gate",
+        "category": "gate_falsification_semireal",
+        "question": "Does ORDER_DENOMINATOR_GATE survive on non-synthetic / semi-real ordered sequences?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact(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 n one_sided stable0 stable1 dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>28s} "
+            f"{row['n_gaps']:>5d} "
+            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_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("--zeta-gaps", type=int, default=1024)
+    parser.add_argument("--include-zeta", action="store_true")
+    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=202605070923)
+    parser.add_argument("--out", default="tools/data/semireal_order_denominator_gate_20260507_0923.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

codex
Il deposito richiede una formulazione stretta: non "il gate fallisce" e non "il gate è universale". Il report dirà che su domini semi-reali il gate resta operatore per ordine spettrale/aritmetico misurabile dai canonici, mentre i ritorni logistici sono un controperimetro dove l'ordine reale non attraversa questo set di osservabili.
apply patch
patch: completed
/opt/MM_D-ND/tools/data/reports/agent_20260507_0923.md
diff --git a/tools/data/reports/agent_20260507_0923.md b/tools/data/reports/agent_20260507_0923.md
new file mode 100644
index 0000000000000000000000000000000000000000..d7dc7d70d70cbf71dfba4b394064f4bdf842bb15
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0923.md
@@ -0,0 +1,132 @@
+# Agent Report — Semi-Real Order Denominator Gate
+
+timestamp: 2026-05-07 09:23 UTC
+category: gate_falsification_semireal
+verdict: scoped_operator
+observables_registry: 1.0.0-2026-05-06
+observables_used: [SR, SR2, L1, L2, triple_var]
+tool: tools/exp_semireal_order_denominator_gate.py
+data: tools/data/semireal_order_denominator_gate_20260507_0923.json
+seed_check: tools/data/semireal_order_denominator_gate_20260507_0923_seedcheck.json
+
+## Claim Under Test
+
+Verificato dal campo vivo: il cycle 0901 ha nominato `ORDER_DENOMINATOR_GATE`
+come supporto one-sided dell'ordine generato, ma lo ha fatto su perimetri
+sintetici. Il mandato corrente chiede falsificazione su domini non-sintetici o
+semi-reali.
+
+Domanda: se l'endpoint coerente è una sequenza ordinata reale/semi-reale e
+l'endpoint illusorio è una permutazione che preserva la marginale, il supporto
+denominatore resta one-sided o compare un controesempio?
+
+Perimetri:
+
+- `prime_gaps_first`: primi 4096 gap fra primi.
+- `zeta_zero_spacings_first`: primi 512 spacing fra zeri non banali di zeta,
+  calcolati localmente con `mpmath.zetazero`.
+- `logistic_return_intervals`: 4096 intervalli di ritorno a `x > 0.95` nella
+  mappa logistica caotica `x -> 4x(1-x)`.
+
+Gate: osservabile stabile se `abs(original - shuffle_mean) / shuffle_std >= 2`.
+
+## Deposito Numerico
+
+Run principale: `n_replicates=20`, `n_beta=11`, `n_baseline=32`,
+`seed=202605070923`. Seed check: `n_replicates=12`, `n_baseline=24`,
+`seed=202605070924`.
+
+| perimeter | coherent one-sided observables | stable_count coherent | stable_count illusory | endpoint distance gated | beta ambiguous gated |
+|---|---:|---:|---:|---:|---:|
+| prime_gaps_first | SR, L1, triple_var | 3.000 | 0.650 | 3.270 | 0.30 |
+| logistic_return_intervals | [] | 0.200 | 0.100 | 0.000 | [] |
+| zeta_zero_spacings_first | SR, L2 | 2.150 | 0.250 | 2.666 | [] |
+
+Seed check:
+
+| perimeter | coherent one-sided observables | stable_count coherent | stable_count illusory | endpoint distance gated | beta ambiguous gated |
+|---|---:|---:|---:|---:|---:|
+| prime_gaps_first | SR, L1, triple_var | 3.000 | 0.250 | 3.288 | 0.30 |
+| logistic_return_intervals | [] | 0.000 | 0.583 | 0.000 | [] |
+| zeta_zero_spacings_first | SR, L2 | 2.417 | 0.333 | 2.700 | [] |
+
+Endpoint-stable observables: `[]` in all three perimeters in both runs.
+
+## Risultato
+
+1. **The order gate transfers to arithmetic and zeta spacing order.**
+
+   Prime gaps carry one-sided support on `SR`, `L1`, and `triple_var`.
+   Zeta-zero spacings carry one-sided support on `SR` and `L2`. In both cases
+   the illusory endpoint remains weak-denominator under the same marginal.
+
+2. **The logistic return perimeter is the counter-scope.**
+
+   The logistic return sequence is ordered and generated by a deterministic
+   chaotic system, but this canonical observable suite does not read its order
+   as denominator support. The coherent endpoint stable count is `0.200` in the
+   main run and `0.000` in the seed check. The gate does not transfer to this
+   return-time observable.
+
+3. **The transferable object is narrower than "real order".**
+
+   `ORDER_DENOMINATOR_GATE` names order that survives a marginal-preserving
+   shuffle in the canonical gap observables. It does not name every generated
+   sequence. The node regressivo is the observable contract, not the gate
+   threshold: if the order lives in return-time tail structure or symbolic
+   itinerary, `SR/SR2/L1/L2/triple_var` can be blank.
+
+4. **The beta layer is not universal.**
+
+   Prime gaps reproduce beta `0.30` as the ambiguous protocol layer. Zeta has
+   no gated ambiguous beta in this run. Logistic has no gated classifier because
+   the one-sided observable set is empty. This extends 0901: beta `0.30` was a
+   protocol fold in the synthetic matrix, not a cross-domain coordinate.
+
+## Consecutio
+
+`ORDER_DENOMINATOR_GATE` survives as scoped operator:
+
+> In semi-real arithmetic/spectral spacing perimeters, the denominator gate is
+> one-sided support for order against a marginal-preserving shuffle. In
+> logistic return intervals, the canonical gap observables do not carry that
+> support; the gate output is blank rather than false-positive.
+
+Next experiment: do not tune `z_min`. Change the observable perimetro for the
+logistic counter-scope: symbolic itinerary block entropy, return-tail exponent,
+or recurrence-plot diagonal statistics, each with the same original-vs-shuffle
+denominator gate. That tests whether logistic order is absent for this gate or
+only invisible to the current canonical gap suite.
+
+## Self-Audit: 5 Lenti
+
+L1 hard constraint vs bias: no universal claim is made. `endpoint_stable_observables: []`
+is exact for the run perimeters; "weak" means below the declared `abs(z)>=2`
+gate frequency, not numerical zero.
+
+L2 quantity vs ratio: raw stable counts and z means are reported before endpoint
+distances. No percentage drift claim is used.
+
+L3 no silent patching: 0901 claimed transfer on synthetic generated-order
+perimeters. This report narrows the scope after observing the logistic
+counter-perimeter; it does not rescue the original wording.
+
+L4 edge cases: logistic illusory stable_count is `0.583` in the seed check,
+so the claim is not "illusory endpoint absent." It is "no coherent one-sided
+support under the declared frequency rule."
+
+L5 re-discovery vs discovery: prime gap and zeta spacing order-vs-shuffle tests
+sit near known arithmetic/RMT order diagnostics; logistic return intervals sit
+near known recurrence and return-time diagnostics for chaotic maps. No NEW
+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_semireal_order_denominator_gate.py`
+- Verificato: `tools/data/semireal_order_denominator_gate_20260507_0923.json`
+- Verificato: `tools/data/semireal_order_denominator_gate_20260507_0923_seedcheck.json`
+- Inferito: logistic return intervals are a counter-scope for the canonical
+  gap-observable form of `ORDER_DENOMINATOR_GATE`.
diff --git a/tools/exp_semireal_order_denominator_gate.py b/tools/exp_semireal_order_denominator_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..f3da4e51c84f066066367b25fded3ebea3c1c00d
--- /dev/null
+++ b/tools/exp_semireal_order_denominator_gate.py
@@ -0,0 +1,352 @@
+#!/usr/bin/env python3
+"""
+exp_semireal_order_denominator_gate.py
+
+Falsification attempt for ORDER_DENOMINATOR_GATE on non-synthetic / semi-real
+ordered sequences. The coherent endpoint is the observed order of each sequence;
+the illusory endpoint is a marginal-preserving permutation. The same
+original-vs-shuffle denominator gate used in the prior reports is applied to
+canonical observables from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import math
+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 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 sieve_primes_for_count(n_primes: int) -> np.ndarray:
+    if n_primes < 6:
+        limit = 20
+    else:
+        limit = int(n_primes * (math.log(n_primes) + math.log(math.log(n_primes))) * 1.25)
+    while True:
+        sieve = np.ones(limit + 1, dtype=bool)
+        sieve[:2] = False
+        for p in range(2, int(limit**0.5) + 1):
+            if sieve[p]:
+                sieve[p * p : limit + 1 : p] = False
+        primes = np.flatnonzero(sieve)
+        if len(primes) >= n_primes:
+            return primes[:n_primes].astype(float)
+        limit *= 2
+
+
+def prime_gap_sequence(n_gaps: int) -> np.ndarray:
+    primes = sieve_primes_for_count(n_gaps + 1)
+    return normalize(np.diff(primes))
+
+
+def zeta_zero_spacings(n_gaps: int) -> np.ndarray:
+    try:
+        import mpmath as mp
+    except ImportError as exc:
+        raise RuntimeError("mpmath is required for zeta_zero_spacings") from exc
+
+    zeros = np.empty(n_gaps + 1, dtype=float)
+    for i in range(n_gaps + 1):
+        zeros[i] = float(mp.im(mp.zetazero(i + 1)))
+    return normalize(np.diff(zeros))
+
+
+def logistic_return_intervals(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    # Return intervals to a high-density-edge event in the fully chaotic logistic map.
+    threshold = 0.95
+    burn = 2000
+    needed = n_gaps + 1
+    returns: list[int] = []
+    last_hit: int | None = None
+    x = float(rng.random())
+    i = 0
+    max_steps = 50_000_000
+    while len(returns) < needed and i < max_steps:
+        x = 4.0 * x * (1.0 - x)
+        if i >= burn and x > threshold:
+            if last_hit is not None:
+                returns.append(i - last_hit)
+            last_hit = i
+        i += 1
+    if len(returns) < needed:
+        raise RuntimeError(f"logistic generator produced {len(returns)} intervals, need {needed}")
+    return normalize(np.array(returns[:n_gaps], dtype=float))
+
+
+def beta_replace(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        return base.copy()
+    if beta >= 1.0:
+        return illusory
+    out = base.copy()
+    mask = rng.random(len(base)) < beta
+    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], dict[str, float]]:
+    original = compute_canonical(gaps)
+    baseline = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline[name].append(obs[name])
+
+    means = {}
+    sds = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline[name], dtype=float)
+        means[name] = float(np.mean(vals))
+        sds[name] = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        z[name] = float((original[name] - means[name]) / sds[name]) if sds[name] > 1e-15 else 0.0
+    return original, means, sds, 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 = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent, illusory])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent, axis=0)
+    illusory_centroid = np.mean(illusory, 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()):
+        margins = []
+        labels = []
+        coords = []
+        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)),
+            "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_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()},
+        }
+
+    one_sided = []
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    for name in OBS_NAMES:
+        coherent_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in coherent_rows]))
+        illusory_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in illusory_rows]))
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            one_sided.append(name)
+        if coherent_freq >= 0.75 and illusory_freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "coherent_one_sided_observables": one_sided,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def analyze_sequence(name: str, base: np.ndarray, 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):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        for beta in betas:
+            gaps = beta_replace(base, beta, rep_rng)
+            obs, shuffle_mean, 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_mean": shuffle_mean,
+                    "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)
+    return {
+        "source": {
+            "n_gaps": int(len(base)),
+            "mean": float(np.mean(base)),
+            "variance": float(np.var(base)),
+        },
+        "gate": gate,
+        "classification_all_observables": classify_layers(rows, OBS_NAMES),
+        "classification_one_sided_gated": classify_layers(rows, gate["coherent_one_sided_observables"]),
+        "rows": rows,
+    }
+
+
+def build_sequences(args: argparse.Namespace, rng: np.random.Generator) -> dict[str, np.ndarray]:
+    sequences = {
+        "prime_gaps_first": prime_gap_sequence(args.n_gaps),
+        "logistic_return_intervals": logistic_return_intervals(args.n_gaps, rng),
+    }
+    if args.include_zeta:
+        sequences["zeta_zero_spacings_first"] = zeta_zero_spacings(args.zeta_gaps)
+    return sequences
+
+
+def compact(perimeters: dict) -> dict:
+    out = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        all_cls = data["classification_all_observables"]
+        gated_cls = data["classification_one_sided_gated"]
+        out[name] = {
+            "n_gaps": data["source"]["n_gaps"],
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+            "z_mean_coherent": gate["layers"]["0.000"]["z_mean"],
+            "z_mean_illusory": gate["layers"]["1.000"]["z_mean"],
+            "endpoint_distance_all": all_cls["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": gated_cls["endpoint_distance"],
+            "ambiguous_beta_one_sided_gated": gated_cls["ambiguous_beta"],
+        }
+    return out
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    sequences = build_sequences(args, root_rng)
+    perimeters = {}
+    for name, base in sequences.items():
+        perimeters[name] = analyze_sequence(name, base, args, root_rng)
+
+    output = {
+        "experiment": "semireal_order_denominator_gate",
+        "category": "gate_falsification_semireal",
+        "question": "Does ORDER_DENOMINATOR_GATE survive on non-synthetic / semi-real ordered sequences?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact(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 n one_sided stable0 stable1 dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>28s} "
+            f"{row['n_gaps']:>5d} "
+            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_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("--zeta-gaps", type=int, default=1024)
+    parser.add_argument("--include-zeta", action="store_true")
+    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=202605070923)
+    parser.add_argument("--out", default="tools/data/semireal_order_denominator_gate_20260507_0923.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

diff --git a/tools/data/reports/agent_20260507_0923.md b/tools/data/reports/agent_20260507_0923.md
new file mode 100644
index 0000000000000000000000000000000000000000..d7dc7d70d70cbf71dfba4b394064f4bdf842bb15
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0923.md
@@ -0,0 +1,132 @@
+# Agent Report — Semi-Real Order Denominator Gate
+
+timestamp: 2026-05-07 09:23 UTC
+category: gate_falsification_semireal
+verdict: scoped_operator
+observables_registry: 1.0.0-2026-05-06
+observables_used: [SR, SR2, L1, L2, triple_var]
+tool: tools/exp_semireal_order_denominator_gate.py
+data: tools/data/semireal_order_denominator_gate_20260507_0923.json
+seed_check: tools/data/semireal_order_denominator_gate_20260507_0923_seedcheck.json
+
+## Claim Under Test
+
+Verificato dal campo vivo: il cycle 0901 ha nominato `ORDER_DENOMINATOR_GATE`
+come supporto one-sided dell'ordine generato, ma lo ha fatto su perimetri
+sintetici. Il mandato corrente chiede falsificazione su domini non-sintetici o
+semi-reali.
+
+Domanda: se l'endpoint coerente è una sequenza ordinata reale/semi-reale e
+l'endpoint illusorio è una permutazione che preserva la marginale, il supporto
+denominatore resta one-sided o compare un controesempio?
+
+Perimetri:
+
+- `prime_gaps_first`: primi 4096 gap fra primi.
+- `zeta_zero_spacings_first`: primi 512 spacing fra zeri non banali di zeta,
+  calcolati localmente con `mpmath.zetazero`.
+- `logistic_return_intervals`: 4096 intervalli di ritorno a `x > 0.95` nella
+  mappa logistica caotica `x -> 4x(1-x)`.
+
+Gate: osservabile stabile se `abs(original - shuffle_mean) / shuffle_std >= 2`.
+
+## Deposito Numerico
+
+Run principale: `n_replicates=20`, `n_beta=11`, `n_baseline=32`,
+`seed=202605070923`. Seed check: `n_replicates=12`, `n_baseline=24`,
+`seed=202605070924`.
+
+| perimeter | coherent one-sided observables | stable_count coherent | stable_count illusory | endpoint distance gated | beta ambiguous gated |
+|---|---:|---:|---:|---:|---:|
+| prime_gaps_first | SR, L1, triple_var | 3.000 | 0.650 | 3.270 | 0.30 |
+| logistic_return_intervals | [] | 0.200 | 0.100 | 0.000 | [] |
+| zeta_zero_spacings_first | SR, L2 | 2.150 | 0.250 | 2.666 | [] |
+
+Seed check:
+
+| perimeter | coherent one-sided observables | stable_count coherent | stable_count illusory | endpoint distance gated | beta ambiguous gated |
+|---|---:|---:|---:|---:|---:|
+| prime_gaps_first | SR, L1, triple_var | 3.000 | 0.250 | 3.288 | 0.30 |
+| logistic_return_intervals | [] | 0.000 | 0.583 | 0.000 | [] |
+| zeta_zero_spacings_first | SR, L2 | 2.417 | 0.333 | 2.700 | [] |
+
+Endpoint-stable observables: `[]` in all three perimeters in both runs.
+
+## Risultato
+
+1. **The order gate transfers to arithmetic and zeta spacing order.**
+
+   Prime gaps carry one-sided support on `SR`, `L1`, and `triple_var`.
+   Zeta-zero spacings carry one-sided support on `SR` and `L2`. In both cases
+   the illusory endpoint remains weak-denominator under the same marginal.
+
+2. **The logistic return perimeter is the counter-scope.**
+
+   The logistic return sequence is ordered and generated by a deterministic
+   chaotic system, but this canonical observable suite does not read its order
+   as denominator support. The coherent endpoint stable count is `0.200` in the
+   main run and `0.000` in the seed check. The gate does not transfer to this
+   return-time observable.
+
+3. **The transferable object is narrower than "real order".**
+
+   `ORDER_DENOMINATOR_GATE` names order that survives a marginal-preserving
+   shuffle in the canonical gap observables. It does not name every generated
+   sequence. The node regressivo is the observable contract, not the gate
+   threshold: if the order lives in return-time tail structure or symbolic
+   itinerary, `SR/SR2/L1/L2/triple_var` can be blank.
+
+4. **The beta layer is not universal.**
+
+   Prime gaps reproduce beta `0.30` as the ambiguous protocol layer. Zeta has
+   no gated ambiguous beta in this run. Logistic has no gated classifier because
+   the one-sided observable set is empty. This extends 0901: beta `0.30` was a
+   protocol fold in the synthetic matrix, not a cross-domain coordinate.
+
+## Consecutio
+
+`ORDER_DENOMINATOR_GATE` survives as scoped operator:
+
+> In semi-real arithmetic/spectral spacing perimeters, the denominator gate is
+> one-sided support for order against a marginal-preserving shuffle. In
+> logistic return intervals, the canonical gap observables do not carry that
+> support; the gate output is blank rather than false-positive.
+
+Next experiment: do not tune `z_min`. Change the observable perimetro for the
+logistic counter-scope: symbolic itinerary block entropy, return-tail exponent,
+or recurrence-plot diagonal statistics, each with the same original-vs-shuffle
+denominator gate. That tests whether logistic order is absent for this gate or
+only invisible to the current canonical gap suite.
+
+## Self-Audit: 5 Lenti
+
+L1 hard constraint vs bias: no universal claim is made. `endpoint_stable_observables: []`
+is exact for the run perimeters; "weak" means below the declared `abs(z)>=2`
+gate frequency, not numerical zero.
+
+L2 quantity vs ratio: raw stable counts and z means are reported before endpoint
+distances. No percentage drift claim is used.
+
+L3 no silent patching: 0901 claimed transfer on synthetic generated-order
+perimeters. This report narrows the scope after observing the logistic
+counter-perimeter; it does not rescue the original wording.
+
+L4 edge cases: logistic illusory stable_count is `0.583` in the seed check,
+so the claim is not "illusory endpoint absent." It is "no coherent one-sided
+support under the declared frequency rule."
+
+L5 re-discovery vs discovery: prime gap and zeta spacing order-vs-shuffle tests
+sit near known arithmetic/RMT order diagnostics; logistic return intervals sit
+near known recurrence and return-time diagnostics for chaotic maps. No NEW
+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_semireal_order_denominator_gate.py`
+- Verificato: `tools/data/semireal_order_denominator_gate_20260507_0923.json`
+- Verificato: `tools/data/semireal_order_denominator_gate_20260507_0923_seedcheck.json`
+- Inferito: logistic return intervals are a counter-scope for the canonical
+  gap-observable form of `ORDER_DENOMINATOR_GATE`.
diff --git a/tools/exp_semireal_order_denominator_gate.py b/tools/exp_semireal_order_denominator_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..f3da4e51c84f066066367b25fded3ebea3c1c00d
--- /dev/null
+++ b/tools/exp_semireal_order_denominator_gate.py
@@ -0,0 +1,352 @@
+#!/usr/bin/env python3
+"""
+exp_semireal_order_denominator_gate.py
+
+Falsification attempt for ORDER_DENOMINATOR_GATE on non-synthetic / semi-real
+ordered sequences. The coherent endpoint is the observed order of each sequence;
+the illusory endpoint is a marginal-preserving permutation. The same
+original-vs-shuffle denominator gate used in the prior reports is applied to
+canonical observables from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import math
+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 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 sieve_primes_for_count(n_primes: int) -> np.ndarray:
+    if n_primes < 6:
+        limit = 20
+    else:
+        limit = int(n_primes * (math.log(n_primes) + math.log(math.log(n_primes))) * 1.25)
+    while True:
+        sieve = np.ones(limit + 1, dtype=bool)
+        sieve[:2] = False
+        for p in range(2, int(limit**0.5) + 1):
+            if sieve[p]:
+                sieve[p * p : limit + 1 : p] = False
+        primes = np.flatnonzero(sieve)
+        if len(primes) >= n_primes:
+            return primes[:n_primes].astype(float)
+        limit *= 2
+
+
+def prime_gap_sequence(n_gaps: int) -> np.ndarray:
+    primes = sieve_primes_for_count(n_gaps + 1)
+    return normalize(np.diff(primes))
+
+
+def zeta_zero_spacings(n_gaps: int) -> np.ndarray:
+    try:
+        import mpmath as mp
+    except ImportError as exc:
+        raise RuntimeError("mpmath is required for zeta_zero_spacings") from exc
+
+    zeros = np.empty(n_gaps + 1, dtype=float)
+    for i in range(n_gaps + 1):
+        zeros[i] = float(mp.im(mp.zetazero(i + 1)))
+    return normalize(np.diff(zeros))
+
+
+def logistic_return_intervals(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    # Return intervals to a high-density-edge event in the fully chaotic logistic map.
+    threshold = 0.95
+    burn = 2000
+    needed = n_gaps + 1
+    returns: list[int] = []
+    last_hit: int | None = None
+    x = float(rng.random())
+    i = 0
+    max_steps = 50_000_000
+    while len(returns) < needed and i < max_steps:
+        x = 4.0 * x * (1.0 - x)
+        if i >= burn and x > threshold:
+            if last_hit is not None:
+                returns.append(i - last_hit)
+            last_hit = i
+        i += 1
+    if len(returns) < needed:
+        raise RuntimeError(f"logistic generator produced {len(returns)} intervals, need {needed}")
+    return normalize(np.array(returns[:n_gaps], dtype=float))
+
+
+def beta_replace(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        return base.copy()
+    if beta >= 1.0:
+        return illusory
+    out = base.copy()
+    mask = rng.random(len(base)) < beta
+    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], dict[str, float]]:
+    original = compute_canonical(gaps)
+    baseline = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline[name].append(obs[name])
+
+    means = {}
+    sds = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline[name], dtype=float)
+        means[name] = float(np.mean(vals))
+        sds[name] = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        z[name] = float((original[name] - means[name]) / sds[name]) if sds[name] > 1e-15 else 0.0
+    return original, means, sds, 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 = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent, illusory])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent, axis=0)
+    illusory_centroid = np.mean(illusory, 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()):
+        margins = []
+        labels = []
+        coords = []
+        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)),
+            "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_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()},
+        }
+
+    one_sided = []
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    for name in OBS_NAMES:
+        coherent_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in coherent_rows]))
+        illusory_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in illusory_rows]))
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            one_sided.append(name)
+        if coherent_freq >= 0.75 and illusory_freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "coherent_one_sided_observables": one_sided,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def analyze_sequence(name: str, base: np.ndarray, 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):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        for beta in betas:
+            gaps = beta_replace(base, beta, rep_rng)
+            obs, shuffle_mean, 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_mean": shuffle_mean,
+                    "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)
+    return {
+        "source": {
+            "n_gaps": int(len(base)),
+            "mean": float(np.mean(base)),
+            "variance": float(np.var(base)),
+        },
+        "gate": gate,
+        "classification_all_observables": classify_layers(rows, OBS_NAMES),
+        "classification_one_sided_gated": classify_layers(rows, gate["coherent_one_sided_observables"]),
+        "rows": rows,
+    }
+
+
+def build_sequences(args: argparse.Namespace, rng: np.random.Generator) -> dict[str, np.ndarray]:
+    sequences = {
+        "prime_gaps_first": prime_gap_sequence(args.n_gaps),
+        "logistic_return_intervals": logistic_return_intervals(args.n_gaps, rng),
+    }
+    if args.include_zeta:
+        sequences["zeta_zero_spacings_first"] = zeta_zero_spacings(args.zeta_gaps)
+    return sequences
+
+
+def compact(perimeters: dict) -> dict:
+    out = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        all_cls = data["classification_all_observables"]
+        gated_cls = data["classification_one_sided_gated"]
+        out[name] = {
+            "n_gaps": data["source"]["n_gaps"],
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+            "z_mean_coherent": gate["layers"]["0.000"]["z_mean"],
+            "z_mean_illusory": gate["layers"]["1.000"]["z_mean"],
+            "endpoint_distance_all": all_cls["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": gated_cls["endpoint_distance"],
+            "ambiguous_beta_one_sided_gated": gated_cls["ambiguous_beta"],
+        }
+    return out
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    sequences = build_sequences(args, root_rng)
+    perimeters = {}
+    for name, base in sequences.items():
+        perimeters[name] = analyze_sequence(name, base, args, root_rng)
+
+    output = {
+        "experiment": "semireal_order_denominator_gate",
+        "category": "gate_falsification_semireal",
+        "question": "Does ORDER_DENOMINATOR_GATE survive on non-synthetic / semi-real ordered sequences?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact(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 n one_sided stable0 stable1 dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>28s} "
+            f"{row['n_gaps']:>5d} "
+            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_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("--zeta-gaps", type=int, default=1024)
+    parser.add_argument("--include-zeta", action="store_true")
+    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=202605070923)
+    parser.add_argument("--out", default="tools/data/semireal_order_denominator_gate_20260507_0923.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

codex
Aggiorno il seme con un vincolo piccolo, non con una nuova teoria: il finding restringe `ORDER_DENOMINATOR_GATE` al contratto osservabile e lascia aperto il perimetro logistico per il ciclo successivo.
exec
/bin/bash -lc "rg -n 'ORDER_DENOMINATOR_GATE|TRASCENDENZA_LIMITE' tools/data/seme.json" in /opt/MM_D-ND
 succeeded in 0ms:
40:      "id": "ORDER_DENOMINATOR_GATE",
51:      "id": "TRASCENDENZA_LIMITE",
150:      "claim": "Applied valutatore REDESIGN from 20260507_0901: Falsificare ORDER_DENOMINATOR_GATE su domini non-sintetici o semi-reali: applicare il gate one-sided a 2-3 sequenze fisiche/ponte gia' presenti nel sito, con shuffle e surrogati preservanti marginale,",
162:    "Tensioni risolte: {'METRIC_TENSOR', 'PIANO_PRIMARIO_DUE_ASSIOMI', 'OBSERVABLE_REGISTRY', 'TENSIONE_ENTITA', 'PERTURBATION_DENOMINATOR_GATE', 'BOUNDARY_LAYER_GATE', 'TRAJECTORY_APPLY_20260507_0803', 'G_POTENZIALE_NULLA', 'TRASCENDENZA_LIMITE', 'ORDER_DENOMINATOR_GATE', 'DUALITA_DIPOLARE_VS_ILLUSORIA'}"
169:  "direzione": "Falsificare ORDER_DENOMINATOR_GATE su domini non-sintetici o semi-reali: applicare il gate one-sided a 2-3 sequenze fisiche/ponte gia' presenti nel sito, con shuffle e surrogati preservanti marginale, cercando un controesempio dove il supporto denominatore appare anche sul lato illusorio o scompare sul lato coerente.",
337:    "trajectory_apply_20260507_092332": "applied modify_seme from 20260507_0901: direzione → 'Falsificare ORDER_DENOMINATOR_GATE su domini non-sintetici o semi-reali: applicare il gate one-sided...'"

diff --git a/tools/data/reports/agent_20260507_0923.md b/tools/data/reports/agent_20260507_0923.md
new file mode 100644
index 0000000000000000000000000000000000000000..d7dc7d70d70cbf71dfba4b394064f4bdf842bb15
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0923.md
@@ -0,0 +1,132 @@
+# Agent Report — Semi-Real Order Denominator Gate
+
+timestamp: 2026-05-07 09:23 UTC
+category: gate_falsification_semireal
+verdict: scoped_operator
+observables_registry: 1.0.0-2026-05-06
+observables_used: [SR, SR2, L1, L2, triple_var]
+tool: tools/exp_semireal_order_denominator_gate.py
+data: tools/data/semireal_order_denominator_gate_20260507_0923.json
+seed_check: tools/data/semireal_order_denominator_gate_20260507_0923_seedcheck.json
+
+## Claim Under Test
+
+Verificato dal campo vivo: il cycle 0901 ha nominato `ORDER_DENOMINATOR_GATE`
+come supporto one-sided dell'ordine generato, ma lo ha fatto su perimetri
+sintetici. Il mandato corrente chiede falsificazione su domini non-sintetici o
+semi-reali.
+
+Domanda: se l'endpoint coerente è una sequenza ordinata reale/semi-reale e
+l'endpoint illusorio è una permutazione che preserva la marginale, il supporto
+denominatore resta one-sided o compare un controesempio?
+
+Perimetri:
+
+- `prime_gaps_first`: primi 4096 gap fra primi.
+- `zeta_zero_spacings_first`: primi 512 spacing fra zeri non banali di zeta,
+  calcolati localmente con `mpmath.zetazero`.
+- `logistic_return_intervals`: 4096 intervalli di ritorno a `x > 0.95` nella
+  mappa logistica caotica `x -> 4x(1-x)`.
+
+Gate: osservabile stabile se `abs(original - shuffle_mean) / shuffle_std >= 2`.
+
+## Deposito Numerico
+
+Run principale: `n_replicates=20`, `n_beta=11`, `n_baseline=32`,
+`seed=202605070923`. Seed check: `n_replicates=12`, `n_baseline=24`,
+`seed=202605070924`.
+
+| perimeter | coherent one-sided observables | stable_count coherent | stable_count illusory | endpoint distance gated | beta ambiguous gated |
+|---|---:|---:|---:|---:|---:|
+| prime_gaps_first | SR, L1, triple_var | 3.000 | 0.650 | 3.270 | 0.30 |
+| logistic_return_intervals | [] | 0.200 | 0.100 | 0.000 | [] |
+| zeta_zero_spacings_first | SR, L2 | 2.150 | 0.250 | 2.666 | [] |
+
+Seed check:
+
+| perimeter | coherent one-sided observables | stable_count coherent | stable_count illusory | endpoint distance gated | beta ambiguous gated |
+|---|---:|---:|---:|---:|---:|
+| prime_gaps_first | SR, L1, triple_var | 3.000 | 0.250 | 3.288 | 0.30 |
+| logistic_return_intervals | [] | 0.000 | 0.583 | 0.000 | [] |
+| zeta_zero_spacings_first | SR, L2 | 2.417 | 0.333 | 2.700 | [] |
+
+Endpoint-stable observables: `[]` in all three perimeters in both runs.
+
+## Risultato
+
+1. **The order gate transfers to arithmetic and zeta spacing order.**
+
+   Prime gaps carry one-sided support on `SR`, `L1`, and `triple_var`.
+   Zeta-zero spacings carry one-sided support on `SR` and `L2`. In both cases
+   the illusory endpoint remains weak-denominator under the same marginal.
+
+2. **The logistic return perimeter is the counter-scope.**
+
+   The logistic return sequence is ordered and generated by a deterministic
+   chaotic system, but this canonical observable suite does not read its order
+   as denominator support. The coherent endpoint stable count is `0.200` in the
+   main run and `0.000` in the seed check. The gate does not transfer to this
+   return-time observable.
+
+3. **The transferable object is narrower than "real order".**
+
+   `ORDER_DENOMINATOR_GATE` names order that survives a marginal-preserving
+   shuffle in the canonical gap observables. It does not name every generated
+   sequence. The node regressivo is the observable contract, not the gate
+   threshold: if the order lives in return-time tail structure or symbolic
+   itinerary, `SR/SR2/L1/L2/triple_var` can be blank.
+
+4. **The beta layer is not universal.**
+
+   Prime gaps reproduce beta `0.30` as the ambiguous protocol layer. Zeta has
+   no gated ambiguous beta in this run. Logistic has no gated classifier because
+   the one-sided observable set is empty. This extends 0901: beta `0.30` was a
+   protocol fold in the synthetic matrix, not a cross-domain coordinate.
+
+## Consecutio
+
+`ORDER_DENOMINATOR_GATE` survives as scoped operator:
+
+> In semi-real arithmetic/spectral spacing perimeters, the denominator gate is
+> one-sided support for order against a marginal-preserving shuffle. In
+> logistic return intervals, the canonical gap observables do not carry that
+> support; the gate output is blank rather than false-positive.
+
+Next experiment: do not tune `z_min`. Change the observable perimetro for the
+logistic counter-scope: symbolic itinerary block entropy, return-tail exponent,
+or recurrence-plot diagonal statistics, each with the same original-vs-shuffle
+denominator gate. That tests whether logistic order is absent for this gate or
+only invisible to the current canonical gap suite.
+
+## Self-Audit: 5 Lenti
+
+L1 hard constraint vs bias: no universal claim is made. `endpoint_stable_observables: []`
+is exact for the run perimeters; "weak" means below the declared `abs(z)>=2`
+gate frequency, not numerical zero.
+
+L2 quantity vs ratio: raw stable counts and z means are reported before endpoint
+distances. No percentage drift claim is used.
+
+L3 no silent patching: 0901 claimed transfer on synthetic generated-order
+perimeters. This report narrows the scope after observing the logistic
+counter-perimeter; it does not rescue the original wording.
+
+L4 edge cases: logistic illusory stable_count is `0.583` in the seed check,
+so the claim is not "illusory endpoint absent." It is "no coherent one-sided
+support under the declared frequency rule."
+
+L5 re-discovery vs discovery: prime gap and zeta spacing order-vs-shuffle tests
+sit near known arithmetic/RMT order diagnostics; logistic return intervals sit
+near known recurrence and return-time diagnostics for chaotic maps. No NEW
+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_semireal_order_denominator_gate.py`
+- Verificato: `tools/data/semireal_order_denominator_gate_20260507_0923.json`
+- Verificato: `tools/data/semireal_order_denominator_gate_20260507_0923_seedcheck.json`
+- Inferito: logistic return intervals are a counter-scope for the canonical
+  gap-observable form of `ORDER_DENOMINATOR_GATE`.
diff --git a/tools/exp_semireal_order_denominator_gate.py b/tools/exp_semireal_order_denominator_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..f3da4e51c84f066066367b25fded3ebea3c1c00d
--- /dev/null
+++ b/tools/exp_semireal_order_denominator_gate.py
@@ -0,0 +1,352 @@
+#!/usr/bin/env python3
+"""
+exp_semireal_order_denominator_gate.py
+
+Falsification attempt for ORDER_DENOMINATOR_GATE on non-synthetic / semi-real
+ordered sequences. The coherent endpoint is the observed order of each sequence;
+the illusory endpoint is a marginal-preserving permutation. The same
+original-vs-shuffle denominator gate used in the prior reports is applied to
+canonical observables from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import math
+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 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 sieve_primes_for_count(n_primes: int) -> np.ndarray:
+    if n_primes < 6:
+        limit = 20
+    else:
+        limit = int(n_primes * (math.log(n_primes) + math.log(math.log(n_primes))) * 1.25)
+    while True:
+        sieve = np.ones(limit + 1, dtype=bool)
+        sieve[:2] = False
+        for p in range(2, int(limit**0.5) + 1):
+            if sieve[p]:
+                sieve[p * p : limit + 1 : p] = False
+        primes = np.flatnonzero(sieve)
+        if len(primes) >= n_primes:
+            return primes[:n_primes].astype(float)
+        limit *= 2
+
+
+def prime_gap_sequence(n_gaps: int) -> np.ndarray:
+    primes = sieve_primes_for_count(n_gaps + 1)
+    return normalize(np.diff(primes))
+
+
+def zeta_zero_spacings(n_gaps: int) -> np.ndarray:
+    try:
+        import mpmath as mp
+    except ImportError as exc:
+        raise RuntimeError("mpmath is required for zeta_zero_spacings") from exc
+
+    zeros = np.empty(n_gaps + 1, dtype=float)
+    for i in range(n_gaps + 1):
+        zeros[i] = float(mp.im(mp.zetazero(i + 1)))
+    return normalize(np.diff(zeros))
+
+
+def logistic_return_intervals(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    # Return intervals to a high-density-edge event in the fully chaotic logistic map.
+    threshold = 0.95
+    burn = 2000
+    needed = n_gaps + 1
+    returns: list[int] = []
+    last_hit: int | None = None
+    x = float(rng.random())
+    i = 0
+    max_steps = 50_000_000
+    while len(returns) < needed and i < max_steps:
+        x = 4.0 * x * (1.0 - x)
+        if i >= burn and x > threshold:
+            if last_hit is not None:
+                returns.append(i - last_hit)
+            last_hit = i
+        i += 1
+    if len(returns) < needed:
+        raise RuntimeError(f"logistic generator produced {len(returns)} intervals, need {needed}")
+    return normalize(np.array(returns[:n_gaps], dtype=float))
+
+
+def beta_replace(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        return base.copy()
+    if beta >= 1.0:
+        return illusory
+    out = base.copy()
+    mask = rng.random(len(base)) < beta
+    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], dict[str, float]]:
+    original = compute_canonical(gaps)
+    baseline = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline[name].append(obs[name])
+
+    means = {}
+    sds = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline[name], dtype=float)
+        means[name] = float(np.mean(vals))
+        sds[name] = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        z[name] = float((original[name] - means[name]) / sds[name]) if sds[name] > 1e-15 else 0.0
+    return original, means, sds, 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 = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent, illusory])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent, axis=0)
+    illusory_centroid = np.mean(illusory, 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()):
+        margins = []
+        labels = []
+        coords = []
+        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)),
+            "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_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()},
+        }
+
+    one_sided = []
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    for name in OBS_NAMES:
+        coherent_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in coherent_rows]))
+        illusory_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in illusory_rows]))
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            one_sided.append(name)
+        if coherent_freq >= 0.75 and illusory_freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "coherent_one_sided_observables": one_sided,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def analyze_sequence(name: str, base: np.ndarray, 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):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        for beta in betas:
+            gaps = beta_replace(base, beta, rep_rng)
+            obs, shuffle_mean, 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_mean": shuffle_mean,
+                    "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)
+    return {
+        "source": {
+            "n_gaps": int(len(base)),
+            "mean": float(np.mean(base)),
+            "variance": float(np.var(base)),
+        },
+        "gate": gate,
+        "classification_all_observables": classify_layers(rows, OBS_NAMES),
+        "classification_one_sided_gated": classify_layers(rows, gate["coherent_one_sided_observables"]),
+        "rows": rows,
+    }
+
+
+def build_sequences(args: argparse.Namespace, rng: np.random.Generator) -> dict[str, np.ndarray]:
+    sequences = {
+        "prime_gaps_first": prime_gap_sequence(args.n_gaps),
+        "logistic_return_intervals": logistic_return_intervals(args.n_gaps, rng),
+    }
+    if args.include_zeta:
+        sequences["zeta_zero_spacings_first"] = zeta_zero_spacings(args.zeta_gaps)
+    return sequences
+
+
+def compact(perimeters: dict) -> dict:
+    out = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        all_cls = data["classification_all_observables"]
+        gated_cls = data["classification_one_sided_gated"]
+        out[name] = {
+            "n_gaps": data["source"]["n_gaps"],
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+            "z_mean_coherent": gate["layers"]["0.000"]["z_mean"],
+            "z_mean_illusory": gate["layers"]["1.000"]["z_mean"],
+            "endpoint_distance_all": all_cls["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": gated_cls["endpoint_distance"],
+            "ambiguous_beta_one_sided_gated": gated_cls["ambiguous_beta"],
+        }
+    return out
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    sequences = build_sequences(args, root_rng)
+    perimeters = {}
+    for name, base in sequences.items():
+        perimeters[name] = analyze_sequence(name, base, args, root_rng)
+
+    output = {
+        "experiment": "semireal_order_denominator_gate",
+        "category": "gate_falsification_semireal",
+        "question": "Does ORDER_DENOMINATOR_GATE survive on non-synthetic / semi-real ordered sequences?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact(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 n one_sided stable0 stable1 dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>28s} "
+            f"{row['n_gaps']:>5d} "
+            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_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("--zeta-gaps", type=int, default=1024)
+    parser.add_argument("--include-zeta", action="store_true")
+    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=202605070923)
+    parser.add_argument("--out", default="tools/data/semireal_order_denominator_gate_20260507_0923.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

exec
/bin/bash -lc "sed -n '34,62p' tools/data/seme.json" in /opt/MM_D-ND
 succeeded in 0ms:
      "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": "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"
    },
    {
      "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",

diff --git a/tools/data/reports/agent_20260507_0923.md b/tools/data/reports/agent_20260507_0923.md
new file mode 100644
index 0000000000000000000000000000000000000000..d7dc7d70d70cbf71dfba4b394064f4bdf842bb15
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0923.md
@@ -0,0 +1,132 @@
+# Agent Report — Semi-Real Order Denominator Gate
+
+timestamp: 2026-05-07 09:23 UTC
+category: gate_falsification_semireal
+verdict: scoped_operator
+observables_registry: 1.0.0-2026-05-06
+observables_used: [SR, SR2, L1, L2, triple_var]
+tool: tools/exp_semireal_order_denominator_gate.py
+data: tools/data/semireal_order_denominator_gate_20260507_0923.json
+seed_check: tools/data/semireal_order_denominator_gate_20260507_0923_seedcheck.json
+
+## Claim Under Test
+
+Verificato dal campo vivo: il cycle 0901 ha nominato `ORDER_DENOMINATOR_GATE`
+come supporto one-sided dell'ordine generato, ma lo ha fatto su perimetri
+sintetici. Il mandato corrente chiede falsificazione su domini non-sintetici o
+semi-reali.
+
+Domanda: se l'endpoint coerente è una sequenza ordinata reale/semi-reale e
+l'endpoint illusorio è una permutazione che preserva la marginale, il supporto
+denominatore resta one-sided o compare un controesempio?
+
+Perimetri:
+
+- `prime_gaps_first`: primi 4096 gap fra primi.
+- `zeta_zero_spacings_first`: primi 512 spacing fra zeri non banali di zeta,
+  calcolati localmente con `mpmath.zetazero`.
+- `logistic_return_intervals`: 4096 intervalli di ritorno a `x > 0.95` nella
+  mappa logistica caotica `x -> 4x(1-x)`.
+
+Gate: osservabile stabile se `abs(original - shuffle_mean) / shuffle_std >= 2`.
+
+## Deposito Numerico
+
+Run principale: `n_replicates=20`, `n_beta=11`, `n_baseline=32`,
+`seed=202605070923`. Seed check: `n_replicates=12`, `n_baseline=24`,
+`seed=202605070924`.
+
+| perimeter | coherent one-sided observables | stable_count coherent | stable_count illusory | endpoint distance gated | beta ambiguous gated |
+|---|---:|---:|---:|---:|---:|
+| prime_gaps_first | SR, L1, triple_var | 3.000 | 0.650 | 3.270 | 0.30 |
+| logistic_return_intervals | [] | 0.200 | 0.100 | 0.000 | [] |
+| zeta_zero_spacings_first | SR, L2 | 2.150 | 0.250 | 2.666 | [] |
+
+Seed check:
+
+| perimeter | coherent one-sided observables | stable_count coherent | stable_count illusory | endpoint distance gated | beta ambiguous gated |
+|---|---:|---:|---:|---:|---:|
+| prime_gaps_first | SR, L1, triple_var | 3.000 | 0.250 | 3.288 | 0.30 |
+| logistic_return_intervals | [] | 0.000 | 0.583 | 0.000 | [] |
+| zeta_zero_spacings_first | SR, L2 | 2.417 | 0.333 | 2.700 | [] |
+
+Endpoint-stable observables: `[]` in all three perimeters in both runs.
+
+## Risultato
+
+1. **The order gate transfers to arithmetic and zeta spacing order.**
+
+   Prime gaps carry one-sided support on `SR`, `L1`, and `triple_var`.
+   Zeta-zero spacings carry one-sided support on `SR` and `L2`. In both cases
+   the illusory endpoint remains weak-denominator under the same marginal.
+
+2. **The logistic return perimeter is the counter-scope.**
+
+   The logistic return sequence is ordered and generated by a deterministic
+   chaotic system, but this canonical observable suite does not read its order
+   as denominator support. The coherent endpoint stable count is `0.200` in the
+   main run and `0.000` in the seed check. The gate does not transfer to this
+   return-time observable.
+
+3. **The transferable object is narrower than "real order".**
+
+   `ORDER_DENOMINATOR_GATE` names order that survives a marginal-preserving
+   shuffle in the canonical gap observables. It does not name every generated
+   sequence. The node regressivo is the observable contract, not the gate
+   threshold: if the order lives in return-time tail structure or symbolic
+   itinerary, `SR/SR2/L1/L2/triple_var` can be blank.
+
+4. **The beta layer is not universal.**
+
+   Prime gaps reproduce beta `0.30` as the ambiguous protocol layer. Zeta has
+   no gated ambiguous beta in this run. Logistic has no gated classifier because
+   the one-sided observable set is empty. This extends 0901: beta `0.30` was a
+   protocol fold in the synthetic matrix, not a cross-domain coordinate.
+
+## Consecutio
+
+`ORDER_DENOMINATOR_GATE` survives as scoped operator:
+
+> In semi-real arithmetic/spectral spacing perimeters, the denominator gate is
+> one-sided support for order against a marginal-preserving shuffle. In
+> logistic return intervals, the canonical gap observables do not carry that
+> support; the gate output is blank rather than false-positive.
+
+Next experiment: do not tune `z_min`. Change the observable perimetro for the
+logistic counter-scope: symbolic itinerary block entropy, return-tail exponent,
+or recurrence-plot diagonal statistics, each with the same original-vs-shuffle
+denominator gate. That tests whether logistic order is absent for this gate or
+only invisible to the current canonical gap suite.
+
+## Self-Audit: 5 Lenti
+
+L1 hard constraint vs bias: no universal claim is made. `endpoint_stable_observables: []`
+is exact for the run perimeters; "weak" means below the declared `abs(z)>=2`
+gate frequency, not numerical zero.
+
+L2 quantity vs ratio: raw stable counts and z means are reported before endpoint
+distances. No percentage drift claim is used.
+
+L3 no silent patching: 0901 claimed transfer on synthetic generated-order
+perimeters. This report narrows the scope after observing the logistic
+counter-perimeter; it does not rescue the original wording.
+
+L4 edge cases: logistic illusory stable_count is `0.583` in the seed check,
+so the claim is not "illusory endpoint absent." It is "no coherent one-sided
+support under the declared frequency rule."
+
+L5 re-discovery vs discovery: prime gap and zeta spacing order-vs-shuffle tests
+sit near known arithmetic/RMT order diagnostics; logistic return intervals sit
+near known recurrence and return-time diagnostics for chaotic maps. No NEW
+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_semireal_order_denominator_gate.py`
+- Verificato: `tools/data/semireal_order_denominator_gate_20260507_0923.json`
+- Verificato: `tools/data/semireal_order_denominator_gate_20260507_0923_seedcheck.json`
+- Inferito: logistic return intervals are a counter-scope for the canonical
+  gap-observable form of `ORDER_DENOMINATOR_GATE`.
diff --git a/tools/exp_semireal_order_denominator_gate.py b/tools/exp_semireal_order_denominator_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..f3da4e51c84f066066367b25fded3ebea3c1c00d
--- /dev/null
+++ b/tools/exp_semireal_order_denominator_gate.py
@@ -0,0 +1,352 @@
+#!/usr/bin/env python3
+"""
+exp_semireal_order_denominator_gate.py
+
+Falsification attempt for ORDER_DENOMINATOR_GATE on non-synthetic / semi-real
+ordered sequences. The coherent endpoint is the observed order of each sequence;
+the illusory endpoint is a marginal-preserving permutation. The same
+original-vs-shuffle denominator gate used in the prior reports is applied to
+canonical observables from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import math
+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 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 sieve_primes_for_count(n_primes: int) -> np.ndarray:
+    if n_primes < 6:
+        limit = 20
+    else:
+        limit = int(n_primes * (math.log(n_primes) + math.log(math.log(n_primes))) * 1.25)
+    while True:
+        sieve = np.ones(limit + 1, dtype=bool)
+        sieve[:2] = False
+        for p in range(2, int(limit**0.5) + 1):
+            if sieve[p]:
+                sieve[p * p : limit + 1 : p] = False
+        primes = np.flatnonzero(sieve)
+        if len(primes) >= n_primes:
+            return primes[:n_primes].astype(float)
+        limit *= 2
+
+
+def prime_gap_sequence(n_gaps: int) -> np.ndarray:
+    primes = sieve_primes_for_count(n_gaps + 1)
+    return normalize(np.diff(primes))
+
+
+def zeta_zero_spacings(n_gaps: int) -> np.ndarray:
+    try:
+        import mpmath as mp
+    except ImportError as exc:
+        raise RuntimeError("mpmath is required for zeta_zero_spacings") from exc
+
+    zeros = np.empty(n_gaps + 1, dtype=float)
+    for i in range(n_gaps + 1):
+        zeros[i] = float(mp.im(mp.zetazero(i + 1)))
+    return normalize(np.diff(zeros))
+
+
+def logistic_return_intervals(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    # Return intervals to a high-density-edge event in the fully chaotic logistic map.
+    threshold = 0.95
+    burn = 2000
+    needed = n_gaps + 1
+    returns: list[int] = []
+    last_hit: int | None = None
+    x = float(rng.random())
+    i = 0
+    max_steps = 50_000_000
+    while len(returns) < needed and i < max_steps:
+        x = 4.0 * x * (1.0 - x)
+        if i >= burn and x > threshold:
+            if last_hit is not None:
+                returns.append(i - last_hit)
+            last_hit = i
+        i += 1
+    if len(returns) < needed:
+        raise RuntimeError(f"logistic generator produced {len(returns)} intervals, need {needed}")
+    return normalize(np.array(returns[:n_gaps], dtype=float))
+
+
+def beta_replace(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        return base.copy()
+    if beta >= 1.0:
+        return illusory
+    out = base.copy()
+    mask = rng.random(len(base)) < beta
+    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], dict[str, float]]:
+    original = compute_canonical(gaps)
+    baseline = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline[name].append(obs[name])
+
+    means = {}
+    sds = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline[name], dtype=float)
+        means[name] = float(np.mean(vals))
+        sds[name] = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        z[name] = float((original[name] - means[name]) / sds[name]) if sds[name] > 1e-15 else 0.0
+    return original, means, sds, 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 = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent, illusory])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent, axis=0)
+    illusory_centroid = np.mean(illusory, 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()):
+        margins = []
+        labels = []
+        coords = []
+        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)),
+            "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_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()},
+        }
+
+    one_sided = []
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    for name in OBS_NAMES:
+        coherent_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in coherent_rows]))
+        illusory_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in illusory_rows]))
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            one_sided.append(name)
+        if coherent_freq >= 0.75 and illusory_freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "coherent_one_sided_observables": one_sided,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def analyze_sequence(name: str, base: np.ndarray, 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):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        for beta in betas:
+            gaps = beta_replace(base, beta, rep_rng)
+            obs, shuffle_mean, 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_mean": shuffle_mean,
+                    "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)
+    return {
+        "source": {
+            "n_gaps": int(len(base)),
+            "mean": float(np.mean(base)),
+            "variance": float(np.var(base)),
+        },
+        "gate": gate,
+        "classification_all_observables": classify_layers(rows, OBS_NAMES),
+        "classification_one_sided_gated": classify_layers(rows, gate["coherent_one_sided_observables"]),
+        "rows": rows,
+    }
+
+
+def build_sequences(args: argparse.Namespace, rng: np.random.Generator) -> dict[str, np.ndarray]:
+    sequences = {
+        "prime_gaps_first": prime_gap_sequence(args.n_gaps),
+        "logistic_return_intervals": logistic_return_intervals(args.n_gaps, rng),
+    }
+    if args.include_zeta:
+        sequences["zeta_zero_spacings_first"] = zeta_zero_spacings(args.zeta_gaps)
+    return sequences
+
+
+def compact(perimeters: dict) -> dict:
+    out = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        all_cls = data["classification_all_observables"]
+        gated_cls = data["classification_one_sided_gated"]
+        out[name] = {
+            "n_gaps": data["source"]["n_gaps"],
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+            "z_mean_coherent": gate["layers"]["0.000"]["z_mean"],
+            "z_mean_illusory": gate["layers"]["1.000"]["z_mean"],
+            "endpoint_distance_all": all_cls["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": gated_cls["endpoint_distance"],
+            "ambiguous_beta_one_sided_gated": gated_cls["ambiguous_beta"],
+        }
+    return out
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    sequences = build_sequences(args, root_rng)
+    perimeters = {}
+    for name, base in sequences.items():
+        perimeters[name] = analyze_sequence(name, base, args, root_rng)
+
+    output = {
+        "experiment": "semireal_order_denominator_gate",
+        "category": "gate_falsification_semireal",
+        "question": "Does ORDER_DENOMINATOR_GATE survive on non-synthetic / semi-real ordered sequences?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact(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 n one_sided stable0 stable1 dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>28s} "
+            f"{row['n_gaps']:>5d} "
+            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_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("--zeta-gaps", type=int, default=1024)
+    parser.add_argument("--include-zeta", action="store_true")
+    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=202605070923)
+    parser.add_argument("--out", default="tools/data/semireal_order_denominator_gate_20260507_0923.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_0923.md b/tools/data/reports/agent_20260507_0923.md
new file mode 100644
index 0000000000000000000000000000000000000000..d7dc7d70d70cbf71dfba4b394064f4bdf842bb15
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0923.md
@@ -0,0 +1,132 @@
+# Agent Report — Semi-Real Order Denominator Gate
+
+timestamp: 2026-05-07 09:23 UTC
+category: gate_falsification_semireal
+verdict: scoped_operator
+observables_registry: 1.0.0-2026-05-06
+observables_used: [SR, SR2, L1, L2, triple_var]
+tool: tools/exp_semireal_order_denominator_gate.py
+data: tools/data/semireal_order_denominator_gate_20260507_0923.json
+seed_check: tools/data/semireal_order_denominator_gate_20260507_0923_seedcheck.json
+
+## Claim Under Test
+
+Verificato dal campo vivo: il cycle 0901 ha nominato `ORDER_DENOMINATOR_GATE`
+come supporto one-sided dell'ordine generato, ma lo ha fatto su perimetri
+sintetici. Il mandato corrente chiede falsificazione su domini non-sintetici o
+semi-reali.
+
+Domanda: se l'endpoint coerente è una sequenza ordinata reale/semi-reale e
+l'endpoint illusorio è una permutazione che preserva la marginale, il supporto
+denominatore resta one-sided o compare un controesempio?
+
+Perimetri:
+
+- `prime_gaps_first`: primi 4096 gap fra primi.
+- `zeta_zero_spacings_first`: primi 512 spacing fra zeri non banali di zeta,
+  calcolati localmente con `mpmath.zetazero`.
+- `logistic_return_intervals`: 4096 intervalli di ritorno a `x > 0.95` nella
+  mappa logistica caotica `x -> 4x(1-x)`.
+
+Gate: osservabile stabile se `abs(original - shuffle_mean) / shuffle_std >= 2`.
+
+## Deposito Numerico
+
+Run principale: `n_replicates=20`, `n_beta=11`, `n_baseline=32`,
+`seed=202605070923`. Seed check: `n_replicates=12`, `n_baseline=24`,
+`seed=202605070924`.
+
+| perimeter | coherent one-sided observables | stable_count coherent | stable_count illusory | endpoint distance gated | beta ambiguous gated |
+|---|---:|---:|---:|---:|---:|
+| prime_gaps_first | SR, L1, triple_var | 3.000 | 0.650 | 3.270 | 0.30 |
+| logistic_return_intervals | [] | 0.200 | 0.100 | 0.000 | [] |
+| zeta_zero_spacings_first | SR, L2 | 2.150 | 0.250 | 2.666 | [] |
+
+Seed check:
+
+| perimeter | coherent one-sided observables | stable_count coherent | stable_count illusory | endpoint distance gated | beta ambiguous gated |
+|---|---:|---:|---:|---:|---:|
+| prime_gaps_first | SR, L1, triple_var | 3.000 | 0.250 | 3.288 | 0.30 |
+| logistic_return_intervals | [] | 0.000 | 0.583 | 0.000 | [] |
+| zeta_zero_spacings_first | SR, L2 | 2.417 | 0.333 | 2.700 | [] |
+
+Endpoint-stable observables: `[]` in all three perimeters in both runs.
+
+## Risultato
+
+1. **The order gate transfers to arithmetic and zeta spacing order.**
+
+   Prime gaps carry one-sided support on `SR`, `L1`, and `triple_var`.
+   Zeta-zero spacings carry one-sided support on `SR` and `L2`. In both cases
+   the illusory endpoint remains weak-denominator under the same marginal.
+
+2. **The logistic return perimeter is the counter-scope.**
+
+   The logistic return sequence is ordered and generated by a deterministic
+   chaotic system, but this canonical observable suite does not read its order
+   as denominator support. The coherent endpoint stable count is `0.200` in the
+   main run and `0.000` in the seed check. The gate does not transfer to this
+   return-time observable.
+
+3. **The transferable object is narrower than "real order".**
+
+   `ORDER_DENOMINATOR_GATE` names order that survives a marginal-preserving
+   shuffle in the canonical gap observables. It does not name every generated
+   sequence. The node regressivo is the observable contract, not the gate
+   threshold: if the order lives in return-time tail structure or symbolic
+   itinerary, `SR/SR2/L1/L2/triple_var` can be blank.
+
+4. **The beta layer is not universal.**
+
+   Prime gaps reproduce beta `0.30` as the ambiguous protocol layer. Zeta has
+   no gated ambiguous beta in this run. Logistic has no gated classifier because
+   the one-sided observable set is empty. This extends 0901: beta `0.30` was a
+   protocol fold in the synthetic matrix, not a cross-domain coordinate.
+
+## Consecutio
+
+`ORDER_DENOMINATOR_GATE` survives as scoped operator:
+
+> In semi-real arithmetic/spectral spacing perimeters, the denominator gate is
+> one-sided support for order against a marginal-preserving shuffle. In
+> logistic return intervals, the canonical gap observables do not carry that
+> support; the gate output is blank rather than false-positive.
+
+Next experiment: do not tune `z_min`. Change the observable perimetro for the
+logistic counter-scope: symbolic itinerary block entropy, return-tail exponent,
+or recurrence-plot diagonal statistics, each with the same original-vs-shuffle
+denominator gate. That tests whether logistic order is absent for this gate or
+only invisible to the current canonical gap suite.
+
+## Self-Audit: 5 Lenti
+
+L1 hard constraint vs bias: no universal claim is made. `endpoint_stable_observables: []`
+is exact for the run perimeters; "weak" means below the declared `abs(z)>=2`
+gate frequency, not numerical zero.
+
+L2 quantity vs ratio: raw stable counts and z means are reported before endpoint
+distances. No percentage drift claim is used.
+
+L3 no silent patching: 0901 claimed transfer on synthetic generated-order
+perimeters. This report narrows the scope after observing the logistic
+counter-perimeter; it does not rescue the original wording.
+
+L4 edge cases: logistic illusory stable_count is `0.583` in the seed check,
+so the claim is not "illusory endpoint absent." It is "no coherent one-sided
+support under the declared frequency rule."
+
+L5 re-discovery vs discovery: prime gap and zeta spacing order-vs-shuffle tests
+sit near known arithmetic/RMT order diagnostics; logistic return intervals sit
+near known recurrence and return-time diagnostics for chaotic maps. No NEW
+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_semireal_order_denominator_gate.py`
+- Verificato: `tools/data/semireal_order_denominator_gate_20260507_0923.json`
+- Verificato: `tools/data/semireal_order_denominator_gate_20260507_0923_seedcheck.json`
+- Inferito: logistic return intervals are a counter-scope for the canonical
+  gap-observable form of `ORDER_DENOMINATOR_GATE`.
diff --git a/tools/data/seme.json b/tools/data/seme.json
index cfd4593825fee57518a2e251e74292bb104253d9..d236f9223863b878fed1dd46bd69c22877e12b4a
--- a/tools/data/seme.json
+++ b/tools/data/seme.json
@@ -38,12 +38,12 @@
     {
       "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.",
+      "claim": "Il denominator gate trasferisce come supporto one-sided dell'ordine quando l'ordine e visibile agli osservabili canonici del perimetro, non come endpoint-stable support a due poli. Nel perimetro sintetico agent_20260507_0901, 4/4 domini non-BOUNDARY hanno endpoint_stable_observables=[] e polo coerente stable_count 3.0-5.0. Nel perimetro semi-reale agent_20260507_0923, primi e zeta trasferiscono (primi: SR,L1,triple_var; zeta: SR,L2), ma logistic_return_intervals e blank: stable_count coerente 0.0-0.2. La beta 0.30 e coordinata del protocollo quando compare, non coordinata universale.",
       "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",
+      "origine": "cycle agent_20260507_0901 + agent_20260507_0923: transfer matrix sintetica e falsificazione semi-reale su primi, zeta, logistic returns",
       "added_at": "2026-05-07T09:01:00+00:00"
     },
     {
@@ -336,4 +336,4 @@
   "_seed_history": {
     "trajectory_apply_20260507_092332": "applied modify_seme from 20260507_0901: direzione → 'Falsificare ORDER_DENOMINATOR_GATE su domini non-sintetici o semi-reali: applicare il gate one-sided...'"
   }
-}
\ No newline at end of file
+}
diff --git a/tools/exp_semireal_order_denominator_gate.py b/tools/exp_semireal_order_denominator_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..f3da4e51c84f066066367b25fded3ebea3c1c00d
--- /dev/null
+++ b/tools/exp_semireal_order_denominator_gate.py
@@ -0,0 +1,352 @@
+#!/usr/bin/env python3
+"""
+exp_semireal_order_denominator_gate.py
+
+Falsification attempt for ORDER_DENOMINATOR_GATE on non-synthetic / semi-real
+ordered sequences. The coherent endpoint is the observed order of each sequence;
+the illusory endpoint is a marginal-preserving permutation. The same
+original-vs-shuffle denominator gate used in the prior reports is applied to
+canonical observables from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import math
+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 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 sieve_primes_for_count(n_primes: int) -> np.ndarray:
+    if n_primes < 6:
+        limit = 20
+    else:
+        limit = int(n_primes * (math.log(n_primes) + math.log(math.log(n_primes))) * 1.25)
+    while True:
+        sieve = np.ones(limit + 1, dtype=bool)
+        sieve[:2] = False
+        for p in range(2, int(limit**0.5) + 1):
+            if sieve[p]:
+                sieve[p * p : limit + 1 : p] = False
+        primes = np.flatnonzero(sieve)
+        if len(primes) >= n_primes:
+            return primes[:n_primes].astype(float)
+        limit *= 2
+
+
+def prime_gap_sequence(n_gaps: int) -> np.ndarray:
+    primes = sieve_primes_for_count(n_gaps + 1)
+    return normalize(np.diff(primes))
+
+
+def zeta_zero_spacings(n_gaps: int) -> np.ndarray:
+    try:
+        import mpmath as mp
+    except ImportError as exc:
+        raise RuntimeError("mpmath is required for zeta_zero_spacings") from exc
+
+    zeros = np.empty(n_gaps + 1, dtype=float)
+    for i in range(n_gaps + 1):
+        zeros[i] = float(mp.im(mp.zetazero(i + 1)))
+    return normalize(np.diff(zeros))
+
+
+def logistic_return_intervals(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    # Return intervals to a high-density-edge event in the fully chaotic logistic map.
+    threshold = 0.95
+    burn = 2000
+    needed = n_gaps + 1
+    returns: list[int] = []
+    last_hit: int | None = None
+    x = float(rng.random())
+    i = 0
+    max_steps = 50_000_000
+    while len(returns) < needed and i < max_steps:
+        x = 4.0 * x * (1.0 - x)
+        if i >= burn and x > threshold:
+            if last_hit is not None:
+                returns.append(i - last_hit)
+            last_hit = i
+        i += 1
+    if len(returns) < needed:
+        raise RuntimeError(f"logistic generator produced {len(returns)} intervals, need {needed}")
+    return normalize(np.array(returns[:n_gaps], dtype=float))
+
+
+def beta_replace(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        return base.copy()
+    if beta >= 1.0:
+        return illusory
+    out = base.copy()
+    mask = rng.random(len(base)) < beta
+    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], dict[str, float]]:
+    original = compute_canonical(gaps)
+    baseline = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline[name].append(obs[name])
+
+    means = {}
+    sds = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline[name], dtype=float)
+        means[name] = float(np.mean(vals))
+        sds[name] = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        z[name] = float((original[name] - means[name]) / sds[name]) if sds[name] > 1e-15 else 0.0
+    return original, means, sds, 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 = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent, illusory])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent, axis=0)
+    illusory_centroid = np.mean(illusory, 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()):
+        margins = []
+        labels = []
+        coords = []
+        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)),
+            "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_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()},
+        }
+
+    one_sided = []
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    for name in OBS_NAMES:
+        coherent_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in coherent_rows]))
+        illusory_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in illusory_rows]))
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            one_sided.append(name)
+        if coherent_freq >= 0.75 and illusory_freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "coherent_one_sided_observables": one_sided,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def analyze_sequence(name: str, base: np.ndarray, 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):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        for beta in betas:
+            gaps = beta_replace(base, beta, rep_rng)
+            obs, shuffle_mean, 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_mean": shuffle_mean,
+                    "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)
+    return {
+        "source": {
+            "n_gaps": int(len(base)),
+            "mean": float(np.mean(base)),
+            "variance": float(np.var(base)),
+        },
+        "gate": gate,
+        "classification_all_observables": classify_layers(rows, OBS_NAMES),
+        "classification_one_sided_gated": classify_layers(rows, gate["coherent_one_sided_observables"]),
+        "rows": rows,
+    }
+
+
+def build_sequences(args: argparse.Namespace, rng: np.random.Generator) -> dict[str, np.ndarray]:
+    sequences = {
+        "prime_gaps_first": prime_gap_sequence(args.n_gaps),
+        "logistic_return_intervals": logistic_return_intervals(args.n_gaps, rng),
+    }
+    if args.include_zeta:
+        sequences["zeta_zero_spacings_first"] = zeta_zero_spacings(args.zeta_gaps)
+    return sequences
+
+
+def compact(perimeters: dict) -> dict:
+    out = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        all_cls = data["classification_all_observables"]
+        gated_cls = data["classification_one_sided_gated"]
+        out[name] = {
+            "n_gaps": data["source"]["n_gaps"],
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+            "z_mean_coherent": gate["layers"]["0.000"]["z_mean"],
+            "z_mean_illusory": gate["layers"]["1.000"]["z_mean"],
+            "endpoint_distance_all": all_cls["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": gated_cls["endpoint_distance"],
+            "ambiguous_beta_one_sided_gated": gated_cls["ambiguous_beta"],
+        }
+    return out
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    sequences = build_sequences(args, root_rng)
+    perimeters = {}
+    for name, base in sequences.items():
+        perimeters[name] = analyze_sequence(name, base, args, root_rng)
+
+    output = {
+        "experiment": "semireal_order_denominator_gate",
+        "category": "gate_falsification_semireal",
+        "question": "Does ORDER_DENOMINATOR_GATE survive on non-synthetic / semi-real ordered sequences?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact(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 n one_sided stable0 stable1 dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>28s} "
+            f"{row['n_gaps']:>5d} "
+            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_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("--zeta-gaps", type=int, default=1024)
+    parser.add_argument("--include-zeta", action="store_true")
+    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=202605070923)
+    parser.add_argument("--out", default="tools/data/semireal_order_denominator_gate_20260507_0923.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

diff --git a/tools/data/reports/agent_20260507_0923.md b/tools/data/reports/agent_20260507_0923.md
new file mode 100644
index 0000000000000000000000000000000000000000..d7dc7d70d70cbf71dfba4b394064f4bdf842bb15
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0923.md
@@ -0,0 +1,132 @@
+# Agent Report — Semi-Real Order Denominator Gate
+
+timestamp: 2026-05-07 09:23 UTC
+category: gate_falsification_semireal
+verdict: scoped_operator
+observables_registry: 1.0.0-2026-05-06
+observables_used: [SR, SR2, L1, L2, triple_var]
+tool: tools/exp_semireal_order_denominator_gate.py
+data: tools/data/semireal_order_denominator_gate_20260507_0923.json
+seed_check: tools/data/semireal_order_denominator_gate_20260507_0923_seedcheck.json
+
+## Claim Under Test
+
+Verificato dal campo vivo: il cycle 0901 ha nominato `ORDER_DENOMINATOR_GATE`
+come supporto one-sided dell'ordine generato, ma lo ha fatto su perimetri
+sintetici. Il mandato corrente chiede falsificazione su domini non-sintetici o
+semi-reali.
+
+Domanda: se l'endpoint coerente è una sequenza ordinata reale/semi-reale e
+l'endpoint illusorio è una permutazione che preserva la marginale, il supporto
+denominatore resta one-sided o compare un controesempio?
+
+Perimetri:
+
+- `prime_gaps_first`: primi 4096 gap fra primi.
+- `zeta_zero_spacings_first`: primi 512 spacing fra zeri non banali di zeta,
+  calcolati localmente con `mpmath.zetazero`.
+- `logistic_return_intervals`: 4096 intervalli di ritorno a `x > 0.95` nella
+  mappa logistica caotica `x -> 4x(1-x)`.
+
+Gate: osservabile stabile se `abs(original - shuffle_mean) / shuffle_std >= 2`.
+
+## Deposito Numerico
+
+Run principale: `n_replicates=20`, `n_beta=11`, `n_baseline=32`,
+`seed=202605070923`. Seed check: `n_replicates=12`, `n_baseline=24`,
+`seed=202605070924`.
+
+| perimeter | coherent one-sided observables | stable_count coherent | stable_count illusory | endpoint distance gated | beta ambiguous gated |
+|---|---:|---:|---:|---:|---:|
+| prime_gaps_first | SR, L1, triple_var | 3.000 | 0.650 | 3.270 | 0.30 |
+| logistic_return_intervals | [] | 0.200 | 0.100 | 0.000 | [] |
+| zeta_zero_spacings_first | SR, L2 | 2.150 | 0.250 | 2.666 | [] |
+
+Seed check:
+
+| perimeter | coherent one-sided observables | stable_count coherent | stable_count illusory | endpoint distance gated | beta ambiguous gated |
+|---|---:|---:|---:|---:|---:|
+| prime_gaps_first | SR, L1, triple_var | 3.000 | 0.250 | 3.288 | 0.30 |
+| logistic_return_intervals | [] | 0.000 | 0.583 | 0.000 | [] |
+| zeta_zero_spacings_first | SR, L2 | 2.417 | 0.333 | 2.700 | [] |
+
+Endpoint-stable observables: `[]` in all three perimeters in both runs.
+
+## Risultato
+
+1. **The order gate transfers to arithmetic and zeta spacing order.**
+
+   Prime gaps carry one-sided support on `SR`, `L1`, and `triple_var`.
+   Zeta-zero spacings carry one-sided support on `SR` and `L2`. In both cases
+   the illusory endpoint remains weak-denominator under the same marginal.
+
+2. **The logistic return perimeter is the counter-scope.**
+
+   The logistic return sequence is ordered and generated by a deterministic
+   chaotic system, but this canonical observable suite does not read its order
+   as denominator support. The coherent endpoint stable count is `0.200` in the
+   main run and `0.000` in the seed check. The gate does not transfer to this
+   return-time observable.
+
+3. **The transferable object is narrower than "real order".**
+
+   `ORDER_DENOMINATOR_GATE` names order that survives a marginal-preserving
+   shuffle in the canonical gap observables. It does not name every generated
+   sequence. The node regressivo is the observable contract, not the gate
+   threshold: if the order lives in return-time tail structure or symbolic
+   itinerary, `SR/SR2/L1/L2/triple_var` can be blank.
+
+4. **The beta layer is not universal.**
+
+   Prime gaps reproduce beta `0.30` as the ambiguous protocol layer. Zeta has
+   no gated ambiguous beta in this run. Logistic has no gated classifier because
+   the one-sided observable set is empty. This extends 0901: beta `0.30` was a
+   protocol fold in the synthetic matrix, not a cross-domain coordinate.
+
+## Consecutio
+
+`ORDER_DENOMINATOR_GATE` survives as scoped operator:
+
+> In semi-real arithmetic/spectral spacing perimeters, the denominator gate is
+> one-sided support for order against a marginal-preserving shuffle. In
+> logistic return intervals, the canonical gap observables do not carry that
+> support; the gate output is blank rather than false-positive.
+
+Next experiment: do not tune `z_min`. Change the observable perimetro for the
+logistic counter-scope: symbolic itinerary block entropy, return-tail exponent,
+or recurrence-plot diagonal statistics, each with the same original-vs-shuffle
+denominator gate. That tests whether logistic order is absent for this gate or
+only invisible to the current canonical gap suite.
+
+## Self-Audit: 5 Lenti
+
+L1 hard constraint vs bias: no universal claim is made. `endpoint_stable_observables: []`
+is exact for the run perimeters; "weak" means below the declared `abs(z)>=2`
+gate frequency, not numerical zero.
+
+L2 quantity vs ratio: raw stable counts and z means are reported before endpoint
+distances. No percentage drift claim is used.
+
+L3 no silent patching: 0901 claimed transfer on synthetic generated-order
+perimeters. This report narrows the scope after observing the logistic
+counter-perimeter; it does not rescue the original wording.
+
+L4 edge cases: logistic illusory stable_count is `0.583` in the seed check,
+so the claim is not "illusory endpoint absent." It is "no coherent one-sided
+support under the declared frequency rule."
+
+L5 re-discovery vs discovery: prime gap and zeta spacing order-vs-shuffle tests
+sit near known arithmetic/RMT order diagnostics; logistic return intervals sit
+near known recurrence and return-time diagnostics for chaotic maps. No NEW
+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_semireal_order_denominator_gate.py`
+- Verificato: `tools/data/semireal_order_denominator_gate_20260507_0923.json`
+- Verificato: `tools/data/semireal_order_denominator_gate_20260507_0923_seedcheck.json`
+- Inferito: logistic return intervals are a counter-scope for the canonical
+  gap-observable form of `ORDER_DENOMINATOR_GATE`.
diff --git a/tools/data/seme.json b/tools/data/seme.json
index cfd4593825fee57518a2e251e74292bb104253d9..d236f9223863b878fed1dd46bd69c22877e12b4a
--- a/tools/data/seme.json
+++ b/tools/data/seme.json
@@ -38,12 +38,12 @@
     {
       "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.",
+      "claim": "Il denominator gate trasferisce come supporto one-sided dell'ordine quando l'ordine e visibile agli osservabili canonici del perimetro, non come endpoint-stable support a due poli. Nel perimetro sintetico agent_20260507_0901, 4/4 domini non-BOUNDARY hanno endpoint_stable_observables=[] e polo coerente stable_count 3.0-5.0. Nel perimetro semi-reale agent_20260507_0923, primi e zeta trasferiscono (primi: SR,L1,triple_var; zeta: SR,L2), ma logistic_return_intervals e blank: stable_count coerente 0.0-0.2. La beta 0.30 e coordinata del protocollo quando compare, non coordinata universale.",
       "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",
+      "origine": "cycle agent_20260507_0901 + agent_20260507_0923: transfer matrix sintetica e falsificazione semi-reale su primi, zeta, logistic returns",
       "added_at": "2026-05-07T09:01:00+00:00"
     },
     {
@@ -336,4 +336,4 @@
   "_seed_history": {
     "trajectory_apply_20260507_092332": "applied modify_seme from 20260507_0901: direzione → 'Falsificare ORDER_DENOMINATOR_GATE su domini non-sintetici o semi-reali: applicare il gate one-sided...'"
   }
-}
\ No newline at end of file
+}
diff --git a/tools/exp_semireal_order_denominator_gate.py b/tools/exp_semireal_order_denominator_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..f3da4e51c84f066066367b25fded3ebea3c1c00d
--- /dev/null
+++ b/tools/exp_semireal_order_denominator_gate.py
@@ -0,0 +1,352 @@
+#!/usr/bin/env python3
+"""
+exp_semireal_order_denominator_gate.py
+
+Falsification attempt for ORDER_DENOMINATOR_GATE on non-synthetic / semi-real
+ordered sequences. The coherent endpoint is the observed order of each sequence;
+the illusory endpoint is a marginal-preserving permutation. The same
+original-vs-shuffle denominator gate used in the prior reports is applied to
+canonical observables from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import math
+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 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 sieve_primes_for_count(n_primes: int) -> np.ndarray:
+    if n_primes < 6:
+        limit = 20
+    else:
+        limit = int(n_primes * (math.log(n_primes) + math.log(math.log(n_primes))) * 1.25)
+    while True:
+        sieve = np.ones(limit + 1, dtype=bool)
+        sieve[:2] = False
+        for p in range(2, int(limit**0.5) + 1):
+            if sieve[p]:
+                sieve[p * p : limit + 1 : p] = False
+        primes = np.flatnonzero(sieve)
+        if len(primes) >= n_primes:
+            return primes[:n_primes].astype(float)
+        limit *= 2
+
+
+def prime_gap_sequence(n_gaps: int) -> np.ndarray:
+    primes = sieve_primes_for_count(n_gaps + 1)
+    return normalize(np.diff(primes))
+
+
+def zeta_zero_spacings(n_gaps: int) -> np.ndarray:
+    try:
+        import mpmath as mp
+    except ImportError as exc:
+        raise RuntimeError("mpmath is required for zeta_zero_spacings") from exc
+
+    zeros = np.empty(n_gaps + 1, dtype=float)
+    for i in range(n_gaps + 1):
+        zeros[i] = float(mp.im(mp.zetazero(i + 1)))
+    return normalize(np.diff(zeros))
+
+
+def logistic_return_intervals(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    # Return intervals to a high-density-edge event in the fully chaotic logistic map.
+    threshold = 0.95
+    burn = 2000
+    needed = n_gaps + 1
+    returns: list[int] = []
+    last_hit: int | None = None
+    x = float(rng.random())
+    i = 0
+    max_steps = 50_000_000
+    while len(returns) < needed and i < max_steps:
+        x = 4.0 * x * (1.0 - x)
+        if i >= burn and x > threshold:
+            if last_hit is not None:
+                returns.append(i - last_hit)
+            last_hit = i
+        i += 1
+    if len(returns) < needed:
+        raise RuntimeError(f"logistic generator produced {len(returns)} intervals, need {needed}")
+    return normalize(np.array(returns[:n_gaps], dtype=float))
+
+
+def beta_replace(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        return base.copy()
+    if beta >= 1.0:
+        return illusory
+    out = base.copy()
+    mask = rng.random(len(base)) < beta
+    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], dict[str, float]]:
+    original = compute_canonical(gaps)
+    baseline = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline[name].append(obs[name])
+
+    means = {}
+    sds = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline[name], dtype=float)
+        means[name] = float(np.mean(vals))
+        sds[name] = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        z[name] = float((original[name] - means[name]) / sds[name]) if sds[name] > 1e-15 else 0.0
+    return original, means, sds, 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 = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent, illusory])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent, axis=0)
+    illusory_centroid = np.mean(illusory, 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()):
+        margins = []
+        labels = []
+        coords = []
+        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)),
+            "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_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()},
+        }
+
+    one_sided = []
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    for name in OBS_NAMES:
+        coherent_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in coherent_rows]))
+        illusory_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in illusory_rows]))
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            one_sided.append(name)
+        if coherent_freq >= 0.75 and illusory_freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "coherent_one_sided_observables": one_sided,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def analyze_sequence(name: str, base: np.ndarray, 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):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        for beta in betas:
+            gaps = beta_replace(base, beta, rep_rng)
+            obs, shuffle_mean, 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_mean": shuffle_mean,
+                    "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)
+    return {
+        "source": {
+            "n_gaps": int(len(base)),
+            "mean": float(np.mean(base)),
+            "variance": float(np.var(base)),
+        },
+        "gate": gate,
+        "classification_all_observables": classify_layers(rows, OBS_NAMES),
+        "classification_one_sided_gated": classify_layers(rows, gate["coherent_one_sided_observables"]),
+        "rows": rows,
+    }
+
+
+def build_sequences(args: argparse.Namespace, rng: np.random.Generator) -> dict[str, np.ndarray]:
+    sequences = {
+        "prime_gaps_first": prime_gap_sequence(args.n_gaps),
+        "logistic_return_intervals": logistic_return_intervals(args.n_gaps, rng),
+    }
+    if args.include_zeta:
+        sequences["zeta_zero_spacings_first"] = zeta_zero_spacings(args.zeta_gaps)
+    return sequences
+
+
+def compact(perimeters: dict) -> dict:
+    out = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        all_cls = data["classification_all_observables"]
+        gated_cls = data["classification_one_sided_gated"]
+        out[name] = {
+            "n_gaps": data["source"]["n_gaps"],
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+            "z_mean_coherent": gate["layers"]["0.000"]["z_mean"],
+            "z_mean_illusory": gate["layers"]["1.000"]["z_mean"],
+            "endpoint_distance_all": all_cls["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": gated_cls["endpoint_distance"],
+            "ambiguous_beta_one_sided_gated": gated_cls["ambiguous_beta"],
+        }
+    return out
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    sequences = build_sequences(args, root_rng)
+    perimeters = {}
+    for name, base in sequences.items():
+        perimeters[name] = analyze_sequence(name, base, args, root_rng)
+
+    output = {
+        "experiment": "semireal_order_denominator_gate",
+        "category": "gate_falsification_semireal",
+        "question": "Does ORDER_DENOMINATOR_GATE survive on non-synthetic / semi-real ordered sequences?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact(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 n one_sided stable0 stable1 dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>28s} "
+            f"{row['n_gaps']:>5d} "
+            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_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("--zeta-gaps", type=int, default=1024)
+    parser.add_argument("--include-zeta", action="store_true")
+    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=202605070923)
+    parser.add_argument("--out", default="tools/data/semireal_order_denominator_gate_20260507_0923.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

exec
/bin/bash -lc 'python tools/lab_falsifier.py tools/data/reports/agent_20260507_0923.md' in /opt/MM_D-ND
 exited 2 in 0ms:
usage: lab_falsifier.py [-h] --report REPORT --output OUTPUT
                        [--timeout TIMEOUT]
lab_falsifier.py: error: the following arguments are required: --report, --output

exec
/bin/bash -lc 'python -m json.tool tools/data/seme.json >/tmp/seme_check.json' in /opt/MM_D-ND
 succeeded in 0ms:
diff --git a/tools/data/reports/agent_20260507_0923.md b/tools/data/reports/agent_20260507_0923.md
new file mode 100644
index 0000000000000000000000000000000000000000..d7dc7d70d70cbf71dfba4b394064f4bdf842bb15
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0923.md
@@ -0,0 +1,132 @@
+# Agent Report — Semi-Real Order Denominator Gate
+
+timestamp: 2026-05-07 09:23 UTC
+category: gate_falsification_semireal
+verdict: scoped_operator
+observables_registry: 1.0.0-2026-05-06
+observables_used: [SR, SR2, L1, L2, triple_var]
+tool: tools/exp_semireal_order_denominator_gate.py
+data: tools/data/semireal_order_denominator_gate_20260507_0923.json
+seed_check: tools/data/semireal_order_denominator_gate_20260507_0923_seedcheck.json
+
+## Claim Under Test
+
+Verificato dal campo vivo: il cycle 0901 ha nominato `ORDER_DENOMINATOR_GATE`
+come supporto one-sided dell'ordine generato, ma lo ha fatto su perimetri
+sintetici. Il mandato corrente chiede falsificazione su domini non-sintetici o
+semi-reali.
+
+Domanda: se l'endpoint coerente è una sequenza ordinata reale/semi-reale e
+l'endpoint illusorio è una permutazione che preserva la marginale, il supporto
+denominatore resta one-sided o compare un controesempio?
+
+Perimetri:
+
+- `prime_gaps_first`: primi 4096 gap fra primi.
+- `zeta_zero_spacings_first`: primi 512 spacing fra zeri non banali di zeta,
+  calcolati localmente con `mpmath.zetazero`.
+- `logistic_return_intervals`: 4096 intervalli di ritorno a `x > 0.95` nella
+  mappa logistica caotica `x -> 4x(1-x)`.
+
+Gate: osservabile stabile se `abs(original - shuffle_mean) / shuffle_std >= 2`.
+
+## Deposito Numerico
+
+Run principale: `n_replicates=20`, `n_beta=11`, `n_baseline=32`,
+`seed=202605070923`. Seed check: `n_replicates=12`, `n_baseline=24`,
+`seed=202605070924`.
+
+| perimeter | coherent one-sided observables | stable_count coherent | stable_count illusory | endpoint distance gated | beta ambiguous gated |
+|---|---:|---:|---:|---:|---:|
+| prime_gaps_first | SR, L1, triple_var | 3.000 | 0.650 | 3.270 | 0.30 |
+| logistic_return_intervals | [] | 0.200 | 0.100 | 0.000 | [] |
+| zeta_zero_spacings_first | SR, L2 | 2.150 | 0.250 | 2.666 | [] |
+
+Seed check:
+
+| perimeter | coherent one-sided observables | stable_count coherent | stable_count illusory | endpoint distance gated | beta ambiguous gated |
+|---|---:|---:|---:|---:|---:|
+| prime_gaps_first | SR, L1, triple_var | 3.000 | 0.250 | 3.288 | 0.30 |
+| logistic_return_intervals | [] | 0.000 | 0.583 | 0.000 | [] |
+| zeta_zero_spacings_first | SR, L2 | 2.417 | 0.333 | 2.700 | [] |
+
+Endpoint-stable observables: `[]` in all three perimeters in both runs.
+
+## Risultato
+
+1. **The order gate transfers to arithmetic and zeta spacing order.**
+
+   Prime gaps carry one-sided support on `SR`, `L1`, and `triple_var`.
+   Zeta-zero spacings carry one-sided support on `SR` and `L2`. In both cases
+   the illusory endpoint remains weak-denominator under the same marginal.
+
+2. **The logistic return perimeter is the counter-scope.**
+
+   The logistic return sequence is ordered and generated by a deterministic
+   chaotic system, but this canonical observable suite does not read its order
+   as denominator support. The coherent endpoint stable count is `0.200` in the
+   main run and `0.000` in the seed check. The gate does not transfer to this
+   return-time observable.
+
+3. **The transferable object is narrower than "real order".**
+
+   `ORDER_DENOMINATOR_GATE` names order that survives a marginal-preserving
+   shuffle in the canonical gap observables. It does not name every generated
+   sequence. The node regressivo is the observable contract, not the gate
+   threshold: if the order lives in return-time tail structure or symbolic
+   itinerary, `SR/SR2/L1/L2/triple_var` can be blank.
+
+4. **The beta layer is not universal.**
+
+   Prime gaps reproduce beta `0.30` as the ambiguous protocol layer. Zeta has
+   no gated ambiguous beta in this run. Logistic has no gated classifier because
+   the one-sided observable set is empty. This extends 0901: beta `0.30` was a
+   protocol fold in the synthetic matrix, not a cross-domain coordinate.
+
+## Consecutio
+
+`ORDER_DENOMINATOR_GATE` survives as scoped operator:
+
+> In semi-real arithmetic/spectral spacing perimeters, the denominator gate is
+> one-sided support for order against a marginal-preserving shuffle. In
+> logistic return intervals, the canonical gap observables do not carry that
+> support; the gate output is blank rather than false-positive.
+
+Next experiment: do not tune `z_min`. Change the observable perimetro for the
+logistic counter-scope: symbolic itinerary block entropy, return-tail exponent,
+or recurrence-plot diagonal statistics, each with the same original-vs-shuffle
+denominator gate. That tests whether logistic order is absent for this gate or
+only invisible to the current canonical gap suite.
+
+## Self-Audit: 5 Lenti
+
+L1 hard constraint vs bias: no universal claim is made. `endpoint_stable_observables: []`
+is exact for the run perimeters; "weak" means below the declared `abs(z)>=2`
+gate frequency, not numerical zero.
+
+L2 quantity vs ratio: raw stable counts and z means are reported before endpoint
+distances. No percentage drift claim is used.
+
+L3 no silent patching: 0901 claimed transfer on synthetic generated-order
+perimeters. This report narrows the scope after observing the logistic
+counter-perimeter; it does not rescue the original wording.
+
+L4 edge cases: logistic illusory stable_count is `0.583` in the seed check,
+so the claim is not "illusory endpoint absent." It is "no coherent one-sided
+support under the declared frequency rule."
+
+L5 re-discovery vs discovery: prime gap and zeta spacing order-vs-shuffle tests
+sit near known arithmetic/RMT order diagnostics; logistic return intervals sit
+near known recurrence and return-time diagnostics for chaotic maps. No NEW
+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_semireal_order_denominator_gate.py`
+- Verificato: `tools/data/semireal_order_denominator_gate_20260507_0923.json`
+- Verificato: `tools/data/semireal_order_denominator_gate_20260507_0923_seedcheck.json`
+- Inferito: logistic return intervals are a counter-scope for the canonical
+  gap-observable form of `ORDER_DENOMINATOR_GATE`.
diff --git a/tools/data/seme.json b/tools/data/seme.json
index cfd4593825fee57518a2e251e74292bb104253d9..d236f9223863b878fed1dd46bd69c22877e12b4a
--- a/tools/data/seme.json
+++ b/tools/data/seme.json
@@ -38,12 +38,12 @@
     {
       "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.",
+      "claim": "Il denominator gate trasferisce come supporto one-sided dell'ordine quando l'ordine e visibile agli osservabili canonici del perimetro, non come endpoint-stable support a due poli. Nel perimetro sintetico agent_20260507_0901, 4/4 domini non-BOUNDARY hanno endpoint_stable_observables=[] e polo coerente stable_count 3.0-5.0. Nel perimetro semi-reale agent_20260507_0923, primi e zeta trasferiscono (primi: SR,L1,triple_var; zeta: SR,L2), ma logistic_return_intervals e blank: stable_count coerente 0.0-0.2. La beta 0.30 e coordinata del protocollo quando compare, non coordinata universale.",
       "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",
+      "origine": "cycle agent_20260507_0901 + agent_20260507_0923: transfer matrix sintetica e falsificazione semi-reale su primi, zeta, logistic returns",
       "added_at": "2026-05-07T09:01:00+00:00"
     },
     {
@@ -336,4 +336,4 @@
   "_seed_history": {
     "trajectory_apply_20260507_092332": "applied modify_seme from 20260507_0901: direzione → 'Falsificare ORDER_DENOMINATOR_GATE su domini non-sintetici o semi-reali: applicare il gate one-sided...'"
   }
-}
\ No newline at end of file
+}
diff --git a/tools/exp_semireal_order_denominator_gate.py b/tools/exp_semireal_order_denominator_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..f3da4e51c84f066066367b25fded3ebea3c1c00d
--- /dev/null
+++ b/tools/exp_semireal_order_denominator_gate.py
@@ -0,0 +1,352 @@
+#!/usr/bin/env python3
+"""
+exp_semireal_order_denominator_gate.py
+
+Falsification attempt for ORDER_DENOMINATOR_GATE on non-synthetic / semi-real
+ordered sequences. The coherent endpoint is the observed order of each sequence;
+the illusory endpoint is a marginal-preserving permutation. The same
+original-vs-shuffle denominator gate used in the prior reports is applied to
+canonical observables from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import math
+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 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 sieve_primes_for_count(n_primes: int) -> np.ndarray:
+    if n_primes < 6:
+        limit = 20
+    else:
+        limit = int(n_primes * (math.log(n_primes) + math.log(math.log(n_primes))) * 1.25)
+    while True:
+        sieve = np.ones(limit + 1, dtype=bool)
+        sieve[:2] = False
+        for p in range(2, int(limit**0.5) + 1):
+            if sieve[p]:
+                sieve[p * p : limit + 1 : p] = False
+        primes = np.flatnonzero(sieve)
+        if len(primes) >= n_primes:
+            return primes[:n_primes].astype(float)
+        limit *= 2
+
+
+def prime_gap_sequence(n_gaps: int) -> np.ndarray:
+    primes = sieve_primes_for_count(n_gaps + 1)
+    return normalize(np.diff(primes))
+
+
+def zeta_zero_spacings(n_gaps: int) -> np.ndarray:
+    try:
+        import mpmath as mp
+    except ImportError as exc:
+        raise RuntimeError("mpmath is required for zeta_zero_spacings") from exc
+
+    zeros = np.empty(n_gaps + 1, dtype=float)
+    for i in range(n_gaps + 1):
+        zeros[i] = float(mp.im(mp.zetazero(i + 1)))
+    return normalize(np.diff(zeros))
+
+
+def logistic_return_intervals(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    # Return intervals to a high-density-edge event in the fully chaotic logistic map.
+    threshold = 0.95
+    burn = 2000
+    needed = n_gaps + 1
+    returns: list[int] = []
+    last_hit: int | None = None
+    x = float(rng.random())
+    i = 0
+    max_steps = 50_000_000
+    while len(returns) < needed and i < max_steps:
+        x = 4.0 * x * (1.0 - x)
+        if i >= burn and x > threshold:
+            if last_hit is not None:
+                returns.append(i - last_hit)
+            last_hit = i
+        i += 1
+    if len(returns) < needed:
+        raise RuntimeError(f"logistic generator produced {len(returns)} intervals, need {needed}")
+    return normalize(np.array(returns[:n_gaps], dtype=float))
+
+
+def beta_replace(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        return base.copy()
+    if beta >= 1.0:
+        return illusory
+    out = base.copy()
+    mask = rng.random(len(base)) < beta
+    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], dict[str, float]]:
+    original = compute_canonical(gaps)
+    baseline = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline[name].append(obs[name])
+
+    means = {}
+    sds = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline[name], dtype=float)
+        means[name] = float(np.mean(vals))
+        sds[name] = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        z[name] = float((original[name] - means[name]) / sds[name]) if sds[name] > 1e-15 else 0.0
+    return original, means, sds, 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 = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent, illusory])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent, axis=0)
+    illusory_centroid = np.mean(illusory, 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()):
+        margins = []
+        labels = []
+        coords = []
+        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)),
+            "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_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()},
+        }
+
+    one_sided = []
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    for name in OBS_NAMES:
+        coherent_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in coherent_rows]))
+        illusory_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in illusory_rows]))
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            one_sided.append(name)
+        if coherent_freq >= 0.75 and illusory_freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "coherent_one_sided_observables": one_sided,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def analyze_sequence(name: str, base: np.ndarray, 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):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        for beta in betas:
+            gaps = beta_replace(base, beta, rep_rng)
+            obs, shuffle_mean, 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_mean": shuffle_mean,
+                    "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)
+    return {
+        "source": {
+            "n_gaps": int(len(base)),
+            "mean": float(np.mean(base)),
+            "variance": float(np.var(base)),
+        },
+        "gate": gate,
+        "classification_all_observables": classify_layers(rows, OBS_NAMES),
+        "classification_one_sided_gated": classify_layers(rows, gate["coherent_one_sided_observables"]),
+        "rows": rows,
+    }
+
+
+def build_sequences(args: argparse.Namespace, rng: np.random.Generator) -> dict[str, np.ndarray]:
+    sequences = {
+        "prime_gaps_first": prime_gap_sequence(args.n_gaps),
+        "logistic_return_intervals": logistic_return_intervals(args.n_gaps, rng),
+    }
+    if args.include_zeta:
+        sequences["zeta_zero_spacings_first"] = zeta_zero_spacings(args.zeta_gaps)
+    return sequences
+
+
+def compact(perimeters: dict) -> dict:
+    out = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        all_cls = data["classification_all_observables"]
+        gated_cls = data["classification_one_sided_gated"]
+        out[name] = {
+            "n_gaps": data["source"]["n_gaps"],
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+            "z_mean_coherent": gate["layers"]["0.000"]["z_mean"],
+            "z_mean_illusory": gate["layers"]["1.000"]["z_mean"],
+            "endpoint_distance_all": all_cls["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": gated_cls["endpoint_distance"],
+            "ambiguous_beta_one_sided_gated": gated_cls["ambiguous_beta"],
+        }
+    return out
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    sequences = build_sequences(args, root_rng)
+    perimeters = {}
+    for name, base in sequences.items():
+        perimeters[name] = analyze_sequence(name, base, args, root_rng)
+
+    output = {
+        "experiment": "semireal_order_denominator_gate",
+        "category": "gate_falsification_semireal",
+        "question": "Does ORDER_DENOMINATOR_GATE survive on non-synthetic / semi-real ordered sequences?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact(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 n one_sided stable0 stable1 dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>28s} "
+            f"{row['n_gaps']:>5d} "
+            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_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("--zeta-gaps", type=int, default=1024)
+    parser.add_argument("--include-zeta", action="store_true")
+    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=202605070923)
+    parser.add_argument("--out", default="tools/data/semireal_order_denominator_gate_20260507_0923.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

codex
Il JSON del seme è valido. Il falsifier richiede output esplicito; lo rilancio nel formato corretto e poi correggo il report se segnala un problema reale.
exec
/bin/bash -lc 'python tools/lab_falsifier.py --report tools/data/reports/agent_20260507_0923.md --output tools/data/reports/falsifier_20260507_0923.json' in /opt/MM_D-ND
diff --git a/tools/data/reports/agent_20260507_0923.md b/tools/data/reports/agent_20260507_0923.md
new file mode 100644
index 0000000000000000000000000000000000000000..d7dc7d70d70cbf71dfba4b394064f4bdf842bb15
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0923.md
@@ -0,0 +1,132 @@
+# Agent Report — Semi-Real Order Denominator Gate
+
+timestamp: 2026-05-07 09:23 UTC
+category: gate_falsification_semireal
+verdict: scoped_operator
+observables_registry: 1.0.0-2026-05-06
+observables_used: [SR, SR2, L1, L2, triple_var]
+tool: tools/exp_semireal_order_denominator_gate.py
+data: tools/data/semireal_order_denominator_gate_20260507_0923.json
+seed_check: tools/data/semireal_order_denominator_gate_20260507_0923_seedcheck.json
+
+## Claim Under Test
+
+Verificato dal campo vivo: il cycle 0901 ha nominato `ORDER_DENOMINATOR_GATE`
+come supporto one-sided dell'ordine generato, ma lo ha fatto su perimetri
+sintetici. Il mandato corrente chiede falsificazione su domini non-sintetici o
+semi-reali.
+
+Domanda: se l'endpoint coerente è una sequenza ordinata reale/semi-reale e
+l'endpoint illusorio è una permutazione che preserva la marginale, il supporto
+denominatore resta one-sided o compare un controesempio?
+
+Perimetri:
+
+- `prime_gaps_first`: primi 4096 gap fra primi.
+- `zeta_zero_spacings_first`: primi 512 spacing fra zeri non banali di zeta,
+  calcolati localmente con `mpmath.zetazero`.
+- `logistic_return_intervals`: 4096 intervalli di ritorno a `x > 0.95` nella
+  mappa logistica caotica `x -> 4x(1-x)`.
+
+Gate: osservabile stabile se `abs(original - shuffle_mean) / shuffle_std >= 2`.
+
+## Deposito Numerico
+
+Run principale: `n_replicates=20`, `n_beta=11`, `n_baseline=32`,
+`seed=202605070923`. Seed check: `n_replicates=12`, `n_baseline=24`,
+`seed=202605070924`.
+
+| perimeter | coherent one-sided observables | stable_count coherent | stable_count illusory | endpoint distance gated | beta ambiguous gated |
+|---|---:|---:|---:|---:|---:|
+| prime_gaps_first | SR, L1, triple_var | 3.000 | 0.650 | 3.270 | 0.30 |
+| logistic_return_intervals | [] | 0.200 | 0.100 | 0.000 | [] |
+| zeta_zero_spacings_first | SR, L2 | 2.150 | 0.250 | 2.666 | [] |
+
+Seed check:
+
+| perimeter | coherent one-sided observables | stable_count coherent | stable_count illusory | endpoint distance gated | beta ambiguous gated |
+|---|---:|---:|---:|---:|---:|
+| prime_gaps_first | SR, L1, triple_var | 3.000 | 0.250 | 3.288 | 0.30 |
+| logistic_return_intervals | [] | 0.000 | 0.583 | 0.000 | [] |
+| zeta_zero_spacings_first | SR, L2 | 2.417 | 0.333 | 2.700 | [] |
+
+Endpoint-stable observables: `[]` in all three perimeters in both runs.
+
+## Risultato
+
+1. **The order gate transfers to arithmetic and zeta spacing order.**
+
+   Prime gaps carry one-sided support on `SR`, `L1`, and `triple_var`.
+   Zeta-zero spacings carry one-sided support on `SR` and `L2`. In both cases
+   the illusory endpoint remains weak-denominator under the same marginal.
+
+2. **The logistic return perimeter is the counter-scope.**
+
+   The logistic return sequence is ordered and generated by a deterministic
+   chaotic system, but this canonical observable suite does not read its order
+   as denominator support. The coherent endpoint stable count is `0.200` in the
+   main run and `0.000` in the seed check. The gate does not transfer to this
+   return-time observable.
+
+3. **The transferable object is narrower than "real order".**
+
+   `ORDER_DENOMINATOR_GATE` names order that survives a marginal-preserving
+   shuffle in the canonical gap observables. It does not name every generated
+   sequence. The node regressivo is the observable contract, not the gate
+   threshold: if the order lives in return-time tail structure or symbolic
+   itinerary, `SR/SR2/L1/L2/triple_var` can be blank.
+
+4. **The beta layer is not universal.**
+
+   Prime gaps reproduce beta `0.30` as the ambiguous protocol layer. Zeta has
+   no gated ambiguous beta in this run. Logistic has no gated classifier because
+   the one-sided observable set is empty. This extends 0901: beta `0.30` was a
+   protocol fold in the synthetic matrix, not a cross-domain coordinate.
+
+## Consecutio
+
+`ORDER_DENOMINATOR_GATE` survives as scoped operator:
+
+> In semi-real arithmetic/spectral spacing perimeters, the denominator gate is
+> one-sided support for order against a marginal-preserving shuffle. In
+> logistic return intervals, the canonical gap observables do not carry that
+> support; the gate output is blank rather than false-positive.
+
+Next experiment: do not tune `z_min`. Change the observable perimetro for the
+logistic counter-scope: symbolic itinerary block entropy, return-tail exponent,
+or recurrence-plot diagonal statistics, each with the same original-vs-shuffle
+denominator gate. That tests whether logistic order is absent for this gate or
+only invisible to the current canonical gap suite.
+
+## Self-Audit: 5 Lenti
+
+L1 hard constraint vs bias: no universal claim is made. `endpoint_stable_observables: []`
+is exact for the run perimeters; "weak" means below the declared `abs(z)>=2`
+gate frequency, not numerical zero.
+
+L2 quantity vs ratio: raw stable counts and z means are reported before endpoint
+distances. No percentage drift claim is used.
+
+L3 no silent patching: 0901 claimed transfer on synthetic generated-order
+perimeters. This report narrows the scope after observing the logistic
+counter-perimeter; it does not rescue the original wording.
+
+L4 edge cases: logistic illusory stable_count is `0.583` in the seed check,
+so the claim is not "illusory endpoint absent." It is "no coherent one-sided
+support under the declared frequency rule."
+
+L5 re-discovery vs discovery: prime gap and zeta spacing order-vs-shuffle tests
+sit near known arithmetic/RMT order diagnostics; logistic return intervals sit
+near known recurrence and return-time diagnostics for chaotic maps. No NEW
+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_semireal_order_denominator_gate.py`
+- Verificato: `tools/data/semireal_order_denominator_gate_20260507_0923.json`
+- Verificato: `tools/data/semireal_order_denominator_gate_20260507_0923_seedcheck.json`
+- Inferito: logistic return intervals are a counter-scope for the canonical
+  gap-observable form of `ORDER_DENOMINATOR_GATE`.
diff --git a/tools/data/seme.json b/tools/data/seme.json
index cfd4593825fee57518a2e251e74292bb104253d9..d236f9223863b878fed1dd46bd69c22877e12b4a
--- a/tools/data/seme.json
+++ b/tools/data/seme.json
@@ -38,12 +38,12 @@
     {
       "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.",
+      "claim": "Il denominator gate trasferisce come supporto one-sided dell'ordine quando l'ordine e visibile agli osservabili canonici del perimetro, non come endpoint-stable support a due poli. Nel perimetro sintetico agent_20260507_0901, 4/4 domini non-BOUNDARY hanno endpoint_stable_observables=[] e polo coerente stable_count 3.0-5.0. Nel perimetro semi-reale agent_20260507_0923, primi e zeta trasferiscono (primi: SR,L1,triple_var; zeta: SR,L2), ma logistic_return_intervals e blank: stable_count coerente 0.0-0.2. La beta 0.30 e coordinata del protocollo quando compare, non coordinata universale.",
       "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",
+      "origine": "cycle agent_20260507_0901 + agent_20260507_0923: transfer matrix sintetica e falsificazione semi-reale su primi, zeta, logistic returns",
       "added_at": "2026-05-07T09:01:00+00:00"
     },
     {
@@ -336,4 +336,4 @@
   "_seed_history": {
     "trajectory_apply_20260507_092332": "applied modify_seme from 20260507_0901: direzione → 'Falsificare ORDER_DENOMINATOR_GATE su domini non-sintetici o semi-reali: applicare il gate one-sided...'"
   }
-}
\ No newline at end of file
+}
diff --git a/tools/exp_semireal_order_denominator_gate.py b/tools/exp_semireal_order_denominator_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..f3da4e51c84f066066367b25fded3ebea3c1c00d
--- /dev/null
+++ b/tools/exp_semireal_order_denominator_gate.py
@@ -0,0 +1,352 @@
+#!/usr/bin/env python3
+"""
+exp_semireal_order_denominator_gate.py
+
+Falsification attempt for ORDER_DENOMINATOR_GATE on non-synthetic / semi-real
+ordered sequences. The coherent endpoint is the observed order of each sequence;
+the illusory endpoint is a marginal-preserving permutation. The same
+original-vs-shuffle denominator gate used in the prior reports is applied to
+canonical observables from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import math
+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 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 sieve_primes_for_count(n_primes: int) -> np.ndarray:
+    if n_primes < 6:
+        limit = 20
+    else:
+        limit = int(n_primes * (math.log(n_primes) + math.log(math.log(n_primes))) * 1.25)
+    while True:
+        sieve = np.ones(limit + 1, dtype=bool)
+        sieve[:2] = False
+        for p in range(2, int(limit**0.5) + 1):
+            if sieve[p]:
+                sieve[p * p : limit + 1 : p] = False
+        primes = np.flatnonzero(sieve)
+        if len(primes) >= n_primes:
+            return primes[:n_primes].astype(float)
+        limit *= 2
+
+
+def prime_gap_sequence(n_gaps: int) -> np.ndarray:
+    primes = sieve_primes_for_count(n_gaps + 1)
+    return normalize(np.diff(primes))
+
+
+def zeta_zero_spacings(n_gaps: int) -> np.ndarray:
+    try:
+        import mpmath as mp
+    except ImportError as exc:
+        raise RuntimeError("mpmath is required for zeta_zero_spacings") from exc
+
+    zeros = np.empty(n_gaps + 1, dtype=float)
+    for i in range(n_gaps + 1):
+        zeros[i] = float(mp.im(mp.zetazero(i + 1)))
+    return normalize(np.diff(zeros))
+
+
+def logistic_return_intervals(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    # Return intervals to a high-density-edge event in the fully chaotic logistic map.
+    threshold = 0.95
+    burn = 2000
+    needed = n_gaps + 1
+    returns: list[int] = []
+    last_hit: int | None = None
+    x = float(rng.random())
+    i = 0
+    max_steps = 50_000_000
+    while len(returns) < needed and i < max_steps:
+        x = 4.0 * x * (1.0 - x)
+        if i >= burn and x > threshold:
+            if last_hit is not None:
+                returns.append(i - last_hit)
+            last_hit = i
+        i += 1
+    if len(returns) < needed:
+        raise RuntimeError(f"logistic generator produced {len(returns)} intervals, need {needed}")
+    return normalize(np.array(returns[:n_gaps], dtype=float))
+
+
+def beta_replace(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        return base.copy()
+    if beta >= 1.0:
+        return illusory
+    out = base.copy()
+    mask = rng.random(len(base)) < beta
+    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], dict[str, float]]:
+    original = compute_canonical(gaps)
+    baseline = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline[name].append(obs[name])
+
+    means = {}
+    sds = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline[name], dtype=float)
+        means[name] = float(np.mean(vals))
+        sds[name] = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        z[name] = float((original[name] - means[name]) / sds[name]) if sds[name] > 1e-15 else 0.0
+    return original, means, sds, 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 = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent, illusory])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent, axis=0)
+    illusory_centroid = np.mean(illusory, 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()):
+        margins = []
+        labels = []
+        coords = []
+        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)),
+            "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_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()},
+        }
+
+    one_sided = []
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    for name in OBS_NAMES:
+        coherent_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in coherent_rows]))
+        illusory_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in illusory_rows]))
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            one_sided.append(name)
+        if coherent_freq >= 0.75 and illusory_freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "coherent_one_sided_observables": one_sided,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def analyze_sequence(name: str, base: np.ndarray, 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):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        for beta in betas:
+            gaps = beta_replace(base, beta, rep_rng)
+            obs, shuffle_mean, 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_mean": shuffle_mean,
+                    "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)
+    return {
+        "source": {
+            "n_gaps": int(len(base)),
+            "mean": float(np.mean(base)),
+            "variance": float(np.var(base)),
+        },
+        "gate": gate,
+        "classification_all_observables": classify_layers(rows, OBS_NAMES),
+        "classification_one_sided_gated": classify_layers(rows, gate["coherent_one_sided_observables"]),
+        "rows": rows,
+    }
+
+
+def build_sequences(args: argparse.Namespace, rng: np.random.Generator) -> dict[str, np.ndarray]:
+    sequences = {
+        "prime_gaps_first": prime_gap_sequence(args.n_gaps),
+        "logistic_return_intervals": logistic_return_intervals(args.n_gaps, rng),
+    }
+    if args.include_zeta:
+        sequences["zeta_zero_spacings_first"] = zeta_zero_spacings(args.zeta_gaps)
+    return sequences
+
+
+def compact(perimeters: dict) -> dict:
+    out = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        all_cls = data["classification_all_observables"]
+        gated_cls = data["classification_one_sided_gated"]
+        out[name] = {
+            "n_gaps": data["source"]["n_gaps"],
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+            "z_mean_coherent": gate["layers"]["0.000"]["z_mean"],
+            "z_mean_illusory": gate["layers"]["1.000"]["z_mean"],
+            "endpoint_distance_all": all_cls["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": gated_cls["endpoint_distance"],
+            "ambiguous_beta_one_sided_gated": gated_cls["ambiguous_beta"],
+        }
+    return out
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    sequences = build_sequences(args, root_rng)
+    perimeters = {}
+    for name, base in sequences.items():
+        perimeters[name] = analyze_sequence(name, base, args, root_rng)
+
+    output = {
+        "experiment": "semireal_order_denominator_gate",
+        "category": "gate_falsification_semireal",
+        "question": "Does ORDER_DENOMINATOR_GATE survive on non-synthetic / semi-real ordered sequences?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact(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 n one_sided stable0 stable1 dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>28s} "
+            f"{row['n_gaps']:>5d} "
+            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_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("--zeta-gaps", type=int, default=1024)
+    parser.add_argument("--include-zeta", action="store_true")
+    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=202605070923)
+    parser.add_argument("--out", default="tools/data/semireal_order_denominator_gate_20260507_0923.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

diff --git a/tools/data/reports/agent_20260507_0923.md b/tools/data/reports/agent_20260507_0923.md
new file mode 100644
index 0000000000000000000000000000000000000000..d7dc7d70d70cbf71dfba4b394064f4bdf842bb15
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0923.md
@@ -0,0 +1,132 @@
+# Agent Report — Semi-Real Order Denominator Gate
+
+timestamp: 2026-05-07 09:23 UTC
+category: gate_falsification_semireal
+verdict: scoped_operator
+observables_registry: 1.0.0-2026-05-06
+observables_used: [SR, SR2, L1, L2, triple_var]
+tool: tools/exp_semireal_order_denominator_gate.py
+data: tools/data/semireal_order_denominator_gate_20260507_0923.json
+seed_check: tools/data/semireal_order_denominator_gate_20260507_0923_seedcheck.json
+
+## Claim Under Test
+
+Verificato dal campo vivo: il cycle 0901 ha nominato `ORDER_DENOMINATOR_GATE`
+come supporto one-sided dell'ordine generato, ma lo ha fatto su perimetri
+sintetici. Il mandato corrente chiede falsificazione su domini non-sintetici o
+semi-reali.
+
+Domanda: se l'endpoint coerente è una sequenza ordinata reale/semi-reale e
+l'endpoint illusorio è una permutazione che preserva la marginale, il supporto
+denominatore resta one-sided o compare un controesempio?
+
+Perimetri:
+
+- `prime_gaps_first`: primi 4096 gap fra primi.
+- `zeta_zero_spacings_first`: primi 512 spacing fra zeri non banali di zeta,
+  calcolati localmente con `mpmath.zetazero`.
+- `logistic_return_intervals`: 4096 intervalli di ritorno a `x > 0.95` nella
+  mappa logistica caotica `x -> 4x(1-x)`.
+
+Gate: osservabile stabile se `abs(original - shuffle_mean) / shuffle_std >= 2`.
+
+## Deposito Numerico
+
+Run principale: `n_replicates=20`, `n_beta=11`, `n_baseline=32`,
+`seed=202605070923`. Seed check: `n_replicates=12`, `n_baseline=24`,
+`seed=202605070924`.
+
+| perimeter | coherent one-sided observables | stable_count coherent | stable_count illusory | endpoint distance gated | beta ambiguous gated |
+|---|---:|---:|---:|---:|---:|
+| prime_gaps_first | SR, L1, triple_var | 3.000 | 0.650 | 3.270 | 0.30 |
+| logistic_return_intervals | [] | 0.200 | 0.100 | 0.000 | [] |
+| zeta_zero_spacings_first | SR, L2 | 2.150 | 0.250 | 2.666 | [] |
+
+Seed check:
+
+| perimeter | coherent one-sided observables | stable_count coherent | stable_count illusory | endpoint distance gated | beta ambiguous gated |
+|---|---:|---:|---:|---:|---:|
+| prime_gaps_first | SR, L1, triple_var | 3.000 | 0.250 | 3.288 | 0.30 |
+| logistic_return_intervals | [] | 0.000 | 0.583 | 0.000 | [] |
+| zeta_zero_spacings_first | SR, L2 | 2.417 | 0.333 | 2.700 | [] |
+
+Endpoint-stable observables: `[]` in all three perimeters in both runs.
+
+## Risultato
+
+1. **The order gate transfers to arithmetic and zeta spacing order.**
+
+   Prime gaps carry one-sided support on `SR`, `L1`, and `triple_var`.
+   Zeta-zero spacings carry one-sided support on `SR` and `L2`. In both cases
+   the illusory endpoint remains weak-denominator under the same marginal.
+
+2. **The logistic return perimeter is the counter-scope.**
+
+   The logistic return sequence is ordered and generated by a deterministic
+   chaotic system, but this canonical observable suite does not read its order
+   as denominator support. The coherent endpoint stable count is `0.200` in the
+   main run and `0.000` in the seed check. The gate does not transfer to this
+   return-time observable.
+
+3. **The transferable object is narrower than "real order".**
+
+   `ORDER_DENOMINATOR_GATE` names order that survives a marginal-preserving
+   shuffle in the canonical gap observables. It does not name every generated
+   sequence. The node regressivo is the observable contract, not the gate
+   threshold: if the order lives in return-time tail structure or symbolic
+   itinerary, `SR/SR2/L1/L2/triple_var` can be blank.
+
+4. **The beta layer is not universal.**
+
+   Prime gaps reproduce beta `0.30` as the ambiguous protocol layer. Zeta has
+   no gated ambiguous beta in this run. Logistic has no gated classifier because
+   the one-sided observable set is empty. This extends 0901: beta `0.30` was a
+   protocol fold in the synthetic matrix, not a cross-domain coordinate.
+
+## Consecutio
+
+`ORDER_DENOMINATOR_GATE` survives as scoped operator:
+
+> In semi-real arithmetic/spectral spacing perimeters, the denominator gate is
+> one-sided support for order against a marginal-preserving shuffle. In
+> logistic return intervals, the canonical gap observables do not carry that
+> support; the gate output is blank rather than false-positive.
+
+Next experiment: do not tune `z_min`. Change the observable perimetro for the
+logistic counter-scope: symbolic itinerary block entropy, return-tail exponent,
+or recurrence-plot diagonal statistics, each with the same original-vs-shuffle
+denominator gate. That tests whether logistic order is absent for this gate or
+only invisible to the current canonical gap suite.
+
+## Self-Audit: 5 Lenti
+
+L1 hard constraint vs bias: no universal claim is made. `endpoint_stable_observables: []`
+is exact for the run perimeters; "weak" means below the declared `abs(z)>=2`
+gate frequency, not numerical zero.
+
+L2 quantity vs ratio: raw stable counts and z means are reported before endpoint
+distances. No percentage drift claim is used.
+
+L3 no silent patching: 0901 claimed transfer on synthetic generated-order
+perimeters. This report narrows the scope after observing the logistic
+counter-perimeter; it does not rescue the original wording.
+
+L4 edge cases: logistic illusory stable_count is `0.583` in the seed check,
+so the claim is not "illusory endpoint absent." It is "no coherent one-sided
+support under the declared frequency rule."
+
+L5 re-discovery vs discovery: prime gap and zeta spacing order-vs-shuffle tests
+sit near known arithmetic/RMT order diagnostics; logistic return intervals sit
+near known recurrence and return-time diagnostics for chaotic maps. No NEW
+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_semireal_order_denominator_gate.py`
+- Verificato: `tools/data/semireal_order_denominator_gate_20260507_0923.json`
+- Verificato: `tools/data/semireal_order_denominator_gate_20260507_0923_seedcheck.json`
+- Inferito: logistic return intervals are a counter-scope for the canonical
+  gap-observable form of `ORDER_DENOMINATOR_GATE`.
diff --git a/tools/data/seme.json b/tools/data/seme.json
index cfd4593825fee57518a2e251e74292bb104253d9..d236f9223863b878fed1dd46bd69c22877e12b4a
--- a/tools/data/seme.json
+++ b/tools/data/seme.json
@@ -38,12 +38,12 @@
     {
       "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.",
+      "claim": "Il denominator gate trasferisce come supporto one-sided dell'ordine quando l'ordine e visibile agli osservabili canonici del perimetro, non come endpoint-stable support a due poli. Nel perimetro sintetico agent_20260507_0901, 4/4 domini non-BOUNDARY hanno endpoint_stable_observables=[] e polo coerente stable_count 3.0-5.0. Nel perimetro semi-reale agent_20260507_0923, primi e zeta trasferiscono (primi: SR,L1,triple_var; zeta: SR,L2), ma logistic_return_intervals e blank: stable_count coerente 0.0-0.2. La beta 0.30 e coordinata del protocollo quando compare, non coordinata universale.",
       "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",
+      "origine": "cycle agent_20260507_0901 + agent_20260507_0923: transfer matrix sintetica e falsificazione semi-reale su primi, zeta, logistic returns",
       "added_at": "2026-05-07T09:01:00+00:00"
     },
     {
@@ -336,4 +336,4 @@
   "_seed_history": {
     "trajectory_apply_20260507_092332": "applied modify_seme from 20260507_0901: direzione → 'Falsificare ORDER_DENOMINATOR_GATE su domini non-sintetici o semi-reali: applicare il gate one-sided...'"
   }
-}
\ No newline at end of file
+}
diff --git a/tools/exp_semireal_order_denominator_gate.py b/tools/exp_semireal_order_denominator_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..f3da4e51c84f066066367b25fded3ebea3c1c00d
--- /dev/null
+++ b/tools/exp_semireal_order_denominator_gate.py
@@ -0,0 +1,352 @@
+#!/usr/bin/env python3
+"""
+exp_semireal_order_denominator_gate.py
+
+Falsification attempt for ORDER_DENOMINATOR_GATE on non-synthetic / semi-real
+ordered sequences. The coherent endpoint is the observed order of each sequence;
+the illusory endpoint is a marginal-preserving permutation. The same
+original-vs-shuffle denominator gate used in the prior reports is applied to
+canonical observables from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import math
+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 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 sieve_primes_for_count(n_primes: int) -> np.ndarray:
+    if n_primes < 6:
+        limit = 20
+    else:
+        limit = int(n_primes * (math.log(n_primes) + math.log(math.log(n_primes))) * 1.25)
+    while True:
+        sieve = np.ones(limit + 1, dtype=bool)
+        sieve[:2] = False
+        for p in range(2, int(limit**0.5) + 1):
+            if sieve[p]:
+                sieve[p * p : limit + 1 : p] = False
+        primes = np.flatnonzero(sieve)
+        if len(primes) >= n_primes:
+            return primes[:n_primes].astype(float)
+        limit *= 2
+
+
+def prime_gap_sequence(n_gaps: int) -> np.ndarray:
+    primes = sieve_primes_for_count(n_gaps + 1)
+    return normalize(np.diff(primes))
+
+
+def zeta_zero_spacings(n_gaps: int) -> np.ndarray:
+    try:
+        import mpmath as mp
+    except ImportError as exc:
+        raise RuntimeError("mpmath is required for zeta_zero_spacings") from exc
+
+    zeros = np.empty(n_gaps + 1, dtype=float)
+    for i in range(n_gaps + 1):
+        zeros[i] = float(mp.im(mp.zetazero(i + 1)))
+    return normalize(np.diff(zeros))
+
+
+def logistic_return_intervals(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    # Return intervals to a high-density-edge event in the fully chaotic logistic map.
+    threshold = 0.95
+    burn = 2000
+    needed = n_gaps + 1
+    returns: list[int] = []
+    last_hit: int | None = None
+    x = float(rng.random())
+    i = 0
+    max_steps = 50_000_000
+    while len(returns) < needed and i < max_steps:
+        x = 4.0 * x * (1.0 - x)
+        if i >= burn and x > threshold:
+            if last_hit is not None:
+                returns.append(i - last_hit)
+            last_hit = i
+        i += 1
+    if len(returns) < needed:
+        raise RuntimeError(f"logistic generator produced {len(returns)} intervals, need {needed}")
+    return normalize(np.array(returns[:n_gaps], dtype=float))
+
+
+def beta_replace(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        return base.copy()
+    if beta >= 1.0:
+        return illusory
+    out = base.copy()
+    mask = rng.random(len(base)) < beta
+    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], dict[str, float]]:
+    original = compute_canonical(gaps)
+    baseline = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline[name].append(obs[name])
+
+    means = {}
+    sds = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline[name], dtype=float)
+        means[name] = float(np.mean(vals))
+        sds[name] = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        z[name] = float((original[name] - means[name]) / sds[name]) if sds[name] > 1e-15 else 0.0
+    return original, means, sds, 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 = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent, illusory])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent, axis=0)
+    illusory_centroid = np.mean(illusory, 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()):
+        margins = []
+        labels = []
+        coords = []
+        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)),
+            "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_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()},
+        }
+
+    one_sided = []
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    for name in OBS_NAMES:
+        coherent_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in coherent_rows]))
+        illusory_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in illusory_rows]))
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            one_sided.append(name)
+        if coherent_freq >= 0.75 and illusory_freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "coherent_one_sided_observables": one_sided,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def analyze_sequence(name: str, base: np.ndarray, 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):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        for beta in betas:
+            gaps = beta_replace(base, beta, rep_rng)
+            obs, shuffle_mean, 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_mean": shuffle_mean,
+                    "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)
+    return {
+        "source": {
+            "n_gaps": int(len(base)),
+            "mean": float(np.mean(base)),
+            "variance": float(np.var(base)),
+        },
+        "gate": gate,
+        "classification_all_observables": classify_layers(rows, OBS_NAMES),
+        "classification_one_sided_gated": classify_layers(rows, gate["coherent_one_sided_observables"]),
+        "rows": rows,
+    }
+
+
+def build_sequences(args: argparse.Namespace, rng: np.random.Generator) -> dict[str, np.ndarray]:
+    sequences = {
+        "prime_gaps_first": prime_gap_sequence(args.n_gaps),
+        "logistic_return_intervals": logistic_return_intervals(args.n_gaps, rng),
+    }
+    if args.include_zeta:
+        sequences["zeta_zero_spacings_first"] = zeta_zero_spacings(args.zeta_gaps)
+    return sequences
+
+
+def compact(perimeters: dict) -> dict:
+    out = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        all_cls = data["classification_all_observables"]
+        gated_cls = data["classification_one_sided_gated"]
+        out[name] = {
+            "n_gaps": data["source"]["n_gaps"],
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+            "z_mean_coherent": gate["layers"]["0.000"]["z_mean"],
+            "z_mean_illusory": gate["layers"]["1.000"]["z_mean"],
+            "endpoint_distance_all": all_cls["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": gated_cls["endpoint_distance"],
+            "ambiguous_beta_one_sided_gated": gated_cls["ambiguous_beta"],
+        }
+    return out
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    sequences = build_sequences(args, root_rng)
+    perimeters = {}
+    for name, base in sequences.items():
+        perimeters[name] = analyze_sequence(name, base, args, root_rng)
+
+    output = {
+        "experiment": "semireal_order_denominator_gate",
+        "category": "gate_falsification_semireal",
+        "question": "Does ORDER_DENOMINATOR_GATE survive on non-synthetic / semi-real ordered sequences?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact(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 n one_sided stable0 stable1 dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>28s} "
+            f"{row['n_gaps']:>5d} "
+            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_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("--zeta-gaps", type=int, default=1024)
+    parser.add_argument("--include-zeta", action="store_true")
+    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=202605070923)
+    parser.add_argument("--out", default="tools/data/semireal_order_denominator_gate_20260507_0923.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

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

exec
/bin/bash -lc "test -f tools/data/reports/falsifier_20260507_0923.json && sed -n '1,220p' tools/data/reports/falsifier_20260507_0923.json || true" in /opt/MM_D-ND
 succeeded in 0ms:
exec
/bin/bash -lc "git diff -- tools/exp_semireal_order_denominator_gate.py tools/data/reports/agent_20260507_0923.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..d236f92 100644
--- a/tools/data/seme.json
+++ b/tools/data/seme.json
@@ -1,7 +1,51 @@
 {
-  "timestamp": "2026-04-23T03:35:37.611830",
-  "piano": 48,
+  "timestamp": "2026-05-07T09:07:22.652946",
+  "piano": 71,
   "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": "vincolo",
+      "id": "ORDER_DENOMINATOR_GATE",
+      "claim": "Il denominator gate trasferisce come supporto one-sided dell'ordine quando l'ordine e visibile agli osservabili canonici del perimetro, non come endpoint-stable support a due poli. Nel perimetro sintetico agent_20260507_0901, 4/4 domini non-BOUNDARY hanno endpoint_stable_observables=[] e polo coerente stable_count 3.0-5.0. Nel perimetro semi-reale agent_20260507_0923, primi e zeta trasferiscono (primi: SR,L1,triple_var; zeta: SR,L2), ma logistic_return_intervals e blank: stable_count coerente 0.0-0.2. La beta 0.30 e coordinata del protocollo quando compare, non coordinata universale.",
+      "intensita": 0.92,
+      "porta": "META",
+      "manuale": true,
+      "condensato_ref": "A4,A8,A14,C2",
+      "origine": "cycle agent_20260507_0901 + agent_20260507_0923: transfer matrix sintetica e falsificazione semi-reale su primi, zeta, logistic returns",
+      "added_at": "2026-05-07T09:01:00+00:00"
+    },
     {
       "tipo": "confine_inesplorato",
       "id": "TRASCENDENZA_LIMITE",
@@ -78,28 +122,51 @@
       "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": "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": "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": 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_0901",
+      "claim": "Applied valutatore REDESIGN from 20260507_0901: Falsificare ORDER_DENOMINATOR_GATE su domini non-sintetici o semi-reali: applicare il gate one-sided a 2-3 sequenze fisiche/ponte gia' presenti nel sito, con shuffle e surrogati preservanti marginale,",
+      "intensità": 0.7,
+      "porta": "trajectory_apply",
+      "condensato_ref": "A8,A14,A15",
+      "manuale": true,
+      "_source_log": "2026-05-07T09:07:53.577876+00:00",
+      "_source_decision": "REDESIGN",
+      "_source_reasoning": "La direzione corrente ha eseguito il mandato: la matrice di trasferibilita' su 4 perimetri non-BOUNDARY e' stata prodotta e ha nominato una nuova categoria, order_denominator_gate. Un altro giro nello stesso frame rischia accumulo numerico locale su sintetici; la prossima mossa deve falsificare il n"
     }
   ],
   "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: {'METRIC_TENSOR', 'PIANO_PRIMARIO_DUE_ASSIOMI', 'OBSERVABLE_REGISTRY', 'TENSIONE_ENTITA', 'PERTURBATION_DENOMINATOR_GATE', 'BOUNDARY_LAYER_GATE', 'TRAJECTORY_APPLY_20260507_0803', 'G_POTENZIALE_NULLA', 'TRASCENDENZA_LIMITE', 'ORDER_DENOMINATOR_GATE', 'DUALITA_DIPOLARE_VS_ILLUSORIA'}"
   ],
   "filtro": {
-    "promosse": 8,
+    "promosse": 13,
     "filtrate": 0,
     "condensato_claims": 30
   },
-  "direzione": "Esplorare il confine: 8 domini GUE, 5 Poisson — il confine è il terzo incluso operativo",
+  "direzione": "Falsificare ORDER_DENOMINATOR_GATE su domini non-sintetici o semi-reali: applicare il gate one-sided a 2-3 sequenze fisiche/ponte gia' presenti nel sito, con shuffle e surrogati preservanti marginale, cercando un controesempio dove il supporto denominatore appare anche sul lato illusorio o scompare sul lato coerente.",
   "verifica": {
     "pass": 11,
     "fail": 0,
@@ -265,5 +332,8 @@
       "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_092332": "applied modify_seme from 20260507_0901: direzione → 'Falsificare ORDER_DENOMINATOR_GATE su domini non-sintetici o semi-reali: applicare il gate one-sided...'"
+  }
 }

diff --git a/tools/data/reports/agent_20260507_0923.md b/tools/data/reports/agent_20260507_0923.md
new file mode 100644
index 0000000000000000000000000000000000000000..d7dc7d70d70cbf71dfba4b394064f4bdf842bb15
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0923.md
@@ -0,0 +1,132 @@
+# Agent Report — Semi-Real Order Denominator Gate
+
+timestamp: 2026-05-07 09:23 UTC
+category: gate_falsification_semireal
+verdict: scoped_operator
+observables_registry: 1.0.0-2026-05-06
+observables_used: [SR, SR2, L1, L2, triple_var]
+tool: tools/exp_semireal_order_denominator_gate.py
+data: tools/data/semireal_order_denominator_gate_20260507_0923.json
+seed_check: tools/data/semireal_order_denominator_gate_20260507_0923_seedcheck.json
+
+## Claim Under Test
+
+Verificato dal campo vivo: il cycle 0901 ha nominato `ORDER_DENOMINATOR_GATE`
+come supporto one-sided dell'ordine generato, ma lo ha fatto su perimetri
+sintetici. Il mandato corrente chiede falsificazione su domini non-sintetici o
+semi-reali.
+
+Domanda: se l'endpoint coerente è una sequenza ordinata reale/semi-reale e
+l'endpoint illusorio è una permutazione che preserva la marginale, il supporto
+denominatore resta one-sided o compare un controesempio?
+
+Perimetri:
+
+- `prime_gaps_first`: primi 4096 gap fra primi.
+- `zeta_zero_spacings_first`: primi 512 spacing fra zeri non banali di zeta,
+  calcolati localmente con `mpmath.zetazero`.
+- `logistic_return_intervals`: 4096 intervalli di ritorno a `x > 0.95` nella
+  mappa logistica caotica `x -> 4x(1-x)`.
+
+Gate: osservabile stabile se `abs(original - shuffle_mean) / shuffle_std >= 2`.
+
+## Deposito Numerico
+
+Run principale: `n_replicates=20`, `n_beta=11`, `n_baseline=32`,
+`seed=202605070923`. Seed check: `n_replicates=12`, `n_baseline=24`,
+`seed=202605070924`.
+
+| perimeter | coherent one-sided observables | stable_count coherent | stable_count illusory | endpoint distance gated | beta ambiguous gated |
+|---|---:|---:|---:|---:|---:|
+| prime_gaps_first | SR, L1, triple_var | 3.000 | 0.650 | 3.270 | 0.30 |
+| logistic_return_intervals | [] | 0.200 | 0.100 | 0.000 | [] |
+| zeta_zero_spacings_first | SR, L2 | 2.150 | 0.250 | 2.666 | [] |
+
+Seed check:
+
+| perimeter | coherent one-sided observables | stable_count coherent | stable_count illusory | endpoint distance gated | beta ambiguous gated |
+|---|---:|---:|---:|---:|---:|
+| prime_gaps_first | SR, L1, triple_var | 3.000 | 0.250 | 3.288 | 0.30 |
+| logistic_return_intervals | [] | 0.000 | 0.583 | 0.000 | [] |
+| zeta_zero_spacings_first | SR, L2 | 2.417 | 0.333 | 2.700 | [] |
+
+Endpoint-stable observables: `[]` in all three perimeters in both runs.
+
+## Risultato
+
+1. **The order gate transfers to arithmetic and zeta spacing order.**
+
+   Prime gaps carry one-sided support on `SR`, `L1`, and `triple_var`.
+   Zeta-zero spacings carry one-sided support on `SR` and `L2`. In both cases
+   the illusory endpoint remains weak-denominator under the same marginal.
+
+2. **The logistic return perimeter is the counter-scope.**
+
+   The logistic return sequence is ordered and generated by a deterministic
+   chaotic system, but this canonical observable suite does not read its order
+   as denominator support. The coherent endpoint stable count is `0.200` in the
+   main run and `0.000` in the seed check. The gate does not transfer to this
+   return-time observable.
+
+3. **The transferable object is narrower than "real order".**
+
+   `ORDER_DENOMINATOR_GATE` names order that survives a marginal-preserving
+   shuffle in the canonical gap observables. It does not name every generated
+   sequence. The node regressivo is the observable contract, not the gate
+   threshold: if the order lives in return-time tail structure or symbolic
+   itinerary, `SR/SR2/L1/L2/triple_var` can be blank.
+
+4. **The beta layer is not universal.**
+
+   Prime gaps reproduce beta `0.30` as the ambiguous protocol layer. Zeta has
+   no gated ambiguous beta in this run. Logistic has no gated classifier because
+   the one-sided observable set is empty. This extends 0901: beta `0.30` was a
+   protocol fold in the synthetic matrix, not a cross-domain coordinate.
+
+## Consecutio
+
+`ORDER_DENOMINATOR_GATE` survives as scoped operator:
+
+> In semi-real arithmetic/spectral spacing perimeters, the denominator gate is
+> one-sided support for order against a marginal-preserving shuffle. In
+> logistic return intervals, the canonical gap observables do not carry that
+> support; the gate output is blank rather than false-positive.
+
+Next experiment: do not tune `z_min`. Change the observable perimetro for the
+logistic counter-scope: symbolic itinerary block entropy, return-tail exponent,
+or recurrence-plot diagonal statistics, each with the same original-vs-shuffle
+denominator gate. That tests whether logistic order is absent for this gate or
+only invisible to the current canonical gap suite.
+
+## Self-Audit: 5 Lenti
+
+L1 hard constraint vs bias: no universal claim is made. `endpoint_stable_observables: []`
+is exact for the run perimeters; "weak" means below the declared `abs(z)>=2`
+gate frequency, not numerical zero.
+
+L2 quantity vs ratio: raw stable counts and z means are reported before endpoint
+distances. No percentage drift claim is used.
+
+L3 no silent patching: 0901 claimed transfer on synthetic generated-order
+perimeters. This report narrows the scope after observing the logistic
+counter-perimeter; it does not rescue the original wording.
+
+L4 edge cases: logistic illusory stable_count is `0.583` in the seed check,
+so the claim is not "illusory endpoint absent." It is "no coherent one-sided
+support under the declared frequency rule."
+
+L5 re-discovery vs discovery: prime gap and zeta spacing order-vs-shuffle tests
+sit near known arithmetic/RMT order diagnostics; logistic return intervals sit
+near known recurrence and return-time diagnostics for chaotic maps. No NEW
+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_semireal_order_denominator_gate.py`
+- Verificato: `tools/data/semireal_order_denominator_gate_20260507_0923.json`
+- Verificato: `tools/data/semireal_order_denominator_gate_20260507_0923_seedcheck.json`
+- Inferito: logistic return intervals are a counter-scope for the canonical
+  gap-observable form of `ORDER_DENOMINATOR_GATE`.
diff --git a/tools/data/seme.json b/tools/data/seme.json
index cfd4593825fee57518a2e251e74292bb104253d9..d236f9223863b878fed1dd46bd69c22877e12b4a
--- a/tools/data/seme.json
+++ b/tools/data/seme.json
@@ -38,12 +38,12 @@
     {
       "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.",
+      "claim": "Il denominator gate trasferisce come supporto one-sided dell'ordine quando l'ordine e visibile agli osservabili canonici del perimetro, non come endpoint-stable support a due poli. Nel perimetro sintetico agent_20260507_0901, 4/4 domini non-BOUNDARY hanno endpoint_stable_observables=[] e polo coerente stable_count 3.0-5.0. Nel perimetro semi-reale agent_20260507_0923, primi e zeta trasferiscono (primi: SR,L1,triple_var; zeta: SR,L2), ma logistic_return_intervals e blank: stable_count coerente 0.0-0.2. La beta 0.30 e coordinata del protocollo quando compare, non coordinata universale.",
       "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",
+      "origine": "cycle agent_20260507_0901 + agent_20260507_0923: transfer matrix sintetica e falsificazione semi-reale su primi, zeta, logistic returns",
       "added_at": "2026-05-07T09:01:00+00:00"
     },
     {
@@ -336,4 +336,4 @@
   "_seed_history": {
     "trajectory_apply_20260507_092332": "applied modify_seme from 20260507_0901: direzione → 'Falsificare ORDER_DENOMINATOR_GATE su domini non-sintetici o semi-reali: applicare il gate one-sided...'"
   }
-}
\ No newline at end of file
+}
diff --git a/tools/exp_semireal_order_denominator_gate.py b/tools/exp_semireal_order_denominator_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..f3da4e51c84f066066367b25fded3ebea3c1c00d
--- /dev/null
+++ b/tools/exp_semireal_order_denominator_gate.py
@@ -0,0 +1,352 @@
+#!/usr/bin/env python3
+"""
+exp_semireal_order_denominator_gate.py
+
+Falsification attempt for ORDER_DENOMINATOR_GATE on non-synthetic / semi-real
+ordered sequences. The coherent endpoint is the observed order of each sequence;
+the illusory endpoint is a marginal-preserving permutation. The same
+original-vs-shuffle denominator gate used in the prior reports is applied to
+canonical observables from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import math
+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 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 sieve_primes_for_count(n_primes: int) -> np.ndarray:
+    if n_primes < 6:
+        limit = 20
+    else:
+        limit = int(n_primes * (math.log(n_primes) + math.log(math.log(n_primes))) * 1.25)
+    while True:
+        sieve = np.ones(limit + 1, dtype=bool)
+        sieve[:2] = False
+        for p in range(2, int(limit**0.5) + 1):
+            if sieve[p]:
+                sieve[p * p : limit + 1 : p] = False
+        primes = np.flatnonzero(sieve)
+        if len(primes) >= n_primes:
+            return primes[:n_primes].astype(float)
+        limit *= 2
+
+
+def prime_gap_sequence(n_gaps: int) -> np.ndarray:
+    primes = sieve_primes_for_count(n_gaps + 1)
+    return normalize(np.diff(primes))
+
+
+def zeta_zero_spacings(n_gaps: int) -> np.ndarray:
+    try:
+        import mpmath as mp
+    except ImportError as exc:
+        raise RuntimeError("mpmath is required for zeta_zero_spacings") from exc
+
+    zeros = np.empty(n_gaps + 1, dtype=float)
+    for i in range(n_gaps + 1):
+        zeros[i] = float(mp.im(mp.zetazero(i + 1)))
+    return normalize(np.diff(zeros))
+
+
+def logistic_return_intervals(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    # Return intervals to a high-density-edge event in the fully chaotic logistic map.
+    threshold = 0.95
+    burn = 2000
+    needed = n_gaps + 1
+    returns: list[int] = []
+    last_hit: int | None = None
+    x = float(rng.random())
+    i = 0
+    max_steps = 50_000_000
+    while len(returns) < needed and i < max_steps:
+        x = 4.0 * x * (1.0 - x)
+        if i >= burn and x > threshold:
+            if last_hit is not None:
+                returns.append(i - last_hit)
+            last_hit = i
+        i += 1
+    if len(returns) < needed:
+        raise RuntimeError(f"logistic generator produced {len(returns)} intervals, need {needed}")
+    return normalize(np.array(returns[:n_gaps], dtype=float))
+
+
+def beta_replace(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        return base.copy()
+    if beta >= 1.0:
+        return illusory
+    out = base.copy()
+    mask = rng.random(len(base)) < beta
+    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], dict[str, float]]:
+    original = compute_canonical(gaps)
+    baseline = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline[name].append(obs[name])
+
+    means = {}
+    sds = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline[name], dtype=float)
+        means[name] = float(np.mean(vals))
+        sds[name] = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        z[name] = float((original[name] - means[name]) / sds[name]) if sds[name] > 1e-15 else 0.0
+    return original, means, sds, 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 = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent, illusory])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent, axis=0)
+    illusory_centroid = np.mean(illusory, 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()):
+        margins = []
+        labels = []
+        coords = []
+        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)),
+            "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_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()},
+        }
+
+    one_sided = []
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    for name in OBS_NAMES:
+        coherent_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in coherent_rows]))
+        illusory_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in illusory_rows]))
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            one_sided.append(name)
+        if coherent_freq >= 0.75 and illusory_freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "coherent_one_sided_observables": one_sided,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def analyze_sequence(name: str, base: np.ndarray, 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):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        for beta in betas:
+            gaps = beta_replace(base, beta, rep_rng)
+            obs, shuffle_mean, 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_mean": shuffle_mean,
+                    "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)
+    return {
+        "source": {
+            "n_gaps": int(len(base)),
+            "mean": float(np.mean(base)),
+            "variance": float(np.var(base)),
+        },
+        "gate": gate,
+        "classification_all_observables": classify_layers(rows, OBS_NAMES),
+        "classification_one_sided_gated": classify_layers(rows, gate["coherent_one_sided_observables"]),
+        "rows": rows,
+    }
+
+
+def build_sequences(args: argparse.Namespace, rng: np.random.Generator) -> dict[str, np.ndarray]:
+    sequences = {
+        "prime_gaps_first": prime_gap_sequence(args.n_gaps),
+        "logistic_return_intervals": logistic_return_intervals(args.n_gaps, rng),
+    }
+    if args.include_zeta:
+        sequences["zeta_zero_spacings_first"] = zeta_zero_spacings(args.zeta_gaps)
+    return sequences
+
+
+def compact(perimeters: dict) -> dict:
+    out = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        all_cls = data["classification_all_observables"]
+        gated_cls = data["classification_one_sided_gated"]
+        out[name] = {
+            "n_gaps": data["source"]["n_gaps"],
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+            "z_mean_coherent": gate["layers"]["0.000"]["z_mean"],
+            "z_mean_illusory": gate["layers"]["1.000"]["z_mean"],
+            "endpoint_distance_all": all_cls["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": gated_cls["endpoint_distance"],
+            "ambiguous_beta_one_sided_gated": gated_cls["ambiguous_beta"],
+        }
+    return out
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    sequences = build_sequences(args, root_rng)
+    perimeters = {}
+    for name, base in sequences.items():
+        perimeters[name] = analyze_sequence(name, base, args, root_rng)
+
+    output = {
+        "experiment": "semireal_order_denominator_gate",
+        "category": "gate_falsification_semireal",
+        "question": "Does ORDER_DENOMINATOR_GATE survive on non-synthetic / semi-real ordered sequences?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact(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 n one_sided stable0 stable1 dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>28s} "
+            f"{row['n_gaps']:>5d} "
+            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_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("--zeta-gaps", type=int, default=1024)
+    parser.add_argument("--include-zeta", action="store_true")
+    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=202605070923)
+    parser.add_argument("--out", default="tools/data/semireal_order_denominator_gate_20260507_0923.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

codex
Eseguito il cycle e scritto il report richiesto:

[tools/data/reports/agent_20260507_0923.md](/opt/MM_D-ND/tools/data/reports/agent_20260507_0923.md)

Creato anche lo strumento riusabile:

[tools/exp_semireal_order_denominator_gate.py](/opt/MM_D-ND/tools/exp_semireal_order_denominator_gate.py)

Output dati:

- [tools/data/semireal_order_denominator_gate_20260507_0923.json](/opt/MM_D-ND/tools/data/semireal_order_denominator_gate_20260507_0923.json)
- [tools/data/semireal_order_denominator_gate_20260507_0923_seedcheck.json](/opt/MM_D-ND/tools/data/semireal_order_denominator_gate_20260507_0923_seedcheck.json)

Risultato netto: `ORDER_DENOMINATOR_GATE` resta operatore scoped su primi e zeta, ma non trasferisce ai `logistic_return_intervals` con gli osservabili canonici. Ho aggiornato [tools/data/seme.json](/opt/MM_D-ND/tools/data/seme.json) restringendo il claim al contratto osservabile.

Verifiche fatte: `py_compile` OK, `seme.json` JSON valido. Il `lab_falsifier.py` non ha potuto girare perché tutti i provider LLM locali risultano indisponibili; il report include comunque la self-audit delle 5 lenti.
diff --git a/tools/data/reports/agent_20260507_0923.md b/tools/data/reports/agent_20260507_0923.md
new file mode 100644
index 0000000000000000000000000000000000000000..d7dc7d70d70cbf71dfba4b394064f4bdf842bb15
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0923.md
@@ -0,0 +1,132 @@
+# Agent Report — Semi-Real Order Denominator Gate
+
+timestamp: 2026-05-07 09:23 UTC
+category: gate_falsification_semireal
+verdict: scoped_operator
+observables_registry: 1.0.0-2026-05-06
+observables_used: [SR, SR2, L1, L2, triple_var]
+tool: tools/exp_semireal_order_denominator_gate.py
+data: tools/data/semireal_order_denominator_gate_20260507_0923.json
+seed_check: tools/data/semireal_order_denominator_gate_20260507_0923_seedcheck.json
+
+## Claim Under Test
+
+Verificato dal campo vivo: il cycle 0901 ha nominato `ORDER_DENOMINATOR_GATE`
+come supporto one-sided dell'ordine generato, ma lo ha fatto su perimetri
+sintetici. Il mandato corrente chiede falsificazione su domini non-sintetici o
+semi-reali.
+
+Domanda: se l'endpoint coerente è una sequenza ordinata reale/semi-reale e
+l'endpoint illusorio è una permutazione che preserva la marginale, il supporto
+denominatore resta one-sided o compare un controesempio?
+
+Perimetri:
+
+- `prime_gaps_first`: primi 4096 gap fra primi.
+- `zeta_zero_spacings_first`: primi 512 spacing fra zeri non banali di zeta,
+  calcolati localmente con `mpmath.zetazero`.
+- `logistic_return_intervals`: 4096 intervalli di ritorno a `x > 0.95` nella
+  mappa logistica caotica `x -> 4x(1-x)`.
+
+Gate: osservabile stabile se `abs(original - shuffle_mean) / shuffle_std >= 2`.
+
+## Deposito Numerico
+
+Run principale: `n_replicates=20`, `n_beta=11`, `n_baseline=32`,
+`seed=202605070923`. Seed check: `n_replicates=12`, `n_baseline=24`,
+`seed=202605070924`.
+
+| perimeter | coherent one-sided observables | stable_count coherent | stable_count illusory | endpoint distance gated | beta ambiguous gated |
+|---|---:|---:|---:|---:|---:|
+| prime_gaps_first | SR, L1, triple_var | 3.000 | 0.650 | 3.270 | 0.30 |
+| logistic_return_intervals | [] | 0.200 | 0.100 | 0.000 | [] |
+| zeta_zero_spacings_first | SR, L2 | 2.150 | 0.250 | 2.666 | [] |
+
+Seed check:
+
+| perimeter | coherent one-sided observables | stable_count coherent | stable_count illusory | endpoint distance gated | beta ambiguous gated |
+|---|---:|---:|---:|---:|---:|
+| prime_gaps_first | SR, L1, triple_var | 3.000 | 0.250 | 3.288 | 0.30 |
+| logistic_return_intervals | [] | 0.000 | 0.583 | 0.000 | [] |
+| zeta_zero_spacings_first | SR, L2 | 2.417 | 0.333 | 2.700 | [] |
+
+Endpoint-stable observables: `[]` in all three perimeters in both runs.
+
+## Risultato
+
+1. **The order gate transfers to arithmetic and zeta spacing order.**
+
+   Prime gaps carry one-sided support on `SR`, `L1`, and `triple_var`.
+   Zeta-zero spacings carry one-sided support on `SR` and `L2`. In both cases
+   the illusory endpoint remains weak-denominator under the same marginal.
+
+2. **The logistic return perimeter is the counter-scope.**
+
+   The logistic return sequence is ordered and generated by a deterministic
+   chaotic system, but this canonical observable suite does not read its order
+   as denominator support. The coherent endpoint stable count is `0.200` in the
+   main run and `0.000` in the seed check. The gate does not transfer to this
+   return-time observable.
+
+3. **The transferable object is narrower than "real order".**
+
+   `ORDER_DENOMINATOR_GATE` names order that survives a marginal-preserving
+   shuffle in the canonical gap observables. It does not name every generated
+   sequence. The node regressivo is the observable contract, not the gate
+   threshold: if the order lives in return-time tail structure or symbolic
+   itinerary, `SR/SR2/L1/L2/triple_var` can be blank.
+
+4. **The beta layer is not universal.**
+
+   Prime gaps reproduce beta `0.30` as the ambiguous protocol layer. Zeta has
+   no gated ambiguous beta in this run. Logistic has no gated classifier because
+   the one-sided observable set is empty. This extends 0901: beta `0.30` was a
+   protocol fold in the synthetic matrix, not a cross-domain coordinate.
+
+## Consecutio
+
+`ORDER_DENOMINATOR_GATE` survives as scoped operator:
+
+> In semi-real arithmetic/spectral spacing perimeters, the denominator gate is
+> one-sided support for order against a marginal-preserving shuffle. In
+> logistic return intervals, the canonical gap observables do not carry that
+> support; the gate output is blank rather than false-positive.
+
+Next experiment: do not tune `z_min`. Change the observable perimetro for the
+logistic counter-scope: symbolic itinerary block entropy, return-tail exponent,
+or recurrence-plot diagonal statistics, each with the same original-vs-shuffle
+denominator gate. That tests whether logistic order is absent for this gate or
+only invisible to the current canonical gap suite.
+
+## Self-Audit: 5 Lenti
+
+L1 hard constraint vs bias: no universal claim is made. `endpoint_stable_observables: []`
+is exact for the run perimeters; "weak" means below the declared `abs(z)>=2`
+gate frequency, not numerical zero.
+
+L2 quantity vs ratio: raw stable counts and z means are reported before endpoint
+distances. No percentage drift claim is used.
+
+L3 no silent patching: 0901 claimed transfer on synthetic generated-order
+perimeters. This report narrows the scope after observing the logistic
+counter-perimeter; it does not rescue the original wording.
+
+L4 edge cases: logistic illusory stable_count is `0.583` in the seed check,
+so the claim is not "illusory endpoint absent." It is "no coherent one-sided
+support under the declared frequency rule."
+
+L5 re-discovery vs discovery: prime gap and zeta spacing order-vs-shuffle tests
+sit near known arithmetic/RMT order diagnostics; logistic return intervals sit
+near known recurrence and return-time diagnostics for chaotic maps. No NEW
+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_semireal_order_denominator_gate.py`
+- Verificato: `tools/data/semireal_order_denominator_gate_20260507_0923.json`
+- Verificato: `tools/data/semireal_order_denominator_gate_20260507_0923_seedcheck.json`
+- Inferito: logistic return intervals are a counter-scope for the canonical
+  gap-observable form of `ORDER_DENOMINATOR_GATE`.
diff --git a/tools/data/seme.json b/tools/data/seme.json
index cfd4593825fee57518a2e251e74292bb104253d9..d236f9223863b878fed1dd46bd69c22877e12b4a
--- a/tools/data/seme.json
+++ b/tools/data/seme.json
@@ -38,12 +38,12 @@
     {
       "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.",
+      "claim": "Il denominator gate trasferisce come supporto one-sided dell'ordine quando l'ordine e visibile agli osservabili canonici del perimetro, non come endpoint-stable support a due poli. Nel perimetro sintetico agent_20260507_0901, 4/4 domini non-BOUNDARY hanno endpoint_stable_observables=[] e polo coerente stable_count 3.0-5.0. Nel perimetro semi-reale agent_20260507_0923, primi e zeta trasferiscono (primi: SR,L1,triple_var; zeta: SR,L2), ma logistic_return_intervals e blank: stable_count coerente 0.0-0.2. La beta 0.30 e coordinata del protocollo quando compare, non coordinata universale.",
       "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",
+      "origine": "cycle agent_20260507_0901 + agent_20260507_0923: transfer matrix sintetica e falsificazione semi-reale su primi, zeta, logistic returns",
       "added_at": "2026-05-07T09:01:00+00:00"
     },
     {
@@ -336,4 +336,4 @@
   "_seed_history": {
     "trajectory_apply_20260507_092332": "applied modify_seme from 20260507_0901: direzione → 'Falsificare ORDER_DENOMINATOR_GATE su domini non-sintetici o semi-reali: applicare il gate one-sided...'"
   }
-}
\ No newline at end of file
+}
diff --git a/tools/exp_semireal_order_denominator_gate.py b/tools/exp_semireal_order_denominator_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..f3da4e51c84f066066367b25fded3ebea3c1c00d
--- /dev/null
+++ b/tools/exp_semireal_order_denominator_gate.py
@@ -0,0 +1,352 @@
+#!/usr/bin/env python3
+"""
+exp_semireal_order_denominator_gate.py
+
+Falsification attempt for ORDER_DENOMINATOR_GATE on non-synthetic / semi-real
+ordered sequences. The coherent endpoint is the observed order of each sequence;
+the illusory endpoint is a marginal-preserving permutation. The same
+original-vs-shuffle denominator gate used in the prior reports is applied to
+canonical observables from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import math
+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 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 sieve_primes_for_count(n_primes: int) -> np.ndarray:
+    if n_primes < 6:
+        limit = 20
+    else:
+        limit = int(n_primes * (math.log(n_primes) + math.log(math.log(n_primes))) * 1.25)
+    while True:
+        sieve = np.ones(limit + 1, dtype=bool)
+        sieve[:2] = False
+        for p in range(2, int(limit**0.5) + 1):
+            if sieve[p]:
+                sieve[p * p : limit + 1 : p] = False
+        primes = np.flatnonzero(sieve)
+        if len(primes) >= n_primes:
+            return primes[:n_primes].astype(float)
+        limit *= 2
+
+
+def prime_gap_sequence(n_gaps: int) -> np.ndarray:
+    primes = sieve_primes_for_count(n_gaps + 1)
+    return normalize(np.diff(primes))
+
+
+def zeta_zero_spacings(n_gaps: int) -> np.ndarray:
+    try:
+        import mpmath as mp
+    except ImportError as exc:
+        raise RuntimeError("mpmath is required for zeta_zero_spacings") from exc
+
+    zeros = np.empty(n_gaps + 1, dtype=float)
+    for i in range(n_gaps + 1):
+        zeros[i] = float(mp.im(mp.zetazero(i + 1)))
+    return normalize(np.diff(zeros))
+
+
+def logistic_return_intervals(n_gaps: int, rng: np.random.Generator) -> np.ndarray:
+    # Return intervals to a high-density-edge event in the fully chaotic logistic map.
+    threshold = 0.95
+    burn = 2000
+    needed = n_gaps + 1
+    returns: list[int] = []
+    last_hit: int | None = None
+    x = float(rng.random())
+    i = 0
+    max_steps = 50_000_000
+    while len(returns) < needed and i < max_steps:
+        x = 4.0 * x * (1.0 - x)
+        if i >= burn and x > threshold:
+            if last_hit is not None:
+                returns.append(i - last_hit)
+            last_hit = i
+        i += 1
+    if len(returns) < needed:
+        raise RuntimeError(f"logistic generator produced {len(returns)} intervals, need {needed}")
+    return normalize(np.array(returns[:n_gaps], dtype=float))
+
+
+def beta_replace(base: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
+    illusory = rng.permutation(base)
+    if beta <= 0.0:
+        return base.copy()
+    if beta >= 1.0:
+        return illusory
+    out = base.copy()
+    mask = rng.random(len(base)) < beta
+    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], dict[str, float]]:
+    original = compute_canonical(gaps)
+    baseline = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline[name].append(obs[name])
+
+    means = {}
+    sds = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline[name], dtype=float)
+        means[name] = float(np.mean(vals))
+        sds[name] = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        z[name] = float((original[name] - means[name]) / sds[name]) if sds[name] > 1e-15 else 0.0
+    return original, means, sds, 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 = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    illusory = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([coherent, illusory])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    coherent_centroid = np.mean(coherent, axis=0)
+    illusory_centroid = np.mean(illusory, 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()):
+        margins = []
+        labels = []
+        coords = []
+        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)),
+            "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_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()},
+        }
+
+    one_sided = []
+    endpoint_stable = []
+    coherent_rows = by_beta[0.0]
+    illusory_rows = by_beta[1.0]
+    for name in OBS_NAMES:
+        coherent_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in coherent_rows]))
+        illusory_freq = float(np.mean([abs(row["z"][name]) >= z_min for row in illusory_rows]))
+        if coherent_freq >= 0.75 and illusory_freq < 0.25:
+            one_sided.append(name)
+        if coherent_freq >= 0.75 and illusory_freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "coherent_one_sided_observables": one_sided,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def analyze_sequence(name: str, base: np.ndarray, 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):
+        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+        for beta in betas:
+            gaps = beta_replace(base, beta, rep_rng)
+            obs, shuffle_mean, 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_mean": shuffle_mean,
+                    "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)
+    return {
+        "source": {
+            "n_gaps": int(len(base)),
+            "mean": float(np.mean(base)),
+            "variance": float(np.var(base)),
+        },
+        "gate": gate,
+        "classification_all_observables": classify_layers(rows, OBS_NAMES),
+        "classification_one_sided_gated": classify_layers(rows, gate["coherent_one_sided_observables"]),
+        "rows": rows,
+    }
+
+
+def build_sequences(args: argparse.Namespace, rng: np.random.Generator) -> dict[str, np.ndarray]:
+    sequences = {
+        "prime_gaps_first": prime_gap_sequence(args.n_gaps),
+        "logistic_return_intervals": logistic_return_intervals(args.n_gaps, rng),
+    }
+    if args.include_zeta:
+        sequences["zeta_zero_spacings_first"] = zeta_zero_spacings(args.zeta_gaps)
+    return sequences
+
+
+def compact(perimeters: dict) -> dict:
+    out = {}
+    for name, data in perimeters.items():
+        gate = data["gate"]
+        all_cls = data["classification_all_observables"]
+        gated_cls = data["classification_one_sided_gated"]
+        out[name] = {
+            "n_gaps": data["source"]["n_gaps"],
+            "coherent_one_sided_observables": gate["coherent_one_sided_observables"],
+            "endpoint_stable_observables": gate["endpoint_stable_observables"],
+            "stable_count_coherent": gate["layers"]["0.000"]["stable_count_mean"],
+            "stable_count_illusory": gate["layers"]["1.000"]["stable_count_mean"],
+            "z_mean_coherent": gate["layers"]["0.000"]["z_mean"],
+            "z_mean_illusory": gate["layers"]["1.000"]["z_mean"],
+            "endpoint_distance_all": all_cls["endpoint_distance"],
+            "endpoint_distance_one_sided_gated": gated_cls["endpoint_distance"],
+            "ambiguous_beta_one_sided_gated": gated_cls["ambiguous_beta"],
+        }
+    return out
+
+
+def run(args: argparse.Namespace) -> dict:
+    root_rng = np.random.default_rng(args.seed)
+    sequences = build_sequences(args, root_rng)
+    perimeters = {}
+    for name, base in sequences.items():
+        perimeters[name] = analyze_sequence(name, base, args, root_rng)
+
+    output = {
+        "experiment": "semireal_order_denominator_gate",
+        "category": "gate_falsification_semireal",
+        "question": "Does ORDER_DENOMINATOR_GATE survive on non-synthetic / semi-real ordered sequences?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "matrix": compact(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 n one_sided stable0 stable1 dist_gate ambiguous_gate")
+    for name, row in output["matrix"].items():
+        print(
+            f"{name:>28s} "
+            f"{row['n_gaps']:>5d} "
+            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_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("--zeta-gaps", type=int, default=1024)
+    parser.add_argument("--include-zeta", action="store_true")
+    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=202605070923)
+    parser.add_argument("--out", default="tools/data/semireal_order_denominator_gate_20260507_0923.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

tokens used
122,772
Eseguito il cycle e scritto il report richiesto:

[tools/data/reports/agent_20260507_0923.md](/opt/MM_D-ND/tools/data/reports/agent_20260507_0923.md)

Creato anche lo strumento riusabile:

[tools/exp_semireal_order_denominator_gate.py](/opt/MM_D-ND/tools/exp_semireal_order_denominator_gate.py)

Output dati:

- [tools/data/semireal_order_denominator_gate_20260507_0923.json](/opt/MM_D-ND/tools/data/semireal_order_denominator_gate_20260507_0923.json)
- [tools/data/semireal_order_denominator_gate_20260507_0923_seedcheck.json](/opt/MM_D-ND/tools/data/semireal_order_denominator_gate_20260507_0923_seedcheck.json)

Risultato netto: `ORDER_DENOMINATOR_GATE` resta operatore scoped su primi e zeta, ma non trasferisce ai `logistic_return_intervals` con gli osservabili canonici. Ho aggiornato [tools/data/seme.json](/opt/MM_D-ND/tools/data/seme.json) restringendo il claim al contratto osservabile.

Verifiche fatte: `py_compile` OK, `seme.json` JSON valido. Il `lab_falsifier.py` non ha potuto girare perché tutti i provider LLM locali risultano indisponibili; il report include comunque la self-audit delle 5 lenti.
