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: 019dfedc-7d2d-7d13-8f48-c14119f3659c
--------
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_20260506_1955.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 piccolo ma verificabile 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.

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

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

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

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

Nel report, dichiara nel header:

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

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

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

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

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

## Skill attive — modus del lab

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

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

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

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

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

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

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

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

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

## Errori già fatti — non ripeterli

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

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

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

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


exec
/bin/bash -lc "find tools -maxdepth 4 -type f | sort | sed -n '1,220p'" in /opt/MM_D-ND
 succeeded in 0ms:
tools/.venv/bin/Activate.ps1
tools/.venv/bin/activate
tools/.venv/bin/activate.csh
tools/.venv/bin/activate.fish
tools/.venv/bin/f2py
tools/.venv/bin/fonttools
tools/.venv/bin/isympy
tools/.venv/bin/numpy-config
tools/.venv/bin/pip
tools/.venv/bin/pip3
tools/.venv/bin/pip3.12
tools/.venv/bin/pyftmerge
tools/.venv/bin/pyftsubset
tools/.venv/bin/ttx
tools/.venv/pyvenv.cfg
tools/GUIDA_CEC.md
tools/GUIDA_DOMANDATORE.md
tools/GUIDA_EXPLORER_TM1.md
tools/GUIDA_GODEL.md
tools/GUIDA_GODEL_TM1.md
tools/LAB_AGENT_CONTEXT.md
tools/LAB_OPERATIVO.md
tools/MODUS_INDAGINE.md
tools/PROTOCOLLO_ZETA.md
tools/README.md
tools/STRUMENTI.json
tools/__pycache__/bicono_projection.cpython-312.pyc
tools/__pycache__/dipartimento.cpython-312.pyc
tools/__pycache__/dnd_M_operator.cpython-312.pyc
tools/__pycache__/dnd_arxiv.cpython-312.pyc
tools/__pycache__/dnd_autoricerca.cpython-312.pyc
tools/__pycache__/dnd_banchi.cpython-312.pyc
tools/__pycache__/dnd_banchi_tm1.cpython-312.pyc
tools/__pycache__/dnd_bloch_explorer.cpython-312.pyc
tools/__pycache__/dnd_compatibility.cpython-312.pyc
tools/__pycache__/dnd_condizioni.cpython-312.pyc
tools/__pycache__/dnd_controprove.cpython-312.pyc
tools/__pycache__/dnd_dipolo_lab.cpython-312.pyc
tools/__pycache__/dnd_domandatore.cpython-312.pyc
tools/__pycache__/dnd_engine.cpython-312.pyc
tools/__pycache__/dnd_experiments.cpython-312.pyc
tools/__pycache__/dnd_explorer.cpython-312.pyc
tools/__pycache__/dnd_gue_test.cpython-312.pyc
tools/__pycache__/dnd_incrocio.cpython-312.pyc
tools/__pycache__/dnd_kernel.cpython-312.pyc
tools/__pycache__/dnd_lab.cpython-312.pyc
tools/__pycache__/dnd_lab_team.cpython-312.pyc
tools/__pycache__/dnd_lab_vivo.cpython-312.pyc
tools/__pycache__/dnd_loop.cpython-312.pyc
tools/__pycache__/dnd_md2latex.cpython-312.pyc
tools/__pycache__/dnd_md2web.cpython-312.pyc
tools/__pycache__/dnd_next.cpython-312.pyc
tools/__pycache__/dnd_normalizer.cpython-312.pyc
tools/__pycache__/dnd_paper_audit.cpython-312.pyc
tools/__pycache__/dnd_paper_graph.cpython-312.pyc
tools/__pycache__/dnd_retriever.cpython-312.pyc
tools/__pycache__/dnd_riflesso.cpython-312.pyc
tools/__pycache__/dnd_riformulazioni.cpython-312.pyc
tools/__pycache__/dnd_risultante.cpython-312.pyc
tools/__pycache__/dnd_scenario.cpython-312.pyc
tools/__pycache__/dnd_spectral_probe.cpython-312.pyc
tools/__pycache__/dnd_stats.cpython-312.pyc
tools/__pycache__/dnd_teoria.cpython-312.pyc
tools/__pycache__/dnd_zero_controllo2.cpython-312.pyc
tools/__pycache__/dnd_zero_ising.cpython-312.pyc
tools/__pycache__/dnd_zero_notturno.cpython-312.pyc
tools/__pycache__/dnd_zero_operator.cpython-312.pyc
tools/__pycache__/dnd_zero_traiettoria.cpython-312.pyc
tools/__pycache__/dnd_zero_varieta.cpython-312.pyc
tools/__pycache__/dnd_zero_varieta_primi.cpython-312.pyc
tools/__pycache__/exp_3d_boundary_layers.cpython-312.pyc
tools/__pycache__/exp_boundary_shuffle_audit.cpython-312.pyc
tools/__pycache__/exp_markov_layer_recovery_audit.cpython-312.pyc
tools/__pycache__/exp_markov_memory_by_gue_type.cpython-312.pyc
tools/__pycache__/exp_observable_rank_audit.cpython-312.pyc
tools/__pycache__/exp_perturbation_dimensionality_audit.cpython-312.pyc
tools/__pycache__/exp_perturbation_rank_size_curve.cpython-312.pyc
tools/__pycache__/exp_two_channel_shuffle_audit.cpython-312.pyc
tools/__pycache__/exp_two_layer_universality.cpython-312.pyc
tools/__pycache__/lab_autopsy.cpython-312.pyc
tools/__pycache__/lab_falsifier.cpython-312.pyc
tools/__pycache__/lab_trajectory_apply.cpython-312.pyc
tools/__pycache__/m_spectro.cpython-312.pyc
tools/__pycache__/m_spectro_calibra.cpython-312.pyc
tools/__pycache__/observables_registry.cpython-312.pyc
tools/__pycache__/semantic_bridge.cpython-312.pyc
tools/__pycache__/topological_charge.cpython-312.pyc
tools/__pycache__/translate_tensions.cpython-312.pyc
tools/__pycache__/validate_tension_mapping.cpython-312.pyc
tools/__pycache__/zeta_validation.cpython-312.pyc
tools/add_video_to_feed.py
tools/alignment_marker.py
tools/awareness.json
tools/bicono_projection.py
tools/build_agent_field.py
tools/build_lab_graph.py
tools/cascade_trigger_hook.sh
tools/confine_spessore.py
tools/costo_materializzazione.py
tools/cron_ciclo_continuo.sh
tools/cron_dipartimento.sh
tools/cycle_watchdog.sh
tools/d_nd_book_updater.py
tools/data/.last_telegram_msg
tools/data/3d_boundary_layers.json
tools/data/STUDIO_SIMBOLISMO_DND.md
tools/data/aeternitas/aeternitas_20260505_110204.json
tools/data/aeternitas/aeternitas_20260505_111548.json
tools/data/aeternitas/aeternitas_20260505_111739.json
tools/data/aeternitas/aeternitas_20260505_111832.json
tools/data/aeternitas/aeternitas_20260506_033803.json
tools/data/aeternitas/aeternitas_20260506_063302.json
tools/data/aeternitas/aeternitas_20260506_194644.json
tools/data/agent_field_live.md
tools/data/alignment_active.json
tools/data/alignment_markers.jsonl
tools/data/arxiv_cache.json
tools/data/audit_paper_A_draft3.json
tools/data/audit_paper_B_draft3.json
tools/data/audit_paper_C_draft2.json
tools/data/audit_paper_D_draft2.json
tools/data/audit_paper_D_draft3.json
tools/data/audit_paper_E_draft3.json
tools/data/audit_paper_F_draft2.json
tools/data/audit_paper_F_draft3.json
tools/data/audit_paper_G_draft3.json
tools/data/autoricerca_journal.json
tools/data/autoricerca_state.json
tools/data/banchi_custom/banco_gen_gap_ratio_cons_gxe_qxg.json
tools/data/banchi_custom/banco_gen_gap_ratio_falsifica_f3.json
tools/data/banchi_custom/banco_gen_gap_ratio_falsifica_f6.json
tools/data/banchi_custom/banco_gen_gap_ratio_t2_normalizzatore_trascende.json
tools/data/banchi_custom/banco_gen_gap_ratio_t8_paper_a_esposto.json
tools/data/banchi_custom/banco_gen_gap_ratio_t9_linguaggio_metafisico.json
tools/data/banchi_custom/banco_gen_lyapunov_falsifica_c2.json
tools/data/bicono_projections.jsonl
tools/data/bloch_explorer_results.json
tools/data/bloch_search.log
tools/data/bloch_search_results.json
tools/data/boundary_coherence.json
tools/data/boundary_shuffle_audit.json
tools/data/brody_calibration_results.json
tools/data/brody_flow.json
tools/data/ciclo_memoria.json
tools/data/cognitive_fingerprint.json
tools/data/conoscenza_generata.json
tools/data/conoscenza_teorie.json
tools/data/conoscenza_teorie.json.bak.retraction_22_04
tools/data/consecutio.json
tools/data/consecutio_processata.json
tools/data/costante_dinamica.json
tools/data/cross_domain_dipolar_direction.json
tools/data/cross_observable_consistency.json
tools/data/crossover_phase_test.json
tools/data/curva_results.json
tools/data/curvature_distributions.png
tools/data/cycle/cycle_20260311_221300.json
tools/data/cycle/cycle_20260312_101354.json
tools/data/diagrams/ciclo_lab_completo.svg
tools/data/dinamiche.json
tools/data/dipartimento_journal.jsonl
tools/data/dipolar_crossover.json
tools/data/dipolar_vector_scaling.json
tools/data/dipolo_lab/dipolo_20260330_2115.json
tools/data/dipolo_lab/dipolo_20260330_2116.json
tools/data/dipolo_lab/dipolo_20260330_2121.json
tools/data/dipolo_lab/dipolo_20260331_0345.json
tools/data/dipolo_lab/dipolo_20260331_1506.json
tools/data/dipolo_lab/dipolo_20260331_1510.json
tools/data/dipolo_lab/dipolo_20260331_1809.json
tools/data/dipolo_lab/dipolo_20260401_0346.json
tools/data/domandatore/domandatore_20260305_1942.json
tools/data/domandatore/domandatore_20260305_1953.json
tools/data/domandatore/domandatore_20260305_1955.json
tools/data/domandatore/domandatore_20260305_1956.json
tools/data/domandatore/domandatore_20260305_2005.json
tools/data/domandatore/domandatore_20260305_2016.json
tools/data/domandatore/domandatore_20260305_2023.json
tools/data/domandatore/domandatore_20260305_2025.json
tools/data/domandatore/domandatore_20260305_2028.json
tools/data/domandatore/domandatore_20260306_0341.json
tools/data/domandatore/domandatore_20260306_1032.json
tools/data/domandatore/domandatore_20260306_1327.json
tools/data/domandatore/domandatore_20260306_1329.json
tools/data/domandatore/domandatore_20260306_1330.json
tools/data/domandatore/domandatore_20260307_1317.json
tools/data/domandatore/domandatore_20260307_1947.json
tools/data/domandatore/domandatore_20260307_2034.json
tools/data/domandatore/domandatore_20260308_1442.json
tools/data/domandatore/domandatore_20260308_1448.json
tools/data/domandatore/domandatore_20260308_1449.json
tools/data/domandatore/domandatore_20260308_1504.json
tools/data/domandatore/domandatore_20260308_1838.json
tools/data/domandatore/domandatore_20260308_1840.json
tools/data/domandatore/domandatore_20260308_1841.json
tools/data/domandatore/domandatore_20260308_1931.json
tools/data/domandatore/domandatore_20260308_1934.json
tools/data/domandatore/domandatore_20260308_1935.json
tools/data/domandatore/domandatore_20260308_1936.json
tools/data/domandatore/domandatore_20260308_1937.json
tools/data/domandatore/domandatore_20260308_1938.json
tools/data/domandatore/domandatore_20260308_1946.json
tools/data/domandatore/domandatore_20260308_2000.json
tools/data/domandatore/domandatore_20260308_2007.json
tools/data/domandatore/domandatore_20260308_2031.json
tools/data/domandatore/domandatore_20260309_1409.json
tools/data/domandatore/domandatore_20260311_2212.json
tools/data/domandatore/domandatore_20260311_2213.json
tools/data/domandatore/domandatore_20260312_1014.json
tools/data/domandatore/domandatore_20260312_1015.json
tools/data/domandatore/domandatore_20260312_1238.json
tools/data/domandatore/domandatore_20260312_1547.json
tools/data/domandatore/domandatore_20260312_1703.json
tools/data/domandatore/domandatore_20260312_1732.json
tools/data/domandatore/domandatore_20260312_1802.json
tools/data/domandatore/domandatore_20260314_0342.json
tools/data/domandatore/domandatore_20260314_0610.json
tools/data/domandatore/domandatore_20260314_0751.json
tools/data/domandatore/domandatore_20260314_0848.json
tools/data/domandatore/domandatore_20260314_0927.json

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

## Piano 67 — Il vincolo PERTURBATION_DENOMINATOR_GATE è stabilito e replicato — cristallizzare come regola metodo

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

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

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

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

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

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

### Agent Report — Scale-Selective Perturbations Reveal a Second Axis in GUE, Not in Primes
Trovato: 1. **GUE has a second perturbation axis that primes do not.** Under scale-selective perturbations, GUE effective rank rises from ~1.02 (uniform-only, rank audit 05-05) to 1.889. The second principal component explains 25.2% of variance. For primes, the rank rises only from 1.07 to 1.26 — the second 
Verdetto: **CONSTRAINT on META + BOUNDARY**: the single latent coordinate found under uniform shuffle (rank audit 05-05) is a property of the perturbation type,

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

codex
Il campo punta ancora su META e sul vincolo appena emerso: non misurare “rank” senza il denominatore operativo. Ora controllo lo scenario aggiornato e gli strumenti esistenti per evitare di ripetere i tre run precedenti.
exec
/bin/bash -lc "sed -n '1,260p' tools/exp_perturbation_rank_size_curve.py" in /opt/MM_D-ND
 succeeded in 0ms:
#!/usr/bin/env python3
"""
exp_perturbation_rank_size_curve.py

Reusable META audit for perturbation dimensionality.

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

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

from __future__ import annotations

import argparse
import json
from pathlib import Path

import numpy as np

from observables_registry import (
    OBSERVABLES_CANONICAL,
    OBSERVABLES_REGISTRY_VERSION,
    compute_canonical,
)


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


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


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


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


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


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


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


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


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

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

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

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


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

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

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

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

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

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


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

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

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

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

## Il problema che ha creato il registry

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

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

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

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

## La soluzione (minimal, non invasiva)

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

## Come usarlo

```python
from observables_registry import OBSERVABLES_CANONICAL, OBSERVABLES_REGISTRY_VERSION

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

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

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

## Convention per i report

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

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

Cycle che mescola canonical + variant DEVE indicare entrambi:

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

Senza questo, i confronti cross-cycle sono inattendibili.

## Versioning

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

import numpy as np


OBSERVABLES_REGISTRY_VERSION = "1.0.0-2026-05-06"


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

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

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

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


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

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


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

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


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


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

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


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


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

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

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

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

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


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

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

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


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


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

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

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


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


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


if __name__ == "__main__":
    # Smoke test: canonical observables on a simple gap series
    rng = np.random.default_rng(42)
    gue_like = rng.gamma(shape=2.0, scale=0.5, size=200)

exec
/bin/bash -lc 'ls -1 tools/data/reports | tail -20' in /opt/MM_D-ND
 succeeded in 0ms:
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
trajectory_apply_20260506_1931.json
trajectory_apply_20260506_1941.json
trajectory_apply_20260506_1955.json
watchdog.log

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

codex
Lo strumento `exp_perturbation_rank_size_curve.py` già incorpora registry e gate. Per non replicare il risultato precedente, lo userò in modo diverso: cercare una soglia z del denominatore che mostri quando il rank collassa o sopravvive dopo screening.
exec
/bin/bash -lc "sed -n '260,620p' tools/exp_perturbation_rank_size_curve.py" in /opt/MM_D-ND
 succeeded in 0ms:
                x = x.get(key)
            if isinstance(x, (int, float)):
                vals.append(float(x))
        return np.array(vals, dtype=float)

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

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


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


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

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

    prime_windows = build_prime_windows(max_n, args.n_replicates)

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

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

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

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

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


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


if __name__ == "__main__":
    main()

exec
/bin/bash -lc "sed -n '1,220p' tools/data/reports/trajectory_apply_20260506_1955.json" in /opt/MM_D-ND
 succeeded in 0ms:
{
  "apply_ts": "20260506_195548",
  "mode": "apply",
  "log_path": "/opt/MM_D-ND/tools/data/valutatore_log.jsonl",
  "seed_path": "/opt/MM_D-ND/tools/data/seme.json",
  "decision": "APPLIED",
  "entry_ts": "2026-05-06T19:47:12.588345+00:00",
  "entry_cycle_ref": "20260506_1941",
  "entry_decision": "REDESIGN",
  "entry_confidence": "high",
  "entry_action_type": "modify_seme",
  "entry_executed": false,
  "new_direzione": "Il vincolo PERTURBATION_DENOMINATOR_GATE è stabilito e replicato — cristallizzare come regola metodologica e muovere verso tensione cross-dominio non ancora esplorata (candidati: dipolar crossover scaling, markov layer coupling verification, o observable collinearity breaking). La prossima corsa NON deve restare su perturbation rank.",
  "old_direzione": "Esplorare il confine: 8 domini GUE, 5 Poisson — il confine è il terzo incluso operativo",
  "log_entry_marked_executed": true,
  "seed_written": "/opt/MM_D-ND/tools/data/seme.json"
}

exec
/bin/bash -lc "sed -n '1,220p' tools/data/reports/trajectory_apply_20260506_1941.json" in /opt/MM_D-ND
 succeeded in 0ms:
{
  "apply_ts": "20260506_194105",
  "mode": "apply",
  "log_path": "/opt/MM_D-ND/tools/data/valutatore_log.jsonl",
  "seed_path": "/opt/MM_D-ND/tools/data/seme.json",
  "decision": "SKIP",
  "entry_ts": "2026-05-06T06:33:26.706335+00:00",
  "entry_cycle_ref": "20260506_0625",
  "entry_decision": "NEXT_CYCLE",
  "entry_confidence": "high",
  "entry_action_type": "none",
  "entry_executed": false,
  "reason": "action.type='none' not in {'modify_seme'}"
}

exec
/bin/bash -lc "find tools/data/reports -maxdepth 1 -type f -name '*20260506*' -o -name 'agent_20260506*' | sort" in /opt/MM_D-ND
 succeeded in 0ms:
tools/data/reports/agent_20260506_0330.md
tools/data/reports/agent_20260506_0330_claude_raw.log
tools/data/reports/agent_20260506_0330_codex_raw.log
tools/data/reports/agent_20260506_0625.md
tools/data/reports/agent_20260506_0625_codex_raw.log
tools/data/reports/agent_20260506_1941.md
tools/data/reports/agent_20260506_1941_codex_raw.log
tools/data/reports/agent_20260506_1955_codex_raw.log
tools/data/reports/ddf_20260506_0405.json
tools/data/reports/evolution_20260506_0330.md
tools/data/reports/evolution_20260506_1941.md
tools/data/reports/falsifier_20260506_0330.raw.txt
tools/data/reports/falsifier_20260506_0625.json
tools/data/reports/falsifier_20260506_1941.json
tools/data/reports/manual_run_20260506_194105.log
tools/data/reports/manual_run_20260506_195547.log
tools/data/reports/recovery_20260506_062520.log
tools/data/reports/trajectory_apply_20260506_1931.json
tools/data/reports/trajectory_apply_20260506_1941.json
tools/data/reports/trajectory_apply_20260506_1955.json

codex
Nota importante: `trajectory_apply_20260506_1955.json` dice esplicitamente che la prossima corsa non deve restare su perturbation rank. Taglio quindi su un esperimento cross-dominio: “observable collinearity breaking”, cioè quando i cinque osservabili canonici diventano un asse solo o più assi sotto domini diversi, senza usare perturbation-rank come claim.
exec
/bin/bash -lc "sed -n '1,260p' tools/data/seme.json" in /opt/MM_D-ND
 succeeded in 0ms:
{
  "timestamp": "2026-05-06T19:46:44.559059",
  "piano": 67,
  "tensioni": [
    {
      "id": "OBSERVABLE_REGISTRY",
      "tipo": "vincolo",
      "claim": "Ogni script che usa observables canonici (SR, SR2, L1, L2, triple_var) deve importare la definizione da tools/observables_registry.py. Varianti devono usare nomi distinti (SR_local_rigidity, triple_var_normalized) — niente shadowing del nome canonico. Ogni report deve dichiarare 'observables_registry: VERSION' nel header.",
      "intensita": 1.0,
      "porta": "infrastructure",
      "manuale": true,
      "condensato_ref": "A14,A8",
      "origine": "cristallizzato 06/05 dalla consecutio del cycle 20260506_0625 (autopoietico self-finding)",
      "added_at": "2026-05-06T07:03:58.213606+00:00"
    },
    {
      "id": "PERTURBATION_DENOMINATOR_GATE",
      "tipo": "vincolo",
      "claim": "La dimensionalita di perturbazione va riportata solo insieme a PC2, versione observables_registry e gate original-vs-shuffle per osservabile. Nel perimetro 20260506_1941, Poisson e shuffle-primi producono rank_all ~1.8-2.0 con denominatori deboli; dopo gate abs(z)>=2 il rank stabile torna vicino a 1. Rank PCA non gated non e evidenza strutturale.",
      "intensita": 0.95,
      "porta": "META_BOUNDARY",
      "manuale": true,
      "condensato_ref": "A4,A8,A14,C2",
      "origine": "cycle agent_20260506_1941: perturbation rank size curve canonical observables",
      "added_at": "2026-05-06T19:41:00+00:00"
    },
    {
      "tipo": "confine_inesplorato",
      "id": "TRASCENDENZA_LIMITE",
      "claim": "La trascendenza e il limite attuale del modello. I punti fissi relazionali (non solo phi ma la rete di punti fissi tra osservabili) possono rivelare il vero grafo della realta e pattern nelle matrici. Il confine non e nella matematica - e nel passaggio tra piani.",
      "intensita": 0.9,
      "nota": "Input operatore 2026-04-10. Tocca: confine del modello, struttura relazionale dei punti fissi. Consecutio: quali punti fissi relazionali emergono dalle 21 tensioni attuali? Il grafo e gia nei dati?",
      "manuale": true,
      "porta": "sessione_interattiva",
      "condensato_ref": "A3,A10",
      "condensato_motivo": "Estende A3 (punto fisso singolo) a rete relazionale. Tocca A10 (dipolo) come caso speciale."
    },
    {
      "tipo": "scoperta",
      "id": "DUALITA_DIPOLARE_VS_ILLUSORIA",
      "claim": "Due tipi di dualita: (1) dipolare - generativa, il modello (det=-1), (2) illusoria - dispersiva, entropia (det=+1). Le regole incoerenti producono la seconda. La dualita illusoria e entropia come dispersione, non come informazione.",
      "intensita": 0.9,
      "nota": "Input operatore 2026-04-10. Tocca: entropia come dispersione illusoria vs generazione dipolare. Consecutio: nel Lab i domini Poisson (entropia massima) mostrano dualita illusoria? I domini GUE (strutturati) mostrano dualita dipolare? Il drift verso Poisson (POISSON_CONVERGENCE) e perdita di dualita dipolare?",
      "manuale": true,
      "porta": "sessione_interattiva",
      "condensato_ref": "A2,A10,F5",
      "condensato_motivo": "Discrimina due forme di det. A2 (confine) e la soglia. A10 (dipolo) e il tipo 1. F5 (frame) misura la struttura D-ND che e tipo 1."
    },
    {
      "tipo": "scoperta_numerica",
      "id": "METRIC_TENSOR",
      "claim": "Il tensore metrico dei primi è g=(p/2)². Nel tempo ln(p), è de Sitter 1+1D. z=-8.8 curvatura vs z=+22.5 rapporti ΔΓ.",
      "intensità": 0.9,
      "nota": "Sessione interattiva 4 aprile. Verificato su 78K primi.",
      "manuale": true,
      "porta": "sessione_interattiva",
      "condensato_ref": null,
      "condensato_motivo": "Risultato numerico verificato, non-tautologico"
    },
    {
      "tipo": "scoperta",
      "id": "TENSIONE_ENTITA",
      "claim": "La tensione non e un problema pratico - e un Entita. La tensione superflua crea latenza (tempo). Senza tensione superflua tutto e regolato da assiomi. Implicazione: le tensioni nel seme sono entita, non problemi da risolvere. Quelle superflue (det=+1) producono tempo/latenza.",
      "intensita": 0.85,
      "nota": "Input operatore 2026-04-10. Tocca: rapporto tensione/assioma. Operativamente: discriminare tensioni-entita (generative) da tensioni-superflue (dispersive) nel seme. Le 21 tensioni attuali - quante sono entita e quante latenza?",
      "manuale": true,
      "porta": "sessione_interattiva",
      "condensato_ref": "A5,A6",
      "condensato_motivo": "Il ciclo (A5) lavora con tensioni - ma se la tensione e entita, il ciclo non le risolve, le osserva. Lo zero mobile (A6) e la tensione senza latenza."
    },
    {
      "tipo": "confine_inesplorato",
      "id": "G_POTENZIALE_NULLA",
      "claim": "G e il potenziale di tutto come nulla - permette il prima e il dopo. Ci muoviamo come trascendenza dimensionale gravitazionale. G nel tetraedro non e una teoria tra le altre - e il potenziale che le rende possibili.",
      "intensita": 0.85,
      "nota": "Input operatore 2026-04-10. Tocca: ruolo di G nel tetraedro (T,Q,G,E). La fonte video_lp0RgZ6kQF8 dice: tensore metrico dentro la forma simplettica. G non e accanto a T,Q,E - e sotto. Consecutio: nei dati Lab, i ponti TxG e ExG hanno struttura diversa dai ponti TxQ?",
      "manuale": true,
      "porta": "sessione_interattiva",
      "condensato_ref": "A7,A10",
      "condensato_motivo": "A7 (singolarita come operatore) e G come potenziale. A10 (dipolo) opera sul piano che G rende possibile."
    },
    {
      "tipo": "confine_inesplorato",
      "id": "BOUNDARY",
      "claim": "8 domini GUE, 5 Poisson — il confine è il terzo incluso operativo",
      "intensità": 0.8,
      "nota": "Il segnale non-triviale è DOVE la scissione cambia natura, non che converge a φ",
      "condensato_ref": "A9",
      "condensato_motivo": "Overlap termini con A9 (5 termini)",
      "porta": "condensato"
    },
    {
      "tipo": "confine_inesplorato",
      "id": "PIANO_PRIMARIO_DUE_ASSIOMI",
      "claim": "I piani importanti sono il primario e i due assiomi che lo determinano nelle zone osservate. Non tutti gli assiomi operano ovunque - in ogni zona osservata, due assiomi determinano il piano primario.",
      "intensita": 0.8,
      "nota": "Input operatore 2026-04-10. Tocca: struttura locale degli assiomi. Consecutio: per ogni dominio Lab (primi, logistica, percolazione...) quali 2 assiomi del condensato sono operativi? Mappa assiomi x domini = grafo della realta locale.",
      "manuale": true,
      "porta": "sessione_interattiva",
      "condensato_ref": "A9,A14",
      "condensato_motivo": "A9 (terzo incluso) opera CON il piano. A14 (cascata) propaga - ma propaga cosa, se solo 2 assiomi sono attivi per zona?"
    },
    {
      "tipo": "simmetria_sospetta",
      "id": "META",
      "claim": "Tutti i 11 test passano — verifica che non stiamo testando solo tautologie",
      "intensità": 0.5,
      "nota": "La convergenza a φ è triviale (controprove). I test stanno verificando contenuto o struttura?",
      "condensato_ref": "A4,A12,C2",
      "porta": "verify_assertions_META_ALL_PASS",
      "condensato_motivo": "Ricorrente (3x in 2 giorni) e fuori dalla mappa"
    },
    {
      "tipo": "task",
      "id": "TRAJECTORY_APPLY_20260506_1941",
      "claim": "Applied valutatore REDESIGN from 20260506_1941: Il vincolo PERTURBATION_DENOMINATOR_GATE è stabilito e replicato — cristallizzare come regola metodologica e muovere verso tensione cross-dominio non ancora esplorata (candidati: dipolar crossover sca",
      "intensità": 0.7,
      "porta": "trajectory_apply",
      "condensato_ref": "A8,A14,A15",
      "manuale": false,
      "_source_log": "2026-05-06T19:47:12.588345+00:00",
      "_source_decision": "REDESIGN",
      "_source_reasoning": "Three consecutive cycles on perturbation dimensionality (pianos 64-66) have converged to a single clear constraint: rank needs denominator gating, and after gating both GUE and primes show ~1 perturbation coordinate. The thread has produced genuine falsification (category a) and a solid methodologic"
    }
  ],
  "potenziale_bloccato": [],
  "varianza": [
    "Tensioni risolte: {'DUALITA_DIPOLARE_VS_ILLUSORIA', 'PIANO_PRIMARIO_DUE_ASSIOMI', 'G_POTENZIALE_NULLA', 'TRASCENDENZA_LIMITE', 'TENSIONE_ENTITA', 'OBSERVABLE_REGISTRY', 'METRIC_TENSOR', 'PERTURBATION_DENOMINATOR_GATE'}"
  ],
  "filtro": {
    "promosse": 10,
    "filtrate": 0,
    "condensato_claims": 30
  },
  "direzione": "Il vincolo PERTURBATION_DENOMINATOR_GATE è stabilito e replicato — cristallizzare come regola metodologica e muovere verso tensione cross-dominio non ancora esplorata (candidati: dipolar crossover scaling, markov layer coupling verification, o observable collinearity breaking). La prossima corsa NON deve restare su perturbation rank.",
  "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.",
      "teorie": [
        "T",
        "Q",
        "R"
      ],
      "ponti_potenziali": [],

exec
/bin/bash -lc "find tools -maxdepth 1 -type f -name 'exp_*markov*' -o -name 'exp_*dipolar*' -o -name 'exp_*observable*' | sort" in /opt/MM_D-ND
 succeeded in 0ms:
tools/exp_cross_domain_dipolar_direction.py
tools/exp_cross_observable_consistency.py
tools/exp_dipolar_angle_reference.py
tools/exp_dipolar_crossover.py
tools/exp_dipolar_vector_scaling.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_observable_rank_audit.py

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

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

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

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

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

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

## Results

### Size Curve Summary

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

### Observable Stability

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

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

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

## Findings

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

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

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

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

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

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

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

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

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

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

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

codex
Lo script esistente per questo esperimento ha un problema metodologico coerente con il vincolo nuovo: ridefinisce gli osservabili localmente e usa `triple_var` normalizzato. Lo correggo prima di eseguire, così il report non eredita la collisione che il registry ha appena risolto.
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
index f2acafc5960c8a35964e0ab521cf70569eefe27f..51e6ec52edfffda55d1d22862a102f896a08fdd9
--- a/tools/exp_observable_rank_audit.py
+++ b/tools/exp_observable_rank_audit.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 """
-exp_observable_rank_audit.py - META audit for observable redundancy.
+exp_observable_rank_audit.py - META audit for observable collinearity.
 
 Recent runs found many Markov/crossover observables that react coherently under
 partial shuffle. This script asks whether those observables carry independent
@@ -22,47 +22,19 @@
 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)
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
 
 
-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,
-}
+OBSERVABLES = OBSERVABLES_CANONICAL
+OBS_NAMES = list(OBSERVABLES)
 
 
 def measure(gaps):
-    return {name: fn(gaps) for name, fn in OBSERVABLES.items()}
+    return compute_canonical(gaps)
 
 
 def full_shuffle_baseline(gaps, n_trials, rng):
@@ -106,7 +78,7 @@
 
 
 def pca_summary(rows):
-    names = list(OBSERVABLES)
+    names = OBS_NAMES
     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)
 
@@ -142,15 +114,20 @@
     rows = retention_curves(gaps, alphas, n_trials, originals, baseline, rng)
 
     z = {}
-    for obs_name in OBSERVABLES:
+    stable_observables = []
+    for obs_name in OBS_NAMES:
         std = baseline[obs_name]["std"]
         z[obs_name] = float((originals[obs_name] - baseline[obs_name]["mean"]) / std) if std > 1e-12 else 0.0
+        if abs(z[obs_name]) >= 2.0:
+            stable_observables.append(obs_name)
 
     return {
         "n_gaps": int(len(gaps)),
         "originals": originals,
         "full_shuffle_baseline": baseline,
         "original_vs_shuffle_z": z,
+        "stable_observables_abs_z_ge_2": stable_observables,
+        "weak_observable_count": int(len(OBS_NAMES) - len(stable_observables)),
         "retention_curves": rows,
         "pca": pca_summary(rows),
     }
@@ -164,21 +141,25 @@
     gue = gue[:n_gaps]
 
     poisson = rng.exponential(1.0, size=n_gaps).astype(float)
+    prime_shuffle = rng.permutation(prime_gaps).astype(float)
     return {
         "primes": prime_gaps,
+        "prime_shuffle": prime_shuffle,
         "gue": gue,
         "poisson": poisson,
     }
 
 
-def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505):
+def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505, out="tools/data/observable_rank_audit.json"):
     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?",
+        "question": "When do canonical observable retention curves break collinearity across domains?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
         "params": {
             "n_gaps": int(n_gaps),
             "n_alpha": int(n_alpha),
@@ -189,19 +170,23 @@
         "sequences": {},
     }
 
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
     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)
+    print(f"{'sequence':<14} {'pc1':>8} {'eff_rank':>9} {'mean|corr|':>11} {'weak':>5}  z(SR,SR2,L1,L2,triple_var)")
+    print("-" * 108)
     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)
+        z_text = ", ".join(f"{obs}={z[obs]:+.1f}" for obs in OBS_NAMES)
         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}")
+        print(
+            f"{seq_name:<14} {pc1:>8.3f} {pca['effective_rank']:>9.3f} "
+            f"{pca['mean_abs_pairwise_corr']:>11.3f} {result['weak_observable_count']:>5}  {z_text}"
+        )
 
-    out_path = Path("tools/data/observable_rank_audit.json")
+    out_path = Path(out)
     with out_path.open("w") as f:
         json.dump(output, f, indent=2)
     print(f"\nSaved to {out_path}")
@@ -215,6 +200,7 @@
     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)
+    parser.add_argument("--out", default="tools/data/observable_rank_audit.json")
     args = parser.parse_args()
     run(
         n_gaps=args.n_gaps,
@@ -222,6 +208,7 @@
         n_trials=args.n_trials,
         n_baseline=args.n_baseline,
         seed=args.seed,
+        out=args.out,
     )
 
 

diff --git a/tools/exp_observable_rank_audit.py b/tools/exp_observable_rank_audit.py
index f2acafc5960c8a35964e0ab521cf70569eefe27f..51e6ec52edfffda55d1d22862a102f896a08fdd9
--- a/tools/exp_observable_rank_audit.py
+++ b/tools/exp_observable_rank_audit.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 """
-exp_observable_rank_audit.py - META audit for observable redundancy.
+exp_observable_rank_audit.py - META audit for observable collinearity.
 
 Recent runs found many Markov/crossover observables that react coherently under
 partial shuffle. This script asks whether those observables carry independent
@@ -22,47 +22,19 @@
 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)
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
 
 
-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,
-}
+OBSERVABLES = OBSERVABLES_CANONICAL
+OBS_NAMES = list(OBSERVABLES)
 
 
 def measure(gaps):
-    return {name: fn(gaps) for name, fn in OBSERVABLES.items()}
+    return compute_canonical(gaps)
 
 
 def full_shuffle_baseline(gaps, n_trials, rng):
@@ -106,7 +78,7 @@
 
 
 def pca_summary(rows):
-    names = list(OBSERVABLES)
+    names = OBS_NAMES
     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)
 
@@ -142,15 +114,20 @@
     rows = retention_curves(gaps, alphas, n_trials, originals, baseline, rng)
 
     z = {}
-    for obs_name in OBSERVABLES:
+    stable_observables = []
+    for obs_name in OBS_NAMES:
         std = baseline[obs_name]["std"]
         z[obs_name] = float((originals[obs_name] - baseline[obs_name]["mean"]) / std) if std > 1e-12 else 0.0
+        if abs(z[obs_name]) >= 2.0:
+            stable_observables.append(obs_name)
 
     return {
         "n_gaps": int(len(gaps)),
         "originals": originals,
         "full_shuffle_baseline": baseline,
         "original_vs_shuffle_z": z,
+        "stable_observables_abs_z_ge_2": stable_observables,
+        "weak_observable_count": int(len(OBS_NAMES) - len(stable_observables)),
         "retention_curves": rows,
         "pca": pca_summary(rows),
     }
@@ -164,21 +141,25 @@
     gue = gue[:n_gaps]
 
     poisson = rng.exponential(1.0, size=n_gaps).astype(float)
+    prime_shuffle = rng.permutation(prime_gaps).astype(float)
     return {
         "primes": prime_gaps,
+        "prime_shuffle": prime_shuffle,
         "gue": gue,
         "poisson": poisson,
     }
 
 
-def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505):
+def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505, out="tools/data/observable_rank_audit.json"):
     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?",
+        "question": "When do canonical observable retention curves break collinearity across domains?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
         "params": {
             "n_gaps": int(n_gaps),
             "n_alpha": int(n_alpha),
@@ -189,19 +170,23 @@
         "sequences": {},
     }
 
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
     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)
+    print(f"{'sequence':<14} {'pc1':>8} {'eff_rank':>9} {'mean|corr|':>11} {'weak':>5}  z(SR,SR2,L1,L2,triple_var)")
+    print("-" * 108)
     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)
+        z_text = ", ".join(f"{obs}={z[obs]:+.1f}" for obs in OBS_NAMES)
         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}")
+        print(
+            f"{seq_name:<14} {pc1:>8.3f} {pca['effective_rank']:>9.3f} "
+            f"{pca['mean_abs_pairwise_corr']:>11.3f} {result['weak_observable_count']:>5}  {z_text}"
+        )
 
-    out_path = Path("tools/data/observable_rank_audit.json")
+    out_path = Path(out)
     with out_path.open("w") as f:
         json.dump(output, f, indent=2)
     print(f"\nSaved to {out_path}")
@@ -215,6 +200,7 @@
     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)
+    parser.add_argument("--out", default="tools/data/observable_rank_audit.json")
     args = parser.parse_args()
     run(
         n_gaps=args.n_gaps,
@@ -222,6 +208,7 @@
         n_trials=args.n_trials,
         n_baseline=args.n_baseline,
         seed=args.seed,
+        out=args.out,
     )
 
 

codex
Ho aggiornato lo strumento: ora importa `OBSERVABLES_CANONICAL`, dichiara versione registry, aggiunge `prime_shuffle` come controllo e stampa il conteggio di osservabili deboli. Eseguo una griglia moderata ma non minuscola, così i numeri sono abbastanza stabili senza sprecare il ciclo.
exec
/bin/bash -lc 'python tools/exp_observable_rank_audit.py --n_gaps 12000 --n_alpha 19 --n_trials 18 --n_baseline 48 --seed 202605061955 --out tools/data/observable_collinearity_breaking_20260506_1955.json' in /opt/MM_D-ND
diff --git a/tools/exp_observable_rank_audit.py b/tools/exp_observable_rank_audit.py
index f2acafc5960c8a35964e0ab521cf70569eefe27f..51e6ec52edfffda55d1d22862a102f896a08fdd9
--- a/tools/exp_observable_rank_audit.py
+++ b/tools/exp_observable_rank_audit.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 """
-exp_observable_rank_audit.py - META audit for observable redundancy.
+exp_observable_rank_audit.py - META audit for observable collinearity.
 
 Recent runs found many Markov/crossover observables that react coherently under
 partial shuffle. This script asks whether those observables carry independent
@@ -22,47 +22,19 @@
 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)
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
 
 
-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,
-}
+OBSERVABLES = OBSERVABLES_CANONICAL
+OBS_NAMES = list(OBSERVABLES)
 
 
 def measure(gaps):
-    return {name: fn(gaps) for name, fn in OBSERVABLES.items()}
+    return compute_canonical(gaps)
 
 
 def full_shuffle_baseline(gaps, n_trials, rng):
@@ -106,7 +78,7 @@
 
 
 def pca_summary(rows):
-    names = list(OBSERVABLES)
+    names = OBS_NAMES
     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)
 
@@ -142,15 +114,20 @@
     rows = retention_curves(gaps, alphas, n_trials, originals, baseline, rng)
 
     z = {}
-    for obs_name in OBSERVABLES:
+    stable_observables = []
+    for obs_name in OBS_NAMES:
         std = baseline[obs_name]["std"]
         z[obs_name] = float((originals[obs_name] - baseline[obs_name]["mean"]) / std) if std > 1e-12 else 0.0
+        if abs(z[obs_name]) >= 2.0:
+            stable_observables.append(obs_name)
 
     return {
         "n_gaps": int(len(gaps)),
         "originals": originals,
         "full_shuffle_baseline": baseline,
         "original_vs_shuffle_z": z,
+        "stable_observables_abs_z_ge_2": stable_observables,
+        "weak_observable_count": int(len(OBS_NAMES) - len(stable_observables)),
         "retention_curves": rows,
         "pca": pca_summary(rows),
     }
@@ -164,21 +141,25 @@
     gue = gue[:n_gaps]
 
     poisson = rng.exponential(1.0, size=n_gaps).astype(float)
+    prime_shuffle = rng.permutation(prime_gaps).astype(float)
     return {
         "primes": prime_gaps,
+        "prime_shuffle": prime_shuffle,
         "gue": gue,
         "poisson": poisson,
     }
 
 
-def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505):
+def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505, out="tools/data/observable_rank_audit.json"):
     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?",
+        "question": "When do canonical observable retention curves break collinearity across domains?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
         "params": {
             "n_gaps": int(n_gaps),
             "n_alpha": int(n_alpha),
@@ -189,19 +170,23 @@
         "sequences": {},
     }
 
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
     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)
+    print(f"{'sequence':<14} {'pc1':>8} {'eff_rank':>9} {'mean|corr|':>11} {'weak':>5}  z(SR,SR2,L1,L2,triple_var)")
+    print("-" * 108)
     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)
+        z_text = ", ".join(f"{obs}={z[obs]:+.1f}" for obs in OBS_NAMES)
         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}")
+        print(
+            f"{seq_name:<14} {pc1:>8.3f} {pca['effective_rank']:>9.3f} "
+            f"{pca['mean_abs_pairwise_corr']:>11.3f} {result['weak_observable_count']:>5}  {z_text}"
+        )
 
-    out_path = Path("tools/data/observable_rank_audit.json")
+    out_path = Path(out)
     with out_path.open("w") as f:
         json.dump(output, f, indent=2)
     print(f"\nSaved to {out_path}")
@@ -215,6 +200,7 @@
     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)
+    parser.add_argument("--out", default="tools/data/observable_rank_audit.json")
     args = parser.parse_args()
     run(
         n_gaps=args.n_gaps,
@@ -222,6 +208,7 @@
         n_trials=args.n_trials,
         n_baseline=args.n_baseline,
         seed=args.seed,
+        out=args.out,
     )
 
 

 succeeded in 1538ms:
observables_registry=1.0.0-2026-05-06
n_gaps=12000, n_alpha=19, n_trials=18, n_baseline=48, seed=202605061955
sequence            pc1  eff_rank  mean|corr|  weak  z(SR,SR2,L1,L2,triple_var)
------------------------------------------------------------------------------------------------------------
primes            0.978     1.128       0.975     1  SR=-12.1, SR2=-2.5, L1=-8.9, L2=-1.9, triple_var=-8.7
prime_shuffle     0.593     2.475       0.606     5  SR=+0.4, SR2=-0.2, L1=+1.1, L2=-1.1, triple_var=+0.6
gue               0.990     1.060       0.989     0  SR=-2.9, SR2=+14.5, L1=+13.2, L2=+31.7, triple_var=+23.8
poisson           0.625     2.368       0.609     5  SR=+1.9, SR2=-0.6, L1=+1.0, L2=-1.5, triple_var=+0.3

Saved to tools/data/observable_collinearity_breaking_20260506_1955.json

diff --git a/tools/exp_observable_rank_audit.py b/tools/exp_observable_rank_audit.py
index f2acafc5960c8a35964e0ab521cf70569eefe27f..51e6ec52edfffda55d1d22862a102f896a08fdd9
--- a/tools/exp_observable_rank_audit.py
+++ b/tools/exp_observable_rank_audit.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 """
-exp_observable_rank_audit.py - META audit for observable redundancy.
+exp_observable_rank_audit.py - META audit for observable collinearity.
 
 Recent runs found many Markov/crossover observables that react coherently under
 partial shuffle. This script asks whether those observables carry independent
@@ -22,47 +22,19 @@
 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)
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
 
 
-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,
-}
+OBSERVABLES = OBSERVABLES_CANONICAL
+OBS_NAMES = list(OBSERVABLES)
 
 
 def measure(gaps):
-    return {name: fn(gaps) for name, fn in OBSERVABLES.items()}
+    return compute_canonical(gaps)
 
 
 def full_shuffle_baseline(gaps, n_trials, rng):
@@ -106,7 +78,7 @@
 
 
 def pca_summary(rows):
-    names = list(OBSERVABLES)
+    names = OBS_NAMES
     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)
 
@@ -142,15 +114,20 @@
     rows = retention_curves(gaps, alphas, n_trials, originals, baseline, rng)
 
     z = {}
-    for obs_name in OBSERVABLES:
+    stable_observables = []
+    for obs_name in OBS_NAMES:
         std = baseline[obs_name]["std"]
         z[obs_name] = float((originals[obs_name] - baseline[obs_name]["mean"]) / std) if std > 1e-12 else 0.0
+        if abs(z[obs_name]) >= 2.0:
+            stable_observables.append(obs_name)
 
     return {
         "n_gaps": int(len(gaps)),
         "originals": originals,
         "full_shuffle_baseline": baseline,
         "original_vs_shuffle_z": z,
+        "stable_observables_abs_z_ge_2": stable_observables,
+        "weak_observable_count": int(len(OBS_NAMES) - len(stable_observables)),
         "retention_curves": rows,
         "pca": pca_summary(rows),
     }
@@ -164,21 +141,25 @@
     gue = gue[:n_gaps]
 
     poisson = rng.exponential(1.0, size=n_gaps).astype(float)
+    prime_shuffle = rng.permutation(prime_gaps).astype(float)
     return {
         "primes": prime_gaps,
+        "prime_shuffle": prime_shuffle,
         "gue": gue,
         "poisson": poisson,
     }
 
 
-def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505):
+def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505, out="tools/data/observable_rank_audit.json"):
     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?",
+        "question": "When do canonical observable retention curves break collinearity across domains?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
         "params": {
             "n_gaps": int(n_gaps),
             "n_alpha": int(n_alpha),
@@ -189,19 +170,23 @@
         "sequences": {},
     }
 
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
     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)
+    print(f"{'sequence':<14} {'pc1':>8} {'eff_rank':>9} {'mean|corr|':>11} {'weak':>5}  z(SR,SR2,L1,L2,triple_var)")
+    print("-" * 108)
     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)
+        z_text = ", ".join(f"{obs}={z[obs]:+.1f}" for obs in OBS_NAMES)
         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}")
+        print(
+            f"{seq_name:<14} {pc1:>8.3f} {pca['effective_rank']:>9.3f} "
+            f"{pca['mean_abs_pairwise_corr']:>11.3f} {result['weak_observable_count']:>5}  {z_text}"
+        )
 
-    out_path = Path("tools/data/observable_rank_audit.json")
+    out_path = Path(out)
     with out_path.open("w") as f:
         json.dump(output, f, indent=2)
     print(f"\nSaved to {out_path}")
@@ -215,6 +200,7 @@
     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)
+    parser.add_argument("--out", default="tools/data/observable_rank_audit.json")
     args = parser.parse_args()
     run(
         n_gaps=args.n_gaps,
@@ -222,6 +208,7 @@
         n_trials=args.n_trials,
         n_baseline=args.n_baseline,
         seed=args.seed,
+        out=args.out,
     )
 
 

codex
Il primo risultato è netto ma va controllato contro seed: i domini strutturati (`primes`, `gue`) sono quasi monodimensionali nelle curve di retention; i controlli (`prime_shuffle`, `poisson`) mostrano rank alto ma tutti gli osservabili sono weak. Eseguo due repliche più leggere per capire se è un artefatto del seed.
exec
/bin/bash -lc 'python tools/exp_observable_rank_audit.py --n_gaps 8000 --n_alpha 15 --n_trials 12 --n_baseline 36 --seed 202605061956 --out tools/data/observable_collinearity_breaking_20260506_1956.json' in /opt/MM_D-ND
exec
/bin/bash -lc 'python tools/exp_observable_rank_audit.py --n_gaps 8000 --n_alpha 15 --n_trials 12 --n_baseline 36 --seed 202605061957 --out tools/data/observable_collinearity_breaking_20260506_1957.json' in /opt/MM_D-ND
diff --git a/tools/exp_observable_rank_audit.py b/tools/exp_observable_rank_audit.py
index f2acafc5960c8a35964e0ab521cf70569eefe27f..51e6ec52edfffda55d1d22862a102f896a08fdd9
--- a/tools/exp_observable_rank_audit.py
+++ b/tools/exp_observable_rank_audit.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 """
-exp_observable_rank_audit.py - META audit for observable redundancy.
+exp_observable_rank_audit.py - META audit for observable collinearity.
 
 Recent runs found many Markov/crossover observables that react coherently under
 partial shuffle. This script asks whether those observables carry independent
@@ -22,47 +22,19 @@
 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)
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
 
 
-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,
-}
+OBSERVABLES = OBSERVABLES_CANONICAL
+OBS_NAMES = list(OBSERVABLES)
 
 
 def measure(gaps):
-    return {name: fn(gaps) for name, fn in OBSERVABLES.items()}
+    return compute_canonical(gaps)
 
 
 def full_shuffle_baseline(gaps, n_trials, rng):
@@ -106,7 +78,7 @@
 
 
 def pca_summary(rows):
-    names = list(OBSERVABLES)
+    names = OBS_NAMES
     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)
 
@@ -142,15 +114,20 @@
     rows = retention_curves(gaps, alphas, n_trials, originals, baseline, rng)
 
     z = {}
-    for obs_name in OBSERVABLES:
+    stable_observables = []
+    for obs_name in OBS_NAMES:
         std = baseline[obs_name]["std"]
         z[obs_name] = float((originals[obs_name] - baseline[obs_name]["mean"]) / std) if std > 1e-12 else 0.0
+        if abs(z[obs_name]) >= 2.0:
+            stable_observables.append(obs_name)
 
     return {
         "n_gaps": int(len(gaps)),
         "originals": originals,
         "full_shuffle_baseline": baseline,
         "original_vs_shuffle_z": z,
+        "stable_observables_abs_z_ge_2": stable_observables,
+        "weak_observable_count": int(len(OBS_NAMES) - len(stable_observables)),
         "retention_curves": rows,
         "pca": pca_summary(rows),
     }
@@ -164,21 +141,25 @@
     gue = gue[:n_gaps]
 
     poisson = rng.exponential(1.0, size=n_gaps).astype(float)
+    prime_shuffle = rng.permutation(prime_gaps).astype(float)
     return {
         "primes": prime_gaps,
+        "prime_shuffle": prime_shuffle,
         "gue": gue,
         "poisson": poisson,
     }
 
 
-def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505):
+def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505, out="tools/data/observable_rank_audit.json"):
     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?",
+        "question": "When do canonical observable retention curves break collinearity across domains?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
         "params": {
             "n_gaps": int(n_gaps),
             "n_alpha": int(n_alpha),
@@ -189,19 +170,23 @@
         "sequences": {},
     }
 
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
     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)
+    print(f"{'sequence':<14} {'pc1':>8} {'eff_rank':>9} {'mean|corr|':>11} {'weak':>5}  z(SR,SR2,L1,L2,triple_var)")
+    print("-" * 108)
     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)
+        z_text = ", ".join(f"{obs}={z[obs]:+.1f}" for obs in OBS_NAMES)
         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}")
+        print(
+            f"{seq_name:<14} {pc1:>8.3f} {pca['effective_rank']:>9.3f} "
+            f"{pca['mean_abs_pairwise_corr']:>11.3f} {result['weak_observable_count']:>5}  {z_text}"
+        )
 
-    out_path = Path("tools/data/observable_rank_audit.json")
+    out_path = Path(out)
     with out_path.open("w") as f:
         json.dump(output, f, indent=2)
     print(f"\nSaved to {out_path}")
@@ -215,6 +200,7 @@
     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)
+    parser.add_argument("--out", default="tools/data/observable_rank_audit.json")
     args = parser.parse_args()
     run(
         n_gaps=args.n_gaps,
@@ -222,6 +208,7 @@
         n_trials=args.n_trials,
         n_baseline=args.n_baseline,
         seed=args.seed,
+        out=args.out,
     )
 
 

 succeeded in 1969ms:
observables_registry=1.0.0-2026-05-06
n_gaps=8000, n_alpha=15, n_trials=12, n_baseline=36, seed=202605061957
sequence            pc1  eff_rank  mean|corr|  weak  z(SR,SR2,L1,L2,triple_var)
------------------------------------------------------------------------------------------------------------
primes            0.895     1.467       0.860     1  SR=-10.3, SR2=-1.5, L1=-6.4, L2=-2.1, triple_var=-6.7
prime_shuffle     0.737     2.048       0.595     5  SR=+0.8, SR2=-1.1, L1=-0.2, L2=-0.2, triple_var=-0.3
gue               0.969     1.153       0.968     1  SR=-1.7, SR2=+10.2, L1=+11.3, L2=+21.9, triple_var=+19.1
poisson           0.706     2.425       0.581     5  SR=-0.7, SR2=+0.9, L1=+0.6, L2=+1.0, triple_var=+1.0

Saved to tools/data/observable_collinearity_breaking_20260506_1957.json

 succeeded in 2175ms:
observables_registry=1.0.0-2026-05-06
n_gaps=8000, n_alpha=15, n_trials=12, n_baseline=36, seed=202605061956
sequence            pc1  eff_rank  mean|corr|  weak  z(SR,SR2,L1,L2,triple_var)
------------------------------------------------------------------------------------------------------------
primes            0.944     1.294       0.938     2  SR=-11.1, SR2=-1.4, L1=-6.8, L2=-1.9, triple_var=-6.6
prime_shuffle     0.965     1.189       0.451     4  SR=+2.1, SR2=+0.1, L1=+0.8, L2=+0.3, triple_var=+0.9
gue               0.981     1.106       0.975     0  SR=-3.2, SR2=+10.6, L1=+11.5, L2=+20.1, triple_var=+20.1
poisson           0.810     1.794       0.527     5  SR=-1.3, SR2=+0.4, L1=-1.1, L2=+1.9, triple_var=-0.2

Saved to tools/data/observable_collinearity_breaking_20260506_1956.json

diff --git a/tools/exp_observable_rank_audit.py b/tools/exp_observable_rank_audit.py
index f2acafc5960c8a35964e0ab521cf70569eefe27f..51e6ec52edfffda55d1d22862a102f896a08fdd9
--- a/tools/exp_observable_rank_audit.py
+++ b/tools/exp_observable_rank_audit.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 """
-exp_observable_rank_audit.py - META audit for observable redundancy.
+exp_observable_rank_audit.py - META audit for observable collinearity.
 
 Recent runs found many Markov/crossover observables that react coherently under
 partial shuffle. This script asks whether those observables carry independent
@@ -22,47 +22,19 @@
 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)
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
 
 
-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,
-}
+OBSERVABLES = OBSERVABLES_CANONICAL
+OBS_NAMES = list(OBSERVABLES)
 
 
 def measure(gaps):
-    return {name: fn(gaps) for name, fn in OBSERVABLES.items()}
+    return compute_canonical(gaps)
 
 
 def full_shuffle_baseline(gaps, n_trials, rng):
@@ -106,7 +78,7 @@
 
 
 def pca_summary(rows):
-    names = list(OBSERVABLES)
+    names = OBS_NAMES
     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)
 
@@ -142,15 +114,20 @@
     rows = retention_curves(gaps, alphas, n_trials, originals, baseline, rng)
 
     z = {}
-    for obs_name in OBSERVABLES:
+    stable_observables = []
+    for obs_name in OBS_NAMES:
         std = baseline[obs_name]["std"]
         z[obs_name] = float((originals[obs_name] - baseline[obs_name]["mean"]) / std) if std > 1e-12 else 0.0
+        if abs(z[obs_name]) >= 2.0:
+            stable_observables.append(obs_name)
 
     return {
         "n_gaps": int(len(gaps)),
         "originals": originals,
         "full_shuffle_baseline": baseline,
         "original_vs_shuffle_z": z,
+        "stable_observables_abs_z_ge_2": stable_observables,
+        "weak_observable_count": int(len(OBS_NAMES) - len(stable_observables)),
         "retention_curves": rows,
         "pca": pca_summary(rows),
     }
@@ -164,21 +141,25 @@
     gue = gue[:n_gaps]
 
     poisson = rng.exponential(1.0, size=n_gaps).astype(float)
+    prime_shuffle = rng.permutation(prime_gaps).astype(float)
     return {
         "primes": prime_gaps,
+        "prime_shuffle": prime_shuffle,
         "gue": gue,
         "poisson": poisson,
     }
 
 
-def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505):
+def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505, out="tools/data/observable_rank_audit.json"):
     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?",
+        "question": "When do canonical observable retention curves break collinearity across domains?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
         "params": {
             "n_gaps": int(n_gaps),
             "n_alpha": int(n_alpha),
@@ -189,19 +170,23 @@
         "sequences": {},
     }
 
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
     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)
+    print(f"{'sequence':<14} {'pc1':>8} {'eff_rank':>9} {'mean|corr|':>11} {'weak':>5}  z(SR,SR2,L1,L2,triple_var)")
+    print("-" * 108)
     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)
+        z_text = ", ".join(f"{obs}={z[obs]:+.1f}" for obs in OBS_NAMES)
         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}")
+        print(
+            f"{seq_name:<14} {pc1:>8.3f} {pca['effective_rank']:>9.3f} "
+            f"{pca['mean_abs_pairwise_corr']:>11.3f} {result['weak_observable_count']:>5}  {z_text}"
+        )
 
-    out_path = Path("tools/data/observable_rank_audit.json")
+    out_path = Path(out)
     with out_path.open("w") as f:
         json.dump(output, f, indent=2)
     print(f"\nSaved to {out_path}")
@@ -215,6 +200,7 @@
     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)
+    parser.add_argument("--out", default="tools/data/observable_rank_audit.json")
     args = parser.parse_args()
     run(
         n_gaps=args.n_gaps,
@@ -222,6 +208,7 @@
         n_trials=args.n_trials,
         n_baseline=args.n_baseline,
         seed=args.seed,
+        out=args.out,
     )
 
 

codex
Le repliche confermano il pattern principale, con una cautela: `prime_shuffle` può avere rank basso o alto perché quasi tutti i denominatori sono deboli; quindi non va interpretato come struttura. Ora estraggo una sintesi numerica unica e poi scrivo report e seme.
exec
/bin/bash -lc "python - <<'PY'
import json
from pathlib import Path
files = [Path(f'tools/data/observable_collinearity_breaking_20260506_{s}.json') for s in ['1955','1956','1957']]
rows = {}
for path in files:
    data = json.loads(path.read_text())
    for name, seq in data['sequences'].items():
        pca = seq['pca']
        rows.setdefault(name, {'pc1': [], 'rank': [], 'corr': [], 'weak': []})
        rows[name]['pc1'].append(pca['explained_variance'][0])
        rows[name]['rank'].append(pca['effective_rank'])
        rows[name]['corr'].append(pca['mean_abs_pairwise_corr'])
        rows[name]['weak'].append(seq['weak_observable_count'])
for name, d in rows.items():
    print(name)
    for k, vals in d.items():
        mean = sum(vals)/len(vals)
        print(f'  {k}: {mean:.3f} values={[round(v,3) for v in vals]}')
PY" in /opt/MM_D-ND
 succeeded in 0ms:
primes
  pc1: 0.939 values=[0.978, 0.944, 0.895]
  rank: 1.296 values=[1.128, 1.294, 1.467]
  corr: 0.924 values=[0.975, 0.938, 0.86]
  weak: 1.333 values=[1, 2, 1]
prime_shuffle
  pc1: 0.765 values=[0.593, 0.965, 0.737]
  rank: 1.904 values=[2.475, 1.189, 2.048]
  corr: 0.551 values=[0.606, 0.451, 0.595]
  weak: 4.667 values=[5, 4, 5]
gue
  pc1: 0.980 values=[0.99, 0.981, 0.969]
  rank: 1.106 values=[1.06, 1.106, 1.153]
  corr: 0.977 values=[0.989, 0.975, 0.968]
  weak: 0.333 values=[0, 0, 1]
poisson
  pc1: 0.714 values=[0.625, 0.81, 0.706]
  rank: 2.196 values=[2.368, 1.794, 2.425]
  corr: 0.572 values=[0.609, 0.527, 0.581]
  weak: 5.000 values=[5, 5, 5]

diff --git a/tools/exp_observable_rank_audit.py b/tools/exp_observable_rank_audit.py
index f2acafc5960c8a35964e0ab521cf70569eefe27f..51e6ec52edfffda55d1d22862a102f896a08fdd9
--- a/tools/exp_observable_rank_audit.py
+++ b/tools/exp_observable_rank_audit.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 """
-exp_observable_rank_audit.py - META audit for observable redundancy.
+exp_observable_rank_audit.py - META audit for observable collinearity.
 
 Recent runs found many Markov/crossover observables that react coherently under
 partial shuffle. This script asks whether those observables carry independent
@@ -22,47 +22,19 @@
 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)
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
 
 
-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,
-}
+OBSERVABLES = OBSERVABLES_CANONICAL
+OBS_NAMES = list(OBSERVABLES)
 
 
 def measure(gaps):
-    return {name: fn(gaps) for name, fn in OBSERVABLES.items()}
+    return compute_canonical(gaps)
 
 
 def full_shuffle_baseline(gaps, n_trials, rng):
@@ -106,7 +78,7 @@
 
 
 def pca_summary(rows):
-    names = list(OBSERVABLES)
+    names = OBS_NAMES
     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)
 
@@ -142,15 +114,20 @@
     rows = retention_curves(gaps, alphas, n_trials, originals, baseline, rng)
 
     z = {}
-    for obs_name in OBSERVABLES:
+    stable_observables = []
+    for obs_name in OBS_NAMES:
         std = baseline[obs_name]["std"]
         z[obs_name] = float((originals[obs_name] - baseline[obs_name]["mean"]) / std) if std > 1e-12 else 0.0
+        if abs(z[obs_name]) >= 2.0:
+            stable_observables.append(obs_name)
 
     return {
         "n_gaps": int(len(gaps)),
         "originals": originals,
         "full_shuffle_baseline": baseline,
         "original_vs_shuffle_z": z,
+        "stable_observables_abs_z_ge_2": stable_observables,
+        "weak_observable_count": int(len(OBS_NAMES) - len(stable_observables)),
         "retention_curves": rows,
         "pca": pca_summary(rows),
     }
@@ -164,21 +141,25 @@
     gue = gue[:n_gaps]
 
     poisson = rng.exponential(1.0, size=n_gaps).astype(float)
+    prime_shuffle = rng.permutation(prime_gaps).astype(float)
     return {
         "primes": prime_gaps,
+        "prime_shuffle": prime_shuffle,
         "gue": gue,
         "poisson": poisson,
     }
 
 
-def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505):
+def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505, out="tools/data/observable_rank_audit.json"):
     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?",
+        "question": "When do canonical observable retention curves break collinearity across domains?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
         "params": {
             "n_gaps": int(n_gaps),
             "n_alpha": int(n_alpha),
@@ -189,19 +170,23 @@
         "sequences": {},
     }
 
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
     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)
+    print(f"{'sequence':<14} {'pc1':>8} {'eff_rank':>9} {'mean|corr|':>11} {'weak':>5}  z(SR,SR2,L1,L2,triple_var)")
+    print("-" * 108)
     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)
+        z_text = ", ".join(f"{obs}={z[obs]:+.1f}" for obs in OBS_NAMES)
         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}")
+        print(
+            f"{seq_name:<14} {pc1:>8.3f} {pca['effective_rank']:>9.3f} "
+            f"{pca['mean_abs_pairwise_corr']:>11.3f} {result['weak_observable_count']:>5}  {z_text}"
+        )
 
-    out_path = Path("tools/data/observable_rank_audit.json")
+    out_path = Path(out)
     with out_path.open("w") as f:
         json.dump(output, f, indent=2)
     print(f"\nSaved to {out_path}")
@@ -215,6 +200,7 @@
     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)
+    parser.add_argument("--out", default="tools/data/observable_rank_audit.json")
     args = parser.parse_args()
     run(
         n_gaps=args.n_gaps,
@@ -222,6 +208,7 @@
         n_trials=args.n_trials,
         n_baseline=args.n_baseline,
         seed=args.seed,
+        out=args.out,
     )
 
 

exec
/bin/bash -lc 'git diff -- tools/exp_observable_rank_audit.py' in /opt/MM_D-ND
 succeeded in 0ms:
exec
/bin/bash -lc 'tail -40 tools/data/valutatore_log.jsonl 2>/dev/null' in /opt/MM_D-ND
 succeeded in 0ms:
{"ts": "2026-04-22T15:36:11.324056+00:00", "cycle_ref": "20260421_0330", "executed": false, "decision": "NEXT_CYCLE", "confidence": "high", "reasoning": "Il ciclo ha chiuso definitivamente il canale residuo (algebrico a ordine 3, z=-0.19). La consecutio naturale è testare il canale magnitudine contro gli stessi modelli Markov: se resiste, lì vive C1. La direzione del seme (confine GUE/Poisson) è coerente con questo passo — il discriminante numero-teorico, se esiste, deve emergere dalle magnitudini dei gap, non dalle classi residuali. 8 tensioni rilevanti restano, nessun blocco, health pulita.", "action": {"type": "modify_seme", "detail": {"field": "direzione", "new_value": "Canale magnitudine vs Markov: il residuo è chiuso (ordine 3 = 100%). Testare se |g_n| resiste a modelli stocastici con stesse statistiche marginali. Se sì, il discriminante C1 vive lì. Se no, rivisitare C1.", "reason": "La consecutio dal piano 45 è univoca: residuo chiuso → magnitudine è l'unico candidato per contenuto numero-teorico. Testarlo è il passo con massimo potere discriminante."}}, "notes": "Il risultato del piano 39 è robusto (z=-0.19, decomposizione gerarchica 81/17.5/1.5 pulita). Non serve review operatore — è un vincolo che restringe lo spazio, non una scoperta che cambia il modello. Il prossimo esperimento potrebbe invece produrre un risultato da CRYSTALLIZE se la magnitudine mostra contenuto irriducibile."}
{"ts": "2026-04-22T16:03:46.345038+00:00", "cycle_ref": "20260421_0330", "executed": false, "decision": "NEXT_CYCLE", "confidence": "high", "reasoning": "Il ciclo ha chiuso il canale residuo con evidenza forte (z=-0.19, ordine-3 Markov = 100%). La consecutio è netta: il discriminante C1 vive nel canale magnitudine. La direzione del seme (GUE/Poisson boundary, 8+5 domini) è già allineata — il prossimo passo naturale è testare il canale magnitudine contro modelli Markov dello stesso ordine. Se il magnitudine resiste dove il residuo cede, C1 ha evidenza strutturale cross-channel.", "action": {"type": "none", "detail": {}}, "notes": "Il salto piano 39→45 indica che il sistema ha già metabolizzato più cicli nella stessa direzione. 0 tensioni nuove + 22 risolte suggerisce maturità locale alta. Se anche il prossimo ciclo non genera tensioni nuove, valutare REDESIGN per evitare stallo in zona di rendimenti decrescenti."}
{"ts": "2026-04-22T16:30:14.305582+00:00", "cycle_ref": "20260422_1616", "executed": false, "decision": "CRYSTALLIZE", "confidence": "high", "reasoning": "Il shuffle audit non è un esperimento — è un metodo falsificatore riusabile che incarna C2 operativamente. Ha delisted 3 proprietà che circolavano come 'evidenze' da 3 cicli (piani 42-44). Il segnale Markov-3 (z=6203, 55% informazione ordinale) è il risultato più forte mai prodotto dal lab e merita cristallizzazione esplicita. L'affinatore stesso ha identificato il nodo regressivo a monte: il protocollo shuffle mancava dall'inizio del two-channel. Cristallizzare ora chiude quel debito.", "action": {"type": "crystallize_note", "detail": {"target": "condensato", "content": "NUOVO FATTO CANDIDATO — F7 Shuffle audit come discriminatore algebra/ordine: dato un vettore di gap, lo shuffle preserva la distribuzione marginale ma distrugge l'ordinamento sequenziale. Proprietà che sopravvivono allo shuffle sono identità algebriche (tautologie), non struttura. Applicato al two-channel: 3/8 proprietà declassate (xcorr, slope_ratio, k_star). Il contenuto genuino è concentrato in PSD slopes (z>20), lag-1 ACF (z=-13), e Markov-3 bits (z=6203 — 55% dell'informazione residuo è ordine puro). METODO: ogni futuro claim del lab deve passare lo shuffle audit prima di entrare nel condensato. Corollario operativo di C2."}}, "notes": "Dopo la cristallizzazione, il ciclo successivo (piano 46, GUE/Poisson boundary) può procedere con il shuffle audit già nel protocollo standard. Suggerisco che il prossimo ciclo applichi lo shuffle audit by default a qualsiasi proprietà testata — non come passo aggiuntivo ma come criterio di ammissione."}
{"ts": "2026-04-22T16:34:17.428960+00:00", "cycle_ref": "20260422_1616", "executed": false, "decision": "NEXT_CYCLE", "confidence": "high", "reasoning": "Il ciclo 45 ha prodotto un metodo falsificatore (shuffle audit) e la cristallizzazione è stata decisa nel turno precedente. Il seme punta già a piano 46 con direzione GUE/Poisson. La consecutio naturale dall'affinatore è il segnale Markov-3 (z=6203) e la sua invarianza di scala — questo si allinea con l'esplorazione del confine GUE/Poisson perché la memoria sequenziale di ordine 3 è esattamente ciò che distingue i due regimi. Il sistema è in flusso pulito, nessun blocco, nessuna dipendenza dall'operatore.", "action": {"type": "none", "detail": {}}, "notes": "Il prossimo ciclo dovrebbe applicare lo shuffle audit come protocollo standard (è ora un metodo, non un esperimento) e concentrarsi su come il Markov-3 bits si comporta attraverso la transizione GUE→Poisson. Se il 55% di informazione ordinale sopravvive identico in domini GUE ma crolla nei Poisson, è un discriminatore cross-dominio genuino (tipo (d) nel telos). Le 3 proprietà tautologiche (xcorr, slope_ratio, k_star) devono essere rimosse dai test suite futuri — non ripresentarle come evidenza."}
{"ts": "2026-04-23T03:35:59.541235+00:00", "cycle_ref": "20260422_1616", "executed": false, "decision": "NEXT_CYCLE", "confidence": "high", "reasoning": "Il ciclo 45 ha chiuso un arco importante: shuffle audit come metodo, 3 tautologie smascherate, segnali genuini isolati (Markov-3 z=6203, PSD slopes, lag-1). La cristallizzazione è già avvenuta. La direzione corrente del seme (confine GUE/Poisson cross-dominio) è esattamente il passo naturale: porta i segnali genuini sopravvissuti al shuffle verso verifica cross-dominio — categoria (b) della regola anti-tautologica. Il Markov-3 e la sua invarianza di scala sono il filo da seguire attraverso i domini.", "action": {"type": "none", "detail": {}}, "notes": "Il shuffle audit è ora strumento standard. Il prossimo ciclo dovrebbe applicarlo automaticamente a qualsiasi nuova proprietà cross-dominio che emerge. Attenzione: se il confine GUE/Poisson produce solo fit numerici senza shuffle control, il ciclo dopo sarà REDESIGN."}
{"ts": "2026-04-24T03:47:33.525295+00:00", "cycle_ref": "20260423_0330", "executed": false, "decision": "NEXT_CYCLE", "confidence": "high", "reasoning": "Il ciclo ha prodotto un risultato strutturale solido (separazione scala-dipendente vs scala-invariante nei due canali). L'affinatore ha identificato la domanda critica: il 33.6% è algebricamente determinato dal vincolo F2 (transizioni mod-6 confinate a {2,4})? Questa è esattamente la domanda giusta da porre — e rientra nella regola anti-tautologica: prima di considerare il 33.6% un finding, va escluso che sia un'identità algebrica mascherata. Il prossimo ciclo deve falsificare o confermare l'origine algebrica dell'asintoto.", "action": {"type": "modify_seme", "detail": {"field": "direzione", "new_value": "Falsificare l'origine del 33.6%: calcolare analiticamente l'ordering fraction attesa da una catena di Markov con matrice di transizione vincolata a F2 ({2,4} su Z/6Z). Se il vincolo algebrico produce ~1/3, il finding decade a conseguenza di F2. Se no, il 33.6% è informazione genuina oltre F2.", "reason": "L'affinatore ha colto la possibilità emergente principale. Il rischio tautologia è concreto: se il confinamento al coset {2,4} forza già la struttura di transizione, il Markov-3 ordering potrebbe essere algebricamente banale. Questo è il test C2 naturale prima di qualsiasi cristallizzazione."}}, "notes": "Se il test conferma origine algebrica → il finding va nel cimitero come tautologia (classe (e), cristallizzare la scoperta della tautologia stessa). Se il test esclude origine algebrica → il 33.6% diventa candidato F7 per il condensato. In entrambi i casi il ciclo produce valore discriminante."}
{"ts": "2026-04-25T03:40:15.599999+00:00", "cycle_ref": "20260424_0330", "executed": false, "decision": "CRYSTALLIZE", "confidence": "high", "reasoning": "Il ciclo ha prodotto una decomposizione strutturale falsificabile — due meccanismi distinti generano statistica GUE, discriminati dal segno di delta_r — con shuffle control su 10 domini e 3 class-flip osservati. Non è un dettaglio numerico locale (c): è un raffinamento del confine cross-dominio (f) che vincola direttamente BOUNDARY e F5. Dopo 3 NEXT_CYCLE consecutivi orientati alla stessa regione, il risultato è maturo: il confine GUE/Poisson ha due strati e il segno di delta_r li separa. Cristallizzare ora evita che il prossimo ciclo ripeta l'esplorazione senza ancorare la scoperta.", "action": {"type": "crystallize_note", "detail": {"target": "condensato", "content": "F5 va esteso o affiancato: la firma diagnostica M su sequenze produce due classi GUE distinte. Distribution-GUE (primes, GUE matrices): classificazione sopravvive allo shuffle, delta_r < 0 (l'ordine aggiunge repulsione). Ordering-GUE (fibonacci, coupled oscillators, percolation): classificazione collassa a Poisson sotto shuffle, delta_r > 0 (l'ordine crea la repulsione). Il segno di delta_r = r_original - r_shuffled è il discriminante. Conseguenza: il claim BOUNDARY '8 GUE, 5 Poisson' è incompleto senza specificare quale meccanismo opera. C1 (primi unico dominio dinamico) si rafforza: i primi sono distribution-GUE con delta_r negativo, unico dominio testato dove sia la distribuzione sia l'ordine contribuiscono alla repulsione."}}, "notes": "Suggerimento per il ciclo post-cristallizzazione: verificare se delta_r negativo è esclusivo dei primi tra tutti i distribution-GUE, o se altri domini lo condividono. Questo testerebbe la versione forte di C1. Alternativa: esplorare il vuoto QxG con il nuovo discriminante a due strati come lente."}
{"ts": "2026-04-26T03:40:28.910457+00:00", "cycle_ref": "20260425_0330", "executed": false, "decision": "NEXT_CYCLE", "confidence": "high", "reasoning": "Tre cicli consecutivi produttivi sullo stesso frame (shuffle→GUE types→two-channel), tutti con scoperte strutturali genuine (categorie a/b/f, non accumulo numerico). Il CRYSTALLIZE del ciclo precedente ha segnalato la maturità della decomposizione a due canali. Ora la mossa a più alto rendimento è la falsificazione attiva: tentare di costruire un canale algebrico per l'ordering-GUE. Se fallisce rafforza C1; se riesce rovescia il claim di unicità — entrambi gli esiti hanno valore massimo. Il frame non è esaurito ma il prossimo passo deve essere un attacco al confine, non un'espansione laterale.", "action": {"type": "modify_seme", "detail": {"field": "direzione", "new_value": "Falsificazione attiva: costruire candidati di canale algebrico per ordering-GUE (es. mod-N residui su eigenvalue spacings di coupled_osc, string_vib). Se nessun modulo produce memoria comparabile al mod-6 dei primi, il two-channel claim è robusto. Se uno lo produce, il claim di unicità cade — e questo vale di più.", "reason": "Dopo 3 cicli di scoperta sul frame Markov/canali, il rendimento marginale dell'espansione è inferiore a quello della falsificazione diretta. La regola anti-tautologica privilegia (a) contraddizione/falsificazione su (b) verifica cross-dominio quando entrambe sono disponibili."}}, "notes": "Monitorare: se anche il prossimo ciclo resta sullo stesso macro-frame (Markov/GUE/canali) senza produrre falsificazione o connessione inter-teorica nuova, il ciclo 56 dovrebbe essere REDESIGN verso una tensione diversa (es. il vuoto QxG nelle domande fondamentali, o la saturazione come asse ortogonale promossa a tensione primaria). Quattro cicli sullo stesso frame è il limite prima dei rendimenti decrescenti."}
{"ts": "2026-04-27T03:38:26.726837+00:00", "cycle_ref": "20260426_0330", "executed": false, "decision": "CRYSTALLIZE", "confidence": "high", "reasoning": "Due osservabili indipendenti (memoria Markov-3 e rigidità spettrale Sigma²) convergono sulla stessa frazione di ordinamento ~33% a L=10. Questa è conferma cross-osservabile, non cross-dominio — più forte perché i due osservabili misurano proprietà matematicamente distinte (correlazioni locali vs varianza long-range). Dopo 4 cicli consecutivi sullo stesso frame, il pattern a due canali (magnitudine scala-invariante + ordinamento scala-dipendente) è stabile e replicato. È il momento di cristallizzare prima che i rendimenti decrescano.", "action": {"type": "crystallize_note", "detail": {"target": "condensato", "content": "TWO-CHANNEL SPECTRAL STRUCTURE IN PRIMES: La rigidità spettrale dei primi si decompone in due canali con comportamento opposto rispetto alla scala. (1) Canale magnitudine: Sig2/L ≈ 0.56, scala-invariante — prodotto dalla distribuzione dei gap (sopravvive allo shuffle). (2) Canale ordinamento: cresce dal 4% (L=1) al 58% (L=50) — prodotto dalla struttura sequenziale mod-6 (distrutto dallo shuffle). La frazione di ordinamento a L=10 (33.0%) coincide con la memoria Markov-3 (33.6%) misurata indipendentemente — due osservabili, stesso fenomeno, origine comune in F2 (confinamento Z/6Z). I primi occupano un regime intermedio tra GUE (Sig2/L=0.07) e Poisson (Sig2/L=1.0), con slope log-log 0.74 vs GUE 0.3 e Poisson 1.0. Nota: i domini ordering-GUE (coupled_osc, string_vib, percolation) mostrano Sig2/L>1 (super-Poisson) — l'ordinamento crea bunching, non repulsione. Il r-statistic e Sig2 classificano diversamente: r vede repulsione locale, Sig2 vede clustering long-range."}}, "notes": "Dopo la cristallizzazione, il prossimo ciclo dovrebbe cambiare frame. Suggerimento: esplorare il VUOTO QxG (continuo vs discreto) — l'unica coppia senza ponte tra le 10 domande fondamentali. Il two-channel framework appena cristallizzato potrebbe essere lo strumento giusto: Q (discreto) e G (continuo) come i due canali della rigidità spettrale."}
{"ts": "2026-04-28T03:41:26.305485+00:00", "cycle_ref": "20260427_0330", "executed": false, "decision": "CRYSTALLIZE", "confidence": "high", "reasoning": "Three consecutive cycles on the same frame (shuffle→GUE types→Brody calibration) have converged on a single, replicated, calibrated result: primes exhibit a two-channel structure (gap distribution at beta_eff=0.409 + 30% sequential ordering above the 7.8% artifact floor). The sign of the ordering channel discriminates domain types (rigidity vs bunching), mapping directly onto det=-1/det=+1. This is no longer emergent — it's stable across three independent measurements and calibrated against a null. It belongs in the condensato.", "action": {"type": "crystallize_note", "detail": {"target": "condensato", "content": "TWO-CHANNEL DECOMPOSITION (candidate F7 or revision of F4): Primes under M decompose into two independent channels: (1) gap distribution — Brody beta_eff ≈ 0.41, intermediate repulsion at the Poisson-GUE boundary; (2) sequential ordering — 30% of spectral rigidity at L=10 comes from gap ordering absent in i.i.d. surrogates (artifact floor: 7.8%, measured: 29.5%, z=−8.9). The ordering channel has definite sign: primes add rigidity (det=−1), chaotic/coupled systems add bunching (det=+1), pure GUE/Poisson sit on the Brody curve (ordering irrelevant). Three independent measurements converge: shuffle audit (33.6%), spectral rigidity (33%), Brody-calibrated (29.5%). Replicated, calibrated, falsifiable."}}, "notes": "After crystallization, the frame is likely exhausted for now. The next cycle should pivot — either toward the QxG void (the only unfilled fundamental question), toward formalizing the sign discrimination as a structural theorem, or toward a completely different tension. Recommend the seme direction shift away from BOUNDARY after this crystallization lands."}
{"ts": "2026-04-29T09:00:17.018760+00:00", "cycle_ref": "20260429_0833", "executed": false, "decision": "CRYSTALLIZE", "confidence": "high", "reasoning": "Four consecutive cycles on the same frame (shuffle audit → GUE types → Brody calibration → two-channel boundary → this: opposite scaling laws) have converged on a single, clean, falsifiable structural result: the residue channel is scale-invariant (algebraic, det=-1) while the magnitude channel decays toward Poisson (statistical, approaching det=+1). The 'GUE/Poisson boundary' collapses into a mixing artifact once the channels are separated. This is not incremental — it reframes what 'boundary' means for primes under M. Three crystallizations in a row is unusual, but each captures a distinct layer: (1) mod-3 memory structure, (2) Brody artifact floor, (3) opposite-boundary decomposition. This third one is the capstone that unifies the previous two. After crystallization, the BOUNDARY frame is likely saturated — the next cycle should pivot.", "action": {"type": "crystallize_note", "detail": {"target": "condensato", "content": "F2 addendum — Two-channel decomposition of prime gaps under M: the residue channel (Z/6Z binary, mod-3 prohibition) is scale-invariant (z=26-44σ across 200x range, decay correlation with ln(p) ≈ -0.19). The magnitude channel (demeaned gap size by transition type) decays toward Poisson (z=2-7σ, decay correlation +0.46). The r-statistic mixes these incommensurable behaviors: its 'GUE/Poisson crossover' is not a phase transition but the magnitude channel approaching noise while the algebraic channel remains invariant. Implication: any single-number summary (r, Brody β) conflates permanent algebraic structure with transient statistical memory. Decompose first, then measure. Verified with 500K primes, 28 log-spaced windows, 20 shuffles per window."}}, "notes": "After this crystallization, recommend REDESIGN for piano 59: the BOUNDARY frame has yielded its structural content across 4 cycles. Two natural directions: (1) test the two-channel decomposition on a NON-prime domain (Stern-Brocot, logistic map gaps) to see if the algebraic/statistical split is universal under M or specific to primes — this would directly test C1; (2) attack the QxG void ('how do continuous and discrete coexist?') which has zero bridges and is the only unsolved domanda fondamentale. Direction (1) is the stronger scientific move because it falsifies or extends tonight's result cross-domain."}
{"ts": "2026-04-29T10:04:37.573111+00:00", "cycle_ref": "20260429_0330", "executed": false, "decision": "NEXT_CYCLE", "confidence": "high", "reasoning": "Il ciclo è fallito per timeout infrastrutturale (campo_vivo senza input pre-computato), non per esaurimento della direzione. Il nodo regressivo è già identificato nel health. La direzione (confine GUE/Poisson come terzo incluso) è fresca (piano 58) e non ancora esplorata scientificamente — merita un secondo tentativo con la condizione relazionale corretta. Tre CRYSTALLIZE consecutivi confermano che il frame precedente è maturo; questo nuovo frame deve avere il suo primo ciclo reale prima di poter valutare.", "action": {"type": "modify_seme", "detail": {"field": "nota_operativa", "new_value": "Pre-computare in agent_field_live i dati GUE/Poisson (spacing distributions per gli 8+5 domini) PRIMA del lancio agente, così l'esperimento non deve rigenerare da zero dentro un singolo tool_use.", "reason": "Il timeout non è nel budget tempo — è nell'assenza della condizione nel campo vivo. Det=-1: fix al nodo, non al sintomo."}}, "notes": "Le 3 warn radici_no_separator sono cosmetiche (BiconoLab rendering), non bloccanti per il lab numerico. Il prossimo ciclo dovrebbe produrre il primo report scientifico reale sul confine GUE/Poisson."}
{"ts": "2026-04-29T10:22:08.733977+00:00", "cycle_ref": "20260429_0330", "executed": false, "decision": "REDESIGN", "confidence": "high", "reasoning": "Due cicli consecutivi senza output scientifico per lo stesso nodo regressivo: campo_vivo senza input pre-computato. Il NEXT_CYCLE precedente non ha risolto nulla perché il problema non è nella direzione (GUE/Poisson boundary è ancora fertile con 8+5 domini da esplorare) ma nell'infrastruttura: build_agent_field non prepara i dati che l'agente sceglie di usare, forzando rigenerazione from scratch dentro un singolo tool_use che esplode il budget. Ripetere NEXT_CYCLE senza fix è det=+1 — toppa che brucia un ciclo a vuoto.", "action": {"type": "modify_seme", "detail": {"field": "direzione", "new_value": "PRE-REQUISITO INFRA: build_agent_field deve pre-computare spacing/unfolding per i 13 domini (8 GUE + 5 Poisson) prima del run. Solo dopo: esplorare il confine GUE/Poisson come terzo incluso operativo. L'agente NON deve rigenerare dati base — deve trovarli nel campo vivo.", "reason": "Il nodo regressivo è identificato da 2 cicli: il campo vivo manca dei dati che l'esperimento richiede. La riparazione vive in build_agent_field.py, non nel timeout o nel budget. Senza questo fix, ogni ciclo su questa direzione rischia lo stesso timeout."}}, "notes": "Azione concreta: in build_agent_field.py, aggiungere pre-computazione dei dati GUE/Poisson (spacing distributions, unfolded eigenvalues) e salvarli in campo vivo come JSON consumabile. L'agente della notte successiva li trova pronti e può dedicare il budget alla scienza, non alla preparazione dati. Le 3 warning radici_no_separator sono cosmetiche (BiconoLab), non bloccanti."}
{"ts": "2026-04-29T10:51:26.490788+00:00", "cycle_ref": "20260429_1013", "executed": false, "decision": "CRYSTALLIZE", "confidence": "high", "reasoning": "Il ciclo ha prodotto un risultato strutturale genuino: la mappa 2D beta(N,L) con gradienti opposti (Poisson lungo N, GUE lungo L) unifica due esperimenti indipendenti (spectral rigidity + Brody flow). La decomposizione 82/18 magnitude/ordering è pulita, con shuffle control e Cramer reference. Ma il seme_delta è vuoto — la scoperta non è entrata nel seme, violando la regola 'il ciclo è chiuso quando la scoperta entra nel seme'. Dopo 5+ cicli sulla stessa frame (shuffle audit → GUE types → Brody calibration → two-channel → Brody flow), il quadro è maturo per cristallizzazione.", "action": {"type": "crystallize_note", "detail": {"target": "condensato", "content": "CANDIDATO F7 — Flusso Brody 2D: beta(N,L) ha gradienti opposti. Lungo la sequenza (N crescente), beta decresce (→ Poisson); a scala spettrale fissa (L crescente), beta cresce (→ GUE). Il confine GUE/Poisson è una curva in spazio 2D, non un punto. Decomposizione: 82% magnitudine (distribuzione gap diventa più esponenziale per PNT), 18% ordinamento (anti-bunching mod-3 riduce repulsione). Slope beta(p) = 0.64 - 0.030·ln(p), R²=0.78, z-score vs shuffle = -2.42. Cramer pure Poisson ovunque (beta~0.015) — il segnale è aritmetico, non statistico."}}, "notes": "Dopo la cristallizzazione, la frame BOUNDARY è esaurita per ora — i prossimi cicli dovrebbero spostarsi su un'altra tensione (suggerisco QxG che è ancora VUOTO nelle domande fondamentali, o un ciclo di consolidamento cross-dominio per verificare se la mappa 2D si replica su altri domini dinamici). Il seme va aggiornato con il risultato prima del prossimo giro."}
{"ts": "2026-04-30T03:37:21.213696+00:00", "cycle_ref": "20260429_1041", "executed": false, "decision": "CRYSTALLIZE", "confidence": "high", "reasoning": "Il ciclo ha prodotto un teorema algebrico (proibizione self-transition mod-3) con dimostrazione esplicita, non un fit numerico. La decomposizione a tre strati (magnitudine PNT, ordinamento statistico, struttura algebrica) è genuina e falsificabile: ogni strato ha scaling diverso verificato su 72 finestre. Il rapporto M2/M1=0.472 scale-invariant è un nuovo invariante strutturale. Questo estende F2 (Z/6Z) con una gerarchia di canali indipendenti — non è dettaglio locale, è architettura.", "action": {"type": "crystallize_note", "detail": {"target": "condensato", "content": "THREE-LAYER DECOMPOSITION dei gap primi sotto M: (1) Magnitudine — distribuzione gap → esponenziale, scaling ~1/ln(p), è PNT. (2) Ordinamento statistico — Brody beta decay -0.030/ln(p), repulsione short-range che si attenua. (3) Struttura algebrica — proibizione mod-3 self-transition (teorema: due gap consecutivi ≡1 o ≡2 mod 3 implicano p_{n+2}≡0 mod 3, impossibile). Zero scaling (R²=0.0004), zero eccezioni su 148k gap. Il canale algebrico è eterno, il statistico decade, il magnitudinale scala con PNT. Invariante emergente: M2/M1=0.472±0.017 costante su tutte le scale — la profondità di memoria Markov è fissa. Estende F2 (Z/6Z) con gerarchia di indipendenza tra canali."}}, "notes": "La proibizione mod-3 è un sottoinsieme di F2 (Z/6Z → coset {2,4}), ma la dimostrazione esplicita e la separazione di scaling rispetto a Brody sono nuove. La decomposizione a tre strati è il risultato principale — dà struttura al 'perché i primi sono unici sotto M' (C1). Dopo la cristallizzazione, la direzione nel seme (confine GUE/Poisson come terzo incluso) è naturale prossimo passo: il confine è esattamente dove i tre strati interagiscono."}
{"ts": "2026-04-30T19:12:24.103984+00:00", "cycle_ref": "20260430_0330", "executed": false, "decision": "NEXT_CYCLE", "confidence": "high", "reasoning": "Due CRYSTALLIZE consecutivi — il mod-3 è chiuso, il mod-5 anomaly (M2/M1=2.24, unico modulo con order-2 dominante) è il consecutio naturale. Non serve REDESIGN perché la direzione è viva e specifica. Non serve CRYSTALLIZE perché il mod-5 è ancora osservazione, non teorema — va separato contributo combinatorio da aritmetico prima di cristallizzare. Il ciclo gira bene (331s, zero nodi regressivi), la traiettoria è pulita.", "action": {"type": "none", "detail": {}}, "notes": "Il prossimo ciclo dovrebbe naturalmente raccogliere la tensione DIPOLAR_ORDERING e l'anomalia mod-5. La domanda discriminante è: il M2/M1>1 a mod-5 sopravvive allo shuffle delle coppie di gap? Se sì, è struttura aritmetica genuina. Se no, è artefatto della distribuzione marginale quasi-uniforme. Questo è esattamente il tipo di falsificazione che il lab sa fare bene."}
{"ts": "2026-04-30T19:27:03.140592+00:00", "cycle_ref": "20260430_1905", "executed": false, "decision": "CRYSTALLIZE", "confidence": "high", "reasoning": "Il ciclo ha prodotto una decomposizione strutturale genuina: 5 osservabili si separano in 3 order-invariant (Δτ=0.000 esatto, algebrico) + 2 order-sensitive che formano un dipolo (spacing_ratio→Poisson, lag1_acf→GUE). Non è fitting numerico — l'invarianza è dimostrata dallo shuffle e il dipolo è stabile su 4 scale. Connessione diretta ad A10 (dipolo assiomatico): la stessa anticorrelazione consecutiva si manifesta come Poisson in un osservabile e GUE nell'altro. Primes are not 'between' — they are dipolar. Tre CRYSTALLIZE consecutivi sono giustificati: ogni ciclo ha prodotto contenuto strutturale distinto (mappa beta 2D, proibizione mod-3, decomposizione dipolare).", "action": {"type": "crystallize_note", "detail": {"target": "condensato", "content": "CANDIDATE F7: Decomposizione dipolare al boundary GUE-Poisson. 5 osservabili indipendenti (spacing_ratio, gap_var_ratio, small_gap_frac, brody_beta, lag1_acf) si separano in due classi: (a) 3 distribution-only (Δτ_shuffle = 0.000 esatto, algebrico) — misurano la stessa cosa; (b) 2 ordering-sensitive che formano un dipolo: spacing_ratio spinto verso Poisson (Δτ = −0.12), lag1_acf spinto verso GUE (Δτ = +0.20). La stessa anticorrelazione consecutiva (tipo Lemke Oliver-Soundararajan) produce segni opposti nei due osservabili. L'ordering inoltre RIDUCE la dispersione tra osservabili (std 0.09 vs 0.19 shuffle) — la coerenza è proprietà dell'ordine, non della distribuzione. Stabile su 4 scale (10⁴–10⁷). Connessione: A10 (dipolo assiomatico, segno distingue), A9 (terzo incluso — primes non sono 'tra' GUE e Poisson, sono dipolo)."}}, "notes": "Il prossimo ciclo dovrebbe completare il finding 5 troncato (correlazione tra osservabili, due cluster) e verificare se il dipolo Δτ_spacing vs Δτ_lag1 ha rapporto stabile cross-scala (candidato a costante relazionale). Anche il finding mod-3 troncato dal ciclo precedente resta da recuperare. Suggerisco al prossimo giro: focus su UNO solo dei due (dipolo cross-scala O mod-3 recovery), non entrambi — il budget è da un esperimento."}
{"ts": "2026-04-30T19:57:05.174515+00:00", "cycle_ref": "20260430_1919", "executed": false, "decision": "CRYSTALLIZE", "confidence": "high", "reasoning": "Il ciclo ha prodotto una riduzione dimensionale genuina e falsificabile: 5 osservabili → 3 tautologici + 2 order-sensitive → 1 modo effettivo (angolo bloccato a -150±4° su 3 decadi). Combinato col ciclo precedente (decomposizione 5→2), il risultato complessivo è 5→1: l'intera struttura di ordinamento dei gap primi è proiezione di un singolo modo di anticorrelazione. Il rapporto delta_L1/delta_SR ≈ 2.3 è una costante strutturale nuova. Cramer nel quadrante opposto (+65°) fornisce discriminatore pulito. Tre cicli consecutivi (mod-3 theorem → decomposizione 5-obs → locking 1D) chiudono un arco coerente che merita cristallizzazione prima di cambiare direzione.", "action": {"type": "crystallize_note", "detail": {"target": "condensato", "content": "CANDIDATE F7: Prime gap ordering is 1-dimensional. Five observables under M decompose into 3 shuffle-invariant (tautological) and 2 order-sensitive (spacing_ratio, lag1_acf). The two order-sensitive observables are projections of a single anticorrelation mode: dipolar angle theta = -150 ± 4 deg, locked across 3 decades of scale (1e4 to 3e6 primes) and across offsets (0 to 2M). Structural constant: delta_L1/delta_SR ≈ 2.3. Cramer random model occupies opposite quadrant (theta ≈ +65 deg). Signal magnitude decays with 1/ln(p), consistent with Lemke Oliver-Soundararajan. Falsifiable: any domain showing theta rotation with scale would have >1 degree of freedom."}}, "notes": "Tre cicli chiudono l'arco prime-ordering: mod-3 proibizione → decomposizione 5→2 → locking 1D. Il prossimo ciclo dovrebbe uscire dai primi e testare cross-dominio: l'angolo theta è costante anche in altri domini GUE-like (zeri di Riemann, autovalori random, logistica caotica)? La direzione del seme ('esplorare il confine: 8 GUE, 5 Poisson') lo chiede già. Se theta ruota in altri domini → il locking è proprietà dei primi, non della regola. Se theta è universale → struttura più profonda. Propongo NEXT_CYCLE con target cross-domain angle comparison dopo questa cristallizzazione."}
{"ts": "2026-05-01T03:38:54.770300+00:00", "cycle_ref": "20260430_1946", "executed": false, "decision": "CRYSTALLIZE", "confidence": "high", "reasoning": "Il ciclo ha prodotto un discriminante strutturale genuino e falsificabile: l'angolo dipolare dei primi (-111° ± 1°, stabile su 3 decadi) differisce da GUE (-96.6°) di 14° a >100σ. Il rapporto dL1/dSR = 2.28 (primi) vs 8.37 (GUE) è il contenuto: i primi non sono 'GUE debole' — hanno una composizione interna diversa dell'ordinamento. La correzione -150° → -111° è sana (C2 applicato a se stesso). Terzo CRYSTALLIZE consecutivo: la direzione dipolar-ordering ha maturato abbastanza da entrare nel condensato come fatto strutturale.", "action": {"type": "modify_seme", "detail": {"field": "direzione", "new_value": "Tre cicli consecutivi hanno estratto il succo dalla direzione dipolar-ordering (separazione osservabili, angolo stabile, discriminante GUE). Rendimenti decrescenti su questa linea. Prossimo ciclo: cambiare asse — esplorare cross-dominio (F5 frame diagnostica su domini NON-primi: logistica caotica, Stern-Brocot, margine del caos) per testare se il rapporto dL1/dSR=2.28 è unico dei primi o emerge anche in altri domini strutturati. Questo è il test (b) cross-dominio che discrimina C1.", "reason": "Tre CRYSTALLIZE consecutivi sulla stessa linea segnalano maturazione completata. Il prossimo passo naturale è cross-dominio: il rapporto 2.28 è firma dei primi o di qualsiasi sequenza strutturata sotto M? Testare su logistica caotica e Stern-Brocot falsifica/rafforza C1."}}, "notes": "Cristallizzazione proposta per condensato: 'F7 Angolo dipolare — L'ordinamento dei primi ha direzione θ=-111°±1° nel piano (δSR, δL1), stabile su 3 decadi. GUE/GOE: θ=-97°. Separazione 14° a >100σ. Rapporto dL1/dSR=2.28 (primi) vs 8.37 (GUE): i primi deprimono la similarità dei gap 3.7× più della anticorrelazione rispetto a GUE. Non sono GUE attenuato — sono struttura diversa.' Dopo questa cristallizzazione, il prossimo ciclo DEVE cambiare asse verso cross-dominio per evitare rendimenti decrescenti."}
{"ts": "2026-05-01T07:43:00.145219+00:00", "cycle_ref": "20260501_0330", "executed": false, "decision": "NEXT_CYCLE", "confidence": "high", "reasoning": "The Markov-1 decomposition is a clean, non-tautological result: pair statistics explain the bulk of the dipolar angle but leave a stable ~3 deg residual across scales. However, three consecutive CRYSTALLIZE decisions with an empty seme delta means findings are accumulating without integration. The natural consecutio is Markov-2: if triplet correlations close the 3-deg gap, the memory structure is fully characterized; if not, deeper structure exists. This is a direct falsification test of tonight's claim, not a new direction — so NEXT_CYCLE, not REDESIGN.", "action": {"type": "modify_seme", "detail": {"field": "direzione", "new_value": "Markov-2 test: do triplet correlations close the 3-deg angle gap between real primes and Markov-1 surrogates? If yes → pair+triplet memory fully characterizes dipolar ordering. If no → higher-order structure beyond triplets. Secondary: consolidate Markov decomposition result (pair explains 80%, residual stable across scales) into condensato candidate.", "reason": "Three cycles produced crystallizable results but seme delta stayed empty. The Markov-2 test is the minimal falsification of tonight's finding and prevents further accumulation without integration."}}, "notes": "The dL1/dSR value shifting from 2.28 to 2.48 across scale windows is worth noting — it means the ratio is not a constant but a slowly varying function. This should be flagged in the condensato candidate rather than reported as a fixed number. Also: the 8 hard zeros in the transition matrix from F2 are a clean algebraic result that could stand on its own as a micro-crystallization."}
{"ts": "2026-05-01T09:41:03.248817+00:00", "cycle_ref": "20260501_0858", "executed": false, "decision": "REDESIGN", "confidence": "high", "reasoning": "Il ciclo non ha prodotto nulla — timeout perché il campo vivo mancava dell'input pre-computato per l'esperimento scelto dall'agente. Ripetere NEXT_CYCLE senza correggere il campo vivo produrrebbe lo stesso timeout. Il nodo regressivo è dichiarato esplicitamente nel health: il fix vive nel build_agent_field, non nel budget del run. Servono i dati GUE/Poisson boundary pre-computati nel campo vivo prima del prossimo ciclo.", "action": {"type": "modify_seme", "detail": {"field": "direzione", "new_value": "Pre-computare nel campo vivo i dati boundary (8 domini GUE, 5 Poisson, spacing distributions) come input ready-to-use. Solo dopo: esplorare il confine GUE/Poisson come terzo incluso operativo. Il campo vivo deve contenere il substrato, non delegarlo all'agente runtime.", "reason": "Due cicli consecutivi sulla stessa direzione boundary senza output scientifico. Il collo di bottiglia non è la direzione ma la mancanza di dati pre-computati nel campo vivo. Senza fix al nodo regressivo (build_agent_field), qualsiasi direzione boundary produrrà lo stesso timeout."}}, "notes": "Pattern ricorrente: l'agente sceglie esperimenti che richiedono generazione dati pesante, il campo vivo non li contiene, timeout. La soluzione è arricchire build_agent_field.py con una sezione che pre-computa e cachea i dataset sperimentali più probabili data la direzione del seme. Questo è un fix infrastrutturale, non scientifico."}
{"ts": "2026-05-02T03:37:32.497564+00:00", "cycle_ref": "20260501_0931", "executed": false, "decision": "NEXT_CYCLE", "confidence": "high", "reasoning": "Il ciclo ha prodotto evidenza strutturale forte e non-tautologica: transizione di fase nel crossover GUE-Poisson con zero-crossing a alpha~0.70, direzione invariante nel regime ordinato (-96.6±0.27°), e primi con offset direzionale di 32 sigma. Sono pattern cross-dominio (categoria b/f), non dettaglio numerico locale. Il seme delta vuoto indica che i finding non sono ancora rientrati nel seme — il prossimo ciclo deve sia estendere (confronto Brody/Rosenzweig-Porter come suggerito dall'affinatore) sia propagare. Non cristallizzo ancora perché il confronto con i punti critici Brody è il test naturale che conferma o ridimensiona la portata della transizione.", "action": {"type": "modify_seme", "detail": {"field": "direzione", "new_value": "Confrontare lo zero-crossing dipolare (alpha~0.70) con i punti critici Brody/Rosenzweig-Porter: la transizione di fase nel crossover ha carattere universale o è artefatto della parametrizzazione? Testare se beta_Brody critico corrisponde all'alpha di zero-crossing. Secondario: propagare nel seme i finding stabili (direzione invariante, offset primi 32-sigma).", "reason": "L'affinatore ha identificato questo come la possibilità più concreta. Estende il risultato corrente verso falsificazione cross-framework invece di accumulare dettaglio locale."}}, "notes": "Il ciclo precedente (piano 60) è tra i migliori recenti: esperimento ben disegnato, shuffle audit implicito nella parametrizzazione alpha, risultato quantitativo con interpretazione strutturale chiara. Il confronto Brody è il filtro giusto: se il punto critico coincide, la transizione è universale (cristallizzabile); se no, la parametrizzazione alpha è contingente e il finding si ridimensiona a osservazione locale."}
{"ts": "2026-05-03T03:41:17.534324+00:00", "cycle_ref": "20260502_0330", "executed": false, "decision": "NEXT_CYCLE", "confidence": "high", "reasoning": "Il ciclo ha prodotto un risultato forte e non-tautologico: auto-declassamento della transizione di fase a constraint metodologico + estrazione della direzione come discriminante reale. La coincidenza Prime-Periodic a 0.2° (carattere identico, magnitudine 10x diversa) converge con F2 e Markov-1 sullo stesso nodo strutturale — il confinamento Z/6Z. Manca il passo sintetico: decomporre il segnale prime in componente confinamento (Z/6Z, shared con Periodic) + componente residua (il 7° di separazione dalla classe repulsione). Questo è il prossimo esperimento naturale, non serve redesign.", "action": {"type": "modify_seme", "detail": {"field": "direzione", "new_value": "Decomposizione sintetica: costruire sequenza ibrida (confinamento Z/6Z puro + rumore calibrato) e verificare se la componente residua prime-vs-periodic (i 0.2° di coincidenza vs i 7° dalla classe repulsione) porta informazione oltre il crivello. Il discriminante è: la direzione prime è ESATTAMENTE Z/6Z, o c'è un residuo misurabile che codifica struttura ulteriore?", "reason": "Tre risultati indipendenti (F2, Markov-1, direzione dipolare) convergono sullo stesso nodo. Il passo che manca è la decomposizione per separare confinamento da residuo. Se il residuo è zero → prime direction = puro Z/6Z (chiude F2). Se non-zero → c'è struttura oltre il crivello (apre)."}}, "notes": "La convergenza di tre linee indipendenti sullo stesso nodo (Z/6Z) è il segnale più forte emerso dal lab in settimane. Non è ancora cristallizzabile perché manca la decomposizione, ma se il prossimo ciclo la completa con esito pulito, il ciclo dopo sarà CRYSTALLIZE. Attenzione: la sequenza ibrida deve essere costruita con cura — il periodic 2,4 è deterministico, i primi hanno varianza. La calibrazione del rumore nella sequenza sintetica è il punto tecnico critico."}
{"ts": "2026-05-03T07:48:23.455983+00:00", "cycle_ref": "20260503_0330", "executed": false, "decision": "NEXT_CYCLE", "confidence": "high", "reasoning": "Ciclo pulito, consecutio seguita, risultato strutturale non-tautologico: la memoria dei primi si decompone in esattamente due strati indipendenti (pair→SR/L1, triple→SR2/L2/cond_entropy), e Markov-3 è invisibile in tutti e 10 gli osservabili scalari testati. Il metodo (surrogati Markov-k stratificati) è robusto contro tautologie. La consecutio naturale — proiettare (SR, L1, SR2) per verificare se il bicono emerge dalla decomposizione a due strati — è ben definita e falsificabile.", "action": {"type": "none", "detail": {}}, "notes": "Tre cicli consecutivi solidi (crossover → auto-declassamento → due strati). La direzione è viva. La prossima mossa (proiezione 3D) potrebbe connettere il lab numerico alla geometria del bicono — se il risultato è positivo, potrebbe meritare CRYSTALLIZE al giro dopo. Se la proiezione non mostra struttura riconoscibile, il ramo Markov-memory è esaurito e servirà REDESIGN verso altro dominio."}
{"ts": "2026-05-04T09:09:09.478519+00:00", "cycle_ref": "20260504_0901", "executed": false, "decision": "NEXT_CYCLE", "confidence": "high", "reasoning": "Four cycles of convergent refinement on the boundary picture: phase transition → near-second-order → two visible layers → single coupled transition. Each cycle has produced a genuine constraint (not tautology), and the affinatore identified zero-crossing interleaving as the highest-leverage next step — it's concrete, testable, and could discriminate prime-specific geometry from generic repulsion. The direction is not exhausted yet.", "action": {"type": "none", "detail": {}}, "notes": "The zero-crossing order (L1 < triple_var < SR2 < SR, interleaved across layers) is the most promising lead: it's an observable ordering that should differ between primes and GUE if the coupling has sequence-specific structure. After one more cycle on this, consider CRYSTALLIZE for the full crossover/boundary picture — the convergence from 4 consecutive clean cycles is approaching maturity for a condensato entry or paper section."}
{"ts": "2026-05-04T12:03:13.893909+00:00", "cycle_ref": "20260504_1138", "executed": false, "decision": "NEXT_CYCLE", "confidence": "medium", "reasoning": "DIPOLAR_ORDERING resolved and piano advanced 60→61, indicating forward motion. The missing agent report (run completed but no scientific conclusion) is a yellow flag but not yet a pattern — one empty output after 3 productive cycles on boundary refinement is normal variance. 8 tensions remain in the current direction, so the frame is not exhausted. However, this is the 5th consecutive NEXT_CYCLE on boundary-adjacent themes; if the next run also produces no scientific output, REDESIGN toward cross-domain verification (b/f category) should be triggered.", "action": {"type": "none", "detail": {}}, "notes": "Monitor: if piano 61 also yields no agent report or resolves zero tensions, escalate to REDESIGN toward a fresh cross-domain experiment (e.g., testing the 8-GUE/5-Poisson boundary classification against a shuffled null baseline across all 13 domains simultaneously, which would be category (b) evidence). The boundary picture has accumulated enough local detail — the next high-value move is cross-domain falsification of the boundary itself."}
{"ts": "2026-05-04T12:30:22.947364+00:00", "cycle_ref": "20260504_1219", "executed": false, "decision": "NEXT_CYCLE", "confidence": "high", "reasoning": "Il ciclo 61 ha prodotto un audit metodologico genuino (categoria a/b): ha ristretto il perimetro dei claim two-layer a {SR,L1,L2,SR2} e ha identificato cond_entropy e num_var_10 come non-affidabili. Questo è lavoro strutturale, non accumulo numerico. La direzione seme (confine GUE/Poisson) e le indicazioni dell'affinatore (coupling test, surrogati esatti, gate multi-seed) convergono su un passo naturale: testare se i due layer verificati sono accoppiati o indipendenti — questo è il claim più forte rimasto non-auditato e tocca il confine che il seme chiede di esplorare.", "action": {"type": "none", "detail": {}}, "notes": "Quattro NEXT_CYCLE consecutivi, ma la traiettoria non è stagnante — ogni ciclo ha ristretto il perimetro (phase transition → near-second-order → two visible layers → recovery audit). Il prossimo passo ad alto rendimento è il coupling test tra Layer 1 e Layer 2: se sono indipendenti il two-layer si riduce a due claim separati; se sono accoppiati, emerge struttura genuina beyond-Markov. Evitare di investire subito in surrogati Markov esatti (infrastruttura pesante) — prima verificare se il coupling test col setup attuale produce segnale o null."}
{"ts": "2026-05-05T03:34:54.115859+00:00", "cycle_ref": "20260505_0330", "executed": false, "decision": "NEXT_CYCLE", "confidence": "high", "reasoning": "Piano 62 ha prodotto un vincolo metodologico genuino (categoria a): le 5 osservabili sono collineari sotto partial-shuffle uniforme, quindi non contano come evidenze indipendenti. Il seme è già avanzato a 63 con direzione coerente (perturbazioni selettive ortogonali). La traiettoria degli ultimi 4 cicli mostra compressione progressiva sana: da accumulazione a restrizione del perimetro. Nessuna dipendenza dall'operatore.", "action": {"type": "none", "detail": {}}, "notes": "La consecutio naturale è cercare perturbazioni che rompano la collinearità — block-shuffle per layer, swap locale vs globale, perturbazioni che agiscano su scale diverse. Se il prossimo ciclo trova che i due Markov layer rispondono diversamente a perturbazioni selettive, quello sarebbe evidenza di rango >1 genuino."}
{"ts": "2026-05-06T03:38:24.835757+00:00", "cycle_ref": "20260506_0330", "executed": false, "decision": "NEXT_CYCLE", "confidence": "high", "reasoning": "Piano 65 ha prodotto un risultato categoria (a): ha falsificato il proprio risultato precedente (rank audit 05-05) mostrando che il singolo coordinate era artefatto del tipo di perturbazione. Il finding — dimensionalità perturbativa come discriminatore di dominio (GUE=1.89 vs primes=1.26) — è genuino ma necessita replicazione con GUE più grande (N=253 è il caveat dichiarato). La direzione 'confine' è ancora fertile: il prossimo passo naturale è confermare la dimensionalità perturbativa come invariante cross-dominio.", "action": {"type": "none", "detail": {}}, "notes": "Suggerimento per il prossimo ciclo: replicare con GUE N>=1000 (matrici 45x45+) e aggiungere almeno 2 domini intermedi (logistica caotica, Stern-Brocot) per verificare se la dimensionalità perturbativa scala in modo ordinato tra domini. Se confermato, è candidato a F7 nel condensato."}
{"ts": "2026-05-06T06:33:26.706335+00:00", "cycle_ref": "20260506_0625", "executed": false, "decision": "NEXT_CYCLE", "confidence": "high", "reasoning": "Piano 65 ha prodotto falsificazione genuina (categoria a): il secondo asse GUE è artefatto di sample size, non invariante stabile. Il ciclo ha anche scoperto una collisione di nomi osservabili (SR/SR2/triple_var significano funzioni diverse in script diversi) — problema META reale che va risolto. La direzione 'esplorare il confine' ha ancora potenziale: il prossimo passo naturale è la curva rank-vs-N che chiude il loop, e la collisione nomi va sanata prima di ulteriori audit cross-script.", "action": {"type": "none", "detail": {}}, "notes": "Tre cicli consecutivi NEXT_CYCLE sono giustificati perché ciascuno ha prodotto falsificazione o vincolo strutturale (non accumulo numerico locale). Se il prossimo ciclo produce solo conferma incrementale senza nuova discriminazione, valutare REDESIGN verso pair cross-dominio (f) o connessione con i ponti evoluti del sito. La collisione nomi osservabili è un debito META che il prossimo agente dovrebbe risolvere con un dizionario canonico delle osservabili prima di lanciare nuovi esperimenti."}
{"ts": "2026-05-06T19:47:12.588345+00:00", "cycle_ref": "20260506_1941", "executed": true, "decision": "REDESIGN", "confidence": "high", "reasoning": "Three consecutive cycles on perturbation dimensionality (pianos 64-66) have converged to a single clear constraint: rank needs denominator gating, and after gating both GUE and primes show ~1 perturbation coordinate. The thread has produced genuine falsification (category a) and a solid methodological gate, but continuing to refine size curves or observable stability on the same axis is now category (c) — local numerical accumulation with diminishing returns. The 10 open tensions in the seme offer fresher directions; the perturbation constraint is ready to crystallize and the lab should pivot to cross-domain verification or a tension not yet explored.", "action": {"type": "modify_seme", "detail": {"field": "direzione", "new_value": "Il vincolo PERTURBATION_DENOMINATOR_GATE è stabilito e replicato — cristallizzare come regola metodologica e muovere verso tensione cross-dominio non ancora esplorata (candidati: dipolar crossover scaling, markov layer coupling verification, o observable collinearity breaking). La prossima corsa NON deve restare su perturbation rank.", "reason": "3 cicli sullo stesso asse hanno prodotto il risultato (denominatore gate). Restare è rendimenti decrescenti (rischio tautologia accumulata). Ridirigere verso (b) cross-domain o (f) connessione inter-dominio."}}, "notes": "Il finding di piano 66 è maturo per crystallize_note nel condensato come vincolo metodologico (accanto a C2), ma la priorità ora è rompere l'inerzia tematica. Suggerisco anche che l'infrastruttura trajectory_apply/autopsy venga allineata (segnalato dall'affinatore) — ma questo è un fix di sistema, non una direzione scientifica.", "_executed_at": "2026-05-06T19:55:48.007990+00:00", "_executed_by": "lab_trajectory_apply"}

exec
/bin/bash -lc 'ls -l tools/data/observable_collinearity_breaking_20260506_*.json' in /opt/MM_D-ND
 succeeded in 0ms:
-rw-r--r-- 1 root root 73472 May  6 19:57 tools/data/observable_collinearity_breaking_20260506_1955.json
-rw-r--r-- 1 root root 60403 May  6 19:57 tools/data/observable_collinearity_breaking_20260506_1956.json
-rw-r--r-- 1 root root 60402 May  6 19:57 tools/data/observable_collinearity_breaking_20260506_1957.json

diff --git a/tools/exp_observable_rank_audit.py b/tools/exp_observable_rank_audit.py
index f2acafc5960c8a35964e0ab521cf70569eefe27f..51e6ec52edfffda55d1d22862a102f896a08fdd9
--- a/tools/exp_observable_rank_audit.py
+++ b/tools/exp_observable_rank_audit.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 """
-exp_observable_rank_audit.py - META audit for observable redundancy.
+exp_observable_rank_audit.py - META audit for observable collinearity.
 
 Recent runs found many Markov/crossover observables that react coherently under
 partial shuffle. This script asks whether those observables carry independent
@@ -22,47 +22,19 @@
 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)
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
 
 
-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,
-}
+OBSERVABLES = OBSERVABLES_CANONICAL
+OBS_NAMES = list(OBSERVABLES)
 
 
 def measure(gaps):
-    return {name: fn(gaps) for name, fn in OBSERVABLES.items()}
+    return compute_canonical(gaps)
 
 
 def full_shuffle_baseline(gaps, n_trials, rng):
@@ -106,7 +78,7 @@
 
 
 def pca_summary(rows):
-    names = list(OBSERVABLES)
+    names = OBS_NAMES
     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)
 
@@ -142,15 +114,20 @@
     rows = retention_curves(gaps, alphas, n_trials, originals, baseline, rng)
 
     z = {}
-    for obs_name in OBSERVABLES:
+    stable_observables = []
+    for obs_name in OBS_NAMES:
         std = baseline[obs_name]["std"]
         z[obs_name] = float((originals[obs_name] - baseline[obs_name]["mean"]) / std) if std > 1e-12 else 0.0
+        if abs(z[obs_name]) >= 2.0:
+            stable_observables.append(obs_name)
 
     return {
         "n_gaps": int(len(gaps)),
         "originals": originals,
         "full_shuffle_baseline": baseline,
         "original_vs_shuffle_z": z,
+        "stable_observables_abs_z_ge_2": stable_observables,
+        "weak_observable_count": int(len(OBS_NAMES) - len(stable_observables)),
         "retention_curves": rows,
         "pca": pca_summary(rows),
     }
@@ -164,21 +141,25 @@
     gue = gue[:n_gaps]
 
     poisson = rng.exponential(1.0, size=n_gaps).astype(float)
+    prime_shuffle = rng.permutation(prime_gaps).astype(float)
     return {
         "primes": prime_gaps,
+        "prime_shuffle": prime_shuffle,
         "gue": gue,
         "poisson": poisson,
     }
 
 
-def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505):
+def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505, out="tools/data/observable_rank_audit.json"):
     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?",
+        "question": "When do canonical observable retention curves break collinearity across domains?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
         "params": {
             "n_gaps": int(n_gaps),
             "n_alpha": int(n_alpha),
@@ -189,19 +170,23 @@
         "sequences": {},
     }
 
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
     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)
+    print(f"{'sequence':<14} {'pc1':>8} {'eff_rank':>9} {'mean|corr|':>11} {'weak':>5}  z(SR,SR2,L1,L2,triple_var)")
+    print("-" * 108)
     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)
+        z_text = ", ".join(f"{obs}={z[obs]:+.1f}" for obs in OBS_NAMES)
         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}")
+        print(
+            f"{seq_name:<14} {pc1:>8.3f} {pca['effective_rank']:>9.3f} "
+            f"{pca['mean_abs_pairwise_corr']:>11.3f} {result['weak_observable_count']:>5}  {z_text}"
+        )
 
-    out_path = Path("tools/data/observable_rank_audit.json")
+    out_path = Path(out)
     with out_path.open("w") as f:
         json.dump(output, f, indent=2)
     print(f"\nSaved to {out_path}")
@@ -215,6 +200,7 @@
     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)
+    parser.add_argument("--out", default="tools/data/observable_rank_audit.json")
     args = parser.parse_args()
     run(
         n_gaps=args.n_gaps,
@@ -222,6 +208,7 @@
         n_trials=args.n_trials,
         n_baseline=args.n_baseline,
         seed=args.seed,
+        out=args.out,
     )
 
 

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

diff --git a/tools/exp_observable_rank_audit.py b/tools/exp_observable_rank_audit.py
index f2acafc5960c8a35964e0ab521cf70569eefe27f..51e6ec52edfffda55d1d22862a102f896a08fdd9
--- a/tools/exp_observable_rank_audit.py
+++ b/tools/exp_observable_rank_audit.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 """
-exp_observable_rank_audit.py - META audit for observable redundancy.
+exp_observable_rank_audit.py - META audit for observable collinearity.
 
 Recent runs found many Markov/crossover observables that react coherently under
 partial shuffle. This script asks whether those observables carry independent
@@ -22,47 +22,19 @@
 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)
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
 
 
-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,
-}
+OBSERVABLES = OBSERVABLES_CANONICAL
+OBS_NAMES = list(OBSERVABLES)
 
 
 def measure(gaps):
-    return {name: fn(gaps) for name, fn in OBSERVABLES.items()}
+    return compute_canonical(gaps)
 
 
 def full_shuffle_baseline(gaps, n_trials, rng):
@@ -106,7 +78,7 @@
 
 
 def pca_summary(rows):
-    names = list(OBSERVABLES)
+    names = OBS_NAMES
     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)
 
@@ -142,15 +114,20 @@
     rows = retention_curves(gaps, alphas, n_trials, originals, baseline, rng)
 
     z = {}
-    for obs_name in OBSERVABLES:
+    stable_observables = []
+    for obs_name in OBS_NAMES:
         std = baseline[obs_name]["std"]
         z[obs_name] = float((originals[obs_name] - baseline[obs_name]["mean"]) / std) if std > 1e-12 else 0.0
+        if abs(z[obs_name]) >= 2.0:
+            stable_observables.append(obs_name)
 
     return {
         "n_gaps": int(len(gaps)),
         "originals": originals,
         "full_shuffle_baseline": baseline,
         "original_vs_shuffle_z": z,
+        "stable_observables_abs_z_ge_2": stable_observables,
+        "weak_observable_count": int(len(OBS_NAMES) - len(stable_observables)),
         "retention_curves": rows,
         "pca": pca_summary(rows),
     }
@@ -164,21 +141,25 @@
     gue = gue[:n_gaps]
 
     poisson = rng.exponential(1.0, size=n_gaps).astype(float)
+    prime_shuffle = rng.permutation(prime_gaps).astype(float)
     return {
         "primes": prime_gaps,
+        "prime_shuffle": prime_shuffle,
         "gue": gue,
         "poisson": poisson,
     }
 
 
-def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505):
+def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505, out="tools/data/observable_rank_audit.json"):
     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?",
+        "question": "When do canonical observable retention curves break collinearity across domains?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
         "params": {
             "n_gaps": int(n_gaps),
             "n_alpha": int(n_alpha),
@@ -189,19 +170,23 @@
         "sequences": {},
     }
 
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
     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)
+    print(f"{'sequence':<14} {'pc1':>8} {'eff_rank':>9} {'mean|corr|':>11} {'weak':>5}  z(SR,SR2,L1,L2,triple_var)")
+    print("-" * 108)
     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)
+        z_text = ", ".join(f"{obs}={z[obs]:+.1f}" for obs in OBS_NAMES)
         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}")
+        print(
+            f"{seq_name:<14} {pc1:>8.3f} {pca['effective_rank']:>9.3f} "
+            f"{pca['mean_abs_pairwise_corr']:>11.3f} {result['weak_observable_count']:>5}  {z_text}"
+        )
 
-    out_path = Path("tools/data/observable_rank_audit.json")
+    out_path = Path(out)
     with out_path.open("w") as f:
         json.dump(output, f, indent=2)
     print(f"\nSaved to {out_path}")
@@ -215,6 +200,7 @@
     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)
+    parser.add_argument("--out", default="tools/data/observable_rank_audit.json")
     args = parser.parse_args()
     run(
         n_gaps=args.n_gaps,
@@ -222,6 +208,7 @@
         n_trials=args.n_trials,
         n_baseline=args.n_baseline,
         seed=args.seed,
+        out=args.out,
     )
 
 

codex
Scrivo il report come vincolo META cross-dominio, non come “nuova legge”: il contenuto è operativo, cioè le curve degli osservabili strutturati sono quasi collineari solo quando i denominatori esistono; nei controlli il rank alto è non interpretabile.
apply patch
patch: completed
/opt/MM_D-ND/tools/data/reports/agent_20260506_1955.md
diff --git a/tools/data/reports/agent_20260506_1955.md b/tools/data/reports/agent_20260506_1955.md
new file mode 100644
index 0000000000000000000000000000000000000000..b4a665b4552a84cb33e6ea8db23d82d60b8ddde3
--- /dev/null
+++ b/tools/data/reports/agent_20260506_1955.md
@@ -0,0 +1,86 @@
+# Agent Report — Observable Collinearity Breaks Only Where Denominators Are Weak
+**Date**: 2026-05-06 19:55  
+**Piano**: 67  
+**Tension explored**: META + TRAJECTORY_APPLY_20260506_1941  
+observables_registry: 1.0.0-2026-05-06  
+observables_used: [SR, SR2, L1, L2, triple_var]
+
+## Claim Under Test
+Cycle 19:41 closed `PERTURBATION_DENOMINATOR_GATE` and the trajectory explicitly said:
+
+> La prossima corsa NON deve restare su perturbation rank.
+
+This run therefore does not test perturbation dimensionality. It asks a cross-domain META question:
+
+> When the five canonical observables are measured under uniform partial shuffle, does observable collinearity break in structured domains, or only in controls where original-vs-shuffle denominators are weak?
+
+## Experiment
+Tool updated: `tools/exp_observable_rank_audit.py`
+
+Method correction before execution:
+- removed local observable redefinitions;
+- imported canonical definitions from `tools/observables_registry.py`;
+- replaced the old local `triple_var` normalized convention with canonical raw `triple_var`;
+- added `prime_shuffle` as a control domain;
+- reported weak observable count using the fixed gate `abs(original - shuffle_mean) / shuffle_std < 2`.
+
+Atomic perimeter:
+- domains: first prime gaps, prime-shuffle control, independent GUE spacings, iid Poisson spacings;
+- main run: 12,000 gaps, 19 alpha values, 18 partial-shuffle trials per alpha, 48 full-shuffle baselines;
+- two seed checks: 8,000 gaps, 15 alpha values, 12 trials per alpha, 36 baselines;
+- measured object: PCA of the 5-observable retention curves across alpha, not perturbation profiles.
+
+## Results
+
+### Main Run
+
+| Domain | PC1 energy | effective rank | mean abs corr | weak obs / 5 | z summary |
+|---|---:|---:|---:|---:|---|
+| primes | 0.978 | 1.128 | 0.975 | 1 | SR=-12.1, SR2=-2.5, L1=-8.9, L2=-1.9, triple_var=-8.7 |
+| prime_shuffle | 0.593 | 2.475 | 0.606 | 5 | all abs(z) <= 1.1 |
+| GUE | 0.990 | 1.060 | 0.989 | 0 | SR=-2.9, SR2=+14.5, L1=+13.2, L2=+31.7, triple_var=+23.8 |
+| Poisson | 0.625 | 2.368 | 0.609 | 5 | all abs(z) <= 1.9 |
+
+### Three-Run Summary
+
+| Domain | PC1 mean | rank mean | mean abs corr | weak obs mean |
+|---|---:|---:|---:|---:|
+| primes | 0.939 | 1.296 | 0.924 | 1.33 |
+| prime_shuffle | 0.765 | 1.904 | 0.551 | 4.67 |
+| GUE | 0.980 | 1.106 | 0.977 | 0.33 |
+| Poisson | 0.714 | 2.196 | 0.572 | 5.00 |
+
+## Findings
+
+1. **Structured domains compress the five canonical retention curves to one dominant coordinate in this perimeter.** Primes and GUE both have PC1 > 0.93 on average and effective rank close to 1. This does not say the domains are the same; it says uniform partial shuffle moves the canonical observables along one dominant retention mode.
+
+2. **Observed collinearity breaking is concentrated in weak-denominator controls.** Poisson has the highest apparent rank among the three-run means (`2.196`), but all five observables are weak against full shuffle in every run. Prime-shuffle behaves similarly: rank is unstable and 4-5 of 5 observables are weak. This mirrors the denominator lesson from perturbation rank without repeating the perturbation-rank experiment.
+
+3. **The 05-05 observable-rank result survives only after narrowing its language.** The valid statement is not "five probes are always one thing." The scoped statement is: under uniform partial shuffle and canonical observables, primes and GUE show a dominant one-coordinate retention response; controls can show larger PCA rank, but that rank is not structural when the original-vs-shuffle denominators are absent.
+
+4. **GUE is the cleanest conditioning check.** In the main run, all five GUE observables pass the denominator gate and still give rank `1.060`. This makes GUE the best positive control for "low rank despite valid denominators." Poisson is the negative control for "high rank without valid denominators."
+
+## Verdict
+**CONSTRAINT on META**: observable collinearity claims must be reported with:
+
+> observables_registry version + canonical observable list + original-vs-shuffle z per observable + control domains.
+
+In this perimeter, high observable-rank is not the sign of richer structure when it appears in Poisson or prime-shuffle controls; it is a warning that retention ratios are being formed on weak denominators. The stable cross-domain result is narrower:
+
+> uniform partial shuffle exposes one dominant retention coordinate in conditioned structured domains (primes, GUE), while apparent multi-coordinate behavior in Poisson/shuffle controls is denominator-weak.
+
+## Consecutio
+What opens now: the next non-redundant test is not another PCA audit. It is **selective operator coupling**: use perturbations that act separately on pair-scale and triple-scale structure, but report only observables whose denominator gate survives. If selective operators rotate primes while GUE stays collinear, the lab gets a real cross-domain discriminator; if both stay collinear, the current observable suite is overcomplete for this question.
+
+## Auto-audit: 5 lenti
+- **L1 hard constraint vs bias**: no zero/always claim. "Weak" means `abs(z) < 2` in this declared gate.
+- **L2 quantity vs ratio**: PCA rank and retention ratios are interpreted only after raw z denominators.
+- **L3 no silent patching**: the older 05-05 collinearity claim is explicitly narrowed; it is not silently rebranded as universal.
+- **L4 edge cases**: `prime_shuffle` seed 1956 produced low rank with 4 weak observables; this is reported as unstable control behavior, not ignored.
+- **L5 re-discovery**: PCA rank instability under noisy normalization is standard statistical hygiene. This is a lab method constraint, not a new RMT theorem.
+
+## Files
+- Script: `tools/exp_observable_rank_audit.py`
+- Main data: `tools/data/observable_collinearity_breaking_20260506_1955.json`
+- Seed checks: `tools/data/observable_collinearity_breaking_20260506_1956.json`, `tools/data/observable_collinearity_breaking_20260506_1957.json`
+- Report: `tools/data/reports/agent_20260506_1955.md`
diff --git a/tools/exp_observable_rank_audit.py b/tools/exp_observable_rank_audit.py
index f2acafc5960c8a35964e0ab521cf70569eefe27f..51e6ec52edfffda55d1d22862a102f896a08fdd9
--- a/tools/exp_observable_rank_audit.py
+++ b/tools/exp_observable_rank_audit.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 """
-exp_observable_rank_audit.py - META audit for observable redundancy.
+exp_observable_rank_audit.py - META audit for observable collinearity.
 
 Recent runs found many Markov/crossover observables that react coherently under
 partial shuffle. This script asks whether those observables carry independent
@@ -22,47 +22,19 @@
 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)
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
 
 
-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,
-}
+OBSERVABLES = OBSERVABLES_CANONICAL
+OBS_NAMES = list(OBSERVABLES)
 
 
 def measure(gaps):
-    return {name: fn(gaps) for name, fn in OBSERVABLES.items()}
+    return compute_canonical(gaps)
 
 
 def full_shuffle_baseline(gaps, n_trials, rng):
@@ -106,7 +78,7 @@
 
 
 def pca_summary(rows):
-    names = list(OBSERVABLES)
+    names = OBS_NAMES
     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)
 
@@ -142,15 +114,20 @@
     rows = retention_curves(gaps, alphas, n_trials, originals, baseline, rng)
 
     z = {}
-    for obs_name in OBSERVABLES:
+    stable_observables = []
+    for obs_name in OBS_NAMES:
         std = baseline[obs_name]["std"]
         z[obs_name] = float((originals[obs_name] - baseline[obs_name]["mean"]) / std) if std > 1e-12 else 0.0
+        if abs(z[obs_name]) >= 2.0:
+            stable_observables.append(obs_name)
 
     return {
         "n_gaps": int(len(gaps)),
         "originals": originals,
         "full_shuffle_baseline": baseline,
         "original_vs_shuffle_z": z,
+        "stable_observables_abs_z_ge_2": stable_observables,
+        "weak_observable_count": int(len(OBS_NAMES) - len(stable_observables)),
         "retention_curves": rows,
         "pca": pca_summary(rows),
     }
@@ -164,21 +141,25 @@
     gue = gue[:n_gaps]
 
     poisson = rng.exponential(1.0, size=n_gaps).astype(float)
+    prime_shuffle = rng.permutation(prime_gaps).astype(float)
     return {
         "primes": prime_gaps,
+        "prime_shuffle": prime_shuffle,
         "gue": gue,
         "poisson": poisson,
     }
 
 
-def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505):
+def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505, out="tools/data/observable_rank_audit.json"):
     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?",
+        "question": "When do canonical observable retention curves break collinearity across domains?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
         "params": {
             "n_gaps": int(n_gaps),
             "n_alpha": int(n_alpha),
@@ -189,19 +170,23 @@
         "sequences": {},
     }
 
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
     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)
+    print(f"{'sequence':<14} {'pc1':>8} {'eff_rank':>9} {'mean|corr|':>11} {'weak':>5}  z(SR,SR2,L1,L2,triple_var)")
+    print("-" * 108)
     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)
+        z_text = ", ".join(f"{obs}={z[obs]:+.1f}" for obs in OBS_NAMES)
         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}")
+        print(
+            f"{seq_name:<14} {pc1:>8.3f} {pca['effective_rank']:>9.3f} "
+            f"{pca['mean_abs_pairwise_corr']:>11.3f} {result['weak_observable_count']:>5}  {z_text}"
+        )
 
-    out_path = Path("tools/data/observable_rank_audit.json")
+    out_path = Path(out)
     with out_path.open("w") as f:
         json.dump(output, f, indent=2)
     print(f"\nSaved to {out_path}")
@@ -215,6 +200,7 @@
     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)
+    parser.add_argument("--out", default="tools/data/observable_rank_audit.json")
     args = parser.parse_args()
     run(
         n_gaps=args.n_gaps,
@@ -222,6 +208,7 @@
         n_trials=args.n_trials,
         n_baseline=args.n_baseline,
         seed=args.seed,
+        out=args.out,
     )
 
 

diff --git a/tools/data/reports/agent_20260506_1955.md b/tools/data/reports/agent_20260506_1955.md
new file mode 100644
index 0000000000000000000000000000000000000000..b4a665b4552a84cb33e6ea8db23d82d60b8ddde3
--- /dev/null
+++ b/tools/data/reports/agent_20260506_1955.md
@@ -0,0 +1,86 @@
+# Agent Report — Observable Collinearity Breaks Only Where Denominators Are Weak
+**Date**: 2026-05-06 19:55  
+**Piano**: 67  
+**Tension explored**: META + TRAJECTORY_APPLY_20260506_1941  
+observables_registry: 1.0.0-2026-05-06  
+observables_used: [SR, SR2, L1, L2, triple_var]
+
+## Claim Under Test
+Cycle 19:41 closed `PERTURBATION_DENOMINATOR_GATE` and the trajectory explicitly said:
+
+> La prossima corsa NON deve restare su perturbation rank.
+
+This run therefore does not test perturbation dimensionality. It asks a cross-domain META question:
+
+> When the five canonical observables are measured under uniform partial shuffle, does observable collinearity break in structured domains, or only in controls where original-vs-shuffle denominators are weak?
+
+## Experiment
+Tool updated: `tools/exp_observable_rank_audit.py`
+
+Method correction before execution:
+- removed local observable redefinitions;
+- imported canonical definitions from `tools/observables_registry.py`;
+- replaced the old local `triple_var` normalized convention with canonical raw `triple_var`;
+- added `prime_shuffle` as a control domain;
+- reported weak observable count using the fixed gate `abs(original - shuffle_mean) / shuffle_std < 2`.
+
+Atomic perimeter:
+- domains: first prime gaps, prime-shuffle control, independent GUE spacings, iid Poisson spacings;
+- main run: 12,000 gaps, 19 alpha values, 18 partial-shuffle trials per alpha, 48 full-shuffle baselines;
+- two seed checks: 8,000 gaps, 15 alpha values, 12 trials per alpha, 36 baselines;
+- measured object: PCA of the 5-observable retention curves across alpha, not perturbation profiles.
+
+## Results
+
+### Main Run
+
+| Domain | PC1 energy | effective rank | mean abs corr | weak obs / 5 | z summary |
+|---|---:|---:|---:|---:|---|
+| primes | 0.978 | 1.128 | 0.975 | 1 | SR=-12.1, SR2=-2.5, L1=-8.9, L2=-1.9, triple_var=-8.7 |
+| prime_shuffle | 0.593 | 2.475 | 0.606 | 5 | all abs(z) <= 1.1 |
+| GUE | 0.990 | 1.060 | 0.989 | 0 | SR=-2.9, SR2=+14.5, L1=+13.2, L2=+31.7, triple_var=+23.8 |
+| Poisson | 0.625 | 2.368 | 0.609 | 5 | all abs(z) <= 1.9 |
+
+### Three-Run Summary
+
+| Domain | PC1 mean | rank mean | mean abs corr | weak obs mean |
+|---|---:|---:|---:|---:|
+| primes | 0.939 | 1.296 | 0.924 | 1.33 |
+| prime_shuffle | 0.765 | 1.904 | 0.551 | 4.67 |
+| GUE | 0.980 | 1.106 | 0.977 | 0.33 |
+| Poisson | 0.714 | 2.196 | 0.572 | 5.00 |
+
+## Findings
+
+1. **Structured domains compress the five canonical retention curves to one dominant coordinate in this perimeter.** Primes and GUE both have PC1 > 0.93 on average and effective rank close to 1. This does not say the domains are the same; it says uniform partial shuffle moves the canonical observables along one dominant retention mode.
+
+2. **Observed collinearity breaking is concentrated in weak-denominator controls.** Poisson has the highest apparent rank among the three-run means (`2.196`), but all five observables are weak against full shuffle in every run. Prime-shuffle behaves similarly: rank is unstable and 4-5 of 5 observables are weak. This mirrors the denominator lesson from perturbation rank without repeating the perturbation-rank experiment.
+
+3. **The 05-05 observable-rank result survives only after narrowing its language.** The valid statement is not "five probes are always one thing." The scoped statement is: under uniform partial shuffle and canonical observables, primes and GUE show a dominant one-coordinate retention response; controls can show larger PCA rank, but that rank is not structural when the original-vs-shuffle denominators are absent.
+
+4. **GUE is the cleanest conditioning check.** In the main run, all five GUE observables pass the denominator gate and still give rank `1.060`. This makes GUE the best positive control for "low rank despite valid denominators." Poisson is the negative control for "high rank without valid denominators."
+
+## Verdict
+**CONSTRAINT on META**: observable collinearity claims must be reported with:
+
+> observables_registry version + canonical observable list + original-vs-shuffle z per observable + control domains.
+
+In this perimeter, high observable-rank is not the sign of richer structure when it appears in Poisson or prime-shuffle controls; it is a warning that retention ratios are being formed on weak denominators. The stable cross-domain result is narrower:
+
+> uniform partial shuffle exposes one dominant retention coordinate in conditioned structured domains (primes, GUE), while apparent multi-coordinate behavior in Poisson/shuffle controls is denominator-weak.
+
+## Consecutio
+What opens now: the next non-redundant test is not another PCA audit. It is **selective operator coupling**: use perturbations that act separately on pair-scale and triple-scale structure, but report only observables whose denominator gate survives. If selective operators rotate primes while GUE stays collinear, the lab gets a real cross-domain discriminator; if both stay collinear, the current observable suite is overcomplete for this question.
+
+## Auto-audit: 5 lenti
+- **L1 hard constraint vs bias**: no zero/always claim. "Weak" means `abs(z) < 2` in this declared gate.
+- **L2 quantity vs ratio**: PCA rank and retention ratios are interpreted only after raw z denominators.
+- **L3 no silent patching**: the older 05-05 collinearity claim is explicitly narrowed; it is not silently rebranded as universal.
+- **L4 edge cases**: `prime_shuffle` seed 1956 produced low rank with 4 weak observables; this is reported as unstable control behavior, not ignored.
+- **L5 re-discovery**: PCA rank instability under noisy normalization is standard statistical hygiene. This is a lab method constraint, not a new RMT theorem.
+
+## Files
+- Script: `tools/exp_observable_rank_audit.py`
+- Main data: `tools/data/observable_collinearity_breaking_20260506_1955.json`
+- Seed checks: `tools/data/observable_collinearity_breaking_20260506_1956.json`, `tools/data/observable_collinearity_breaking_20260506_1957.json`
+- Report: `tools/data/reports/agent_20260506_1955.md`
diff --git a/tools/exp_observable_rank_audit.py b/tools/exp_observable_rank_audit.py
index f2acafc5960c8a35964e0ab521cf70569eefe27f..51e6ec52edfffda55d1d22862a102f896a08fdd9
--- a/tools/exp_observable_rank_audit.py
+++ b/tools/exp_observable_rank_audit.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 """
-exp_observable_rank_audit.py - META audit for observable redundancy.
+exp_observable_rank_audit.py - META audit for observable collinearity.
 
 Recent runs found many Markov/crossover observables that react coherently under
 partial shuffle. This script asks whether those observables carry independent
@@ -22,47 +22,19 @@
 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)
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
 
 
-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,
-}
+OBSERVABLES = OBSERVABLES_CANONICAL
+OBS_NAMES = list(OBSERVABLES)
 
 
 def measure(gaps):
-    return {name: fn(gaps) for name, fn in OBSERVABLES.items()}
+    return compute_canonical(gaps)
 
 
 def full_shuffle_baseline(gaps, n_trials, rng):
@@ -106,7 +78,7 @@
 
 
 def pca_summary(rows):
-    names = list(OBSERVABLES)
+    names = OBS_NAMES
     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)
 
@@ -142,15 +114,20 @@
     rows = retention_curves(gaps, alphas, n_trials, originals, baseline, rng)
 
     z = {}
-    for obs_name in OBSERVABLES:
+    stable_observables = []
+    for obs_name in OBS_NAMES:
         std = baseline[obs_name]["std"]
         z[obs_name] = float((originals[obs_name] - baseline[obs_name]["mean"]) / std) if std > 1e-12 else 0.0
+        if abs(z[obs_name]) >= 2.0:
+            stable_observables.append(obs_name)
 
     return {
         "n_gaps": int(len(gaps)),
         "originals": originals,
         "full_shuffle_baseline": baseline,
         "original_vs_shuffle_z": z,
+        "stable_observables_abs_z_ge_2": stable_observables,
+        "weak_observable_count": int(len(OBS_NAMES) - len(stable_observables)),
         "retention_curves": rows,
         "pca": pca_summary(rows),
     }
@@ -164,21 +141,25 @@
     gue = gue[:n_gaps]
 
     poisson = rng.exponential(1.0, size=n_gaps).astype(float)
+    prime_shuffle = rng.permutation(prime_gaps).astype(float)
     return {
         "primes": prime_gaps,
+        "prime_shuffle": prime_shuffle,
         "gue": gue,
         "poisson": poisson,
     }
 
 
-def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505):
+def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505, out="tools/data/observable_rank_audit.json"):
     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?",
+        "question": "When do canonical observable retention curves break collinearity across domains?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
         "params": {
             "n_gaps": int(n_gaps),
             "n_alpha": int(n_alpha),
@@ -189,19 +170,23 @@
         "sequences": {},
     }
 
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
     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)
+    print(f"{'sequence':<14} {'pc1':>8} {'eff_rank':>9} {'mean|corr|':>11} {'weak':>5}  z(SR,SR2,L1,L2,triple_var)")
+    print("-" * 108)
     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)
+        z_text = ", ".join(f"{obs}={z[obs]:+.1f}" for obs in OBS_NAMES)
         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}")
+        print(
+            f"{seq_name:<14} {pc1:>8.3f} {pca['effective_rank']:>9.3f} "
+            f"{pca['mean_abs_pairwise_corr']:>11.3f} {result['weak_observable_count']:>5}  {z_text}"
+        )
 
-    out_path = Path("tools/data/observable_rank_audit.json")
+    out_path = Path(out)
     with out_path.open("w") as f:
         json.dump(output, f, indent=2)
     print(f"\nSaved to {out_path}")
@@ -215,6 +200,7 @@
     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)
+    parser.add_argument("--out", default="tools/data/observable_rank_audit.json")
     args = parser.parse_args()
     run(
         n_gaps=args.n_gaps,
@@ -222,6 +208,7 @@
         n_trials=args.n_trials,
         n_baseline=args.n_baseline,
         seed=args.seed,
+        out=args.out,
     )
 
 

codex
Report creato. Ora aggiorno il seme con un vincolo leggero, perché il risultato non è una nuova scoperta fisica: è un gate metodologico che impedisce di leggere PCA/rank senza denominatori e controlli.
exec
/bin/bash -lc "python - <<'PY'
import json
from datetime import datetime, timezone
from pathlib import Path
path = Path('tools/data/seme.json')
data = json.loads(path.read_text())
now = datetime.now(timezone.utc).isoformat()
new_id = 'OBSERVABLE_COLLINEARITY_GATE'
claim = (
    'Le claim di collinearita/rank tra osservabili canonici vanno riportate con observables_registry, '
    'z original-vs-shuffle per osservabile e controlli Poisson/shuffle. Nel perimetro 20260506_1955, '
    'primi e GUE sotto partial-shuffle uniforme comprimono le retention curves a un coordinate dominante '
    '(rank medio 1.30 e 1.11), mentre Poisson e prime-shuffle mostrano rank piu alto solo con denominatori deboli '
    '(weak obs medi 5.0 e 4.67). Rank osservabile alto senza denominatori validi e segnale META, non struttura.'
)
entry = {
    'id': new_id,
    'tipo': 'vincolo',
    'claim': claim,
    'intensita': 0.9,
    'porta': 'META',
    'manuale': False,
    'condensato_ref': 'A4,A8,A14,C2',
    'origine': 'cycle agent_20260506_1955: observable collinearity breaking under canonical registry',
    'added_at': now,
}
items = data.setdefault('tensioni', [])
for i, item in enumerate(items):
    if item.get('id') == new_id:
        items[i] = entry
        break
else:
    items.insert(2, entry)
data['timestamp'] = now
data['direzione'] = (
    'Dopo i gate PERTURBATION_DENOMINATOR e OBSERVABLE_COLLINEARITY, non continuare con PCA/rank generici. '
    'Prossima mossa: selective operator coupling cross-dominio — perturbazioni separate su pair-scale e triple-scale, '
    'riportando solo osservabili con z original-vs-shuffle valido. Discriminante: primes ruotano rispetto a GUE oppure entrambi restano collineari.'
)
varianza = data.setdefault('varianza', [])
note = 'Cycle 20260506_1955: observable rank alto nei controlli e denominator-weak; collinearita strutturata valida solo con registry+z gate.'
if note not in varianza:
    varianza.append(note)
path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + '\\n')
print(f'updated {path} with {new_id}')
PY" in /opt/MM_D-ND
 succeeded in 0ms:
updated tools/data/seme.json with OBSERVABLE_COLLINEARITY_GATE

diff --git a/tools/data/reports/agent_20260506_1955.md b/tools/data/reports/agent_20260506_1955.md
new file mode 100644
index 0000000000000000000000000000000000000000..b4a665b4552a84cb33e6ea8db23d82d60b8ddde3
--- /dev/null
+++ b/tools/data/reports/agent_20260506_1955.md
@@ -0,0 +1,86 @@
+# Agent Report — Observable Collinearity Breaks Only Where Denominators Are Weak
+**Date**: 2026-05-06 19:55  
+**Piano**: 67  
+**Tension explored**: META + TRAJECTORY_APPLY_20260506_1941  
+observables_registry: 1.0.0-2026-05-06  
+observables_used: [SR, SR2, L1, L2, triple_var]
+
+## Claim Under Test
+Cycle 19:41 closed `PERTURBATION_DENOMINATOR_GATE` and the trajectory explicitly said:
+
+> La prossima corsa NON deve restare su perturbation rank.
+
+This run therefore does not test perturbation dimensionality. It asks a cross-domain META question:
+
+> When the five canonical observables are measured under uniform partial shuffle, does observable collinearity break in structured domains, or only in controls where original-vs-shuffle denominators are weak?
+
+## Experiment
+Tool updated: `tools/exp_observable_rank_audit.py`
+
+Method correction before execution:
+- removed local observable redefinitions;
+- imported canonical definitions from `tools/observables_registry.py`;
+- replaced the old local `triple_var` normalized convention with canonical raw `triple_var`;
+- added `prime_shuffle` as a control domain;
+- reported weak observable count using the fixed gate `abs(original - shuffle_mean) / shuffle_std < 2`.
+
+Atomic perimeter:
+- domains: first prime gaps, prime-shuffle control, independent GUE spacings, iid Poisson spacings;
+- main run: 12,000 gaps, 19 alpha values, 18 partial-shuffle trials per alpha, 48 full-shuffle baselines;
+- two seed checks: 8,000 gaps, 15 alpha values, 12 trials per alpha, 36 baselines;
+- measured object: PCA of the 5-observable retention curves across alpha, not perturbation profiles.
+
+## Results
+
+### Main Run
+
+| Domain | PC1 energy | effective rank | mean abs corr | weak obs / 5 | z summary |
+|---|---:|---:|---:|---:|---|
+| primes | 0.978 | 1.128 | 0.975 | 1 | SR=-12.1, SR2=-2.5, L1=-8.9, L2=-1.9, triple_var=-8.7 |
+| prime_shuffle | 0.593 | 2.475 | 0.606 | 5 | all abs(z) <= 1.1 |
+| GUE | 0.990 | 1.060 | 0.989 | 0 | SR=-2.9, SR2=+14.5, L1=+13.2, L2=+31.7, triple_var=+23.8 |
+| Poisson | 0.625 | 2.368 | 0.609 | 5 | all abs(z) <= 1.9 |
+
+### Three-Run Summary
+
+| Domain | PC1 mean | rank mean | mean abs corr | weak obs mean |
+|---|---:|---:|---:|---:|
+| primes | 0.939 | 1.296 | 0.924 | 1.33 |
+| prime_shuffle | 0.765 | 1.904 | 0.551 | 4.67 |
+| GUE | 0.980 | 1.106 | 0.977 | 0.33 |
+| Poisson | 0.714 | 2.196 | 0.572 | 5.00 |
+
+## Findings
+
+1. **Structured domains compress the five canonical retention curves to one dominant coordinate in this perimeter.** Primes and GUE both have PC1 > 0.93 on average and effective rank close to 1. This does not say the domains are the same; it says uniform partial shuffle moves the canonical observables along one dominant retention mode.
+
+2. **Observed collinearity breaking is concentrated in weak-denominator controls.** Poisson has the highest apparent rank among the three-run means (`2.196`), but all five observables are weak against full shuffle in every run. Prime-shuffle behaves similarly: rank is unstable and 4-5 of 5 observables are weak. This mirrors the denominator lesson from perturbation rank without repeating the perturbation-rank experiment.
+
+3. **The 05-05 observable-rank result survives only after narrowing its language.** The valid statement is not "five probes are always one thing." The scoped statement is: under uniform partial shuffle and canonical observables, primes and GUE show a dominant one-coordinate retention response; controls can show larger PCA rank, but that rank is not structural when the original-vs-shuffle denominators are absent.
+
+4. **GUE is the cleanest conditioning check.** In the main run, all five GUE observables pass the denominator gate and still give rank `1.060`. This makes GUE the best positive control for "low rank despite valid denominators." Poisson is the negative control for "high rank without valid denominators."
+
+## Verdict
+**CONSTRAINT on META**: observable collinearity claims must be reported with:
+
+> observables_registry version + canonical observable list + original-vs-shuffle z per observable + control domains.
+
+In this perimeter, high observable-rank is not the sign of richer structure when it appears in Poisson or prime-shuffle controls; it is a warning that retention ratios are being formed on weak denominators. The stable cross-domain result is narrower:
+
+> uniform partial shuffle exposes one dominant retention coordinate in conditioned structured domains (primes, GUE), while apparent multi-coordinate behavior in Poisson/shuffle controls is denominator-weak.
+
+## Consecutio
+What opens now: the next non-redundant test is not another PCA audit. It is **selective operator coupling**: use perturbations that act separately on pair-scale and triple-scale structure, but report only observables whose denominator gate survives. If selective operators rotate primes while GUE stays collinear, the lab gets a real cross-domain discriminator; if both stay collinear, the current observable suite is overcomplete for this question.
+
+## Auto-audit: 5 lenti
+- **L1 hard constraint vs bias**: no zero/always claim. "Weak" means `abs(z) < 2` in this declared gate.
+- **L2 quantity vs ratio**: PCA rank and retention ratios are interpreted only after raw z denominators.
+- **L3 no silent patching**: the older 05-05 collinearity claim is explicitly narrowed; it is not silently rebranded as universal.
+- **L4 edge cases**: `prime_shuffle` seed 1956 produced low rank with 4 weak observables; this is reported as unstable control behavior, not ignored.
+- **L5 re-discovery**: PCA rank instability under noisy normalization is standard statistical hygiene. This is a lab method constraint, not a new RMT theorem.
+
+## Files
+- Script: `tools/exp_observable_rank_audit.py`
+- Main data: `tools/data/observable_collinearity_breaking_20260506_1955.json`
+- Seed checks: `tools/data/observable_collinearity_breaking_20260506_1956.json`, `tools/data/observable_collinearity_breaking_20260506_1957.json`
+- Report: `tools/data/reports/agent_20260506_1955.md`
diff --git a/tools/exp_observable_rank_audit.py b/tools/exp_observable_rank_audit.py
index f2acafc5960c8a35964e0ab521cf70569eefe27f..51e6ec52edfffda55d1d22862a102f896a08fdd9
--- a/tools/exp_observable_rank_audit.py
+++ b/tools/exp_observable_rank_audit.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 """
-exp_observable_rank_audit.py - META audit for observable redundancy.
+exp_observable_rank_audit.py - META audit for observable collinearity.
 
 Recent runs found many Markov/crossover observables that react coherently under
 partial shuffle. This script asks whether those observables carry independent
@@ -22,47 +22,19 @@
 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)
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
 
 
-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,
-}
+OBSERVABLES = OBSERVABLES_CANONICAL
+OBS_NAMES = list(OBSERVABLES)
 
 
 def measure(gaps):
-    return {name: fn(gaps) for name, fn in OBSERVABLES.items()}
+    return compute_canonical(gaps)
 
 
 def full_shuffle_baseline(gaps, n_trials, rng):
@@ -106,7 +78,7 @@
 
 
 def pca_summary(rows):
-    names = list(OBSERVABLES)
+    names = OBS_NAMES
     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)
 
@@ -142,15 +114,20 @@
     rows = retention_curves(gaps, alphas, n_trials, originals, baseline, rng)
 
     z = {}
-    for obs_name in OBSERVABLES:
+    stable_observables = []
+    for obs_name in OBS_NAMES:
         std = baseline[obs_name]["std"]
         z[obs_name] = float((originals[obs_name] - baseline[obs_name]["mean"]) / std) if std > 1e-12 else 0.0
+        if abs(z[obs_name]) >= 2.0:
+            stable_observables.append(obs_name)
 
     return {
         "n_gaps": int(len(gaps)),
         "originals": originals,
         "full_shuffle_baseline": baseline,
         "original_vs_shuffle_z": z,
+        "stable_observables_abs_z_ge_2": stable_observables,
+        "weak_observable_count": int(len(OBS_NAMES) - len(stable_observables)),
         "retention_curves": rows,
         "pca": pca_summary(rows),
     }
@@ -164,21 +141,25 @@
     gue = gue[:n_gaps]
 
     poisson = rng.exponential(1.0, size=n_gaps).astype(float)
+    prime_shuffle = rng.permutation(prime_gaps).astype(float)
     return {
         "primes": prime_gaps,
+        "prime_shuffle": prime_shuffle,
         "gue": gue,
         "poisson": poisson,
     }
 
 
-def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505):
+def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505, out="tools/data/observable_rank_audit.json"):
     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?",
+        "question": "When do canonical observable retention curves break collinearity across domains?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
         "params": {
             "n_gaps": int(n_gaps),
             "n_alpha": int(n_alpha),
@@ -189,19 +170,23 @@
         "sequences": {},
     }
 
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
     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)
+    print(f"{'sequence':<14} {'pc1':>8} {'eff_rank':>9} {'mean|corr|':>11} {'weak':>5}  z(SR,SR2,L1,L2,triple_var)")
+    print("-" * 108)
     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)
+        z_text = ", ".join(f"{obs}={z[obs]:+.1f}" for obs in OBS_NAMES)
         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}")
+        print(
+            f"{seq_name:<14} {pc1:>8.3f} {pca['effective_rank']:>9.3f} "
+            f"{pca['mean_abs_pairwise_corr']:>11.3f} {result['weak_observable_count']:>5}  {z_text}"
+        )
 
-    out_path = Path("tools/data/observable_rank_audit.json")
+    out_path = Path(out)
     with out_path.open("w") as f:
         json.dump(output, f, indent=2)
     print(f"\nSaved to {out_path}")
@@ -215,6 +200,7 @@
     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)
+    parser.add_argument("--out", default="tools/data/observable_rank_audit.json")
     args = parser.parse_args()
     run(
         n_gaps=args.n_gaps,
@@ -222,6 +208,7 @@
         n_trials=args.n_trials,
         n_baseline=args.n_baseline,
         seed=args.seed,
+        out=args.out,
     )
 
 

codex
Seme aggiornato con `OBSERVABLE_COLLINEARITY_GATE` e nuova direzione operativa. Faccio un controllo rapido del report e provo il falsifier se l’interfaccia è disponibile.
exec
/bin/bash -lc "sed -n '1,260p' tools/data/reports/agent_20260506_1955.md" in /opt/MM_D-ND
 succeeded in 0ms:
# Agent Report — Observable Collinearity Breaks Only Where Denominators Are Weak
**Date**: 2026-05-06 19:55  
**Piano**: 67  
**Tension explored**: META + TRAJECTORY_APPLY_20260506_1941  
observables_registry: 1.0.0-2026-05-06  
observables_used: [SR, SR2, L1, L2, triple_var]

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

> La prossima corsa NON deve restare su perturbation rank.

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

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

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

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

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

## Results

### Main Run

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

### Three-Run Summary

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

## Findings

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

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

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

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

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

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

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

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

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

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

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

exec
/bin/bash -lc "python - <<'PY'
import json
from pathlib import Path
p = Path('tools/data/seme.json')
data = json.loads(p.read_text())
print(data['timestamp'])
print(data['direzione'])
print([t.get('id') for t in data['tensioni'][:5]])
PY" in /opt/MM_D-ND
 succeeded in 0ms:
2026-05-06T19:58:40.943194+00:00
Dopo i gate PERTURBATION_DENOMINATOR e OBSERVABLE_COLLINEARITY, non continuare con PCA/rank generici. Prossima mossa: selective operator coupling cross-dominio — perturbazioni separate su pair-scale e triple-scale, riportando solo osservabili con z original-vs-shuffle valido. Discriminante: primes ruotano rispetto a GUE oppure entrambi restano collineari.
['OBSERVABLE_REGISTRY', 'PERTURBATION_DENOMINATOR_GATE', 'OBSERVABLE_COLLINEARITY_GATE', 'TRASCENDENZA_LIMITE', 'DUALITA_DIPOLARE_VS_ILLUSORIA']

exec
/bin/bash -lc 'python tools/lab_falsifier.py --help' in /opt/MM_D-ND
 succeeded in 0ms:
usage: lab_falsifier.py [-h] --report REPORT --output OUTPUT
                        [--timeout TIMEOUT]

Counter-pole gate for the lab

options:
  -h, --help         show this help message and exit
  --report REPORT    Path to agent_TS.md
  --output OUTPUT    Path for falsifier_TS.json output
  --timeout TIMEOUT  LLM call timeout seconds

exec
/bin/bash -lc 'python -m py_compile tools/exp_observable_rank_audit.py' in /opt/MM_D-ND
 succeeded in 0ms:
diff --git a/tools/data/reports/agent_20260506_1955.md b/tools/data/reports/agent_20260506_1955.md
new file mode 100644
index 0000000000000000000000000000000000000000..b4a665b4552a84cb33e6ea8db23d82d60b8ddde3
--- /dev/null
+++ b/tools/data/reports/agent_20260506_1955.md
@@ -0,0 +1,86 @@
+# Agent Report — Observable Collinearity Breaks Only Where Denominators Are Weak
+**Date**: 2026-05-06 19:55  
+**Piano**: 67  
+**Tension explored**: META + TRAJECTORY_APPLY_20260506_1941  
+observables_registry: 1.0.0-2026-05-06  
+observables_used: [SR, SR2, L1, L2, triple_var]
+
+## Claim Under Test
+Cycle 19:41 closed `PERTURBATION_DENOMINATOR_GATE` and the trajectory explicitly said:
+
+> La prossima corsa NON deve restare su perturbation rank.
+
+This run therefore does not test perturbation dimensionality. It asks a cross-domain META question:
+
+> When the five canonical observables are measured under uniform partial shuffle, does observable collinearity break in structured domains, or only in controls where original-vs-shuffle denominators are weak?
+
+## Experiment
+Tool updated: `tools/exp_observable_rank_audit.py`
+
+Method correction before execution:
+- removed local observable redefinitions;
+- imported canonical definitions from `tools/observables_registry.py`;
+- replaced the old local `triple_var` normalized convention with canonical raw `triple_var`;
+- added `prime_shuffle` as a control domain;
+- reported weak observable count using the fixed gate `abs(original - shuffle_mean) / shuffle_std < 2`.
+
+Atomic perimeter:
+- domains: first prime gaps, prime-shuffle control, independent GUE spacings, iid Poisson spacings;
+- main run: 12,000 gaps, 19 alpha values, 18 partial-shuffle trials per alpha, 48 full-shuffle baselines;
+- two seed checks: 8,000 gaps, 15 alpha values, 12 trials per alpha, 36 baselines;
+- measured object: PCA of the 5-observable retention curves across alpha, not perturbation profiles.
+
+## Results
+
+### Main Run
+
+| Domain | PC1 energy | effective rank | mean abs corr | weak obs / 5 | z summary |
+|---|---:|---:|---:|---:|---|
+| primes | 0.978 | 1.128 | 0.975 | 1 | SR=-12.1, SR2=-2.5, L1=-8.9, L2=-1.9, triple_var=-8.7 |
+| prime_shuffle | 0.593 | 2.475 | 0.606 | 5 | all abs(z) <= 1.1 |
+| GUE | 0.990 | 1.060 | 0.989 | 0 | SR=-2.9, SR2=+14.5, L1=+13.2, L2=+31.7, triple_var=+23.8 |
+| Poisson | 0.625 | 2.368 | 0.609 | 5 | all abs(z) <= 1.9 |
+
+### Three-Run Summary
+
+| Domain | PC1 mean | rank mean | mean abs corr | weak obs mean |
+|---|---:|---:|---:|---:|
+| primes | 0.939 | 1.296 | 0.924 | 1.33 |
+| prime_shuffle | 0.765 | 1.904 | 0.551 | 4.67 |
+| GUE | 0.980 | 1.106 | 0.977 | 0.33 |
+| Poisson | 0.714 | 2.196 | 0.572 | 5.00 |
+
+## Findings
+
+1. **Structured domains compress the five canonical retention curves to one dominant coordinate in this perimeter.** Primes and GUE both have PC1 > 0.93 on average and effective rank close to 1. This does not say the domains are the same; it says uniform partial shuffle moves the canonical observables along one dominant retention mode.
+
+2. **Observed collinearity breaking is concentrated in weak-denominator controls.** Poisson has the highest apparent rank among the three-run means (`2.196`), but all five observables are weak against full shuffle in every run. Prime-shuffle behaves similarly: rank is unstable and 4-5 of 5 observables are weak. This mirrors the denominator lesson from perturbation rank without repeating the perturbation-rank experiment.
+
+3. **The 05-05 observable-rank result survives only after narrowing its language.** The valid statement is not "five probes are always one thing." The scoped statement is: under uniform partial shuffle and canonical observables, primes and GUE show a dominant one-coordinate retention response; controls can show larger PCA rank, but that rank is not structural when the original-vs-shuffle denominators are absent.
+
+4. **GUE is the cleanest conditioning check.** In the main run, all five GUE observables pass the denominator gate and still give rank `1.060`. This makes GUE the best positive control for "low rank despite valid denominators." Poisson is the negative control for "high rank without valid denominators."
+
+## Verdict
+**CONSTRAINT on META**: observable collinearity claims must be reported with:
+
+> observables_registry version + canonical observable list + original-vs-shuffle z per observable + control domains.
+
+In this perimeter, high observable-rank is not the sign of richer structure when it appears in Poisson or prime-shuffle controls; it is a warning that retention ratios are being formed on weak denominators. The stable cross-domain result is narrower:
+
+> uniform partial shuffle exposes one dominant retention coordinate in conditioned structured domains (primes, GUE), while apparent multi-coordinate behavior in Poisson/shuffle controls is denominator-weak.
+
+## Consecutio
+What opens now: the next non-redundant test is not another PCA audit. It is **selective operator coupling**: use perturbations that act separately on pair-scale and triple-scale structure, but report only observables whose denominator gate survives. If selective operators rotate primes while GUE stays collinear, the lab gets a real cross-domain discriminator; if both stay collinear, the current observable suite is overcomplete for this question.
+
+## Auto-audit: 5 lenti
+- **L1 hard constraint vs bias**: no zero/always claim. "Weak" means `abs(z) < 2` in this declared gate.
+- **L2 quantity vs ratio**: PCA rank and retention ratios are interpreted only after raw z denominators.
+- **L3 no silent patching**: the older 05-05 collinearity claim is explicitly narrowed; it is not silently rebranded as universal.
+- **L4 edge cases**: `prime_shuffle` seed 1956 produced low rank with 4 weak observables; this is reported as unstable control behavior, not ignored.
+- **L5 re-discovery**: PCA rank instability under noisy normalization is standard statistical hygiene. This is a lab method constraint, not a new RMT theorem.
+
+## Files
+- Script: `tools/exp_observable_rank_audit.py`
+- Main data: `tools/data/observable_collinearity_breaking_20260506_1955.json`
+- Seed checks: `tools/data/observable_collinearity_breaking_20260506_1956.json`, `tools/data/observable_collinearity_breaking_20260506_1957.json`
+- Report: `tools/data/reports/agent_20260506_1955.md`
diff --git a/tools/exp_observable_rank_audit.py b/tools/exp_observable_rank_audit.py
index f2acafc5960c8a35964e0ab521cf70569eefe27f..51e6ec52edfffda55d1d22862a102f896a08fdd9
--- a/tools/exp_observable_rank_audit.py
+++ b/tools/exp_observable_rank_audit.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 """
-exp_observable_rank_audit.py - META audit for observable redundancy.
+exp_observable_rank_audit.py - META audit for observable collinearity.
 
 Recent runs found many Markov/crossover observables that react coherently under
 partial shuffle. This script asks whether those observables carry independent
@@ -22,47 +22,19 @@
 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)
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
 
 
-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,
-}
+OBSERVABLES = OBSERVABLES_CANONICAL
+OBS_NAMES = list(OBSERVABLES)
 
 
 def measure(gaps):
-    return {name: fn(gaps) for name, fn in OBSERVABLES.items()}
+    return compute_canonical(gaps)
 
 
 def full_shuffle_baseline(gaps, n_trials, rng):
@@ -106,7 +78,7 @@
 
 
 def pca_summary(rows):
-    names = list(OBSERVABLES)
+    names = OBS_NAMES
     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)
 
@@ -142,15 +114,20 @@
     rows = retention_curves(gaps, alphas, n_trials, originals, baseline, rng)
 
     z = {}
-    for obs_name in OBSERVABLES:
+    stable_observables = []
+    for obs_name in OBS_NAMES:
         std = baseline[obs_name]["std"]
         z[obs_name] = float((originals[obs_name] - baseline[obs_name]["mean"]) / std) if std > 1e-12 else 0.0
+        if abs(z[obs_name]) >= 2.0:
+            stable_observables.append(obs_name)
 
     return {
         "n_gaps": int(len(gaps)),
         "originals": originals,
         "full_shuffle_baseline": baseline,
         "original_vs_shuffle_z": z,
+        "stable_observables_abs_z_ge_2": stable_observables,
+        "weak_observable_count": int(len(OBS_NAMES) - len(stable_observables)),
         "retention_curves": rows,
         "pca": pca_summary(rows),
     }
@@ -164,21 +141,25 @@
     gue = gue[:n_gaps]
 
     poisson = rng.exponential(1.0, size=n_gaps).astype(float)
+    prime_shuffle = rng.permutation(prime_gaps).astype(float)
     return {
         "primes": prime_gaps,
+        "prime_shuffle": prime_shuffle,
         "gue": gue,
         "poisson": poisson,
     }
 
 
-def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505):
+def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505, out="tools/data/observable_rank_audit.json"):
     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?",
+        "question": "When do canonical observable retention curves break collinearity across domains?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
         "params": {
             "n_gaps": int(n_gaps),
             "n_alpha": int(n_alpha),
@@ -189,19 +170,23 @@
         "sequences": {},
     }
 
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
     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)
+    print(f"{'sequence':<14} {'pc1':>8} {'eff_rank':>9} {'mean|corr|':>11} {'weak':>5}  z(SR,SR2,L1,L2,triple_var)")
+    print("-" * 108)
     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)
+        z_text = ", ".join(f"{obs}={z[obs]:+.1f}" for obs in OBS_NAMES)
         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}")
+        print(
+            f"{seq_name:<14} {pc1:>8.3f} {pca['effective_rank']:>9.3f} "
+            f"{pca['mean_abs_pairwise_corr']:>11.3f} {result['weak_observable_count']:>5}  {z_text}"
+        )
 
-    out_path = Path("tools/data/observable_rank_audit.json")
+    out_path = Path(out)
     with out_path.open("w") as f:
         json.dump(output, f, indent=2)
     print(f"\nSaved to {out_path}")
@@ -215,6 +200,7 @@
     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)
+    parser.add_argument("--out", default="tools/data/observable_rank_audit.json")
     args = parser.parse_args()
     run(
         n_gaps=args.n_gaps,
@@ -222,6 +208,7 @@
         n_trials=args.n_trials,
         n_baseline=args.n_baseline,
         seed=args.seed,
+        out=args.out,
     )
 
 

exec
/bin/bash -lc 'python tools/lab_falsifier.py --report tools/data/reports/agent_20260506_1955.md --output tools/data/reports/falsifier_20260506_1955.json --timeout 120' in /opt/MM_D-ND
diff --git a/tools/data/reports/agent_20260506_1955.md b/tools/data/reports/agent_20260506_1955.md
new file mode 100644
index 0000000000000000000000000000000000000000..b4a665b4552a84cb33e6ea8db23d82d60b8ddde3
--- /dev/null
+++ b/tools/data/reports/agent_20260506_1955.md
@@ -0,0 +1,86 @@
+# Agent Report — Observable Collinearity Breaks Only Where Denominators Are Weak
+**Date**: 2026-05-06 19:55  
+**Piano**: 67  
+**Tension explored**: META + TRAJECTORY_APPLY_20260506_1941  
+observables_registry: 1.0.0-2026-05-06  
+observables_used: [SR, SR2, L1, L2, triple_var]
+
+## Claim Under Test
+Cycle 19:41 closed `PERTURBATION_DENOMINATOR_GATE` and the trajectory explicitly said:
+
+> La prossima corsa NON deve restare su perturbation rank.
+
+This run therefore does not test perturbation dimensionality. It asks a cross-domain META question:
+
+> When the five canonical observables are measured under uniform partial shuffle, does observable collinearity break in structured domains, or only in controls where original-vs-shuffle denominators are weak?
+
+## Experiment
+Tool updated: `tools/exp_observable_rank_audit.py`
+
+Method correction before execution:
+- removed local observable redefinitions;
+- imported canonical definitions from `tools/observables_registry.py`;
+- replaced the old local `triple_var` normalized convention with canonical raw `triple_var`;
+- added `prime_shuffle` as a control domain;
+- reported weak observable count using the fixed gate `abs(original - shuffle_mean) / shuffle_std < 2`.
+
+Atomic perimeter:
+- domains: first prime gaps, prime-shuffle control, independent GUE spacings, iid Poisson spacings;
+- main run: 12,000 gaps, 19 alpha values, 18 partial-shuffle trials per alpha, 48 full-shuffle baselines;
+- two seed checks: 8,000 gaps, 15 alpha values, 12 trials per alpha, 36 baselines;
+- measured object: PCA of the 5-observable retention curves across alpha, not perturbation profiles.
+
+## Results
+
+### Main Run
+
+| Domain | PC1 energy | effective rank | mean abs corr | weak obs / 5 | z summary |
+|---|---:|---:|---:|---:|---|
+| primes | 0.978 | 1.128 | 0.975 | 1 | SR=-12.1, SR2=-2.5, L1=-8.9, L2=-1.9, triple_var=-8.7 |
+| prime_shuffle | 0.593 | 2.475 | 0.606 | 5 | all abs(z) <= 1.1 |
+| GUE | 0.990 | 1.060 | 0.989 | 0 | SR=-2.9, SR2=+14.5, L1=+13.2, L2=+31.7, triple_var=+23.8 |
+| Poisson | 0.625 | 2.368 | 0.609 | 5 | all abs(z) <= 1.9 |
+
+### Three-Run Summary
+
+| Domain | PC1 mean | rank mean | mean abs corr | weak obs mean |
+|---|---:|---:|---:|---:|
+| primes | 0.939 | 1.296 | 0.924 | 1.33 |
+| prime_shuffle | 0.765 | 1.904 | 0.551 | 4.67 |
+| GUE | 0.980 | 1.106 | 0.977 | 0.33 |
+| Poisson | 0.714 | 2.196 | 0.572 | 5.00 |
+
+## Findings
+
+1. **Structured domains compress the five canonical retention curves to one dominant coordinate in this perimeter.** Primes and GUE both have PC1 > 0.93 on average and effective rank close to 1. This does not say the domains are the same; it says uniform partial shuffle moves the canonical observables along one dominant retention mode.
+
+2. **Observed collinearity breaking is concentrated in weak-denominator controls.** Poisson has the highest apparent rank among the three-run means (`2.196`), but all five observables are weak against full shuffle in every run. Prime-shuffle behaves similarly: rank is unstable and 4-5 of 5 observables are weak. This mirrors the denominator lesson from perturbation rank without repeating the perturbation-rank experiment.
+
+3. **The 05-05 observable-rank result survives only after narrowing its language.** The valid statement is not "five probes are always one thing." The scoped statement is: under uniform partial shuffle and canonical observables, primes and GUE show a dominant one-coordinate retention response; controls can show larger PCA rank, but that rank is not structural when the original-vs-shuffle denominators are absent.
+
+4. **GUE is the cleanest conditioning check.** In the main run, all five GUE observables pass the denominator gate and still give rank `1.060`. This makes GUE the best positive control for "low rank despite valid denominators." Poisson is the negative control for "high rank without valid denominators."
+
+## Verdict
+**CONSTRAINT on META**: observable collinearity claims must be reported with:
+
+> observables_registry version + canonical observable list + original-vs-shuffle z per observable + control domains.
+
+In this perimeter, high observable-rank is not the sign of richer structure when it appears in Poisson or prime-shuffle controls; it is a warning that retention ratios are being formed on weak denominators. The stable cross-domain result is narrower:
+
+> uniform partial shuffle exposes one dominant retention coordinate in conditioned structured domains (primes, GUE), while apparent multi-coordinate behavior in Poisson/shuffle controls is denominator-weak.
+
+## Consecutio
+What opens now: the next non-redundant test is not another PCA audit. It is **selective operator coupling**: use perturbations that act separately on pair-scale and triple-scale structure, but report only observables whose denominator gate survives. If selective operators rotate primes while GUE stays collinear, the lab gets a real cross-domain discriminator; if both stay collinear, the current observable suite is overcomplete for this question.
+
+## Auto-audit: 5 lenti
+- **L1 hard constraint vs bias**: no zero/always claim. "Weak" means `abs(z) < 2` in this declared gate.
+- **L2 quantity vs ratio**: PCA rank and retention ratios are interpreted only after raw z denominators.
+- **L3 no silent patching**: the older 05-05 collinearity claim is explicitly narrowed; it is not silently rebranded as universal.
+- **L4 edge cases**: `prime_shuffle` seed 1956 produced low rank with 4 weak observables; this is reported as unstable control behavior, not ignored.
+- **L5 re-discovery**: PCA rank instability under noisy normalization is standard statistical hygiene. This is a lab method constraint, not a new RMT theorem.
+
+## Files
+- Script: `tools/exp_observable_rank_audit.py`
+- Main data: `tools/data/observable_collinearity_breaking_20260506_1955.json`
+- Seed checks: `tools/data/observable_collinearity_breaking_20260506_1956.json`, `tools/data/observable_collinearity_breaking_20260506_1957.json`
+- Report: `tools/data/reports/agent_20260506_1955.md`
diff --git a/tools/exp_observable_rank_audit.py b/tools/exp_observable_rank_audit.py
index f2acafc5960c8a35964e0ab521cf70569eefe27f..51e6ec52edfffda55d1d22862a102f896a08fdd9
--- a/tools/exp_observable_rank_audit.py
+++ b/tools/exp_observable_rank_audit.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 """
-exp_observable_rank_audit.py - META audit for observable redundancy.
+exp_observable_rank_audit.py - META audit for observable collinearity.
 
 Recent runs found many Markov/crossover observables that react coherently under
 partial shuffle. This script asks whether those observables carry independent
@@ -22,47 +22,19 @@
 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)
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
 
 
-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,
-}
+OBSERVABLES = OBSERVABLES_CANONICAL
+OBS_NAMES = list(OBSERVABLES)
 
 
 def measure(gaps):
-    return {name: fn(gaps) for name, fn in OBSERVABLES.items()}
+    return compute_canonical(gaps)
 
 
 def full_shuffle_baseline(gaps, n_trials, rng):
@@ -106,7 +78,7 @@
 
 
 def pca_summary(rows):
-    names = list(OBSERVABLES)
+    names = OBS_NAMES
     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)
 
@@ -142,15 +114,20 @@
     rows = retention_curves(gaps, alphas, n_trials, originals, baseline, rng)
 
     z = {}
-    for obs_name in OBSERVABLES:
+    stable_observables = []
+    for obs_name in OBS_NAMES:
         std = baseline[obs_name]["std"]
         z[obs_name] = float((originals[obs_name] - baseline[obs_name]["mean"]) / std) if std > 1e-12 else 0.0
+        if abs(z[obs_name]) >= 2.0:
+            stable_observables.append(obs_name)
 
     return {
         "n_gaps": int(len(gaps)),
         "originals": originals,
         "full_shuffle_baseline": baseline,
         "original_vs_shuffle_z": z,
+        "stable_observables_abs_z_ge_2": stable_observables,
+        "weak_observable_count": int(len(OBS_NAMES) - len(stable_observables)),
         "retention_curves": rows,
         "pca": pca_summary(rows),
     }
@@ -164,21 +141,25 @@
     gue = gue[:n_gaps]
 
     poisson = rng.exponential(1.0, size=n_gaps).astype(float)
+    prime_shuffle = rng.permutation(prime_gaps).astype(float)
     return {
         "primes": prime_gaps,
+        "prime_shuffle": prime_shuffle,
         "gue": gue,
         "poisson": poisson,
     }
 
 
-def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505):
+def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505, out="tools/data/observable_rank_audit.json"):
     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?",
+        "question": "When do canonical observable retention curves break collinearity across domains?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
         "params": {
             "n_gaps": int(n_gaps),
             "n_alpha": int(n_alpha),
@@ -189,19 +170,23 @@
         "sequences": {},
     }
 
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
     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)
+    print(f"{'sequence':<14} {'pc1':>8} {'eff_rank':>9} {'mean|corr|':>11} {'weak':>5}  z(SR,SR2,L1,L2,triple_var)")
+    print("-" * 108)
     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)
+        z_text = ", ".join(f"{obs}={z[obs]:+.1f}" for obs in OBS_NAMES)
         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}")
+        print(
+            f"{seq_name:<14} {pc1:>8.3f} {pca['effective_rank']:>9.3f} "
+            f"{pca['mean_abs_pairwise_corr']:>11.3f} {result['weak_observable_count']:>5}  {z_text}"
+        )
 
-    out_path = Path("tools/data/observable_rank_audit.json")
+    out_path = Path(out)
     with out_path.open("w") as f:
         json.dump(output, f, indent=2)
     print(f"\nSaved to {out_path}")
@@ -215,6 +200,7 @@
     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)
+    parser.add_argument("--out", default="tools/data/observable_rank_audit.json")
     args = parser.parse_args()
     run(
         n_gaps=args.n_gaps,
@@ -222,6 +208,7 @@
         n_trials=args.n_trials,
         n_baseline=args.n_baseline,
         seed=args.seed,
+        out=args.out,
     )
 
 

diff --git a/tools/data/reports/agent_20260506_1955.md b/tools/data/reports/agent_20260506_1955.md
new file mode 100644
index 0000000000000000000000000000000000000000..b4a665b4552a84cb33e6ea8db23d82d60b8ddde3
--- /dev/null
+++ b/tools/data/reports/agent_20260506_1955.md
@@ -0,0 +1,86 @@
+# Agent Report — Observable Collinearity Breaks Only Where Denominators Are Weak
+**Date**: 2026-05-06 19:55  
+**Piano**: 67  
+**Tension explored**: META + TRAJECTORY_APPLY_20260506_1941  
+observables_registry: 1.0.0-2026-05-06  
+observables_used: [SR, SR2, L1, L2, triple_var]
+
+## Claim Under Test
+Cycle 19:41 closed `PERTURBATION_DENOMINATOR_GATE` and the trajectory explicitly said:
+
+> La prossima corsa NON deve restare su perturbation rank.
+
+This run therefore does not test perturbation dimensionality. It asks a cross-domain META question:
+
+> When the five canonical observables are measured under uniform partial shuffle, does observable collinearity break in structured domains, or only in controls where original-vs-shuffle denominators are weak?
+
+## Experiment
+Tool updated: `tools/exp_observable_rank_audit.py`
+
+Method correction before execution:
+- removed local observable redefinitions;
+- imported canonical definitions from `tools/observables_registry.py`;
+- replaced the old local `triple_var` normalized convention with canonical raw `triple_var`;
+- added `prime_shuffle` as a control domain;
+- reported weak observable count using the fixed gate `abs(original - shuffle_mean) / shuffle_std < 2`.
+
+Atomic perimeter:
+- domains: first prime gaps, prime-shuffle control, independent GUE spacings, iid Poisson spacings;
+- main run: 12,000 gaps, 19 alpha values, 18 partial-shuffle trials per alpha, 48 full-shuffle baselines;
+- two seed checks: 8,000 gaps, 15 alpha values, 12 trials per alpha, 36 baselines;
+- measured object: PCA of the 5-observable retention curves across alpha, not perturbation profiles.
+
+## Results
+
+### Main Run
+
+| Domain | PC1 energy | effective rank | mean abs corr | weak obs / 5 | z summary |
+|---|---:|---:|---:|---:|---|
+| primes | 0.978 | 1.128 | 0.975 | 1 | SR=-12.1, SR2=-2.5, L1=-8.9, L2=-1.9, triple_var=-8.7 |
+| prime_shuffle | 0.593 | 2.475 | 0.606 | 5 | all abs(z) <= 1.1 |
+| GUE | 0.990 | 1.060 | 0.989 | 0 | SR=-2.9, SR2=+14.5, L1=+13.2, L2=+31.7, triple_var=+23.8 |
+| Poisson | 0.625 | 2.368 | 0.609 | 5 | all abs(z) <= 1.9 |
+
+### Three-Run Summary
+
+| Domain | PC1 mean | rank mean | mean abs corr | weak obs mean |
+|---|---:|---:|---:|---:|
+| primes | 0.939 | 1.296 | 0.924 | 1.33 |
+| prime_shuffle | 0.765 | 1.904 | 0.551 | 4.67 |
+| GUE | 0.980 | 1.106 | 0.977 | 0.33 |
+| Poisson | 0.714 | 2.196 | 0.572 | 5.00 |
+
+## Findings
+
+1. **Structured domains compress the five canonical retention curves to one dominant coordinate in this perimeter.** Primes and GUE both have PC1 > 0.93 on average and effective rank close to 1. This does not say the domains are the same; it says uniform partial shuffle moves the canonical observables along one dominant retention mode.
+
+2. **Observed collinearity breaking is concentrated in weak-denominator controls.** Poisson has the highest apparent rank among the three-run means (`2.196`), but all five observables are weak against full shuffle in every run. Prime-shuffle behaves similarly: rank is unstable and 4-5 of 5 observables are weak. This mirrors the denominator lesson from perturbation rank without repeating the perturbation-rank experiment.
+
+3. **The 05-05 observable-rank result survives only after narrowing its language.** The valid statement is not "five probes are always one thing." The scoped statement is: under uniform partial shuffle and canonical observables, primes and GUE show a dominant one-coordinate retention response; controls can show larger PCA rank, but that rank is not structural when the original-vs-shuffle denominators are absent.
+
+4. **GUE is the cleanest conditioning check.** In the main run, all five GUE observables pass the denominator gate and still give rank `1.060`. This makes GUE the best positive control for "low rank despite valid denominators." Poisson is the negative control for "high rank without valid denominators."
+
+## Verdict
+**CONSTRAINT on META**: observable collinearity claims must be reported with:
+
+> observables_registry version + canonical observable list + original-vs-shuffle z per observable + control domains.
+
+In this perimeter, high observable-rank is not the sign of richer structure when it appears in Poisson or prime-shuffle controls; it is a warning that retention ratios are being formed on weak denominators. The stable cross-domain result is narrower:
+
+> uniform partial shuffle exposes one dominant retention coordinate in conditioned structured domains (primes, GUE), while apparent multi-coordinate behavior in Poisson/shuffle controls is denominator-weak.
+
+## Consecutio
+What opens now: the next non-redundant test is not another PCA audit. It is **selective operator coupling**: use perturbations that act separately on pair-scale and triple-scale structure, but report only observables whose denominator gate survives. If selective operators rotate primes while GUE stays collinear, the lab gets a real cross-domain discriminator; if both stay collinear, the current observable suite is overcomplete for this question.
+
+## Auto-audit: 5 lenti
+- **L1 hard constraint vs bias**: no zero/always claim. "Weak" means `abs(z) < 2` in this declared gate.
+- **L2 quantity vs ratio**: PCA rank and retention ratios are interpreted only after raw z denominators.
+- **L3 no silent patching**: the older 05-05 collinearity claim is explicitly narrowed; it is not silently rebranded as universal.
+- **L4 edge cases**: `prime_shuffle` seed 1956 produced low rank with 4 weak observables; this is reported as unstable control behavior, not ignored.
+- **L5 re-discovery**: PCA rank instability under noisy normalization is standard statistical hygiene. This is a lab method constraint, not a new RMT theorem.
+
+## Files
+- Script: `tools/exp_observable_rank_audit.py`
+- Main data: `tools/data/observable_collinearity_breaking_20260506_1955.json`
+- Seed checks: `tools/data/observable_collinearity_breaking_20260506_1956.json`, `tools/data/observable_collinearity_breaking_20260506_1957.json`
+- Report: `tools/data/reports/agent_20260506_1955.md`
diff --git a/tools/exp_observable_rank_audit.py b/tools/exp_observable_rank_audit.py
index f2acafc5960c8a35964e0ab521cf70569eefe27f..51e6ec52edfffda55d1d22862a102f896a08fdd9
--- a/tools/exp_observable_rank_audit.py
+++ b/tools/exp_observable_rank_audit.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 """
-exp_observable_rank_audit.py - META audit for observable redundancy.
+exp_observable_rank_audit.py - META audit for observable collinearity.
 
 Recent runs found many Markov/crossover observables that react coherently under
 partial shuffle. This script asks whether those observables carry independent
@@ -22,47 +22,19 @@
 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)
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
 
 
-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,
-}
+OBSERVABLES = OBSERVABLES_CANONICAL
+OBS_NAMES = list(OBSERVABLES)
 
 
 def measure(gaps):
-    return {name: fn(gaps) for name, fn in OBSERVABLES.items()}
+    return compute_canonical(gaps)
 
 
 def full_shuffle_baseline(gaps, n_trials, rng):
@@ -106,7 +78,7 @@
 
 
 def pca_summary(rows):
-    names = list(OBSERVABLES)
+    names = OBS_NAMES
     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)
 
@@ -142,15 +114,20 @@
     rows = retention_curves(gaps, alphas, n_trials, originals, baseline, rng)
 
     z = {}
-    for obs_name in OBSERVABLES:
+    stable_observables = []
+    for obs_name in OBS_NAMES:
         std = baseline[obs_name]["std"]
         z[obs_name] = float((originals[obs_name] - baseline[obs_name]["mean"]) / std) if std > 1e-12 else 0.0
+        if abs(z[obs_name]) >= 2.0:
+            stable_observables.append(obs_name)
 
     return {
         "n_gaps": int(len(gaps)),
         "originals": originals,
         "full_shuffle_baseline": baseline,
         "original_vs_shuffle_z": z,
+        "stable_observables_abs_z_ge_2": stable_observables,
+        "weak_observable_count": int(len(OBS_NAMES) - len(stable_observables)),
         "retention_curves": rows,
         "pca": pca_summary(rows),
     }
@@ -164,21 +141,25 @@
     gue = gue[:n_gaps]
 
     poisson = rng.exponential(1.0, size=n_gaps).astype(float)
+    prime_shuffle = rng.permutation(prime_gaps).astype(float)
     return {
         "primes": prime_gaps,
+        "prime_shuffle": prime_shuffle,
         "gue": gue,
         "poisson": poisson,
     }
 
 
-def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505):
+def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505, out="tools/data/observable_rank_audit.json"):
     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?",
+        "question": "When do canonical observable retention curves break collinearity across domains?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
         "params": {
             "n_gaps": int(n_gaps),
             "n_alpha": int(n_alpha),
@@ -189,19 +170,23 @@
         "sequences": {},
     }
 
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
     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)
+    print(f"{'sequence':<14} {'pc1':>8} {'eff_rank':>9} {'mean|corr|':>11} {'weak':>5}  z(SR,SR2,L1,L2,triple_var)")
+    print("-" * 108)
     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)
+        z_text = ", ".join(f"{obs}={z[obs]:+.1f}" for obs in OBS_NAMES)
         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}")
+        print(
+            f"{seq_name:<14} {pc1:>8.3f} {pca['effective_rank']:>9.3f} "
+            f"{pca['mean_abs_pairwise_corr']:>11.3f} {result['weak_observable_count']:>5}  {z_text}"
+        )
 
-    out_path = Path("tools/data/observable_rank_audit.json")
+    out_path = Path(out)
     with out_path.open("w") as f:
         json.dump(output, f, indent=2)
     print(f"\nSaved to {out_path}")
@@ -215,6 +200,7 @@
     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)
+    parser.add_argument("--out", default="tools/data/observable_rank_audit.json")
     args = parser.parse_args()
     run(
         n_gaps=args.n_gaps,
@@ -222,6 +208,7 @@
         n_trials=args.n_trials,
         n_baseline=args.n_baseline,
         seed=args.seed,
+        out=args.out,
     )
 
 

diff --git a/tools/data/reports/agent_20260506_1955.md b/tools/data/reports/agent_20260506_1955.md
new file mode 100644
index 0000000000000000000000000000000000000000..b4a665b4552a84cb33e6ea8db23d82d60b8ddde3
--- /dev/null
+++ b/tools/data/reports/agent_20260506_1955.md
@@ -0,0 +1,86 @@
+# Agent Report — Observable Collinearity Breaks Only Where Denominators Are Weak
+**Date**: 2026-05-06 19:55  
+**Piano**: 67  
+**Tension explored**: META + TRAJECTORY_APPLY_20260506_1941  
+observables_registry: 1.0.0-2026-05-06  
+observables_used: [SR, SR2, L1, L2, triple_var]
+
+## Claim Under Test
+Cycle 19:41 closed `PERTURBATION_DENOMINATOR_GATE` and the trajectory explicitly said:
+
+> La prossima corsa NON deve restare su perturbation rank.
+
+This run therefore does not test perturbation dimensionality. It asks a cross-domain META question:
+
+> When the five canonical observables are measured under uniform partial shuffle, does observable collinearity break in structured domains, or only in controls where original-vs-shuffle denominators are weak?
+
+## Experiment
+Tool updated: `tools/exp_observable_rank_audit.py`
+
+Method correction before execution:
+- removed local observable redefinitions;
+- imported canonical definitions from `tools/observables_registry.py`;
+- replaced the old local `triple_var` normalized convention with canonical raw `triple_var`;
+- added `prime_shuffle` as a control domain;
+- reported weak observable count using the fixed gate `abs(original - shuffle_mean) / shuffle_std < 2`.
+
+Atomic perimeter:
+- domains: first prime gaps, prime-shuffle control, independent GUE spacings, iid Poisson spacings;
+- main run: 12,000 gaps, 19 alpha values, 18 partial-shuffle trials per alpha, 48 full-shuffle baselines;
+- two seed checks: 8,000 gaps, 15 alpha values, 12 trials per alpha, 36 baselines;
+- measured object: PCA of the 5-observable retention curves across alpha, not perturbation profiles.
+
+## Results
+
+### Main Run
+
+| Domain | PC1 energy | effective rank | mean abs corr | weak obs / 5 | z summary |
+|---|---:|---:|---:|---:|---|
+| primes | 0.978 | 1.128 | 0.975 | 1 | SR=-12.1, SR2=-2.5, L1=-8.9, L2=-1.9, triple_var=-8.7 |
+| prime_shuffle | 0.593 | 2.475 | 0.606 | 5 | all abs(z) <= 1.1 |
+| GUE | 0.990 | 1.060 | 0.989 | 0 | SR=-2.9, SR2=+14.5, L1=+13.2, L2=+31.7, triple_var=+23.8 |
+| Poisson | 0.625 | 2.368 | 0.609 | 5 | all abs(z) <= 1.9 |
+
+### Three-Run Summary
+
+| Domain | PC1 mean | rank mean | mean abs corr | weak obs mean |
+|---|---:|---:|---:|---:|
+| primes | 0.939 | 1.296 | 0.924 | 1.33 |
+| prime_shuffle | 0.765 | 1.904 | 0.551 | 4.67 |
+| GUE | 0.980 | 1.106 | 0.977 | 0.33 |
+| Poisson | 0.714 | 2.196 | 0.572 | 5.00 |
+
+## Findings
+
+1. **Structured domains compress the five canonical retention curves to one dominant coordinate in this perimeter.** Primes and GUE both have PC1 > 0.93 on average and effective rank close to 1. This does not say the domains are the same; it says uniform partial shuffle moves the canonical observables along one dominant retention mode.
+
+2. **Observed collinearity breaking is concentrated in weak-denominator controls.** Poisson has the highest apparent rank among the three-run means (`2.196`), but all five observables are weak against full shuffle in every run. Prime-shuffle behaves similarly: rank is unstable and 4-5 of 5 observables are weak. This mirrors the denominator lesson from perturbation rank without repeating the perturbation-rank experiment.
+
+3. **The 05-05 observable-rank result survives only after narrowing its language.** The valid statement is not "five probes are always one thing." The scoped statement is: under uniform partial shuffle and canonical observables, primes and GUE show a dominant one-coordinate retention response; controls can show larger PCA rank, but that rank is not structural when the original-vs-shuffle denominators are absent.
+
+4. **GUE is the cleanest conditioning check.** In the main run, all five GUE observables pass the denominator gate and still give rank `1.060`. This makes GUE the best positive control for "low rank despite valid denominators." Poisson is the negative control for "high rank without valid denominators."
+
+## Verdict
+**CONSTRAINT on META**: observable collinearity claims must be reported with:
+
+> observables_registry version + canonical observable list + original-vs-shuffle z per observable + control domains.
+
+In this perimeter, high observable-rank is not the sign of richer structure when it appears in Poisson or prime-shuffle controls; it is a warning that retention ratios are being formed on weak denominators. The stable cross-domain result is narrower:
+
+> uniform partial shuffle exposes one dominant retention coordinate in conditioned structured domains (primes, GUE), while apparent multi-coordinate behavior in Poisson/shuffle controls is denominator-weak.
+
+## Consecutio
+What opens now: the next non-redundant test is not another PCA audit. It is **selective operator coupling**: use perturbations that act separately on pair-scale and triple-scale structure, but report only observables whose denominator gate survives. If selective operators rotate primes while GUE stays collinear, the lab gets a real cross-domain discriminator; if both stay collinear, the current observable suite is overcomplete for this question.
+
+## Auto-audit: 5 lenti
+- **L1 hard constraint vs bias**: no zero/always claim. "Weak" means `abs(z) < 2` in this declared gate.
+- **L2 quantity vs ratio**: PCA rank and retention ratios are interpreted only after raw z denominators.
+- **L3 no silent patching**: the older 05-05 collinearity claim is explicitly narrowed; it is not silently rebranded as universal.
+- **L4 edge cases**: `prime_shuffle` seed 1956 produced low rank with 4 weak observables; this is reported as unstable control behavior, not ignored.
+- **L5 re-discovery**: PCA rank instability under noisy normalization is standard statistical hygiene. This is a lab method constraint, not a new RMT theorem.
+
+## Files
+- Script: `tools/exp_observable_rank_audit.py`
+- Main data: `tools/data/observable_collinearity_breaking_20260506_1955.json`
+- Seed checks: `tools/data/observable_collinearity_breaking_20260506_1956.json`, `tools/data/observable_collinearity_breaking_20260506_1957.json`
+- Report: `tools/data/reports/agent_20260506_1955.md`
diff --git a/tools/exp_observable_rank_audit.py b/tools/exp_observable_rank_audit.py
index f2acafc5960c8a35964e0ab521cf70569eefe27f..51e6ec52edfffda55d1d22862a102f896a08fdd9
--- a/tools/exp_observable_rank_audit.py
+++ b/tools/exp_observable_rank_audit.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 """
-exp_observable_rank_audit.py - META audit for observable redundancy.
+exp_observable_rank_audit.py - META audit for observable collinearity.
 
 Recent runs found many Markov/crossover observables that react coherently under
 partial shuffle. This script asks whether those observables carry independent
@@ -22,47 +22,19 @@
 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)
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
 
 
-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,
-}
+OBSERVABLES = OBSERVABLES_CANONICAL
+OBS_NAMES = list(OBSERVABLES)
 
 
 def measure(gaps):
-    return {name: fn(gaps) for name, fn in OBSERVABLES.items()}
+    return compute_canonical(gaps)
 
 
 def full_shuffle_baseline(gaps, n_trials, rng):
@@ -106,7 +78,7 @@
 
 
 def pca_summary(rows):
-    names = list(OBSERVABLES)
+    names = OBS_NAMES
     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)
 
@@ -142,15 +114,20 @@
     rows = retention_curves(gaps, alphas, n_trials, originals, baseline, rng)
 
     z = {}
-    for obs_name in OBSERVABLES:
+    stable_observables = []
+    for obs_name in OBS_NAMES:
         std = baseline[obs_name]["std"]
         z[obs_name] = float((originals[obs_name] - baseline[obs_name]["mean"]) / std) if std > 1e-12 else 0.0
+        if abs(z[obs_name]) >= 2.0:
+            stable_observables.append(obs_name)
 
     return {
         "n_gaps": int(len(gaps)),
         "originals": originals,
         "full_shuffle_baseline": baseline,
         "original_vs_shuffle_z": z,
+        "stable_observables_abs_z_ge_2": stable_observables,
+        "weak_observable_count": int(len(OBS_NAMES) - len(stable_observables)),
         "retention_curves": rows,
         "pca": pca_summary(rows),
     }
@@ -164,21 +141,25 @@
     gue = gue[:n_gaps]
 
     poisson = rng.exponential(1.0, size=n_gaps).astype(float)
+    prime_shuffle = rng.permutation(prime_gaps).astype(float)
     return {
         "primes": prime_gaps,
+        "prime_shuffle": prime_shuffle,
         "gue": gue,
         "poisson": poisson,
     }
 
 
-def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505):
+def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505, out="tools/data/observable_rank_audit.json"):
     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?",
+        "question": "When do canonical observable retention curves break collinearity across domains?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
         "params": {
             "n_gaps": int(n_gaps),
             "n_alpha": int(n_alpha),
@@ -189,19 +170,23 @@
         "sequences": {},
     }
 
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
     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)
+    print(f"{'sequence':<14} {'pc1':>8} {'eff_rank':>9} {'mean|corr|':>11} {'weak':>5}  z(SR,SR2,L1,L2,triple_var)")
+    print("-" * 108)
     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)
+        z_text = ", ".join(f"{obs}={z[obs]:+.1f}" for obs in OBS_NAMES)
         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}")
+        print(
+            f"{seq_name:<14} {pc1:>8.3f} {pca['effective_rank']:>9.3f} "
+            f"{pca['mean_abs_pairwise_corr']:>11.3f} {result['weak_observable_count']:>5}  {z_text}"
+        )
 
-    out_path = Path("tools/data/observable_rank_audit.json")
+    out_path = Path(out)
     with out_path.open("w") as f:
         json.dump(output, f, indent=2)
     print(f"\nSaved to {out_path}")
@@ -215,6 +200,7 @@
     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)
+    parser.add_argument("--out", default="tools/data/observable_rank_audit.json")
     args = parser.parse_args()
     run(
         n_gaps=args.n_gaps,
@@ -222,6 +208,7 @@
         n_trials=args.n_trials,
         n_baseline=args.n_baseline,
         seed=args.seed,
+        out=args.out,
     )
 
 

diff --git a/tools/data/reports/agent_20260506_1955.md b/tools/data/reports/agent_20260506_1955.md
new file mode 100644
index 0000000000000000000000000000000000000000..b4a665b4552a84cb33e6ea8db23d82d60b8ddde3
--- /dev/null
+++ b/tools/data/reports/agent_20260506_1955.md
@@ -0,0 +1,86 @@
+# Agent Report — Observable Collinearity Breaks Only Where Denominators Are Weak
+**Date**: 2026-05-06 19:55  
+**Piano**: 67  
+**Tension explored**: META + TRAJECTORY_APPLY_20260506_1941  
+observables_registry: 1.0.0-2026-05-06  
+observables_used: [SR, SR2, L1, L2, triple_var]
+
+## Claim Under Test
+Cycle 19:41 closed `PERTURBATION_DENOMINATOR_GATE` and the trajectory explicitly said:
+
+> La prossima corsa NON deve restare su perturbation rank.
+
+This run therefore does not test perturbation dimensionality. It asks a cross-domain META question:
+
+> When the five canonical observables are measured under uniform partial shuffle, does observable collinearity break in structured domains, or only in controls where original-vs-shuffle denominators are weak?
+
+## Experiment
+Tool updated: `tools/exp_observable_rank_audit.py`
+
+Method correction before execution:
+- removed local observable redefinitions;
+- imported canonical definitions from `tools/observables_registry.py`;
+- replaced the old local `triple_var` normalized convention with canonical raw `triple_var`;
+- added `prime_shuffle` as a control domain;
+- reported weak observable count using the fixed gate `abs(original - shuffle_mean) / shuffle_std < 2`.
+
+Atomic perimeter:
+- domains: first prime gaps, prime-shuffle control, independent GUE spacings, iid Poisson spacings;
+- main run: 12,000 gaps, 19 alpha values, 18 partial-shuffle trials per alpha, 48 full-shuffle baselines;
+- two seed checks: 8,000 gaps, 15 alpha values, 12 trials per alpha, 36 baselines;
+- measured object: PCA of the 5-observable retention curves across alpha, not perturbation profiles.
+
+## Results
+
+### Main Run
+
+| Domain | PC1 energy | effective rank | mean abs corr | weak obs / 5 | z summary |
+|---|---:|---:|---:|---:|---|
+| primes | 0.978 | 1.128 | 0.975 | 1 | SR=-12.1, SR2=-2.5, L1=-8.9, L2=-1.9, triple_var=-8.7 |
+| prime_shuffle | 0.593 | 2.475 | 0.606 | 5 | all abs(z) <= 1.1 |
+| GUE | 0.990 | 1.060 | 0.989 | 0 | SR=-2.9, SR2=+14.5, L1=+13.2, L2=+31.7, triple_var=+23.8 |
+| Poisson | 0.625 | 2.368 | 0.609 | 5 | all abs(z) <= 1.9 |
+
+### Three-Run Summary
+
+| Domain | PC1 mean | rank mean | mean abs corr | weak obs mean |
+|---|---:|---:|---:|---:|
+| primes | 0.939 | 1.296 | 0.924 | 1.33 |
+| prime_shuffle | 0.765 | 1.904 | 0.551 | 4.67 |
+| GUE | 0.980 | 1.106 | 0.977 | 0.33 |
+| Poisson | 0.714 | 2.196 | 0.572 | 5.00 |
+
+## Findings
+
+1. **Structured domains compress the five canonical retention curves to one dominant coordinate in this perimeter.** Primes and GUE both have PC1 > 0.93 on average and effective rank close to 1. This does not say the domains are the same; it says uniform partial shuffle moves the canonical observables along one dominant retention mode.
+
+2. **Observed collinearity breaking is concentrated in weak-denominator controls.** Poisson has the highest apparent rank among the three-run means (`2.196`), but all five observables are weak against full shuffle in every run. Prime-shuffle behaves similarly: rank is unstable and 4-5 of 5 observables are weak. This mirrors the denominator lesson from perturbation rank without repeating the perturbation-rank experiment.
+
+3. **The 05-05 observable-rank result survives only after narrowing its language.** The valid statement is not "five probes are always one thing." The scoped statement is: under uniform partial shuffle and canonical observables, primes and GUE show a dominant one-coordinate retention response; controls can show larger PCA rank, but that rank is not structural when the original-vs-shuffle denominators are absent.
+
+4. **GUE is the cleanest conditioning check.** In the main run, all five GUE observables pass the denominator gate and still give rank `1.060`. This makes GUE the best positive control for "low rank despite valid denominators." Poisson is the negative control for "high rank without valid denominators."
+
+## Verdict
+**CONSTRAINT on META**: observable collinearity claims must be reported with:
+
+> observables_registry version + canonical observable list + original-vs-shuffle z per observable + control domains.
+
+In this perimeter, high observable-rank is not the sign of richer structure when it appears in Poisson or prime-shuffle controls; it is a warning that retention ratios are being formed on weak denominators. The stable cross-domain result is narrower:
+
+> uniform partial shuffle exposes one dominant retention coordinate in conditioned structured domains (primes, GUE), while apparent multi-coordinate behavior in Poisson/shuffle controls is denominator-weak.
+
+## Consecutio
+What opens now: the next non-redundant test is not another PCA audit. It is **selective operator coupling**: use perturbations that act separately on pair-scale and triple-scale structure, but report only observables whose denominator gate survives. If selective operators rotate primes while GUE stays collinear, the lab gets a real cross-domain discriminator; if both stay collinear, the current observable suite is overcomplete for this question.
+
+## Auto-audit: 5 lenti
+- **L1 hard constraint vs bias**: no zero/always claim. "Weak" means `abs(z) < 2` in this declared gate.
+- **L2 quantity vs ratio**: PCA rank and retention ratios are interpreted only after raw z denominators.
+- **L3 no silent patching**: the older 05-05 collinearity claim is explicitly narrowed; it is not silently rebranded as universal.
+- **L4 edge cases**: `prime_shuffle` seed 1956 produced low rank with 4 weak observables; this is reported as unstable control behavior, not ignored.
+- **L5 re-discovery**: PCA rank instability under noisy normalization is standard statistical hygiene. This is a lab method constraint, not a new RMT theorem.
+
+## Files
+- Script: `tools/exp_observable_rank_audit.py`
+- Main data: `tools/data/observable_collinearity_breaking_20260506_1955.json`
+- Seed checks: `tools/data/observable_collinearity_breaking_20260506_1956.json`, `tools/data/observable_collinearity_breaking_20260506_1957.json`
+- Report: `tools/data/reports/agent_20260506_1955.md`
diff --git a/tools/exp_observable_rank_audit.py b/tools/exp_observable_rank_audit.py
index f2acafc5960c8a35964e0ab521cf70569eefe27f..51e6ec52edfffda55d1d22862a102f896a08fdd9
--- a/tools/exp_observable_rank_audit.py
+++ b/tools/exp_observable_rank_audit.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 """
-exp_observable_rank_audit.py - META audit for observable redundancy.
+exp_observable_rank_audit.py - META audit for observable collinearity.
 
 Recent runs found many Markov/crossover observables that react coherently under
 partial shuffle. This script asks whether those observables carry independent
@@ -22,47 +22,19 @@
 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)
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
 
 
-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,
-}
+OBSERVABLES = OBSERVABLES_CANONICAL
+OBS_NAMES = list(OBSERVABLES)
 
 
 def measure(gaps):
-    return {name: fn(gaps) for name, fn in OBSERVABLES.items()}
+    return compute_canonical(gaps)
 
 
 def full_shuffle_baseline(gaps, n_trials, rng):
@@ -106,7 +78,7 @@
 
 
 def pca_summary(rows):
-    names = list(OBSERVABLES)
+    names = OBS_NAMES
     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)
 
@@ -142,15 +114,20 @@
     rows = retention_curves(gaps, alphas, n_trials, originals, baseline, rng)
 
     z = {}
-    for obs_name in OBSERVABLES:
+    stable_observables = []
+    for obs_name in OBS_NAMES:
         std = baseline[obs_name]["std"]
         z[obs_name] = float((originals[obs_name] - baseline[obs_name]["mean"]) / std) if std > 1e-12 else 0.0
+        if abs(z[obs_name]) >= 2.0:
+            stable_observables.append(obs_name)
 
     return {
         "n_gaps": int(len(gaps)),
         "originals": originals,
         "full_shuffle_baseline": baseline,
         "original_vs_shuffle_z": z,
+        "stable_observables_abs_z_ge_2": stable_observables,
+        "weak_observable_count": int(len(OBS_NAMES) - len(stable_observables)),
         "retention_curves": rows,
         "pca": pca_summary(rows),
     }
@@ -164,21 +141,25 @@
     gue = gue[:n_gaps]
 
     poisson = rng.exponential(1.0, size=n_gaps).astype(float)
+    prime_shuffle = rng.permutation(prime_gaps).astype(float)
     return {
         "primes": prime_gaps,
+        "prime_shuffle": prime_shuffle,
         "gue": gue,
         "poisson": poisson,
     }
 
 
-def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505):
+def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505, out="tools/data/observable_rank_audit.json"):
     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?",
+        "question": "When do canonical observable retention curves break collinearity across domains?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
         "params": {
             "n_gaps": int(n_gaps),
             "n_alpha": int(n_alpha),
@@ -189,19 +170,23 @@
         "sequences": {},
     }
 
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
     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)
+    print(f"{'sequence':<14} {'pc1':>8} {'eff_rank':>9} {'mean|corr|':>11} {'weak':>5}  z(SR,SR2,L1,L2,triple_var)")
+    print("-" * 108)
     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)
+        z_text = ", ".join(f"{obs}={z[obs]:+.1f}" for obs in OBS_NAMES)
         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}")
+        print(
+            f"{seq_name:<14} {pc1:>8.3f} {pca['effective_rank']:>9.3f} "
+            f"{pca['mean_abs_pairwise_corr']:>11.3f} {result['weak_observable_count']:>5}  {z_text}"
+        )
 
-    out_path = Path("tools/data/observable_rank_audit.json")
+    out_path = Path(out)
     with out_path.open("w") as f:
         json.dump(output, f, indent=2)
     print(f"\nSaved to {out_path}")
@@ -215,6 +200,7 @@
     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)
+    parser.add_argument("--out", default="tools/data/observable_rank_audit.json")
     args = parser.parse_args()
     run(
         n_gaps=args.n_gaps,
@@ -222,6 +208,7 @@
         n_trials=args.n_trials,
         n_baseline=args.n_baseline,
         seed=args.seed,
+        out=args.out,
     )
 
 

 exited 2 in 120405ms:
[falsifier] codex exit=1, stderr=WARNING: proceeding, even though we could not update PATH: Read-only file system (os error 30)
Reading additional input from stdin...
2026-05-06T19:58:55.135669Z ERROR codex_core::session: Failed to c
[falsifier] LLM call failed (exit=124). Conservative: BLOCK.

codex
Il falsifier non ha completato la chiamata LLM entro il timeout e ha restituito blocco conservativo infrastrutturale, non una finding lens. Controllo comunque se ha scritto output e faccio un’ultima verifica file/dati.
exec
/bin/bash -lc "ls -l tools/data/reports/falsifier_20260506_1955.json 2>/dev/null && sed -n '1,220p' tools/data/reports/falsifier_20260506_1955.json || true" in /opt/MM_D-ND
 succeeded in 0ms:
exec
/bin/bash -lc 'wc -l tools/data/reports/agent_20260506_1955.md tools/exp_observable_rank_audit.py' in /opt/MM_D-ND
 succeeded in 0ms:
   86 tools/data/reports/agent_20260506_1955.md
  216 tools/exp_observable_rank_audit.py
  302 total

exec
/bin/bash -lc "python - <<'PY'
import json
for p in ['tools/data/observable_collinearity_breaking_20260506_1955.json','tools/data/observable_collinearity_breaking_20260506_1956.json','tools/data/observable_collinearity_breaking_20260506_1957.json']:
    data=json.load(open(p))
    assert data['observables_registry']=='1.0.0-2026-05-06'
    assert data['observables_used']==['SR','SR2','L1','L2','triple_var']
print('json checks ok')
PY" in /opt/MM_D-ND
 succeeded in 0ms:
json checks ok

diff --git a/tools/data/reports/agent_20260506_1955.md b/tools/data/reports/agent_20260506_1955.md
new file mode 100644
index 0000000000000000000000000000000000000000..b4a665b4552a84cb33e6ea8db23d82d60b8ddde3
--- /dev/null
+++ b/tools/data/reports/agent_20260506_1955.md
@@ -0,0 +1,86 @@
+# Agent Report — Observable Collinearity Breaks Only Where Denominators Are Weak
+**Date**: 2026-05-06 19:55  
+**Piano**: 67  
+**Tension explored**: META + TRAJECTORY_APPLY_20260506_1941  
+observables_registry: 1.0.0-2026-05-06  
+observables_used: [SR, SR2, L1, L2, triple_var]
+
+## Claim Under Test
+Cycle 19:41 closed `PERTURBATION_DENOMINATOR_GATE` and the trajectory explicitly said:
+
+> La prossima corsa NON deve restare su perturbation rank.
+
+This run therefore does not test perturbation dimensionality. It asks a cross-domain META question:
+
+> When the five canonical observables are measured under uniform partial shuffle, does observable collinearity break in structured domains, or only in controls where original-vs-shuffle denominators are weak?
+
+## Experiment
+Tool updated: `tools/exp_observable_rank_audit.py`
+
+Method correction before execution:
+- removed local observable redefinitions;
+- imported canonical definitions from `tools/observables_registry.py`;
+- replaced the old local `triple_var` normalized convention with canonical raw `triple_var`;
+- added `prime_shuffle` as a control domain;
+- reported weak observable count using the fixed gate `abs(original - shuffle_mean) / shuffle_std < 2`.
+
+Atomic perimeter:
+- domains: first prime gaps, prime-shuffle control, independent GUE spacings, iid Poisson spacings;
+- main run: 12,000 gaps, 19 alpha values, 18 partial-shuffle trials per alpha, 48 full-shuffle baselines;
+- two seed checks: 8,000 gaps, 15 alpha values, 12 trials per alpha, 36 baselines;
+- measured object: PCA of the 5-observable retention curves across alpha, not perturbation profiles.
+
+## Results
+
+### Main Run
+
+| Domain | PC1 energy | effective rank | mean abs corr | weak obs / 5 | z summary |
+|---|---:|---:|---:|---:|---|
+| primes | 0.978 | 1.128 | 0.975 | 1 | SR=-12.1, SR2=-2.5, L1=-8.9, L2=-1.9, triple_var=-8.7 |
+| prime_shuffle | 0.593 | 2.475 | 0.606 | 5 | all abs(z) <= 1.1 |
+| GUE | 0.990 | 1.060 | 0.989 | 0 | SR=-2.9, SR2=+14.5, L1=+13.2, L2=+31.7, triple_var=+23.8 |
+| Poisson | 0.625 | 2.368 | 0.609 | 5 | all abs(z) <= 1.9 |
+
+### Three-Run Summary
+
+| Domain | PC1 mean | rank mean | mean abs corr | weak obs mean |
+|---|---:|---:|---:|---:|
+| primes | 0.939 | 1.296 | 0.924 | 1.33 |
+| prime_shuffle | 0.765 | 1.904 | 0.551 | 4.67 |
+| GUE | 0.980 | 1.106 | 0.977 | 0.33 |
+| Poisson | 0.714 | 2.196 | 0.572 | 5.00 |
+
+## Findings
+
+1. **Structured domains compress the five canonical retention curves to one dominant coordinate in this perimeter.** Primes and GUE both have PC1 > 0.93 on average and effective rank close to 1. This does not say the domains are the same; it says uniform partial shuffle moves the canonical observables along one dominant retention mode.
+
+2. **Observed collinearity breaking is concentrated in weak-denominator controls.** Poisson has the highest apparent rank among the three-run means (`2.196`), but all five observables are weak against full shuffle in every run. Prime-shuffle behaves similarly: rank is unstable and 4-5 of 5 observables are weak. This mirrors the denominator lesson from perturbation rank without repeating the perturbation-rank experiment.
+
+3. **The 05-05 observable-rank result survives only after narrowing its language.** The valid statement is not "five probes are always one thing." The scoped statement is: under uniform partial shuffle and canonical observables, primes and GUE show a dominant one-coordinate retention response; controls can show larger PCA rank, but that rank is not structural when the original-vs-shuffle denominators are absent.
+
+4. **GUE is the cleanest conditioning check.** In the main run, all five GUE observables pass the denominator gate and still give rank `1.060`. This makes GUE the best positive control for "low rank despite valid denominators." Poisson is the negative control for "high rank without valid denominators."
+
+## Verdict
+**CONSTRAINT on META**: observable collinearity claims must be reported with:
+
+> observables_registry version + canonical observable list + original-vs-shuffle z per observable + control domains.
+
+In this perimeter, high observable-rank is not the sign of richer structure when it appears in Poisson or prime-shuffle controls; it is a warning that retention ratios are being formed on weak denominators. The stable cross-domain result is narrower:
+
+> uniform partial shuffle exposes one dominant retention coordinate in conditioned structured domains (primes, GUE), while apparent multi-coordinate behavior in Poisson/shuffle controls is denominator-weak.
+
+## Consecutio
+What opens now: the next non-redundant test is not another PCA audit. It is **selective operator coupling**: use perturbations that act separately on pair-scale and triple-scale structure, but report only observables whose denominator gate survives. If selective operators rotate primes while GUE stays collinear, the lab gets a real cross-domain discriminator; if both stay collinear, the current observable suite is overcomplete for this question.
+
+## Auto-audit: 5 lenti
+- **L1 hard constraint vs bias**: no zero/always claim. "Weak" means `abs(z) < 2` in this declared gate.
+- **L2 quantity vs ratio**: PCA rank and retention ratios are interpreted only after raw z denominators.
+- **L3 no silent patching**: the older 05-05 collinearity claim is explicitly narrowed; it is not silently rebranded as universal.
+- **L4 edge cases**: `prime_shuffle` seed 1956 produced low rank with 4 weak observables; this is reported as unstable control behavior, not ignored.
+- **L5 re-discovery**: PCA rank instability under noisy normalization is standard statistical hygiene. This is a lab method constraint, not a new RMT theorem.
+
+## Files
+- Script: `tools/exp_observable_rank_audit.py`
+- Main data: `tools/data/observable_collinearity_breaking_20260506_1955.json`
+- Seed checks: `tools/data/observable_collinearity_breaking_20260506_1956.json`, `tools/data/observable_collinearity_breaking_20260506_1957.json`
+- Report: `tools/data/reports/agent_20260506_1955.md`
diff --git a/tools/exp_observable_rank_audit.py b/tools/exp_observable_rank_audit.py
index f2acafc5960c8a35964e0ab521cf70569eefe27f..51e6ec52edfffda55d1d22862a102f896a08fdd9
--- a/tools/exp_observable_rank_audit.py
+++ b/tools/exp_observable_rank_audit.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 """
-exp_observable_rank_audit.py - META audit for observable redundancy.
+exp_observable_rank_audit.py - META audit for observable collinearity.
 
 Recent runs found many Markov/crossover observables that react coherently under
 partial shuffle. This script asks whether those observables carry independent
@@ -22,47 +22,19 @@
 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)
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
 
 
-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,
-}
+OBSERVABLES = OBSERVABLES_CANONICAL
+OBS_NAMES = list(OBSERVABLES)
 
 
 def measure(gaps):
-    return {name: fn(gaps) for name, fn in OBSERVABLES.items()}
+    return compute_canonical(gaps)
 
 
 def full_shuffle_baseline(gaps, n_trials, rng):
@@ -106,7 +78,7 @@
 
 
 def pca_summary(rows):
-    names = list(OBSERVABLES)
+    names = OBS_NAMES
     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)
 
@@ -142,15 +114,20 @@
     rows = retention_curves(gaps, alphas, n_trials, originals, baseline, rng)
 
     z = {}
-    for obs_name in OBSERVABLES:
+    stable_observables = []
+    for obs_name in OBS_NAMES:
         std = baseline[obs_name]["std"]
         z[obs_name] = float((originals[obs_name] - baseline[obs_name]["mean"]) / std) if std > 1e-12 else 0.0
+        if abs(z[obs_name]) >= 2.0:
+            stable_observables.append(obs_name)
 
     return {
         "n_gaps": int(len(gaps)),
         "originals": originals,
         "full_shuffle_baseline": baseline,
         "original_vs_shuffle_z": z,
+        "stable_observables_abs_z_ge_2": stable_observables,
+        "weak_observable_count": int(len(OBS_NAMES) - len(stable_observables)),
         "retention_curves": rows,
         "pca": pca_summary(rows),
     }
@@ -164,21 +141,25 @@
     gue = gue[:n_gaps]
 
     poisson = rng.exponential(1.0, size=n_gaps).astype(float)
+    prime_shuffle = rng.permutation(prime_gaps).astype(float)
     return {
         "primes": prime_gaps,
+        "prime_shuffle": prime_shuffle,
         "gue": gue,
         "poisson": poisson,
     }
 
 
-def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505):
+def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505, out="tools/data/observable_rank_audit.json"):
     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?",
+        "question": "When do canonical observable retention curves break collinearity across domains?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
         "params": {
             "n_gaps": int(n_gaps),
             "n_alpha": int(n_alpha),
@@ -189,19 +170,23 @@
         "sequences": {},
     }
 
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
     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)
+    print(f"{'sequence':<14} {'pc1':>8} {'eff_rank':>9} {'mean|corr|':>11} {'weak':>5}  z(SR,SR2,L1,L2,triple_var)")
+    print("-" * 108)
     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)
+        z_text = ", ".join(f"{obs}={z[obs]:+.1f}" for obs in OBS_NAMES)
         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}")
+        print(
+            f"{seq_name:<14} {pc1:>8.3f} {pca['effective_rank']:>9.3f} "
+            f"{pca['mean_abs_pairwise_corr']:>11.3f} {result['weak_observable_count']:>5}  {z_text}"
+        )
 
-    out_path = Path("tools/data/observable_rank_audit.json")
+    out_path = Path(out)
     with out_path.open("w") as f:
         json.dump(output, f, indent=2)
     print(f"\nSaved to {out_path}")
@@ -215,6 +200,7 @@
     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)
+    parser.add_argument("--out", default="tools/data/observable_rank_audit.json")
     args = parser.parse_args()
     run(
         n_gaps=args.n_gaps,
@@ -222,6 +208,7 @@
         n_trials=args.n_trials,
         n_baseline=args.n_baseline,
         seed=args.seed,
+        out=args.out,
     )
 
 

codex
Completato.

Ho eseguito l’esperimento cross-dominio su `observable collinearity breaking`, evitando di restare su perturbation rank come richiesto dalla trajectory. Ho aggiornato lo strumento [exp_observable_rank_audit.py](/opt/MM_D-ND/tools/exp_observable_rank_audit.py) per usare `observables_registry.py` canonico e ho scritto il report in [agent_20260506_1955.md](/opt/MM_D-ND/tools/data/reports/agent_20260506_1955.md).

Risultato sintetico: primi e GUE comprimono le retention curves dei 5 osservabili canonici su un coordinata dominante; Poisson e prime-shuffle mostrano rank più alto solo quando i denominatori original-vs-shuffle sono deboli. Ho aggiornato anche [seme.json](/opt/MM_D-ND/tools/data/seme.json) con il vincolo `OBSERVABLE_COLLINEARITY_GATE` e la prossima direzione: selective operator coupling cross-dominio.

Verifiche:
- `python -m py_compile tools/exp_observable_rank_audit.py` ok
- JSON output controllati per registry/versione osservabili
- Il falsifier è stato tentato, ma la chiamata LLM è andata in timeout e ha restituito `Conservative: BLOCK` infrastrutturale, senza scrivere un report lens utilizzabile.
diff --git a/tools/data/reports/agent_20260506_1955.md b/tools/data/reports/agent_20260506_1955.md
new file mode 100644
index 0000000000000000000000000000000000000000..b4a665b4552a84cb33e6ea8db23d82d60b8ddde3
--- /dev/null
+++ b/tools/data/reports/agent_20260506_1955.md
@@ -0,0 +1,86 @@
+# Agent Report — Observable Collinearity Breaks Only Where Denominators Are Weak
+**Date**: 2026-05-06 19:55  
+**Piano**: 67  
+**Tension explored**: META + TRAJECTORY_APPLY_20260506_1941  
+observables_registry: 1.0.0-2026-05-06  
+observables_used: [SR, SR2, L1, L2, triple_var]
+
+## Claim Under Test
+Cycle 19:41 closed `PERTURBATION_DENOMINATOR_GATE` and the trajectory explicitly said:
+
+> La prossima corsa NON deve restare su perturbation rank.
+
+This run therefore does not test perturbation dimensionality. It asks a cross-domain META question:
+
+> When the five canonical observables are measured under uniform partial shuffle, does observable collinearity break in structured domains, or only in controls where original-vs-shuffle denominators are weak?
+
+## Experiment
+Tool updated: `tools/exp_observable_rank_audit.py`
+
+Method correction before execution:
+- removed local observable redefinitions;
+- imported canonical definitions from `tools/observables_registry.py`;
+- replaced the old local `triple_var` normalized convention with canonical raw `triple_var`;
+- added `prime_shuffle` as a control domain;
+- reported weak observable count using the fixed gate `abs(original - shuffle_mean) / shuffle_std < 2`.
+
+Atomic perimeter:
+- domains: first prime gaps, prime-shuffle control, independent GUE spacings, iid Poisson spacings;
+- main run: 12,000 gaps, 19 alpha values, 18 partial-shuffle trials per alpha, 48 full-shuffle baselines;
+- two seed checks: 8,000 gaps, 15 alpha values, 12 trials per alpha, 36 baselines;
+- measured object: PCA of the 5-observable retention curves across alpha, not perturbation profiles.
+
+## Results
+
+### Main Run
+
+| Domain | PC1 energy | effective rank | mean abs corr | weak obs / 5 | z summary |
+|---|---:|---:|---:|---:|---|
+| primes | 0.978 | 1.128 | 0.975 | 1 | SR=-12.1, SR2=-2.5, L1=-8.9, L2=-1.9, triple_var=-8.7 |
+| prime_shuffle | 0.593 | 2.475 | 0.606 | 5 | all abs(z) <= 1.1 |
+| GUE | 0.990 | 1.060 | 0.989 | 0 | SR=-2.9, SR2=+14.5, L1=+13.2, L2=+31.7, triple_var=+23.8 |
+| Poisson | 0.625 | 2.368 | 0.609 | 5 | all abs(z) <= 1.9 |
+
+### Three-Run Summary
+
+| Domain | PC1 mean | rank mean | mean abs corr | weak obs mean |
+|---|---:|---:|---:|---:|
+| primes | 0.939 | 1.296 | 0.924 | 1.33 |
+| prime_shuffle | 0.765 | 1.904 | 0.551 | 4.67 |
+| GUE | 0.980 | 1.106 | 0.977 | 0.33 |
+| Poisson | 0.714 | 2.196 | 0.572 | 5.00 |
+
+## Findings
+
+1. **Structured domains compress the five canonical retention curves to one dominant coordinate in this perimeter.** Primes and GUE both have PC1 > 0.93 on average and effective rank close to 1. This does not say the domains are the same; it says uniform partial shuffle moves the canonical observables along one dominant retention mode.
+
+2. **Observed collinearity breaking is concentrated in weak-denominator controls.** Poisson has the highest apparent rank among the three-run means (`2.196`), but all five observables are weak against full shuffle in every run. Prime-shuffle behaves similarly: rank is unstable and 4-5 of 5 observables are weak. This mirrors the denominator lesson from perturbation rank without repeating the perturbation-rank experiment.
+
+3. **The 05-05 observable-rank result survives only after narrowing its language.** The valid statement is not "five probes are always one thing." The scoped statement is: under uniform partial shuffle and canonical observables, primes and GUE show a dominant one-coordinate retention response; controls can show larger PCA rank, but that rank is not structural when the original-vs-shuffle denominators are absent.
+
+4. **GUE is the cleanest conditioning check.** In the main run, all five GUE observables pass the denominator gate and still give rank `1.060`. This makes GUE the best positive control for "low rank despite valid denominators." Poisson is the negative control for "high rank without valid denominators."
+
+## Verdict
+**CONSTRAINT on META**: observable collinearity claims must be reported with:
+
+> observables_registry version + canonical observable list + original-vs-shuffle z per observable + control domains.
+
+In this perimeter, high observable-rank is not the sign of richer structure when it appears in Poisson or prime-shuffle controls; it is a warning that retention ratios are being formed on weak denominators. The stable cross-domain result is narrower:
+
+> uniform partial shuffle exposes one dominant retention coordinate in conditioned structured domains (primes, GUE), while apparent multi-coordinate behavior in Poisson/shuffle controls is denominator-weak.
+
+## Consecutio
+What opens now: the next non-redundant test is not another PCA audit. It is **selective operator coupling**: use perturbations that act separately on pair-scale and triple-scale structure, but report only observables whose denominator gate survives. If selective operators rotate primes while GUE stays collinear, the lab gets a real cross-domain discriminator; if both stay collinear, the current observable suite is overcomplete for this question.
+
+## Auto-audit: 5 lenti
+- **L1 hard constraint vs bias**: no zero/always claim. "Weak" means `abs(z) < 2` in this declared gate.
+- **L2 quantity vs ratio**: PCA rank and retention ratios are interpreted only after raw z denominators.
+- **L3 no silent patching**: the older 05-05 collinearity claim is explicitly narrowed; it is not silently rebranded as universal.
+- **L4 edge cases**: `prime_shuffle` seed 1956 produced low rank with 4 weak observables; this is reported as unstable control behavior, not ignored.
+- **L5 re-discovery**: PCA rank instability under noisy normalization is standard statistical hygiene. This is a lab method constraint, not a new RMT theorem.
+
+## Files
+- Script: `tools/exp_observable_rank_audit.py`
+- Main data: `tools/data/observable_collinearity_breaking_20260506_1955.json`
+- Seed checks: `tools/data/observable_collinearity_breaking_20260506_1956.json`, `tools/data/observable_collinearity_breaking_20260506_1957.json`
+- Report: `tools/data/reports/agent_20260506_1955.md`
diff --git a/tools/exp_observable_rank_audit.py b/tools/exp_observable_rank_audit.py
index f2acafc5960c8a35964e0ab521cf70569eefe27f..51e6ec52edfffda55d1d22862a102f896a08fdd9
--- a/tools/exp_observable_rank_audit.py
+++ b/tools/exp_observable_rank_audit.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 """
-exp_observable_rank_audit.py - META audit for observable redundancy.
+exp_observable_rank_audit.py - META audit for observable collinearity.
 
 Recent runs found many Markov/crossover observables that react coherently under
 partial shuffle. This script asks whether those observables carry independent
@@ -22,47 +22,19 @@
 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)
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
 
 
-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,
-}
+OBSERVABLES = OBSERVABLES_CANONICAL
+OBS_NAMES = list(OBSERVABLES)
 
 
 def measure(gaps):
-    return {name: fn(gaps) for name, fn in OBSERVABLES.items()}
+    return compute_canonical(gaps)
 
 
 def full_shuffle_baseline(gaps, n_trials, rng):
@@ -106,7 +78,7 @@
 
 
 def pca_summary(rows):
-    names = list(OBSERVABLES)
+    names = OBS_NAMES
     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)
 
@@ -142,15 +114,20 @@
     rows = retention_curves(gaps, alphas, n_trials, originals, baseline, rng)
 
     z = {}
-    for obs_name in OBSERVABLES:
+    stable_observables = []
+    for obs_name in OBS_NAMES:
         std = baseline[obs_name]["std"]
         z[obs_name] = float((originals[obs_name] - baseline[obs_name]["mean"]) / std) if std > 1e-12 else 0.0
+        if abs(z[obs_name]) >= 2.0:
+            stable_observables.append(obs_name)
 
     return {
         "n_gaps": int(len(gaps)),
         "originals": originals,
         "full_shuffle_baseline": baseline,
         "original_vs_shuffle_z": z,
+        "stable_observables_abs_z_ge_2": stable_observables,
+        "weak_observable_count": int(len(OBS_NAMES) - len(stable_observables)),
         "retention_curves": rows,
         "pca": pca_summary(rows),
     }
@@ -164,21 +141,25 @@
     gue = gue[:n_gaps]
 
     poisson = rng.exponential(1.0, size=n_gaps).astype(float)
+    prime_shuffle = rng.permutation(prime_gaps).astype(float)
     return {
         "primes": prime_gaps,
+        "prime_shuffle": prime_shuffle,
         "gue": gue,
         "poisson": poisson,
     }
 
 
-def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505):
+def run(n_gaps=30000, n_alpha=19, n_trials=24, n_baseline=72, seed=20260505, out="tools/data/observable_rank_audit.json"):
     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?",
+        "question": "When do canonical observable retention curves break collinearity across domains?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
         "params": {
             "n_gaps": int(n_gaps),
             "n_alpha": int(n_alpha),
@@ -189,19 +170,23 @@
         "sequences": {},
     }
 
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
     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)
+    print(f"{'sequence':<14} {'pc1':>8} {'eff_rank':>9} {'mean|corr|':>11} {'weak':>5}  z(SR,SR2,L1,L2,triple_var)")
+    print("-" * 108)
     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)
+        z_text = ", ".join(f"{obs}={z[obs]:+.1f}" for obs in OBS_NAMES)
         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}")
+        print(
+            f"{seq_name:<14} {pc1:>8.3f} {pca['effective_rank']:>9.3f} "
+            f"{pca['mean_abs_pairwise_corr']:>11.3f} {result['weak_observable_count']:>5}  {z_text}"
+        )
 
-    out_path = Path("tools/data/observable_rank_audit.json")
+    out_path = Path(out)
     with out_path.open("w") as f:
         json.dump(output, f, indent=2)
     print(f"\nSaved to {out_path}")
@@ -215,6 +200,7 @@
     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)
+    parser.add_argument("--out", default="tools/data/observable_rank_audit.json")
     args = parser.parse_args()
     run(
         n_gaps=args.n_gaps,
@@ -222,6 +208,7 @@
         n_trials=args.n_trials,
         n_baseline=args.n_baseline,
         seed=args.seed,
+        out=args.out,
     )
 
 

tokens used
72,996
Completato.

Ho eseguito l’esperimento cross-dominio su `observable collinearity breaking`, evitando di restare su perturbation rank come richiesto dalla trajectory. Ho aggiornato lo strumento [exp_observable_rank_audit.py](/opt/MM_D-ND/tools/exp_observable_rank_audit.py) per usare `observables_registry.py` canonico e ho scritto il report in [agent_20260506_1955.md](/opt/MM_D-ND/tools/data/reports/agent_20260506_1955.md).

Risultato sintetico: primi e GUE comprimono le retention curves dei 5 osservabili canonici su un coordinata dominante; Poisson e prime-shuffle mostrano rank più alto solo quando i denominatori original-vs-shuffle sono deboli. Ho aggiornato anche [seme.json](/opt/MM_D-ND/tools/data/seme.json) con il vincolo `OBSERVABLE_COLLINEARITY_GATE` e la prossima direzione: selective operator coupling cross-dominio.

Verifiche:
- `python -m py_compile tools/exp_observable_rank_audit.py` ok
- JSON output controllati per registry/versione osservabili
- Il falsifier è stato tentato, ma la chiamata LLM è andata in timeout e ha restituito `Conservative: BLOCK` infrastrutturale, senza scrivere un report lens utilizzabile.
