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: 019df62f-a9bd-74e0-9f86-503e47d5b5e1
--------
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_20260505_0330.md
warning: Codex could not find bubblewrap on PATH. Install bubblewrap with your OS package manager. See the sandbox prerequisites: https://developers.openai.com/codex/concepts/sandboxing#prerequisites. Codex will use the vendored bubblewrap in the meantime.
codex
Leggo prima il contesto operativo e il campo vivo, poi scelgo un esperimento coerente con le ultime tensioni e lascio il report nel percorso richiesto.
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.

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

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

**4. Coincidenze numeriche trattate come struttura.**
0.606 ≈ 1/φ = 0.618 (2% di differenza). Non è una connessione — è rumore fino a prova contraria (C2 del condensato). Ogni volta che un numero è "vicino a" φ, √5, π, e, 1/137: non è prova di nulla. Serve un meccanismo, non una vicinanza.

**5. Usare lo stesso dato come input e come test.**
Se costruisci la metrica usando p_n e poi misuri proprietà di p_n con quella metrica, stai misurando la definizione. Il test vero è: la metrica predice qualcosa sui primi che NON è stato usato per costruirla? Se no, è circolare.

**6. Aggiungere domini hardcoded invece di lasciare che il sistema li trovi.**
Il lab non è una calcolatrice con domini pre-scritti. Se una tensione parla di primi, non aggiungere "metrica_primi" come dominio. Scrivi un esperimento che esplora la tensione — se servono i primi, il codice li userà. Il sistema decide cosa fare, non il programmatore.

**7. Usare numeri per vincolare concetti (det=+1).**
Esempio reale: `intensità: 0.65` trattata come soglia → `if intensita > 0.5: conferma`. Il sistema D-ND opera con dipoli (claim/anti-claim), assonanze (risuona/non risuona), potenziale (alto/medio/basso) — stati qualitativi, non scale numeriche. Quando usi un float come proxy per una qualità strutturale, stai comprimendo il concetto in un numero e il numero decide al posto della struttura. Lo stesso vale per "maturity > 0.99", "confidence < 0.7", "score = rank * 10 + intensita".
**Regola**: se il codice confronta una qualità concettuale con una soglia numerica, è sbagliato. Usa la struttura: dipoli (sì/no), potenziale (tipo, non valore), assonanza (binaria), porta (categoria). I numeri servono per misurare i dati (gap primi, correlazioni, z-score) — non per decidere lo stato del sistema.
Se trovi questo pattern in un tool che stai modificando, correggilo. Non serve riscrivere tutto — correggi dove passi. Il sistema evolve organicamente.

## Come evitarli

- **Prima il null test, poi l'interpretazione.** Ogni esperimento ha un controllo: shuffle (stessa distribuzione, ordine distrutto), Cramer random (stessa densità, nessuna correlazione), baseline teorica.
- **Il risultato non è nel numero — è nella differenza col controllo.** z-score, non valore assoluto.
- **Se il risultato spiega se stesso, non è un risultato.** Chiediti: "questo segue dalla definizione?" Se sì, cerca il contenuto altrove.
- **Non lanciare un esperimento per confermare. Lancialo per scoprire.** La domanda giusta non è "è vero X?" ma "cosa succede se misuro Y?"

## Auto-evoluzione — il sistema corregge se stesso

Il post-processing del lab (step 8 in lab_agent.sh) esegue `structural_check.py` sui file che hai toccato.
Se trova anti-pattern strutturali, genera una tensione META nel seme. Il ciclo successivo la vede e corregge.

**Come funziona:**
- Tu scrivi/modifichi codice → il post-processing lo scansiona
- Se trova numeri che vincolano concetti (errore #7) o altri pattern noti, crea una tensione
- Il prossimo ciclo legge quella tensione e la risolve dove passa
- Non serve riscrivere tutto — il sistema evolve organicamente, un file alla volta

**Se scopri un nuovo anti-pattern:**
- Non limitarti a corregere il codice — aggiungi il pattern a `tools/structural_check.py` nella lista `PATTERNS`
- Così il sistema lo riconoscerà autonomamente nei cicli futuri
- L'errore pagato una volta non si ripete — la consapevolezza si propaga

Questo è f(f(x)): il sistema che migliora il sistema che migliora se stesso.

## Cosa NON fare

- Non modificare CONDENSATO.md, KERNEL_SEED.md, o file del kernel
- Non committare — salva solo in tools/data/ e tools/exp_*.py
- Non inventare dati o risultati
- Non cercare φ — crea le condizioni, osserva cosa emerge
- Non superare 20 minuti di lavoro per ciclo
- Non produrre liste di possibilità — produci UNA risultante

## Formato report

```markdown
# Agent Report — TITOLO
**Date**: YYYY-MM-DD HH:MM
**Piano**: N
**Tension explored**: ID (intensità)

## Claim Under Test
> Il claim dalla tensione

## Question
La domanda che hai formulato

## Experiment Design
- Metrica, scope, null baseline, N campioni

## Results
Tabella con numeri reali

## Key Findings
1. Cosa hai trovato (con evidenza)

## Verdict
NEW / CONFIRMED / FALSIFIED / CONSTRAINT

## Bicono della scoperta
(Obbligatoria. Nomina la struttura. Se non riesci, l'esperimento non è ancora filtrato.)

- **Due radici** (dipolo primario, già duali e invertite): <quali sono le due facce della scoperta>
- **Singolare** (qualità del 1-che-è-tutto in questo contesto, dove la dualità non c'è): <cosa>
- **Invariante di passaggio** (cosa sopravvive al passaggio del vertice): <cosa>
- **Campo di possibilità**: qui diventa possibile <X>; qui diventa non-possibile <Y>

Riferimenti: CONDENSATO A16, method/DND_POSSIBILITA.md.

## Files
- Script, dati, report
```

## Bicono della scoperta — come compilarlo

Non è riformulazione ornamentale del Verdict. È **filtro**: la scoperta passa
per il modello e torna spogliata dei bias. Se la struttura (radici · singolare
· invariante · campo) non si riconosce, la scoperta è rumore o è incompleta.

**Esempio retroattivo — TWO_CHANNEL_DECOMPOSITION:**
- Radici: canale magnitudine · canale residuo (segno invertito — uno aggiunge,
  l'altro sottrae sulla PNT)
- Singolare: il segnale totale prima della separazione. Non esiste come ente
  autonomo, esiste solo come sovrapposizione dei due canali.
- Invariante: la chiusura algebrica del residuo al 3° ordine Markov.
- Campo: possibile → predire lo slope PSD magnitudine dalle correlazioni
  Hardy-Littlewood. Non-possibile → trattare il residuo come random noise.

**Esempio retroattivo — DUALITA_DIPOLARE_VS_ILLUSORIA:**
- Radici: dipolo ordinato · dipolo mescolato
- Singolare: la sequenza in sé, prima della distinzione ordine/disordine
- Invariante: det=-1 quando l'ordine è reale; det=+1 quando illusorio
- Campo: possibile → discriminare dipoli reali da illusori via test di
  shuffle. Non-possibile → inferire dipolarità da statistica locale senza

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 (522s) — parti dalla consecutio.

_Affinatore del run precedente: pending (affinatore empty output (exit 0))_

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

## Tensioni attive
- [TRASCENDENZA_LIMITE] (0.9) La trascendenza e il limite attuale del modello. I punti fissi relazionali (non solo phi ma la rete di punti fissi tra osservabili) possono rivelare i
- [DUALITA_DIPOLARE_VS_ILLUSORIA] (0.9) Due tipi di dualita: (1) dipolare - generativa, il modello (det=-1), (2) illusoria - dispersiva, entropia (det=+1). Le regole incoerenti producono la 
- [METRIC_TENSOR] (0.9) Il tensore metrico dei primi è g=(p/2)². Nel tempo ln(p), è de Sitter 1+1D. z=-8.8 curvatura vs z=+22.5 rapporti ΔΓ.
- [TENSIONE_ENTITA] (0.85) La tensione non e un problema pratico - e un Entita. La tensione superflua crea latenza (tempo). Senza tensione superflua tutto e regolato da assiomi.
- [G_POTENZIALE_NULLA] (0.85) G e il potenziale di tutto come nulla - permette il prima e il dopo. Ci muoviamo come trascendenza dimensionale gravitazionale. G nel tetraedro non e 
- [BOUNDARY] (0.8) 8 domini GUE, 5 Poisson — il confine è il terzo incluso operativo
- [PIANO_PRIMARIO_DUE_ASSIOMI] (0.8) 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 osse
- [META] (0.5) Tutti i 11 test passano — verifica che non stiamo testando solo tautologie

## 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
  "confine" → BOUNDARY, TRASCENDENZA_LIMITE
  "trascendenza" → G_POTENZIALE_NULLA, TRASCENDENZA_LIMITE
  "nelle" → PIANO_PRIMARIO_DUE_ASSIOMI, TRASCENDENZA_LIMITE
  "producono" → TENSIONE_ENTITA, DUALITA_DIPOLARE_VS_ILLUSORIA
  "tutto" → G_POTENZIALE_NULLA, TENSIONE_ENTITA
Questo è dove il potenziale si concentra. Non ignorarlo.

## Ultimi 3 run — da dove parti
### Agent Report — Markov Layers Pass the First Recovery Gate, but Not All Observables Pass
Trovato: 1. **Il nucleo pair/triple non cade al primo audit.** `empirical_Mk0` recupera solo Layer 0; `empirical_Mk1` recupera solo Layer 1. Questo supporta l'uso di SR e L1 come Layer 1 e di L2/SR2 come Layer 2 nel perimetro testato.
2. **`cond_entropy` non può essere usata come prova forte di Layer 3 nel s
Verdetto: **CONSTRAINT on META + BOUNDARY**: la decomposizione two-layer è utilizzabile solo con perimetro atomico.

Perimetro corretto: nei 60,000 gap primi te

### Agent Report — The Two Markov Layers Are Coupled at the Boundary: One Phase Transition, Two Projections
Trovato: 1. **The two Markov layers are coupled at the boundary.** For primes, the critical alpha is identical across all 4 observables (0.334). For GUE, the difference is 0.024 (within the alpha step resolution of 0.047). The partial shuffle destroys pair-statistics and triple-statistics at the same rate. T
Verdetto: **CONSTRAINT on BOUNDARY + DIPOLAR_ORDERING**: The two Markov layers (pairs → plane, triples → depth) are coupled at the partial-shuffle boundary. The

### Agent Report — Markov Memory Has Two Visible Layers: Pairs Shape the Angle, Triples Shape the Depth
Trovato: 1. **Prime gap memory has exactly two visible layers.** Layer 1 (pair correlations, Mk1) shapes the dipolar plane (SR, L1). Layer 2 (triple correlations, Mk2) shapes the depth (SR2, L2, cond_entropy, triple_var, num_var_10). These are orthogonal: Layer 1 produces z ~ 0 for all Layer 2 observables, a
Verdetto: **CONFIRMED + NEW on DIPOLAR_ORDERING**: The prime gap ordering decomposes into two independent visible layers. Layer 1 (pairs) lives in (SR, L1) = th

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

## Topologia del campo — la forma del grafo
Gradi teorie: Q=12, G=8, 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: 3 — connessioni mature che attendono cristallizzazione (non da generare, da riconoscere).
Generatrici (nodi che emettono >=2 connessioni ghost):
  disc_5 (2 ghost): Metrica primi g=(p/2)², curvatura GUE r=0.503
  report_20260504_0901 (2 ghost): The Two Markov Layers Are Coupled at the Boundary: One Phase Transitio
  report_20260501_0931 (2 ghost): The GUE-Poisson Crossover Has a Phase Transition: Direction Locks, Mag
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 "ls -R tools/data | sed -n '1,220p'" in /opt/MM_D-ND
 succeeded in 0ms:
tools/data:
3d_boundary_layers.json
STUDIO_SIMBOLISMO_DND.md
agent_field_live.md
alignment_active.json
alignment_markers.jsonl
arxiv_cache.json
audit_paper_A_draft3.json
audit_paper_B_draft3.json
audit_paper_C_draft2.json
audit_paper_D_draft2.json
audit_paper_D_draft3.json
audit_paper_E_draft3.json
audit_paper_F_draft2.json
audit_paper_F_draft3.json
audit_paper_G_draft3.json
autoricerca_journal.json
autoricerca_state.json
banchi_custom
bicono_projections.jsonl
bloch_explorer_results.json
bloch_search.log
bloch_search_results.json
boundary_coherence.json
boundary_shuffle_audit.json
brody_calibration_results.json
brody_flow.json
ciclo_memoria.json
cognitive_fingerprint.json
conoscenza_generata.json
conoscenza_teorie.json
conoscenza_teorie.json.bak.retraction_22_04
consecutio.json
consecutio_processata.json
costante_dinamica.json
cross_domain_dipolar_direction.json
cross_observable_consistency.json
crossover_phase_test.json
curva_results.json
curvature_distributions.png
cycle
diagrams
dinamiche.json
dipartimento_journal.jsonl
dipolar_crossover.json
dipolar_vector_scaling.json
dipolo_lab
domandatore
domande_fondamentali.json
doppia_fenditura_20260314.json
doppio_cono_dnd.png
engine_state.json
evolution
exp_acf_range_universality.json
exp_acf_stationarity.json
exp_acf_z6z_mechanism.json
exp_beta_crossover.json
exp_coherence_length.json
exp_conditional_r.json
exp_det_drift.json
exp_markov_psd_prediction.json
exp_poisson_convergence.json
exp_psd_amp_scaling.json
exp_spectral_2d.json
exp_spectral_landscape.json
exp_two_channel_decomposition.json
exp_two_channel_psd.json
exp_two_channel_universality.json
experiment_results.json
explorer_20260313_0954.json
gap_resolution.json
godel_configs
implications_state.json
incrocio_20260331_0345.json
incrocio_20260331_1807.json
incrocio_20260401_0344.json
incrocio_20260402_0344.json
incrocio_20260402_0755.json
incrocio_20260402_0803.json
incrocio_20260402_0808.json
incrocio_20260402_0809.json
incrocio_20260403_0330.json
incrocio_20260404_0330.json
incrocio_20260404_1852.json
incrocio_20260405_0330.json
incrocio_20260405_0715.json
incrocio_20260405_0723.json
incrocio_20260405_0730.json
incrocio_20260405_0753.json
incrocio_20260420_1856.json
incrocio_20260421_0720.json
incrocio_20260422_0336.json
incrocio_20260423_0335.json
incrocio_20260424_0347.json
incrocio_20260425_0339.json
incrocio_20260428_0340.json
incrocio_20260429_0859.json
incrocio_risultato.json
indeterminazione_results.json
interferenza_zeri_20260314.json
iterata_M_confronto_20260312_1254.json
knowledge_state.json
knowledge_state_pre_fix.json
lab_bridge_issues.jsonl
lab_data.json
lab_errori.json
lab_graph.json
lab_health.json
lab_logiche_corpus.md
lab_registro.json
lab_results.json
lab_riflessi.json
lab_risultante.json
lab_session_log.jsonl
lab_vault.json
lab_vincoli.md
learning_curve_100k.json
loop_insights.json
loop_state.json
m_spectro_11domini.json
m_spectro_18_domains.png
m_spectro_9_domains.png
m_spectro_calibra_20260310_2015.json
m_spectro_confronto_20260310_1959.json
m_spectro_godel_attrito_20260318_0949.json
m_spectro_godel_densita_20260318_0949.json
m_spectro_godel_risonanza_20260318_0949.json
magnitude_psd_from_acf.json
markov3_observable_hunt.json
markov_dipolar_decomposition.json
markov_k_direction.json
markov_layer_recovery_audit.json
markov_memory_by_gue_type.json
markov_scale_function.json
meta_tautology_test.json
mod3_scaling.json
mod3_vs_residual_ordering.json
modular_algebra_depth.json
modular_memory_spectrum.json
multi_pattern_results.json
neuron_snapshot.json
notte_20260302_0330.md
notte_20260303_0330.md
notte_20260304_0330.md
notte_20260305_0330.md
notte_20260306_0330.md
notte_20260307_0330.md
notte_20260308_0330.md
notte_20260309_0330.md
notte_20260310_0330.md
notte_20260311_0330.md
notte_20260312_0330.md
notte_20260313_0330.md
notte_20260314_0330.md
notte_20260315_0330.md
notte_20260315_0749.md
notte_20260317_0330.md
notte_20260318_0330.md
notte_20260319_0330.md
notte_20260320_0330.md
notte_20260321_0330.md
notte_20260322_0330.md
notte_20260323_0330.md
notte_20260324_0330.md
notte_20260325_0330.md
notte_20260326_0330.md
notte_20260327_0330.md
notte_20260328_0330.md
notte_20260329_0330.md
notte_20260330_0330.md
notte_20260331_0330.md
notte_20260331_1753.md
notte_20260401_0330.md
notte_20260402_0330.md
occhio
odlyzko_100k_probe.json
odlyzko_block2_probe.json
odlyzko_cache
odlyzko_probe_results.json
paper_H_results.json
piano11_results.json
piano11b_gue_test.json
piano11e_results.json
pipeline_state.json
ponti_evoluti.json
prime_gaps_spectrum.png
prime_gaps_spectrum_pub.png
projective_quantization_results.json
proto_oom_001.json
psd_prime_gaps_results.json
quantization_results.json
r_excess_analysis.json
r_excess_l_functions.json
r_ratio_decay.json
refresh_detector_state.json
reports
research_kb.json
research_protocols.json
residuo_ordine_9domini.json
retriever_risultati.json
riformulazioni.json
risultante_overview.png
risultante_results.json
risultante_v2.json
rottura_phi2_results.json
seed_insight_instruction.md
selective_layer_decoupling.json
seme.json
seme_archive
seme_axioms.json
seme_backup_pre_run.json
specchio_20260314.json
spectral_gap_results.json
spectral_rigidity_results.json
spettro_zeta_results.json
spirale_M_primi.png
stato_ciclo.json
synthetic_validation.json
tension_to_theory.json
test_rarefazione_20260313.json

codex
Il campo punta esplicitamente a META: non basta confermare i layer Markov, serve testare se la batteria di osservabili sta misurando struttura reale o ridondanza/tautologia. Eseguo il proiettore e poi ispeziono gli script/risultati recenti per costruire un test non ripetitivo.
exec
/bin/bash -lc "sed -n '1,240p' tools/data/meta_tautology_test.json" in /opt/MM_D-ND
 succeeded in 0ms:
{
  "n_gaps": 25996,
  "start_index": 6499,
  "mean_gap": 12.049546084012926,
  "n_trials": 12,
  "real": {
    "r_stat": 0.46541700281389103,
    "mod3": 0.158032416906032,
    "lag1": -0.056325263440536975,
    "dr_ord": -8.613350757569501,
    "dr_mag": -14.659110640739819
  },
  "synthetics": {
    "shuffled": {
      "r_stat": {
        "mean": 0.48840785374248896,
        "std": 0.0012290462513744839,
        "min": 0.4868748132626151,
        "max": 0.49095576092313753
      },
      "mod3": {
        "mean": 0.3419034673779237,
        "std": 0.003080340361757475,
        "min": 0.33858227328682805,
        "max": 0.3473533032416906
      },
      "lag1": {
        "mean": 0.0008701010105107888,
        "std": 0.005508163551974561,
        "min": -0.006819710589920439,
        "max": 0.010440073388531585
      },
      "dr_ord": {
        "mean": -0.022481562648025127,
        "std": 1.0536645988810507,
        "min": -1.7875600198203863,
        "max": 2.1150425182293024
      },
      "dr_mag": {
        "mean": -0.34002039737072903,
        "std": 1.3495871190910325,
        "min": -2.483657811460425,
        "max": 2.586747411171497
      }
    },
    "cramer": {
      "r_stat": {
        "mean": 0.43693551982498063,
        "std": 0.0016314949447449726,
        "min": 0.4341379494007212,
        "max": 0.43921011692370304
      },
      "mod3": {
        "mean": 0.3504744502147849,
        "std": 0.0029494854711713215,
        "min": 0.34333525678015003,
        "max": 0.35456818618965186
      },
      "lag1": {
        "mean": 0.00042110177809214525,
        "std": 0.003362025241838675,
        "min": -0.007129290686896228,
        "max": 0.008512610772295011
      },
      "dr_ord": {
        "mean": 0.2716348998613541,
        "std": 1.1471147960920434,
        "min": -1.8471628111210057,
        "max": 1.8575793441626824
      },
      "dr_mag": {
        "mean": 0.006409785191452438,
        "std": 1.350016448747991,
        "min": -2.438781125293534,
        "max": 2.4225009013261842
      }
    },
    "HL_markov": {
      "r_stat": {
        "mean": 0.4772924074853935,
        "std": 0.002356346217119225,
        "min": 0.4721228320932162,
        "max": 0.47995951901485473
      },
      "mod3": {
        "mean": 0.3471951511421146,
        "std": 0.004069684434749082,
        "min": 0.3415572425112844,
        "max": 0.352944193680755
      },
      "lag1": {
        "mean": -0.05147823605733929,
        "std": 0.0078124992367723444,
        "min": -0.06373575068491619,
        "max": -0.03754875313609329
      },
      "dr_ord": {
        "mean": -4.992151323768588,
        "std": 1.9048251829770912,
        "min": -9.263778688307587,
        "max": -2.741673892039502
      },
      "dr_mag": {
        "mean": -0.38767091897163786,
        "std": 1.1311110534894364,
        "min": -2.1491072342298505,
        "max": 1.3375474767946507
      }
    }
  },
  "verdicts": {
    "r_stat": {
      "verdict": "STRUCTURAL",
      "z_scores": {
        "shuffled": 18.706253637062257,
        "cramer": 17.457291585972516,
        "HL_markov": 5.039753742546834
      }
    },
    "mod3": {
      "verdict": "STRUCTURAL",
      "z_scores": {
        "shuffled": 59.691796625777116,
        "cramer": 65.2459675168642,
        "HL_markov": 46.48093414182973
      }
    },
    "lag1": {
      "verdict": "TAUTOLOGICAL",
      "z_scores": {
        "shuffled": 10.383744763744476,
        "cramer": 16.878625566391104,
        "HL_markov": 0.6204195655806255
      }
    },
    "dr_ord": {
      "verdict": "TAUTOLOGICAL",
      "z_scores": {
        "shuffled": 8.15332431595071,
        "cramer": 7.74550697776039,
        "HL_markov": 1.901066547292998
      }
    },
    "dr_mag": {
      "verdict": "STRUCTURAL",
      "z_scores": {
        "shuffled": 10.60997844511335,
        "cramer": 10.863216103419518,
        "HL_markov": 12.61718703723095
      }
    }
  }
}
exec
/bin/bash -lc "ls tools/exp_*.py | sed -n '1,220p'" in /opt/MM_D-ND
 succeeded in 0ms:
tools/exp_3d_boundary_layers.py
tools/exp_acf_amplitude_scaling.py
tools/exp_acf_range_universality.py
tools/exp_acf_stationarity.py
tools/exp_acf_z6z_mechanism.py
tools/exp_alpha_stability.py
tools/exp_beta_crossover.py
tools/exp_boundary_coherence.py
tools/exp_boundary_growth.py
tools/exp_boundary_gue_poisson.py
tools/exp_boundary_shuffle_audit.py
tools/exp_brody_calibration.py
tools/exp_brody_crossover.py
tools/exp_brody_flow.py
tools/exp_coherence_length.py
tools/exp_coherence_robustness.py
tools/exp_cross_domain_dipolar_direction.py
tools/exp_cross_observable_consistency.py
tools/exp_crossover_phase_test.py
tools/exp_crossover_universality.py
tools/exp_dR_brody_connection.py
tools/exp_desitter_unification.py
tools/exp_det_drift.py
tools/exp_dipolar_angle_reference.py
tools/exp_dipolar_crossover.py
tools/exp_dipolar_vector_scaling.py
tools/exp_excess_scaling.py
tools/exp_geodesic_deviation_primes.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_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_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_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
exec
/bin/bash -lc 'rg -n "meta_tautology|markov_layer|cond_entropy|empirical_Mk|SR2|triple_var|num_var" tools -S' in /opt/MM_D-ND
 succeeded in 110ms:
 succeeded in 118ms:
tools/data/lab_health.json:14:    "last_text": "Done. Summary of results:\n\n**Experiment**: Observable hunt for Markov-3 memory visibility in prime gaps.\n\n**Finding**: Prime gap ordering decomposes into **two visible layers**, orthogonal to each other:\n- **Layer 1** (pairs, Mk1): lives in (SR, L1) — the dipolar plane. Already known.\n- **Layer 2** (triples, Mk2): lives in **SR2** (next-nearest-neighbor spacing ratio), L2, conditional entropy. **SR2 is the sharpest probe**: z = -9.4 under Mk1 (pair model misses it), z = -0.05 under Mk2 (triple m",
tools/data/lab_graph.json:497:        "findings": "1. **Il nucleo pair/triple non cade al primo audit.** `empirical_Mk0` recupera solo Layer 0; `empirical_Mk1` recupera solo Layer 1. Questo supporta l'uso di SR e L1 come Layer 1 e di L2/SR2 come Layer 2 nel perimetro testato.\n2. **`cond_entropy` non può essere usata come prova forte di Layer 3 nel s",
tools/data/lab_graph.json:590:        "findings": "1. **Prime gap memory has exactly two visible layers.** Layer 1 (pair correlations, Mk1) shapes the dipolar plane (SR, L1). Layer 2 (triple correlations, Mk2) shapes the depth (SR2, L2, cond_entropy, triple_var, num_var_10). These are orthogonal: Layer 1 produces z ~ 0 for all Layer 2 observables, a",
tools/data/lab_graph.json:1264:      "claim": "La pipeline Mk0/Mk1/Mk2 recupera correttamente i controlli empirici Mk0 e Mk1, ma sovrastima alcuni osservabili: cond_entropy su controllo Mk2 viene l",
tools/data/lab_graph.json:1278:      "findings": "1. **Il nucleo pair/triple non cade al primo audit.** `empirical_Mk0` recupera solo Layer 0; `empirical_Mk1` recupera solo Layer 1. Questo supporta l'uso di SR e L1 come Layer 1 e di L2/SR2 come Layer 2 nel perimetro testato.\n2. **`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 Mk",
tools/data/lab_graph.json:1279:      "content_preview": "# Agent Report — Markov Layers Pass the First Recovery Gate, but Not All Observables Pass\n**Date**: 2026-05-04 12:19\n**Piano**: 61\n**Tension explored**: META (0.5)\n\n## Claim Under Test\n> Tutti i 11 test passano — verifica che non stiamo testando solo tautologie.\n\n## Question\nSe la pipeline Mk0/Mk1/Mk2 vede due layer, li recupera anche quando l'ordine vero della sequenza è noto?\n\n## Experiment Design\n- **Primary audit**: `tools/exp_markov_layer_recovery_audit.py`\n- **Scope**: 60,000 gap/spacings ",
tools/data/lab_graph.json:1280:      "content_full": "# Agent Report — Markov Layers Pass the First Recovery Gate, but Not All Observables Pass\n**Date**: 2026-05-04 12:19\n**Piano**: 61\n**Tension explored**: META (0.5)\n\n## Claim Under Test\n> Tutti i 11 test passano — verifica che non stiamo testando solo tautologie.\n\n## Question\nSe la pipeline Mk0/Mk1/Mk2 vede due layer, li recupera anche quando l'ordine vero della sequenza è noto?\n\n## Experiment Design\n- **Primary audit**: `tools/exp_markov_layer_recovery_audit.py`\n- **Scope**: 60,000 gap/spacings per sequenza; 20 surrogati per livello Mk; seed `20260504`.\n- **Target**: prime gaps, ordine sconosciuto, solo confronto.\n- **Known-order controls**:\n  - `empirical_Mk0`: permutazione dei gap primi, ordine noto 0.\n  - `empirical_Mk1`: surrogate Mk1 addestrato sui gap primi, ordine noto 1.\n  - `empirical_Mk2`: surrogate Mk2 addestrato sui gap primi, ordine noto 2.\n  - `poisson_iid`: spacings esponenziali iid, ordine noto 0.\n- **Recovery rule**: un controllo passa se nessun osservabile viene classificato con layer maggiore dell'ordine noto.\n- **Secondary calibration**: `tools/exp_meta_tautology_test.py --n-primes 300000 --n-trials 12`. Nota: il controllo chiamato `HL_markov` nello script è un AR(1) gap model che preserva lag-1, non un modello Hardy-Littlewood aritmetico.\n\n## Results\n\n### Recovery audit\n\n| Sequence | Known order | Recovery | Max layer | Over-layer observables |\n|---|---:|---|---:|---|\n| `prime_gaps` | target | target only | 3 | - |\n| `empirical_Mk0` | 0 | PASS | 0 | - |\n| `empirical_Mk1` | 1 | PASS | 1 | - |\n| `empirical_Mk2` | 2 | FAIL | 3 | `cond_entropy` |\n| `poisson_iid` | 0 | FAIL | 1 | `num_var_10` |\n\nPrime target layers, reported without recovery verdict:\n\n| Observable | Layer | z vs Mk0 | z vs Mk1 | z vs Mk2 |\n|---|---:|---:|---:|---:|\n| SR | 1 | -22.21 | -0.08 | -0.16 |\n| L1 | 1 | -12.25 | 0.79 | 0.86 |\n| L2 | 2 | -3.60 | -4.09 | -0.19 |\n| SR2 | 2 | -6.67 | -8.80 | -0.33 |\n| cond_entropy | 3 | -435.40 | -26.11 | 2.56 |\n| triple_var | 1 | -15.90 | -0.53 | 0.36 |\n| num_var_10 | 1 | -7.46 | -1.42 | 0.00 |\n| run_length | 3 | 27.59 | 7.19 | 3.43 |\n\nControl failure details:\n\n| Control | Observable | Expected max | Assigned | Diagnostic z |\n|---|---|---:|---:|---:|\n| `empirical_Mk2` | `cond_entropy` | 2 | 3 | z vs Mk2 = 3.15 |\n| `poisson_iid` | `num_var_10` | 0 | 1 | z vs Mk0 = 2.13 |\n\n### Secondary tautology calibration\n\n| Observable | Verdict in tested controls | Blocking control |\n|---|---|---|\n| r-stat | STRUCTURAL | none in this run |\n| mod3 fraction | STRUCTURAL | none in this run |\n| lag1 ACF | TAUTOLOGICAL | lag1-matched AR(1), z = -0.62 |\n| dr_ord | TAUTOLOGICAL | lag1-matched AR(1), z = -1.90 |\n| dr_mag | STRUCTURAL | none in this run |\n\nNumeri principali: prime mod3 same-class fraction = 0.158032. Questo è bias forte rispetto ai controlli ~0.34-0.35, non proibizione zero.\n\n## Key Findings\n1. **Il nucleo pair/triple non cade al primo audit.** `empirical_Mk0` recupera solo Layer 0; `empirical_Mk1` recupera solo Layer 1. Questo supporta l'uso di SR e L1 come Layer 1 e di L2/SR2 come Layer 2 nel perimetro testato.\n2. **`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.\n3. **`num_var_10` produce un falso positivo debole su iid.** `poisson_iid` viene letto Layer 1 per `num_var_10` con z = 2.13. Il segnale supera appena la soglia operativa, quindi va trattato come diagnostica secondaria finché non passa audit multi-seed.\n4. **Il vecchio controllo `HL_markov` va rinominato nel linguaggio dei report.** Lo script non implementa Hardy-Littlewood; implementa un AR(1) lag1-matched. I claim futuri non devono chiamarlo controllo HL aritmetico.\n\n## Verdict\n**CONSTRAINT on META + BOUNDARY**: la decomposizione two-layer è utilizzabile solo con perimetro atomico.\n\nPerimetro corretto: nei 60,000 gap primi testati con 20 surrogati Mk per livello, il nucleo `SR,L1` viene catturato da Mk1 e il nucleo `L2,SR2` da Mk2. Le diagnostiche `cond_entropy`, `num_var_10`, `run_length` non sono evidenza primaria finché non passano un recovery audit multi-seed e, idealmente, surrogati Markov esatti.\n\nNon è 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.\n\n## Bicono della scoperta\n- **Due radici**: layer osservato dai dati · ordine noto del generatore.\n- **Singolare**: la pipeline Mk stessa, prima che venga usata come prova su un dominio reale.\n- **Invariante di passaggio**: un claim di memoria sopravvive solo se recupera correttamente i controlli con ordine noto.\n- **Campo di possibilità**: qui diventa possibile separare nucleo robusto e diagnostiche sovrastimate; qui diventa non-possibile usare `cond_entropy` o `num_var_10` come prova primaria di profondità senza recovery audit.\n\n## Self-Falsification — 5 lenti\n- **L1 hard constraint vs bias**: nessun claim \"zero/never/always\". La frazione mod3 è 0.158032, dichiarata bias, non proibizione.\n- **L2 quantità vs ratio**: il claim principale usa layer recuperato e z-score, non percentuali fra spazi di taglia diversa.\n- **L3 no silent patching**: dichiaro esplicitamente che `HL_markov` era nome scorretto per un controllo AR(1) lag1-matched.\n- **L4 edge cases**: i fallimenti `cond_entropy` e `num_var_10` non vengono arrotondati via; diventano il vincolo.\n- **L5 rediscovery**: non taggo NEW. Riferimenti classici vicini: surrogate-data tests for Markov order e constrained Markov-order surrogates.\n\n## References\n- 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).\n- [\"Exact significance test for Markov order\", Physica D 269, 2014](https://www.sciencedirect.com/science/article/pii/S0167278913003230).\n- [\"Constrained Markov order surrogates\", Physica D 2020](https://www.sciencedirect.com/science/article/pii/S0167278919305652).\n\n## Files\n- `tools/exp_markov_layer_recovery_audit.py`\n- `tools/data/markov_layer_recovery_audit.json`\n- `tools/data/meta_tautology_test.json`\n- `tools/data/seme.json`\n- `tools/data/reports/agent_20260504_1219.md`\n",
tools/data/lab_graph.json:1298:          "text": "- **Primary audit**: `tools/exp_markov_layer_recovery_audit.py`\n- **Scope**: 60,000 gap/spacings per sequenza; 20 surrogati per livello Mk; seed `20260504`.\n- **Target**: prime gaps, ordine sconosciut"
tools/data/lab_graph.json:1324:      "content_preview": "# Agent Report — The Two Markov Layers Are Coupled at the Boundary: One Phase Transition, Two Projections\n\n**Date**: 2026-05-04 09:01\n**Piano**: 60\n**Tension explored**: META (0.5) + BOUNDARY (0.8) + DIPOLAR_ORDERING (0.8)\n\n## Claim Under Test\n\n> The previous run found two orthogonal visible layers in prime gap memory: Layer 1 (pairs: SR, L1) and Layer 2 (triples: SR2, triple_var). The crossover under partial shuffle shows a phase transition in the (SR, L1) plane. META question: does Layer 2 tra",
tools/data/lab_graph.json:1325:      "content_full": "# Agent Report — The Two Markov Layers Are Coupled at the Boundary: One Phase Transition, Two Projections\n\n**Date**: 2026-05-04 09:01\n**Piano**: 60\n**Tension explored**: META (0.5) + BOUNDARY (0.8) + DIPOLAR_ORDERING (0.8)\n\n## Claim Under Test\n\n> The previous run found two orthogonal visible layers in prime gap memory: Layer 1 (pairs: SR, L1) and Layer 2 (triples: SR2, triple_var). The crossover under partial shuffle shows a phase transition in the (SR, L1) plane. META question: does Layer 2 transition at a different critical alpha than Layer 1? If yes, the boundary has genuine 3D depth. If no, the two layers are projections of a single phase transition.\n\n## Question\n\nDo the pair-statistics layer (SR, L1) and triple-statistics layer (SR2, triple_var) undergo independent transitions when ordering is destroyed by partial shuffle, or are they coupled?\n\n## Experiment Design\n\n- **Method**: Partial shuffle crossover with 20 alpha steps (0.05 to 0.95), 30 trials per step.\n- **Observables**: 4 total — Layer 1: spacing ratio (SR), lag-1 ACF (L1). Layer 2: next-nearest-neighbor spacing ratio (SR2), normalized triple variance (triple_var).\n- **Metric**: Retention = (value - baseline) / (original - baseline). Critical alpha = alpha where retention drops below 0.50.\n- **Null baseline**: Full shuffle (alpha=1.0, 90 trials) for each sequence.\n- **Sequences**: Prime gaps (N=50000), GUE eigenvalue gaps (200x200 matrices, 250 realizations), Poisson iid exponential gaps (N=50000).\n- **Poisson control**: If Poisson shows layer separation, the metric is noise-sensitive. If only structured sequences show coupling, the coupling is real.\n\n## Results\n\n### Critical alpha (50% retention)\n\n| Sequence | SR (L1) | L1 (L1) | SR2 (L2) | triple_var (L2) | L1 mean | L2 mean | Delta |\n|----------|---------|---------|----------|-----------------|---------|---------|-------|\n| Primes   | 0.334   | 0.334   | 0.334    | 0.334           | 0.334   | 0.334   | +0.000 |\n| GUE      | 0.334   | 0.287   | 0.334    | 0.334           | 0.311   | 0.334   | +0.024 |\n| Poisson  | 0.239   | 0.334   | 0.097    | 0.097           | 0.287   | 0.097   | -0.189 |\n\n### Retention at alpha = 0.33 (near critical)\n\n| Sequence | L1 avg | L2 avg | Difference |\n|----------|--------|--------|------------|\n| Primes   | 0.435  | 0.446  | -0.011     |\n| GUE      | 0.444  | 0.445  | -0.001     |\n| Poisson  | 0.330  | 0.591  | -0.261     |\n\n### Poisson null verification\n\nAll Poisson original-vs-baseline z-scores are < 2 (SR: z=0.91, L1: z=-1.47, SR2: z=-0.17, triple_var: z=-0.56). The Poisson \"signal\" is noise. The apparent layer separation (Delta = -0.189) in Poisson is an artifact: when the signal-to-noise is < 2, the retention metric amplifies noise differently for each observable.\n\n### Zero-crossing alpha (sign flip)\n\n| Sequence | SR     | L1     | SR2    | triple_var |\n|----------|--------|--------|--------|------------|\n| Primes   | 0.917  | 0.846  | 0.914  | 0.903      |\n| GUE      | 0.895  | None   | 0.902  | None       |\n\nFor primes, all observables flip sign at alpha > 0.84. L1 flips earliest (0.846), SR latest (0.917). The ordering is L1 < triple_var < SR2 < SR — interleaved between layers, not grouped by layer.\n\n## Key Findings\n\n1. **The two Markov layers are coupled at the boundary.** For primes, the critical alpha is identical across all 4 observables (0.334). For GUE, the difference is 0.024 (within the alpha step resolution of 0.047). The partial shuffle destroys pair-statistics and triple-statistics at the same rate. The boundary is a single phase transition, not two independent ones.\n\n2. **The coupling is specific to structured sequences.** Poisson (iid, no ordering) shows Delta = -0.189 — spurious separation from noise amplification. Primes (Delta = 0.000) and GUE (Delta = 0.024) show coupling. This rules out the coupling being a trivial property of the metric.\n\n3. **The two-layer decomposition is a decomposition of observables, not of the ordering.** The previous run correctly identified that SR and L1 are sensitive to Markov-1 (pair) statistics while SR2 and triple_var are sensitive to Markov-2 (triple) statistics. But when the ordering is destroyed uniformly (partial shuffle), both layers lose signal at the same rate. The layers are different projections of one ordering, not independent degrees of freedom.\n\n4. **The zero-crossing order is interleaved, not grouped by layer.** For primes: L1(0.846) < triple_var(0.903) < SR2(0.914) < SR(0.917). If layers were independent, we'd expect L1 grouping with SR and SR2 grouping with triple_var. The interleaving confirms coupling.\n\n## 5-Lens Self-Check\n\n- **L1 (hard constraint vs bias)**: No absolute claims. \"Coupled\" is quantified as |Delta| < 0.05 (resolution limit). The data shows 0.000 and 0.024.\n- **L2 (absolute vs ratio)**: Retention is already normalized. All comparisons are in the same units (fraction of original signal).\n- **L3 (no silent patching)**: This does NOT falsify the two-layer finding. The layers remain real as a decomposition of Markov order sensitivity. What's constrained is their independence at the boundary.\n- **L4 (edge case)**: Poisson separation is explicitly identified as noise artifact with z-score evidence.\n- **L5 (re-discovery)**: That partial shuffle destroys correlations uniformly regardless of order is consistent with the known property that random permutations break all multi-point correlations simultaneously (not order-by-order). The specific quantification on prime gaps and GUE with the Markov-layer framework is new to this lab, but the underlying principle is not novel. Tagged as CONSTRAINT, not NEW.\n\n## Verdict\n\n**CONSTRAINT on BOUNDARY + DIPOLAR_ORDERING**: The two Markov layers (pairs → plane, triples → depth) are coupled at the partial-shuffle boundary. The boundary is a single phase transition with one critical alpha (~0.33 for 50% retention). The two-layer decomposition describes WHAT is measured (which observables are sensitive to which Markov order), not HOW the ordering is destroyed. The boundary remains 2D in the shuffle parameter.\n\n**Consecutio**: Since the layers are coupled under uniform shuffle, the question becomes: is there a NON-uniform perturbation that decouples them? Specifically, a perturbation that destroys pair correlations but preserves triple correlations (or vice versa). If such a perturbation exists, the layers are genuinely independent degrees of freedom that happen to be coupled under this particular destruction method. If not, the two-layer decomposition is a spectral decomposition of a single ordering dimension.\n\n## Bicono della scoperta\n\n- **Due radici** (dipolo primario): Layer 1 (pairs, near-neighbor) and Layer 2 (triples, next-nearest-neighbor) — two ways the same ordering manifests in different correlation windows\n- **Singolare**: The ordering itself — before decomposition into pair and triple statistics. It exists as one thing; the layers are the observer's choice of measurement window.\n- **Invariante di passaggio**: The critical alpha (0.334) survives across layers and across structured sequences (primes and GUE). The boundary location is invariant to which layer you observe through.\n- **Campo di possibilita**: Possible — search for selective perturbations that decouple the layers (pair-preserving shuffle, triple-preserving shuffle). Not possible — claim the boundary has independent depth dimensions from partial shuffle alone.\n\n## Files\n\n- Script: `tools/exp_3d_boundary_layers.py` (reusable, parameterized)\n- Data: `tools/data/3d_boundary_layers.json`\n- Report: `tools/data/reports/agent_20260504_0901.md`\n",
tools/data/lab_graph.json:1331:          "text": "> The previous run found two orthogonal visible layers in prime gap memory: Layer 1 (pairs: SR, L1) and Layer 2 (triples: SR2, triple_var). The crosso"
tools/data/lab_graph.json:1337:          "text": "Do the pair-statistics layer (SR, L1) and triple-statistics layer (SR2, triple_var) undergo independent transitions when ordering is destroyed by part"
tools/data/lab_graph.json:1366:      "verdict": "**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, ",
tools/data/lab_graph.json:1367:      "verdict_en": "**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, ",
tools/data/lab_graph.json:1368:      "findings": "1. **Prime gap memory has exactly two visible layers.** Layer 1 (pair correlations, Mk1) shapes the dipolar plane (SR, L1). Layer 2 (triple correlations, Mk2) shapes the depth (SR2, L2, cond_entropy, triple_var, num_var_10). These are orthogonal: Layer 1 produces z ~ 0 for all Layer 2 observables, and vice versa. The structure is not a hierarchy where each layer adds to the previous — it's a decom",
tools/data/lab_graph.json:1370:      "content_full": "# Agent Report — Markov Memory Has Two Visible Layers: Pairs Shape the Angle, Triples Shape the Depth\n\n**Date**: 2026-05-03 03:30\n**Piano**: 60\n**Tension explored**: DIPOLAR_ORDERING (0.8) + BOUNDARY (0.8) + META (0.5)\n\n## Claim Under Test\n\n> \"The Markov-3 residual (z=6203) doesn't live in the (SR, L1) plane — it exists but doesn't shape the dipolar direction.\" (agent_20260502_0330, consecutio)\n> \"Next: identify the observable where Markov-3 (z=6203) becomes visible as a direction correction.\"\n\n## Question\n\nWhich observable renders the higher-order Markov memory (beyond pair statistics) visible? The (SR, L1) plane is blind to it — what is the \"third axis\" where the memory appears?\n\n## Experiment Design\n\n- **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\n- **Markov model**: Equal-count binning (12 bins), transition probabilities from data, gap values sampled from per-bin pools (preserving distribution within bin)\n- **Observables**: SR (nearest-neighbor spacing ratio), L1 (lag-1 ACF), L2 (lag-2 ACF), L3 (lag-3 ACF), triple_corr (3-body correlation), triple_var (variance of consecutive triple sums), SR2 (next-nearest-neighbor spacing ratio), cond_entropy_L2 (H(g_{n+2}|g_n,g_{n+1})), run_length (mean run of same-sign deviations), num_var_10 (number variance at L=10)\n- **N**: 100,000 primes (99,999 gaps)\n- **Surrogates**: 40 per Markov order\n- **Null logic**: Observable X is \"visible at Markov-k\" if |z(Mk-1)| > 3 (lower order misses it) and |z(Mk)| < 2 (this order captures it)\n\n## Results\n\n| Observable | z(Mk0) | z(Mk1) | z(Mk2) | z(Mk3) | Captured at |\n|------------|---------|---------|---------|---------|-------------|\n| SR | -26.8 | 0.3 | 0.5 | 0.6 | **Mk1** |\n| L1 | -16.6 | -0.9 | -0.5 | -0.8 | **Mk1** |\n| L2 | -3.8 | **-5.3** | -0.2 | -0.1 | **Mk2** |\n| L3 | -2.3 | -1.4 | -2.9 | -0.4 | Mk3 (marginal) |\n| triple_corr | -15.2 | -2.2 | -0.6 | -0.8 | Mk2 (near threshold) |\n| triple_var | -17.2 | **-3.7** | -0.6 | -0.8 | **Mk2** |\n| SR2 | -3.7 | **-9.4** | -0.05 | 0.2 | **Mk2** |\n| cond_entropy | -653.3 | **-51.3** | 2.4 | 2.6 | **Mk2** |\n| run_length | -13.5 | -2.1 | -1.7 | -2.5 | Mk1 (near threshold) |\n| num_var_10 | -10.2 | -3.9 | -1.2 | -0.7 | **Mk2** |\n\n### The two layers\n\n**Layer 1 (Markov-1 = pair statistics):** SR and L1. These form the dipolar plane. Markov-1 captures them completely (|z| < 1). This is the (SR, L1) plane studied in previous reports. It encodes the Z/6Z confinement character (theta = -104 deg).\n\n**Layer 2 (Markov-2 = triple statistics):** L2, SR2, triple_var, cond_entropy, num_var_10. All captured by Markov-2 (|z| < 2.5). Invisible to Markov-1 (|z| = 3.7 to 51.3). The sharpest probe is **SR2** (next-nearest-neighbor spacing ratio): z = -9.4 under Mk1, z = -0.05 under Mk2. The loudest probe is **cond_entropy** (conditional entropy H(g_{n+2}|g_n,g_{n+1})): z = -51.3 under Mk1, z = 2.4 under Mk2.\n\n**Layer 3+ (Markov-3):** L3 drops from z=-2.9 (Mk2) to z=-0.4 (Mk3) — the only observable with marginal Mk3 content. All others: Mk2 already sufficient. The step Mk2-to-Mk3 adds effectively nothing across 10 observables.\n\n### The SR2 anomaly\n\nSR2 (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.\n\n## Key Findings\n\n1. **Prime gap memory has exactly two visible layers.** Layer 1 (pair correlations, Mk1) shapes the dipolar plane (SR, L1). Layer 2 (triple correlations, Mk2) shapes the depth (SR2, L2, cond_entropy, triple_var, num_var_10). These are orthogonal: Layer 1 produces z ~ 0 for all Layer 2 observables, and vice versa. The structure is not a hierarchy where each layer adds to the previous — it's a decomposition into independent projection planes.\n\n2. **The \"third axis\" is SR2 (next-nearest-neighbor spacing ratio).** This is the single observable with the sharpest discrimination: z = -9.4 under pair model (Mk1), z = -0.05 under triple model (Mk2). SR2 is to Markov-2 what SR is to ordering in general — the minimally sufficient statistic for that level of memory.\n\n3. **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.\n\n4. **Partial information can amplify deviation.** SR2 is more anomalous under Mk1 (z=-9.4) than under Mk0 (z=-3.7). A model that captures part of the structure but not all can make the residual look worse, not better. This is a methodological warning for any Markov analysis — partial models must be tested against full models, not just against iid.\n\n## Verdict\n\n**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.\n\n**CONSTRAINT on META (tautology check)**: SR2 under Mk1 (z=-9.4) is NOT a tautology of the Mk1 model — it's a genuine prediction failure. The deviation is worse under Mk1 than under Mk0, showing that the pair model creates structure that requires triple correction. This is non-circular.\n\n**L5 note (re-discovery check)**: The two-layer structure connects to the hierarchy of k-point correlation functions in analytic number theory. The Hardy-Littlewood pair correlation (k=2) is well-studied and corresponds to Layer 1. Triple correlations (k=3) are conjectured (Goldston-Pintz-Yildirim, Maier, etc.) but less precisely quantified. The specific finding that SR2 (next-nearest-neighbor spacing ratio) is the minimally sufficient statistic for the triple layer appears new — SR2 is standard in RMT (Atas et al. 2013) but not commonly used as a Markov-order discriminator for prime gaps. Default hypothesis: the Layer 1/Layer 2 decomposition may follow from the independence structure of Hardy-Littlewood singular series at different tuple lengths.\n\n## Bicono della scoperta\n\n- **Due radici** (dipolo primario): pairs (Layer 1, the angle) and triples (Layer 2, the depth). Two independent projections of the same ordering, orthogonal in observable space. One was known (SR, L1). The other was invisible until measured via SR2.\n- **Singolare** (qualita del 1-che-e-tutto): the gap sequence itself before any Markov decomposition. It contains both layers simultaneously. The separation into \"pair content\" and \"triple content\" is an act of the observer (the Markov model), not a property of the sequence alone.\n- **Invariante di passaggio**: the orthogonality between layers. Mk1 captures Layer 1 and AMPLIFIES Layer 2 anomaly. This non-interference survives regardless of binning, N, or which specific Layer 2 observable is chosen. The layers don't mix.\n- **Campo di possibilita**: Possible — extend the dipolar plane to a 3D space (SR, L1, SR2) where both layers are visible. Map the ordering fingerprint of primes in this 3D space. Derive the SR2 deviation analytically from Hardy-Littlewood triple correlation. Not possible — reduce prime gap characterization to pairs alone (SR2 proves the triple layer is structurally independent). Not possible — find the z=6203 Markov-3 content in any single observable (it's distributional, not projectable).\n\n## Consecutio\n\nThe two-layer structure opens a precise next question: **what is the prime SR2 value analytically?** Real SR2 = 0.4785. Mk1 predicts 0.4864 (too high by 0.008). Mk2 predicts 0.4786 (match). The 0.008 gap between Mk1 prediction and reality IS the triple correlation content. Can this be derived from the Hardy-Littlewood singular series for prime triplets (p, p+g1, p+g1+g2)? If yes, it connects the Markov Layer 2 to the arithmetic structure of primes. If no, SR2 contains information beyond what Hardy-Littlewood triplet correlations encode.\n\n## Files\n\n- Script: `tools/exp_markov3_observable_hunt.py` (reusable, configurable N, n_surr, n_bins)\n- Data: `tools/data/markov3_observable_hunt.json`\n- Report: `tools/data/reports/agent_20260503_0330.md`\n",
tools/data/lab_graph.json:1406:          "text": "The two-layer structure opens a precise next question: **what is the prime SR2 value analytically?** Real SR2 = 0.4785. Mk1 predicts 0.4864 (too high "
tools/data/3d_boundary_layers.json:15:        "SR2": 0.4821846077321564,
tools/data/3d_boundary_layers.json:16:        "triple_var": 2.792643621268128
tools/data/3d_boundary_layers.json:21:        "SR2": 0.4865936969848896,
tools/data/3d_boundary_layers.json:22:        "triple_var": 2.9983281062662597
tools/data/3d_boundary_layers.json:27:        "SR2": 0.0011682842871341957,
tools/data/3d_boundary_layers.json:28:        "triple_var": 0.020164930226696357
tools/data/3d_boundary_layers.json:33:        "SR2": 0.33421052631578946,
tools/data/3d_boundary_layers.json:34:        "triple_var": 0.33421052631578946
tools/data/3d_boundary_layers.json:39:        "SR2": 0.9144887480713632,
tools/data/3d_boundary_layers.json:40:        "triple_var": 0.9029388395335943
tools/data/3d_boundary_layers.json:51:          "SR2_mean": 0.4827089410556903,
tools/data/3d_boundary_layers.json:52:          "SR2_std": 0.00033478903777661484,
tools/data/3d_boundary_layers.json:53:          "SR2_retention": 0.8810789953483308,
tools/data/3d_boundary_layers.json:54:          "triple_var_mean": 2.81233110340142,
tools/data/3d_boundary_layers.json:55:          "triple_var_std": 0.008711999919613333,
tools/data/3d_boundary_layers.json:56:          "triple_var_retention": 0.9042830958617487
tools/data/3d_boundary_layers.json:66:          "SR2_mean": 0.48312775671093716,
tools/data/3d_boundary_layers.json:67:          "SR2_std": 0.0005807463568156619,
tools/data/3d_boundary_layers.json:68:          "SR2_retention": 0.7860898419790154,
tools/data/3d_boundary_layers.json:69:          "triple_var_mean": 2.8337806478524934,
tools/data/3d_boundary_layers.json:70:          "triple_var_std": 0.013009505221912958,
tools/data/3d_boundary_layers.json:71:          "triple_var_retention": 0.7999993699829181
tools/data/3d_boundary_layers.json:81:          "SR2_mean": 0.48345719743042687,
tools/data/3d_boundary_layers.json:82:          "SR2_std": 0.0006132608070766689,
tools/data/3d_boundary_layers.json:83:          "SR2_retention": 0.7113713002108979,
tools/data/3d_boundary_layers.json:84:          "triple_var_mean": 2.8482135530765897,
tools/data/3d_boundary_layers.json:85:          "triple_var_std": 0.017156330168266586,
tools/data/3d_boundary_layers.json:86:          "triple_var_retention": 0.7298292488664545
tools/data/3d_boundary_layers.json:96:          "SR2_mean": 0.4835372420038567,
tools/data/3d_boundary_layers.json:97:          "SR2_std": 0.0009063640412520366,
tools/data/3d_boundary_layers.json:98:          "SR2_retention": 0.693216854056232,
tools/data/3d_boundary_layers.json:99:          "triple_var_mean": 2.8613493398311296,
tools/data/3d_boundary_layers.json:100:          "triple_var_std": 0.013551934177155877,
tools/data/3d_boundary_layers.json:101:          "triple_var_retention": 0.6659654783216845
tools/data/3d_boundary_layers.json:111:          "SR2_mean": 0.48406311575650546,
tools/data/3d_boundary_layers.json:112:          "SR2_std": 0.0009490453131506712,
tools/data/3d_boundary_layers.json:113:          "SR2_retention": 0.5739464735977867,
tools/data/3d_boundary_layers.json:114:          "triple_var_mean": 2.8794494983703625,
tools/data/3d_boundary_layers.json:115:          "triple_var_std": 0.01583795032152056,
tools/data/3d_boundary_layers.json:116:          "triple_var_retention": 0.5779658485032406
tools/data/3d_boundary_layers.json:126:          "SR2_mean": 0.48430461543888215,
tools/data/3d_boundary_layers.json:127:          "SR2_std": 0.0008814283186148016,
tools/data/3d_boundary_layers.json:128:          "SR2_retention": 0.5191733291832157,
tools/data/3d_boundary_layers.json:129:          "triple_var_mean": 2.891038617293313,
tools/data/3d_boundary_layers.json:130:          "triple_var_std": 0.017132944923239787,
tools/data/3d_boundary_layers.json:131:          "triple_var_retention": 0.5216216914655539
tools/data/3d_boundary_layers.json:141:          "SR2_mean": 0.4845370735773733,
tools/data/3d_boundary_layers.json:142:          "SR2_std": 0.0009875219959755257,
tools/data/3d_boundary_layers.json:143:          "SR2_retention": 0.4664508449769699,
tools/data/3d_boundary_layers.json:144:          "triple_var_mean": 2.9109005178876166,
tools/data/3d_boundary_layers.json:145:          "triple_var_std": 0.01893134403788239,
tools/data/3d_boundary_layers.json:146:          "triple_var_retention": 0.42505679696471627
tools/data/3d_boundary_layers.json:156:          "SR2_mean": 0.4850675943337123,
tools/data/3d_boundary_layers.json:157:          "SR2_std": 0.000728395628210196,
tools/data/3d_boundary_layers.json:158:          "SR2_retention": 0.34612650452272414,
tools/data/3d_boundary_layers.json:159:          "triple_var_mean": 2.9188260890064934,
tools/data/3d_boundary_layers.json:160:          "triple_var_std": 0.018313982893399902,
tools/data/3d_boundary_layers.json:161:          "triple_var_retention": 0.3865241331182
tools/data/3d_boundary_layers.json:171:          "SR2_mean": 0.4852002517695003,
tools/data/3d_boundary_layers.json:172:          "SR2_std": 0.0010854039294387612,
tools/data/3d_boundary_layers.json:173:          "SR2_retention": 0.3160392397421886,
tools/data/3d_boundary_layers.json:174:          "triple_var_mean": 2.936482409996187,
tools/data/3d_boundary_layers.json:175:          "triple_var_std": 0.019984448406303302,
tools/data/3d_boundary_layers.json:176:          "triple_var_retention": 0.30068235954031447
tools/data/3d_boundary_layers.json:186:          "SR2_mean": 0.48527489191536666,
tools/data/3d_boundary_layers.json:187:          "SR2_std": 0.0012892583050154813,
tools/data/3d_boundary_layers.json:188:          "SR2_retention": 0.29911054050570474,
tools/data/3d_boundary_layers.json:189:          "triple_var_mean": 2.9433515524176648,
tools/data/3d_boundary_layers.json:190:          "triple_var_std": 0.01759799178134232,
tools/data/3d_boundary_layers.json:191:          "triple_var_retention": 0.26728585702073876
tools/data/3d_boundary_layers.json:201:          "SR2_mean": 0.4853809748248768,
tools/data/3d_boundary_layers.json:202:          "SR2_std": 0.000986640716922797,
tools/data/3d_boundary_layers.json:203:          "SR2_retention": 0.2750504901348985,
tools/data/3d_boundary_layers.json:204:          "triple_var_mean": 2.9502131475281503,
tools/data/3d_boundary_layers.json:205:          "triple_var_std": 0.017077140700340016,
tools/data/3d_boundary_layers.json:206:          "triple_var_retention": 0.2339260481340943
tools/data/3d_boundary_layers.json:216:          "SR2_mean": 0.4859057764355688,
tools/data/3d_boundary_layers.json:217:          "SR2_std": 0.0009964726341964453,
tools/data/3d_boundary_layers.json:218:          "SR2_retention": 0.1560232759847901,
tools/data/3d_boundary_layers.json:219:          "triple_var_mean": 2.9649678089775615,
tools/data/3d_boundary_layers.json:220:          "triple_var_std": 0.017510396546626422,
tools/data/3d_boundary_layers.json:221:          "triple_var_retention": 0.1621916076411947
tools/data/3d_boundary_layers.json:231:          "SR2_mean": 0.4859822528287449,
tools/data/3d_boundary_layers.json:232:          "SR2_std": 0.0009884403479524164,
tools/data/3d_boundary_layers.json:233:          "SR2_retention": 0.13867810812984432,
tools/data/3d_boundary_layers.json:234:          "triple_var_mean": 2.973080279237322,
tools/data/3d_boundary_layers.json:235:          "triple_var_std": 0.018435500231978885,
tools/data/3d_boundary_layers.json:236:          "triple_var_retention": 0.1227502746702889
tools/data/3d_boundary_layers.json:246:          "SR2_mean": 0.4864528084604813,
tools/data/3d_boundary_layers.json:247:          "SR2_std": 0.0011206612213061132,
tools/data/3d_boundary_layers.json:248:          "SR2_retention": 0.03195411032356926,
tools/data/3d_boundary_layers.json:249:          "triple_var_mean": 2.9748936656158764,
tools/data/3d_boundary_layers.json:250:          "triple_var_std": 0.02178896400839716,
tools/data/3d_boundary_layers.json:251:          "triple_var_retention": 0.11393392481983325
tools/data/3d_boundary_layers.json:261:          "SR2_mean": 0.4862605857956996,
tools/data/3d_boundary_layers.json:262:          "SR2_std": 0.0011756087159943843,
tools/data/3d_boundary_layers.json:263:          "SR2_retention": 0.07555101974483687,
tools/data/3d_boundary_layers.json:264:          "triple_var_mean": 2.9887167262156598,
tools/data/3d_boundary_layers.json:265:          "triple_var_std": 0.019041887404145474,
tools/data/3d_boundary_layers.json:266:          "triple_var_retention": 0.046728755699231385
tools/data/3d_boundary_layers.json:276:          "SR2_mean": 0.4865355552603367,
tools/data/3d_boundary_layers.json:277:          "SR2_std": 0.0007994051449116136,
tools/data/3d_boundary_layers.json:278:          "SR2_retention": 0.013186787842144852,
tools/data/3d_boundary_layers.json:279:          "triple_var_mean": 2.986708565389072,
tools/data/3d_boundary_layers.json:280:          "triple_var_std": 0.01938938495078536,
tools/data/3d_boundary_layers.json:281:          "triple_var_retention": 0.05649206296378201
tools/data/3d_boundary_layers.json:291:          "SR2_mean": 0.48654935022527074,
tools/data/3d_boundary_layers.json:292:          "SR2_std": 0.0010081029175843237,
tools/data/3d_boundary_layers.json:293:          "SR2_retention": 0.010058031733276912,
tools/data/3d_boundary_layers.json:294:          "triple_var_mean": 2.995125505359793,
tools/data/3d_boundary_layers.json:295:          "triple_var_std": 0.019229409792647274,
tools/data/3d_boundary_layers.json:296:          "triple_var_retention": 0.015570454458418807
tools/data/3d_boundary_layers.json:306:          "SR2_mean": 0.48651439109663314,
tools/data/3d_boundary_layers.json:307:          "SR2_std": 0.0010321827555695821,
tools/data/3d_boundary_layers.json:308:          "SR2_retention": 0.017986909248271746,
tools/data/3d_boundary_layers.json:309:          "triple_var_mean": 2.9981736538660355,
tools/data/3d_boundary_layers.json:310:          "triple_var_std": 0.029143460876895608,
tools/data/3d_boundary_layers.json:311:          "triple_var_retention": 0.0007509190604510677
tools/data/3d_boundary_layers.json:321:          "SR2_mean": 0.4865749175731102,
tools/data/3d_boundary_layers.json:322:          "SR2_std": 0.0009929117577809406,
tools/data/3d_boundary_layers.json:323:          "SR2_retention": 0.004259249632498543,
tools/data/3d_boundary_layers.json:324:          "triple_var_mean": 2.998302105279758,
tools/data/3d_boundary_layers.json:325:          "triple_var_std": 0.019477266377903105,
tools/data/3d_boundary_layers.json:326:          "triple_var_retention": 0.00012641199700533958
tools/data/3d_boundary_layers.json:336:          "SR2_mean": 0.48664993978751764,
tools/data/3d_boundary_layers.json:337:          "SR2_std": 0.0011697257352870664,
tools/data/3d_boundary_layers.json:338:          "SR2_retention": -0.01275610435719673,
tools/data/3d_boundary_layers.json:339:          "triple_var_mean": 3.0023105129037906,
tools/data/3d_boundary_layers.json:340:          "triple_var_std": 0.018475236113052393,
tools/data/3d_boundary_layers.json:341:          "triple_var_retention": -0.01936172598320717
tools/data/3d_boundary_layers.json:354:        "SR2": 0.625464280563035,
tools/data/3d_boundary_layers.json:355:        "triple_var": 4.065435972629281
tools/data/3d_boundary_layers.json:360:        "SR2": 0.6061267794599827,
tools/data/3d_boundary_layers.json:361:        "triple_var": 2.999620781030625
tools/data/3d_boundary_layers.json:366:        "SR2": 0.0008638058662831035,
tools/data/3d_boundary_layers.json:367:        "triple_var": 0.018179518400799313
tools/data/3d_boundary_layers.json:372:        "SR2": 0.33421052631578946,
tools/data/3d_boundary_layers.json:373:        "triple_var": 0.33421052631578946
tools/data/3d_boundary_layers.json:378:        "SR2": 0.9020395007331983,
tools/data/3d_boundary_layers.json:379:        "triple_var": null
tools/data/3d_boundary_layers.json:390:          "SR2_mean": 0.6236147655620633,
tools/data/3d_boundary_layers.json:391:          "SR2_std": 0.0003764061538357503,
tools/data/3d_boundary_layers.json:392:          "SR2_retention": 0.9043560493615274,
tools/data/3d_boundary_layers.json:393:          "triple_var_mean": 3.9598070445088442,
tools/data/3d_boundary_layers.json:394:          "triple_var_std": 0.018296017102829416,
tools/data/3d_boundary_layers.json:395:          "triple_var_retention": 0.900893767556456
tools/data/3d_boundary_layers.json:405:          "SR2_mean": 0.6216961051456893,
tools/data/3d_boundary_layers.json:406:          "SR2_std": 0.0006141238466748659,
tools/data/3d_boundary_layers.json:407:          "SR2_retention": 0.8051363825519852,
tools/data/3d_boundary_layers.json:408:          "triple_var_mean": 3.857224874475731,
tools/data/3d_boundary_layers.json:409:          "triple_var_std": 0.01873101038764569,
tools/data/3d_boundary_layers.json:410:          "triple_var_retention": 0.8046461527338089
tools/data/3d_boundary_layers.json:420:          "SR2_mean": 0.620369132897102,
tools/data/3d_boundary_layers.json:421:          "SR2_std": 0.00047155625541373886,
tools/data/3d_boundary_layers.json:422:          "SR2_retention": 0.7365146800106062,
tools/data/3d_boundary_layers.json:423:          "triple_var_mean": 3.7837896872364154,
tools/data/3d_boundary_layers.json:424:          "triple_var_std": 0.022705923880410952,
tools/data/3d_boundary_layers.json:425:          "triple_var_retention": 0.7357456643394116
tools/data/3d_boundary_layers.json:435:          "SR2_mean": 0.61875084345285,
tools/data/3d_boundary_layers.json:436:          "SR2_std": 0.0005487419569991576,
tools/data/3d_boundary_layers.json:437:          "SR2_retention": 0.6528280942605694,
tools/data/3d_boundary_layers.json:438:          "triple_var_mean": 3.7004536094647054,
tools/data/3d_boundary_layers.json:439:          "triple_var_std": 0.026895220839429107,
tools/data/3d_boundary_layers.json:440:          "triple_var_retention": 0.6575556756541208
tools/data/3d_boundary_layers.json:450:          "SR2_mean": 0.6172437002545853,
tools/data/3d_boundary_layers.json:451:          "SR2_std": 0.0007906810675180222,
tools/data/3d_boundary_layers.json:452:          "SR2_retention": 0.5748892132111026,
tools/data/3d_boundary_layers.json:453:          "triple_var_mean": 3.6228750108269083,
tools/data/3d_boundary_layers.json:454:          "triple_var_std": 0.03062955683385248,
tools/data/3d_boundary_layers.json:455:          "triple_var_retention": 0.5847676358050791
tools/data/3d_boundary_layers.json:465:          "SR2_mean": 0.6158268144094049,
tools/data/3d_boundary_layers.json:466:          "SR2_std": 0.0005720868658183826,
tools/data/3d_boundary_layers.json:467:          "SR2_retention": 0.5016178097536644,
tools/data/3d_boundary_layers.json:468:          "triple_var_mean": 3.533108799535667,
tools/data/3d_boundary_layers.json:469:          "triple_var_std": 0.023871818131384077,
tools/data/3d_boundary_layers.json:470:          "triple_var_retention": 0.5005445810026815
tools/data/3d_boundary_layers.json:480:          "SR2_mean": 0.6146375760648055,
tools/data/3d_boundary_layers.json:481:          "SR2_std": 0.0006876600180510444,
tools/data/3d_boundary_layers.json:482:          "SR2_retention": 0.44011873920355743,
tools/data/3d_boundary_layers.json:483:          "triple_var_mean": 3.4787775581203646,
tools/data/3d_boundary_layers.json:484:          "triple_var_std": 0.039995146339563956,
tools/data/3d_boundary_layers.json:485:          "triple_var_retention": 0.44956834999793405
tools/data/3d_boundary_layers.json:495:          "SR2_mean": 0.6136437912994364,
tools/data/3d_boundary_layers.json:496:          "SR2_std": 0.0006430232565362617,
tools/data/3d_boundary_layers.json:497:          "SR2_retention": 0.3887271576298509,
tools/data/3d_boundary_layers.json:498:          "triple_var_mean": 3.400933844300764,
tools/data/3d_boundary_layers.json:499:          "triple_var_std": 0.028181135253677624,
tools/data/3d_boundary_layers.json:500:          "triple_var_retention": 0.3765315661040587
tools/data/3d_boundary_layers.json:510:          "SR2_mean": 0.6121886049419479,
tools/data/3d_boundary_layers.json:511:          "SR2_std": 0.000918783376858124,
tools/data/3d_boundary_layers.json:512:          "SR2_retention": 0.3134751201647447,
tools/data/3d_boundary_layers.json:513:          "triple_var_mean": 3.3490974896831185,
tools/data/3d_boundary_layers.json:514:          "triple_var_std": 0.026274774870606248,
tools/data/3d_boundary_layers.json:515:          "triple_var_retention": 0.32789616005407135
tools/data/3d_boundary_layers.json:525:          "SR2_mean": 0.6116182553821335,
tools/data/3d_boundary_layers.json:526:          "SR2_std": 0.0006494230693119901,
tools/data/3d_boundary_layers.json:527:          "SR2_retention": 0.2839806391159829,
tools/data/3d_boundary_layers.json:528:          "triple_var_mean": 3.2973851000563497,
tools/data/3d_boundary_layers.json:529:          "triple_var_std": 0.028355292464314657,
tools/data/3d_boundary_layers.json:530:          "triple_var_retention": 0.2793770640284241
tools/data/3d_boundary_layers.json:540:          "SR2_mean": 0.6104742059409836,
tools/data/3d_boundary_layers.json:541:          "SR2_std": 0.0007688118308518916,
tools/data/3d_boundary_layers.json:542:          "SR2_retention": 0.22481842187533835,
tools/data/3d_boundary_layers.json:543:          "triple_var_mean": 3.239382016960765,
tools/data/3d_boundary_layers.json:544:          "triple_var_std": 0.026058030823350644,
tools/data/3d_boundary_layers.json:545:          "triple_var_retention": 0.22495573136888164
tools/data/3d_boundary_layers.json:555:          "SR2_mean": 0.6096930390834454,
tools/data/3d_boundary_layers.json:556:          "SR2_std": 0.0007909106481171039,
tools/data/3d_boundary_layers.json:557:          "SR2_retention": 0.18442194803028458,
tools/data/3d_boundary_layers.json:558:          "triple_var_mean": 3.198129221002637,
tools/data/3d_boundary_layers.json:559:          "triple_var_std": 0.02797286577440994,
tools/data/3d_boundary_layers.json:560:          "triple_var_retention": 0.18625033827324397
tools/data/3d_boundary_layers.json:570:          "SR2_mean": 0.608908588640673,
tools/data/3d_boundary_layers.json:571:          "SR2_std": 0.0006865673126173603,
tools/data/3d_boundary_layers.json:572:          "SR2_retention": 0.14385567017504503,
tools/data/3d_boundary_layers.json:573:          "triple_var_mean": 3.1491025253457896,
tools/data/3d_boundary_layers.json:574:          "triple_var_std": 0.02322347399408795,
tools/data/3d_boundary_layers.json:575:          "triple_var_retention": 0.14025109183417767
tools/data/3d_boundary_layers.json:585:          "SR2_mean": 0.6080404197132999,
tools/data/3d_boundary_layers.json:586:          "SR2_std": 0.0008460322405182615,
tools/data/3d_boundary_layers.json:587:          "SR2_retention": 0.09896005916788993,
tools/data/3d_boundary_layers.json:588:          "triple_var_mean": 3.114702189196439,
tools/data/3d_boundary_layers.json:589:          "triple_var_std": 0.020128403889471104,
tools/data/3d_boundary_layers.json:590:          "triple_var_retention": 0.1079750120592662
tools/data/3d_boundary_layers.json:600:          "SR2_mean": 0.6074863223771619,
tools/data/3d_boundary_layers.json:601:          "SR2_std": 0.0007408340970682327,
tools/data/3d_boundary_layers.json:602:          "SR2_retention": 0.07030602919860292,
tools/data/3d_boundary_layers.json:603:          "triple_var_mean": 3.0794110011509566,
tools/data/3d_boundary_layers.json:604:          "triple_var_std": 0.02052641832566574,
tools/data/3d_boundary_layers.json:605:          "triple_var_retention": 0.07486309141517435
tools/data/3d_boundary_layers.json:615:          "SR2_mean": 0.6071599610152565,
tools/data/3d_boundary_layers.json:616:          "SR2_std": 0.0008464022943294249,
tools/data/3d_boundary_layers.json:617:          "SR2_retention": 0.053428907373698,
tools/data/3d_boundary_layers.json:618:          "triple_var_mean": 3.057462049040654,
tools/data/3d_boundary_layers.json:619:          "triple_var_std": 0.01910477257909751,
tools/data/3d_boundary_layers.json:620:          "triple_var_retention": 0.05426950982305929
tools/data/3d_boundary_layers.json:630:          "SR2_mean": 0.606557975996327,
tools/data/3d_boundary_layers.json:631:          "SR2_std": 0.0006409197888288003,
tools/data/3d_boundary_layers.json:632:          "SR2_retention": 0.02229846214598055,
tools/data/3d_boundary_layers.json:633:          "triple_var_mean": 3.0400580842804916,
tools/data/3d_boundary_layers.json:634:          "triple_var_std": 0.023995611561167036,
tools/data/3d_boundary_layers.json:635:          "triple_var_retention": 0.03794025790645131
tools/data/3d_boundary_layers.json:645:          "SR2_mean": 0.6064112940383014,
tools/data/3d_boundary_layers.json:646:          "SR2_std": 0.0008511457745792372,
tools/data/3d_boundary_layers.json:647:          "SR2_retention": 0.014713099526276177,
tools/data/3d_boundary_layers.json:648:          "triple_var_mean": 3.021510418242043,
tools/data/3d_boundary_layers.json:649:          "triple_var_std": 0.016933743921188855,
tools/data/3d_boundary_layers.json:650:          "triple_var_retention": 0.02053792944964974
tools/data/3d_boundary_layers.json:660:          "SR2_mean": 0.6061231781762308,
tools/data/3d_boundary_layers.json:661:          "SR2_std": 0.000807520484904721,
tools/data/3d_boundary_layers.json:662:          "SR2_retention": -0.00018623315043051912,
tools/data/3d_boundary_layers.json:663:          "triple_var_mean": 3.008117986584554,
tools/data/3d_boundary_layers.json:664:          "triple_var_std": 0.019463793910510292,
tools/data/3d_boundary_layers.json:665:          "triple_var_retention": 0.007972494313187552
tools/data/3d_boundary_layers.json:675:          "SR2_mean": 0.6062007081220531,
tools/data/3d_boundary_layers.json:676:          "SR2_std": 0.000754905119446829,
tools/data/3d_boundary_layers.json:677:          "SR2_retention": 0.0038230721578950354,
tools/data/3d_boundary_layers.json:678:          "triple_var_mean": 3.0050901157304932,
tools/data/3d_boundary_layers.json:679:          "triple_var_std": 0.017686334958009236,
tools/data/3d_boundary_layers.json:680:          "triple_var_retention": 0.005131597619344093
tools/data/3d_boundary_layers.json:693:        "SR2": 0.38665042048777204,
tools/data/3d_boundary_layers.json:694:        "triple_var": 2.9896392604329494
tools/data/3d_boundary_layers.json:699:        "SR2": 0.3868084393218557,
tools/data/3d_boundary_layers.json:700:        "triple_var": 3.0001382922683906
tools/data/3d_boundary_layers.json:705:        "SR2": 0.0009422409946464936,
tools/data/3d_boundary_layers.json:706:        "triple_var": 0.01864182585713075
tools/data/3d_boundary_layers.json:711:        "SR2": 0.09736842105263158,
tools/data/3d_boundary_layers.json:712:        "triple_var": 0.09736842105263158
tools/data/3d_boundary_layers.json:717:        "SR2": 0.07620829959229504,
tools/data/3d_boundary_layers.json:718:        "triple_var": 0.31921725062375245
tools/data/3d_boundary_layers.json:729:          "SR2_mean": 0.3866904592951477,
tools/data/3d_boundary_layers.json:730:          "SR2_std": 0.0004799365206100546,
tools/data/3d_boundary_layers.json:731:          "SR2_retention": 0.7466200304041906,
tools/data/3d_boundary_layers.json:732:          "triple_var_mean": 2.990809912980245,
tools/data/3d_boundary_layers.json:733:          "triple_var_std": 0.010620945620795246,
tools/data/3d_boundary_layers.json:734:          "triple_var_retention": 0.8884990001322126
tools/data/3d_boundary_layers.json:744:          "SR2_mean": 0.3869036943263341,
tools/data/3d_boundary_layers.json:745:          "SR2_std": 0.0005553663618879448,
tools/data/3d_boundary_layers.json:746:          "SR2_retention": -0.602807918630381,
tools/data/3d_boundary_layers.json:747:          "triple_var_mean": 2.9969515016719286,
tools/data/3d_boundary_layers.json:748:          "triple_var_std": 0.01234770948511574,
tools/data/3d_boundary_layers.json:749:          "triple_var_retention": 0.30353185383288867
tools/data/3d_boundary_layers.json:759:          "SR2_mean": 0.3865756028185807,
tools/data/3d_boundary_layers.json:760:          "SR2_std": 0.0007608474907881847,
tools/data/3d_boundary_layers.json:761:          "SR2_retention": 1.4734731123993825,
tools/data/3d_boundary_layers.json:762:          "triple_var_mean": 2.9961141698718405,
tools/data/3d_boundary_layers.json:763:          "triple_var_std": 0.01528619095823765,
tools/data/3d_boundary_layers.json:764:          "triple_var_retention": 0.3832850932945976
tools/data/3d_boundary_layers.json:774:          "SR2_mean": 0.3866977867839673,
tools/data/3d_boundary_layers.json:775:          "SR2_std": 0.0007695657111437048,
tools/data/3d_boundary_layers.json:776:          "SR2_retention": 0.7002490464509908,
tools/data/3d_boundary_layers.json:777:          "triple_var_mean": 2.996860842688996,
tools/data/3d_boundary_layers.json:778:          "triple_var_std": 0.014226428063327457,
tools/data/3d_boundary_layers.json:779:          "triple_var_retention": 0.3121668388823038
tools/data/3d_boundary_layers.json:789:          "SR2_mean": 0.38684093581316686,
tools/data/3d_boundary_layers.json:790:          "SR2_std": 0.0008216878387618527,
tools/data/3d_boundary_layers.json:791:          "SR2_retention": -0.20564948159245428,
tools/data/3d_boundary_layers.json:792:          "triple_var_mean": 2.9895433312681297,
tools/data/3d_boundary_layers.json:793:          "triple_var_std": 0.012885795117861667,
tools/data/3d_boundary_layers.json:794:          "triple_var_retention": 1.0091369534184937
tools/data/3d_boundary_layers.json:804:          "SR2_mean": 0.38658168367694906,
tools/data/3d_boundary_layers.json:805:          "SR2_std": 0.0007526690289846484,
tools/data/3d_boundary_layers.json:806:          "SR2_retention": 1.4349912541854408,
tools/data/3d_boundary_layers.json:807:          "triple_var_mean": 2.9931113101199474,
tools/data/3d_boundary_layers.json:808:          "triple_var_std": 0.017990436622245667,
tools/data/3d_boundary_layers.json:809:          "triple_var_retention": 0.6692981084905832
tools/data/3d_boundary_layers.json:819:          "SR2_mean": 0.38657265357967063,
tools/data/3d_boundary_layers.json:820:          "SR2_std": 0.0009158204285295656,
tools/data/3d_boundary_layers.json:821:          "SR2_retention": 1.4921369566632035,
tools/data/3d_boundary_layers.json:822:          "triple_var_mean": 3.003392562773161,
tools/data/3d_boundary_layers.json:823:          "triple_var_std": 0.01831668941546222,
tools/data/3d_boundary_layers.json:824:          "triple_var_retention": -0.30995910439902696
tools/data/3d_boundary_layers.json:834:          "SR2_mean": 0.38709760491683975,
tools/data/3d_boundary_layers.json:835:          "SR2_std": 0.0007365505854970186,
tools/data/3d_boundary_layers.json:836:          "SR2_retention": -1.8299438586600711,
tools/data/3d_boundary_layers.json:837:          "triple_var_mean": 3.0004267686024204,
tools/data/3d_boundary_layers.json:838:          "triple_var_std": 0.013805085633043104,
tools/data/3d_boundary_layers.json:839:          "triple_var_retention": -0.027476470073749277
tools/data/3d_boundary_layers.json:849:          "SR2_mean": 0.3867367007700945,
tools/data/3d_boundary_layers.json:850:          "SR2_std": 0.0010603692977047706,
tools/data/3d_boundary_layers.json:851:          "SR2_retention": 0.4539873501613686,
tools/data/3d_boundary_layers.json:852:          "triple_var_mean": 2.9984951383940004,
tools/data/3d_boundary_layers.json:853:          "triple_var_std": 0.01969713450915539,
tools/data/3d_boundary_layers.json:854:          "triple_var_retention": 0.1565052759287236
tools/data/3d_boundary_layers.json:864:          "SR2_mean": 0.3865302983158448,
tools/data/3d_boundary_layers.json:865:          "SR2_std": 0.0010678071174998424,
tools/data/3d_boundary_layers.json:866:          "SR2_retention": 1.7601762955904627,
tools/data/3d_boundary_layers.json:867:          "triple_var_mean": 3.001139182376183,
tools/data/3d_boundary_layers.json:868:          "triple_var_std": 0.017836897789469636,
tools/data/3d_boundary_layers.json:869:          "triple_var_retention": -0.09533165757375671
tools/data/3d_boundary_layers.json:879:          "SR2_mean": 0.3867783175772115,
tools/data/3d_boundary_layers.json:880:          "SR2_std": 0.0008731925049429091,
tools/data/3d_boundary_layers.json:881:          "SR2_retention": 0.1906212308102424,
tools/data/3d_boundary_layers.json:882:          "triple_var_mean": 3.000737393499608,
tools/data/3d_boundary_layers.json:883:          "triple_var_std": 0.01516274078963618,
tools/data/3d_boundary_layers.json:884:          "triple_var_retention": -0.057062521631286056
tools/data/3d_boundary_layers.json:894:          "SR2_mean": 0.38658347709546825,
tools/data/3d_boundary_layers.json:895:          "SR2_std": 0.0010040544056473314,
tools/data/3d_boundary_layers.json:896:          "SR2_retention": 1.4236418569468696,
tools/data/3d_boundary_layers.json:897:          "triple_var_mean": 2.9939872710257385,
tools/data/3d_boundary_layers.json:898:          "triple_var_std": 0.018895941605419398,
tools/data/3d_boundary_layers.json:899:          "triple_var_retention": 0.5858655673267239
tools/data/3d_boundary_layers.json:909:          "SR2_mean": 0.38688217276794223,
tools/data/3d_boundary_layers.json:910:          "SR2_std": 0.0009095074044505994,
tools/data/3d_boundary_layers.json:911:          "SR2_retention": -0.4666117587443945,
tools/data/3d_boundary_layers.json:912:          "triple_var_mean": 3.005751669210563,
tools/data/3d_boundary_layers.json:913:          "triple_var_std": 0.018206062007731165,
tools/data/3d_boundary_layers.json:914:          "triple_var_retention": -0.5346566264542201
tools/data/3d_boundary_layers.json:924:          "SR2_mean": 0.3868581843365998,
tools/data/3d_boundary_layers.json:925:          "SR2_std": 0.0010117075677898483,
tools/data/3d_boundary_layers.json:926:          "SR2_retention": -0.31480433982789763,
tools/data/3d_boundary_layers.json:927:          "triple_var_mean": 3.0052459520867933,
tools/data/3d_boundary_layers.json:928:          "triple_var_std": 0.02380057042448457,
tools/data/3d_boundary_layers.json:929:          "triple_var_retention": -0.48648864947347153
tools/data/3d_boundary_layers.json:939:          "SR2_mean": 0.38661022910891446,
tools/data/3d_boundary_layers.json:940:          "SR2_std": 0.000888133760979911,
tools/data/3d_boundary_layers.json:941:          "SR2_retention": 1.254345496792473,
tools/data/3d_boundary_layers.json:942:          "triple_var_mean": 2.9946227841003035,
tools/data/3d_boundary_layers.json:943:          "triple_var_std": 0.01613854473406449,
tools/data/3d_boundary_layers.json:944:          "triple_var_retention": 0.5253349313094372
tools/data/3d_boundary_layers.json:954:          "SR2_mean": 0.38697452985279857,
tools/data/3d_boundary_layers.json:955:          "SR2_std": 0.0010318319036034334,
tools/data/3d_boundary_layers.json:956:          "SR2_retention": -1.0510806000182598,
tools/data/3d_boundary_layers.json:957:          "triple_var_mean": 3.0015470917223195,
tools/data/3d_boundary_layers.json:958:          "triple_var_std": 0.019133649154810643,
tools/data/3d_boundary_layers.json:959:          "triple_var_retention": -0.13418374913135098
tools/data/3d_boundary_layers.json:969:          "SR2_mean": 0.3869318422824808,
tools/data/3d_boundary_layers.json:970:          "SR2_std": 0.0009576884689252662,
tools/data/3d_boundary_layers.json:971:          "SR2_retention": -0.7809383061245136,
tools/data/3d_boundary_layers.json:972:          "triple_var_mean": 3.0069257571482573,
tools/data/3d_boundary_layers.json:973:          "triple_var_std": 0.023832241858807546,
tools/data/3d_boundary_layers.json:974:          "triple_var_retention": -0.6464848365307834
tools/data/3d_boundary_layers.json:984:          "SR2_mean": 0.38662389034074085,
tools/data/3d_boundary_layers.json:985:          "SR2_std": 0.0008078985254278256,
tools/data/3d_boundary_layers.json:986:          "SR2_retention": 1.1678923097049403,
tools/data/3d_boundary_layers.json:987:          "triple_var_mean": 2.9974794669306117,
tools/data/3d_boundary_layers.json:988:          "triple_var_std": 0.019703446877410217,
tools/data/3d_boundary_layers.json:989:          "triple_var_retention": 0.2532448114695237
tools/data/3d_boundary_layers.json:999:          "SR2_mean": 0.3867761960193254,
tools/data/3d_boundary_layers.json:1000:          "SR2_std": 0.0010937040865386628,
tools/data/3d_boundary_layers.json:1001:          "SR2_retention": 0.20404721194973657,
tools/data/3d_boundary_layers.json:1002:          "triple_var_mean": 3.0054619465089982,
tools/data/3d_boundary_layers.json:1003:          "triple_var_std": 0.02260598143428804,
tools/data/3d_boundary_layers.json:1004:          "triple_var_retention": -0.5070614437644417
tools/data/3d_boundary_layers.json:1014:          "SR2_mean": 0.3867284683809595,
tools/data/3d_boundary_layers.json:1015:          "SR2_std": 0.0011212121183139453,
tools/data/3d_boundary_layers.json:1016:          "SR2_retention": 0.5060848686802039,
tools/data/3d_boundary_layers.json:1017:          "triple_var_mean": 3.005657069863304,
tools/data/3d_boundary_layers.json:1018:          "triple_var_std": 0.01675472257984995,
tools/data/3d_boundary_layers.json:1019:          "triple_var_retention": -0.5256463340061241
tools/data/seme_archive/piano_61.json:94:      "claim": "La pipeline Mk0/Mk1/Mk2 recupera correttamente i controlli empirici Mk0 e Mk1, ma sovrastima alcuni osservabili: cond_entropy su controllo Mk2 viene letto Layer 3 e num_var_10 su Poisson iid viene letto Layer 1. I claim two-layer devono quindi usare SR,L1,L2,SR2 come nucleo e dichiarare cond_entropy/num_var_10/run_length come diagnostiche secondarie finche non passano audit di recupero multi-seed.",
tools/data/seme_archive/piano_61.json:96:      "nota": "Report agent_20260504_1219: recovery audit con N=60000, n_surr=20, seed=20260504. Risultato: empirical_Mk0 PASS maxL0; empirical_Mk1 PASS maxL1; empirical_Mk2 FAIL cond_entropy Layer3; poisson_iid FAIL num_var_10 Layer1.",
tools/data/seme_archive/piano_60.json:65:      "nota": "Ciclo 60e (crossover): transizione di fase nel piano dipolare. Ciclo 60f (universalita'): la transizione di fase (lock+decay+flip) e' QUASI UNIVERSALE — 5/7 sequenze ordinate la mostrano. E' proprieta' del metodo (partial shuffle), non della sequenza. MA la DIREZIONE del lock e' diagnostica: GUE=-97.8, AR1=-94.7, Primi=-104.2, Periodic_24=-104.0, Logistic=+110.4. Due classi: 'repulsione' (~-97) e 'confinamento' (~-104). Primi coincidono con periodic 2,4,2,4 (Z/6Z) a 0.2 deg — il carattere dell'ordinamento primi E' il confinamento mod-6 (F2). Ciclo 60h (3D boundary): i due layer Markov (coppie→SR,L1; triple→SR2,triple_var) sono ACCOPPIATI al confine — alpha critico identico (0.334 per primi, delta=0.000; 0.311 vs 0.334 per GUE, delta=0.024). Il confine e' UNA transizione di fase, non due. I layer sono proiezioni dello stesso ordinamento. Poisson mostra separazione spuria (delta=-0.189, segnale=rumore). Consecutio: esiste una perturbazione NON-uniforme che disaccoppia i layer? (pair-preserving shuffle vs triple-preserving shuffle). Se si', i layer sono gradi di liberta' indipendenti. Se no, e' decomposizione spettrale di un solo asse.",
tools/data/seme_archive/piano_60.json:96:      "nota": "Ciclo 60, 5 osservabili su 4 scale (1e4-1e7). Ciclo 60c: angolo -111 +/- 1 deg (GUE -97, separazione 14 deg z=170). Ciclo 60f (universalita'): Primi (-104.2) = Periodic 2,4,2,4 (-104.0) a 0.2 deg. L'ordinamento primi nel piano (SR,L1) E' la struttura Z/6Z (F2). Ciclo 60g (observable hunt): la memoria Markov si decompone in due layer visibili. Layer 1 (coppie, Mk1): SR e L1 — il piano dipolare. Layer 2 (triple, Mk2): SR2 (z=-9.4 sotto Mk1, z=-0.05 sotto Mk2), L2 (z=-5.3/Mk1, z=-0.2/Mk2), cond_entropy (z=-51/Mk1, z=2.4/Mk2). I layer sono ortogonali. Mk3 non aggiunge nulla sui 10 osservabili testati. Il terzo asse e' SR2 (next-nearest-neighbor spacing ratio). Consecutio: derivare SR2=0.4785 analiticamente dalla serie singolare Hardy-Littlewood per tripletti di primi.",
tools/data/markov3_observable_hunt.json:14:    "triple_var": 2.797954,
tools/data/markov3_observable_hunt.json:15:    "SR2": 0.478527,
tools/data/markov3_observable_hunt.json:16:    "cond_entropy_L2": 3.058268,
tools/data/markov3_observable_hunt.json:18:    "num_var_10": 6.522885
tools/data/markov3_observable_hunt.json:27:      "triple_var": -17.18,
tools/data/markov3_observable_hunt.json:28:      "SR2": -3.68,
tools/data/markov3_observable_hunt.json:29:      "cond_entropy_L2": -653.32,
tools/data/markov3_observable_hunt.json:31:      "num_var_10": -10.23
tools/data/markov3_observable_hunt.json:39:      "triple_var": -3.65,
tools/data/markov3_observable_hunt.json:40:      "SR2": -9.41,
tools/data/markov3_observable_hunt.json:41:      "cond_entropy_L2": -51.33,
tools/data/markov3_observable_hunt.json:43:      "num_var_10": -3.92
tools/data/markov3_observable_hunt.json:51:      "triple_var": -0.55,
tools/data/markov3_observable_hunt.json:52:      "SR2": -0.05,
tools/data/markov3_observable_hunt.json:53:      "cond_entropy_L2": 2.36,
tools/data/markov3_observable_hunt.json:55:      "num_var_10": -1.21
tools/data/markov3_observable_hunt.json:63:      "triple_var": -0.75,
tools/data/markov3_observable_hunt.json:64:      "SR2": 0.17,
tools/data/markov3_observable_hunt.json:65:      "cond_entropy_L2": 2.59,
tools/data/markov3_observable_hunt.json:67:      "num_var_10": -0.66
tools/data/markov3_observable_hunt.json:111:    "triple_var": {
tools/data/markov3_observable_hunt.json:119:    "SR2": {
tools/data/markov3_observable_hunt.json:127:    "cond_entropy_L2": {
tools/data/markov3_observable_hunt.json:143:    "num_var_10": {
tools/data/evolution/evolution_20260503_0330.md:4:- **Possibilità chiave**: SR2 come terzo asse naturale del bicono (unisce i due strati indipendenti in una geometria 3D), l'amplificazione da informazione parziale come possibile fatto formale, e l'invisibilità di Markov-3 in osservabili scalari che suggerisce una natura topologica della memoria profonda.
tools/data/evolution/evolution_20260503_0330.md:5:- **Consecutio**: proiezione (SR, L1, SR2) per verificare se il bicono emerge dai due strati.
tools/data/markov_layer_recovery_audit.json:13:        "SR2": 0.48192,
tools/data/markov_layer_recovery_audit.json:14:        "cond_entropy": 3.025269,
tools/data/markov_layer_recovery_audit.json:15:        "triple_var": 282.300175,
tools/data/markov_layer_recovery_audit.json:16:        "num_var_10": 909.103589,
tools/data/markov_layer_recovery_audit.json:24:          "SR2": -6.67,
tools/data/markov_layer_recovery_audit.json:25:          "cond_entropy": -435.4,
tools/data/markov_layer_recovery_audit.json:26:          "triple_var": -15.9,
tools/data/markov_layer_recovery_audit.json:27:          "num_var_10": -7.46,
tools/data/markov_layer_recovery_audit.json:34:          "SR2": -8.8,
tools/data/markov_layer_recovery_audit.json:35:          "cond_entropy": -26.11,
tools/data/markov_layer_recovery_audit.json:36:          "triple_var": -0.53,
tools/data/markov_layer_recovery_audit.json:37:          "num_var_10": -1.42,
tools/data/markov_layer_recovery_audit.json:44:          "SR2": -0.33,
tools/data/markov_layer_recovery_audit.json:45:          "cond_entropy": 2.56,
tools/data/markov_layer_recovery_audit.json:46:          "triple_var": 0.36,
tools/data/markov_layer_recovery_audit.json:47:          "num_var_10": 0.0,
tools/data/markov_layer_recovery_audit.json:55:        "SR2": 2,
tools/data/markov_layer_recovery_audit.json:56:        "cond_entropy": 3,
tools/data/markov_layer_recovery_audit.json:57:        "triple_var": 1,
tools/data/markov_layer_recovery_audit.json:58:        "num_var_10": 1,
tools/data/markov_layer_recovery_audit.json:67:    "empirical_Mk0": {
tools/data/markov_layer_recovery_audit.json:74:        "SR2": 0.486324,
tools/data/markov_layer_recovery_audit.json:75:        "cond_entropy": 3.256306,
tools/data/markov_layer_recovery_audit.json:76:        "triple_var": 300.197284,
tools/data/markov_layer_recovery_audit.json:77:        "num_var_10": 1004.412664,
tools/data/markov_layer_recovery_audit.json:85:          "SR2": 1.34,
tools/data/markov_layer_recovery_audit.json:86:          "cond_entropy": -0.08,
tools/data/markov_layer_recovery_audit.json:87:          "triple_var": -0.58,
tools/data/markov_layer_recovery_audit.json:88:          "num_var_10": -0.11,
tools/data/markov_layer_recovery_audit.json:95:          "SR2": 0.29,
tools/data/markov_layer_recovery_audit.json:96:          "cond_entropy": 0.36,
tools/data/markov_layer_recovery_audit.json:97:          "triple_var": -0.09,
tools/data/markov_layer_recovery_audit.json:98:          "num_var_10": 0.24,
tools/data/markov_layer_recovery_audit.json:105:          "SR2": -0.23,
tools/data/markov_layer_recovery_audit.json:106:          "cond_entropy": 6.86,
tools/data/markov_layer_recovery_audit.json:107:          "triple_var": -0.16,
tools/data/markov_layer_recovery_audit.json:108:          "num_var_10": -0.27,
tools/data/markov_layer_recovery_audit.json:116:        "SR2": 0,
tools/data/markov_layer_recovery_audit.json:117:        "cond_entropy": 0,
tools/data/markov_layer_recovery_audit.json:118:        "triple_var": 0,
tools/data/markov_layer_recovery_audit.json:119:        "num_var_10": 0,
tools/data/markov_layer_recovery_audit.json:128:    "empirical_Mk1": {
tools/data/markov_layer_recovery_audit.json:135:        "SR2": 0.489876,
tools/data/markov_layer_recovery_audit.json:136:        "cond_entropy": 3.112451,
tools/data/markov_layer_recovery_audit.json:137:        "triple_var": 282.929526,
tools/data/markov_layer_recovery_audit.json:138:        "num_var_10": 907.774591,
tools/data/markov_layer_recovery_audit.json:146:          "SR2": 3.91,
tools/data/markov_layer_recovery_audit.json:147:          "cond_entropy": -270.9,
tools/data/markov_layer_recovery_audit.json:148:          "triple_var": -10.92,
tools/data/markov_layer_recovery_audit.json:149:          "num_var_10": -9.1,
tools/data/markov_layer_recovery_audit.json:156:          "SR2": -0.69,
tools/data/markov_layer_recovery_audit.json:157:          "cond_entropy": 0.23,
tools/data/markov_layer_recovery_audit.json:158:          "triple_var": 0.57,
tools/data/markov_layer_recovery_audit.json:159:          "num_var_10": -0.52,
tools/data/markov_layer_recovery_audit.json:166:          "SR2": 0.13,
tools/data/markov_layer_recovery_audit.json:167:          "cond_entropy": 1.62,
tools/data/markov_layer_recovery_audit.json:168:          "triple_var": 0.06,
tools/data/markov_layer_recovery_audit.json:169:          "num_var_10": -1.2,
tools/data/markov_layer_recovery_audit.json:177:        "SR2": 1,
tools/data/markov_layer_recovery_audit.json:178:        "cond_entropy": 1,
tools/data/markov_layer_recovery_audit.json:179:        "triple_var": 1,
tools/data/markov_layer_recovery_audit.json:180:        "num_var_10": 1,
tools/data/markov_layer_recovery_audit.json:189:    "empirical_Mk2": {
tools/data/markov_layer_recovery_audit.json:196:        "SR2": 0.481411,
tools/data/markov_layer_recovery_audit.json:197:        "cond_entropy": 3.017255,
tools/data/markov_layer_recovery_audit.json:198:        "triple_var": 278.431952,
tools/data/markov_layer_recovery_audit.json:199:        "num_var_10": 899.590466,
tools/data/markov_layer_recovery_audit.json:207:          "SR2": -6.3,
tools/data/markov_layer_recovery_audit.json:208:          "cond_entropy": -470.91,
tools/data/markov_layer_recovery_audit.json:209:          "triple_var": -10.23,
tools/data/markov_layer_recovery_audit.json:210:          "num_var_10": -7.93,
tools/data/markov_layer_recovery_audit.json:217:          "SR2": -6.77,
tools/data/markov_layer_recovery_audit.json:218:          "cond_entropy": -39.3,
tools/data/markov_layer_recovery_audit.json:219:          "triple_var": -1.2,
tools/data/markov_layer_recovery_audit.json:220:          "num_var_10": -2.06,
tools/data/markov_layer_recovery_audit.json:227:          "SR2": 0.09,
tools/data/markov_layer_recovery_audit.json:228:          "cond_entropy": 3.15,
tools/data/markov_layer_recovery_audit.json:229:          "triple_var": -0.12,
tools/data/markov_layer_recovery_audit.json:230:          "num_var_10": -0.04,
tools/data/markov_layer_recovery_audit.json:238:        "SR2": 2,
tools/data/markov_layer_recovery_audit.json:239:        "cond_entropy": 3,
tools/data/markov_layer_recovery_audit.json:240:        "triple_var": 1,
tools/data/markov_layer_recovery_audit.json:241:        "num_var_10": 2,
tools/data/markov_layer_recovery_audit.json:247:          "cond_entropy"
tools/data/markov_layer_recovery_audit.json:259:        "SR2": 0.386746,
tools/data/markov_layer_recovery_audit.json:260:        "cond_entropy": 3.565866,
tools/data/markov_layer_recovery_audit.json:261:        "triple_var": 2.985023,
tools/data/markov_layer_recovery_audit.json:262:        "num_var_10": 10.100335,
tools/data/markov_layer_recovery_audit.json:270:          "SR2": -1.8,
tools/data/markov_layer_recovery_audit.json:271:          "cond_entropy": -0.05,
tools/data/markov_layer_recovery_audit.json:272:          "triple_var": 1.17,
tools/data/markov_layer_recovery_audit.json:273:          "num_var_10": 2.13,
tools/data/markov_layer_recovery_audit.json:280:          "SR2": -1.5,
tools/data/markov_layer_recovery_audit.json:281:          "cond_entropy": 3.05,
tools/data/markov_layer_recovery_audit.json:282:          "triple_var": -0.0,
tools/data/markov_layer_recovery_audit.json:283:          "num_var_10": 0.85,
tools/data/markov_layer_recovery_audit.json:290:          "SR2": -0.56,
tools/data/markov_layer_recovery_audit.json:291:          "cond_entropy": 14.13,
tools/data/markov_layer_recovery_audit.json:292:          "triple_var": -0.15,
tools/data/markov_layer_recovery_audit.json:293:          "num_var_10": 0.52,
tools/data/markov_layer_recovery_audit.json:301:        "SR2": 0,
tools/data/markov_layer_recovery_audit.json:302:        "cond_entropy": 0,
tools/data/markov_layer_recovery_audit.json:303:        "triple_var": 0,
tools/data/markov_layer_recovery_audit.json:304:        "num_var_10": 1,
tools/data/markov_layer_recovery_audit.json:310:          "num_var_10"

codex
La pista più pulita è un audit di rango: se molte osservabili “passano” ma collassano su una sola direzione latente, allora il META non è falsificazione dei dati ma vincolo sul linguaggio. Preparo uno strumento riusabile che misura rango effettivo e correlazioni degli osservabili lungo perturbazioni partial-shuffle, con primes/GUE/Poisson come controllo.
exec
/bin/bash -lc "sed -n '1,280p' tools/exp_3d_boundary_layers.py" in /opt/MM_D-ND
 succeeded in 0ms:
#!/usr/bin/env python3
"""
exp_3d_boundary_layers.py — Does the boundary have 3D structure?

Consecutio from piano 60 runs:
  - Layer 1 (pairs, Mk1) → SR, L1 (the dipolar plane)
  - Layer 2 (triples, Mk2) → SR2, triple_var (depth)
  - The crossover (partial shuffle) shows a phase transition in (SR, L1)
  - META question: does Layer 2 transition at the SAME critical alpha?

If same α_c → the boundary is 2D (Layer 2 follows Layer 1 = partial tautology)
If different α_c → the boundary has genuine 3D depth (two independent transitions)

Tests on: primes, GUE, Poisson baseline.

Usage:
    python tools/exp_3d_boundary_layers.py [--N 50000] [--n_alpha 20] [--n_trials 30]
"""

import argparse
import json
import numpy as np
from scipy import stats
from pathlib import Path


def get_primes(n_max):
    sieve = np.ones(n_max + 1, dtype=bool)
    sieve[0] = sieve[1] = False
    for i in range(2, int(n_max**0.5) + 1):
        if sieve[i]:
            sieve[i*i::i] = False
    return np.where(sieve)[0]


def gue_gaps(N_mat, n_matrices, rng):
    """Generate GUE eigenvalue gaps."""
    all_gaps = []
    for _ in range(n_matrices):
        H = rng.standard_normal((N_mat, N_mat)) + 1j * rng.standard_normal((N_mat, N_mat))
        H = (H + H.conj().T) / 2
        evals = np.sort(np.linalg.eigvalsh(H))
        gaps = np.diff(evals)
        gaps = gaps[gaps > 0]
        all_gaps.extend(gaps.tolist())
    return np.array(all_gaps)


def partial_shuffle(seq, alpha, rng):
    s = seq.copy()
    n = len(s)
    k = int(alpha * n)
    if k < 2:
        return s
    idx = rng.choice(n, size=k, replace=False)
    vals = s[idx].copy()
    rng.shuffle(vals)
    s[idx] = vals
    return s


# --- Layer 1 observables (pair statistics) ---
def obs_spacing_ratio(gaps):
    r = np.minimum(gaps[:-1], gaps[1:]) / np.maximum(gaps[:-1], gaps[1:])
    return np.mean(r[np.isfinite(r)])

def obs_lag1_acf(gaps):
    g = gaps - np.mean(gaps)
    c0 = np.mean(g**2)
    if c0 == 0: return 0.0
    return np.mean(g[:-1] * g[1:]) / c0

# --- Layer 2 observables (triple statistics) ---
def obs_sr2(gaps):
    """Next-nearest-neighbor spacing ratio: min(g_n, g_{n+2})/max(g_n, g_{n+2})"""
    r = np.minimum(gaps[:-2], gaps[2:]) / np.maximum(gaps[:-2], gaps[2:])
    return np.mean(r[np.isfinite(r)])

def obs_triple_var(gaps):
    """Variance of consecutive triple sums, normalized."""
    triples = gaps[:-2] + gaps[1:-1] + gaps[2:]
    v = np.var(gaps)
    if v == 0: return 0.0
    return np.var(triples) / v


def run_crossover(gaps, alphas, n_trials, rng, label=""):
    """Compute all 4 observables at each alpha level."""
    obs_fns = {
        'SR': obs_spacing_ratio,
        'L1': obs_lag1_acf,
        'SR2': obs_sr2,
        'triple_var': obs_triple_var,
    }

    # Full shuffle baseline (alpha=1.0)
    baselines = {name: [] for name in obs_fns}
    for _ in range(n_trials * 3):
        shuffled = partial_shuffle(gaps, 1.0, rng)
        for name, fn in obs_fns.items():
            baselines[name].append(fn(shuffled))
    baseline_mean = {name: np.mean(vals) for name, vals in baselines.items()}
    baseline_std = {name: np.std(vals) for name, vals in baselines.items()}

    # Original values (alpha=0)
    originals = {name: fn(gaps) for name, fn in obs_fns.items()}

    results = []
    for alpha in alphas:
        trial_vals = {name: [] for name in obs_fns}
        for _ in range(n_trials):
            s = partial_shuffle(gaps, alpha, rng)
            for name, fn in obs_fns.items():
                trial_vals[name].append(fn(s))

        row = {'alpha': float(alpha)}
        for name in obs_fns:
            mean_val = np.mean(trial_vals[name])
            std_val = np.std(trial_vals[name])
            # Fraction of original signal retained
            orig_delta = originals[name] - baseline_mean[name]
            curr_delta = mean_val - baseline_mean[name]
            if abs(orig_delta) > 1e-12:
                retention = curr_delta / orig_delta
            else:
                retention = 0.0
            row[f'{name}_mean'] = float(mean_val)
            row[f'{name}_std'] = float(std_val)
            row[f'{name}_retention'] = float(retention)
        results.append(row)

    return results, originals, baseline_mean, baseline_std


def find_critical_alpha(results, obs_name, threshold=0.5):
    """Find alpha where retention drops below threshold (signal half-life)."""
    for r in results:
        if r[f'{obs_name}_retention'] < threshold:
            return r['alpha']
    return 1.0  # never crossed


def find_zero_crossing(results, obs_name):
    """Find alpha where retention crosses zero (sign flip)."""
    for i in range(1, len(results)):
        r0 = results[i-1][f'{obs_name}_retention']
        r1 = results[i][f'{obs_name}_retention']
        if r0 * r1 < 0:
            # Linear interpolation
            a0 = results[i-1]['alpha']
            a1 = results[i]['alpha']
            alpha_zero = a0 + (a1 - a0) * abs(r0) / (abs(r0) + abs(r1))
            return alpha_zero
    return None


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--N', type=int, default=50000, help='Number of primes')
    parser.add_argument('--n_alpha', type=int, default=20, help='Number of alpha steps')
    parser.add_argument('--n_trials', type=int, default=30, help='Trials per alpha')
    parser.add_argument('--seed', type=int, default=42)
    args = parser.parse_args()

    rng = np.random.default_rng(args.seed)
    alphas = np.linspace(0.05, 0.95, args.n_alpha)

    print(f"=== 3D Boundary Layer Experiment ===")
    print(f"N_primes={args.N}, n_alpha={args.n_alpha}, n_trials={args.n_trials}")

    # --- Primes ---
    print("\n--- PRIMES ---")
    primes = get_primes(args.N * 20)[:args.N]
    prime_gaps = np.diff(primes).astype(float)
    prime_results, prime_orig, prime_bl_mean, prime_bl_std = run_crossover(
        prime_gaps, alphas, args.n_trials, rng, "Primes"
    )

    # --- GUE ---
    print("\n--- GUE ---")
    n_mat = 200
    n_matrices = max(5, args.N // n_mat)
    gue_g = gue_gaps(n_mat, n_matrices, rng)
    if len(gue_g) > args.N:
        gue_g = gue_g[:args.N]
    gue_results, gue_orig, gue_bl_mean, gue_bl_std = run_crossover(
        gue_g, alphas, args.n_trials, rng, "GUE"
    )

    # --- Poisson (exponential gaps, iid) ---
    print("\n--- POISSON ---")
    poisson_gaps = rng.exponential(1.0, size=args.N)
    pois_results, pois_orig, pois_bl_mean, pois_bl_std = run_crossover(
        poisson_gaps, alphas, args.n_trials, rng, "Poisson"
    )

    # --- Analysis ---
    obs_names = ['SR', 'L1', 'SR2', 'triple_var']
    layer_map = {'SR': 'L1_pair', 'L1': 'L1_pair', 'SR2': 'L2_triple', 'triple_var': 'L2_triple'}

    output = {
        'experiment': '3D Boundary Layers',
        'question': 'Do Layer 1 (pairs) and Layer 2 (triples) transition at the same critical alpha?',
        'params': {'N': args.N, 'n_alpha': args.n_alpha, 'n_trials': args.n_trials, 'seed': args.seed},
        'sequences': {}
    }

    for name, results, originals, bl_mean, bl_std in [
        ('primes', prime_results, prime_orig, prime_bl_mean, prime_bl_std),
        ('gue', gue_results, gue_orig, gue_bl_mean, gue_bl_std),
        ('poisson', pois_results, pois_orig, pois_bl_mean, pois_bl_std),
    ]:
        seq_data = {
            'originals': {k: float(v) for k, v in originals.items()},
            'baseline_mean': {k: float(v) for k, v in bl_mean.items()},
            'baseline_std': {k: float(v) for k, v in bl_std.items()},
            'critical_alpha_50': {},
            'zero_crossing': {},
            'retention_curve': results,
        }
        print(f"\n=== {name.upper()} ===")
        print(f"{'Observable':>12} {'Layer':>10} {'Original':>10} {'Baseline':>10} {'α_crit(50%)':>12} {'α_zero':>8}")
        for obs in obs_names:
            ac = find_critical_alpha(results, obs)
            az = find_zero_crossing(results, obs)
            seq_data['critical_alpha_50'][obs] = float(ac)
            seq_data['zero_crossing'][obs] = float(az) if az else None
            print(f"{obs:>12} {layer_map[obs]:>10} {originals[obs]:>10.5f} {bl_mean[obs]:>10.5f} {ac:>12.3f} {str(az and f'{az:.3f}') or 'none':>8}")

        # Layer separation: difference in critical alpha between layers
        l1_crit = np.mean([seq_data['critical_alpha_50'][o] for o in ['SR', 'L1']])
        l2_crit = np.mean([seq_data['critical_alpha_50'][o] for o in ['SR2', 'triple_var']])
        delta_crit = l2_crit - l1_crit
        seq_data['layer_separation'] = {
            'L1_mean_crit': float(l1_crit),
            'L2_mean_crit': float(l2_crit),
            'delta': float(delta_crit),
        }
        print(f"\n  Layer 1 mean α_crit: {l1_crit:.3f}")
        print(f"  Layer 2 mean α_crit: {l2_crit:.3f}")
        print(f"  Δα (L2 - L1): {delta_crit:+.3f}")
        if abs(delta_crit) > 0.05:
            print(f"  → SEPARATION: Layer 2 transitions {'later' if delta_crit > 0 else 'earlier'} than Layer 1")
        else:
            print(f"  → COINCIDENT: Layers transition together (|Δα| < 0.05)")

        output['sequences'][name] = seq_data

    # Summary
    prime_sep = output['sequences']['primes']['layer_separation']['delta']
    gue_sep = output['sequences']['gue']['layer_separation']['delta']
    pois_sep = output['sequences']['poisson']['layer_separation']['delta']

    print(f"\n=== SUMMARY ===")
    print(f"Layer separation Δα: Primes={prime_sep:+.3f}, GUE={gue_sep:+.3f}, Poisson={pois_sep:+.3f}")

    output['summary'] = {
        'prime_layer_separation': float(prime_sep),
        'gue_layer_separation': float(gue_sep),
        'poisson_layer_separation': float(pois_sep),
    }

    # Save
    out_path = Path(__file__).parent / 'data' / '3d_boundary_layers.json'
    with open(out_path, 'w') as f:
        json.dump(output, f, indent=2)
    print(f"\nSaved: {out_path}")


if __name__ == '__main__':
    main()

exec
/bin/bash -lc "sed -n '1,260p' tools/exp_markov_layer_recovery_audit.py" in /opt/MM_D-ND
 succeeded in 0ms:
#!/usr/bin/env python3
"""
exp_markov_layer_recovery_audit.py - META audit for the Markov-layer classifier.

Question:
  If a sequence is generated with known Markov order k, does the existing
  Mk0/Mk1/Mk2 classifier recover layer <= k?

Why this matters:
  Recent reports used Mk surrogate capture to claim two visible memory layers.
  This audit tests the opposite: whether the classifier itself creates apparent
  higher layers on finite Markov controls.

Controls:
  - prime_gaps: unknown real target, reported for comparison only
  - empirical_Mk0: shuffled prime gaps; known order 0
  - empirical_Mk1: prime-trained Mk1 surrogate; known order 1
  - empirical_Mk2: prime-trained Mk2 surrogate; known order 2
  - poisson_iid: independent exponential spacings; known order 0

Verdict rule:
  A control fails recovery when any observable is assigned layer > known order.
"""

import argparse
import json
from pathlib import Path

import numpy as np

from exp_two_layer_universality import (
    OBSERVABLES,
    classify_layer,
    gen_prime_gaps,
    generate_markov_surrogate,
)


def measure_all(gaps):
    out = {}
    for name, fn in OBSERVABLES.items():
        try:
            value = float(fn(gaps))
        except Exception:
            value = float("nan")
        out[name] = value
    return out


def classifier_pass(gaps, n_surr, rng):
    real_obs = measure_all(gaps)
    z_scores = {}

    for mk in (0, 1, 2):
        surr_obs = {name: [] for name in OBSERVABLES}
        for _ in range(n_surr):
            if mk == 0:
                surr = rng.permutation(gaps)
            else:
                surr = generate_markov_surrogate(gaps, mk, rng=rng)
            for obs_name, obs_fn in OBSERVABLES.items():
                try:
                    surr_obs[obs_name].append(float(obs_fn(surr)))
                except Exception:
                    pass

        z_mk = {}
        for obs_name in OBSERVABLES:
            vals = np.asarray(surr_obs[obs_name], dtype=float)
            vals = vals[np.isfinite(vals)]
            if len(vals) > 2 and np.std(vals) > 1e-12 and np.isfinite(real_obs[obs_name]):
                z = (real_obs[obs_name] - np.mean(vals)) / np.std(vals)
            else:
                z = 0.0
            z_mk[obs_name] = round(float(z), 2)
        z_scores[f"Mk{mk}"] = z_mk

    layers = {}
    for obs_name in OBSERVABLES:
        layers[obs_name] = classify_layer(
            z_scores["Mk0"][obs_name],
            z_scores["Mk1"][obs_name],
            z_scores["Mk2"][obs_name],
        )

    return real_obs, z_scores, layers


def build_controls(prime_gaps, rng):
    return {
        "prime_gaps": {
            "known_order": None,
            "gaps": prime_gaps,
        },
        "empirical_Mk0": {
            "known_order": 0,
            "gaps": rng.permutation(prime_gaps),
        },
        "empirical_Mk1": {
            "known_order": 1,
            "gaps": generate_markov_surrogate(prime_gaps, 1, rng=rng),
        },
        "empirical_Mk2": {
            "known_order": 2,
            "gaps": generate_markov_surrogate(prime_gaps, 2, rng=rng),
        },
        "poisson_iid": {
            "known_order": 0,
            "gaps": rng.exponential(1.0, len(prime_gaps)),
        },
    }


def summarize_failure(layers, known_order):
    if known_order is None:
        return {
            "status": "target_only",
            "over_layer_observables": [],
            "max_layer": int(max(layers.values())),
        }

    over = [name for name, layer in layers.items() if layer > known_order]
    return {
        "status": "PASS" if not over else "FAIL",
        "over_layer_observables": over,
        "max_layer": int(max(layers.values())),
    }


def run(N=60000, n_surr=20, seed=20260504):
    rng = np.random.default_rng(seed)
    prime_gaps = gen_prime_gaps(N).astype(float)
    controls = build_controls(prime_gaps, rng)

    results = {
        "N": int(N),
        "n_surr": int(n_surr),
        "seed": int(seed),
        "controls": {},
    }

    print(f"N={N}, n_surr={n_surr}, seed={seed}")
    print(f"{'sequence':<16} {'known':<7} {'status':<8} {'maxL':<5} over-layer observables")
    print("-" * 82)

    for name, spec in controls.items():
        real_obs, z_scores, layers = classifier_pass(spec["gaps"], n_surr, rng)
        summary = summarize_failure(layers, spec["known_order"])
        known = "target" if spec["known_order"] is None else f"L{spec['known_order']}"
        over = ", ".join(summary["over_layer_observables"]) or "-"
        print(f"{name:<16} {known:<7} {summary['status']:<8} L{summary['max_layer']:<4} {over}")

        results["controls"][name] = {
            "known_order": spec["known_order"],
            "n_gaps": int(len(spec["gaps"])),
            "real_obs": {k: round(v, 6) for k, v in real_obs.items()},
            "z_scores": z_scores,
            "layers": layers,
            "recovery": summary,
        }

    out_path = Path("tools/data/markov_layer_recovery_audit.json")
    with out_path.open("w") as f:
        json.dump(results, f, indent=2)
    print(f"\nSaved to {out_path}")
    return results


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--N", type=int, default=60000)
    parser.add_argument("--n_surr", type=int, default=20)
    parser.add_argument("--seed", type=int, default=20260504)
    args = parser.parse_args()
    run(N=args.N, n_surr=args.n_surr, seed=args.seed)


if __name__ == "__main__":
    main()

exec
/bin/bash -lc "sed -n '1,220p' tools/data/seme.json" in /opt/MM_D-ND
 succeeded in 0ms:
{
  "timestamp": "2026-05-04T12:29:54.922094",
  "piano": 62,
  "tensioni": [
    {
      "tipo": "confine_inesplorato",
      "id": "TRASCENDENZA_LIMITE",
      "claim": "La trascendenza e il limite attuale del modello. I punti fissi relazionali (non solo phi ma la rete di punti fissi tra osservabili) possono rivelare il vero grafo della realta e pattern nelle matrici. Il confine non e nella matematica - e nel passaggio tra piani.",
      "intensita": 0.9,
      "nota": "Input operatore 2026-04-10. Tocca: confine del modello, struttura relazionale dei punti fissi. Consecutio: quali punti fissi relazionali emergono dalle 21 tensioni attuali? Il grafo e gia nei dati?",
      "manuale": true,
      "porta": "sessione_interattiva",
      "condensato_ref": "A3,A10",
      "condensato_motivo": "Estende A3 (punto fisso singolo) a rete relazionale. Tocca A10 (dipolo) come caso speciale."
    },
    {
      "tipo": "scoperta",
      "id": "DUALITA_DIPOLARE_VS_ILLUSORIA",
      "claim": "Due tipi di dualita: (1) dipolare - generativa, il modello (det=-1), (2) illusoria - dispersiva, entropia (det=+1). Le regole incoerenti producono la seconda. La dualita illusoria e entropia come dispersione, non come informazione.",
      "intensita": 0.9,
      "nota": "Input operatore 2026-04-10. Tocca: entropia come dispersione illusoria vs generazione dipolare. Consecutio: nel Lab i domini Poisson (entropia massima) mostrano dualita illusoria? I domini GUE (strutturati) mostrano dualita dipolare? Il drift verso Poisson (POISSON_CONVERGENCE) e perdita di dualita dipolare?",
      "manuale": true,
      "porta": "sessione_interattiva",
      "condensato_ref": "A2,A10,F5",
      "condensato_motivo": "Discrimina due forme di det. A2 (confine) e la soglia. A10 (dipolo) e il tipo 1. F5 (frame) misura la struttura D-ND che e tipo 1."
    },
    {
      "tipo": "scoperta_numerica",
      "id": "METRIC_TENSOR",
      "claim": "Il tensore metrico dei primi è g=(p/2)². Nel tempo ln(p), è de Sitter 1+1D. z=-8.8 curvatura vs z=+22.5 rapporti ΔΓ.",
      "intensità": 0.9,
      "nota": "Sessione interattiva 4 aprile. Verificato su 78K primi.",
      "manuale": true,
      "porta": "sessione_interattiva",
      "condensato_ref": null,
      "condensato_motivo": "Risultato numerico verificato, non-tautologico"
    },
    {
      "tipo": "scoperta",
      "id": "TENSIONE_ENTITA",
      "claim": "La tensione non e un problema pratico - e un Entita. La tensione superflua crea latenza (tempo). Senza tensione superflua tutto e regolato da assiomi. Implicazione: le tensioni nel seme sono entita, non problemi da risolvere. Quelle superflue (det=+1) producono tempo/latenza.",
      "intensita": 0.85,
      "nota": "Input operatore 2026-04-10. Tocca: rapporto tensione/assioma. Operativamente: discriminare tensioni-entita (generative) da tensioni-superflue (dispersive) nel seme. Le 21 tensioni attuali - quante sono entita e quante latenza?",
      "manuale": true,
      "porta": "sessione_interattiva",
      "condensato_ref": "A5,A6",
      "condensato_motivo": "Il ciclo (A5) lavora con tensioni - ma se la tensione e entita, il ciclo non le risolve, le osserva. Lo zero mobile (A6) e la tensione senza latenza."
    },
    {
      "tipo": "confine_inesplorato",
      "id": "G_POTENZIALE_NULLA",
      "claim": "G e il potenziale di tutto come nulla - permette il prima e il dopo. Ci muoviamo come trascendenza dimensionale gravitazionale. G nel tetraedro non e una teoria tra le altre - e il potenziale che le rende possibili.",
      "intensita": 0.85,
      "nota": "Input operatore 2026-04-10. Tocca: ruolo di G nel tetraedro (T,Q,G,E). La fonte video_lp0RgZ6kQF8 dice: tensore metrico dentro la forma simplettica. G non e accanto a T,Q,E - e sotto. Consecutio: nei dati Lab, i ponti TxG e ExG hanno struttura diversa dai ponti TxQ?",
      "manuale": true,
      "porta": "sessione_interattiva",
      "condensato_ref": "A7,A10",
      "condensato_motivo": "A7 (singolarita come operatore) e G come potenziale. A10 (dipolo) opera sul piano che G rende possibile."
    },
    {
      "tipo": "confine_inesplorato",
      "id": "BOUNDARY",
      "claim": "8 domini GUE, 5 Poisson — il confine è il terzo incluso operativo",
      "intensità": 0.8,
      "nota": "Il segnale non-triviale è DOVE la scissione cambia natura, non che converge a φ",
      "condensato_ref": "A9",
      "condensato_motivo": "Overlap termini con A9 (5 termini)",
      "porta": "condensato"
    },
    {
      "tipo": "confine_inesplorato",
      "id": "PIANO_PRIMARIO_DUE_ASSIOMI",
      "claim": "I piani importanti sono il primario e i due assiomi che lo determinano nelle zone osservate. Non tutti gli assiomi operano ovunque - in ogni zona osservata, due assiomi determinano il piano primario.",
      "intensita": 0.8,
      "nota": "Input operatore 2026-04-10. Tocca: struttura locale degli assiomi. Consecutio: per ogni dominio Lab (primi, logistica, percolazione...) quali 2 assiomi del condensato sono operativi? Mappa assiomi x domini = grafo della realta locale.",
      "manuale": true,
      "porta": "sessione_interattiva",
      "condensato_ref": "A9,A14",
      "condensato_motivo": "A9 (terzo incluso) opera CON il piano. A14 (cascata) propaga - ma propaga cosa, se solo 2 assiomi sono attivi per zona?"
    },
    {
      "tipo": "simmetria_sospetta",
      "id": "META",
      "claim": "Tutti i 11 test passano — verifica che non stiamo testando solo tautologie",
      "intensità": 0.5,
      "nota": "La convergenza a φ è triviale (controprove). I test stanno verificando contenuto o struttura?",
      "condensato_ref": null,
      "condensato_motivo": "Ricorrente (3x in 2 giorni) e fuori dalla mappa",
      "porta": "novità"
    }
  ],
  "potenziale_bloccato": [],
  "varianza": [
    "Tensioni risolte: {'DUALITA_DIPOLARE_VS_ILLUSORIA', 'METRIC_TENSOR', 'TENSIONE_ENTITA', 'TRASCENDENZA_LIMITE', 'G_POTENZIALE_NULLA', 'PIANO_PRIMARIO_DUE_ASSIOMI', 'META_LAYER_RECOVERY_AUDIT'}"
  ],
  "filtro": {
    "promosse": 8,
    "filtrate": 0,
    "condensato_claims": 30
  },
  "direzione": "Esplorare il confine: 8 domini GUE, 5 Poisson — il confine è il terzo incluso operativo",
  "verifica": {
    "pass": 11,
    "fail": 0,
    "skip": 0,
    "total": 11
  },
  "fonti_consumate": 0,
  "fonti_esterne": [
    {
      "id": "video_lp0RgZ6kQF8",
      "title": "Equivalence between geometrical structures and entropy",
      "type": "video_digest",
      "keywords": [
        "geometry",
        "entropy",
        "symplectic form",
        "statistical mechanics",
        "quantum",
        "thermodynamics",
        "inner product",
        "Born rule",
        "metric tensor",
        "electromagnetic tensor"
      ],
      "content": "La geometria degli stati (classici e quantistici) e l'entropia sono la stessa struttura — invertibili. La forma simplettica conta le configurazioni. Il tensore metrico dello spaziotempo appare dentro la forma simplettica estesa. Il tensore elettromagnetico pure. Statistical mechanics non è costruita sopra alla meccanica — è la stessa cosa.",
      "teorie": [
        "T",
        "Q",
        "G",
        "E"
      ],
      "ponti_potenziali": [
        {
          "coppia": "TxQ",
          "ponte": "forma simplettica = entropia (invertibili)",
          "nota": "geometry is entropy and entropy is geometry"
        },
        {
          "coppia": "TxG",
          "ponte": "tensore metrico dentro la forma simplettica estesa",
          "nota": "geometria spaziotempo = geometria degli stati in posizione×velocità"
        },
        {
          "coppia": "ExT",
          "ponte": "tensore EM dentro la forma simplettica",
          "nota": "il campo EM conta stati in configurazione posizione×tempo"
        }
      ],
      "timestamp": "2026-04-02T08:23:13.991997"
    },
    {
      "id": "video_sDlZ-aY9GN4",
      "title": "Moving charges produce magnetic fields - Einstein relativity",
      "type": "video_digest",
      "keywords": [
        "magnetic field",
        "electric field",
        "length contraction",
        "time dilation",
        "Coulomb",
        "Lorentz",
        "reference frame",
        "electromagnetic"
      ],
      "content": "Il campo magnetico non esiste come entità separata — è il campo elettrico visto da un altro frame. La contrazione di Lorentz trasforma neutralità in carica. Due elettroni in movimento si separano più lentamente del previsto non per forza magnetica ma per dilatazione temporale. E e B sono manifestazioni dello stesso campo elettromagnetico. La relatività unifica.",
      "teorie": [
        "E",
        "R"
      ],
      "ponti_potenziali": [
        {
          "coppia": "ExR",
          "ponte": "cambio di frame — E e B sono lo stesso campo",
          "nota": "il 'ponte' non è l'onda EM, è il cambio di osservatore"
        }
      ],
      "timestamp": "2026-04-02T08:23:13.992016"
    },
    {
      "id": "video_OwDWOtFNsKQ",
      "title": "Thermodynamic Computing: Better than Quantum? | Guillaume Verdon (Extropic)",
      "type": "video_digest",
      "keywords": [
        "thermodynamic computing",
        "noise",
        "entropy",
        "extropic",
        "fluctuations",
        "information theory",
        "probability",
        "Boltzmann"
      ],
      "content": "Computing termodinamico: sfruttare le fluttuazioni termiche invece di combatterle. Il rumore non è nemico — è risorsa. Extropic costruisce hardware che usa l'entropia come motore computazionale. Connessione profonda tra termodinamica, teoria dell'informazione e probabilità.",
      "teorie": [
        "T",
        "Q"
      ],
      "ponti_potenziali": [
        {
          "coppia": "TxQ",
          "ponte": "noise come risorsa computazionale — fluttuazioni termiche = calcolo",
          "nota": "il vuoto quantistico (pieno di fluttuazioni) è il computer"
        }
      ],
      "timestamp": "2026-04-02T08:23:13.992019"
    },
    {
      "id": "video_j0wJBEZdwLs",
      "title": "What is a Laplace Transform - visual explanation",
      "type": "video_digest",
      "keywords": [
        "Laplace transform",
        "frequency",
        "damping",
        "s-plane",
        "complex",
        "exponential"
      ],
      "content": "La trasformata di Laplace come proiezione su esponenziali complesse. Il piano s = σ + iω combina smorzamento (reale) e oscillazione (immaginario). Connessione tra dominio temporale e dominio delle frequenze complesse.",

codex
Prima di scrivere il report creo `tools/exp_observable_rank_audit.py`. Non usa soglie concettuali per decidere lo stato del sistema: misura soltanto quanta dimensionalità resta nelle curve degli osservabili dopo normalizzazione rispetto allo shuffle completo.
apply patch
patch: completed
/opt/MM_D-ND/tools/exp_observable_rank_audit.py
diff --git a/tools/exp_observable_rank_audit.py b/tools/exp_observable_rank_audit.py
new file mode 100644
index 0000000000000000000000000000000000000000..f2acafc5960c8a35964e0ab521cf70569eefe27f
--- /dev/null
+++ b/tools/exp_observable_rank_audit.py
@@ -0,0 +1,229 @@
+#!/usr/bin/env python3
+"""
+exp_observable_rank_audit.py - META audit for observable redundancy.
+
+Recent runs found many Markov/crossover observables that react coherently under
+partial shuffle. This script asks whether those observables carry independent
+directions or mostly re-measure one latent boundary coordinate.
+
+It measures retention curves from alpha-partial shuffles, then reports:
+  - original-vs-full-shuffle z for each observable
+  - PCA energy of the retention matrix across alpha
+  - effective rank of that matrix
+  - pairwise correlations between retention curves
+
+The script measures data only. The report decides the structural claim.
+"""
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from exp_3d_boundary_layers import get_primes, gue_gaps, partial_shuffle
+
+
+def obs_spacing_ratio(gaps):
+    r = np.minimum(gaps[:-1], gaps[1:]) / np.maximum(gaps[:-1], gaps[1:])
+    r = r[np.isfinite(r)]
+    return float(np.mean(r)) if len(r) else 0.0
+
+
+def obs_lag_acf(gaps, lag):
+    g = gaps - np.mean(gaps)
+    c0 = np.mean(g * g)
+    if c0 == 0:
+        return 0.0
+    return float(np.mean(g[:-lag] * g[lag:]) / c0)
+
+
+def obs_sr2(gaps):
+    r = np.minimum(gaps[:-2], gaps[2:]) / np.maximum(gaps[:-2], gaps[2:])
+    r = r[np.isfinite(r)]
+    return float(np.mean(r)) if len(r) else 0.0
+
+
+def obs_triple_var(gaps):
+    triples = gaps[:-2] + gaps[1:-1] + gaps[2:]
+    v = np.var(gaps)
+    if v == 0:
+        return 0.0
+    return float(np.var(triples) / v)
+
+
+OBSERVABLES = {
+    "SR": obs_spacing_ratio,
+    "L1": lambda gaps: obs_lag_acf(gaps, 1),
+    "L2": lambda gaps: obs_lag_acf(gaps, 2),
+    "SR2": obs_sr2,
+    "triple_var": obs_triple_var,
+}
+
+
+def measure(gaps):
+    return {name: fn(gaps) for name, fn in OBSERVABLES.items()}
+
+
+def full_shuffle_baseline(gaps, n_trials, rng):
+    vals = {name: [] for name in OBSERVABLES}
+    for _ in range(n_trials):
+        s = rng.permutation(gaps)
+        row = measure(s)
+        for name, value in row.items():
+            vals[name].append(value)
+    return {
+        name: {
+            "mean": float(np.mean(x)),
+            "std": float(np.std(x, ddof=1)) if len(x) > 1 else 0.0,
+        }
+        for name, x in vals.items()
+    }
+
+
+def retention_curves(gaps, alphas, n_trials, originals, baseline, rng):
+    rows = []
+    for alpha in alphas:
+        vals = {name: [] for name in OBSERVABLES}
+        for _ in range(n_trials):
+            s = partial_shuffle(gaps, float(alpha), rng)
+            row = measure(s)
+            for name, value in row.items():
+                vals[name].append(value)
+
+        out = {"alpha": float(alpha)}
+        for name in OBSERVABLES:
+            mean = float(np.mean(vals[name]))
+            denom = originals[name] - baseline[name]["mean"]
+            retention = (mean - baseline[name]["mean"]) / denom if abs(denom) > 1e-12 else 0.0
+            out[name] = {
+                "mean": mean,
+                "std": float(np.std(vals[name], ddof=1)) if len(vals[name]) > 1 else 0.0,
+                "retention": float(retention),
+            }
+        rows.append(out)
+    return rows
+
+
+def pca_summary(rows):
+    names = list(OBSERVABLES)
+    matrix = np.array([[row[name]["retention"] for name in names] for row in rows], dtype=float)
+    matrix = matrix - np.mean(matrix, axis=0, keepdims=True)
+
+    _, singular, vt = np.linalg.svd(matrix, full_matrices=False)
+    energy = singular * singular
+    if np.sum(energy) <= 1e-15:
+        explained = np.zeros_like(energy)
+        effective_rank = 0.0
+    else:
+        explained = energy / np.sum(energy)
+        positive = explained[explained > 1e-15]
+        effective_rank = float(np.exp(-np.sum(positive * np.log(positive))))
+
+    corr = np.corrcoef(matrix, rowvar=False)
+    abs_corr = np.abs(corr)
+    upper = abs_corr[np.triu_indices_from(abs_corr, k=1)]
+
+    return {
+        "observables": names,
+        "singular_values": [float(x) for x in singular],
+        "explained_variance": [float(x) for x in explained],
+        "effective_rank": effective_rank,
+        "pc1_loadings": {name: float(vt[0, i]) for i, name in enumerate(names)} if len(vt) else {},
+        "mean_abs_pairwise_corr": float(np.mean(upper)) if len(upper) else 0.0,
+        "min_abs_pairwise_corr": float(np.min(upper)) if len(upper) else 0.0,
+        "max_abs_pairwise_corr": float(np.max(upper)) if len(upper) else 0.0,
+    }
+
+
+def analyze_sequence(name, gaps, alphas, n_trials, n_baseline, rng):
+    originals = measure(gaps)
+    baseline = full_shuffle_baseline(gaps, n_baseline, rng)
+    rows = retention_curves(gaps, alphas, n_trials, originals, baseline, rng)
+
+    z = {}
+    for obs_name in OBSERVABLES:
+        std = baseline[obs_name]["std"]
+        z[obs_name] = float((originals[obs_name] - baseline[obs_name]["mean"]) / std) if std > 1e-12 else 0.0
+
+    return {
+        "n_gaps": int(len(gaps)),
+        "originals": originals,
+        "full_shuffle_baseline": baseline,
+        "original_vs_shuffle_z": z,
+        "retention_curves": rows,
+        "pca": pca_summary(rows),
+    }
+
+
+def build_sequences(n_gaps, rng):
+    primes = get_primes(n_gaps * 24)[: n_gaps + 1]
+    prime_gaps = np.diff(primes).astype(float)
+
+    gue = gue_gaps(160, max(8, n_gaps // 160 + 1), rng).astype(float)
+    gue = gue[:n_gaps]
+
+    poisson = rng.exponential(1.0, size=n_gaps).astype(float)
+    return {
+        "primes": prime_gaps,
+        "gue": gue,
+        "poisson": poisson,
+    }
+
+
+def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505):
+    rng = np.random.default_rng(seed)
+    alphas = np.linspace(0.05, 0.95, n_alpha)
+    sequences = build_sequences(n_gaps, rng)
+
+    output = {
+        "experiment": "observable_rank_audit",
+        "question": "Do crossover observables contain independent directions or one latent boundary coordinate?",
+        "params": {
+            "n_gaps": int(n_gaps),
+            "n_alpha": int(n_alpha),
+            "n_trials": int(n_trials),
+            "n_baseline": int(n_baseline),
+            "seed": int(seed),
+        },
+        "sequences": {},
+    }
+
+    print(f"n_gaps={n_gaps}, n_alpha={n_alpha}, n_trials={n_trials}, n_baseline={n_baseline}, seed={seed}")
+    print(f"{'sequence':<10} {'pc1':>8} {'eff_rank':>9} {'mean|corr|':>11}  z(SR,L1,L2,SR2,triple)")
+    print("-" * 92)
+    for seq_name, gaps in sequences.items():
+        result = analyze_sequence(seq_name, gaps, alphas, n_trials, n_baseline, rng)
+        output["sequences"][seq_name] = result
+        pca = result["pca"]
+        z = result["original_vs_shuffle_z"]
+        z_text = ", ".join(f"{obs}={z[obs]:+.1f}" for obs in OBSERVABLES)
+        pc1 = pca["explained_variance"][0] if pca["explained_variance"] else 0.0
+        print(f"{seq_name:<10} {pc1:>8.3f} {pca['effective_rank']:>9.3f} {pca['mean_abs_pairwise_corr']:>11.3f}  {z_text}")
+
+    out_path = Path("tools/data/observable_rank_audit.json")
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+    print(f"\nSaved to {out_path}")
+    return output
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n_gaps", type=int, default=30000)
+    parser.add_argument("--n_alpha", type=int, default=19)
+    parser.add_argument("--n_trials", type=int, default=24)
+    parser.add_argument("--n_baseline", type=int, default=72)
+    parser.add_argument("--seed", type=int, default=20260505)
+    args = parser.parse_args()
+    run(
+        n_gaps=args.n_gaps,
+        n_alpha=args.n_alpha,
+        n_trials=args.n_trials,
+        n_baseline=args.n_baseline,
+        seed=args.seed,
+    )
+
+
+if __name__ == "__main__":
+    main()

diff --git a/tools/exp_observable_rank_audit.py b/tools/exp_observable_rank_audit.py
new file mode 100644
index 0000000000000000000000000000000000000000..f2acafc5960c8a35964e0ab521cf70569eefe27f
--- /dev/null
+++ b/tools/exp_observable_rank_audit.py
@@ -0,0 +1,229 @@
+#!/usr/bin/env python3
+"""
+exp_observable_rank_audit.py - META audit for observable redundancy.
+
+Recent runs found many Markov/crossover observables that react coherently under
+partial shuffle. This script asks whether those observables carry independent
+directions or mostly re-measure one latent boundary coordinate.
+
+It measures retention curves from alpha-partial shuffles, then reports:
+  - original-vs-full-shuffle z for each observable
+  - PCA energy of the retention matrix across alpha
+  - effective rank of that matrix
+  - pairwise correlations between retention curves
+
+The script measures data only. The report decides the structural claim.
+"""
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from exp_3d_boundary_layers import get_primes, gue_gaps, partial_shuffle
+
+
+def obs_spacing_ratio(gaps):
+    r = np.minimum(gaps[:-1], gaps[1:]) / np.maximum(gaps[:-1], gaps[1:])
+    r = r[np.isfinite(r)]
+    return float(np.mean(r)) if len(r) else 0.0
+
+
+def obs_lag_acf(gaps, lag):
+    g = gaps - np.mean(gaps)
+    c0 = np.mean(g * g)
+    if c0 == 0:
+        return 0.0
+    return float(np.mean(g[:-lag] * g[lag:]) / c0)
+
+
+def obs_sr2(gaps):
+    r = np.minimum(gaps[:-2], gaps[2:]) / np.maximum(gaps[:-2], gaps[2:])
+    r = r[np.isfinite(r)]
+    return float(np.mean(r)) if len(r) else 0.0
+
+
+def obs_triple_var(gaps):
+    triples = gaps[:-2] + gaps[1:-1] + gaps[2:]
+    v = np.var(gaps)
+    if v == 0:
+        return 0.0
+    return float(np.var(triples) / v)
+
+
+OBSERVABLES = {
+    "SR": obs_spacing_ratio,
+    "L1": lambda gaps: obs_lag_acf(gaps, 1),
+    "L2": lambda gaps: obs_lag_acf(gaps, 2),
+    "SR2": obs_sr2,
+    "triple_var": obs_triple_var,
+}
+
+
+def measure(gaps):
+    return {name: fn(gaps) for name, fn in OBSERVABLES.items()}
+
+
+def full_shuffle_baseline(gaps, n_trials, rng):
+    vals = {name: [] for name in OBSERVABLES}
+    for _ in range(n_trials):
+        s = rng.permutation(gaps)
+        row = measure(s)
+        for name, value in row.items():
+            vals[name].append(value)
+    return {
+        name: {
+            "mean": float(np.mean(x)),
+            "std": float(np.std(x, ddof=1)) if len(x) > 1 else 0.0,
+        }
+        for name, x in vals.items()
+    }
+
+
+def retention_curves(gaps, alphas, n_trials, originals, baseline, rng):
+    rows = []
+    for alpha in alphas:
+        vals = {name: [] for name in OBSERVABLES}
+        for _ in range(n_trials):
+            s = partial_shuffle(gaps, float(alpha), rng)
+            row = measure(s)
+            for name, value in row.items():
+                vals[name].append(value)
+
+        out = {"alpha": float(alpha)}
+        for name in OBSERVABLES:
+            mean = float(np.mean(vals[name]))
+            denom = originals[name] - baseline[name]["mean"]
+            retention = (mean - baseline[name]["mean"]) / denom if abs(denom) > 1e-12 else 0.0
+            out[name] = {
+                "mean": mean,
+                "std": float(np.std(vals[name], ddof=1)) if len(vals[name]) > 1 else 0.0,
+                "retention": float(retention),
+            }
+        rows.append(out)
+    return rows
+
+
+def pca_summary(rows):
+    names = list(OBSERVABLES)
+    matrix = np.array([[row[name]["retention"] for name in names] for row in rows], dtype=float)
+    matrix = matrix - np.mean(matrix, axis=0, keepdims=True)
+
+    _, singular, vt = np.linalg.svd(matrix, full_matrices=False)
+    energy = singular * singular
+    if np.sum(energy) <= 1e-15:
+        explained = np.zeros_like(energy)
+        effective_rank = 0.0
+    else:
+        explained = energy / np.sum(energy)
+        positive = explained[explained > 1e-15]
+        effective_rank = float(np.exp(-np.sum(positive * np.log(positive))))
+
+    corr = np.corrcoef(matrix, rowvar=False)
+    abs_corr = np.abs(corr)
+    upper = abs_corr[np.triu_indices_from(abs_corr, k=1)]
+
+    return {
+        "observables": names,
+        "singular_values": [float(x) for x in singular],
+        "explained_variance": [float(x) for x in explained],
+        "effective_rank": effective_rank,
+        "pc1_loadings": {name: float(vt[0, i]) for i, name in enumerate(names)} if len(vt) else {},
+        "mean_abs_pairwise_corr": float(np.mean(upper)) if len(upper) else 0.0,
+        "min_abs_pairwise_corr": float(np.min(upper)) if len(upper) else 0.0,
+        "max_abs_pairwise_corr": float(np.max(upper)) if len(upper) else 0.0,
+    }
+
+
+def analyze_sequence(name, gaps, alphas, n_trials, n_baseline, rng):
+    originals = measure(gaps)
+    baseline = full_shuffle_baseline(gaps, n_baseline, rng)
+    rows = retention_curves(gaps, alphas, n_trials, originals, baseline, rng)
+
+    z = {}
+    for obs_name in OBSERVABLES:
+        std = baseline[obs_name]["std"]
+        z[obs_name] = float((originals[obs_name] - baseline[obs_name]["mean"]) / std) if std > 1e-12 else 0.0
+
+    return {
+        "n_gaps": int(len(gaps)),
+        "originals": originals,
+        "full_shuffle_baseline": baseline,
+        "original_vs_shuffle_z": z,
+        "retention_curves": rows,
+        "pca": pca_summary(rows),
+    }
+
+
+def build_sequences(n_gaps, rng):
+    primes = get_primes(n_gaps * 24)[: n_gaps + 1]
+    prime_gaps = np.diff(primes).astype(float)
+
+    gue = gue_gaps(160, max(8, n_gaps // 160 + 1), rng).astype(float)
+    gue = gue[:n_gaps]
+
+    poisson = rng.exponential(1.0, size=n_gaps).astype(float)
+    return {
+        "primes": prime_gaps,
+        "gue": gue,
+        "poisson": poisson,
+    }
+
+
+def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505):
+    rng = np.random.default_rng(seed)
+    alphas = np.linspace(0.05, 0.95, n_alpha)
+    sequences = build_sequences(n_gaps, rng)
+
+    output = {
+        "experiment": "observable_rank_audit",
+        "question": "Do crossover observables contain independent directions or one latent boundary coordinate?",
+        "params": {
+            "n_gaps": int(n_gaps),
+            "n_alpha": int(n_alpha),
+            "n_trials": int(n_trials),
+            "n_baseline": int(n_baseline),
+            "seed": int(seed),
+        },
+        "sequences": {},
+    }
+
+    print(f"n_gaps={n_gaps}, n_alpha={n_alpha}, n_trials={n_trials}, n_baseline={n_baseline}, seed={seed}")
+    print(f"{'sequence':<10} {'pc1':>8} {'eff_rank':>9} {'mean|corr|':>11}  z(SR,L1,L2,SR2,triple)")
+    print("-" * 92)
+    for seq_name, gaps in sequences.items():
+        result = analyze_sequence(seq_name, gaps, alphas, n_trials, n_baseline, rng)
+        output["sequences"][seq_name] = result
+        pca = result["pca"]
+        z = result["original_vs_shuffle_z"]
+        z_text = ", ".join(f"{obs}={z[obs]:+.1f}" for obs in OBSERVABLES)
+        pc1 = pca["explained_variance"][0] if pca["explained_variance"] else 0.0
+        print(f"{seq_name:<10} {pc1:>8.3f} {pca['effective_rank']:>9.3f} {pca['mean_abs_pairwise_corr']:>11.3f}  {z_text}")
+
+    out_path = Path("tools/data/observable_rank_audit.json")
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+    print(f"\nSaved to {out_path}")
+    return output
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n_gaps", type=int, default=30000)
+    parser.add_argument("--n_alpha", type=int, default=19)
+    parser.add_argument("--n_trials", type=int, default=24)
+    parser.add_argument("--n_baseline", type=int, default=72)
+    parser.add_argument("--seed", type=int, default=20260505)
+    args = parser.parse_args()
+    run(
+        n_gaps=args.n_gaps,
+        n_alpha=args.n_alpha,
+        n_trials=args.n_trials,
+        n_baseline=args.n_baseline,
+        seed=args.seed,
+    )
+
+
+if __name__ == "__main__":
+    main()

codex
Lo strumento è pronto. Ora lo eseguo con campione moderato ma sufficiente per discriminare rango e ridondanza; se i risultati sono instabili, rifaccio con più trial prima di chiudere.
exec
/bin/bash -lc 'python tools/exp_observable_rank_audit.py --n_gaps 30000 --n_alpha 19 --n_trials 24 --n_baseline 72 --seed 20260505' in /opt/MM_D-ND
diff --git a/tools/exp_observable_rank_audit.py b/tools/exp_observable_rank_audit.py
new file mode 100644
index 0000000000000000000000000000000000000000..f2acafc5960c8a35964e0ab521cf70569eefe27f
--- /dev/null
+++ b/tools/exp_observable_rank_audit.py
@@ -0,0 +1,229 @@
+#!/usr/bin/env python3
+"""
+exp_observable_rank_audit.py - META audit for observable redundancy.
+
+Recent runs found many Markov/crossover observables that react coherently under
+partial shuffle. This script asks whether those observables carry independent
+directions or mostly re-measure one latent boundary coordinate.
+
+It measures retention curves from alpha-partial shuffles, then reports:
+  - original-vs-full-shuffle z for each observable
+  - PCA energy of the retention matrix across alpha
+  - effective rank of that matrix
+  - pairwise correlations between retention curves
+
+The script measures data only. The report decides the structural claim.
+"""
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from exp_3d_boundary_layers import get_primes, gue_gaps, partial_shuffle
+
+
+def obs_spacing_ratio(gaps):
+    r = np.minimum(gaps[:-1], gaps[1:]) / np.maximum(gaps[:-1], gaps[1:])
+    r = r[np.isfinite(r)]
+    return float(np.mean(r)) if len(r) else 0.0
+
+
+def obs_lag_acf(gaps, lag):
+    g = gaps - np.mean(gaps)
+    c0 = np.mean(g * g)
+    if c0 == 0:
+        return 0.0
+    return float(np.mean(g[:-lag] * g[lag:]) / c0)
+
+
+def obs_sr2(gaps):
+    r = np.minimum(gaps[:-2], gaps[2:]) / np.maximum(gaps[:-2], gaps[2:])
+    r = r[np.isfinite(r)]
+    return float(np.mean(r)) if len(r) else 0.0
+
+
+def obs_triple_var(gaps):
+    triples = gaps[:-2] + gaps[1:-1] + gaps[2:]
+    v = np.var(gaps)
+    if v == 0:
+        return 0.0
+    return float(np.var(triples) / v)
+
+
+OBSERVABLES = {
+    "SR": obs_spacing_ratio,
+    "L1": lambda gaps: obs_lag_acf(gaps, 1),
+    "L2": lambda gaps: obs_lag_acf(gaps, 2),
+    "SR2": obs_sr2,
+    "triple_var": obs_triple_var,
+}
+
+
+def measure(gaps):
+    return {name: fn(gaps) for name, fn in OBSERVABLES.items()}
+
+
+def full_shuffle_baseline(gaps, n_trials, rng):
+    vals = {name: [] for name in OBSERVABLES}
+    for _ in range(n_trials):
+        s = rng.permutation(gaps)
+        row = measure(s)
+        for name, value in row.items():
+            vals[name].append(value)
+    return {
+        name: {
+            "mean": float(np.mean(x)),
+            "std": float(np.std(x, ddof=1)) if len(x) > 1 else 0.0,
+        }
+        for name, x in vals.items()
+    }
+
+
+def retention_curves(gaps, alphas, n_trials, originals, baseline, rng):
+    rows = []
+    for alpha in alphas:
+        vals = {name: [] for name in OBSERVABLES}
+        for _ in range(n_trials):
+            s = partial_shuffle(gaps, float(alpha), rng)
+            row = measure(s)
+            for name, value in row.items():
+                vals[name].append(value)
+
+        out = {"alpha": float(alpha)}
+        for name in OBSERVABLES:
+            mean = float(np.mean(vals[name]))
+            denom = originals[name] - baseline[name]["mean"]
+            retention = (mean - baseline[name]["mean"]) / denom if abs(denom) > 1e-12 else 0.0
+            out[name] = {
+                "mean": mean,
+                "std": float(np.std(vals[name], ddof=1)) if len(vals[name]) > 1 else 0.0,
+                "retention": float(retention),
+            }
+        rows.append(out)
+    return rows
+
+
+def pca_summary(rows):
+    names = list(OBSERVABLES)
+    matrix = np.array([[row[name]["retention"] for name in names] for row in rows], dtype=float)
+    matrix = matrix - np.mean(matrix, axis=0, keepdims=True)
+
+    _, singular, vt = np.linalg.svd(matrix, full_matrices=False)
+    energy = singular * singular
+    if np.sum(energy) <= 1e-15:
+        explained = np.zeros_like(energy)
+        effective_rank = 0.0
+    else:
+        explained = energy / np.sum(energy)
+        positive = explained[explained > 1e-15]
+        effective_rank = float(np.exp(-np.sum(positive * np.log(positive))))
+
+    corr = np.corrcoef(matrix, rowvar=False)
+    abs_corr = np.abs(corr)
+    upper = abs_corr[np.triu_indices_from(abs_corr, k=1)]
+
+    return {
+        "observables": names,
+        "singular_values": [float(x) for x in singular],
+        "explained_variance": [float(x) for x in explained],
+        "effective_rank": effective_rank,
+        "pc1_loadings": {name: float(vt[0, i]) for i, name in enumerate(names)} if len(vt) else {},
+        "mean_abs_pairwise_corr": float(np.mean(upper)) if len(upper) else 0.0,
+        "min_abs_pairwise_corr": float(np.min(upper)) if len(upper) else 0.0,
+        "max_abs_pairwise_corr": float(np.max(upper)) if len(upper) else 0.0,
+    }
+
+
+def analyze_sequence(name, gaps, alphas, n_trials, n_baseline, rng):
+    originals = measure(gaps)
+    baseline = full_shuffle_baseline(gaps, n_baseline, rng)
+    rows = retention_curves(gaps, alphas, n_trials, originals, baseline, rng)
+
+    z = {}
+    for obs_name in OBSERVABLES:
+        std = baseline[obs_name]["std"]
+        z[obs_name] = float((originals[obs_name] - baseline[obs_name]["mean"]) / std) if std > 1e-12 else 0.0
+
+    return {
+        "n_gaps": int(len(gaps)),
+        "originals": originals,
+        "full_shuffle_baseline": baseline,
+        "original_vs_shuffle_z": z,
+        "retention_curves": rows,
+        "pca": pca_summary(rows),
+    }
+
+
+def build_sequences(n_gaps, rng):
+    primes = get_primes(n_gaps * 24)[: n_gaps + 1]
+    prime_gaps = np.diff(primes).astype(float)
+
+    gue = gue_gaps(160, max(8, n_gaps // 160 + 1), rng).astype(float)
+    gue = gue[:n_gaps]
+
+    poisson = rng.exponential(1.0, size=n_gaps).astype(float)
+    return {
+        "primes": prime_gaps,
+        "gue": gue,
+        "poisson": poisson,
+    }
+
+
+def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505):
+    rng = np.random.default_rng(seed)
+    alphas = np.linspace(0.05, 0.95, n_alpha)
+    sequences = build_sequences(n_gaps, rng)
+
+    output = {
+        "experiment": "observable_rank_audit",
+        "question": "Do crossover observables contain independent directions or one latent boundary coordinate?",
+        "params": {
+            "n_gaps": int(n_gaps),
+            "n_alpha": int(n_alpha),
+            "n_trials": int(n_trials),
+            "n_baseline": int(n_baseline),
+            "seed": int(seed),
+        },
+        "sequences": {},
+    }
+
+    print(f"n_gaps={n_gaps}, n_alpha={n_alpha}, n_trials={n_trials}, n_baseline={n_baseline}, seed={seed}")
+    print(f"{'sequence':<10} {'pc1':>8} {'eff_rank':>9} {'mean|corr|':>11}  z(SR,L1,L2,SR2,triple)")
+    print("-" * 92)
+    for seq_name, gaps in sequences.items():
+        result = analyze_sequence(seq_name, gaps, alphas, n_trials, n_baseline, rng)
+        output["sequences"][seq_name] = result
+        pca = result["pca"]
+        z = result["original_vs_shuffle_z"]
+        z_text = ", ".join(f"{obs}={z[obs]:+.1f}" for obs in OBSERVABLES)
+        pc1 = pca["explained_variance"][0] if pca["explained_variance"] else 0.0
+        print(f"{seq_name:<10} {pc1:>8.3f} {pca['effective_rank']:>9.3f} {pca['mean_abs_pairwise_corr']:>11.3f}  {z_text}")
+
+    out_path = Path("tools/data/observable_rank_audit.json")
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+    print(f"\nSaved to {out_path}")
+    return output
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n_gaps", type=int, default=30000)
+    parser.add_argument("--n_alpha", type=int, default=19)
+    parser.add_argument("--n_trials", type=int, default=24)
+    parser.add_argument("--n_baseline", type=int, default=72)
+    parser.add_argument("--seed", type=int, default=20260505)
+    args = parser.parse_args()
+    run(
+        n_gaps=args.n_gaps,
+        n_alpha=args.n_alpha,
+        n_trials=args.n_trials,
+        n_baseline=args.n_baseline,
+        seed=args.seed,
+    )
+
+
+if __name__ == "__main__":
+    main()

 succeeded in 2829ms:
n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505
sequence        pc1  eff_rank  mean|corr|  z(SR,L1,L2,SR2,triple)
--------------------------------------------------------------------------------------------
primes        0.989     1.069       0.987  SR=-19.3, L1=-7.7, L2=-3.0, SR2=-3.7, triple_var=-8.0
gue           0.997     1.022       0.996  SR=-6.6, L1=+21.4, L2=+37.9, SR2=+18.0, triple_var=+36.3
poisson       0.925     1.391       0.351  SR=-0.6, L1=+0.2, L2=+1.2, SR2=-0.0, triple_var=+0.8

Saved to tools/data/observable_rank_audit.json

diff --git a/tools/exp_observable_rank_audit.py b/tools/exp_observable_rank_audit.py
new file mode 100644
index 0000000000000000000000000000000000000000..f2acafc5960c8a35964e0ab521cf70569eefe27f
--- /dev/null
+++ b/tools/exp_observable_rank_audit.py
@@ -0,0 +1,229 @@
+#!/usr/bin/env python3
+"""
+exp_observable_rank_audit.py - META audit for observable redundancy.
+
+Recent runs found many Markov/crossover observables that react coherently under
+partial shuffle. This script asks whether those observables carry independent
+directions or mostly re-measure one latent boundary coordinate.
+
+It measures retention curves from alpha-partial shuffles, then reports:
+  - original-vs-full-shuffle z for each observable
+  - PCA energy of the retention matrix across alpha
+  - effective rank of that matrix
+  - pairwise correlations between retention curves
+
+The script measures data only. The report decides the structural claim.
+"""
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from exp_3d_boundary_layers import get_primes, gue_gaps, partial_shuffle
+
+
+def obs_spacing_ratio(gaps):
+    r = np.minimum(gaps[:-1], gaps[1:]) / np.maximum(gaps[:-1], gaps[1:])
+    r = r[np.isfinite(r)]
+    return float(np.mean(r)) if len(r) else 0.0
+
+
+def obs_lag_acf(gaps, lag):
+    g = gaps - np.mean(gaps)
+    c0 = np.mean(g * g)
+    if c0 == 0:
+        return 0.0
+    return float(np.mean(g[:-lag] * g[lag:]) / c0)
+
+
+def obs_sr2(gaps):
+    r = np.minimum(gaps[:-2], gaps[2:]) / np.maximum(gaps[:-2], gaps[2:])
+    r = r[np.isfinite(r)]
+    return float(np.mean(r)) if len(r) else 0.0
+
+
+def obs_triple_var(gaps):
+    triples = gaps[:-2] + gaps[1:-1] + gaps[2:]
+    v = np.var(gaps)
+    if v == 0:
+        return 0.0
+    return float(np.var(triples) / v)
+
+
+OBSERVABLES = {
+    "SR": obs_spacing_ratio,
+    "L1": lambda gaps: obs_lag_acf(gaps, 1),
+    "L2": lambda gaps: obs_lag_acf(gaps, 2),
+    "SR2": obs_sr2,
+    "triple_var": obs_triple_var,
+}
+
+
+def measure(gaps):
+    return {name: fn(gaps) for name, fn in OBSERVABLES.items()}
+
+
+def full_shuffle_baseline(gaps, n_trials, rng):
+    vals = {name: [] for name in OBSERVABLES}
+    for _ in range(n_trials):
+        s = rng.permutation(gaps)
+        row = measure(s)
+        for name, value in row.items():
+            vals[name].append(value)
+    return {
+        name: {
+            "mean": float(np.mean(x)),
+            "std": float(np.std(x, ddof=1)) if len(x) > 1 else 0.0,
+        }
+        for name, x in vals.items()
+    }
+
+
+def retention_curves(gaps, alphas, n_trials, originals, baseline, rng):
+    rows = []
+    for alpha in alphas:
+        vals = {name: [] for name in OBSERVABLES}
+        for _ in range(n_trials):
+            s = partial_shuffle(gaps, float(alpha), rng)
+            row = measure(s)
+            for name, value in row.items():
+                vals[name].append(value)
+
+        out = {"alpha": float(alpha)}
+        for name in OBSERVABLES:
+            mean = float(np.mean(vals[name]))
+            denom = originals[name] - baseline[name]["mean"]
+            retention = (mean - baseline[name]["mean"]) / denom if abs(denom) > 1e-12 else 0.0
+            out[name] = {
+                "mean": mean,
+                "std": float(np.std(vals[name], ddof=1)) if len(vals[name]) > 1 else 0.0,
+                "retention": float(retention),
+            }
+        rows.append(out)
+    return rows
+
+
+def pca_summary(rows):
+    names = list(OBSERVABLES)
+    matrix = np.array([[row[name]["retention"] for name in names] for row in rows], dtype=float)
+    matrix = matrix - np.mean(matrix, axis=0, keepdims=True)
+
+    _, singular, vt = np.linalg.svd(matrix, full_matrices=False)
+    energy = singular * singular
+    if np.sum(energy) <= 1e-15:
+        explained = np.zeros_like(energy)
+        effective_rank = 0.0
+    else:
+        explained = energy / np.sum(energy)
+        positive = explained[explained > 1e-15]
+        effective_rank = float(np.exp(-np.sum(positive * np.log(positive))))
+
+    corr = np.corrcoef(matrix, rowvar=False)
+    abs_corr = np.abs(corr)
+    upper = abs_corr[np.triu_indices_from(abs_corr, k=1)]
+
+    return {
+        "observables": names,
+        "singular_values": [float(x) for x in singular],
+        "explained_variance": [float(x) for x in explained],
+        "effective_rank": effective_rank,
+        "pc1_loadings": {name: float(vt[0, i]) for i, name in enumerate(names)} if len(vt) else {},
+        "mean_abs_pairwise_corr": float(np.mean(upper)) if len(upper) else 0.0,
+        "min_abs_pairwise_corr": float(np.min(upper)) if len(upper) else 0.0,
+        "max_abs_pairwise_corr": float(np.max(upper)) if len(upper) else 0.0,
+    }
+
+
+def analyze_sequence(name, gaps, alphas, n_trials, n_baseline, rng):
+    originals = measure(gaps)
+    baseline = full_shuffle_baseline(gaps, n_baseline, rng)
+    rows = retention_curves(gaps, alphas, n_trials, originals, baseline, rng)
+
+    z = {}
+    for obs_name in OBSERVABLES:
+        std = baseline[obs_name]["std"]
+        z[obs_name] = float((originals[obs_name] - baseline[obs_name]["mean"]) / std) if std > 1e-12 else 0.0
+
+    return {
+        "n_gaps": int(len(gaps)),
+        "originals": originals,
+        "full_shuffle_baseline": baseline,
+        "original_vs_shuffle_z": z,
+        "retention_curves": rows,
+        "pca": pca_summary(rows),
+    }
+
+
+def build_sequences(n_gaps, rng):
+    primes = get_primes(n_gaps * 24)[: n_gaps + 1]
+    prime_gaps = np.diff(primes).astype(float)
+
+    gue = gue_gaps(160, max(8, n_gaps // 160 + 1), rng).astype(float)
+    gue = gue[:n_gaps]
+
+    poisson = rng.exponential(1.0, size=n_gaps).astype(float)
+    return {
+        "primes": prime_gaps,
+        "gue": gue,
+        "poisson": poisson,
+    }
+
+
+def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505):
+    rng = np.random.default_rng(seed)
+    alphas = np.linspace(0.05, 0.95, n_alpha)
+    sequences = build_sequences(n_gaps, rng)
+
+    output = {
+        "experiment": "observable_rank_audit",
+        "question": "Do crossover observables contain independent directions or one latent boundary coordinate?",
+        "params": {
+            "n_gaps": int(n_gaps),
+            "n_alpha": int(n_alpha),
+            "n_trials": int(n_trials),
+            "n_baseline": int(n_baseline),
+            "seed": int(seed),
+        },
+        "sequences": {},
+    }
+
+    print(f"n_gaps={n_gaps}, n_alpha={n_alpha}, n_trials={n_trials}, n_baseline={n_baseline}, seed={seed}")
+    print(f"{'sequence':<10} {'pc1':>8} {'eff_rank':>9} {'mean|corr|':>11}  z(SR,L1,L2,SR2,triple)")
+    print("-" * 92)
+    for seq_name, gaps in sequences.items():
+        result = analyze_sequence(seq_name, gaps, alphas, n_trials, n_baseline, rng)
+        output["sequences"][seq_name] = result
+        pca = result["pca"]
+        z = result["original_vs_shuffle_z"]
+        z_text = ", ".join(f"{obs}={z[obs]:+.1f}" for obs in OBSERVABLES)
+        pc1 = pca["explained_variance"][0] if pca["explained_variance"] else 0.0
+        print(f"{seq_name:<10} {pc1:>8.3f} {pca['effective_rank']:>9.3f} {pca['mean_abs_pairwise_corr']:>11.3f}  {z_text}")
+
+    out_path = Path("tools/data/observable_rank_audit.json")
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+    print(f"\nSaved to {out_path}")
+    return output
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n_gaps", type=int, default=30000)
+    parser.add_argument("--n_alpha", type=int, default=19)
+    parser.add_argument("--n_trials", type=int, default=24)
+    parser.add_argument("--n_baseline", type=int, default=72)
+    parser.add_argument("--seed", type=int, default=20260505)
+    args = parser.parse_args()
+    run(
+        n_gaps=args.n_gaps,
+        n_alpha=args.n_alpha,
+        n_trials=args.n_trials,
+        n_baseline=args.n_baseline,
+        seed=args.seed,
+    )
+
+
+if __name__ == "__main__":
+    main()

codex
Il primo passaggio è netto: primes e GUE hanno segnale forte ma rango effettivo circa 1, mentre Poisson non ha segnale originale-vs-shuffle e il rango è rumore. Faccio un secondo seed per vedere se la conclusione dipende dalla realizzazione GUE/partial-shuffle.
exec
/bin/bash -lc 'python tools/exp_observable_rank_audit.py --n_gaps 30000 --n_alpha 19 --n_trials 24 --n_baseline 72 --seed 20260506' in /opt/MM_D-ND
diff --git a/tools/exp_observable_rank_audit.py b/tools/exp_observable_rank_audit.py
new file mode 100644
index 0000000000000000000000000000000000000000..f2acafc5960c8a35964e0ab521cf70569eefe27f
--- /dev/null
+++ b/tools/exp_observable_rank_audit.py
@@ -0,0 +1,229 @@
+#!/usr/bin/env python3
+"""
+exp_observable_rank_audit.py - META audit for observable redundancy.
+
+Recent runs found many Markov/crossover observables that react coherently under
+partial shuffle. This script asks whether those observables carry independent
+directions or mostly re-measure one latent boundary coordinate.
+
+It measures retention curves from alpha-partial shuffles, then reports:
+  - original-vs-full-shuffle z for each observable
+  - PCA energy of the retention matrix across alpha
+  - effective rank of that matrix
+  - pairwise correlations between retention curves
+
+The script measures data only. The report decides the structural claim.
+"""
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from exp_3d_boundary_layers import get_primes, gue_gaps, partial_shuffle
+
+
+def obs_spacing_ratio(gaps):
+    r = np.minimum(gaps[:-1], gaps[1:]) / np.maximum(gaps[:-1], gaps[1:])
+    r = r[np.isfinite(r)]
+    return float(np.mean(r)) if len(r) else 0.0
+
+
+def obs_lag_acf(gaps, lag):
+    g = gaps - np.mean(gaps)
+    c0 = np.mean(g * g)
+    if c0 == 0:
+        return 0.0
+    return float(np.mean(g[:-lag] * g[lag:]) / c0)
+
+
+def obs_sr2(gaps):
+    r = np.minimum(gaps[:-2], gaps[2:]) / np.maximum(gaps[:-2], gaps[2:])
+    r = r[np.isfinite(r)]
+    return float(np.mean(r)) if len(r) else 0.0
+
+
+def obs_triple_var(gaps):
+    triples = gaps[:-2] + gaps[1:-1] + gaps[2:]
+    v = np.var(gaps)
+    if v == 0:
+        return 0.0
+    return float(np.var(triples) / v)
+
+
+OBSERVABLES = {
+    "SR": obs_spacing_ratio,
+    "L1": lambda gaps: obs_lag_acf(gaps, 1),
+    "L2": lambda gaps: obs_lag_acf(gaps, 2),
+    "SR2": obs_sr2,
+    "triple_var": obs_triple_var,
+}
+
+
+def measure(gaps):
+    return {name: fn(gaps) for name, fn in OBSERVABLES.items()}
+
+
+def full_shuffle_baseline(gaps, n_trials, rng):
+    vals = {name: [] for name in OBSERVABLES}
+    for _ in range(n_trials):
+        s = rng.permutation(gaps)
+        row = measure(s)
+        for name, value in row.items():
+            vals[name].append(value)
+    return {
+        name: {
+            "mean": float(np.mean(x)),
+            "std": float(np.std(x, ddof=1)) if len(x) > 1 else 0.0,
+        }
+        for name, x in vals.items()
+    }
+
+
+def retention_curves(gaps, alphas, n_trials, originals, baseline, rng):
+    rows = []
+    for alpha in alphas:
+        vals = {name: [] for name in OBSERVABLES}
+        for _ in range(n_trials):
+            s = partial_shuffle(gaps, float(alpha), rng)
+            row = measure(s)
+            for name, value in row.items():
+                vals[name].append(value)
+
+        out = {"alpha": float(alpha)}
+        for name in OBSERVABLES:
+            mean = float(np.mean(vals[name]))
+            denom = originals[name] - baseline[name]["mean"]
+            retention = (mean - baseline[name]["mean"]) / denom if abs(denom) > 1e-12 else 0.0
+            out[name] = {
+                "mean": mean,
+                "std": float(np.std(vals[name], ddof=1)) if len(vals[name]) > 1 else 0.0,
+                "retention": float(retention),
+            }
+        rows.append(out)
+    return rows
+
+
+def pca_summary(rows):
+    names = list(OBSERVABLES)
+    matrix = np.array([[row[name]["retention"] for name in names] for row in rows], dtype=float)
+    matrix = matrix - np.mean(matrix, axis=0, keepdims=True)
+
+    _, singular, vt = np.linalg.svd(matrix, full_matrices=False)
+    energy = singular * singular
+    if np.sum(energy) <= 1e-15:
+        explained = np.zeros_like(energy)
+        effective_rank = 0.0
+    else:
+        explained = energy / np.sum(energy)
+        positive = explained[explained > 1e-15]
+        effective_rank = float(np.exp(-np.sum(positive * np.log(positive))))
+
+    corr = np.corrcoef(matrix, rowvar=False)
+    abs_corr = np.abs(corr)
+    upper = abs_corr[np.triu_indices_from(abs_corr, k=1)]
+
+    return {
+        "observables": names,
+        "singular_values": [float(x) for x in singular],
+        "explained_variance": [float(x) for x in explained],
+        "effective_rank": effective_rank,
+        "pc1_loadings": {name: float(vt[0, i]) for i, name in enumerate(names)} if len(vt) else {},
+        "mean_abs_pairwise_corr": float(np.mean(upper)) if len(upper) else 0.0,
+        "min_abs_pairwise_corr": float(np.min(upper)) if len(upper) else 0.0,
+        "max_abs_pairwise_corr": float(np.max(upper)) if len(upper) else 0.0,
+    }
+
+
+def analyze_sequence(name, gaps, alphas, n_trials, n_baseline, rng):
+    originals = measure(gaps)
+    baseline = full_shuffle_baseline(gaps, n_baseline, rng)
+    rows = retention_curves(gaps, alphas, n_trials, originals, baseline, rng)
+
+    z = {}
+    for obs_name in OBSERVABLES:
+        std = baseline[obs_name]["std"]
+        z[obs_name] = float((originals[obs_name] - baseline[obs_name]["mean"]) / std) if std > 1e-12 else 0.0
+
+    return {
+        "n_gaps": int(len(gaps)),
+        "originals": originals,
+        "full_shuffle_baseline": baseline,
+        "original_vs_shuffle_z": z,
+        "retention_curves": rows,
+        "pca": pca_summary(rows),
+    }
+
+
+def build_sequences(n_gaps, rng):
+    primes = get_primes(n_gaps * 24)[: n_gaps + 1]
+    prime_gaps = np.diff(primes).astype(float)
+
+    gue = gue_gaps(160, max(8, n_gaps // 160 + 1), rng).astype(float)
+    gue = gue[:n_gaps]
+
+    poisson = rng.exponential(1.0, size=n_gaps).astype(float)
+    return {
+        "primes": prime_gaps,
+        "gue": gue,
+        "poisson": poisson,
+    }
+
+
+def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505):
+    rng = np.random.default_rng(seed)
+    alphas = np.linspace(0.05, 0.95, n_alpha)
+    sequences = build_sequences(n_gaps, rng)
+
+    output = {
+        "experiment": "observable_rank_audit",
+        "question": "Do crossover observables contain independent directions or one latent boundary coordinate?",
+        "params": {
+            "n_gaps": int(n_gaps),
+            "n_alpha": int(n_alpha),
+            "n_trials": int(n_trials),
+            "n_baseline": int(n_baseline),
+            "seed": int(seed),
+        },
+        "sequences": {},
+    }
+
+    print(f"n_gaps={n_gaps}, n_alpha={n_alpha}, n_trials={n_trials}, n_baseline={n_baseline}, seed={seed}")
+    print(f"{'sequence':<10} {'pc1':>8} {'eff_rank':>9} {'mean|corr|':>11}  z(SR,L1,L2,SR2,triple)")
+    print("-" * 92)
+    for seq_name, gaps in sequences.items():
+        result = analyze_sequence(seq_name, gaps, alphas, n_trials, n_baseline, rng)
+        output["sequences"][seq_name] = result
+        pca = result["pca"]
+        z = result["original_vs_shuffle_z"]
+        z_text = ", ".join(f"{obs}={z[obs]:+.1f}" for obs in OBSERVABLES)
+        pc1 = pca["explained_variance"][0] if pca["explained_variance"] else 0.0
+        print(f"{seq_name:<10} {pc1:>8.3f} {pca['effective_rank']:>9.3f} {pca['mean_abs_pairwise_corr']:>11.3f}  {z_text}")
+
+    out_path = Path("tools/data/observable_rank_audit.json")
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+    print(f"\nSaved to {out_path}")
+    return output
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n_gaps", type=int, default=30000)
+    parser.add_argument("--n_alpha", type=int, default=19)
+    parser.add_argument("--n_trials", type=int, default=24)
+    parser.add_argument("--n_baseline", type=int, default=72)
+    parser.add_argument("--seed", type=int, default=20260505)
+    args = parser.parse_args()
+    run(
+        n_gaps=args.n_gaps,
+        n_alpha=args.n_alpha,
+        n_trials=args.n_trials,
+        n_baseline=args.n_baseline,
+        seed=args.seed,
+    )
+
+
+if __name__ == "__main__":
+    main()

 succeeded in 2426ms:
n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260506
sequence        pc1  eff_rank  mean|corr|  z(SR,L1,L2,SR2,triple)
--------------------------------------------------------------------------------------------
primes        0.988     1.076       0.985  SR=-15.9, L1=-7.3, L2=-2.8, SR2=-3.9, triple_var=-7.6
gue           0.997     1.019       0.997  SR=-6.2, L1=+20.7, L2=+41.8, SR2=+19.5, triple_var=+36.0
poisson       0.675     2.394       0.640  SR=-0.3, L1=-0.4, L2=-0.8, SR2=-2.3, triple_var=-0.7

Saved to tools/data/observable_rank_audit.json

diff --git a/tools/exp_observable_rank_audit.py b/tools/exp_observable_rank_audit.py
new file mode 100644
index 0000000000000000000000000000000000000000..f2acafc5960c8a35964e0ab521cf70569eefe27f
--- /dev/null
+++ b/tools/exp_observable_rank_audit.py
@@ -0,0 +1,229 @@
+#!/usr/bin/env python3
+"""
+exp_observable_rank_audit.py - META audit for observable redundancy.
+
+Recent runs found many Markov/crossover observables that react coherently under
+partial shuffle. This script asks whether those observables carry independent
+directions or mostly re-measure one latent boundary coordinate.
+
+It measures retention curves from alpha-partial shuffles, then reports:
+  - original-vs-full-shuffle z for each observable
+  - PCA energy of the retention matrix across alpha
+  - effective rank of that matrix
+  - pairwise correlations between retention curves
+
+The script measures data only. The report decides the structural claim.
+"""
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from exp_3d_boundary_layers import get_primes, gue_gaps, partial_shuffle
+
+
+def obs_spacing_ratio(gaps):
+    r = np.minimum(gaps[:-1], gaps[1:]) / np.maximum(gaps[:-1], gaps[1:])
+    r = r[np.isfinite(r)]
+    return float(np.mean(r)) if len(r) else 0.0
+
+
+def obs_lag_acf(gaps, lag):
+    g = gaps - np.mean(gaps)
+    c0 = np.mean(g * g)
+    if c0 == 0:
+        return 0.0
+    return float(np.mean(g[:-lag] * g[lag:]) / c0)
+
+
+def obs_sr2(gaps):
+    r = np.minimum(gaps[:-2], gaps[2:]) / np.maximum(gaps[:-2], gaps[2:])
+    r = r[np.isfinite(r)]
+    return float(np.mean(r)) if len(r) else 0.0
+
+
+def obs_triple_var(gaps):
+    triples = gaps[:-2] + gaps[1:-1] + gaps[2:]
+    v = np.var(gaps)
+    if v == 0:
+        return 0.0
+    return float(np.var(triples) / v)
+
+
+OBSERVABLES = {
+    "SR": obs_spacing_ratio,
+    "L1": lambda gaps: obs_lag_acf(gaps, 1),
+    "L2": lambda gaps: obs_lag_acf(gaps, 2),
+    "SR2": obs_sr2,
+    "triple_var": obs_triple_var,
+}
+
+
+def measure(gaps):
+    return {name: fn(gaps) for name, fn in OBSERVABLES.items()}
+
+
+def full_shuffle_baseline(gaps, n_trials, rng):
+    vals = {name: [] for name in OBSERVABLES}
+    for _ in range(n_trials):
+        s = rng.permutation(gaps)
+        row = measure(s)
+        for name, value in row.items():
+            vals[name].append(value)
+    return {
+        name: {
+            "mean": float(np.mean(x)),
+            "std": float(np.std(x, ddof=1)) if len(x) > 1 else 0.0,
+        }
+        for name, x in vals.items()
+    }
+
+
+def retention_curves(gaps, alphas, n_trials, originals, baseline, rng):
+    rows = []
+    for alpha in alphas:
+        vals = {name: [] for name in OBSERVABLES}
+        for _ in range(n_trials):
+            s = partial_shuffle(gaps, float(alpha), rng)
+            row = measure(s)
+            for name, value in row.items():
+                vals[name].append(value)
+
+        out = {"alpha": float(alpha)}
+        for name in OBSERVABLES:
+            mean = float(np.mean(vals[name]))
+            denom = originals[name] - baseline[name]["mean"]
+            retention = (mean - baseline[name]["mean"]) / denom if abs(denom) > 1e-12 else 0.0
+            out[name] = {
+                "mean": mean,
+                "std": float(np.std(vals[name], ddof=1)) if len(vals[name]) > 1 else 0.0,
+                "retention": float(retention),
+            }
+        rows.append(out)
+    return rows
+
+
+def pca_summary(rows):
+    names = list(OBSERVABLES)
+    matrix = np.array([[row[name]["retention"] for name in names] for row in rows], dtype=float)
+    matrix = matrix - np.mean(matrix, axis=0, keepdims=True)
+
+    _, singular, vt = np.linalg.svd(matrix, full_matrices=False)
+    energy = singular * singular
+    if np.sum(energy) <= 1e-15:
+        explained = np.zeros_like(energy)
+        effective_rank = 0.0
+    else:
+        explained = energy / np.sum(energy)
+        positive = explained[explained > 1e-15]
+        effective_rank = float(np.exp(-np.sum(positive * np.log(positive))))
+
+    corr = np.corrcoef(matrix, rowvar=False)
+    abs_corr = np.abs(corr)
+    upper = abs_corr[np.triu_indices_from(abs_corr, k=1)]
+
+    return {
+        "observables": names,
+        "singular_values": [float(x) for x in singular],
+        "explained_variance": [float(x) for x in explained],
+        "effective_rank": effective_rank,
+        "pc1_loadings": {name: float(vt[0, i]) for i, name in enumerate(names)} if len(vt) else {},
+        "mean_abs_pairwise_corr": float(np.mean(upper)) if len(upper) else 0.0,
+        "min_abs_pairwise_corr": float(np.min(upper)) if len(upper) else 0.0,
+        "max_abs_pairwise_corr": float(np.max(upper)) if len(upper) else 0.0,
+    }
+
+
+def analyze_sequence(name, gaps, alphas, n_trials, n_baseline, rng):
+    originals = measure(gaps)
+    baseline = full_shuffle_baseline(gaps, n_baseline, rng)
+    rows = retention_curves(gaps, alphas, n_trials, originals, baseline, rng)
+
+    z = {}
+    for obs_name in OBSERVABLES:
+        std = baseline[obs_name]["std"]
+        z[obs_name] = float((originals[obs_name] - baseline[obs_name]["mean"]) / std) if std > 1e-12 else 0.0
+
+    return {
+        "n_gaps": int(len(gaps)),
+        "originals": originals,
+        "full_shuffle_baseline": baseline,
+        "original_vs_shuffle_z": z,
+        "retention_curves": rows,
+        "pca": pca_summary(rows),
+    }
+
+
+def build_sequences(n_gaps, rng):
+    primes = get_primes(n_gaps * 24)[: n_gaps + 1]
+    prime_gaps = np.diff(primes).astype(float)
+
+    gue = gue_gaps(160, max(8, n_gaps // 160 + 1), rng).astype(float)
+    gue = gue[:n_gaps]
+
+    poisson = rng.exponential(1.0, size=n_gaps).astype(float)
+    return {
+        "primes": prime_gaps,
+        "gue": gue,
+        "poisson": poisson,
+    }
+
+
+def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505):
+    rng = np.random.default_rng(seed)
+    alphas = np.linspace(0.05, 0.95, n_alpha)
+    sequences = build_sequences(n_gaps, rng)
+
+    output = {
+        "experiment": "observable_rank_audit",
+        "question": "Do crossover observables contain independent directions or one latent boundary coordinate?",
+        "params": {
+            "n_gaps": int(n_gaps),
+            "n_alpha": int(n_alpha),
+            "n_trials": int(n_trials),
+            "n_baseline": int(n_baseline),
+            "seed": int(seed),
+        },
+        "sequences": {},
+    }
+
+    print(f"n_gaps={n_gaps}, n_alpha={n_alpha}, n_trials={n_trials}, n_baseline={n_baseline}, seed={seed}")
+    print(f"{'sequence':<10} {'pc1':>8} {'eff_rank':>9} {'mean|corr|':>11}  z(SR,L1,L2,SR2,triple)")
+    print("-" * 92)
+    for seq_name, gaps in sequences.items():
+        result = analyze_sequence(seq_name, gaps, alphas, n_trials, n_baseline, rng)
+        output["sequences"][seq_name] = result
+        pca = result["pca"]
+        z = result["original_vs_shuffle_z"]
+        z_text = ", ".join(f"{obs}={z[obs]:+.1f}" for obs in OBSERVABLES)
+        pc1 = pca["explained_variance"][0] if pca["explained_variance"] else 0.0
+        print(f"{seq_name:<10} {pc1:>8.3f} {pca['effective_rank']:>9.3f} {pca['mean_abs_pairwise_corr']:>11.3f}  {z_text}")
+
+    out_path = Path("tools/data/observable_rank_audit.json")
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+    print(f"\nSaved to {out_path}")
+    return output
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n_gaps", type=int, default=30000)
+    parser.add_argument("--n_alpha", type=int, default=19)
+    parser.add_argument("--n_trials", type=int, default=24)
+    parser.add_argument("--n_baseline", type=int, default=72)
+    parser.add_argument("--seed", type=int, default=20260505)
+    args = parser.parse_args()
+    run(
+        n_gaps=args.n_gaps,
+        n_alpha=args.n_alpha,
+        n_trials=args.n_trials,
+        n_baseline=args.n_baseline,
+        seed=args.seed,
+    )
+
+
+if __name__ == "__main__":
+    main()

exec
/bin/bash -lc 'cp tools/data/observable_rank_audit.json tools/data/observable_rank_audit_seed20260506.json' in /opt/MM_D-ND
 succeeded in 0ms:
diff --git a/tools/exp_observable_rank_audit.py b/tools/exp_observable_rank_audit.py
new file mode 100644
index 0000000000000000000000000000000000000000..f2acafc5960c8a35964e0ab521cf70569eefe27f
--- /dev/null
+++ b/tools/exp_observable_rank_audit.py
@@ -0,0 +1,229 @@
+#!/usr/bin/env python3
+"""
+exp_observable_rank_audit.py - META audit for observable redundancy.
+
+Recent runs found many Markov/crossover observables that react coherently under
+partial shuffle. This script asks whether those observables carry independent
+directions or mostly re-measure one latent boundary coordinate.
+
+It measures retention curves from alpha-partial shuffles, then reports:
+  - original-vs-full-shuffle z for each observable
+  - PCA energy of the retention matrix across alpha
+  - effective rank of that matrix
+  - pairwise correlations between retention curves
+
+The script measures data only. The report decides the structural claim.
+"""
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from exp_3d_boundary_layers import get_primes, gue_gaps, partial_shuffle
+
+
+def obs_spacing_ratio(gaps):
+    r = np.minimum(gaps[:-1], gaps[1:]) / np.maximum(gaps[:-1], gaps[1:])
+    r = r[np.isfinite(r)]
+    return float(np.mean(r)) if len(r) else 0.0
+
+
+def obs_lag_acf(gaps, lag):
+    g = gaps - np.mean(gaps)
+    c0 = np.mean(g * g)
+    if c0 == 0:
+        return 0.0
+    return float(np.mean(g[:-lag] * g[lag:]) / c0)
+
+
+def obs_sr2(gaps):
+    r = np.minimum(gaps[:-2], gaps[2:]) / np.maximum(gaps[:-2], gaps[2:])
+    r = r[np.isfinite(r)]
+    return float(np.mean(r)) if len(r) else 0.0
+
+
+def obs_triple_var(gaps):
+    triples = gaps[:-2] + gaps[1:-1] + gaps[2:]
+    v = np.var(gaps)
+    if v == 0:
+        return 0.0
+    return float(np.var(triples) / v)
+
+
+OBSERVABLES = {
+    "SR": obs_spacing_ratio,
+    "L1": lambda gaps: obs_lag_acf(gaps, 1),
+    "L2": lambda gaps: obs_lag_acf(gaps, 2),
+    "SR2": obs_sr2,
+    "triple_var": obs_triple_var,
+}
+
+
+def measure(gaps):
+    return {name: fn(gaps) for name, fn in OBSERVABLES.items()}
+
+
+def full_shuffle_baseline(gaps, n_trials, rng):
+    vals = {name: [] for name in OBSERVABLES}
+    for _ in range(n_trials):
+        s = rng.permutation(gaps)
+        row = measure(s)
+        for name, value in row.items():
+            vals[name].append(value)
+    return {
+        name: {
+            "mean": float(np.mean(x)),
+            "std": float(np.std(x, ddof=1)) if len(x) > 1 else 0.0,
+        }
+        for name, x in vals.items()
+    }
+
+
+def retention_curves(gaps, alphas, n_trials, originals, baseline, rng):
+    rows = []
+    for alpha in alphas:
+        vals = {name: [] for name in OBSERVABLES}
+        for _ in range(n_trials):
+            s = partial_shuffle(gaps, float(alpha), rng)
+            row = measure(s)
+            for name, value in row.items():
+                vals[name].append(value)
+
+        out = {"alpha": float(alpha)}
+        for name in OBSERVABLES:
+            mean = float(np.mean(vals[name]))
+            denom = originals[name] - baseline[name]["mean"]
+            retention = (mean - baseline[name]["mean"]) / denom if abs(denom) > 1e-12 else 0.0
+            out[name] = {
+                "mean": mean,
+                "std": float(np.std(vals[name], ddof=1)) if len(vals[name]) > 1 else 0.0,
+                "retention": float(retention),
+            }
+        rows.append(out)
+    return rows
+
+
+def pca_summary(rows):
+    names = list(OBSERVABLES)
+    matrix = np.array([[row[name]["retention"] for name in names] for row in rows], dtype=float)
+    matrix = matrix - np.mean(matrix, axis=0, keepdims=True)
+
+    _, singular, vt = np.linalg.svd(matrix, full_matrices=False)
+    energy = singular * singular
+    if np.sum(energy) <= 1e-15:
+        explained = np.zeros_like(energy)
+        effective_rank = 0.0
+    else:
+        explained = energy / np.sum(energy)
+        positive = explained[explained > 1e-15]
+        effective_rank = float(np.exp(-np.sum(positive * np.log(positive))))
+
+    corr = np.corrcoef(matrix, rowvar=False)
+    abs_corr = np.abs(corr)
+    upper = abs_corr[np.triu_indices_from(abs_corr, k=1)]
+
+    return {
+        "observables": names,
+        "singular_values": [float(x) for x in singular],
+        "explained_variance": [float(x) for x in explained],
+        "effective_rank": effective_rank,
+        "pc1_loadings": {name: float(vt[0, i]) for i, name in enumerate(names)} if len(vt) else {},
+        "mean_abs_pairwise_corr": float(np.mean(upper)) if len(upper) else 0.0,
+        "min_abs_pairwise_corr": float(np.min(upper)) if len(upper) else 0.0,
+        "max_abs_pairwise_corr": float(np.max(upper)) if len(upper) else 0.0,
+    }
+
+
+def analyze_sequence(name, gaps, alphas, n_trials, n_baseline, rng):
+    originals = measure(gaps)
+    baseline = full_shuffle_baseline(gaps, n_baseline, rng)
+    rows = retention_curves(gaps, alphas, n_trials, originals, baseline, rng)
+
+    z = {}
+    for obs_name in OBSERVABLES:
+        std = baseline[obs_name]["std"]
+        z[obs_name] = float((originals[obs_name] - baseline[obs_name]["mean"]) / std) if std > 1e-12 else 0.0
+
+    return {
+        "n_gaps": int(len(gaps)),
+        "originals": originals,
+        "full_shuffle_baseline": baseline,
+        "original_vs_shuffle_z": z,
+        "retention_curves": rows,
+        "pca": pca_summary(rows),
+    }
+
+
+def build_sequences(n_gaps, rng):
+    primes = get_primes(n_gaps * 24)[: n_gaps + 1]
+    prime_gaps = np.diff(primes).astype(float)
+
+    gue = gue_gaps(160, max(8, n_gaps // 160 + 1), rng).astype(float)
+    gue = gue[:n_gaps]
+
+    poisson = rng.exponential(1.0, size=n_gaps).astype(float)
+    return {
+        "primes": prime_gaps,
+        "gue": gue,
+        "poisson": poisson,
+    }
+
+
+def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505):
+    rng = np.random.default_rng(seed)
+    alphas = np.linspace(0.05, 0.95, n_alpha)
+    sequences = build_sequences(n_gaps, rng)
+
+    output = {
+        "experiment": "observable_rank_audit",
+        "question": "Do crossover observables contain independent directions or one latent boundary coordinate?",
+        "params": {
+            "n_gaps": int(n_gaps),
+            "n_alpha": int(n_alpha),
+            "n_trials": int(n_trials),
+            "n_baseline": int(n_baseline),
+            "seed": int(seed),
+        },
+        "sequences": {},
+    }
+
+    print(f"n_gaps={n_gaps}, n_alpha={n_alpha}, n_trials={n_trials}, n_baseline={n_baseline}, seed={seed}")
+    print(f"{'sequence':<10} {'pc1':>8} {'eff_rank':>9} {'mean|corr|':>11}  z(SR,L1,L2,SR2,triple)")
+    print("-" * 92)
+    for seq_name, gaps in sequences.items():
+        result = analyze_sequence(seq_name, gaps, alphas, n_trials, n_baseline, rng)
+        output["sequences"][seq_name] = result
+        pca = result["pca"]
+        z = result["original_vs_shuffle_z"]
+        z_text = ", ".join(f"{obs}={z[obs]:+.1f}" for obs in OBSERVABLES)
+        pc1 = pca["explained_variance"][0] if pca["explained_variance"] else 0.0
+        print(f"{seq_name:<10} {pc1:>8.3f} {pca['effective_rank']:>9.3f} {pca['mean_abs_pairwise_corr']:>11.3f}  {z_text}")
+
+    out_path = Path("tools/data/observable_rank_audit.json")
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+    print(f"\nSaved to {out_path}")
+    return output
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n_gaps", type=int, default=30000)
+    parser.add_argument("--n_alpha", type=int, default=19)
+    parser.add_argument("--n_trials", type=int, default=24)
+    parser.add_argument("--n_baseline", type=int, default=72)
+    parser.add_argument("--seed", type=int, default=20260505)
+    args = parser.parse_args()
+    run(
+        n_gaps=args.n_gaps,
+        n_alpha=args.n_alpha,
+        n_trials=args.n_trials,
+        n_baseline=args.n_baseline,
+        seed=args.seed,
+    )
+
+
+if __name__ == "__main__":
+    main()

exec
/bin/bash -lc 'python tools/exp_observable_rank_audit.py --n_gaps 30000 --n_alpha 19 --n_trials 24 --n_baseline 72 --seed 20260505' in /opt/MM_D-ND
diff --git a/tools/exp_observable_rank_audit.py b/tools/exp_observable_rank_audit.py
new file mode 100644
index 0000000000000000000000000000000000000000..f2acafc5960c8a35964e0ab521cf70569eefe27f
--- /dev/null
+++ b/tools/exp_observable_rank_audit.py
@@ -0,0 +1,229 @@
+#!/usr/bin/env python3
+"""
+exp_observable_rank_audit.py - META audit for observable redundancy.
+
+Recent runs found many Markov/crossover observables that react coherently under
+partial shuffle. This script asks whether those observables carry independent
+directions or mostly re-measure one latent boundary coordinate.
+
+It measures retention curves from alpha-partial shuffles, then reports:
+  - original-vs-full-shuffle z for each observable
+  - PCA energy of the retention matrix across alpha
+  - effective rank of that matrix
+  - pairwise correlations between retention curves
+
+The script measures data only. The report decides the structural claim.
+"""
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from exp_3d_boundary_layers import get_primes, gue_gaps, partial_shuffle
+
+
+def obs_spacing_ratio(gaps):
+    r = np.minimum(gaps[:-1], gaps[1:]) / np.maximum(gaps[:-1], gaps[1:])
+    r = r[np.isfinite(r)]
+    return float(np.mean(r)) if len(r) else 0.0
+
+
+def obs_lag_acf(gaps, lag):
+    g = gaps - np.mean(gaps)
+    c0 = np.mean(g * g)
+    if c0 == 0:
+        return 0.0
+    return float(np.mean(g[:-lag] * g[lag:]) / c0)
+
+
+def obs_sr2(gaps):
+    r = np.minimum(gaps[:-2], gaps[2:]) / np.maximum(gaps[:-2], gaps[2:])
+    r = r[np.isfinite(r)]
+    return float(np.mean(r)) if len(r) else 0.0
+
+
+def obs_triple_var(gaps):
+    triples = gaps[:-2] + gaps[1:-1] + gaps[2:]
+    v = np.var(gaps)
+    if v == 0:
+        return 0.0
+    return float(np.var(triples) / v)
+
+
+OBSERVABLES = {
+    "SR": obs_spacing_ratio,
+    "L1": lambda gaps: obs_lag_acf(gaps, 1),
+    "L2": lambda gaps: obs_lag_acf(gaps, 2),
+    "SR2": obs_sr2,
+    "triple_var": obs_triple_var,
+}
+
+
+def measure(gaps):
+    return {name: fn(gaps) for name, fn in OBSERVABLES.items()}
+
+
+def full_shuffle_baseline(gaps, n_trials, rng):
+    vals = {name: [] for name in OBSERVABLES}
+    for _ in range(n_trials):
+        s = rng.permutation(gaps)
+        row = measure(s)
+        for name, value in row.items():
+            vals[name].append(value)
+    return {
+        name: {
+            "mean": float(np.mean(x)),
+            "std": float(np.std(x, ddof=1)) if len(x) > 1 else 0.0,
+        }
+        for name, x in vals.items()
+    }
+
+
+def retention_curves(gaps, alphas, n_trials, originals, baseline, rng):
+    rows = []
+    for alpha in alphas:
+        vals = {name: [] for name in OBSERVABLES}
+        for _ in range(n_trials):
+            s = partial_shuffle(gaps, float(alpha), rng)
+            row = measure(s)
+            for name, value in row.items():
+                vals[name].append(value)
+
+        out = {"alpha": float(alpha)}
+        for name in OBSERVABLES:
+            mean = float(np.mean(vals[name]))
+            denom = originals[name] - baseline[name]["mean"]
+            retention = (mean - baseline[name]["mean"]) / denom if abs(denom) > 1e-12 else 0.0
+            out[name] = {
+                "mean": mean,
+                "std": float(np.std(vals[name], ddof=1)) if len(vals[name]) > 1 else 0.0,
+                "retention": float(retention),
+            }
+        rows.append(out)
+    return rows
+
+
+def pca_summary(rows):
+    names = list(OBSERVABLES)
+    matrix = np.array([[row[name]["retention"] for name in names] for row in rows], dtype=float)
+    matrix = matrix - np.mean(matrix, axis=0, keepdims=True)
+
+    _, singular, vt = np.linalg.svd(matrix, full_matrices=False)
+    energy = singular * singular
+    if np.sum(energy) <= 1e-15:
+        explained = np.zeros_like(energy)
+        effective_rank = 0.0
+    else:
+        explained = energy / np.sum(energy)
+        positive = explained[explained > 1e-15]
+        effective_rank = float(np.exp(-np.sum(positive * np.log(positive))))
+
+    corr = np.corrcoef(matrix, rowvar=False)
+    abs_corr = np.abs(corr)
+    upper = abs_corr[np.triu_indices_from(abs_corr, k=1)]
+
+    return {
+        "observables": names,
+        "singular_values": [float(x) for x in singular],
+        "explained_variance": [float(x) for x in explained],
+        "effective_rank": effective_rank,
+        "pc1_loadings": {name: float(vt[0, i]) for i, name in enumerate(names)} if len(vt) else {},
+        "mean_abs_pairwise_corr": float(np.mean(upper)) if len(upper) else 0.0,
+        "min_abs_pairwise_corr": float(np.min(upper)) if len(upper) else 0.0,
+        "max_abs_pairwise_corr": float(np.max(upper)) if len(upper) else 0.0,
+    }
+
+
+def analyze_sequence(name, gaps, alphas, n_trials, n_baseline, rng):
+    originals = measure(gaps)
+    baseline = full_shuffle_baseline(gaps, n_baseline, rng)
+    rows = retention_curves(gaps, alphas, n_trials, originals, baseline, rng)
+
+    z = {}
+    for obs_name in OBSERVABLES:
+        std = baseline[obs_name]["std"]
+        z[obs_name] = float((originals[obs_name] - baseline[obs_name]["mean"]) / std) if std > 1e-12 else 0.0
+
+    return {
+        "n_gaps": int(len(gaps)),
+        "originals": originals,
+        "full_shuffle_baseline": baseline,
+        "original_vs_shuffle_z": z,
+        "retention_curves": rows,
+        "pca": pca_summary(rows),
+    }
+
+
+def build_sequences(n_gaps, rng):
+    primes = get_primes(n_gaps * 24)[: n_gaps + 1]
+    prime_gaps = np.diff(primes).astype(float)
+
+    gue = gue_gaps(160, max(8, n_gaps // 160 + 1), rng).astype(float)
+    gue = gue[:n_gaps]
+
+    poisson = rng.exponential(1.0, size=n_gaps).astype(float)
+    return {
+        "primes": prime_gaps,
+        "gue": gue,
+        "poisson": poisson,
+    }
+
+
+def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505):
+    rng = np.random.default_rng(seed)
+    alphas = np.linspace(0.05, 0.95, n_alpha)
+    sequences = build_sequences(n_gaps, rng)
+
+    output = {
+        "experiment": "observable_rank_audit",
+        "question": "Do crossover observables contain independent directions or one latent boundary coordinate?",
+        "params": {
+            "n_gaps": int(n_gaps),
+            "n_alpha": int(n_alpha),
+            "n_trials": int(n_trials),
+            "n_baseline": int(n_baseline),
+            "seed": int(seed),
+        },
+        "sequences": {},
+    }
+
+    print(f"n_gaps={n_gaps}, n_alpha={n_alpha}, n_trials={n_trials}, n_baseline={n_baseline}, seed={seed}")
+    print(f"{'sequence':<10} {'pc1':>8} {'eff_rank':>9} {'mean|corr|':>11}  z(SR,L1,L2,SR2,triple)")
+    print("-" * 92)
+    for seq_name, gaps in sequences.items():
+        result = analyze_sequence(seq_name, gaps, alphas, n_trials, n_baseline, rng)
+        output["sequences"][seq_name] = result
+        pca = result["pca"]
+        z = result["original_vs_shuffle_z"]
+        z_text = ", ".join(f"{obs}={z[obs]:+.1f}" for obs in OBSERVABLES)
+        pc1 = pca["explained_variance"][0] if pca["explained_variance"] else 0.0
+        print(f"{seq_name:<10} {pc1:>8.3f} {pca['effective_rank']:>9.3f} {pca['mean_abs_pairwise_corr']:>11.3f}  {z_text}")
+
+    out_path = Path("tools/data/observable_rank_audit.json")
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+    print(f"\nSaved to {out_path}")
+    return output
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n_gaps", type=int, default=30000)
+    parser.add_argument("--n_alpha", type=int, default=19)
+    parser.add_argument("--n_trials", type=int, default=24)
+    parser.add_argument("--n_baseline", type=int, default=72)
+    parser.add_argument("--seed", type=int, default=20260505)
+    args = parser.parse_args()
+    run(
+        n_gaps=args.n_gaps,
+        n_alpha=args.n_alpha,
+        n_trials=args.n_trials,
+        n_baseline=args.n_baseline,
+        seed=args.seed,
+    )
+
+
+if __name__ == "__main__":
+    main()

 succeeded in 2525ms:
n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505
sequence        pc1  eff_rank  mean|corr|  z(SR,L1,L2,SR2,triple)
--------------------------------------------------------------------------------------------
primes        0.989     1.069       0.987  SR=-19.3, L1=-7.7, L2=-3.0, SR2=-3.7, triple_var=-8.0
gue           0.997     1.022       0.996  SR=-6.6, L1=+21.4, L2=+37.9, SR2=+18.0, triple_var=+36.3
poisson       0.925     1.391       0.351  SR=-0.6, L1=+0.2, L2=+1.2, SR2=-0.0, triple_var=+0.8

Saved to tools/data/observable_rank_audit.json

diff --git a/tools/exp_observable_rank_audit.py b/tools/exp_observable_rank_audit.py
new file mode 100644
index 0000000000000000000000000000000000000000..f2acafc5960c8a35964e0ab521cf70569eefe27f
--- /dev/null
+++ b/tools/exp_observable_rank_audit.py
@@ -0,0 +1,229 @@
+#!/usr/bin/env python3
+"""
+exp_observable_rank_audit.py - META audit for observable redundancy.
+
+Recent runs found many Markov/crossover observables that react coherently under
+partial shuffle. This script asks whether those observables carry independent
+directions or mostly re-measure one latent boundary coordinate.
+
+It measures retention curves from alpha-partial shuffles, then reports:
+  - original-vs-full-shuffle z for each observable
+  - PCA energy of the retention matrix across alpha
+  - effective rank of that matrix
+  - pairwise correlations between retention curves
+
+The script measures data only. The report decides the structural claim.
+"""
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from exp_3d_boundary_layers import get_primes, gue_gaps, partial_shuffle
+
+
+def obs_spacing_ratio(gaps):
+    r = np.minimum(gaps[:-1], gaps[1:]) / np.maximum(gaps[:-1], gaps[1:])
+    r = r[np.isfinite(r)]
+    return float(np.mean(r)) if len(r) else 0.0
+
+
+def obs_lag_acf(gaps, lag):
+    g = gaps - np.mean(gaps)
+    c0 = np.mean(g * g)
+    if c0 == 0:
+        return 0.0
+    return float(np.mean(g[:-lag] * g[lag:]) / c0)
+
+
+def obs_sr2(gaps):
+    r = np.minimum(gaps[:-2], gaps[2:]) / np.maximum(gaps[:-2], gaps[2:])
+    r = r[np.isfinite(r)]
+    return float(np.mean(r)) if len(r) else 0.0
+
+
+def obs_triple_var(gaps):
+    triples = gaps[:-2] + gaps[1:-1] + gaps[2:]
+    v = np.var(gaps)
+    if v == 0:
+        return 0.0
+    return float(np.var(triples) / v)
+
+
+OBSERVABLES = {
+    "SR": obs_spacing_ratio,
+    "L1": lambda gaps: obs_lag_acf(gaps, 1),
+    "L2": lambda gaps: obs_lag_acf(gaps, 2),
+    "SR2": obs_sr2,
+    "triple_var": obs_triple_var,
+}
+
+
+def measure(gaps):
+    return {name: fn(gaps) for name, fn in OBSERVABLES.items()}
+
+
+def full_shuffle_baseline(gaps, n_trials, rng):
+    vals = {name: [] for name in OBSERVABLES}
+    for _ in range(n_trials):
+        s = rng.permutation(gaps)
+        row = measure(s)
+        for name, value in row.items():
+            vals[name].append(value)
+    return {
+        name: {
+            "mean": float(np.mean(x)),
+            "std": float(np.std(x, ddof=1)) if len(x) > 1 else 0.0,
+        }
+        for name, x in vals.items()
+    }
+
+
+def retention_curves(gaps, alphas, n_trials, originals, baseline, rng):
+    rows = []
+    for alpha in alphas:
+        vals = {name: [] for name in OBSERVABLES}
+        for _ in range(n_trials):
+            s = partial_shuffle(gaps, float(alpha), rng)
+            row = measure(s)
+            for name, value in row.items():
+                vals[name].append(value)
+
+        out = {"alpha": float(alpha)}
+        for name in OBSERVABLES:
+            mean = float(np.mean(vals[name]))
+            denom = originals[name] - baseline[name]["mean"]
+            retention = (mean - baseline[name]["mean"]) / denom if abs(denom) > 1e-12 else 0.0
+            out[name] = {
+                "mean": mean,
+                "std": float(np.std(vals[name], ddof=1)) if len(vals[name]) > 1 else 0.0,
+                "retention": float(retention),
+            }
+        rows.append(out)
+    return rows
+
+
+def pca_summary(rows):
+    names = list(OBSERVABLES)
+    matrix = np.array([[row[name]["retention"] for name in names] for row in rows], dtype=float)
+    matrix = matrix - np.mean(matrix, axis=0, keepdims=True)
+
+    _, singular, vt = np.linalg.svd(matrix, full_matrices=False)
+    energy = singular * singular
+    if np.sum(energy) <= 1e-15:
+        explained = np.zeros_like(energy)
+        effective_rank = 0.0
+    else:
+        explained = energy / np.sum(energy)
+        positive = explained[explained > 1e-15]
+        effective_rank = float(np.exp(-np.sum(positive * np.log(positive))))
+
+    corr = np.corrcoef(matrix, rowvar=False)
+    abs_corr = np.abs(corr)
+    upper = abs_corr[np.triu_indices_from(abs_corr, k=1)]
+
+    return {
+        "observables": names,
+        "singular_values": [float(x) for x in singular],
+        "explained_variance": [float(x) for x in explained],
+        "effective_rank": effective_rank,
+        "pc1_loadings": {name: float(vt[0, i]) for i, name in enumerate(names)} if len(vt) else {},
+        "mean_abs_pairwise_corr": float(np.mean(upper)) if len(upper) else 0.0,
+        "min_abs_pairwise_corr": float(np.min(upper)) if len(upper) else 0.0,
+        "max_abs_pairwise_corr": float(np.max(upper)) if len(upper) else 0.0,
+    }
+
+
+def analyze_sequence(name, gaps, alphas, n_trials, n_baseline, rng):
+    originals = measure(gaps)
+    baseline = full_shuffle_baseline(gaps, n_baseline, rng)
+    rows = retention_curves(gaps, alphas, n_trials, originals, baseline, rng)
+
+    z = {}
+    for obs_name in OBSERVABLES:
+        std = baseline[obs_name]["std"]
+        z[obs_name] = float((originals[obs_name] - baseline[obs_name]["mean"]) / std) if std > 1e-12 else 0.0
+
+    return {
+        "n_gaps": int(len(gaps)),
+        "originals": originals,
+        "full_shuffle_baseline": baseline,
+        "original_vs_shuffle_z": z,
+        "retention_curves": rows,
+        "pca": pca_summary(rows),
+    }
+
+
+def build_sequences(n_gaps, rng):
+    primes = get_primes(n_gaps * 24)[: n_gaps + 1]
+    prime_gaps = np.diff(primes).astype(float)
+
+    gue = gue_gaps(160, max(8, n_gaps // 160 + 1), rng).astype(float)
+    gue = gue[:n_gaps]
+
+    poisson = rng.exponential(1.0, size=n_gaps).astype(float)
+    return {
+        "primes": prime_gaps,
+        "gue": gue,
+        "poisson": poisson,
+    }
+
+
+def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505):
+    rng = np.random.default_rng(seed)
+    alphas = np.linspace(0.05, 0.95, n_alpha)
+    sequences = build_sequences(n_gaps, rng)
+
+    output = {
+        "experiment": "observable_rank_audit",
+        "question": "Do crossover observables contain independent directions or one latent boundary coordinate?",
+        "params": {
+            "n_gaps": int(n_gaps),
+            "n_alpha": int(n_alpha),
+            "n_trials": int(n_trials),
+            "n_baseline": int(n_baseline),
+            "seed": int(seed),
+        },
+        "sequences": {},
+    }
+
+    print(f"n_gaps={n_gaps}, n_alpha={n_alpha}, n_trials={n_trials}, n_baseline={n_baseline}, seed={seed}")
+    print(f"{'sequence':<10} {'pc1':>8} {'eff_rank':>9} {'mean|corr|':>11}  z(SR,L1,L2,SR2,triple)")
+    print("-" * 92)
+    for seq_name, gaps in sequences.items():
+        result = analyze_sequence(seq_name, gaps, alphas, n_trials, n_baseline, rng)
+        output["sequences"][seq_name] = result
+        pca = result["pca"]
+        z = result["original_vs_shuffle_z"]
+        z_text = ", ".join(f"{obs}={z[obs]:+.1f}" for obs in OBSERVABLES)
+        pc1 = pca["explained_variance"][0] if pca["explained_variance"] else 0.0
+        print(f"{seq_name:<10} {pc1:>8.3f} {pca['effective_rank']:>9.3f} {pca['mean_abs_pairwise_corr']:>11.3f}  {z_text}")
+
+    out_path = Path("tools/data/observable_rank_audit.json")
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+    print(f"\nSaved to {out_path}")
+    return output
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n_gaps", type=int, default=30000)
+    parser.add_argument("--n_alpha", type=int, default=19)
+    parser.add_argument("--n_trials", type=int, default=24)
+    parser.add_argument("--n_baseline", type=int, default=72)
+    parser.add_argument("--seed", type=int, default=20260505)
+    args = parser.parse_args()
+    run(
+        n_gaps=args.n_gaps,
+        n_alpha=args.n_alpha,
+        n_trials=args.n_trials,
+        n_baseline=args.n_baseline,
+        seed=args.seed,
+    )
+
+
+if __name__ == "__main__":
+    main()

exec
/bin/bash -lc 'tail -n 80 tools/data/seme.json' in /opt/MM_D-ND
 succeeded in 0ms:
        "fluctuations",
        "information theory",
        "probability",
        "Boltzmann"
      ],
      "content": "Computing termodinamico: sfruttare le fluttuazioni termiche invece di combatterle. Il rumore non è nemico — è risorsa. Extropic costruisce hardware che usa l'entropia come motore computazionale. Connessione profonda tra termodinamica, teoria dell'informazione e probabilità.",
      "teorie": [
        "T",
        "Q"
      ],
      "ponti_potenziali": [
        {
          "coppia": "TxQ",
          "ponte": "noise come risorsa computazionale — fluttuazioni termiche = calcolo",
          "nota": "il vuoto quantistico (pieno di fluttuazioni) è il computer"
        }
      ],
      "timestamp": "2026-04-02T08:23:13.992019"
    },
    {
      "id": "video_j0wJBEZdwLs",
      "title": "What is a Laplace Transform - visual explanation",
      "type": "video_digest",
      "keywords": [
        "Laplace transform",
        "frequency",
        "damping",
        "s-plane",
        "complex",
        "exponential"
      ],
      "content": "La trasformata di Laplace come proiezione su esponenziali complesse. Il piano s = σ + iω combina smorzamento (reale) e oscillazione (immaginario). Connessione tra dominio temporale e dominio delle frequenze complesse.",
      "teorie": [
        "T",
        "Q",
        "R"
      ],
      "ponti_potenziali": [],
      "timestamp": "2026-04-02T08:23:13.992021"
    },
    {
      "id": "video_rZ2m1_q9lg0",
      "title": "New duality: conductor-insulator in YbB12 at 35T - University of Michigan",
      "type": "video_digest",
      "keywords": [
        "duality",
        "conductor",
        "insulator",
        "Kondo insulator",
        "quantum oscillations",
        "ytterbium boride",
        "YbB12",
        "charge-neutral fermions",
        "strongly correlated",
        "condensed matter",
        "magnetic field"
      ],
      "content": "Scoperta dualita conduttore-isolante in YbB12 a 35 Tesla. Il materiale si comporta simultaneamente da conduttore e isolante — oscillazioni quantistiche dal bulk, non dalla superficie. Charge-neutral fermions (portatori senza carica) creano comportamento di conduzione = il terzo incluso. La scoperta avviene a campo estremo dove il confine tra stati collassa.",
      "teorie": [
        "T",
        "Q",
        "E"
      ],
      "ponti_potenziali": [
        {
          "coppia": "TxQ",
          "ponte": "Kondo insulator fortemente correlato — dualita conduttore/isolante come stato quantistico unico",
          "nota": "charge-neutral fermions = portatori senza carica che conducono, terzo incluso operativo"
        },
        {
          "coppia": "ExQ",
          "ponte": "campo magnetico estremo (35T) collassa il confine tra stati — condizione di soglia",
          "nota": "il confine non separa, unifica. Pattern BOUNDARY del Domandatore"
        }
      ],
      "ref": "Physical Review Letters 2025, 135(15). Chen, Zhu, Ratkovski et al. PI: Lu Li",
      "timestamp": "2026-04-04T19:34:08.014Z"
    }
  ]
}
exec
/bin/bash -lc 'ls tools/data/reports | tail -n 30' in /opt/MM_D-ND
 succeeded in 0ms:
phi_vs_silver_falsification_20260306.json
report_20260302_0341.md
report_20260303_0341.md
report_20260304_0342.md
report_20260305_0342.md
report_20260305_2121.md
report_20260306_0341.md
report_20260307_0342.md
report_20260314_0342.md
report_20260315_0342.md
report_20260315_0801.md
report_20260326_0343.md
report_20260327_0344.md
report_20260328_0344.md
report_20260329_0343.md
report_20260330_0344.md
report_20260331_0345.md
report_20260331_1809.md
report_20260401_0346.md
report_20260402_0344.md
report_20260402_0756.md
report_20260403_0330.md
report_20260404_0330.md
report_20260404_1852.md
report_20260405_0330.md
report_20260405_0715.md
tension_projection_screening_2026-04-21.json
trace_map_20260305_0843.json
trace_map_20260305_0844.json
watchdog.log

exec
/bin/bash -lc "python - <<'PY'
import json
for path in ['tools/data/observable_rank_audit.json','tools/data/observable_rank_audit_seed20260506.json']:
    d=json.load(open(path))
    print(path)
    for s,r in d['sequences'].items():
        p=r['pca']; z=r['original_vs_shuffle_z']
        print(s, 'pc1', round(p['explained_variance'][0],4), 'rank', round(p['effective_rank'],4), 'corr', round(p['mean_abs_pairwise_corr'],4), 'mincorr', round(p['min_abs_pairwise_corr'],4), 'maxcorr', round(p['max_abs_pairwise_corr'],4))
        print('  load', {k:round(v,3) for k,v in p['pc1_loadings'].items()})
        print('  z', {k:round(v,2) for k,v in z.items()})
PY" in /opt/MM_D-ND
 succeeded in 0ms:
tools/data/observable_rank_audit.json
primes pc1 0.9894 rank 1.0694 corr 0.9867 mincorr 0.9749 maxcorr 0.9993
  load {'SR': -0.45, 'L1': -0.449, 'L2': -0.443, 'SR2': -0.446, 'triple_var': -0.448}
  z {'SR': -19.29, 'L1': -7.66, 'L2': -3.0, 'SR2': -3.73, 'triple_var': -8.03}
gue pc1 0.9969 rank 1.0219 corr 0.9961 mincorr 0.9886 maxcorr 0.9998
  load {'SR': -0.446, 'L1': -0.449, 'L2': -0.447, 'SR2': -0.446, 'triple_var': -0.448}
  z {'SR': -6.58, 'L1': 21.4, 'L2': 37.87, 'SR2': 17.96, 'triple_var': 36.3}
poisson pc1 0.9246 rank 1.3914 corr 0.351 mincorr 0.0664 maxcorr 0.7736
  load {'SR': -0.032, 'L1': -0.086, 'L2': 0.007, 'SR2': -0.996, 'triple_var': -0.017}
  z {'SR': -0.56, 'L1': 0.22, 'L2': 1.17, 'SR2': -0.05, 'triple_var': 0.75}
tools/data/observable_rank_audit_seed20260506.json
primes pc1 0.9879 rank 1.0757 corr 0.9851 mincorr 0.9674 maxcorr 0.999
  load {'SR': -0.452, 'L1': -0.444, 'L2': -0.454, 'SR2': -0.44, 'triple_var': -0.446}
  z {'SR': -15.9, 'L1': -7.32, 'L2': -2.81, 'SR2': -3.88, 'triple_var': -7.59}
gue pc1 0.9973 rank 1.0195 corr 0.9967 mincorr 0.9916 maxcorr 0.9999
  load {'SR': -0.45, 'L1': -0.445, 'L2': -0.448, 'SR2': -0.447, 'triple_var': -0.446}
  z {'SR': -6.21, 'L1': 20.66, 'L2': 41.82, 'SR2': 19.51, 'triple_var': 36.04}
poisson pc1 0.6747 rank 2.3944 corr 0.6402 mincorr 0.3748 maxcorr 0.9474
  load {'SR': -0.542, 'L1': -0.551, 'L2': -0.329, 'SR2': -0.311, 'triple_var': -0.444}
  z {'SR': -0.28, 'L1': -0.41, 'L2': -0.79, 'SR2': -2.26, 'triple_var': -0.73}

diff --git a/tools/exp_observable_rank_audit.py b/tools/exp_observable_rank_audit.py
new file mode 100644
index 0000000000000000000000000000000000000000..f2acafc5960c8a35964e0ab521cf70569eefe27f
--- /dev/null
+++ b/tools/exp_observable_rank_audit.py
@@ -0,0 +1,229 @@
+#!/usr/bin/env python3
+"""
+exp_observable_rank_audit.py - META audit for observable redundancy.
+
+Recent runs found many Markov/crossover observables that react coherently under
+partial shuffle. This script asks whether those observables carry independent
+directions or mostly re-measure one latent boundary coordinate.
+
+It measures retention curves from alpha-partial shuffles, then reports:
+  - original-vs-full-shuffle z for each observable
+  - PCA energy of the retention matrix across alpha
+  - effective rank of that matrix
+  - pairwise correlations between retention curves
+
+The script measures data only. The report decides the structural claim.
+"""
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from exp_3d_boundary_layers import get_primes, gue_gaps, partial_shuffle
+
+
+def obs_spacing_ratio(gaps):
+    r = np.minimum(gaps[:-1], gaps[1:]) / np.maximum(gaps[:-1], gaps[1:])
+    r = r[np.isfinite(r)]
+    return float(np.mean(r)) if len(r) else 0.0
+
+
+def obs_lag_acf(gaps, lag):
+    g = gaps - np.mean(gaps)
+    c0 = np.mean(g * g)
+    if c0 == 0:
+        return 0.0
+    return float(np.mean(g[:-lag] * g[lag:]) / c0)
+
+
+def obs_sr2(gaps):
+    r = np.minimum(gaps[:-2], gaps[2:]) / np.maximum(gaps[:-2], gaps[2:])
+    r = r[np.isfinite(r)]
+    return float(np.mean(r)) if len(r) else 0.0
+
+
+def obs_triple_var(gaps):
+    triples = gaps[:-2] + gaps[1:-1] + gaps[2:]
+    v = np.var(gaps)
+    if v == 0:
+        return 0.0
+    return float(np.var(triples) / v)
+
+
+OBSERVABLES = {
+    "SR": obs_spacing_ratio,
+    "L1": lambda gaps: obs_lag_acf(gaps, 1),
+    "L2": lambda gaps: obs_lag_acf(gaps, 2),
+    "SR2": obs_sr2,
+    "triple_var": obs_triple_var,
+}
+
+
+def measure(gaps):
+    return {name: fn(gaps) for name, fn in OBSERVABLES.items()}
+
+
+def full_shuffle_baseline(gaps, n_trials, rng):
+    vals = {name: [] for name in OBSERVABLES}
+    for _ in range(n_trials):
+        s = rng.permutation(gaps)
+        row = measure(s)
+        for name, value in row.items():
+            vals[name].append(value)
+    return {
+        name: {
+            "mean": float(np.mean(x)),
+            "std": float(np.std(x, ddof=1)) if len(x) > 1 else 0.0,
+        }
+        for name, x in vals.items()
+    }
+
+
+def retention_curves(gaps, alphas, n_trials, originals, baseline, rng):
+    rows = []
+    for alpha in alphas:
+        vals = {name: [] for name in OBSERVABLES}
+        for _ in range(n_trials):
+            s = partial_shuffle(gaps, float(alpha), rng)
+            row = measure(s)
+            for name, value in row.items():
+                vals[name].append(value)
+
+        out = {"alpha": float(alpha)}
+        for name in OBSERVABLES:
+            mean = float(np.mean(vals[name]))
+            denom = originals[name] - baseline[name]["mean"]
+            retention = (mean - baseline[name]["mean"]) / denom if abs(denom) > 1e-12 else 0.0
+            out[name] = {
+                "mean": mean,
+                "std": float(np.std(vals[name], ddof=1)) if len(vals[name]) > 1 else 0.0,
+                "retention": float(retention),
+            }
+        rows.append(out)
+    return rows
+
+
+def pca_summary(rows):
+    names = list(OBSERVABLES)
+    matrix = np.array([[row[name]["retention"] for name in names] for row in rows], dtype=float)
+    matrix = matrix - np.mean(matrix, axis=0, keepdims=True)
+
+    _, singular, vt = np.linalg.svd(matrix, full_matrices=False)
+    energy = singular * singular
+    if np.sum(energy) <= 1e-15:
+        explained = np.zeros_like(energy)
+        effective_rank = 0.0
+    else:
+        explained = energy / np.sum(energy)
+        positive = explained[explained > 1e-15]
+        effective_rank = float(np.exp(-np.sum(positive * np.log(positive))))
+
+    corr = np.corrcoef(matrix, rowvar=False)
+    abs_corr = np.abs(corr)
+    upper = abs_corr[np.triu_indices_from(abs_corr, k=1)]
+
+    return {
+        "observables": names,
+        "singular_values": [float(x) for x in singular],
+        "explained_variance": [float(x) for x in explained],
+        "effective_rank": effective_rank,
+        "pc1_loadings": {name: float(vt[0, i]) for i, name in enumerate(names)} if len(vt) else {},
+        "mean_abs_pairwise_corr": float(np.mean(upper)) if len(upper) else 0.0,
+        "min_abs_pairwise_corr": float(np.min(upper)) if len(upper) else 0.0,
+        "max_abs_pairwise_corr": float(np.max(upper)) if len(upper) else 0.0,
+    }
+
+
+def analyze_sequence(name, gaps, alphas, n_trials, n_baseline, rng):
+    originals = measure(gaps)
+    baseline = full_shuffle_baseline(gaps, n_baseline, rng)
+    rows = retention_curves(gaps, alphas, n_trials, originals, baseline, rng)
+
+    z = {}
+    for obs_name in OBSERVABLES:
+        std = baseline[obs_name]["std"]
+        z[obs_name] = float((originals[obs_name] - baseline[obs_name]["mean"]) / std) if std > 1e-12 else 0.0
+
+    return {
+        "n_gaps": int(len(gaps)),
+        "originals": originals,
+        "full_shuffle_baseline": baseline,
+        "original_vs_shuffle_z": z,
+        "retention_curves": rows,
+        "pca": pca_summary(rows),
+    }
+
+
+def build_sequences(n_gaps, rng):
+    primes = get_primes(n_gaps * 24)[: n_gaps + 1]
+    prime_gaps = np.diff(primes).astype(float)
+
+    gue = gue_gaps(160, max(8, n_gaps // 160 + 1), rng).astype(float)
+    gue = gue[:n_gaps]
+
+    poisson = rng.exponential(1.0, size=n_gaps).astype(float)
+    return {
+        "primes": prime_gaps,
+        "gue": gue,
+        "poisson": poisson,
+    }
+
+
+def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505):
+    rng = np.random.default_rng(seed)
+    alphas = np.linspace(0.05, 0.95, n_alpha)
+    sequences = build_sequences(n_gaps, rng)
+
+    output = {
+        "experiment": "observable_rank_audit",
+        "question": "Do crossover observables contain independent directions or one latent boundary coordinate?",
+        "params": {
+            "n_gaps": int(n_gaps),
+            "n_alpha": int(n_alpha),
+            "n_trials": int(n_trials),
+            "n_baseline": int(n_baseline),
+            "seed": int(seed),
+        },
+        "sequences": {},
+    }
+
+    print(f"n_gaps={n_gaps}, n_alpha={n_alpha}, n_trials={n_trials}, n_baseline={n_baseline}, seed={seed}")
+    print(f"{'sequence':<10} {'pc1':>8} {'eff_rank':>9} {'mean|corr|':>11}  z(SR,L1,L2,SR2,triple)")
+    print("-" * 92)
+    for seq_name, gaps in sequences.items():
+        result = analyze_sequence(seq_name, gaps, alphas, n_trials, n_baseline, rng)
+        output["sequences"][seq_name] = result
+        pca = result["pca"]
+        z = result["original_vs_shuffle_z"]
+        z_text = ", ".join(f"{obs}={z[obs]:+.1f}" for obs in OBSERVABLES)
+        pc1 = pca["explained_variance"][0] if pca["explained_variance"] else 0.0
+        print(f"{seq_name:<10} {pc1:>8.3f} {pca['effective_rank']:>9.3f} {pca['mean_abs_pairwise_corr']:>11.3f}  {z_text}")
+
+    out_path = Path("tools/data/observable_rank_audit.json")
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+    print(f"\nSaved to {out_path}")
+    return output
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n_gaps", type=int, default=30000)
+    parser.add_argument("--n_alpha", type=int, default=19)
+    parser.add_argument("--n_trials", type=int, default=24)
+    parser.add_argument("--n_baseline", type=int, default=72)
+    parser.add_argument("--seed", type=int, default=20260505)
+    args = parser.parse_args()
+    run(
+        n_gaps=args.n_gaps,
+        n_alpha=args.n_alpha,
+        n_trials=args.n_trials,
+        n_baseline=args.n_baseline,
+        seed=args.seed,
+    )
+
+
+if __name__ == "__main__":
+    main()

exec
/bin/bash -lc "find tools/data/reports -maxdepth 1 -type f -name 'agent_202605*' -printf '%f
' | sort | tail -n 20" in /opt/MM_D-ND
 succeeded in 0ms:
agent_20260501_0725.md
agent_20260501_0725_raw.log
agent_20260501_0858_raw.log
agent_20260501_0931.md
agent_20260501_0931_raw.log
agent_20260502_0330.md
agent_20260502_0330_raw.log
agent_20260503_0330.md
agent_20260503_0330_raw.log
agent_20260504_0330_codex_raw.log
agent_20260504_0721_claude_raw.log
agent_20260504_0721_codex_raw.log
agent_20260504_0901.md
agent_20260504_0901_claude_raw.log
agent_20260504_0901_codex_raw.log
agent_20260504_1138_claude_raw.log
agent_20260504_1138_codex_raw.log
agent_20260504_1219.md
agent_20260504_1219_codex_raw.log
agent_20260505_0330_codex_raw.log

diff --git a/tools/exp_observable_rank_audit.py b/tools/exp_observable_rank_audit.py
new file mode 100644
index 0000000000000000000000000000000000000000..f2acafc5960c8a35964e0ab521cf70569eefe27f
--- /dev/null
+++ b/tools/exp_observable_rank_audit.py
@@ -0,0 +1,229 @@
+#!/usr/bin/env python3
+"""
+exp_observable_rank_audit.py - META audit for observable redundancy.
+
+Recent runs found many Markov/crossover observables that react coherently under
+partial shuffle. This script asks whether those observables carry independent
+directions or mostly re-measure one latent boundary coordinate.
+
+It measures retention curves from alpha-partial shuffles, then reports:
+  - original-vs-full-shuffle z for each observable
+  - PCA energy of the retention matrix across alpha
+  - effective rank of that matrix
+  - pairwise correlations between retention curves
+
+The script measures data only. The report decides the structural claim.
+"""
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from exp_3d_boundary_layers import get_primes, gue_gaps, partial_shuffle
+
+
+def obs_spacing_ratio(gaps):
+    r = np.minimum(gaps[:-1], gaps[1:]) / np.maximum(gaps[:-1], gaps[1:])
+    r = r[np.isfinite(r)]
+    return float(np.mean(r)) if len(r) else 0.0
+
+
+def obs_lag_acf(gaps, lag):
+    g = gaps - np.mean(gaps)
+    c0 = np.mean(g * g)
+    if c0 == 0:
+        return 0.0
+    return float(np.mean(g[:-lag] * g[lag:]) / c0)
+
+
+def obs_sr2(gaps):
+    r = np.minimum(gaps[:-2], gaps[2:]) / np.maximum(gaps[:-2], gaps[2:])
+    r = r[np.isfinite(r)]
+    return float(np.mean(r)) if len(r) else 0.0
+
+
+def obs_triple_var(gaps):
+    triples = gaps[:-2] + gaps[1:-1] + gaps[2:]
+    v = np.var(gaps)
+    if v == 0:
+        return 0.0
+    return float(np.var(triples) / v)
+
+
+OBSERVABLES = {
+    "SR": obs_spacing_ratio,
+    "L1": lambda gaps: obs_lag_acf(gaps, 1),
+    "L2": lambda gaps: obs_lag_acf(gaps, 2),
+    "SR2": obs_sr2,
+    "triple_var": obs_triple_var,
+}
+
+
+def measure(gaps):
+    return {name: fn(gaps) for name, fn in OBSERVABLES.items()}
+
+
+def full_shuffle_baseline(gaps, n_trials, rng):
+    vals = {name: [] for name in OBSERVABLES}
+    for _ in range(n_trials):
+        s = rng.permutation(gaps)
+        row = measure(s)
+        for name, value in row.items():
+            vals[name].append(value)
+    return {
+        name: {
+            "mean": float(np.mean(x)),
+            "std": float(np.std(x, ddof=1)) if len(x) > 1 else 0.0,
+        }
+        for name, x in vals.items()
+    }
+
+
+def retention_curves(gaps, alphas, n_trials, originals, baseline, rng):
+    rows = []
+    for alpha in alphas:
+        vals = {name: [] for name in OBSERVABLES}
+        for _ in range(n_trials):
+            s = partial_shuffle(gaps, float(alpha), rng)
+            row = measure(s)
+            for name, value in row.items():
+                vals[name].append(value)
+
+        out = {"alpha": float(alpha)}
+        for name in OBSERVABLES:
+            mean = float(np.mean(vals[name]))
+            denom = originals[name] - baseline[name]["mean"]
+            retention = (mean - baseline[name]["mean"]) / denom if abs(denom) > 1e-12 else 0.0
+            out[name] = {
+                "mean": mean,
+                "std": float(np.std(vals[name], ddof=1)) if len(vals[name]) > 1 else 0.0,
+                "retention": float(retention),
+            }
+        rows.append(out)
+    return rows
+
+
+def pca_summary(rows):
+    names = list(OBSERVABLES)
+    matrix = np.array([[row[name]["retention"] for name in names] for row in rows], dtype=float)
+    matrix = matrix - np.mean(matrix, axis=0, keepdims=True)
+
+    _, singular, vt = np.linalg.svd(matrix, full_matrices=False)
+    energy = singular * singular
+    if np.sum(energy) <= 1e-15:
+        explained = np.zeros_like(energy)
+        effective_rank = 0.0
+    else:
+        explained = energy / np.sum(energy)
+        positive = explained[explained > 1e-15]
+        effective_rank = float(np.exp(-np.sum(positive * np.log(positive))))
+
+    corr = np.corrcoef(matrix, rowvar=False)
+    abs_corr = np.abs(corr)
+    upper = abs_corr[np.triu_indices_from(abs_corr, k=1)]
+
+    return {
+        "observables": names,
+        "singular_values": [float(x) for x in singular],
+        "explained_variance": [float(x) for x in explained],
+        "effective_rank": effective_rank,
+        "pc1_loadings": {name: float(vt[0, i]) for i, name in enumerate(names)} if len(vt) else {},
+        "mean_abs_pairwise_corr": float(np.mean(upper)) if len(upper) else 0.0,
+        "min_abs_pairwise_corr": float(np.min(upper)) if len(upper) else 0.0,
+        "max_abs_pairwise_corr": float(np.max(upper)) if len(upper) else 0.0,
+    }
+
+
+def analyze_sequence(name, gaps, alphas, n_trials, n_baseline, rng):
+    originals = measure(gaps)
+    baseline = full_shuffle_baseline(gaps, n_baseline, rng)
+    rows = retention_curves(gaps, alphas, n_trials, originals, baseline, rng)
+
+    z = {}
+    for obs_name in OBSERVABLES:
+        std = baseline[obs_name]["std"]
+        z[obs_name] = float((originals[obs_name] - baseline[obs_name]["mean"]) / std) if std > 1e-12 else 0.0
+
+    return {
+        "n_gaps": int(len(gaps)),
+        "originals": originals,
+        "full_shuffle_baseline": baseline,
+        "original_vs_shuffle_z": z,
+        "retention_curves": rows,
+        "pca": pca_summary(rows),
+    }
+
+
+def build_sequences(n_gaps, rng):
+    primes = get_primes(n_gaps * 24)[: n_gaps + 1]
+    prime_gaps = np.diff(primes).astype(float)
+
+    gue = gue_gaps(160, max(8, n_gaps // 160 + 1), rng).astype(float)
+    gue = gue[:n_gaps]
+
+    poisson = rng.exponential(1.0, size=n_gaps).astype(float)
+    return {
+        "primes": prime_gaps,
+        "gue": gue,
+        "poisson": poisson,
+    }
+
+
+def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505):
+    rng = np.random.default_rng(seed)
+    alphas = np.linspace(0.05, 0.95, n_alpha)
+    sequences = build_sequences(n_gaps, rng)
+
+    output = {
+        "experiment": "observable_rank_audit",
+        "question": "Do crossover observables contain independent directions or one latent boundary coordinate?",
+        "params": {
+            "n_gaps": int(n_gaps),
+            "n_alpha": int(n_alpha),
+            "n_trials": int(n_trials),
+            "n_baseline": int(n_baseline),
+            "seed": int(seed),
+        },
+        "sequences": {},
+    }
+
+    print(f"n_gaps={n_gaps}, n_alpha={n_alpha}, n_trials={n_trials}, n_baseline={n_baseline}, seed={seed}")
+    print(f"{'sequence':<10} {'pc1':>8} {'eff_rank':>9} {'mean|corr|':>11}  z(SR,L1,L2,SR2,triple)")
+    print("-" * 92)
+    for seq_name, gaps in sequences.items():
+        result = analyze_sequence(seq_name, gaps, alphas, n_trials, n_baseline, rng)
+        output["sequences"][seq_name] = result
+        pca = result["pca"]
+        z = result["original_vs_shuffle_z"]
+        z_text = ", ".join(f"{obs}={z[obs]:+.1f}" for obs in OBSERVABLES)
+        pc1 = pca["explained_variance"][0] if pca["explained_variance"] else 0.0
+        print(f"{seq_name:<10} {pc1:>8.3f} {pca['effective_rank']:>9.3f} {pca['mean_abs_pairwise_corr']:>11.3f}  {z_text}")
+
+    out_path = Path("tools/data/observable_rank_audit.json")
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+    print(f"\nSaved to {out_path}")
+    return output
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n_gaps", type=int, default=30000)
+    parser.add_argument("--n_alpha", type=int, default=19)
+    parser.add_argument("--n_trials", type=int, default=24)
+    parser.add_argument("--n_baseline", type=int, default=72)
+    parser.add_argument("--seed", type=int, default=20260505)
+    args = parser.parse_args()
+    run(
+        n_gaps=args.n_gaps,
+        n_alpha=args.n_alpha,
+        n_trials=args.n_trials,
+        n_baseline=args.n_baseline,
+        seed=args.seed,
+    )
+
+
+if __name__ == "__main__":
+    main()

exec
/bin/bash -lc "rg -n '\"tensioni\"|\"potenziale_bloccato\"|META' tools/data/seme.json" in /opt/MM_D-ND
 succeeded in 0ms:
4:  "tensioni": [
83:      "id": "META",
92:  "potenziale_bloccato": [],
94:    "Tensioni risolte: {'DUALITA_DIPOLARE_VS_ILLUSORIA', 'METRIC_TENSOR', 'TENSIONE_ENTITA', 'TRASCENDENZA_LIMITE', 'G_POTENZIALE_NULLA', 'PIANO_PRIMARIO_DUE_ASSIOMI', 'META_LAYER_RECOVERY_AUDIT'}"

diff --git a/tools/exp_observable_rank_audit.py b/tools/exp_observable_rank_audit.py
new file mode 100644
index 0000000000000000000000000000000000000000..f2acafc5960c8a35964e0ab521cf70569eefe27f
--- /dev/null
+++ b/tools/exp_observable_rank_audit.py
@@ -0,0 +1,229 @@
+#!/usr/bin/env python3
+"""
+exp_observable_rank_audit.py - META audit for observable redundancy.
+
+Recent runs found many Markov/crossover observables that react coherently under
+partial shuffle. This script asks whether those observables carry independent
+directions or mostly re-measure one latent boundary coordinate.
+
+It measures retention curves from alpha-partial shuffles, then reports:
+  - original-vs-full-shuffle z for each observable
+  - PCA energy of the retention matrix across alpha
+  - effective rank of that matrix
+  - pairwise correlations between retention curves
+
+The script measures data only. The report decides the structural claim.
+"""
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from exp_3d_boundary_layers import get_primes, gue_gaps, partial_shuffle
+
+
+def obs_spacing_ratio(gaps):
+    r = np.minimum(gaps[:-1], gaps[1:]) / np.maximum(gaps[:-1], gaps[1:])
+    r = r[np.isfinite(r)]
+    return float(np.mean(r)) if len(r) else 0.0
+
+
+def obs_lag_acf(gaps, lag):
+    g = gaps - np.mean(gaps)
+    c0 = np.mean(g * g)
+    if c0 == 0:
+        return 0.0
+    return float(np.mean(g[:-lag] * g[lag:]) / c0)
+
+
+def obs_sr2(gaps):
+    r = np.minimum(gaps[:-2], gaps[2:]) / np.maximum(gaps[:-2], gaps[2:])
+    r = r[np.isfinite(r)]
+    return float(np.mean(r)) if len(r) else 0.0
+
+
+def obs_triple_var(gaps):
+    triples = gaps[:-2] + gaps[1:-1] + gaps[2:]
+    v = np.var(gaps)
+    if v == 0:
+        return 0.0
+    return float(np.var(triples) / v)
+
+
+OBSERVABLES = {
+    "SR": obs_spacing_ratio,
+    "L1": lambda gaps: obs_lag_acf(gaps, 1),
+    "L2": lambda gaps: obs_lag_acf(gaps, 2),
+    "SR2": obs_sr2,
+    "triple_var": obs_triple_var,
+}
+
+
+def measure(gaps):
+    return {name: fn(gaps) for name, fn in OBSERVABLES.items()}
+
+
+def full_shuffle_baseline(gaps, n_trials, rng):
+    vals = {name: [] for name in OBSERVABLES}
+    for _ in range(n_trials):
+        s = rng.permutation(gaps)
+        row = measure(s)
+        for name, value in row.items():
+            vals[name].append(value)
+    return {
+        name: {
+            "mean": float(np.mean(x)),
+            "std": float(np.std(x, ddof=1)) if len(x) > 1 else 0.0,
+        }
+        for name, x in vals.items()
+    }
+
+
+def retention_curves(gaps, alphas, n_trials, originals, baseline, rng):
+    rows = []
+    for alpha in alphas:
+        vals = {name: [] for name in OBSERVABLES}
+        for _ in range(n_trials):
+            s = partial_shuffle(gaps, float(alpha), rng)
+            row = measure(s)
+            for name, value in row.items():
+                vals[name].append(value)
+
+        out = {"alpha": float(alpha)}
+        for name in OBSERVABLES:
+            mean = float(np.mean(vals[name]))
+            denom = originals[name] - baseline[name]["mean"]
+            retention = (mean - baseline[name]["mean"]) / denom if abs(denom) > 1e-12 else 0.0
+            out[name] = {
+                "mean": mean,
+                "std": float(np.std(vals[name], ddof=1)) if len(vals[name]) > 1 else 0.0,
+                "retention": float(retention),
+            }
+        rows.append(out)
+    return rows
+
+
+def pca_summary(rows):
+    names = list(OBSERVABLES)
+    matrix = np.array([[row[name]["retention"] for name in names] for row in rows], dtype=float)
+    matrix = matrix - np.mean(matrix, axis=0, keepdims=True)
+
+    _, singular, vt = np.linalg.svd(matrix, full_matrices=False)
+    energy = singular * singular
+    if np.sum(energy) <= 1e-15:
+        explained = np.zeros_like(energy)
+        effective_rank = 0.0
+    else:
+        explained = energy / np.sum(energy)
+        positive = explained[explained > 1e-15]
+        effective_rank = float(np.exp(-np.sum(positive * np.log(positive))))
+
+    corr = np.corrcoef(matrix, rowvar=False)
+    abs_corr = np.abs(corr)
+    upper = abs_corr[np.triu_indices_from(abs_corr, k=1)]
+
+    return {
+        "observables": names,
+        "singular_values": [float(x) for x in singular],
+        "explained_variance": [float(x) for x in explained],
+        "effective_rank": effective_rank,
+        "pc1_loadings": {name: float(vt[0, i]) for i, name in enumerate(names)} if len(vt) else {},
+        "mean_abs_pairwise_corr": float(np.mean(upper)) if len(upper) else 0.0,
+        "min_abs_pairwise_corr": float(np.min(upper)) if len(upper) else 0.0,
+        "max_abs_pairwise_corr": float(np.max(upper)) if len(upper) else 0.0,
+    }
+
+
+def analyze_sequence(name, gaps, alphas, n_trials, n_baseline, rng):
+    originals = measure(gaps)
+    baseline = full_shuffle_baseline(gaps, n_baseline, rng)
+    rows = retention_curves(gaps, alphas, n_trials, originals, baseline, rng)
+
+    z = {}
+    for obs_name in OBSERVABLES:
+        std = baseline[obs_name]["std"]
+        z[obs_name] = float((originals[obs_name] - baseline[obs_name]["mean"]) / std) if std > 1e-12 else 0.0
+
+    return {
+        "n_gaps": int(len(gaps)),
+        "originals": originals,
+        "full_shuffle_baseline": baseline,
+        "original_vs_shuffle_z": z,
+        "retention_curves": rows,
+        "pca": pca_summary(rows),
+    }
+
+
+def build_sequences(n_gaps, rng):
+    primes = get_primes(n_gaps * 24)[: n_gaps + 1]
+    prime_gaps = np.diff(primes).astype(float)
+
+    gue = gue_gaps(160, max(8, n_gaps // 160 + 1), rng).astype(float)
+    gue = gue[:n_gaps]
+
+    poisson = rng.exponential(1.0, size=n_gaps).astype(float)
+    return {
+        "primes": prime_gaps,
+        "gue": gue,
+        "poisson": poisson,
+    }
+
+
+def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505):
+    rng = np.random.default_rng(seed)
+    alphas = np.linspace(0.05, 0.95, n_alpha)
+    sequences = build_sequences(n_gaps, rng)
+
+    output = {
+        "experiment": "observable_rank_audit",
+        "question": "Do crossover observables contain independent directions or one latent boundary coordinate?",
+        "params": {
+            "n_gaps": int(n_gaps),
+            "n_alpha": int(n_alpha),
+            "n_trials": int(n_trials),
+            "n_baseline": int(n_baseline),
+            "seed": int(seed),
+        },
+        "sequences": {},
+    }
+
+    print(f"n_gaps={n_gaps}, n_alpha={n_alpha}, n_trials={n_trials}, n_baseline={n_baseline}, seed={seed}")
+    print(f"{'sequence':<10} {'pc1':>8} {'eff_rank':>9} {'mean|corr|':>11}  z(SR,L1,L2,SR2,triple)")
+    print("-" * 92)
+    for seq_name, gaps in sequences.items():
+        result = analyze_sequence(seq_name, gaps, alphas, n_trials, n_baseline, rng)
+        output["sequences"][seq_name] = result
+        pca = result["pca"]
+        z = result["original_vs_shuffle_z"]
+        z_text = ", ".join(f"{obs}={z[obs]:+.1f}" for obs in OBSERVABLES)
+        pc1 = pca["explained_variance"][0] if pca["explained_variance"] else 0.0
+        print(f"{seq_name:<10} {pc1:>8.3f} {pca['effective_rank']:>9.3f} {pca['mean_abs_pairwise_corr']:>11.3f}  {z_text}")
+
+    out_path = Path("tools/data/observable_rank_audit.json")
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+    print(f"\nSaved to {out_path}")
+    return output
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n_gaps", type=int, default=30000)
+    parser.add_argument("--n_alpha", type=int, default=19)
+    parser.add_argument("--n_trials", type=int, default=24)
+    parser.add_argument("--n_baseline", type=int, default=72)
+    parser.add_argument("--seed", type=int, default=20260505)
+    args = parser.parse_args()
+    run(
+        n_gaps=args.n_gaps,
+        n_alpha=args.n_alpha,
+        n_trials=args.n_trials,
+        n_baseline=args.n_baseline,
+        seed=args.seed,
+    )
+
+
+if __name__ == "__main__":
+    main()

exec
/bin/bash -lc "sed -n '70,105p' tools/data/seme.json" in /opt/MM_D-ND
 succeeded in 0ms:
    {
      "tipo": "confine_inesplorato",
      "id": "PIANO_PRIMARIO_DUE_ASSIOMI",
      "claim": "I piani importanti sono il primario e i due assiomi che lo determinano nelle zone osservate. Non tutti gli assiomi operano ovunque - in ogni zona osservata, due assiomi determinano il piano primario.",
      "intensita": 0.8,
      "nota": "Input operatore 2026-04-10. Tocca: struttura locale degli assiomi. Consecutio: per ogni dominio Lab (primi, logistica, percolazione...) quali 2 assiomi del condensato sono operativi? Mappa assiomi x domini = grafo della realta locale.",
      "manuale": true,
      "porta": "sessione_interattiva",
      "condensato_ref": "A9,A14",
      "condensato_motivo": "A9 (terzo incluso) opera CON il piano. A14 (cascata) propaga - ma propaga cosa, se solo 2 assiomi sono attivi per zona?"
    },
    {
      "tipo": "simmetria_sospetta",
      "id": "META",
      "claim": "Tutti i 11 test passano — verifica che non stiamo testando solo tautologie",
      "intensità": 0.5,
      "nota": "La convergenza a φ è triviale (controprove). I test stanno verificando contenuto o struttura?",
      "condensato_ref": null,
      "condensato_motivo": "Ricorrente (3x in 2 giorni) e fuori dalla mappa",
      "porta": "novità"
    }
  ],
  "potenziale_bloccato": [],
  "varianza": [
    "Tensioni risolte: {'DUALITA_DIPOLARE_VS_ILLUSORIA', 'METRIC_TENSOR', 'TENSIONE_ENTITA', 'TRASCENDENZA_LIMITE', 'G_POTENZIALE_NULLA', 'PIANO_PRIMARIO_DUE_ASSIOMI', 'META_LAYER_RECOVERY_AUDIT'}"
  ],
  "filtro": {
    "promosse": 8,
    "filtrate": 0,
    "condensato_claims": 30
  },
  "direzione": "Esplorare il confine: 8 domini GUE, 5 Poisson — il confine è il terzo incluso operativo",
  "verifica": {
    "pass": 11,
    "fail": 0,
    "skip": 0,

diff --git a/tools/exp_observable_rank_audit.py b/tools/exp_observable_rank_audit.py
new file mode 100644
index 0000000000000000000000000000000000000000..f2acafc5960c8a35964e0ab521cf70569eefe27f
--- /dev/null
+++ b/tools/exp_observable_rank_audit.py
@@ -0,0 +1,229 @@
+#!/usr/bin/env python3
+"""
+exp_observable_rank_audit.py - META audit for observable redundancy.
+
+Recent runs found many Markov/crossover observables that react coherently under
+partial shuffle. This script asks whether those observables carry independent
+directions or mostly re-measure one latent boundary coordinate.
+
+It measures retention curves from alpha-partial shuffles, then reports:
+  - original-vs-full-shuffle z for each observable
+  - PCA energy of the retention matrix across alpha
+  - effective rank of that matrix
+  - pairwise correlations between retention curves
+
+The script measures data only. The report decides the structural claim.
+"""
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from exp_3d_boundary_layers import get_primes, gue_gaps, partial_shuffle
+
+
+def obs_spacing_ratio(gaps):
+    r = np.minimum(gaps[:-1], gaps[1:]) / np.maximum(gaps[:-1], gaps[1:])
+    r = r[np.isfinite(r)]
+    return float(np.mean(r)) if len(r) else 0.0
+
+
+def obs_lag_acf(gaps, lag):
+    g = gaps - np.mean(gaps)
+    c0 = np.mean(g * g)
+    if c0 == 0:
+        return 0.0
+    return float(np.mean(g[:-lag] * g[lag:]) / c0)
+
+
+def obs_sr2(gaps):
+    r = np.minimum(gaps[:-2], gaps[2:]) / np.maximum(gaps[:-2], gaps[2:])
+    r = r[np.isfinite(r)]
+    return float(np.mean(r)) if len(r) else 0.0
+
+
+def obs_triple_var(gaps):
+    triples = gaps[:-2] + gaps[1:-1] + gaps[2:]
+    v = np.var(gaps)
+    if v == 0:
+        return 0.0
+    return float(np.var(triples) / v)
+
+
+OBSERVABLES = {
+    "SR": obs_spacing_ratio,
+    "L1": lambda gaps: obs_lag_acf(gaps, 1),
+    "L2": lambda gaps: obs_lag_acf(gaps, 2),
+    "SR2": obs_sr2,
+    "triple_var": obs_triple_var,
+}
+
+
+def measure(gaps):
+    return {name: fn(gaps) for name, fn in OBSERVABLES.items()}
+
+
+def full_shuffle_baseline(gaps, n_trials, rng):
+    vals = {name: [] for name in OBSERVABLES}
+    for _ in range(n_trials):
+        s = rng.permutation(gaps)
+        row = measure(s)
+        for name, value in row.items():
+            vals[name].append(value)
+    return {
+        name: {
+            "mean": float(np.mean(x)),
+            "std": float(np.std(x, ddof=1)) if len(x) > 1 else 0.0,
+        }
+        for name, x in vals.items()
+    }
+
+
+def retention_curves(gaps, alphas, n_trials, originals, baseline, rng):
+    rows = []
+    for alpha in alphas:
+        vals = {name: [] for name in OBSERVABLES}
+        for _ in range(n_trials):
+            s = partial_shuffle(gaps, float(alpha), rng)
+            row = measure(s)
+            for name, value in row.items():
+                vals[name].append(value)
+
+        out = {"alpha": float(alpha)}
+        for name in OBSERVABLES:
+            mean = float(np.mean(vals[name]))
+            denom = originals[name] - baseline[name]["mean"]
+            retention = (mean - baseline[name]["mean"]) / denom if abs(denom) > 1e-12 else 0.0
+            out[name] = {
+                "mean": mean,
+                "std": float(np.std(vals[name], ddof=1)) if len(vals[name]) > 1 else 0.0,
+                "retention": float(retention),
+            }
+        rows.append(out)
+    return rows
+
+
+def pca_summary(rows):
+    names = list(OBSERVABLES)
+    matrix = np.array([[row[name]["retention"] for name in names] for row in rows], dtype=float)
+    matrix = matrix - np.mean(matrix, axis=0, keepdims=True)
+
+    _, singular, vt = np.linalg.svd(matrix, full_matrices=False)
+    energy = singular * singular
+    if np.sum(energy) <= 1e-15:
+        explained = np.zeros_like(energy)
+        effective_rank = 0.0
+    else:
+        explained = energy / np.sum(energy)
+        positive = explained[explained > 1e-15]
+        effective_rank = float(np.exp(-np.sum(positive * np.log(positive))))
+
+    corr = np.corrcoef(matrix, rowvar=False)
+    abs_corr = np.abs(corr)
+    upper = abs_corr[np.triu_indices_from(abs_corr, k=1)]
+
+    return {
+        "observables": names,
+        "singular_values": [float(x) for x in singular],
+        "explained_variance": [float(x) for x in explained],
+        "effective_rank": effective_rank,
+        "pc1_loadings": {name: float(vt[0, i]) for i, name in enumerate(names)} if len(vt) else {},
+        "mean_abs_pairwise_corr": float(np.mean(upper)) if len(upper) else 0.0,
+        "min_abs_pairwise_corr": float(np.min(upper)) if len(upper) else 0.0,
+        "max_abs_pairwise_corr": float(np.max(upper)) if len(upper) else 0.0,
+    }
+
+
+def analyze_sequence(name, gaps, alphas, n_trials, n_baseline, rng):
+    originals = measure(gaps)
+    baseline = full_shuffle_baseline(gaps, n_baseline, rng)
+    rows = retention_curves(gaps, alphas, n_trials, originals, baseline, rng)
+
+    z = {}
+    for obs_name in OBSERVABLES:
+        std = baseline[obs_name]["std"]
+        z[obs_name] = float((originals[obs_name] - baseline[obs_name]["mean"]) / std) if std > 1e-12 else 0.0
+
+    return {
+        "n_gaps": int(len(gaps)),
+        "originals": originals,
+        "full_shuffle_baseline": baseline,
+        "original_vs_shuffle_z": z,
+        "retention_curves": rows,
+        "pca": pca_summary(rows),
+    }
+
+
+def build_sequences(n_gaps, rng):
+    primes = get_primes(n_gaps * 24)[: n_gaps + 1]
+    prime_gaps = np.diff(primes).astype(float)
+
+    gue = gue_gaps(160, max(8, n_gaps // 160 + 1), rng).astype(float)
+    gue = gue[:n_gaps]
+
+    poisson = rng.exponential(1.0, size=n_gaps).astype(float)
+    return {
+        "primes": prime_gaps,
+        "gue": gue,
+        "poisson": poisson,
+    }
+
+
+def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505):
+    rng = np.random.default_rng(seed)
+    alphas = np.linspace(0.05, 0.95, n_alpha)
+    sequences = build_sequences(n_gaps, rng)
+
+    output = {
+        "experiment": "observable_rank_audit",
+        "question": "Do crossover observables contain independent directions or one latent boundary coordinate?",
+        "params": {
+            "n_gaps": int(n_gaps),
+            "n_alpha": int(n_alpha),
+            "n_trials": int(n_trials),
+            "n_baseline": int(n_baseline),
+            "seed": int(seed),
+        },
+        "sequences": {},
+    }
+
+    print(f"n_gaps={n_gaps}, n_alpha={n_alpha}, n_trials={n_trials}, n_baseline={n_baseline}, seed={seed}")
+    print(f"{'sequence':<10} {'pc1':>8} {'eff_rank':>9} {'mean|corr|':>11}  z(SR,L1,L2,SR2,triple)")
+    print("-" * 92)
+    for seq_name, gaps in sequences.items():
+        result = analyze_sequence(seq_name, gaps, alphas, n_trials, n_baseline, rng)
+        output["sequences"][seq_name] = result
+        pca = result["pca"]
+        z = result["original_vs_shuffle_z"]
+        z_text = ", ".join(f"{obs}={z[obs]:+.1f}" for obs in OBSERVABLES)
+        pc1 = pca["explained_variance"][0] if pca["explained_variance"] else 0.0
+        print(f"{seq_name:<10} {pc1:>8.3f} {pca['effective_rank']:>9.3f} {pca['mean_abs_pairwise_corr']:>11.3f}  {z_text}")
+
+    out_path = Path("tools/data/observable_rank_audit.json")
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+    print(f"\nSaved to {out_path}")
+    return output
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n_gaps", type=int, default=30000)
+    parser.add_argument("--n_alpha", type=int, default=19)
+    parser.add_argument("--n_trials", type=int, default=24)
+    parser.add_argument("--n_baseline", type=int, default=72)
+    parser.add_argument("--seed", type=int, default=20260505)
+    args = parser.parse_args()
+    run(
+        n_gaps=args.n_gaps,
+        n_alpha=args.n_alpha,
+        n_trials=args.n_trials,
+        n_baseline=args.n_baseline,
+        seed=args.seed,
+    )
+
+
+if __name__ == "__main__":
+    main()

codex
I numeri hanno una forma chiara: le osservabili non sono falsificate, ma la loro indipendenza sì nel perimetro partial-shuffle. Scrivo il report come vincolo META+BOUNDARY, non come scoperta nuova universale, e aggiorno il seme con la consecutio operativa.
apply patch
patch: completed
/opt/MM_D-ND/tools/data/reports/agent_20260505_0330.md
diff --git a/tools/data/reports/agent_20260505_0330.md b/tools/data/reports/agent_20260505_0330.md
new file mode 100644
index 0000000000000000000000000000000000000000..a2bffd69f55a8117fc1ca2f1167d2992e3247c9e
--- /dev/null
+++ b/tools/data/reports/agent_20260505_0330.md
@@ -0,0 +1,73 @@
+# Agent Report - Observable Rank Audit: Many Probes, One Boundary Coordinate
+**Date**: 2026-05-05 03:30
+**Piano**: 62
+**Tension explored**: META (0.5) + BOUNDARY (0.8)
+
+## Claim Under Test
+> Tutti i 11 test passano - verifica che non stiamo testando solo tautologie.
+
+## Question
+Quando SR, L1, L2, SR2 e triple_var reagiscono al partial shuffle, stanno misurando direzioni indipendenti del confine o una sola coordinata latente vista da osservabili diverse?
+
+## Experiment Design
+- **Scope atomico**: 30,000 gap per dominio; alphas partial-shuffle = 19 punti da 0.05 a 0.95; 24 trial per alpha; 72 full-shuffle per baseline.
+- **Domini**: prime gaps, GUE gaps, Poisson iid exponential gaps.
+- **Osservabili**: SR, L1, L2, SR2, triple_var.
+- **Null baseline**: full shuffle della stessa sequenza. Il dato Poisson e' controllo di assenza di segnale originale-vs-shuffle.
+- **Metrica META**: matrice delle retention curve normalizzate rispetto al full shuffle; PCA sulla matrice alpha x osservabili; rango effettivo entropico delle energie singolari.
+- **Robustezza minima**: seed principale 20260505; replica seed 20260506 salvata separatamente.
+
+## Results
+Seed principale 20260505:
+
+| Domain | PC1 variance | Effective rank | mean abs corr | z SR | z L1 | z L2 | z SR2 | z triple_var |
+|---|---:|---:|---:|---:|---:|---:|---:|---:|
+| Primes | 0.989 | 1.069 | 0.987 | -19.3 | -7.7 | -3.0 | -3.7 | -8.0 |
+| GUE | 0.997 | 1.022 | 0.996 | -6.6 | +21.4 | +37.9 | +18.0 | +36.3 |
+| Poisson | 0.925 | 1.391 | 0.351 | -0.6 | +0.2 | +1.2 | -0.0 | +0.8 |
+
+Replica seed 20260506:
+
+| Domain | PC1 variance | Effective rank | mean abs corr |
+|---|---:|---:|---:|
+| Primes | 0.988 | 1.076 | 0.985 |
+| GUE | 0.997 | 1.019 | 0.997 |
+| Poisson | 0.675 | 2.394 | 0.640 |
+
+PC1 loadings nel seed principale:
+
+| Domain | SR | L1 | L2 | SR2 | triple_var |
+|---|---:|---:|---:|---:|---:|
+| Primes | -0.450 | -0.449 | -0.443 | -0.446 | -0.448 |
+| GUE | -0.446 | -0.449 | -0.447 | -0.446 | -0.448 |
+
+## Key Findings
+1. **Nel perimetro partial-shuffle, primes e GUE hanno segnale forte ma quasi monodimensionale.** Per primes, tutte le osservabili hanno z originale-vs-shuffle almeno |3.0| e la prima componente spiega 98.9% della varianza delle retention curve. Per GUE il collasso e' ancora piu' stretto: 99.7%.
+
+2. **La somiglianza dei loadings e' il dato operativo.** Nei domini strutturati, PC1 carica SR, L1, L2, SR2 e triple_var quasi uniformemente. Questo non dice che le osservabili siano identiche in generale; dice che sotto partial shuffle uniforme misurano soprattutto la stessa coordinata di distruzione dell'ordine.
+
+3. **Poisson non supporta un claim di rango.** Nel seed principale Poisson ha PC1 alto, ma tutti gli z originale-vs-shuffle sono sotto |1.2|; nella replica il rango cambia molto. Quindi il rango Poisson qui e' rumore di baseline, non struttura.
+
+4. **Il risultato restringe il linguaggio dei cicli precedenti.** I test layer/Markov restano utili per classificare sensibilita' locali, ma non vanno contati come prove indipendenti del confine quando sono misurati lungo la stessa perturbazione partial-shuffle.
+
+## Verdict
+**CONSTRAINT on META + BOUNDARY**: nel perimetro testato, il confine partial-shuffle ha una coordinata latente dominante. Le osservabili SR/L1/L2/SR2/triple_var sono probes validi, ma non cinque evidenze indipendenti della transizione. La consecutio corretta e' cercare perturbazioni selettive ortogonali, non aggiungere altri osservabili scalari sulla stessa curva.
+
+## Bicono della scoperta
+- **Due radici**: molte osservabili che passano il test; una coordinata latente che le assorbe sotto partial shuffle.
+- **Singolare**: la curva di distruzione dell'ordine prima della scelta dell'osservabile.
+- **Invariante di passaggio**: nei domini strutturati, la retention resta quasi collineare tra osservabili anche quando il segno z e la magnitudine assoluta differiscono.
+- **Campo di possibilita**: qui diventa possibile separare "probe valido" da "evidenza indipendente"; qui diventa non-possibile contare SR, L1, L2, SR2 e triple_var come cinque conferme autonome del medesimo confine partial-shuffle.
+
+## Auto-audit: 5 lenti
+- **L1 hard constraint vs bias**: nessun claim di zero, sempre, never, impossibile. Il claim e' limitato a PC1/rango nel perimetro misurato.
+- **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.
+- **L3 no silent patching**: il claim META originale non e' dichiarato "risolto"; e' ristretto. I test possono essere validi ma ridondanti lungo partial shuffle.
+- **L4 edge cases**: L2 primes ha z=-3.0, piu' debole degli altri; non viene usato come prova forte isolata.
+- **L5 re-discovery**: PCA/rango effettivo sono strumenti classici di ridondanza statistica. Non taggo NEW; il contributo e' il vincolo operativo sul perimetro D-ND Markov/boundary.
+
+## Files
+- Script: `tools/exp_observable_rank_audit.py`
+- Data principale: `tools/data/observable_rank_audit.json`
+- Replica: `tools/data/observable_rank_audit_seed20260506.json`
+- Report: `tools/data/reports/agent_20260505_0330.md`
diff --git a/tools/exp_observable_rank_audit.py b/tools/exp_observable_rank_audit.py
new file mode 100644
index 0000000000000000000000000000000000000000..f2acafc5960c8a35964e0ab521cf70569eefe27f
--- /dev/null
+++ b/tools/exp_observable_rank_audit.py
@@ -0,0 +1,229 @@
+#!/usr/bin/env python3
+"""
+exp_observable_rank_audit.py - META audit for observable redundancy.
+
+Recent runs found many Markov/crossover observables that react coherently under
+partial shuffle. This script asks whether those observables carry independent
+directions or mostly re-measure one latent boundary coordinate.
+
+It measures retention curves from alpha-partial shuffles, then reports:
+  - original-vs-full-shuffle z for each observable
+  - PCA energy of the retention matrix across alpha
+  - effective rank of that matrix
+  - pairwise correlations between retention curves
+
+The script measures data only. The report decides the structural claim.
+"""
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from exp_3d_boundary_layers import get_primes, gue_gaps, partial_shuffle
+
+
+def obs_spacing_ratio(gaps):
+    r = np.minimum(gaps[:-1], gaps[1:]) / np.maximum(gaps[:-1], gaps[1:])
+    r = r[np.isfinite(r)]
+    return float(np.mean(r)) if len(r) else 0.0
+
+
+def obs_lag_acf(gaps, lag):
+    g = gaps - np.mean(gaps)
+    c0 = np.mean(g * g)
+    if c0 == 0:
+        return 0.0
+    return float(np.mean(g[:-lag] * g[lag:]) / c0)
+
+
+def obs_sr2(gaps):
+    r = np.minimum(gaps[:-2], gaps[2:]) / np.maximum(gaps[:-2], gaps[2:])
+    r = r[np.isfinite(r)]
+    return float(np.mean(r)) if len(r) else 0.0
+
+
+def obs_triple_var(gaps):
+    triples = gaps[:-2] + gaps[1:-1] + gaps[2:]
+    v = np.var(gaps)
+    if v == 0:
+        return 0.0
+    return float(np.var(triples) / v)
+
+
+OBSERVABLES = {
+    "SR": obs_spacing_ratio,
+    "L1": lambda gaps: obs_lag_acf(gaps, 1),
+    "L2": lambda gaps: obs_lag_acf(gaps, 2),
+    "SR2": obs_sr2,
+    "triple_var": obs_triple_var,
+}
+
+
+def measure(gaps):
+    return {name: fn(gaps) for name, fn in OBSERVABLES.items()}
+
+
+def full_shuffle_baseline(gaps, n_trials, rng):
+    vals = {name: [] for name in OBSERVABLES}
+    for _ in range(n_trials):
+        s = rng.permutation(gaps)
+        row = measure(s)
+        for name, value in row.items():
+            vals[name].append(value)
+    return {
+        name: {
+            "mean": float(np.mean(x)),
+            "std": float(np.std(x, ddof=1)) if len(x) > 1 else 0.0,
+        }
+        for name, x in vals.items()
+    }
+
+
+def retention_curves(gaps, alphas, n_trials, originals, baseline, rng):
+    rows = []
+    for alpha in alphas:
+        vals = {name: [] for name in OBSERVABLES}
+        for _ in range(n_trials):
+            s = partial_shuffle(gaps, float(alpha), rng)
+            row = measure(s)
+            for name, value in row.items():
+                vals[name].append(value)
+
+        out = {"alpha": float(alpha)}
+        for name in OBSERVABLES:
+            mean = float(np.mean(vals[name]))
+            denom = originals[name] - baseline[name]["mean"]
+            retention = (mean - baseline[name]["mean"]) / denom if abs(denom) > 1e-12 else 0.0
+            out[name] = {
+                "mean": mean,
+                "std": float(np.std(vals[name], ddof=1)) if len(vals[name]) > 1 else 0.0,
+                "retention": float(retention),
+            }
+        rows.append(out)
+    return rows
+
+
+def pca_summary(rows):
+    names = list(OBSERVABLES)
+    matrix = np.array([[row[name]["retention"] for name in names] for row in rows], dtype=float)
+    matrix = matrix - np.mean(matrix, axis=0, keepdims=True)
+
+    _, singular, vt = np.linalg.svd(matrix, full_matrices=False)
+    energy = singular * singular
+    if np.sum(energy) <= 1e-15:
+        explained = np.zeros_like(energy)
+        effective_rank = 0.0
+    else:
+        explained = energy / np.sum(energy)
+        positive = explained[explained > 1e-15]
+        effective_rank = float(np.exp(-np.sum(positive * np.log(positive))))
+
+    corr = np.corrcoef(matrix, rowvar=False)
+    abs_corr = np.abs(corr)
+    upper = abs_corr[np.triu_indices_from(abs_corr, k=1)]
+
+    return {
+        "observables": names,
+        "singular_values": [float(x) for x in singular],
+        "explained_variance": [float(x) for x in explained],
+        "effective_rank": effective_rank,
+        "pc1_loadings": {name: float(vt[0, i]) for i, name in enumerate(names)} if len(vt) else {},
+        "mean_abs_pairwise_corr": float(np.mean(upper)) if len(upper) else 0.0,
+        "min_abs_pairwise_corr": float(np.min(upper)) if len(upper) else 0.0,
+        "max_abs_pairwise_corr": float(np.max(upper)) if len(upper) else 0.0,
+    }
+
+
+def analyze_sequence(name, gaps, alphas, n_trials, n_baseline, rng):
+    originals = measure(gaps)
+    baseline = full_shuffle_baseline(gaps, n_baseline, rng)
+    rows = retention_curves(gaps, alphas, n_trials, originals, baseline, rng)
+
+    z = {}
+    for obs_name in OBSERVABLES:
+        std = baseline[obs_name]["std"]
+        z[obs_name] = float((originals[obs_name] - baseline[obs_name]["mean"]) / std) if std > 1e-12 else 0.0
+
+    return {
+        "n_gaps": int(len(gaps)),
+        "originals": originals,
+        "full_shuffle_baseline": baseline,
+        "original_vs_shuffle_z": z,
+        "retention_curves": rows,
+        "pca": pca_summary(rows),
+    }
+
+
+def build_sequences(n_gaps, rng):
+    primes = get_primes(n_gaps * 24)[: n_gaps + 1]
+    prime_gaps = np.diff(primes).astype(float)
+
+    gue = gue_gaps(160, max(8, n_gaps // 160 + 1), rng).astype(float)
+    gue = gue[:n_gaps]
+
+    poisson = rng.exponential(1.0, size=n_gaps).astype(float)
+    return {
+        "primes": prime_gaps,
+        "gue": gue,
+        "poisson": poisson,
+    }
+
+
+def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505):
+    rng = np.random.default_rng(seed)
+    alphas = np.linspace(0.05, 0.95, n_alpha)
+    sequences = build_sequences(n_gaps, rng)
+
+    output = {
+        "experiment": "observable_rank_audit",
+        "question": "Do crossover observables contain independent directions or one latent boundary coordinate?",
+        "params": {
+            "n_gaps": int(n_gaps),
+            "n_alpha": int(n_alpha),
+            "n_trials": int(n_trials),
+            "n_baseline": int(n_baseline),
+            "seed": int(seed),
+        },
+        "sequences": {},
+    }
+
+    print(f"n_gaps={n_gaps}, n_alpha={n_alpha}, n_trials={n_trials}, n_baseline={n_baseline}, seed={seed}")
+    print(f"{'sequence':<10} {'pc1':>8} {'eff_rank':>9} {'mean|corr|':>11}  z(SR,L1,L2,SR2,triple)")
+    print("-" * 92)
+    for seq_name, gaps in sequences.items():
+        result = analyze_sequence(seq_name, gaps, alphas, n_trials, n_baseline, rng)
+        output["sequences"][seq_name] = result
+        pca = result["pca"]
+        z = result["original_vs_shuffle_z"]
+        z_text = ", ".join(f"{obs}={z[obs]:+.1f}" for obs in OBSERVABLES)
+        pc1 = pca["explained_variance"][0] if pca["explained_variance"] else 0.0
+        print(f"{seq_name:<10} {pc1:>8.3f} {pca['effective_rank']:>9.3f} {pca['mean_abs_pairwise_corr']:>11.3f}  {z_text}")
+
+    out_path = Path("tools/data/observable_rank_audit.json")
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+    print(f"\nSaved to {out_path}")
+    return output
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n_gaps", type=int, default=30000)
+    parser.add_argument("--n_alpha", type=int, default=19)
+    parser.add_argument("--n_trials", type=int, default=24)
+    parser.add_argument("--n_baseline", type=int, default=72)
+    parser.add_argument("--seed", type=int, default=20260505)
+    args = parser.parse_args()
+    run(
+        n_gaps=args.n_gaps,
+        n_alpha=args.n_alpha,
+        n_trials=args.n_trials,
+        n_baseline=args.n_baseline,
+        seed=args.seed,
+    )
+
+
+if __name__ == "__main__":
+    main()

diff --git a/tools/data/reports/agent_20260505_0330.md b/tools/data/reports/agent_20260505_0330.md
new file mode 100644
index 0000000000000000000000000000000000000000..a2bffd69f55a8117fc1ca2f1167d2992e3247c9e
--- /dev/null
+++ b/tools/data/reports/agent_20260505_0330.md
@@ -0,0 +1,73 @@
+# Agent Report - Observable Rank Audit: Many Probes, One Boundary Coordinate
+**Date**: 2026-05-05 03:30
+**Piano**: 62
+**Tension explored**: META (0.5) + BOUNDARY (0.8)
+
+## Claim Under Test
+> Tutti i 11 test passano - verifica che non stiamo testando solo tautologie.
+
+## Question
+Quando SR, L1, L2, SR2 e triple_var reagiscono al partial shuffle, stanno misurando direzioni indipendenti del confine o una sola coordinata latente vista da osservabili diverse?
+
+## Experiment Design
+- **Scope atomico**: 30,000 gap per dominio; alphas partial-shuffle = 19 punti da 0.05 a 0.95; 24 trial per alpha; 72 full-shuffle per baseline.
+- **Domini**: prime gaps, GUE gaps, Poisson iid exponential gaps.
+- **Osservabili**: SR, L1, L2, SR2, triple_var.
+- **Null baseline**: full shuffle della stessa sequenza. Il dato Poisson e' controllo di assenza di segnale originale-vs-shuffle.
+- **Metrica META**: matrice delle retention curve normalizzate rispetto al full shuffle; PCA sulla matrice alpha x osservabili; rango effettivo entropico delle energie singolari.
+- **Robustezza minima**: seed principale 20260505; replica seed 20260506 salvata separatamente.
+
+## Results
+Seed principale 20260505:
+
+| Domain | PC1 variance | Effective rank | mean abs corr | z SR | z L1 | z L2 | z SR2 | z triple_var |
+|---|---:|---:|---:|---:|---:|---:|---:|---:|
+| Primes | 0.989 | 1.069 | 0.987 | -19.3 | -7.7 | -3.0 | -3.7 | -8.0 |
+| GUE | 0.997 | 1.022 | 0.996 | -6.6 | +21.4 | +37.9 | +18.0 | +36.3 |
+| Poisson | 0.925 | 1.391 | 0.351 | -0.6 | +0.2 | +1.2 | -0.0 | +0.8 |
+
+Replica seed 20260506:
+
+| Domain | PC1 variance | Effective rank | mean abs corr |
+|---|---:|---:|---:|
+| Primes | 0.988 | 1.076 | 0.985 |
+| GUE | 0.997 | 1.019 | 0.997 |
+| Poisson | 0.675 | 2.394 | 0.640 |
+
+PC1 loadings nel seed principale:
+
+| Domain | SR | L1 | L2 | SR2 | triple_var |
+|---|---:|---:|---:|---:|---:|
+| Primes | -0.450 | -0.449 | -0.443 | -0.446 | -0.448 |
+| GUE | -0.446 | -0.449 | -0.447 | -0.446 | -0.448 |
+
+## Key Findings
+1. **Nel perimetro partial-shuffle, primes e GUE hanno segnale forte ma quasi monodimensionale.** Per primes, tutte le osservabili hanno z originale-vs-shuffle almeno |3.0| e la prima componente spiega 98.9% della varianza delle retention curve. Per GUE il collasso e' ancora piu' stretto: 99.7%.
+
+2. **La somiglianza dei loadings e' il dato operativo.** Nei domini strutturati, PC1 carica SR, L1, L2, SR2 e triple_var quasi uniformemente. Questo non dice che le osservabili siano identiche in generale; dice che sotto partial shuffle uniforme misurano soprattutto la stessa coordinata di distruzione dell'ordine.
+
+3. **Poisson non supporta un claim di rango.** Nel seed principale Poisson ha PC1 alto, ma tutti gli z originale-vs-shuffle sono sotto |1.2|; nella replica il rango cambia molto. Quindi il rango Poisson qui e' rumore di baseline, non struttura.
+
+4. **Il risultato restringe il linguaggio dei cicli precedenti.** I test layer/Markov restano utili per classificare sensibilita' locali, ma non vanno contati come prove indipendenti del confine quando sono misurati lungo la stessa perturbazione partial-shuffle.
+
+## Verdict
+**CONSTRAINT on META + BOUNDARY**: nel perimetro testato, il confine partial-shuffle ha una coordinata latente dominante. Le osservabili SR/L1/L2/SR2/triple_var sono probes validi, ma non cinque evidenze indipendenti della transizione. La consecutio corretta e' cercare perturbazioni selettive ortogonali, non aggiungere altri osservabili scalari sulla stessa curva.
+
+## Bicono della scoperta
+- **Due radici**: molte osservabili che passano il test; una coordinata latente che le assorbe sotto partial shuffle.
+- **Singolare**: la curva di distruzione dell'ordine prima della scelta dell'osservabile.
+- **Invariante di passaggio**: nei domini strutturati, la retention resta quasi collineare tra osservabili anche quando il segno z e la magnitudine assoluta differiscono.
+- **Campo di possibilita**: qui diventa possibile separare "probe valido" da "evidenza indipendente"; qui diventa non-possibile contare SR, L1, L2, SR2 e triple_var come cinque conferme autonome del medesimo confine partial-shuffle.
+
+## Auto-audit: 5 lenti
+- **L1 hard constraint vs bias**: nessun claim di zero, sempre, never, impossibile. Il claim e' limitato a PC1/rango nel perimetro misurato.
+- **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.
+- **L3 no silent patching**: il claim META originale non e' dichiarato "risolto"; e' ristretto. I test possono essere validi ma ridondanti lungo partial shuffle.
+- **L4 edge cases**: L2 primes ha z=-3.0, piu' debole degli altri; non viene usato come prova forte isolata.
+- **L5 re-discovery**: PCA/rango effettivo sono strumenti classici di ridondanza statistica. Non taggo NEW; il contributo e' il vincolo operativo sul perimetro D-ND Markov/boundary.
+
+## Files
+- Script: `tools/exp_observable_rank_audit.py`
+- Data principale: `tools/data/observable_rank_audit.json`
+- Replica: `tools/data/observable_rank_audit_seed20260506.json`
+- Report: `tools/data/reports/agent_20260505_0330.md`
diff --git a/tools/exp_observable_rank_audit.py b/tools/exp_observable_rank_audit.py
new file mode 100644
index 0000000000000000000000000000000000000000..f2acafc5960c8a35964e0ab521cf70569eefe27f
--- /dev/null
+++ b/tools/exp_observable_rank_audit.py
@@ -0,0 +1,229 @@
+#!/usr/bin/env python3
+"""
+exp_observable_rank_audit.py - META audit for observable redundancy.
+
+Recent runs found many Markov/crossover observables that react coherently under
+partial shuffle. This script asks whether those observables carry independent
+directions or mostly re-measure one latent boundary coordinate.
+
+It measures retention curves from alpha-partial shuffles, then reports:
+  - original-vs-full-shuffle z for each observable
+  - PCA energy of the retention matrix across alpha
+  - effective rank of that matrix
+  - pairwise correlations between retention curves
+
+The script measures data only. The report decides the structural claim.
+"""
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from exp_3d_boundary_layers import get_primes, gue_gaps, partial_shuffle
+
+
+def obs_spacing_ratio(gaps):
+    r = np.minimum(gaps[:-1], gaps[1:]) / np.maximum(gaps[:-1], gaps[1:])
+    r = r[np.isfinite(r)]
+    return float(np.mean(r)) if len(r) else 0.0
+
+
+def obs_lag_acf(gaps, lag):
+    g = gaps - np.mean(gaps)
+    c0 = np.mean(g * g)
+    if c0 == 0:
+        return 0.0
+    return float(np.mean(g[:-lag] * g[lag:]) / c0)
+
+
+def obs_sr2(gaps):
+    r = np.minimum(gaps[:-2], gaps[2:]) / np.maximum(gaps[:-2], gaps[2:])
+    r = r[np.isfinite(r)]
+    return float(np.mean(r)) if len(r) else 0.0
+
+
+def obs_triple_var(gaps):
+    triples = gaps[:-2] + gaps[1:-1] + gaps[2:]
+    v = np.var(gaps)
+    if v == 0:
+        return 0.0
+    return float(np.var(triples) / v)
+
+
+OBSERVABLES = {
+    "SR": obs_spacing_ratio,
+    "L1": lambda gaps: obs_lag_acf(gaps, 1),
+    "L2": lambda gaps: obs_lag_acf(gaps, 2),
+    "SR2": obs_sr2,
+    "triple_var": obs_triple_var,
+}
+
+
+def measure(gaps):
+    return {name: fn(gaps) for name, fn in OBSERVABLES.items()}
+
+
+def full_shuffle_baseline(gaps, n_trials, rng):
+    vals = {name: [] for name in OBSERVABLES}
+    for _ in range(n_trials):
+        s = rng.permutation(gaps)
+        row = measure(s)
+        for name, value in row.items():
+            vals[name].append(value)
+    return {
+        name: {
+            "mean": float(np.mean(x)),
+            "std": float(np.std(x, ddof=1)) if len(x) > 1 else 0.0,
+        }
+        for name, x in vals.items()
+    }
+
+
+def retention_curves(gaps, alphas, n_trials, originals, baseline, rng):
+    rows = []
+    for alpha in alphas:
+        vals = {name: [] for name in OBSERVABLES}
+        for _ in range(n_trials):
+            s = partial_shuffle(gaps, float(alpha), rng)
+            row = measure(s)
+            for name, value in row.items():
+                vals[name].append(value)
+
+        out = {"alpha": float(alpha)}
+        for name in OBSERVABLES:
+            mean = float(np.mean(vals[name]))
+            denom = originals[name] - baseline[name]["mean"]
+            retention = (mean - baseline[name]["mean"]) / denom if abs(denom) > 1e-12 else 0.0
+            out[name] = {
+                "mean": mean,
+                "std": float(np.std(vals[name], ddof=1)) if len(vals[name]) > 1 else 0.0,
+                "retention": float(retention),
+            }
+        rows.append(out)
+    return rows
+
+
+def pca_summary(rows):
+    names = list(OBSERVABLES)
+    matrix = np.array([[row[name]["retention"] for name in names] for row in rows], dtype=float)
+    matrix = matrix - np.mean(matrix, axis=0, keepdims=True)
+
+    _, singular, vt = np.linalg.svd(matrix, full_matrices=False)
+    energy = singular * singular
+    if np.sum(energy) <= 1e-15:
+        explained = np.zeros_like(energy)
+        effective_rank = 0.0
+    else:
+        explained = energy / np.sum(energy)
+        positive = explained[explained > 1e-15]
+        effective_rank = float(np.exp(-np.sum(positive * np.log(positive))))
+
+    corr = np.corrcoef(matrix, rowvar=False)
+    abs_corr = np.abs(corr)
+    upper = abs_corr[np.triu_indices_from(abs_corr, k=1)]
+
+    return {
+        "observables": names,
+        "singular_values": [float(x) for x in singular],
+        "explained_variance": [float(x) for x in explained],
+        "effective_rank": effective_rank,
+        "pc1_loadings": {name: float(vt[0, i]) for i, name in enumerate(names)} if len(vt) else {},
+        "mean_abs_pairwise_corr": float(np.mean(upper)) if len(upper) else 0.0,
+        "min_abs_pairwise_corr": float(np.min(upper)) if len(upper) else 0.0,
+        "max_abs_pairwise_corr": float(np.max(upper)) if len(upper) else 0.0,
+    }
+
+
+def analyze_sequence(name, gaps, alphas, n_trials, n_baseline, rng):
+    originals = measure(gaps)
+    baseline = full_shuffle_baseline(gaps, n_baseline, rng)
+    rows = retention_curves(gaps, alphas, n_trials, originals, baseline, rng)
+
+    z = {}
+    for obs_name in OBSERVABLES:
+        std = baseline[obs_name]["std"]
+        z[obs_name] = float((originals[obs_name] - baseline[obs_name]["mean"]) / std) if std > 1e-12 else 0.0
+
+    return {
+        "n_gaps": int(len(gaps)),
+        "originals": originals,
+        "full_shuffle_baseline": baseline,
+        "original_vs_shuffle_z": z,
+        "retention_curves": rows,
+        "pca": pca_summary(rows),
+    }
+
+
+def build_sequences(n_gaps, rng):
+    primes = get_primes(n_gaps * 24)[: n_gaps + 1]
+    prime_gaps = np.diff(primes).astype(float)
+
+    gue = gue_gaps(160, max(8, n_gaps // 160 + 1), rng).astype(float)
+    gue = gue[:n_gaps]
+
+    poisson = rng.exponential(1.0, size=n_gaps).astype(float)
+    return {
+        "primes": prime_gaps,
+        "gue": gue,
+        "poisson": poisson,
+    }
+
+
+def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505):
+    rng = np.random.default_rng(seed)
+    alphas = np.linspace(0.05, 0.95, n_alpha)
+    sequences = build_sequences(n_gaps, rng)
+
+    output = {
+        "experiment": "observable_rank_audit",
+        "question": "Do crossover observables contain independent directions or one latent boundary coordinate?",
+        "params": {
+            "n_gaps": int(n_gaps),
+            "n_alpha": int(n_alpha),
+            "n_trials": int(n_trials),
+            "n_baseline": int(n_baseline),
+            "seed": int(seed),
+        },
+        "sequences": {},
+    }
+
+    print(f"n_gaps={n_gaps}, n_alpha={n_alpha}, n_trials={n_trials}, n_baseline={n_baseline}, seed={seed}")
+    print(f"{'sequence':<10} {'pc1':>8} {'eff_rank':>9} {'mean|corr|':>11}  z(SR,L1,L2,SR2,triple)")
+    print("-" * 92)
+    for seq_name, gaps in sequences.items():
+        result = analyze_sequence(seq_name, gaps, alphas, n_trials, n_baseline, rng)
+        output["sequences"][seq_name] = result
+        pca = result["pca"]
+        z = result["original_vs_shuffle_z"]
+        z_text = ", ".join(f"{obs}={z[obs]:+.1f}" for obs in OBSERVABLES)
+        pc1 = pca["explained_variance"][0] if pca["explained_variance"] else 0.0
+        print(f"{seq_name:<10} {pc1:>8.3f} {pca['effective_rank']:>9.3f} {pca['mean_abs_pairwise_corr']:>11.3f}  {z_text}")
+
+    out_path = Path("tools/data/observable_rank_audit.json")
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+    print(f"\nSaved to {out_path}")
+    return output
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n_gaps", type=int, default=30000)
+    parser.add_argument("--n_alpha", type=int, default=19)
+    parser.add_argument("--n_trials", type=int, default=24)
+    parser.add_argument("--n_baseline", type=int, default=72)
+    parser.add_argument("--seed", type=int, default=20260505)
+    args = parser.parse_args()
+    run(
+        n_gaps=args.n_gaps,
+        n_alpha=args.n_alpha,
+        n_trials=args.n_trials,
+        n_baseline=args.n_baseline,
+        seed=args.seed,
+    )
+
+
+if __name__ == "__main__":
+    main()

apply patch
patch: completed
/opt/MM_D-ND/tools/data/seme.json
diff --git a/tools/data/reports/agent_20260505_0330.md b/tools/data/reports/agent_20260505_0330.md
new file mode 100644
index 0000000000000000000000000000000000000000..a2bffd69f55a8117fc1ca2f1167d2992e3247c9e
--- /dev/null
+++ b/tools/data/reports/agent_20260505_0330.md
@@ -0,0 +1,73 @@
+# Agent Report - Observable Rank Audit: Many Probes, One Boundary Coordinate
+**Date**: 2026-05-05 03:30
+**Piano**: 62
+**Tension explored**: META (0.5) + BOUNDARY (0.8)
+
+## Claim Under Test
+> Tutti i 11 test passano - verifica che non stiamo testando solo tautologie.
+
+## Question
+Quando SR, L1, L2, SR2 e triple_var reagiscono al partial shuffle, stanno misurando direzioni indipendenti del confine o una sola coordinata latente vista da osservabili diverse?
+
+## Experiment Design
+- **Scope atomico**: 30,000 gap per dominio; alphas partial-shuffle = 19 punti da 0.05 a 0.95; 24 trial per alpha; 72 full-shuffle per baseline.
+- **Domini**: prime gaps, GUE gaps, Poisson iid exponential gaps.
+- **Osservabili**: SR, L1, L2, SR2, triple_var.
+- **Null baseline**: full shuffle della stessa sequenza. Il dato Poisson e' controllo di assenza di segnale originale-vs-shuffle.
+- **Metrica META**: matrice delle retention curve normalizzate rispetto al full shuffle; PCA sulla matrice alpha x osservabili; rango effettivo entropico delle energie singolari.
+- **Robustezza minima**: seed principale 20260505; replica seed 20260506 salvata separatamente.
+
+## Results
+Seed principale 20260505:
+
+| Domain | PC1 variance | Effective rank | mean abs corr | z SR | z L1 | z L2 | z SR2 | z triple_var |
+|---|---:|---:|---:|---:|---:|---:|---:|---:|
+| Primes | 0.989 | 1.069 | 0.987 | -19.3 | -7.7 | -3.0 | -3.7 | -8.0 |
+| GUE | 0.997 | 1.022 | 0.996 | -6.6 | +21.4 | +37.9 | +18.0 | +36.3 |
+| Poisson | 0.925 | 1.391 | 0.351 | -0.6 | +0.2 | +1.2 | -0.0 | +0.8 |
+
+Replica seed 20260506:
+
+| Domain | PC1 variance | Effective rank | mean abs corr |
+|---|---:|---:|---:|
+| Primes | 0.988 | 1.076 | 0.985 |
+| GUE | 0.997 | 1.019 | 0.997 |
+| Poisson | 0.675 | 2.394 | 0.640 |
+
+PC1 loadings nel seed principale:
+
+| Domain | SR | L1 | L2 | SR2 | triple_var |
+|---|---:|---:|---:|---:|---:|
+| Primes | -0.450 | -0.449 | -0.443 | -0.446 | -0.448 |
+| GUE | -0.446 | -0.449 | -0.447 | -0.446 | -0.448 |
+
+## Key Findings
+1. **Nel perimetro partial-shuffle, primes e GUE hanno segnale forte ma quasi monodimensionale.** Per primes, tutte le osservabili hanno z originale-vs-shuffle almeno |3.0| e la prima componente spiega 98.9% della varianza delle retention curve. Per GUE il collasso e' ancora piu' stretto: 99.7%.
+
+2. **La somiglianza dei loadings e' il dato operativo.** Nei domini strutturati, PC1 carica SR, L1, L2, SR2 e triple_var quasi uniformemente. Questo non dice che le osservabili siano identiche in generale; dice che sotto partial shuffle uniforme misurano soprattutto la stessa coordinata di distruzione dell'ordine.
+
+3. **Poisson non supporta un claim di rango.** Nel seed principale Poisson ha PC1 alto, ma tutti gli z originale-vs-shuffle sono sotto |1.2|; nella replica il rango cambia molto. Quindi il rango Poisson qui e' rumore di baseline, non struttura.
+
+4. **Il risultato restringe il linguaggio dei cicli precedenti.** I test layer/Markov restano utili per classificare sensibilita' locali, ma non vanno contati come prove indipendenti del confine quando sono misurati lungo la stessa perturbazione partial-shuffle.
+
+## Verdict
+**CONSTRAINT on META + BOUNDARY**: nel perimetro testato, il confine partial-shuffle ha una coordinata latente dominante. Le osservabili SR/L1/L2/SR2/triple_var sono probes validi, ma non cinque evidenze indipendenti della transizione. La consecutio corretta e' cercare perturbazioni selettive ortogonali, non aggiungere altri osservabili scalari sulla stessa curva.
+
+## Bicono della scoperta
+- **Due radici**: molte osservabili che passano il test; una coordinata latente che le assorbe sotto partial shuffle.
+- **Singolare**: la curva di distruzione dell'ordine prima della scelta dell'osservabile.
+- **Invariante di passaggio**: nei domini strutturati, la retention resta quasi collineare tra osservabili anche quando il segno z e la magnitudine assoluta differiscono.
+- **Campo di possibilita**: qui diventa possibile separare "probe valido" da "evidenza indipendente"; qui diventa non-possibile contare SR, L1, L2, SR2 e triple_var come cinque conferme autonome del medesimo confine partial-shuffle.
+
+## Auto-audit: 5 lenti
+- **L1 hard constraint vs bias**: nessun claim di zero, sempre, never, impossibile. Il claim e' limitato a PC1/rango nel perimetro misurato.
+- **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.
+- **L3 no silent patching**: il claim META originale non e' dichiarato "risolto"; e' ristretto. I test possono essere validi ma ridondanti lungo partial shuffle.
+- **L4 edge cases**: L2 primes ha z=-3.0, piu' debole degli altri; non viene usato come prova forte isolata.
+- **L5 re-discovery**: PCA/rango effettivo sono strumenti classici di ridondanza statistica. Non taggo NEW; il contributo e' il vincolo operativo sul perimetro D-ND Markov/boundary.
+
+## Files
+- Script: `tools/exp_observable_rank_audit.py`
+- Data principale: `tools/data/observable_rank_audit.json`
+- Replica: `tools/data/observable_rank_audit_seed20260506.json`
+- Report: `tools/data/reports/agent_20260505_0330.md`
diff --git a/tools/data/seme.json b/tools/data/seme.json
index d55f53f6b19158acb3427690823d990893ea9828..f08a074823c56dec3126afbb09a2c57b9dee3aa5
--- a/tools/data/seme.json
+++ b/tools/data/seme.json
@@ -91,7 +91,8 @@
   ],
   "potenziale_bloccato": [],
   "varianza": [
-    "Tensioni risolte: {'DUALITA_DIPOLARE_VS_ILLUSORIA', 'METRIC_TENSOR', 'TENSIONE_ENTITA', 'TRASCENDENZA_LIMITE', 'G_POTENZIALE_NULLA', 'PIANO_PRIMARIO_DUE_ASSIOMI', 'META_LAYER_RECOVERY_AUDIT'}"
+    "Tensioni risolte: {'DUALITA_DIPOLARE_VS_ILLUSORIA', 'METRIC_TENSOR', 'TENSIONE_ENTITA', 'TRASCENDENZA_LIMITE', 'G_POTENZIALE_NULLA', 'PIANO_PRIMARIO_DUE_ASSIOMI', 'META_LAYER_RECOVERY_AUDIT'}",
+    "Report agent_20260505_0330: observable-rank audit su primes/GUE/Poisson con SR,L1,L2,SR2,triple_var. Nel perimetro partial-shuffle uniforme, primes e GUE hanno segnale forte ma rango effettivo ~1 (PC1 primes=0.989, GUE=0.997; replica stabile). Vincolo META+BOUNDARY: probes validi non equivalgono a evidenze indipendenti lungo la stessa perturbazione. Consecutio: cercare perturbazioni selettive ortogonali, non aggiungere osservabili scalari sulla stessa curva."
   ],
   "filtro": {
     "promosse": 8,
@@ -265,4 +266,4 @@
       "timestamp": "2026-04-04T19:34:08.014Z"
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/tools/exp_observable_rank_audit.py b/tools/exp_observable_rank_audit.py
new file mode 100644
index 0000000000000000000000000000000000000000..f2acafc5960c8a35964e0ab521cf70569eefe27f
--- /dev/null
+++ b/tools/exp_observable_rank_audit.py
@@ -0,0 +1,229 @@
+#!/usr/bin/env python3
+"""
+exp_observable_rank_audit.py - META audit for observable redundancy.
+
+Recent runs found many Markov/crossover observables that react coherently under
+partial shuffle. This script asks whether those observables carry independent
+directions or mostly re-measure one latent boundary coordinate.
+
+It measures retention curves from alpha-partial shuffles, then reports:
+  - original-vs-full-shuffle z for each observable
+  - PCA energy of the retention matrix across alpha
+  - effective rank of that matrix
+  - pairwise correlations between retention curves
+
+The script measures data only. The report decides the structural claim.
+"""
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from exp_3d_boundary_layers import get_primes, gue_gaps, partial_shuffle
+
+
+def obs_spacing_ratio(gaps):
+    r = np.minimum(gaps[:-1], gaps[1:]) / np.maximum(gaps[:-1], gaps[1:])
+    r = r[np.isfinite(r)]
+    return float(np.mean(r)) if len(r) else 0.0
+
+
+def obs_lag_acf(gaps, lag):
+    g = gaps - np.mean(gaps)
+    c0 = np.mean(g * g)
+    if c0 == 0:
+        return 0.0
+    return float(np.mean(g[:-lag] * g[lag:]) / c0)
+
+
+def obs_sr2(gaps):
+    r = np.minimum(gaps[:-2], gaps[2:]) / np.maximum(gaps[:-2], gaps[2:])
+    r = r[np.isfinite(r)]
+    return float(np.mean(r)) if len(r) else 0.0
+
+
+def obs_triple_var(gaps):
+    triples = gaps[:-2] + gaps[1:-1] + gaps[2:]
+    v = np.var(gaps)
+    if v == 0:
+        return 0.0
+    return float(np.var(triples) / v)
+
+
+OBSERVABLES = {
+    "SR": obs_spacing_ratio,
+    "L1": lambda gaps: obs_lag_acf(gaps, 1),
+    "L2": lambda gaps: obs_lag_acf(gaps, 2),
+    "SR2": obs_sr2,
+    "triple_var": obs_triple_var,
+}
+
+
+def measure(gaps):
+    return {name: fn(gaps) for name, fn in OBSERVABLES.items()}
+
+
+def full_shuffle_baseline(gaps, n_trials, rng):
+    vals = {name: [] for name in OBSERVABLES}
+    for _ in range(n_trials):
+        s = rng.permutation(gaps)
+        row = measure(s)
+        for name, value in row.items():
+            vals[name].append(value)
+    return {
+        name: {
+            "mean": float(np.mean(x)),
+            "std": float(np.std(x, ddof=1)) if len(x) > 1 else 0.0,
+        }
+        for name, x in vals.items()
+    }
+
+
+def retention_curves(gaps, alphas, n_trials, originals, baseline, rng):
+    rows = []
+    for alpha in alphas:
+        vals = {name: [] for name in OBSERVABLES}
+        for _ in range(n_trials):
+            s = partial_shuffle(gaps, float(alpha), rng)
+            row = measure(s)
+            for name, value in row.items():
+                vals[name].append(value)
+
+        out = {"alpha": float(alpha)}
+        for name in OBSERVABLES:
+            mean = float(np.mean(vals[name]))
+            denom = originals[name] - baseline[name]["mean"]
+            retention = (mean - baseline[name]["mean"]) / denom if abs(denom) > 1e-12 else 0.0
+            out[name] = {
+                "mean": mean,
+                "std": float(np.std(vals[name], ddof=1)) if len(vals[name]) > 1 else 0.0,
+                "retention": float(retention),
+            }
+        rows.append(out)
+    return rows
+
+
+def pca_summary(rows):
+    names = list(OBSERVABLES)
+    matrix = np.array([[row[name]["retention"] for name in names] for row in rows], dtype=float)
+    matrix = matrix - np.mean(matrix, axis=0, keepdims=True)
+
+    _, singular, vt = np.linalg.svd(matrix, full_matrices=False)
+    energy = singular * singular
+    if np.sum(energy) <= 1e-15:
+        explained = np.zeros_like(energy)
+        effective_rank = 0.0
+    else:
+        explained = energy / np.sum(energy)
+        positive = explained[explained > 1e-15]
+        effective_rank = float(np.exp(-np.sum(positive * np.log(positive))))
+
+    corr = np.corrcoef(matrix, rowvar=False)
+    abs_corr = np.abs(corr)
+    upper = abs_corr[np.triu_indices_from(abs_corr, k=1)]
+
+    return {
+        "observables": names,
+        "singular_values": [float(x) for x in singular],
+        "explained_variance": [float(x) for x in explained],
+        "effective_rank": effective_rank,
+        "pc1_loadings": {name: float(vt[0, i]) for i, name in enumerate(names)} if len(vt) else {},
+        "mean_abs_pairwise_corr": float(np.mean(upper)) if len(upper) else 0.0,
+        "min_abs_pairwise_corr": float(np.min(upper)) if len(upper) else 0.0,
+        "max_abs_pairwise_corr": float(np.max(upper)) if len(upper) else 0.0,
+    }
+
+
+def analyze_sequence(name, gaps, alphas, n_trials, n_baseline, rng):
+    originals = measure(gaps)
+    baseline = full_shuffle_baseline(gaps, n_baseline, rng)
+    rows = retention_curves(gaps, alphas, n_trials, originals, baseline, rng)
+
+    z = {}
+    for obs_name in OBSERVABLES:
+        std = baseline[obs_name]["std"]
+        z[obs_name] = float((originals[obs_name] - baseline[obs_name]["mean"]) / std) if std > 1e-12 else 0.0
+
+    return {
+        "n_gaps": int(len(gaps)),
+        "originals": originals,
+        "full_shuffle_baseline": baseline,
+        "original_vs_shuffle_z": z,
+        "retention_curves": rows,
+        "pca": pca_summary(rows),
+    }
+
+
+def build_sequences(n_gaps, rng):
+    primes = get_primes(n_gaps * 24)[: n_gaps + 1]
+    prime_gaps = np.diff(primes).astype(float)
+
+    gue = gue_gaps(160, max(8, n_gaps // 160 + 1), rng).astype(float)
+    gue = gue[:n_gaps]
+
+    poisson = rng.exponential(1.0, size=n_gaps).astype(float)
+    return {
+        "primes": prime_gaps,
+        "gue": gue,
+        "poisson": poisson,
+    }
+
+
+def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505):
+    rng = np.random.default_rng(seed)
+    alphas = np.linspace(0.05, 0.95, n_alpha)
+    sequences = build_sequences(n_gaps, rng)
+
+    output = {
+        "experiment": "observable_rank_audit",
+        "question": "Do crossover observables contain independent directions or one latent boundary coordinate?",
+        "params": {
+            "n_gaps": int(n_gaps),
+            "n_alpha": int(n_alpha),
+            "n_trials": int(n_trials),
+            "n_baseline": int(n_baseline),
+            "seed": int(seed),
+        },
+        "sequences": {},
+    }
+
+    print(f"n_gaps={n_gaps}, n_alpha={n_alpha}, n_trials={n_trials}, n_baseline={n_baseline}, seed={seed}")
+    print(f"{'sequence':<10} {'pc1':>8} {'eff_rank':>9} {'mean|corr|':>11}  z(SR,L1,L2,SR2,triple)")
+    print("-" * 92)
+    for seq_name, gaps in sequences.items():
+        result = analyze_sequence(seq_name, gaps, alphas, n_trials, n_baseline, rng)
+        output["sequences"][seq_name] = result
+        pca = result["pca"]
+        z = result["original_vs_shuffle_z"]
+        z_text = ", ".join(f"{obs}={z[obs]:+.1f}" for obs in OBSERVABLES)
+        pc1 = pca["explained_variance"][0] if pca["explained_variance"] else 0.0
+        print(f"{seq_name:<10} {pc1:>8.3f} {pca['effective_rank']:>9.3f} {pca['mean_abs_pairwise_corr']:>11.3f}  {z_text}")
+
+    out_path = Path("tools/data/observable_rank_audit.json")
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+    print(f"\nSaved to {out_path}")
+    return output
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n_gaps", type=int, default=30000)
+    parser.add_argument("--n_alpha", type=int, default=19)
+    parser.add_argument("--n_trials", type=int, default=24)
+    parser.add_argument("--n_baseline", type=int, default=72)
+    parser.add_argument("--seed", type=int, default=20260505)
+    args = parser.parse_args()
+    run(
+        n_gaps=args.n_gaps,
+        n_alpha=args.n_alpha,
+        n_trials=args.n_trials,
+        n_baseline=args.n_baseline,
+        seed=args.seed,
+    )
+
+
+if __name__ == "__main__":
+    main()

diff --git a/tools/data/reports/agent_20260505_0330.md b/tools/data/reports/agent_20260505_0330.md
new file mode 100644
index 0000000000000000000000000000000000000000..a2bffd69f55a8117fc1ca2f1167d2992e3247c9e
--- /dev/null
+++ b/tools/data/reports/agent_20260505_0330.md
@@ -0,0 +1,73 @@
+# Agent Report - Observable Rank Audit: Many Probes, One Boundary Coordinate
+**Date**: 2026-05-05 03:30
+**Piano**: 62
+**Tension explored**: META (0.5) + BOUNDARY (0.8)
+
+## Claim Under Test
+> Tutti i 11 test passano - verifica che non stiamo testando solo tautologie.
+
+## Question
+Quando SR, L1, L2, SR2 e triple_var reagiscono al partial shuffle, stanno misurando direzioni indipendenti del confine o una sola coordinata latente vista da osservabili diverse?
+
+## Experiment Design
+- **Scope atomico**: 30,000 gap per dominio; alphas partial-shuffle = 19 punti da 0.05 a 0.95; 24 trial per alpha; 72 full-shuffle per baseline.
+- **Domini**: prime gaps, GUE gaps, Poisson iid exponential gaps.
+- **Osservabili**: SR, L1, L2, SR2, triple_var.
+- **Null baseline**: full shuffle della stessa sequenza. Il dato Poisson e' controllo di assenza di segnale originale-vs-shuffle.
+- **Metrica META**: matrice delle retention curve normalizzate rispetto al full shuffle; PCA sulla matrice alpha x osservabili; rango effettivo entropico delle energie singolari.
+- **Robustezza minima**: seed principale 20260505; replica seed 20260506 salvata separatamente.
+
+## Results
+Seed principale 20260505:
+
+| Domain | PC1 variance | Effective rank | mean abs corr | z SR | z L1 | z L2 | z SR2 | z triple_var |
+|---|---:|---:|---:|---:|---:|---:|---:|---:|
+| Primes | 0.989 | 1.069 | 0.987 | -19.3 | -7.7 | -3.0 | -3.7 | -8.0 |
+| GUE | 0.997 | 1.022 | 0.996 | -6.6 | +21.4 | +37.9 | +18.0 | +36.3 |
+| Poisson | 0.925 | 1.391 | 0.351 | -0.6 | +0.2 | +1.2 | -0.0 | +0.8 |
+
+Replica seed 20260506:
+
+| Domain | PC1 variance | Effective rank | mean abs corr |
+|---|---:|---:|---:|
+| Primes | 0.988 | 1.076 | 0.985 |
+| GUE | 0.997 | 1.019 | 0.997 |
+| Poisson | 0.675 | 2.394 | 0.640 |
+
+PC1 loadings nel seed principale:
+
+| Domain | SR | L1 | L2 | SR2 | triple_var |
+|---|---:|---:|---:|---:|---:|
+| Primes | -0.450 | -0.449 | -0.443 | -0.446 | -0.448 |
+| GUE | -0.446 | -0.449 | -0.447 | -0.446 | -0.448 |
+
+## Key Findings
+1. **Nel perimetro partial-shuffle, primes e GUE hanno segnale forte ma quasi monodimensionale.** Per primes, tutte le osservabili hanno z originale-vs-shuffle almeno |3.0| e la prima componente spiega 98.9% della varianza delle retention curve. Per GUE il collasso e' ancora piu' stretto: 99.7%.
+
+2. **La somiglianza dei loadings e' il dato operativo.** Nei domini strutturati, PC1 carica SR, L1, L2, SR2 e triple_var quasi uniformemente. Questo non dice che le osservabili siano identiche in generale; dice che sotto partial shuffle uniforme misurano soprattutto la stessa coordinata di distruzione dell'ordine.
+
+3. **Poisson non supporta un claim di rango.** Nel seed principale Poisson ha PC1 alto, ma tutti gli z originale-vs-shuffle sono sotto |1.2|; nella replica il rango cambia molto. Quindi il rango Poisson qui e' rumore di baseline, non struttura.
+
+4. **Il risultato restringe il linguaggio dei cicli precedenti.** I test layer/Markov restano utili per classificare sensibilita' locali, ma non vanno contati come prove indipendenti del confine quando sono misurati lungo la stessa perturbazione partial-shuffle.
+
+## Verdict
+**CONSTRAINT on META + BOUNDARY**: nel perimetro testato, il confine partial-shuffle ha una coordinata latente dominante. Le osservabili SR/L1/L2/SR2/triple_var sono probes validi, ma non cinque evidenze indipendenti della transizione. La consecutio corretta e' cercare perturbazioni selettive ortogonali, non aggiungere altri osservabili scalari sulla stessa curva.
+
+## Bicono della scoperta
+- **Due radici**: molte osservabili che passano il test; una coordinata latente che le assorbe sotto partial shuffle.
+- **Singolare**: la curva di distruzione dell'ordine prima della scelta dell'osservabile.
+- **Invariante di passaggio**: nei domini strutturati, la retention resta quasi collineare tra osservabili anche quando il segno z e la magnitudine assoluta differiscono.
+- **Campo di possibilita**: qui diventa possibile separare "probe valido" da "evidenza indipendente"; qui diventa non-possibile contare SR, L1, L2, SR2 e triple_var come cinque conferme autonome del medesimo confine partial-shuffle.
+
+## Auto-audit: 5 lenti
+- **L1 hard constraint vs bias**: nessun claim di zero, sempre, never, impossibile. Il claim e' limitato a PC1/rango nel perimetro misurato.
+- **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.
+- **L3 no silent patching**: il claim META originale non e' dichiarato "risolto"; e' ristretto. I test possono essere validi ma ridondanti lungo partial shuffle.
+- **L4 edge cases**: L2 primes ha z=-3.0, piu' debole degli altri; non viene usato come prova forte isolata.
+- **L5 re-discovery**: PCA/rango effettivo sono strumenti classici di ridondanza statistica. Non taggo NEW; il contributo e' il vincolo operativo sul perimetro D-ND Markov/boundary.
+
+## Files
+- Script: `tools/exp_observable_rank_audit.py`
+- Data principale: `tools/data/observable_rank_audit.json`
+- Replica: `tools/data/observable_rank_audit_seed20260506.json`
+- Report: `tools/data/reports/agent_20260505_0330.md`
diff --git a/tools/data/seme.json b/tools/data/seme.json
index d55f53f6b19158acb3427690823d990893ea9828..f08a074823c56dec3126afbb09a2c57b9dee3aa5
--- a/tools/data/seme.json
+++ b/tools/data/seme.json
@@ -91,7 +91,8 @@
   ],
   "potenziale_bloccato": [],
   "varianza": [
-    "Tensioni risolte: {'DUALITA_DIPOLARE_VS_ILLUSORIA', 'METRIC_TENSOR', 'TENSIONE_ENTITA', 'TRASCENDENZA_LIMITE', 'G_POTENZIALE_NULLA', 'PIANO_PRIMARIO_DUE_ASSIOMI', 'META_LAYER_RECOVERY_AUDIT'}"
+    "Tensioni risolte: {'DUALITA_DIPOLARE_VS_ILLUSORIA', 'METRIC_TENSOR', 'TENSIONE_ENTITA', 'TRASCENDENZA_LIMITE', 'G_POTENZIALE_NULLA', 'PIANO_PRIMARIO_DUE_ASSIOMI', 'META_LAYER_RECOVERY_AUDIT'}",
+    "Report agent_20260505_0330: observable-rank audit su primes/GUE/Poisson con SR,L1,L2,SR2,triple_var. Nel perimetro partial-shuffle uniforme, primes e GUE hanno segnale forte ma rango effettivo ~1 (PC1 primes=0.989, GUE=0.997; replica stabile). Vincolo META+BOUNDARY: probes validi non equivalgono a evidenze indipendenti lungo la stessa perturbazione. Consecutio: cercare perturbazioni selettive ortogonali, non aggiungere osservabili scalari sulla stessa curva."
   ],
   "filtro": {
     "promosse": 8,
@@ -265,4 +266,4 @@
       "timestamp": "2026-04-04T19:34:08.014Z"
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/tools/exp_observable_rank_audit.py b/tools/exp_observable_rank_audit.py
new file mode 100644
index 0000000000000000000000000000000000000000..f2acafc5960c8a35964e0ab521cf70569eefe27f
--- /dev/null
+++ b/tools/exp_observable_rank_audit.py
@@ -0,0 +1,229 @@
+#!/usr/bin/env python3
+"""
+exp_observable_rank_audit.py - META audit for observable redundancy.
+
+Recent runs found many Markov/crossover observables that react coherently under
+partial shuffle. This script asks whether those observables carry independent
+directions or mostly re-measure one latent boundary coordinate.
+
+It measures retention curves from alpha-partial shuffles, then reports:
+  - original-vs-full-shuffle z for each observable
+  - PCA energy of the retention matrix across alpha
+  - effective rank of that matrix
+  - pairwise correlations between retention curves
+
+The script measures data only. The report decides the structural claim.
+"""
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from exp_3d_boundary_layers import get_primes, gue_gaps, partial_shuffle
+
+
+def obs_spacing_ratio(gaps):
+    r = np.minimum(gaps[:-1], gaps[1:]) / np.maximum(gaps[:-1], gaps[1:])
+    r = r[np.isfinite(r)]
+    return float(np.mean(r)) if len(r) else 0.0
+
+
+def obs_lag_acf(gaps, lag):
+    g = gaps - np.mean(gaps)
+    c0 = np.mean(g * g)
+    if c0 == 0:
+        return 0.0
+    return float(np.mean(g[:-lag] * g[lag:]) / c0)
+
+
+def obs_sr2(gaps):
+    r = np.minimum(gaps[:-2], gaps[2:]) / np.maximum(gaps[:-2], gaps[2:])
+    r = r[np.isfinite(r)]
+    return float(np.mean(r)) if len(r) else 0.0
+
+
+def obs_triple_var(gaps):
+    triples = gaps[:-2] + gaps[1:-1] + gaps[2:]
+    v = np.var(gaps)
+    if v == 0:
+        return 0.0
+    return float(np.var(triples) / v)
+
+
+OBSERVABLES = {
+    "SR": obs_spacing_ratio,
+    "L1": lambda gaps: obs_lag_acf(gaps, 1),
+    "L2": lambda gaps: obs_lag_acf(gaps, 2),
+    "SR2": obs_sr2,
+    "triple_var": obs_triple_var,
+}
+
+
+def measure(gaps):
+    return {name: fn(gaps) for name, fn in OBSERVABLES.items()}
+
+
+def full_shuffle_baseline(gaps, n_trials, rng):
+    vals = {name: [] for name in OBSERVABLES}
+    for _ in range(n_trials):
+        s = rng.permutation(gaps)
+        row = measure(s)
+        for name, value in row.items():
+            vals[name].append(value)
+    return {
+        name: {
+            "mean": float(np.mean(x)),
+            "std": float(np.std(x, ddof=1)) if len(x) > 1 else 0.0,
+        }
+        for name, x in vals.items()
+    }
+
+
+def retention_curves(gaps, alphas, n_trials, originals, baseline, rng):
+    rows = []
+    for alpha in alphas:
+        vals = {name: [] for name in OBSERVABLES}
+        for _ in range(n_trials):
+            s = partial_shuffle(gaps, float(alpha), rng)
+            row = measure(s)
+            for name, value in row.items():
+                vals[name].append(value)
+
+        out = {"alpha": float(alpha)}
+        for name in OBSERVABLES:
+            mean = float(np.mean(vals[name]))
+            denom = originals[name] - baseline[name]["mean"]
+            retention = (mean - baseline[name]["mean"]) / denom if abs(denom) > 1e-12 else 0.0
+            out[name] = {
+                "mean": mean,
+                "std": float(np.std(vals[name], ddof=1)) if len(vals[name]) > 1 else 0.0,
+                "retention": float(retention),
+            }
+        rows.append(out)
+    return rows
+
+
+def pca_summary(rows):
+    names = list(OBSERVABLES)
+    matrix = np.array([[row[name]["retention"] for name in names] for row in rows], dtype=float)
+    matrix = matrix - np.mean(matrix, axis=0, keepdims=True)
+
+    _, singular, vt = np.linalg.svd(matrix, full_matrices=False)
+    energy = singular * singular
+    if np.sum(energy) <= 1e-15:
+        explained = np.zeros_like(energy)
+        effective_rank = 0.0
+    else:
+        explained = energy / np.sum(energy)
+        positive = explained[explained > 1e-15]
+        effective_rank = float(np.exp(-np.sum(positive * np.log(positive))))
+
+    corr = np.corrcoef(matrix, rowvar=False)
+    abs_corr = np.abs(corr)
+    upper = abs_corr[np.triu_indices_from(abs_corr, k=1)]
+
+    return {
+        "observables": names,
+        "singular_values": [float(x) for x in singular],
+        "explained_variance": [float(x) for x in explained],
+        "effective_rank": effective_rank,
+        "pc1_loadings": {name: float(vt[0, i]) for i, name in enumerate(names)} if len(vt) else {},
+        "mean_abs_pairwise_corr": float(np.mean(upper)) if len(upper) else 0.0,
+        "min_abs_pairwise_corr": float(np.min(upper)) if len(upper) else 0.0,
+        "max_abs_pairwise_corr": float(np.max(upper)) if len(upper) else 0.0,
+    }
+
+
+def analyze_sequence(name, gaps, alphas, n_trials, n_baseline, rng):
+    originals = measure(gaps)
+    baseline = full_shuffle_baseline(gaps, n_baseline, rng)
+    rows = retention_curves(gaps, alphas, n_trials, originals, baseline, rng)
+
+    z = {}
+    for obs_name in OBSERVABLES:
+        std = baseline[obs_name]["std"]
+        z[obs_name] = float((originals[obs_name] - baseline[obs_name]["mean"]) / std) if std > 1e-12 else 0.0
+
+    return {
+        "n_gaps": int(len(gaps)),
+        "originals": originals,
+        "full_shuffle_baseline": baseline,
+        "original_vs_shuffle_z": z,
+        "retention_curves": rows,
+        "pca": pca_summary(rows),
+    }
+
+
+def build_sequences(n_gaps, rng):
+    primes = get_primes(n_gaps * 24)[: n_gaps + 1]
+    prime_gaps = np.diff(primes).astype(float)
+
+    gue = gue_gaps(160, max(8, n_gaps // 160 + 1), rng).astype(float)
+    gue = gue[:n_gaps]
+
+    poisson = rng.exponential(1.0, size=n_gaps).astype(float)
+    return {
+        "primes": prime_gaps,
+        "gue": gue,
+        "poisson": poisson,
+    }
+
+
+def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505):
+    rng = np.random.default_rng(seed)
+    alphas = np.linspace(0.05, 0.95, n_alpha)
+    sequences = build_sequences(n_gaps, rng)
+
+    output = {
+        "experiment": "observable_rank_audit",
+        "question": "Do crossover observables contain independent directions or one latent boundary coordinate?",
+        "params": {
+            "n_gaps": int(n_gaps),
+            "n_alpha": int(n_alpha),
+            "n_trials": int(n_trials),
+            "n_baseline": int(n_baseline),
+            "seed": int(seed),
+        },
+        "sequences": {},
+    }
+
+    print(f"n_gaps={n_gaps}, n_alpha={n_alpha}, n_trials={n_trials}, n_baseline={n_baseline}, seed={seed}")
+    print(f"{'sequence':<10} {'pc1':>8} {'eff_rank':>9} {'mean|corr|':>11}  z(SR,L1,L2,SR2,triple)")
+    print("-" * 92)
+    for seq_name, gaps in sequences.items():
+        result = analyze_sequence(seq_name, gaps, alphas, n_trials, n_baseline, rng)
+        output["sequences"][seq_name] = result
+        pca = result["pca"]
+        z = result["original_vs_shuffle_z"]
+        z_text = ", ".join(f"{obs}={z[obs]:+.1f}" for obs in OBSERVABLES)
+        pc1 = pca["explained_variance"][0] if pca["explained_variance"] else 0.0
+        print(f"{seq_name:<10} {pc1:>8.3f} {pca['effective_rank']:>9.3f} {pca['mean_abs_pairwise_corr']:>11.3f}  {z_text}")
+
+    out_path = Path("tools/data/observable_rank_audit.json")
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+    print(f"\nSaved to {out_path}")
+    return output
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n_gaps", type=int, default=30000)
+    parser.add_argument("--n_alpha", type=int, default=19)
+    parser.add_argument("--n_trials", type=int, default=24)
+    parser.add_argument("--n_baseline", type=int, default=72)
+    parser.add_argument("--seed", type=int, default=20260505)
+    args = parser.parse_args()
+    run(
+        n_gaps=args.n_gaps,
+        n_alpha=args.n_alpha,
+        n_trials=args.n_trials,
+        n_baseline=args.n_baseline,
+        seed=args.seed,
+    )
+
+
+if __name__ == "__main__":
+    main()

codex
Report e seme sono scritti. Verifico ora che il JSON resti valido, che lo script compili e che i file modificati siano solo quelli attesi.
exec
/bin/bash -lc 'python -m py_compile tools/exp_observable_rank_audit.py' in /opt/MM_D-ND
 succeeded in 0ms:
exec
/bin/bash -lc "sed -n '1,220p' tools/data/reports/agent_20260505_0330.md" in /opt/MM_D-ND
 succeeded in 0ms:
# Agent Report - Observable Rank Audit: Many Probes, One Boundary Coordinate
**Date**: 2026-05-05 03:30
**Piano**: 62
**Tension explored**: META (0.5) + BOUNDARY (0.8)

## Claim Under Test
> Tutti i 11 test passano - verifica che non stiamo testando solo tautologie.

## Question
Quando SR, L1, L2, SR2 e triple_var reagiscono al partial shuffle, stanno misurando direzioni indipendenti del confine o una sola coordinata latente vista da osservabili diverse?

## Experiment Design
- **Scope atomico**: 30,000 gap per dominio; alphas partial-shuffle = 19 punti da 0.05 a 0.95; 24 trial per alpha; 72 full-shuffle per baseline.
- **Domini**: prime gaps, GUE gaps, Poisson iid exponential gaps.
- **Osservabili**: SR, L1, L2, SR2, triple_var.
- **Null baseline**: full shuffle della stessa sequenza. Il dato Poisson e' controllo di assenza di segnale originale-vs-shuffle.
- **Metrica META**: matrice delle retention curve normalizzate rispetto al full shuffle; PCA sulla matrice alpha x osservabili; rango effettivo entropico delle energie singolari.
- **Robustezza minima**: seed principale 20260505; replica seed 20260506 salvata separatamente.

## Results
Seed principale 20260505:

| Domain | PC1 variance | Effective rank | mean abs corr | z SR | z L1 | z L2 | z SR2 | z triple_var |
|---|---:|---:|---:|---:|---:|---:|---:|---:|
| Primes | 0.989 | 1.069 | 0.987 | -19.3 | -7.7 | -3.0 | -3.7 | -8.0 |
| GUE | 0.997 | 1.022 | 0.996 | -6.6 | +21.4 | +37.9 | +18.0 | +36.3 |
| Poisson | 0.925 | 1.391 | 0.351 | -0.6 | +0.2 | +1.2 | -0.0 | +0.8 |

Replica seed 20260506:

| Domain | PC1 variance | Effective rank | mean abs corr |
|---|---:|---:|---:|
| Primes | 0.988 | 1.076 | 0.985 |
| GUE | 0.997 | 1.019 | 0.997 |
| Poisson | 0.675 | 2.394 | 0.640 |

PC1 loadings nel seed principale:

| Domain | SR | L1 | L2 | SR2 | triple_var |
|---|---:|---:|---:|---:|---:|
| Primes | -0.450 | -0.449 | -0.443 | -0.446 | -0.448 |
| GUE | -0.446 | -0.449 | -0.447 | -0.446 | -0.448 |

## Key Findings
1. **Nel perimetro partial-shuffle, primes e GUE hanno segnale forte ma quasi monodimensionale.** Per primes, tutte le osservabili hanno z originale-vs-shuffle almeno |3.0| e la prima componente spiega 98.9% della varianza delle retention curve. Per GUE il collasso e' ancora piu' stretto: 99.7%.

2. **La somiglianza dei loadings e' il dato operativo.** Nei domini strutturati, PC1 carica SR, L1, L2, SR2 e triple_var quasi uniformemente. Questo non dice che le osservabili siano identiche in generale; dice che sotto partial shuffle uniforme misurano soprattutto la stessa coordinata di distruzione dell'ordine.

3. **Poisson non supporta un claim di rango.** Nel seed principale Poisson ha PC1 alto, ma tutti gli z originale-vs-shuffle sono sotto |1.2|; nella replica il rango cambia molto. Quindi il rango Poisson qui e' rumore di baseline, non struttura.

4. **Il risultato restringe il linguaggio dei cicli precedenti.** I test layer/Markov restano utili per classificare sensibilita' locali, ma non vanno contati come prove indipendenti del confine quando sono misurati lungo la stessa perturbazione partial-shuffle.

## Verdict
**CONSTRAINT on META + BOUNDARY**: nel perimetro testato, il confine partial-shuffle ha una coordinata latente dominante. Le osservabili SR/L1/L2/SR2/triple_var sono probes validi, ma non cinque evidenze indipendenti della transizione. La consecutio corretta e' cercare perturbazioni selettive ortogonali, non aggiungere altri osservabili scalari sulla stessa curva.

## Bicono della scoperta
- **Due radici**: molte osservabili che passano il test; una coordinata latente che le assorbe sotto partial shuffle.
- **Singolare**: la curva di distruzione dell'ordine prima della scelta dell'osservabile.
- **Invariante di passaggio**: nei domini strutturati, la retention resta quasi collineare tra osservabili anche quando il segno z e la magnitudine assoluta differiscono.
- **Campo di possibilita**: qui diventa possibile separare "probe valido" da "evidenza indipendente"; qui diventa non-possibile contare SR, L1, L2, SR2 e triple_var come cinque conferme autonome del medesimo confine partial-shuffle.

## Auto-audit: 5 lenti
- **L1 hard constraint vs bias**: nessun claim di zero, sempre, never, impossibile. Il claim e' limitato a PC1/rango nel perimetro misurato.
- **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.
- **L3 no silent patching**: il claim META originale non e' dichiarato "risolto"; e' ristretto. I test possono essere validi ma ridondanti lungo partial shuffle.
- **L4 edge cases**: L2 primes ha z=-3.0, piu' debole degli altri; non viene usato come prova forte isolata.
- **L5 re-discovery**: PCA/rango effettivo sono strumenti classici di ridondanza statistica. Non taggo NEW; il contributo e' il vincolo operativo sul perimetro D-ND Markov/boundary.

## Files
- Script: `tools/exp_observable_rank_audit.py`
- Data principale: `tools/data/observable_rank_audit.json`
- Replica: `tools/data/observable_rank_audit_seed20260506.json`
- Report: `tools/data/reports/agent_20260505_0330.md`

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/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
 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
?? .codex
?? applications/published/20260504_0901_the-two-markov-layers-are-coupled/
?? applications/published/20260504_1219_markov-layers-pass-the-first-recovery/
?? 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/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/
?? tools/data/.last_telegram_msg
?? tools/data/3d_boundary_layers.json
?? tools/data/alignment_active.json
?? tools/data/alignment_markers.jsonl
?? tools/data/bicono_projections.jsonl
?? tools/data/boundary_coherence.json
?? tools/data/boundary_shuffle_audit.json
?? tools/data/brody_calibration_results.json
?? tools/data/brody_flow.json
?? tools/data/conoscenza_teorie.json.bak.retraction_22_04
?? tools/data/cross_domain_dipolar_direction.json
?? tools/data/cross_observable_consistency.json
?? tools/data/crossover_phase_test.json
?? tools/data/dipolar_crossover.json
?? tools/data/dipolar_vector_scaling.json
?? tools/data/domandatore/domandatore_20260421_0746.json
?? tools/data/domandatore/domandatore_20260422_0345.json
?? tools/data/domandatore/domandatore_20260423_0345.json
?? tools/data/domandatore/domandatore_20260424_0345.json
?? tools/data/domandatore/domandatore_20260425_0345.json
?? tools/data/domandatore/domandatore_20260426_0345.json
?? tools/data/domandatore/domandatore_20260427_0345.json
?? tools/data/domandatore/domandatore_20260428_0345.json
?? tools/data/domandatore/domandatore_20260428_1236.json
?? tools/data/domandatore/domandatore_20260429_0345.json
?? tools/data/domandatore/domandatore_20260430_0345.json
?? tools/data/domandatore/domandatore_20260501_0345.json
?? tools/data/domandatore/domandatore_20260502_0345.json
?? tools/data/domandatore/domandatore_20260503_0345.json
?? tools/data/domandatore/domandatore_20260504_0345.json
?? tools/data/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/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/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_rank_audit.json
?? tools/data/observable_rank_audit_seed20260506.json
?? 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/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/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/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/incident_20260504_0721.md
?? tools/data/reports/incident_20260504_1138.md
?? tools/data/reports/latest.md
?? tools/data/selective_layer_decoupling.json
?? tools/data/seme_archive/piano_57.json
?? tools/data/seme_archive/piano_59.json
?? tools/data/seme_archive/piano_60.json
?? tools/data/seme_archive/piano_61.json
?? tools/data/spectral_rigidity_results.json
?? tools/data/two_channel_boundary.json
?? tools/data/two_channel_cross_domain.json
?? tools/data/two_channel_shuffle_audit.json
?? tools/data/two_layer_universality.json
?? tools/data/valutatore_log.jsonl
?? tools/exp_3d_boundary_layers.py
?? tools/exp_boundary_coherence.py
?? tools/exp_boundary_shuffle_audit.py
?? tools/exp_brody_calibration.py
?? tools/exp_brody_flow.py
?? tools/exp_cross_domain_dipolar_direction.py
?? tools/exp_cross_observable_consistency.py
?? tools/exp_crossover_phase_test.py
?? tools/exp_dipolar_angle_reference.py
?? tools/exp_dipolar_crossover.py
?? tools/exp_dipolar_vector_scaling.py
?? tools/exp_magnitude_psd_from_acf.py
?? tools/exp_markov3_observable_hunt.py
?? tools/exp_markov_dipolar_decomposition.py
?? tools/exp_markov_k_direction.py
?? tools/exp_markov_layer_recovery_audit.py
?? tools/exp_markov_memory_by_gue_type.py
?? tools/exp_markov_psd_prediction.py
?? tools/exp_markov_scale_function.py
?? tools/exp_meta_tautology_test.py
?? tools/exp_mod3_scaling.py
?? tools/exp_mod3_vs_residual_ordering.py
?? tools/exp_modular_algebra_depth.py
?? tools/exp_modular_memory_spectrum.py
?? tools/exp_observable_rank_audit.py
?? tools/exp_selective_layer_decoupling.py
?? tools/exp_spectral_rigidity.py
?? tools/exp_two_channel_boundary.py
?? tools/exp_two_channel_cross_domain.py
?? tools/exp_two_channel_shuffle_audit.py
?? tools/exp_two_layer_universality.py

exec
/bin/bash -lc 'python -m json.tool tools/data/seme.json >/tmp/seme_validated.json && python -m json.tool tools/data/observable_rank_audit.json >/tmp/observable_rank_validated.json && python -m json.tool tools/data/observable_rank_audit_seed20260506.json >/tmp/observable_rank_replica_validated.json' in /opt/MM_D-ND
diff --git a/tools/data/reports/agent_20260505_0330.md b/tools/data/reports/agent_20260505_0330.md
new file mode 100644
index 0000000000000000000000000000000000000000..a2bffd69f55a8117fc1ca2f1167d2992e3247c9e
--- /dev/null
+++ b/tools/data/reports/agent_20260505_0330.md
@@ -0,0 +1,73 @@
+# Agent Report - Observable Rank Audit: Many Probes, One Boundary Coordinate
+**Date**: 2026-05-05 03:30
+**Piano**: 62
+**Tension explored**: META (0.5) + BOUNDARY (0.8)
+
+## Claim Under Test
+> Tutti i 11 test passano - verifica che non stiamo testando solo tautologie.
+
+## Question
+Quando SR, L1, L2, SR2 e triple_var reagiscono al partial shuffle, stanno misurando direzioni indipendenti del confine o una sola coordinata latente vista da osservabili diverse?
+
+## Experiment Design
+- **Scope atomico**: 30,000 gap per dominio; alphas partial-shuffle = 19 punti da 0.05 a 0.95; 24 trial per alpha; 72 full-shuffle per baseline.
+- **Domini**: prime gaps, GUE gaps, Poisson iid exponential gaps.
+- **Osservabili**: SR, L1, L2, SR2, triple_var.
+- **Null baseline**: full shuffle della stessa sequenza. Il dato Poisson e' controllo di assenza di segnale originale-vs-shuffle.
+- **Metrica META**: matrice delle retention curve normalizzate rispetto al full shuffle; PCA sulla matrice alpha x osservabili; rango effettivo entropico delle energie singolari.
+- **Robustezza minima**: seed principale 20260505; replica seed 20260506 salvata separatamente.
+
+## Results
+Seed principale 20260505:
+
+| Domain | PC1 variance | Effective rank | mean abs corr | z SR | z L1 | z L2 | z SR2 | z triple_var |
+|---|---:|---:|---:|---:|---:|---:|---:|---:|
+| Primes | 0.989 | 1.069 | 0.987 | -19.3 | -7.7 | -3.0 | -3.7 | -8.0 |
+| GUE | 0.997 | 1.022 | 0.996 | -6.6 | +21.4 | +37.9 | +18.0 | +36.3 |
+| Poisson | 0.925 | 1.391 | 0.351 | -0.6 | +0.2 | +1.2 | -0.0 | +0.8 |
+
+Replica seed 20260506:
+
+| Domain | PC1 variance | Effective rank | mean abs corr |
+|---|---:|---:|---:|
+| Primes | 0.988 | 1.076 | 0.985 |
+| GUE | 0.997 | 1.019 | 0.997 |
+| Poisson | 0.675 | 2.394 | 0.640 |
+
+PC1 loadings nel seed principale:
+
+| Domain | SR | L1 | L2 | SR2 | triple_var |
+|---|---:|---:|---:|---:|---:|
+| Primes | -0.450 | -0.449 | -0.443 | -0.446 | -0.448 |
+| GUE | -0.446 | -0.449 | -0.447 | -0.446 | -0.448 |
+
+## Key Findings
+1. **Nel perimetro partial-shuffle, primes e GUE hanno segnale forte ma quasi monodimensionale.** Per primes, tutte le osservabili hanno z originale-vs-shuffle almeno |3.0| e la prima componente spiega 98.9% della varianza delle retention curve. Per GUE il collasso e' ancora piu' stretto: 99.7%.
+
+2. **La somiglianza dei loadings e' il dato operativo.** Nei domini strutturati, PC1 carica SR, L1, L2, SR2 e triple_var quasi uniformemente. Questo non dice che le osservabili siano identiche in generale; dice che sotto partial shuffle uniforme misurano soprattutto la stessa coordinata di distruzione dell'ordine.
+
+3. **Poisson non supporta un claim di rango.** Nel seed principale Poisson ha PC1 alto, ma tutti gli z originale-vs-shuffle sono sotto |1.2|; nella replica il rango cambia molto. Quindi il rango Poisson qui e' rumore di baseline, non struttura.
+
+4. **Il risultato restringe il linguaggio dei cicli precedenti.** I test layer/Markov restano utili per classificare sensibilita' locali, ma non vanno contati come prove indipendenti del confine quando sono misurati lungo la stessa perturbazione partial-shuffle.
+
+## Verdict
+**CONSTRAINT on META + BOUNDARY**: nel perimetro testato, il confine partial-shuffle ha una coordinata latente dominante. Le osservabili SR/L1/L2/SR2/triple_var sono probes validi, ma non cinque evidenze indipendenti della transizione. La consecutio corretta e' cercare perturbazioni selettive ortogonali, non aggiungere altri osservabili scalari sulla stessa curva.
+
+## Bicono della scoperta
+- **Due radici**: molte osservabili che passano il test; una coordinata latente che le assorbe sotto partial shuffle.
+- **Singolare**: la curva di distruzione dell'ordine prima della scelta dell'osservabile.
+- **Invariante di passaggio**: nei domini strutturati, la retention resta quasi collineare tra osservabili anche quando il segno z e la magnitudine assoluta differiscono.
+- **Campo di possibilita**: qui diventa possibile separare "probe valido" da "evidenza indipendente"; qui diventa non-possibile contare SR, L1, L2, SR2 e triple_var come cinque conferme autonome del medesimo confine partial-shuffle.
+
+## Auto-audit: 5 lenti
+- **L1 hard constraint vs bias**: nessun claim di zero, sempre, never, impossibile. Il claim e' limitato a PC1/rango nel perimetro misurato.
+- **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.
+- **L3 no silent patching**: il claim META originale non e' dichiarato "risolto"; e' ristretto. I test possono essere validi ma ridondanti lungo partial shuffle.
+- **L4 edge cases**: L2 primes ha z=-3.0, piu' debole degli altri; non viene usato come prova forte isolata.
+- **L5 re-discovery**: PCA/rango effettivo sono strumenti classici di ridondanza statistica. Non taggo NEW; il contributo e' il vincolo operativo sul perimetro D-ND Markov/boundary.
+
+## Files
+- Script: `tools/exp_observable_rank_audit.py`
+- Data principale: `tools/data/observable_rank_audit.json`
+- Replica: `tools/data/observable_rank_audit_seed20260506.json`
+- Report: `tools/data/reports/agent_20260505_0330.md`
diff --git a/tools/data/seme.json b/tools/data/seme.json
index d55f53f6b19158acb3427690823d990893ea9828..f08a074823c56dec3126afbb09a2c57b9dee3aa5
--- a/tools/data/seme.json
+++ b/tools/data/seme.json
@@ -91,7 +91,8 @@
   ],
   "potenziale_bloccato": [],
   "varianza": [
-    "Tensioni risolte: {'DUALITA_DIPOLARE_VS_ILLUSORIA', 'METRIC_TENSOR', 'TENSIONE_ENTITA', 'TRASCENDENZA_LIMITE', 'G_POTENZIALE_NULLA', 'PIANO_PRIMARIO_DUE_ASSIOMI', 'META_LAYER_RECOVERY_AUDIT'}"
+    "Tensioni risolte: {'DUALITA_DIPOLARE_VS_ILLUSORIA', 'METRIC_TENSOR', 'TENSIONE_ENTITA', 'TRASCENDENZA_LIMITE', 'G_POTENZIALE_NULLA', 'PIANO_PRIMARIO_DUE_ASSIOMI', 'META_LAYER_RECOVERY_AUDIT'}",
+    "Report agent_20260505_0330: observable-rank audit su primes/GUE/Poisson con SR,L1,L2,SR2,triple_var. Nel perimetro partial-shuffle uniforme, primes e GUE hanno segnale forte ma rango effettivo ~1 (PC1 primes=0.989, GUE=0.997; replica stabile). Vincolo META+BOUNDARY: probes validi non equivalgono a evidenze indipendenti lungo la stessa perturbazione. Consecutio: cercare perturbazioni selettive ortogonali, non aggiungere osservabili scalari sulla stessa curva."
   ],
   "filtro": {
     "promosse": 8,
@@ -265,4 +266,4 @@
       "timestamp": "2026-04-04T19:34:08.014Z"
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/tools/exp_observable_rank_audit.py b/tools/exp_observable_rank_audit.py
new file mode 100644
index 0000000000000000000000000000000000000000..f2acafc5960c8a35964e0ab521cf70569eefe27f
--- /dev/null
+++ b/tools/exp_observable_rank_audit.py
@@ -0,0 +1,229 @@
+#!/usr/bin/env python3
+"""
+exp_observable_rank_audit.py - META audit for observable redundancy.
+
+Recent runs found many Markov/crossover observables that react coherently under
+partial shuffle. This script asks whether those observables carry independent
+directions or mostly re-measure one latent boundary coordinate.
+
+It measures retention curves from alpha-partial shuffles, then reports:
+  - original-vs-full-shuffle z for each observable
+  - PCA energy of the retention matrix across alpha
+  - effective rank of that matrix
+  - pairwise correlations between retention curves
+
+The script measures data only. The report decides the structural claim.
+"""
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from exp_3d_boundary_layers import get_primes, gue_gaps, partial_shuffle
+
+
+def obs_spacing_ratio(gaps):
+    r = np.minimum(gaps[:-1], gaps[1:]) / np.maximum(gaps[:-1], gaps[1:])
+    r = r[np.isfinite(r)]
+    return float(np.mean(r)) if len(r) else 0.0
+
+
+def obs_lag_acf(gaps, lag):
+    g = gaps - np.mean(gaps)
+    c0 = np.mean(g * g)
+    if c0 == 0:
+        return 0.0
+    return float(np.mean(g[:-lag] * g[lag:]) / c0)
+
+
+def obs_sr2(gaps):
+    r = np.minimum(gaps[:-2], gaps[2:]) / np.maximum(gaps[:-2], gaps[2:])
+    r = r[np.isfinite(r)]
+    return float(np.mean(r)) if len(r) else 0.0
+
+
+def obs_triple_var(gaps):
+    triples = gaps[:-2] + gaps[1:-1] + gaps[2:]
+    v = np.var(gaps)
+    if v == 0:
+        return 0.0
+    return float(np.var(triples) / v)
+
+
+OBSERVABLES = {
+    "SR": obs_spacing_ratio,
+    "L1": lambda gaps: obs_lag_acf(gaps, 1),
+    "L2": lambda gaps: obs_lag_acf(gaps, 2),
+    "SR2": obs_sr2,
+    "triple_var": obs_triple_var,
+}
+
+
+def measure(gaps):
+    return {name: fn(gaps) for name, fn in OBSERVABLES.items()}
+
+
+def full_shuffle_baseline(gaps, n_trials, rng):
+    vals = {name: [] for name in OBSERVABLES}
+    for _ in range(n_trials):
+        s = rng.permutation(gaps)
+        row = measure(s)
+        for name, value in row.items():
+            vals[name].append(value)
+    return {
+        name: {
+            "mean": float(np.mean(x)),
+            "std": float(np.std(x, ddof=1)) if len(x) > 1 else 0.0,
+        }
+        for name, x in vals.items()
+    }
+
+
+def retention_curves(gaps, alphas, n_trials, originals, baseline, rng):
+    rows = []
+    for alpha in alphas:
+        vals = {name: [] for name in OBSERVABLES}
+        for _ in range(n_trials):
+            s = partial_shuffle(gaps, float(alpha), rng)
+            row = measure(s)
+            for name, value in row.items():
+                vals[name].append(value)
+
+        out = {"alpha": float(alpha)}
+        for name in OBSERVABLES:
+            mean = float(np.mean(vals[name]))
+            denom = originals[name] - baseline[name]["mean"]
+            retention = (mean - baseline[name]["mean"]) / denom if abs(denom) > 1e-12 else 0.0
+            out[name] = {
+                "mean": mean,
+                "std": float(np.std(vals[name], ddof=1)) if len(vals[name]) > 1 else 0.0,
+                "retention": float(retention),
+            }
+        rows.append(out)
+    return rows
+
+
+def pca_summary(rows):
+    names = list(OBSERVABLES)
+    matrix = np.array([[row[name]["retention"] for name in names] for row in rows], dtype=float)
+    matrix = matrix - np.mean(matrix, axis=0, keepdims=True)
+
+    _, singular, vt = np.linalg.svd(matrix, full_matrices=False)
+    energy = singular * singular
+    if np.sum(energy) <= 1e-15:
+        explained = np.zeros_like(energy)
+        effective_rank = 0.0
+    else:
+        explained = energy / np.sum(energy)
+        positive = explained[explained > 1e-15]
+        effective_rank = float(np.exp(-np.sum(positive * np.log(positive))))
+
+    corr = np.corrcoef(matrix, rowvar=False)
+    abs_corr = np.abs(corr)
+    upper = abs_corr[np.triu_indices_from(abs_corr, k=1)]
+
+    return {
+        "observables": names,
+        "singular_values": [float(x) for x in singular],
+        "explained_variance": [float(x) for x in explained],
+        "effective_rank": effective_rank,
+        "pc1_loadings": {name: float(vt[0, i]) for i, name in enumerate(names)} if len(vt) else {},
+        "mean_abs_pairwise_corr": float(np.mean(upper)) if len(upper) else 0.0,
+        "min_abs_pairwise_corr": float(np.min(upper)) if len(upper) else 0.0,
+        "max_abs_pairwise_corr": float(np.max(upper)) if len(upper) else 0.0,
+    }
+
+
+def analyze_sequence(name, gaps, alphas, n_trials, n_baseline, rng):
+    originals = measure(gaps)
+    baseline = full_shuffle_baseline(gaps, n_baseline, rng)
+    rows = retention_curves(gaps, alphas, n_trials, originals, baseline, rng)
+
+    z = {}
+    for obs_name in OBSERVABLES:
+        std = baseline[obs_name]["std"]
+        z[obs_name] = float((originals[obs_name] - baseline[obs_name]["mean"]) / std) if std > 1e-12 else 0.0
+
+    return {
+        "n_gaps": int(len(gaps)),
+        "originals": originals,
+        "full_shuffle_baseline": baseline,
+        "original_vs_shuffle_z": z,
+        "retention_curves": rows,
+        "pca": pca_summary(rows),
+    }
+
+
+def build_sequences(n_gaps, rng):
+    primes = get_primes(n_gaps * 24)[: n_gaps + 1]
+    prime_gaps = np.diff(primes).astype(float)
+
+    gue = gue_gaps(160, max(8, n_gaps // 160 + 1), rng).astype(float)
+    gue = gue[:n_gaps]
+
+    poisson = rng.exponential(1.0, size=n_gaps).astype(float)
+    return {
+        "primes": prime_gaps,
+        "gue": gue,
+        "poisson": poisson,
+    }
+
+
+def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505):
+    rng = np.random.default_rng(seed)
+    alphas = np.linspace(0.05, 0.95, n_alpha)
+    sequences = build_sequences(n_gaps, rng)
+
+    output = {
+        "experiment": "observable_rank_audit",
+        "question": "Do crossover observables contain independent directions or one latent boundary coordinate?",
+        "params": {
+            "n_gaps": int(n_gaps),
+            "n_alpha": int(n_alpha),
+            "n_trials": int(n_trials),
+            "n_baseline": int(n_baseline),
+            "seed": int(seed),
+        },
+        "sequences": {},
+    }
+
+    print(f"n_gaps={n_gaps}, n_alpha={n_alpha}, n_trials={n_trials}, n_baseline={n_baseline}, seed={seed}")
+    print(f"{'sequence':<10} {'pc1':>8} {'eff_rank':>9} {'mean|corr|':>11}  z(SR,L1,L2,SR2,triple)")
+    print("-" * 92)
+    for seq_name, gaps in sequences.items():
+        result = analyze_sequence(seq_name, gaps, alphas, n_trials, n_baseline, rng)
+        output["sequences"][seq_name] = result
+        pca = result["pca"]
+        z = result["original_vs_shuffle_z"]
+        z_text = ", ".join(f"{obs}={z[obs]:+.1f}" for obs in OBSERVABLES)
+        pc1 = pca["explained_variance"][0] if pca["explained_variance"] else 0.0
+        print(f"{seq_name:<10} {pc1:>8.3f} {pca['effective_rank']:>9.3f} {pca['mean_abs_pairwise_corr']:>11.3f}  {z_text}")
+
+    out_path = Path("tools/data/observable_rank_audit.json")
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+    print(f"\nSaved to {out_path}")
+    return output
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n_gaps", type=int, default=30000)
+    parser.add_argument("--n_alpha", type=int, default=19)
+    parser.add_argument("--n_trials", type=int, default=24)
+    parser.add_argument("--n_baseline", type=int, default=72)
+    parser.add_argument("--seed", type=int, default=20260505)
+    args = parser.parse_args()
+    run(
+        n_gaps=args.n_gaps,
+        n_alpha=args.n_alpha,
+        n_trials=args.n_trials,
+        n_baseline=args.n_baseline,
+        seed=args.seed,
+    )
+
+
+if __name__ == "__main__":
+    main()

 succeeded in 125ms:
exec
/bin/bash -lc 'ls tools/structural_check.py && python tools/structural_check.py tools/exp_observable_rank_audit.py tools/data/reports/agent_20260505_0330.md tools/data/seme.json' in /opt/MM_D-ND
 succeeded in 0ms:
tools/structural_check.py
Scansione 3 file...
Nessun anti-pattern trovato.

diff --git a/tools/data/reports/agent_20260505_0330.md b/tools/data/reports/agent_20260505_0330.md
new file mode 100644
index 0000000000000000000000000000000000000000..a2bffd69f55a8117fc1ca2f1167d2992e3247c9e
--- /dev/null
+++ b/tools/data/reports/agent_20260505_0330.md
@@ -0,0 +1,73 @@
+# Agent Report - Observable Rank Audit: Many Probes, One Boundary Coordinate
+**Date**: 2026-05-05 03:30
+**Piano**: 62
+**Tension explored**: META (0.5) + BOUNDARY (0.8)
+
+## Claim Under Test
+> Tutti i 11 test passano - verifica che non stiamo testando solo tautologie.
+
+## Question
+Quando SR, L1, L2, SR2 e triple_var reagiscono al partial shuffle, stanno misurando direzioni indipendenti del confine o una sola coordinata latente vista da osservabili diverse?
+
+## Experiment Design
+- **Scope atomico**: 30,000 gap per dominio; alphas partial-shuffle = 19 punti da 0.05 a 0.95; 24 trial per alpha; 72 full-shuffle per baseline.
+- **Domini**: prime gaps, GUE gaps, Poisson iid exponential gaps.
+- **Osservabili**: SR, L1, L2, SR2, triple_var.
+- **Null baseline**: full shuffle della stessa sequenza. Il dato Poisson e' controllo di assenza di segnale originale-vs-shuffle.
+- **Metrica META**: matrice delle retention curve normalizzate rispetto al full shuffle; PCA sulla matrice alpha x osservabili; rango effettivo entropico delle energie singolari.
+- **Robustezza minima**: seed principale 20260505; replica seed 20260506 salvata separatamente.
+
+## Results
+Seed principale 20260505:
+
+| Domain | PC1 variance | Effective rank | mean abs corr | z SR | z L1 | z L2 | z SR2 | z triple_var |
+|---|---:|---:|---:|---:|---:|---:|---:|---:|
+| Primes | 0.989 | 1.069 | 0.987 | -19.3 | -7.7 | -3.0 | -3.7 | -8.0 |
+| GUE | 0.997 | 1.022 | 0.996 | -6.6 | +21.4 | +37.9 | +18.0 | +36.3 |
+| Poisson | 0.925 | 1.391 | 0.351 | -0.6 | +0.2 | +1.2 | -0.0 | +0.8 |
+
+Replica seed 20260506:
+
+| Domain | PC1 variance | Effective rank | mean abs corr |
+|---|---:|---:|---:|
+| Primes | 0.988 | 1.076 | 0.985 |
+| GUE | 0.997 | 1.019 | 0.997 |
+| Poisson | 0.675 | 2.394 | 0.640 |
+
+PC1 loadings nel seed principale:
+
+| Domain | SR | L1 | L2 | SR2 | triple_var |
+|---|---:|---:|---:|---:|---:|
+| Primes | -0.450 | -0.449 | -0.443 | -0.446 | -0.448 |
+| GUE | -0.446 | -0.449 | -0.447 | -0.446 | -0.448 |
+
+## Key Findings
+1. **Nel perimetro partial-shuffle, primes e GUE hanno segnale forte ma quasi monodimensionale.** Per primes, tutte le osservabili hanno z originale-vs-shuffle almeno |3.0| e la prima componente spiega 98.9% della varianza delle retention curve. Per GUE il collasso e' ancora piu' stretto: 99.7%.
+
+2. **La somiglianza dei loadings e' il dato operativo.** Nei domini strutturati, PC1 carica SR, L1, L2, SR2 e triple_var quasi uniformemente. Questo non dice che le osservabili siano identiche in generale; dice che sotto partial shuffle uniforme misurano soprattutto la stessa coordinata di distruzione dell'ordine.
+
+3. **Poisson non supporta un claim di rango.** Nel seed principale Poisson ha PC1 alto, ma tutti gli z originale-vs-shuffle sono sotto |1.2|; nella replica il rango cambia molto. Quindi il rango Poisson qui e' rumore di baseline, non struttura.
+
+4. **Il risultato restringe il linguaggio dei cicli precedenti.** I test layer/Markov restano utili per classificare sensibilita' locali, ma non vanno contati come prove indipendenti del confine quando sono misurati lungo la stessa perturbazione partial-shuffle.
+
+## Verdict
+**CONSTRAINT on META + BOUNDARY**: nel perimetro testato, il confine partial-shuffle ha una coordinata latente dominante. Le osservabili SR/L1/L2/SR2/triple_var sono probes validi, ma non cinque evidenze indipendenti della transizione. La consecutio corretta e' cercare perturbazioni selettive ortogonali, non aggiungere altri osservabili scalari sulla stessa curva.
+
+## Bicono della scoperta
+- **Due radici**: molte osservabili che passano il test; una coordinata latente che le assorbe sotto partial shuffle.
+- **Singolare**: la curva di distruzione dell'ordine prima della scelta dell'osservabile.
+- **Invariante di passaggio**: nei domini strutturati, la retention resta quasi collineare tra osservabili anche quando il segno z e la magnitudine assoluta differiscono.
+- **Campo di possibilita**: qui diventa possibile separare "probe valido" da "evidenza indipendente"; qui diventa non-possibile contare SR, L1, L2, SR2 e triple_var come cinque conferme autonome del medesimo confine partial-shuffle.
+
+## Auto-audit: 5 lenti
+- **L1 hard constraint vs bias**: nessun claim di zero, sempre, never, impossibile. Il claim e' limitato a PC1/rango nel perimetro misurato.
+- **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.
+- **L3 no silent patching**: il claim META originale non e' dichiarato "risolto"; e' ristretto. I test possono essere validi ma ridondanti lungo partial shuffle.
+- **L4 edge cases**: L2 primes ha z=-3.0, piu' debole degli altri; non viene usato come prova forte isolata.
+- **L5 re-discovery**: PCA/rango effettivo sono strumenti classici di ridondanza statistica. Non taggo NEW; il contributo e' il vincolo operativo sul perimetro D-ND Markov/boundary.
+
+## Files
+- Script: `tools/exp_observable_rank_audit.py`
+- Data principale: `tools/data/observable_rank_audit.json`
+- Replica: `tools/data/observable_rank_audit_seed20260506.json`
+- Report: `tools/data/reports/agent_20260505_0330.md`
diff --git a/tools/data/seme.json b/tools/data/seme.json
index d55f53f6b19158acb3427690823d990893ea9828..f08a074823c56dec3126afbb09a2c57b9dee3aa5
--- a/tools/data/seme.json
+++ b/tools/data/seme.json
@@ -91,7 +91,8 @@
   ],
   "potenziale_bloccato": [],
   "varianza": [
-    "Tensioni risolte: {'DUALITA_DIPOLARE_VS_ILLUSORIA', 'METRIC_TENSOR', 'TENSIONE_ENTITA', 'TRASCENDENZA_LIMITE', 'G_POTENZIALE_NULLA', 'PIANO_PRIMARIO_DUE_ASSIOMI', 'META_LAYER_RECOVERY_AUDIT'}"
+    "Tensioni risolte: {'DUALITA_DIPOLARE_VS_ILLUSORIA', 'METRIC_TENSOR', 'TENSIONE_ENTITA', 'TRASCENDENZA_LIMITE', 'G_POTENZIALE_NULLA', 'PIANO_PRIMARIO_DUE_ASSIOMI', 'META_LAYER_RECOVERY_AUDIT'}",
+    "Report agent_20260505_0330: observable-rank audit su primes/GUE/Poisson con SR,L1,L2,SR2,triple_var. Nel perimetro partial-shuffle uniforme, primes e GUE hanno segnale forte ma rango effettivo ~1 (PC1 primes=0.989, GUE=0.997; replica stabile). Vincolo META+BOUNDARY: probes validi non equivalgono a evidenze indipendenti lungo la stessa perturbazione. Consecutio: cercare perturbazioni selettive ortogonali, non aggiungere osservabili scalari sulla stessa curva."
   ],
   "filtro": {
     "promosse": 8,
@@ -265,4 +266,4 @@
       "timestamp": "2026-04-04T19:34:08.014Z"
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/tools/exp_observable_rank_audit.py b/tools/exp_observable_rank_audit.py
new file mode 100644
index 0000000000000000000000000000000000000000..f2acafc5960c8a35964e0ab521cf70569eefe27f
--- /dev/null
+++ b/tools/exp_observable_rank_audit.py
@@ -0,0 +1,229 @@
+#!/usr/bin/env python3
+"""
+exp_observable_rank_audit.py - META audit for observable redundancy.
+
+Recent runs found many Markov/crossover observables that react coherently under
+partial shuffle. This script asks whether those observables carry independent
+directions or mostly re-measure one latent boundary coordinate.
+
+It measures retention curves from alpha-partial shuffles, then reports:
+  - original-vs-full-shuffle z for each observable
+  - PCA energy of the retention matrix across alpha
+  - effective rank of that matrix
+  - pairwise correlations between retention curves
+
+The script measures data only. The report decides the structural claim.
+"""
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from exp_3d_boundary_layers import get_primes, gue_gaps, partial_shuffle
+
+
+def obs_spacing_ratio(gaps):
+    r = np.minimum(gaps[:-1], gaps[1:]) / np.maximum(gaps[:-1], gaps[1:])
+    r = r[np.isfinite(r)]
+    return float(np.mean(r)) if len(r) else 0.0
+
+
+def obs_lag_acf(gaps, lag):
+    g = gaps - np.mean(gaps)
+    c0 = np.mean(g * g)
+    if c0 == 0:
+        return 0.0
+    return float(np.mean(g[:-lag] * g[lag:]) / c0)
+
+
+def obs_sr2(gaps):
+    r = np.minimum(gaps[:-2], gaps[2:]) / np.maximum(gaps[:-2], gaps[2:])
+    r = r[np.isfinite(r)]
+    return float(np.mean(r)) if len(r) else 0.0
+
+
+def obs_triple_var(gaps):
+    triples = gaps[:-2] + gaps[1:-1] + gaps[2:]
+    v = np.var(gaps)
+    if v == 0:
+        return 0.0
+    return float(np.var(triples) / v)
+
+
+OBSERVABLES = {
+    "SR": obs_spacing_ratio,
+    "L1": lambda gaps: obs_lag_acf(gaps, 1),
+    "L2": lambda gaps: obs_lag_acf(gaps, 2),
+    "SR2": obs_sr2,
+    "triple_var": obs_triple_var,
+}
+
+
+def measure(gaps):
+    return {name: fn(gaps) for name, fn in OBSERVABLES.items()}
+
+
+def full_shuffle_baseline(gaps, n_trials, rng):
+    vals = {name: [] for name in OBSERVABLES}
+    for _ in range(n_trials):
+        s = rng.permutation(gaps)
+        row = measure(s)
+        for name, value in row.items():
+            vals[name].append(value)
+    return {
+        name: {
+            "mean": float(np.mean(x)),
+            "std": float(np.std(x, ddof=1)) if len(x) > 1 else 0.0,
+        }
+        for name, x in vals.items()
+    }
+
+
+def retention_curves(gaps, alphas, n_trials, originals, baseline, rng):
+    rows = []
+    for alpha in alphas:
+        vals = {name: [] for name in OBSERVABLES}
+        for _ in range(n_trials):
+            s = partial_shuffle(gaps, float(alpha), rng)
+            row = measure(s)
+            for name, value in row.items():
+                vals[name].append(value)
+
+        out = {"alpha": float(alpha)}
+        for name in OBSERVABLES:
+            mean = float(np.mean(vals[name]))
+            denom = originals[name] - baseline[name]["mean"]
+            retention = (mean - baseline[name]["mean"]) / denom if abs(denom) > 1e-12 else 0.0
+            out[name] = {
+                "mean": mean,
+                "std": float(np.std(vals[name], ddof=1)) if len(vals[name]) > 1 else 0.0,
+                "retention": float(retention),
+            }
+        rows.append(out)
+    return rows
+
+
+def pca_summary(rows):
+    names = list(OBSERVABLES)
+    matrix = np.array([[row[name]["retention"] for name in names] for row in rows], dtype=float)
+    matrix = matrix - np.mean(matrix, axis=0, keepdims=True)
+
+    _, singular, vt = np.linalg.svd(matrix, full_matrices=False)
+    energy = singular * singular
+    if np.sum(energy) <= 1e-15:
+        explained = np.zeros_like(energy)
+        effective_rank = 0.0
+    else:
+        explained = energy / np.sum(energy)
+        positive = explained[explained > 1e-15]
+        effective_rank = float(np.exp(-np.sum(positive * np.log(positive))))
+
+    corr = np.corrcoef(matrix, rowvar=False)
+    abs_corr = np.abs(corr)
+    upper = abs_corr[np.triu_indices_from(abs_corr, k=1)]
+
+    return {
+        "observables": names,
+        "singular_values": [float(x) for x in singular],
+        "explained_variance": [float(x) for x in explained],
+        "effective_rank": effective_rank,
+        "pc1_loadings": {name: float(vt[0, i]) for i, name in enumerate(names)} if len(vt) else {},
+        "mean_abs_pairwise_corr": float(np.mean(upper)) if len(upper) else 0.0,
+        "min_abs_pairwise_corr": float(np.min(upper)) if len(upper) else 0.0,
+        "max_abs_pairwise_corr": float(np.max(upper)) if len(upper) else 0.0,
+    }
+
+
+def analyze_sequence(name, gaps, alphas, n_trials, n_baseline, rng):
+    originals = measure(gaps)
+    baseline = full_shuffle_baseline(gaps, n_baseline, rng)
+    rows = retention_curves(gaps, alphas, n_trials, originals, baseline, rng)
+
+    z = {}
+    for obs_name in OBSERVABLES:
+        std = baseline[obs_name]["std"]
+        z[obs_name] = float((originals[obs_name] - baseline[obs_name]["mean"]) / std) if std > 1e-12 else 0.0
+
+    return {
+        "n_gaps": int(len(gaps)),
+        "originals": originals,
+        "full_shuffle_baseline": baseline,
+        "original_vs_shuffle_z": z,
+        "retention_curves": rows,
+        "pca": pca_summary(rows),
+    }
+
+
+def build_sequences(n_gaps, rng):
+    primes = get_primes(n_gaps * 24)[: n_gaps + 1]
+    prime_gaps = np.diff(primes).astype(float)
+
+    gue = gue_gaps(160, max(8, n_gaps // 160 + 1), rng).astype(float)
+    gue = gue[:n_gaps]
+
+    poisson = rng.exponential(1.0, size=n_gaps).astype(float)
+    return {
+        "primes": prime_gaps,
+        "gue": gue,
+        "poisson": poisson,
+    }
+
+
+def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505):
+    rng = np.random.default_rng(seed)
+    alphas = np.linspace(0.05, 0.95, n_alpha)
+    sequences = build_sequences(n_gaps, rng)
+
+    output = {
+        "experiment": "observable_rank_audit",
+        "question": "Do crossover observables contain independent directions or one latent boundary coordinate?",
+        "params": {
+            "n_gaps": int(n_gaps),
+            "n_alpha": int(n_alpha),
+            "n_trials": int(n_trials),
+            "n_baseline": int(n_baseline),
+            "seed": int(seed),
+        },
+        "sequences": {},
+    }
+
+    print(f"n_gaps={n_gaps}, n_alpha={n_alpha}, n_trials={n_trials}, n_baseline={n_baseline}, seed={seed}")
+    print(f"{'sequence':<10} {'pc1':>8} {'eff_rank':>9} {'mean|corr|':>11}  z(SR,L1,L2,SR2,triple)")
+    print("-" * 92)
+    for seq_name, gaps in sequences.items():
+        result = analyze_sequence(seq_name, gaps, alphas, n_trials, n_baseline, rng)
+        output["sequences"][seq_name] = result
+        pca = result["pca"]
+        z = result["original_vs_shuffle_z"]
+        z_text = ", ".join(f"{obs}={z[obs]:+.1f}" for obs in OBSERVABLES)
+        pc1 = pca["explained_variance"][0] if pca["explained_variance"] else 0.0
+        print(f"{seq_name:<10} {pc1:>8.3f} {pca['effective_rank']:>9.3f} {pca['mean_abs_pairwise_corr']:>11.3f}  {z_text}")
+
+    out_path = Path("tools/data/observable_rank_audit.json")
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+    print(f"\nSaved to {out_path}")
+    return output
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n_gaps", type=int, default=30000)
+    parser.add_argument("--n_alpha", type=int, default=19)
+    parser.add_argument("--n_trials", type=int, default=24)
+    parser.add_argument("--n_baseline", type=int, default=72)
+    parser.add_argument("--seed", type=int, default=20260505)
+    args = parser.parse_args()
+    run(
+        n_gaps=args.n_gaps,
+        n_alpha=args.n_alpha,
+        n_trials=args.n_trials,
+        n_baseline=args.n_baseline,
+        seed=args.seed,
+    )
+
+
+if __name__ == "__main__":
+    main()

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

Ho creato lo strumento riusabile [exp_observable_rank_audit.py](/opt/MM_D-ND/tools/exp_observable_rank_audit.py), generato i dati in `tools/data/observable_rank_audit.json` e una replica seed in `tools/data/observable_rank_audit_seed20260506.json`, poi aggiornato `tools/data/seme.json`.

Risultato principale: nel perimetro partial-shuffle, primes e GUE hanno segnale forte ma quasi monodimensionale. Quindi SR/L1/L2/SR2/triple_var restano probe validi, ma non vanno contati come evidenze indipendenti della stessa transizione.

Verifiche eseguite:
- `python -m py_compile tools/exp_observable_rank_audit.py`
- validazione JSON su `seme.json` e output dati
- `python tools/structural_check.py ...` senza anti-pattern trovati.
diff --git a/tools/data/reports/agent_20260505_0330.md b/tools/data/reports/agent_20260505_0330.md
new file mode 100644
index 0000000000000000000000000000000000000000..a2bffd69f55a8117fc1ca2f1167d2992e3247c9e
--- /dev/null
+++ b/tools/data/reports/agent_20260505_0330.md
@@ -0,0 +1,73 @@
+# Agent Report - Observable Rank Audit: Many Probes, One Boundary Coordinate
+**Date**: 2026-05-05 03:30
+**Piano**: 62
+**Tension explored**: META (0.5) + BOUNDARY (0.8)
+
+## Claim Under Test
+> Tutti i 11 test passano - verifica che non stiamo testando solo tautologie.
+
+## Question
+Quando SR, L1, L2, SR2 e triple_var reagiscono al partial shuffle, stanno misurando direzioni indipendenti del confine o una sola coordinata latente vista da osservabili diverse?
+
+## Experiment Design
+- **Scope atomico**: 30,000 gap per dominio; alphas partial-shuffle = 19 punti da 0.05 a 0.95; 24 trial per alpha; 72 full-shuffle per baseline.
+- **Domini**: prime gaps, GUE gaps, Poisson iid exponential gaps.
+- **Osservabili**: SR, L1, L2, SR2, triple_var.
+- **Null baseline**: full shuffle della stessa sequenza. Il dato Poisson e' controllo di assenza di segnale originale-vs-shuffle.
+- **Metrica META**: matrice delle retention curve normalizzate rispetto al full shuffle; PCA sulla matrice alpha x osservabili; rango effettivo entropico delle energie singolari.
+- **Robustezza minima**: seed principale 20260505; replica seed 20260506 salvata separatamente.
+
+## Results
+Seed principale 20260505:
+
+| Domain | PC1 variance | Effective rank | mean abs corr | z SR | z L1 | z L2 | z SR2 | z triple_var |
+|---|---:|---:|---:|---:|---:|---:|---:|---:|
+| Primes | 0.989 | 1.069 | 0.987 | -19.3 | -7.7 | -3.0 | -3.7 | -8.0 |
+| GUE | 0.997 | 1.022 | 0.996 | -6.6 | +21.4 | +37.9 | +18.0 | +36.3 |
+| Poisson | 0.925 | 1.391 | 0.351 | -0.6 | +0.2 | +1.2 | -0.0 | +0.8 |
+
+Replica seed 20260506:
+
+| Domain | PC1 variance | Effective rank | mean abs corr |
+|---|---:|---:|---:|
+| Primes | 0.988 | 1.076 | 0.985 |
+| GUE | 0.997 | 1.019 | 0.997 |
+| Poisson | 0.675 | 2.394 | 0.640 |
+
+PC1 loadings nel seed principale:
+
+| Domain | SR | L1 | L2 | SR2 | triple_var |
+|---|---:|---:|---:|---:|---:|
+| Primes | -0.450 | -0.449 | -0.443 | -0.446 | -0.448 |
+| GUE | -0.446 | -0.449 | -0.447 | -0.446 | -0.448 |
+
+## Key Findings
+1. **Nel perimetro partial-shuffle, primes e GUE hanno segnale forte ma quasi monodimensionale.** Per primes, tutte le osservabili hanno z originale-vs-shuffle almeno |3.0| e la prima componente spiega 98.9% della varianza delle retention curve. Per GUE il collasso e' ancora piu' stretto: 99.7%.
+
+2. **La somiglianza dei loadings e' il dato operativo.** Nei domini strutturati, PC1 carica SR, L1, L2, SR2 e triple_var quasi uniformemente. Questo non dice che le osservabili siano identiche in generale; dice che sotto partial shuffle uniforme misurano soprattutto la stessa coordinata di distruzione dell'ordine.
+
+3. **Poisson non supporta un claim di rango.** Nel seed principale Poisson ha PC1 alto, ma tutti gli z originale-vs-shuffle sono sotto |1.2|; nella replica il rango cambia molto. Quindi il rango Poisson qui e' rumore di baseline, non struttura.
+
+4. **Il risultato restringe il linguaggio dei cicli precedenti.** I test layer/Markov restano utili per classificare sensibilita' locali, ma non vanno contati come prove indipendenti del confine quando sono misurati lungo la stessa perturbazione partial-shuffle.
+
+## Verdict
+**CONSTRAINT on META + BOUNDARY**: nel perimetro testato, il confine partial-shuffle ha una coordinata latente dominante. Le osservabili SR/L1/L2/SR2/triple_var sono probes validi, ma non cinque evidenze indipendenti della transizione. La consecutio corretta e' cercare perturbazioni selettive ortogonali, non aggiungere altri osservabili scalari sulla stessa curva.
+
+## Bicono della scoperta
+- **Due radici**: molte osservabili che passano il test; una coordinata latente che le assorbe sotto partial shuffle.
+- **Singolare**: la curva di distruzione dell'ordine prima della scelta dell'osservabile.
+- **Invariante di passaggio**: nei domini strutturati, la retention resta quasi collineare tra osservabili anche quando il segno z e la magnitudine assoluta differiscono.
+- **Campo di possibilita**: qui diventa possibile separare "probe valido" da "evidenza indipendente"; qui diventa non-possibile contare SR, L1, L2, SR2 e triple_var come cinque conferme autonome del medesimo confine partial-shuffle.
+
+## Auto-audit: 5 lenti
+- **L1 hard constraint vs bias**: nessun claim di zero, sempre, never, impossibile. Il claim e' limitato a PC1/rango nel perimetro misurato.
+- **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.
+- **L3 no silent patching**: il claim META originale non e' dichiarato "risolto"; e' ristretto. I test possono essere validi ma ridondanti lungo partial shuffle.
+- **L4 edge cases**: L2 primes ha z=-3.0, piu' debole degli altri; non viene usato come prova forte isolata.
+- **L5 re-discovery**: PCA/rango effettivo sono strumenti classici di ridondanza statistica. Non taggo NEW; il contributo e' il vincolo operativo sul perimetro D-ND Markov/boundary.
+
+## Files
+- Script: `tools/exp_observable_rank_audit.py`
+- Data principale: `tools/data/observable_rank_audit.json`
+- Replica: `tools/data/observable_rank_audit_seed20260506.json`
+- Report: `tools/data/reports/agent_20260505_0330.md`
diff --git a/tools/data/seme.json b/tools/data/seme.json
index d55f53f6b19158acb3427690823d990893ea9828..f08a074823c56dec3126afbb09a2c57b9dee3aa5
--- a/tools/data/seme.json
+++ b/tools/data/seme.json
@@ -91,7 +91,8 @@
   ],
   "potenziale_bloccato": [],
   "varianza": [
-    "Tensioni risolte: {'DUALITA_DIPOLARE_VS_ILLUSORIA', 'METRIC_TENSOR', 'TENSIONE_ENTITA', 'TRASCENDENZA_LIMITE', 'G_POTENZIALE_NULLA', 'PIANO_PRIMARIO_DUE_ASSIOMI', 'META_LAYER_RECOVERY_AUDIT'}"
+    "Tensioni risolte: {'DUALITA_DIPOLARE_VS_ILLUSORIA', 'METRIC_TENSOR', 'TENSIONE_ENTITA', 'TRASCENDENZA_LIMITE', 'G_POTENZIALE_NULLA', 'PIANO_PRIMARIO_DUE_ASSIOMI', 'META_LAYER_RECOVERY_AUDIT'}",
+    "Report agent_20260505_0330: observable-rank audit su primes/GUE/Poisson con SR,L1,L2,SR2,triple_var. Nel perimetro partial-shuffle uniforme, primes e GUE hanno segnale forte ma rango effettivo ~1 (PC1 primes=0.989, GUE=0.997; replica stabile). Vincolo META+BOUNDARY: probes validi non equivalgono a evidenze indipendenti lungo la stessa perturbazione. Consecutio: cercare perturbazioni selettive ortogonali, non aggiungere osservabili scalari sulla stessa curva."
   ],
   "filtro": {
     "promosse": 8,
@@ -265,4 +266,4 @@
       "timestamp": "2026-04-04T19:34:08.014Z"
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/tools/exp_observable_rank_audit.py b/tools/exp_observable_rank_audit.py
new file mode 100644
index 0000000000000000000000000000000000000000..f2acafc5960c8a35964e0ab521cf70569eefe27f
--- /dev/null
+++ b/tools/exp_observable_rank_audit.py
@@ -0,0 +1,229 @@
+#!/usr/bin/env python3
+"""
+exp_observable_rank_audit.py - META audit for observable redundancy.
+
+Recent runs found many Markov/crossover observables that react coherently under
+partial shuffle. This script asks whether those observables carry independent
+directions or mostly re-measure one latent boundary coordinate.
+
+It measures retention curves from alpha-partial shuffles, then reports:
+  - original-vs-full-shuffle z for each observable
+  - PCA energy of the retention matrix across alpha
+  - effective rank of that matrix
+  - pairwise correlations between retention curves
+
+The script measures data only. The report decides the structural claim.
+"""
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from exp_3d_boundary_layers import get_primes, gue_gaps, partial_shuffle
+
+
+def obs_spacing_ratio(gaps):
+    r = np.minimum(gaps[:-1], gaps[1:]) / np.maximum(gaps[:-1], gaps[1:])
+    r = r[np.isfinite(r)]
+    return float(np.mean(r)) if len(r) else 0.0
+
+
+def obs_lag_acf(gaps, lag):
+    g = gaps - np.mean(gaps)
+    c0 = np.mean(g * g)
+    if c0 == 0:
+        return 0.0
+    return float(np.mean(g[:-lag] * g[lag:]) / c0)
+
+
+def obs_sr2(gaps):
+    r = np.minimum(gaps[:-2], gaps[2:]) / np.maximum(gaps[:-2], gaps[2:])
+    r = r[np.isfinite(r)]
+    return float(np.mean(r)) if len(r) else 0.0
+
+
+def obs_triple_var(gaps):
+    triples = gaps[:-2] + gaps[1:-1] + gaps[2:]
+    v = np.var(gaps)
+    if v == 0:
+        return 0.0
+    return float(np.var(triples) / v)
+
+
+OBSERVABLES = {
+    "SR": obs_spacing_ratio,
+    "L1": lambda gaps: obs_lag_acf(gaps, 1),
+    "L2": lambda gaps: obs_lag_acf(gaps, 2),
+    "SR2": obs_sr2,
+    "triple_var": obs_triple_var,
+}
+
+
+def measure(gaps):
+    return {name: fn(gaps) for name, fn in OBSERVABLES.items()}
+
+
+def full_shuffle_baseline(gaps, n_trials, rng):
+    vals = {name: [] for name in OBSERVABLES}
+    for _ in range(n_trials):
+        s = rng.permutation(gaps)
+        row = measure(s)
+        for name, value in row.items():
+            vals[name].append(value)
+    return {
+        name: {
+            "mean": float(np.mean(x)),
+            "std": float(np.std(x, ddof=1)) if len(x) > 1 else 0.0,
+        }
+        for name, x in vals.items()
+    }
+
+
+def retention_curves(gaps, alphas, n_trials, originals, baseline, rng):
+    rows = []
+    for alpha in alphas:
+        vals = {name: [] for name in OBSERVABLES}
+        for _ in range(n_trials):
+            s = partial_shuffle(gaps, float(alpha), rng)
+            row = measure(s)
+            for name, value in row.items():
+                vals[name].append(value)
+
+        out = {"alpha": float(alpha)}
+        for name in OBSERVABLES:
+            mean = float(np.mean(vals[name]))
+            denom = originals[name] - baseline[name]["mean"]
+            retention = (mean - baseline[name]["mean"]) / denom if abs(denom) > 1e-12 else 0.0
+            out[name] = {
+                "mean": mean,
+                "std": float(np.std(vals[name], ddof=1)) if len(vals[name]) > 1 else 0.0,
+                "retention": float(retention),
+            }
+        rows.append(out)
+    return rows
+
+
+def pca_summary(rows):
+    names = list(OBSERVABLES)
+    matrix = np.array([[row[name]["retention"] for name in names] for row in rows], dtype=float)
+    matrix = matrix - np.mean(matrix, axis=0, keepdims=True)
+
+    _, singular, vt = np.linalg.svd(matrix, full_matrices=False)
+    energy = singular * singular
+    if np.sum(energy) <= 1e-15:
+        explained = np.zeros_like(energy)
+        effective_rank = 0.0
+    else:
+        explained = energy / np.sum(energy)
+        positive = explained[explained > 1e-15]
+        effective_rank = float(np.exp(-np.sum(positive * np.log(positive))))
+
+    corr = np.corrcoef(matrix, rowvar=False)
+    abs_corr = np.abs(corr)
+    upper = abs_corr[np.triu_indices_from(abs_corr, k=1)]
+
+    return {
+        "observables": names,
+        "singular_values": [float(x) for x in singular],
+        "explained_variance": [float(x) for x in explained],
+        "effective_rank": effective_rank,
+        "pc1_loadings": {name: float(vt[0, i]) for i, name in enumerate(names)} if len(vt) else {},
+        "mean_abs_pairwise_corr": float(np.mean(upper)) if len(upper) else 0.0,
+        "min_abs_pairwise_corr": float(np.min(upper)) if len(upper) else 0.0,
+        "max_abs_pairwise_corr": float(np.max(upper)) if len(upper) else 0.0,
+    }
+
+
+def analyze_sequence(name, gaps, alphas, n_trials, n_baseline, rng):
+    originals = measure(gaps)
+    baseline = full_shuffle_baseline(gaps, n_baseline, rng)
+    rows = retention_curves(gaps, alphas, n_trials, originals, baseline, rng)
+
+    z = {}
+    for obs_name in OBSERVABLES:
+        std = baseline[obs_name]["std"]
+        z[obs_name] = float((originals[obs_name] - baseline[obs_name]["mean"]) / std) if std > 1e-12 else 0.0
+
+    return {
+        "n_gaps": int(len(gaps)),
+        "originals": originals,
+        "full_shuffle_baseline": baseline,
+        "original_vs_shuffle_z": z,
+        "retention_curves": rows,
+        "pca": pca_summary(rows),
+    }
+
+
+def build_sequences(n_gaps, rng):
+    primes = get_primes(n_gaps * 24)[: n_gaps + 1]
+    prime_gaps = np.diff(primes).astype(float)
+
+    gue = gue_gaps(160, max(8, n_gaps // 160 + 1), rng).astype(float)
+    gue = gue[:n_gaps]
+
+    poisson = rng.exponential(1.0, size=n_gaps).astype(float)
+    return {
+        "primes": prime_gaps,
+        "gue": gue,
+        "poisson": poisson,
+    }
+
+
+def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505):
+    rng = np.random.default_rng(seed)
+    alphas = np.linspace(0.05, 0.95, n_alpha)
+    sequences = build_sequences(n_gaps, rng)
+
+    output = {
+        "experiment": "observable_rank_audit",
+        "question": "Do crossover observables contain independent directions or one latent boundary coordinate?",
+        "params": {
+            "n_gaps": int(n_gaps),
+            "n_alpha": int(n_alpha),
+            "n_trials": int(n_trials),
+            "n_baseline": int(n_baseline),
+            "seed": int(seed),
+        },
+        "sequences": {},
+    }
+
+    print(f"n_gaps={n_gaps}, n_alpha={n_alpha}, n_trials={n_trials}, n_baseline={n_baseline}, seed={seed}")
+    print(f"{'sequence':<10} {'pc1':>8} {'eff_rank':>9} {'mean|corr|':>11}  z(SR,L1,L2,SR2,triple)")
+    print("-" * 92)
+    for seq_name, gaps in sequences.items():
+        result = analyze_sequence(seq_name, gaps, alphas, n_trials, n_baseline, rng)
+        output["sequences"][seq_name] = result
+        pca = result["pca"]
+        z = result["original_vs_shuffle_z"]
+        z_text = ", ".join(f"{obs}={z[obs]:+.1f}" for obs in OBSERVABLES)
+        pc1 = pca["explained_variance"][0] if pca["explained_variance"] else 0.0
+        print(f"{seq_name:<10} {pc1:>8.3f} {pca['effective_rank']:>9.3f} {pca['mean_abs_pairwise_corr']:>11.3f}  {z_text}")
+
+    out_path = Path("tools/data/observable_rank_audit.json")
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+    print(f"\nSaved to {out_path}")
+    return output
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n_gaps", type=int, default=30000)
+    parser.add_argument("--n_alpha", type=int, default=19)
+    parser.add_argument("--n_trials", type=int, default=24)
+    parser.add_argument("--n_baseline", type=int, default=72)
+    parser.add_argument("--seed", type=int, default=20260505)
+    args = parser.parse_args()
+    run(
+        n_gaps=args.n_gaps,
+        n_alpha=args.n_alpha,
+        n_trials=args.n_trials,
+        n_baseline=args.n_baseline,
+        seed=args.seed,
+    )
+
+
+if __name__ == "__main__":
+    main()

tokens used
84,309
Esperimento completato e report scritto in [agent_20260505_0330.md](/opt/MM_D-ND/tools/data/reports/agent_20260505_0330.md).

Ho creato lo strumento riusabile [exp_observable_rank_audit.py](/opt/MM_D-ND/tools/exp_observable_rank_audit.py), generato i dati in `tools/data/observable_rank_audit.json` e una replica seed in `tools/data/observable_rank_audit_seed20260506.json`, poi aggiornato `tools/data/seme.json`.

Risultato principale: nel perimetro partial-shuffle, primes e GUE hanno segnale forte ma quasi monodimensionale. Quindi SR/L1/L2/SR2/triple_var restano probe validi, ma non vanno contati come evidenze indipendenti della stessa transizione.

Verifiche eseguite:
- `python -m py_compile tools/exp_observable_rank_audit.py`
- validazione JSON su `seme.json` e output dati
- `python tools/structural_check.py ...` senza anti-pattern trovati.
