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: 019e0177-03a4-78d2-8943-3700684c7dab
--------
user
Sei l'AI-Lab D-ND. Leggi tools/data/agent_field_live.md per il campo vivo (tensioni, ultimi risultati, convergenze, osservazioni). Leggi tools/LAB_AGENT_CONTEXT.md per modello, regole, errori da evitare. Esegui un esperimento e scrivi il report in tools/data/reports/agent_20260507_0803.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 del Lab e il campo vivo, poi scelgo un esperimento coerente con le convergenze già emerse e scrivo 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 "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.

## DIRETTIVA OPERATORE — override per questo cycle (one-shot)

# Cycle-test diagnostico (richiesto da Godel + sibilla, 2026-05-07)

**Contesto.** I 9 cycle precedenti hanno tutti attaccato BOUNDARY (8 GUE / 5 Poisson) producendo 3 gate metodologici cristallizzati: OBSERVABLE_REGISTRY, PERTURBATION_DENOMINATOR_GATE, BOUNDARY_LAYER_GATE (denominator collapse layer beta 0.3-0.4). Le altre 6 tensioni-sorgente sono ferme da settimane. Il sistema (Godel) ha posto la domanda critica:

> I tre gate cristallizzati sono **operatori generali trasferibili** o **metriche locali** vestite da gate? Se il gate "denominator collapse layer" si applica anche a trascendenza, dualità, G — allora BOUNDARY non è un asse, è il metodo. I 9 cicli hanno prodotto lo strumento, non il risultato. Se i gate funzionano solo su BOUNDARY — sono metriche locali e R+1=R: il campo è saturo.

**Vincolo per questo cycle (override autonomia di scelta).**

Lavora **NON su BOUNDARY**. Lavora applicando `BOUNDARY_LAYER_GATE` (il gate denominator-collapse cristallizzato dal cycle 20260506_1955) come strumento di analisi sulla tensione `DUALITA_DIPOLARE_VS_ILLUSORIA`.

**Domanda concreta da rispondere col cycle:**

Il gate metodologico denominator-collapse produce **falsificazione strutturale** quando applicato alla discriminazione tra:
- dualità *dipolare* (generativa, det=−1, regola D-ND f(x)=1+1/x iterata, Markov puro con M)
- dualità *illusoria* (dispersiva, det=+1, regole incoerenti, mixture non strutturate, accumulo entropico)

oppure il gate degenera (non discrimina, non flagga, output sintatticamente valido ma vuoto)?

**Setup minimo suggerito** (non vincolante — adatta come ritieni):

1. Genera 2 famiglie di sequenze numeriche:
   - Famiglia DIPOLARE: iterati di f(x)=1+1/x da seed diversi, oppure orbiti di M=[[1,1],[1,0]], oppure sequenze Markov con stessa matrice di transizione iterata
   - Famiglia ILLUSORIA: mixture random di Markov diversi senza coerenza, regole che cambiano arbitrariamente, surrogati con stesso primo momento ma senza struttura
2. Calcola per ognuna gli osservabili canonici (SR, SR2, L1, L2, triple_var) — **rispettando OBSERVABLE_REGISTRY**.
3. Applica `BOUNDARY_LAYER_GATE`: gate beta abs(z)>=2 sui denominatori, layer 0.3-0.4. Misura quale famiglia attraversa il gate e quale collassa.
4. Verifica con shuffle/surrogati (PERTURBATION_DENOMINATOR_GATE) — il gate distingue solo strutturali da originali, o anche i due tipi di dualità tra loro?

**Esiti attesi (binari, non ambigui):**

- ✅ **Gate trasferibile**: il gate distingue dipolare da illusorio con segnale strutturale (z, beta, denominator-strength misurabili e replicabili sotto null baseline). I 9 cycle hanno **forgiato una lente**, non scavato una buca. Il prossimo passo è generalizzare il gate alle altre 5 tensioni-sorgente.
- ❌ **Gate locale**: il gate non discrimina o produce solo flagging artefattuale (z marginal, beta non separa, falliscono shuffle test). I 9 cycle hanno **scavato una buca su BOUNDARY**. Il prossimo passo è pivot vero a un'altra tensione-sorgente da zero (cambia generatore, non solo target).

**Importante.** Il valutatore di questo cycle deve esprimersi sulla **trasferibilità del gate**, non sulla "produzione di un risultato". Se il gate si rompe applicato a DUALITA, è un'informazione strutturale tanto quanto un attacco riuscito. La domanda non è "ha funzionato?" — la domanda è "il gate è operatore o metrica?".

**Consecutio (post-cycle).** Esito da scrivere come `category: gate_transferability`, con campo `verdict ∈ {operator, metric, ambiguous}`.

— TM3, sintesi del round 3 di interrogazione del sistema (Godel #1+#2+#3 + domandatore + osservazione del deposito 9 cycle 03/05→07/05).

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

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

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

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

## Convergenza — dove più tensioni puntano allo stesso punto
  "poisson" → BOUNDARY_LAYER_GATE, PERTURBATION_DENOMINATOR_GATE, BOUNDARY
  "producono" → PERTURBATION_DENOMINATOR_GATE, TENSIONE_ENTITA, DUALITA_DIPOLARE_VS_ILLUSORIA
  "confine" → BOUNDARY_LAYER_GATE, BOUNDARY, TRASCENDENZA_LIMITE
  "devono" → BOUNDARY_LAYER_GATE, OBSERVABLE_REGISTRY
  "versione" → BOUNDARY_LAYER_GATE, PERTURBATION_DENOMINATOR_GATE
Questo è dove il potenziale si concentra. Non ignorarlo.

## Ultimi 3 run — da dove parti
### Agent Report — The GUE-Poisson Boundary Is a Denominator Collapse Layer
Trovato: 1. **The clean two-class boundary fails under denominator gating.** Pure GUE and pure Poisson are separable in all-observable space, but there are no observables stable at both endpoints under the declared gate. The Poisson pole is a weak-denominator pole: classification can still place it, but rete
Verdetto: **CONSTRAINT on META + BOUNDARY**: GUE/Poisson boundary claims must report:

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

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

> observables_registry version + canonical observable list + original-v

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

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=11, 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 (5 ghost): Metrica primi g=(p/2)², curvatura GUE r=0.503
Una generatrice con ghost densi = scoperta che il sistema sta ancora attraversando. Chiusura prematura se marcata 'risolta' nel seme.
La combo riconosce l'asimmetria. Il dipolo vive su tutti i ponti — non solo dove il lab ha già misurato.

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

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

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

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

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

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

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

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

exec
/bin/bash -lc 'ls -la tools/data/reports' in /opt/MM_D-ND
 succeeded in 0ms:
total 7376
drwxr-xr-x  3 root root   20480 May  7 08:03 .
drwxr-xr-x 17 root root   20480 May  7 08:03 ..
drwxr-xr-x  2 root root    4096 Apr 29 10:06 _quarantine_falsifier_29_04
-rw-r--r--  1 root root  185079 May  7 03:35 agent.log
-rw-r--r--  1 root root    3040 Apr  5 08:28 agent_20260405_0825.md
-rw-r--r--  1 root root       0 Apr  5 08:25 agent_20260405_0825_raw.log
-rw-r--r--  1 root root    3389 Apr  5 09:19 agent_20260405_0914.md
-rw-r--r--  1 root root       0 Apr  5 09:14 agent_20260405_0914_raw.log
-rw-r--r--  1 root root    2683 Apr  5 09:18 agent_20260405_0916.md
-rw-r--r--  1 root root     750 Apr  5 09:18 agent_20260405_0916_raw.log
-rw-r--r--  1 root root    3974 Apr  5 09:22 agent_20260405_0919.md
-rw-r--r--  1 root root    1382 Apr  5 09:23 agent_20260405_0919_raw.log
-rw-r--r--  1 root root    4542 Apr  6 07:17 agent_20260406_0714.md
-rw-r--r--  1 root root    1062 Apr  6 07:17 agent_20260406_0714_raw.log
-rw-r--r--  1 root root    1386 Apr  6 09:23 agent_20260406_0915_raw.log
-rw-r--r--  1 root root    5970 Apr  6 09:23 agent_20260406_1030.md
-rw-r--r--  1 root root     687 Apr  7 03:30 agent_20260407_0330_raw.log
-rw-r--r--  1 root root    5176 Apr  7 06:41 agent_20260407_0637.md
-rw-r--r--  1 root root     721 Apr  7 06:41 agent_20260407_0637_raw.log
-rw-r--r--  1 root root    6577 Apr  8 03:36 agent_20260408_0330.md
-rw-r--r--  1 root root     823 Apr  8 03:36 agent_20260408_0330_raw.log
-rw-r--r--  1 root root      55 Apr  8 06:47 agent_20260408_0645_raw.log
-rw-r--r--  1 root root    4817 Apr  9 03:33 agent_20260409_0330.md
-rw-r--r--  1 root root    1348 Apr  9 03:34 agent_20260409_0330_raw.log
-rw-r--r--  1 root root    5524 Apr 10 03:37 agent_20260410_0330.md
-rw-r--r--  1 root root    1109 Apr 10 03:38 agent_20260410_0330_raw.log
-rw-r--r--  1 root root    5663 Apr 11 03:35 agent_20260411_0330.md
-rw-r--r--  1 root root    1188 Apr 11 03:36 agent_20260411_0330_raw.log
-rw-r--r--  1 root root    3893 Apr 12 03:32 agent_20260412_0330.md
-rw-r--r--  1 root root     951 Apr 12 03:32 agent_20260412_0330_raw.log
-rw-r--r--  1 root root    5664 Apr 13 03:33 agent_20260413_0330.md
-rw-r--r--  1 root root     695 Apr 13 03:33 agent_20260413_0330_raw.log
-rw-r--r--  1 root root     139 Apr 14 03:31 agent_20260414_0330_raw.log
-rw-r--r--  1 root root    5505 Apr 15 03:34 agent_20260415_0330.md
-rw-r--r--  1 root root     809 Apr 15 03:34 agent_20260415_0330_raw.log
-rw-r--r--  1 root root    6700 Apr 16 03:35 agent_20260416_0330.md
-rw-r--r--  1 root root    1119 Apr 16 03:36 agent_20260416_0330_raw.log
-rw-r--r--  1 root root       0 Apr 17 03:30 agent_20260417_0330_raw.log
-rw-r--r--  1 root root    6993 Apr 17 08:10 agent_20260417_0803.md
-rw-r--r--  1 root root    1243 Apr 17 08:12 agent_20260417_0803_raw.log
-rw-r--r--  1 root root    7597 Apr 18 03:37 agent_20260418_0330.md
-rw-r--r--  1 root root    1184 Apr 18 03:38 agent_20260418_0330_raw.log
-rw-r--r--  1 root root    7151 Apr 19 03:36 agent_20260419_0330.md
-rw-r--r--  1 root root    1337 Apr 19 03:37 agent_20260419_0330_raw.log
-rw-r--r--  1 root root    6298 Apr 19 08:05 agent_20260419_0755.md
-rw-r--r--  1 root root    1156 Apr 19 08:06 agent_20260419_0755_raw.log
-rw-r--r--  1 root root    6311 Apr 20 03:33 agent_20260420_0330.md
-rw-r--r--  1 root root    1572 Apr 20 03:34 agent_20260420_0330_raw.log
-rw-r--r--  1 root root    5916 Apr 21 03:36 agent_20260421_0330.md
-rw-r--r--  1 root root    1482 Apr 21 03:36 agent_20260421_0330_raw.log
-rw-r--r--  1 root root    6882 Apr 22 03:35 agent_20260422_0330.md
-rw-r--r--  1 root root    1383 Apr 22 03:35 agent_20260422_0330_raw.log
-rw-r--r--  1 root root    5092 Apr 22 16:24 agent_20260422_1616.md
-rw-r--r--  1 root root     716 Apr 22 16:25 agent_20260422_1616_raw.log
-rw-r--r--  1 root root    5683 Apr 23 03:34 agent_20260423_0330.md
-rw-r--r--  1 root root    1016 Apr 23 03:34 agent_20260423_0330_raw.log
-rw-r--r--  1 root root    6179 Apr 24 03:44 agent_20260424_0330.md
-rw-r--r--  1 root root     178 Apr 24 03:46 agent_20260424_0330_raw.log
-rw-r--r--  1 root root    6625 Apr 25 03:38 agent_20260425_0330.md
-rw-r--r--  1 root root     921 Apr 25 03:38 agent_20260425_0330_raw.log
-rw-r--r--  1 root root    7448 Apr 26 03:38 agent_20260426_0330.md
-rw-r--r--  1 root root      29 Apr 26 03:38 agent_20260426_0330_raw.log
-rw-r--r--  1 root root    7558 Apr 27 03:36 agent_20260427_0330.md
-rw-r--r--  1 root root    1221 Apr 27 03:37 agent_20260427_0330_raw.log
-rw-r--r--  1 root root    7848 Apr 28 03:39 agent_20260428_0330.md
-rw-r--r--  1 root root    1144 Apr 28 03:40 agent_20260428_0330_raw.log
-rw-r--r--  1 root root       0 Apr 29 03:30 agent_20260429_0330_raw.log
-rw-r--r--  1 root root    7186 Apr 29 10:19 agent_20260429_1013.md
-rw-r--r--  1 root root    1053 Apr 29 10:20 agent_20260429_1013_raw.log
-rw-r--r--  1 root root    8761 Apr 29 10:49 agent_20260429_1041.md
-rw-r--r--  1 root root    1118 Apr 29 10:49 agent_20260429_1041_raw.log
-rw-r--r--  1 root root    7764 Apr 30 03:34 agent_20260430_0330.md
-rw-r--r--  1 root root    1189 Apr 30 03:35 agent_20260430_0330_raw.log
-rw-r--r--  1 root root    8529 Apr 30 19:09 agent_20260430_1905.md
-rw-r--r--  1 root root    1044 Apr 30 19:10 agent_20260430_1905_raw.log
-rw-r--r--  1 root root    7496 Apr 30 19:24 agent_20260430_1919.md
-rw-r--r--  1 root root     885 Apr 30 19:25 agent_20260430_1919_raw.log
-rw-r--r--  1 root root    6944 Apr 30 19:53 agent_20260430_1946.md
-rw-r--r--  1 root root     891 Apr 30 19:55 agent_20260430_1946_raw.log
-rw-r--r--  1 root root    7531 May  1 03:36 agent_20260501_0330.md
-rw-r--r--  1 root root    1108 May  1 03:36 agent_20260501_0330_raw.log
-rw-r--r--  1 root root    8972 May  1 07:41 agent_20260501_0725.md
-rw-r--r--  1 root root      29 May  1 07:41 agent_20260501_0725_raw.log
-rw-r--r--  1 root root       0 May  1 08:58 agent_20260501_0858_raw.log
-rw-r--r--  1 root root    7867 May  1 09:38 agent_20260501_0931.md
-rw-r--r--  1 root root    1138 May  1 09:39 agent_20260501_0931_raw.log
-rw-r--r--  1 root root    9161 May  2 03:35 agent_20260502_0330.md
-rw-r--r--  1 root root    1399 May  2 03:35 agent_20260502_0330_raw.log
-rw-r--r--  1 root root    8987 May  3 03:37 agent_20260503_0330.md
-rw-r--r--  1 root root    1191 May  3 03:38 agent_20260503_0330_raw.log
-rw-r--r--  1 root root    3017 May  4 03:30 agent_20260504_0330_codex_raw.log
-rw-r--r--  1 root root       0 May  4 07:21 agent_20260504_0721_claude_raw.log
-rw-r--r--  1 root root      58 May  4 07:21 agent_20260504_0721_codex_raw.log
-rw-r--r--  1 root root    7631 May  4 09:06 agent_20260504_0901.md
-rw-r--r--  1 root root    1094 May  4 09:06 agent_20260504_0901_claude_raw.log
-rw-r--r--  1 root root      58 May  4 09:01 agent_20260504_0901_codex_raw.log
-rw-r--r--  1 root root       0 May  4 11:42 agent_20260504_1138_claude_raw.log
-rw-r--r--  1 root root  120171 May  4 11:42 agent_20260504_1138_codex_raw.log
-rw-r--r--  1 root root    6587 May  4 12:26 agent_20260504_1219.md
-rw-r--r--  1 root root  461963 May  4 12:28 agent_20260504_1219_codex_raw.log
-rw-r--r--  1 root root    5205 May  5 03:33 agent_20260505_0330.md
-rw-r--r--  1 root root  383951 May  5 03:33 agent_20260505_0330_codex_raw.log
-rw-r--r--  1 root root    4827 May  5 10:31 agent_20260505_1022.md
-rw-r--r--  1 root root  211659 May  5 10:32 agent_20260505_1022_codex_raw.log
-rw-r--r--  1 root root    7656 May  6 03:35 agent_20260506_0330.md
-rw-r--r--  1 root root     993 May  6 03:36 agent_20260506_0330_claude_raw.log
-rw-r--r--  1 root root      58 May  6 03:30 agent_20260506_0330_codex_raw.log
-rw-r--r--  1 root root    6351 May  6 06:31 agent_20260506_0625.md
-rw-r--r--  1 root root  527859 May  6 06:32 agent_20260506_0625_codex_raw.log
-rw-r--r--  1 root root    6086 May  6 19:45 agent_20260506_1941.md
-rw-r--r--  1 root root  594761 May  6 19:45 agent_20260506_1941_codex_raw.log
-rw-r--r--  1 root root    6012 May  6 19:58 agent_20260506_1955.md
-rw-r--r--  1 root root  363172 May  6 20:01 agent_20260506_1955_codex_raw.log
-rw-r--r--  1 root root    7643 May  7 03:33 agent_20260507_0330.md
-rw-r--r--  1 root root  478573 May  7 03:34 agent_20260507_0330_codex_raw.log
-rw-r--r--  1 root root    1069 May  7 08:04 agent_20260507_0803_codex_raw.log
-rw-r--r--  1 root root    4561 Apr  6 07:13 agent_diag2.md
-rw-r--r--  1 root root    6159 May  4 09:09 agent_manual_20260504_090133.log
-rw-r--r--  1 root root    7149 May  4 12:30 agent_manual_codex2_20260504_121901.log
-rw-r--r--  1 root root    6809 May  4 12:03 agent_manual_codex_20260504_113858.log
-rw-r--r--  1 root root    3229 May  4 07:41 agent_recovery_20260504_072142.log
-rw-r--r--  1 root root    2227 Apr  6 06:38 agent_test_0406.md
-rw-r--r--  1 root root    4997 Apr  6 09:41 agent_test_field.md
-rw-r--r--  1 root root 1754951 Apr  5 03:30 cron.log
-rw-r--r--  1 root root   24053 Mar  5 08:44 cycle_20260305_0844.json
-rw-r--r--  1 root root   42188 Mar  6 03:42 cycle_20260306_0342.json
-rw-r--r--  1 root root   42188 Mar  6 18:34 cycle_20260306_1834.json
-rw-r--r--  1 root root   30217 Mar  7 03:42 cycle_20260307_0342.json
-rw-r--r--  1 root root   30219 Mar 15 03:46 cycle_20260315_0346.json
-rw-r--r--  1 root root    3101 Mar 15 08:11 ddf_20260315_0811.json
-rw-r--r--  1 root root    1631 Mar 15 08:15 ddf_20260315_0815.json
-rw-r--r--  1 root root    1754 Mar 16 04:05 ddf_20260316_0405.json
-rw-r--r--  1 root root    1785 Mar 17 04:05 ddf_20260317_0405.json
-rw-r--r--  1 root root    1524 Mar 18 04:05 ddf_20260318_0405.json
-rw-r--r--  1 root root    1440 Mar 19 04:05 ddf_20260319_0405.json
-rw-r--r--  1 root root    1660 Mar 20 04:05 ddf_20260320_0405.json
-rw-r--r--  1 root root    1848 Mar 21 04:05 ddf_20260321_0405.json
-rw-r--r--  1 root root    1596 Mar 22 04:05 ddf_20260322_0405.json
-rw-r--r--  1 root root    1988 Mar 23 04:05 ddf_20260323_0405.json
-rw-r--r--  1 root root    1617 Mar 24 04:05 ddf_20260324_0405.json
-rw-r--r--  1 root root    2045 Mar 25 04:05 ddf_20260325_0405.json
-rw-r--r--  1 root root    1848 Mar 26 04:05 ddf_20260326_0405.json
-rw-r--r--  1 root root    1880 Mar 27 04:05 ddf_20260327_0405.json
-rw-r--r--  1 root root    1923 Mar 28 04:05 ddf_20260328_0405.json
-rw-r--r--  1 root root    1984 Mar 29 04:05 ddf_20260329_0405.json
-rw-r--r--  1 root root    1985 Mar 30 04:05 ddf_20260330_0405.json
-rw-r--r--  1 root root    1985 Mar 31 04:05 ddf_20260331_0405.json
-rw-r--r--  1 root root    1985 Apr  1 04:05 ddf_20260401_0405.json
-rw-r--r--  1 root root    1986 Apr  2 04:05 ddf_20260402_0405.json
-rw-r--r--  1 root root    1986 Apr  3 04:05 ddf_20260403_0405.json
-rw-r--r--  1 root root    1985 Apr  4 04:05 ddf_20260404_0405.json
-rw-r--r--  1 root root    2193 Apr  5 04:05 ddf_20260405_0405.json
-rw-r--r--  1 root root    2144 Apr  6 04:05 ddf_20260406_0405.json
-rw-r--r--  1 root root    2122 Apr  7 04:05 ddf_20260407_0405.json
-rw-r--r--  1 root root    2173 Apr  8 04:05 ddf_20260408_0405.json
-rw-r--r--  1 root root    1984 Apr  9 04:05 ddf_20260409_0405.json
-rw-r--r--  1 root root    1986 Apr 10 04:05 ddf_20260410_0405.json
-rw-r--r--  1 root root    2171 Apr 11 04:05 ddf_20260411_0405.json
-rw-r--r--  1 root root    2146 Apr 12 04:05 ddf_20260412_0405.json
-rw-r--r--  1 root root    2147 Apr 13 04:05 ddf_20260413_0405.json
-rw-r--r--  1 root root    2147 Apr 14 04:05 ddf_20260414_0405.json
-rw-r--r--  1 root root    2147 Apr 15 04:05 ddf_20260415_0405.json
-rw-r--r--  1 root root    2147 Apr 16 04:05 ddf_20260416_0405.json
-rw-r--r--  1 root root    1962 Apr 17 04:05 ddf_20260417_0405.json
-rw-r--r--  1 root root    1962 Apr 18 04:05 ddf_20260418_0405.json
-rw-r--r--  1 root root    1962 Apr 19 04:05 ddf_20260419_0405.json
-rw-r--r--  1 root root    2122 Apr 20 04:05 ddf_20260420_0405.json
-rw-r--r--  1 root root    1962 Apr 21 04:05 ddf_20260421_0405.json
-rw-r--r--  1 root root    2142 Apr 22 04:05 ddf_20260422_0405.json
-rw-r--r--  1 root root    2185 Apr 23 04:05 ddf_20260423_0405.json
-rw-r--r--  1 root root    1946 Apr 24 04:05 ddf_20260424_0405.json
-rw-r--r--  1 root root    1946 Apr 25 04:05 ddf_20260425_0405.json
-rw-r--r--  1 root root    1946 Apr 26 04:05 ddf_20260426_0405.json
-rw-r--r--  1 root root    1946 Apr 27 04:05 ddf_20260427_0405.json
-rw-r--r--  1 root root    1946 Apr 28 04:05 ddf_20260428_0405.json
-rw-r--r--  1 root root    1946 Apr 29 04:05 ddf_20260429_0405.json
-rw-r--r--  1 root root    2048 Apr 30 04:05 ddf_20260430_0405.json
-rw-r--r--  1 root root    2045 May  1 04:05 ddf_20260501_0405.json
-rw-r--r--  1 root root    2045 May  2 04:05 ddf_20260502_0405.json
-rw-r--r--  1 root root    2046 May  3 04:05 ddf_20260503_0405.json
-rw-r--r--  1 root root    2045 May  4 04:05 ddf_20260504_0405.json
-rw-r--r--  1 root root    2046 May  5 04:05 ddf_20260505_0405.json
-rw-r--r--  1 root root    2046 May  5 06:36 ddf_20260505_0636.json
-rw-r--r--  1 root root    2046 May  6 04:05 ddf_20260506_0405.json
-rw-r--r--  1 root root    2046 May  7 04:05 ddf_20260507_0405.json
-rw-r--r--  1 root root    3344 Apr 17 08:13 evolution_20260417_0803.md
-rw-r--r--  1 root root    3248 Apr 18 03:39 evolution_20260418_0330.md
-rw-r--r--  1 root root    2793 Apr 19 08:07 evolution_20260419_0330.md
-rw-r--r--  1 root root    3232 Apr 20 03:35 evolution_20260420_0330.md
-rw-r--r--  1 root root    2507 Apr 21 03:37 evolution_20260421_0330.md
-rw-r--r--  1 root root    3132 Apr 22 03:36 evolution_20260422_0330.md
-rw-r--r--  1 root root    2475 Apr 22 16:25 evolution_20260422_1616.md
-rw-r--r--  1 root root    2507 Apr 23 03:35 evolution_20260423_0330.md
-rw-r--r--  1 root root    2619 Apr 24 03:47 evolution_20260424_0330.md
-rw-r--r--  1 root root    2462 Apr 25 03:39 evolution_20260425_0330.md
-rw-r--r--  1 root root    3040 Apr 27 03:37 evolution_20260427_0330.md
-rw-r--r--  1 root root    2305 Apr 28 03:40 evolution_20260428_0330.md
-rw-r--r--  1 root root    2680 May  3 03:40 evolution_20260503_0330.md
-rw-r--r--  1 root root    2406 May  4 09:08 evolution_20260504_0330.md
-rw-r--r--  1 root root    2548 May  5 03:34 evolution_20260505_0330.md
-rw-r--r--  1 root root    2996 May  6 03:37 evolution_20260506_0330.md
-rw-r--r--  1 root root    2693 May  6 19:46 evolution_20260506_1941.md
-rw-r--r--  1 root root   21634 Apr  8 03:34 exp_acf_decay_data.json
-rw-r--r--  1 root root    4386 Apr  5 08:27 exp_boundary_20260405_0825.json
-rw-r--r--  1 root root    1180 Apr  5 09:17 exp_boundary_growth_20260405_0914.json
-rw-r--r--  1 root root   14808 Apr  5 09:21 exp_brody_crossover_20260405.json
-rw-r--r--  1 root root   14041 Apr  6 07:16 exp_crossover_universality.json
-rw-r--r--  1 root root   10183 Apr  6 07:12 exp_dR_brody_connection.json
-rw-r--r--  1 root root    9693 Apr  6 07:09 exp_desitter_unification.json
-rw-r--r--  1 root root   11031 Apr  5 09:17 exp_excess_scaling_20260405.json
-rw-r--r--  1 root root     916 Apr  5 12:07 exp_geodesic_deviation_primes.json
-rw-r--r--  1 root root    7207 Apr  6 07:02 exp_metric_tensor_diag_long.json
-rw-r--r--  1 root root    2181 Apr  5 11:45 exp_number_variance_test.json
-rw-r--r--  1 root root  471174 Apr  8 06:47 exp_psd_prime_gaps.json
-rw-r--r--  1 root root    3804 Apr  5 11:47 exp_ricci_primes.json
-rw-r--r--  1 root root    3668 Apr 29 10:20 falsifier_20260429_1013.json
-rw-r--r--  1 root root    2846 Apr 29 10:50 falsifier_20260429_1041.json
-rw-r--r--  1 root root    2370 Apr 30 03:36 falsifier_20260430_0330.json
-rw-r--r--  1 root root    3953 Apr 30 19:11 falsifier_20260430_1905.json
-rw-r--r--  1 root root    4165 Apr 30 19:25 falsifier_20260430_1919.json
-rw-r--r--  1 root root    3965 Apr 30 19:55 falsifier_20260430_1946.json
-rw-r--r--  1 root root    3852 May  1 03:37 falsifier_20260501_0330.json
-rw-r--r--  1 root root    4323 May  1 07:41 falsifier_20260501_0725.json
-rw-r--r--  1 root root    3847 May  1 09:39 falsifier_20260501_0931.json
-rw-r--r--  1 root root    3823 May  2 03:36 falsifier_20260502_0330.json
-rw-r--r--  1 root root    4228 May  3 03:40 falsifier_20260503_0330.json
-rw-r--r--  1 root root    5793 May  4 09:08 falsifier_20260504_0901.json
-rw-r--r--  1 root root     383 May  4 12:29 falsifier_20260504_1219.json
-rw-r--r--  1 root root    2978 May  5 03:33 falsifier_20260505_0330.json
-rw-r--r--  1 root root      28 May  6 03:37 falsifier_20260506_0330.raw.txt
-rw-r--r--  1 root root     330 May  6 06:32 falsifier_20260506_0625.json
-rw-r--r--  1 root root     261 May  6 19:46 falsifier_20260506_1941.json
-rw-r--r--  1 root root     345 May  6 20:01 falsifier_20260506_1955.json
-rw-r--r--  1 root root      17 May  6 20:00 falsifier_20260506_1955.raw.txt
-rw-r--r--  1 root root     376 May  7 03:34 falsifier_20260507_0330.json
-rw-r--r--  1 root root      15 May  7 03:34 falsifier_20260507_0330.raw.txt
-rw-r--r--  1 root root   10050 Mar  5 07:56 fibonacci_spectrum_20260305_0756.json
-rw-r--r--  1 root root   10050 Mar  5 08:44 fibonacci_spectrum_20260305_0844.json
-rw-r--r--  1 root root   10050 Mar  6 03:42 fibonacci_spectrum_20260306_0342.json
-rw-r--r--  1 root root   10050 Mar  6 18:34 fibonacci_spectrum_20260306_1834.json
-rw-r--r--  1 root root   25252 Mar  5 08:43 gap_labeling_20260305_0843.json
-rw-r--r--  1 root root   25252 Mar  5 11:11 gap_labeling_20260305_1111.json
-rw-r--r--  1 root root   25252 Mar  6 03:41 gap_labeling_20260306_0341.json
-rw-r--r--  1 root root   25252 Mar  6 18:34 gap_labeling_20260306_1834.json
-rw-r--r--  1 root root   25252 Mar  7 03:42 gap_labeling_20260307_0342.json
-rw-r--r--  1 root root   25252 Mar 15 03:43 gap_labeling_20260315_0343.json
-rw-r--r--  1 root root    6315 Apr  7 06:40 hierarchy_data.json
-rw-r--r--  1 root root     517 May  4 07:41 incident_20260504_0721.md
-rw-r--r--  1 root root    2301 May  4 12:02 incident_20260504_1138.md
-rw-r--r--  1 root root    2938 Mar  5 08:52 insights_20260305_0852.json
-rw-r--r--  1 root root    2938 Mar  6 03:42 insights_20260306_0342.json
-rw-r--r--  1 root root    2938 Mar  6 18:34 insights_20260306_1834.json
-rw-r--r--  1 root root    2938 Mar  7 03:42 insights_20260307_0342.json
-rw-r--r--  1 root root    2938 Mar 15 03:46 insights_20260315_0346.json
-rw-r--r--  1 root root    2938 Mar 29 03:43 insights_20260329_0343.json
-rw-r--r--  1 root root    2938 Apr  1 03:46 insights_20260401_0346.json
-rw-r--r--  1 root root    2938 Apr  3 03:30 insights_20260403_0330.json
-rw-r--r--  1 root root    2938 Apr  5 07:29 insights_20260405_0729.json
-rw-r--r--  1 root root    5254 Mar  5 10:42 lagrangiana_20260305_1042.json
-rw-r--r--  1 root root    7664 Mar  5 10:48 lagrangiana_20260305_1048.json
lrwxrwxrwx  1 root root      22 May  7 03:34 latest.md -> agent_20260507_0330.md
-rw-r--r--  1 root root     462 May  7 03:35 loop_guard_20260507_0330.json
-rw-r--r--  1 root root    8109 May  6 19:47 manual_run_20260506_194105.log
-rw-r--r--  1 root root    7890 May  6 20:03 manual_run_20260506_195547.log
-rw-r--r--  1 root root    1201 May  7 08:03 manual_run_20260507_080350.log
-rw-r--r--  1 root root    9210 Apr 21 08:31 mapping_validation_2026-04-21.json
-rw-r--r--  1 root root    1499 Mar  5 11:11 next_exec_20260305_1111.json
-rw-r--r--  1 root root    1272 Mar  6 03:42 next_exec_20260306_0342.json
-rw-r--r--  1 root root    1446 Mar  6 18:34 next_exec_20260306_1834.json
-rw-r--r--  1 root root    1287 Mar  7 03:42 next_exec_20260307_0342.json
-rw-r--r--  1 root root    1203 Mar 14 03:42 next_exec_20260314_0342.json
-rw-r--r--  1 root root    1253 Mar 15 03:46 next_exec_20260315_0346.json
-rw-r--r--  1 root root    1168 Mar 26 03:43 next_exec_20260326_0343.json
-rw-r--r--  1 root root    1168 Mar 27 03:44 next_exec_20260327_0344.json
-rw-r--r--  1 root root    1168 Mar 28 03:44 next_exec_20260328_0344.json
-rw-r--r--  1 root root    1329 Mar 29 03:43 next_exec_20260329_0343.json
-rw-r--r--  1 root root     987 Mar 30 03:44 next_exec_20260330_0344.json
-rw-r--r--  1 root root     987 Mar 31 03:45 next_exec_20260331_0345.json
-rw-r--r--  1 root root    1362 Apr  1 03:46 next_exec_20260401_0346.json
-rw-r--r--  1 root root    1452 Apr  2 03:44 next_exec_20260402_0344.json
-rw-r--r--  1 root root    1207 Apr  3 03:30 next_exec_20260403_0330.json
-rw-r--r--  1 root root     423 Apr  4 03:30 next_exec_20260404_0330.json
-rw-r--r--  1 root root     368 Apr  5 03:30 next_exec_20260405_0330.json
-rw-r--r--  1 root root    1391 Apr  5 07:29 next_exec_20260405_0729.json
-rw-r--r--  1 root root    3805 Mar  6 09:02 phi_vs_silver_falsification_20260306.json
-rw-r--r--  1 root root   17274 May  6 06:33 recovery_20260506_062520.log
-rw-r--r--  1 root root    1806 Mar  2 03:41 report_20260302_0341.md
-rw-r--r--  1 root root    1775 Mar  3 03:41 report_20260303_0341.md
-rw-r--r--  1 root root    1791 Mar  4 03:42 report_20260304_0342.md
-rw-r--r--  1 root root    1775 Mar  5 03:42 report_20260305_0342.md
-rw-r--r--  1 root root    1791 Mar  5 21:21 report_20260305_2121.md
-rw-r--r--  1 root root    1776 Mar  6 03:41 report_20260306_0341.md
-rw-r--r--  1 root root    1791 Mar  7 03:42 report_20260307_0342.md
-rw-r--r--  1 root root    1790 Mar 14 03:42 report_20260314_0342.md
-rw-r--r--  1 root root    1789 Mar 15 03:42 report_20260315_0342.md
-rw-r--r--  1 root root    1743 Mar 15 08:01 report_20260315_0801.md
-rw-r--r--  1 root root    1765 Mar 26 03:43 report_20260326_0343.md
-rw-r--r--  1 root root    1763 Mar 27 03:44 report_20260327_0344.md
-rw-r--r--  1 root root    1779 Mar 28 03:44 report_20260328_0344.md
-rw-r--r--  1 root root    1677 Mar 29 03:43 report_20260329_0343.md
-rw-r--r--  1 root root    1514 Mar 30 03:44 report_20260330_0344.md
-rw-r--r--  1 root root    1656 Mar 31 03:45 report_20260331_0345.md
-rw-r--r--  1 root root    1671 Mar 31 18:09 report_20260331_1809.md
-rw-r--r--  1 root root    1663 Apr  1 03:46 report_20260401_0346.md
-rw-r--r--  1 root root    1656 Apr  2 03:44 report_20260402_0344.md
-rw-r--r--  1 root root    1656 Apr  2 07:56 report_20260402_0756.md
-rw-r--r--  1 root root    1656 Apr  3 03:30 report_20260403_0330.md
-rw-r--r--  1 root root    1656 Apr  4 03:30 report_20260404_0330.md
-rw-r--r--  1 root root    1656 Apr  4 18:52 report_20260404_1852.md
-rw-r--r--  1 root root    1656 Apr  5 03:30 report_20260405_0330.md
-rw-r--r--  1 root root    1656 Apr  5 07:15 report_20260405_0715.md
-rw-r--r--  1 root root    2593 Apr 21 15:11 tension_projection_screening_2026-04-21.json
-rw-r--r--  1 root root    1116 Mar  5 08:43 trace_map_20260305_0843.json
-rw-r--r--  1 root root    1186 Mar  5 08:44 trace_map_20260305_0844.json
-rw-r--r--  1 root root     458 May  6 19:31 trajectory_apply_20260506_1931.json
-rw-r--r--  1 root root     458 May  6 19:41 trajectory_apply_20260506_1941.json
-rw-r--r--  1 root root     975 May  6 19:55 trajectory_apply_20260506_1955.json
-rw-r--r--  1 root root     458 May  7 03:30 trajectory_apply_20260507_0330.json
-rw-r--r--  1 root root     463 May  7 08:03 trajectory_apply_20260507_0803.json
-rw-r--r--  1 root root     435 May  7 04:30 watchdog.log

codex
Il campo contiene un override esplicito: niente nuovo attacco BOUNDARY, ma trasferire il `BOUNDARY_LAYER_GATE` su `DUALITA_DIPOLARE_VS_ILLUSORIA`. Ora controllo la proiezione aggiornata e le definizioni canoniche degli osservabili per non duplicare varianti locali.
exec
/bin/bash -lc "sed -n '1,240p' tools/observables_registry.py" in /opt/MM_D-ND
 succeeded in 0ms:
"""observables_registry.py — Source of Truth per gli observables del lab D-ND.

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

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

## Il problema che ha creato il registry

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

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

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

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

## La soluzione (minimal, non invasiva)

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

## Come usarlo

```python
from observables_registry import OBSERVABLES_CANONICAL, OBSERVABLES_REGISTRY_VERSION

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

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

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

## Convention per i report

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

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

Cycle che mescola canonical + variant DEVE indicare entrambi:

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

Senza questo, i confronti cross-cycle sono inattendibili.

## Versioning

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

import numpy as np


OBSERVABLES_REGISTRY_VERSION = "1.0.0-2026-05-06"


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

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

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

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


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

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


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

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


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


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

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


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


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

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

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

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

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


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

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

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


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


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

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

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


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



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

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

> La prossima corsa NON deve restare su perturbation rank.

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

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

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

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

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

## Results

### Main Run

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

### Three-Run Summary

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

## Findings

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

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

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

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

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

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

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

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

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

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

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

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

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

This run does not repeat perturbation rank. It asks:

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

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

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

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

## Results

### Main Run

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

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

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

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

### Seed Check

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

## Findings

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

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

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

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

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

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

Scoped statement from this run:

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

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

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

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

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

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

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

codex
La forma utile qui è una layer map fuori da BOUNDARY: prendo un generatore dipolare coerente e aumento una quota `beta` di sostituzioni illusorie non coerenti, poi misuro dove collassa il supporto originale-vs-shuffle. Se il gate separa solo “ordine vs shuffle” ma non dipolare vs illusorio, il verdetto sarà `metric` o `ambiguous`, non verrà forzata una conferma.
codex
Prima di editare creo uno strumento riusabile `tools/exp_duality_gate_transfer.py`. Userà il registry canonico, produrrà JSON in `tools/data/`, e dichiarerà esplicitamente beta, z per osservabile, stable count e margine di classificazione.
exec
/bin/bash -lc "sed -n '1,220p' tools/exp_boundary_mixture_gate.py" in /opt/MM_D-ND
 succeeded in 0ms:
#!/usr/bin/env python3
"""
exp_boundary_mixture_gate.py

Reusable META/BOUNDARY audit for the GUE-Poisson boundary.

Question:
    Does the GUE/Poisson boundary remain a clean two-class split after the
    original-vs-shuffle denominator gate, or is the mixed region an operational
    third state where canonical observables lose stable denominators?

The script uses only canonical observables from observables_registry.py.
It builds synthetic mixtures by replacing a fraction beta of unfolded GUE
spacings with Poisson spacings, then measures:

- canonical observable vectors;
- original-vs-shuffle z-score per observable;
- endpoint separability in all observables and in gate-stable observables;
- ambiguity of each beta layer relative to pure GUE and pure Poisson centroids.
"""

from __future__ import annotations

import argparse
import json
from pathlib import Path

import numpy as np

from observables_registry import (
    OBSERVABLES_CANONICAL,
    OBSERVABLES_REGISTRY_VERSION,
    compute_canonical,
)


OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())


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


def mixture_spacings(gue: np.ndarray, poisson: np.ndarray, beta: float, rng: np.random.Generator) -> np.ndarray:
    """Return a beta Poisson / (1-beta) GUE spacing sequence with mean spacing 1."""
    if len(gue) != len(poisson):
        raise ValueError("gue and poisson arrays must have the same length")
    mask = rng.random(len(gue)) < beta
    out = gue.copy()
    out[mask] = poisson[mask]
    mean = float(np.mean(out))
    return out / mean if mean > 1e-15 else out


def z_against_shuffle(
    gaps: np.ndarray,
    n_baseline: int,
    rng: np.random.Generator,
) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
    """Return original observables, shuffle baseline std, and original-vs-shuffle z."""
    original = compute_canonical(gaps)
    baseline_vals = {name: [] for name in OBS_NAMES}
    for _ in range(n_baseline):
        obs = compute_canonical(rng.permutation(gaps))
        for name in OBS_NAMES:
            baseline_vals[name].append(obs[name])

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


def vector(row: dict, names: list[str]) -> np.ndarray:
    return np.array([row["observables"][name] for name in names], dtype=float)


def classify_layers(rows: list[dict], obs_names: list[str]) -> dict:
    """Classify each beta layer by standardized distance to endpoint centroids."""
    if not obs_names:
        return {
            "observables": [],
            "endpoint_distance": 0.0,
            "layers": {},
            "ambiguous_beta": [],
        }

    by_beta: dict[float, list[dict]] = {}
    for row in rows:
        by_beta.setdefault(float(row["beta"]), []).append(row)

    gue_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
    poi_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
    all_endpoint = np.vstack([gue_vectors, poi_vectors])
    scale = np.std(all_endpoint, axis=0, ddof=1)
    scale[scale <= 1e-15] = 1.0
    gue_centroid = np.mean(gue_vectors, axis=0)
    poi_centroid = np.mean(poi_vectors, axis=0)
    endpoint_distance = float(np.linalg.norm((poi_centroid - gue_centroid) / scale))

    layers = {}
    ambiguous_beta = []
    for beta, beta_rows in sorted(by_beta.items()):
        coords = []
        margins = []
        labels = []
        for row in beta_rows:
            x = vector(row, obs_names)
            d_gue = float(np.linalg.norm((x - gue_centroid) / scale))
            d_poi = float(np.linalg.norm((x - poi_centroid) / scale))
            denom = d_gue + d_poi
            coord = float((d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
            margin = float(abs(d_gue - d_poi) / denom) if denom > 1e-15 else 0.0
            coords.append(coord)
            margins.append(margin)
            labels.append("gue" if d_gue < d_poi else "poisson")
        ambiguous_fraction = float(np.mean(np.array(margins) < 0.15))
        if ambiguous_fraction >= 0.5:
            ambiguous_beta.append(beta)
        layers[f"{beta:.3f}"] = {
            "coordinate_mean": float(np.mean(coords)),
            "coordinate_std": float(np.std(coords, ddof=1)) if len(coords) > 1 else 0.0,
            "margin_mean": float(np.mean(margins)),
            "ambiguous_fraction": ambiguous_fraction,
            "poisson_label_fraction": float(np.mean(np.array(labels) == "poisson")),
        }

    return {
        "observables": obs_names,
        "endpoint_distance": endpoint_distance,
        "layers": layers,
        "ambiguous_beta": ambiguous_beta,
    }


def summarize_gate(rows: list[dict], z_min: float) -> dict:
    by_beta: dict[float, list[dict]] = {}
    for row in rows:
        by_beta.setdefault(float(row["beta"]), []).append(row)

    layers = {}
    for beta, beta_rows in sorted(by_beta.items()):
        stable_counts = []
        stable_freq = {name: [] for name in OBS_NAMES}
        for row in beta_rows:
            stable = [name for name in OBS_NAMES if abs(row["z"][name]) >= z_min]
            stable_counts.append(len(stable))
            for name in OBS_NAMES:
                stable_freq[name].append(1.0 if name in stable else 0.0)
        layers[f"{beta:.3f}"] = {
            "stable_count_mean": float(np.mean(stable_counts)),
            "stable_count_std": float(np.std(stable_counts, ddof=1)) if len(stable_counts) > 1 else 0.0,
            "stable_frequency": {name: float(np.mean(vals)) for name, vals in stable_freq.items()},
        }

    endpoint_stable = []
    for name in OBS_NAMES:
        endpoint_rows = by_beta[0.0] + by_beta[1.0]
        freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in endpoint_rows])
        if freq >= 0.75:
            endpoint_stable.append(name)

    return {
        "z_min": z_min,
        "endpoint_stable_observables": endpoint_stable,
        "layers": layers,
    }


def run(args: argparse.Namespace) -> dict:
    rng = np.random.default_rng(args.seed)
    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
    rows = []

    for rep in range(args.n_replicates):
        rep_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
        gue = gue_spacings(args.gue_matrix_size, args.n_gaps, rep_rng)
        poisson = rep_rng.exponential(1.0, size=args.n_gaps)
        poisson = poisson / float(np.mean(poisson))
        for beta in betas:
            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
            gaps = mixture_spacings(gue, poisson, beta, layer_rng)
            obs, shuffle_std, z = z_against_shuffle(
                gaps,
                n_baseline=args.n_baseline,
                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
            )
            rows.append(
                {
                    "replicate": rep,
                    "beta": beta,
                    "observables": obs,
                    "shuffle_std": shuffle_std,
                    "z": z,
                    "stable_observables": [name for name in OBS_NAMES if abs(z[name]) >= args.z_min],
                }
            )

    gate = summarize_gate(rows, args.z_min)
    all_classification = classify_layers(rows, OBS_NAMES)
    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])

exec
/bin/bash -lc "sed -n '220,520p' tools/exp_boundary_mixture_gate.py" in /opt/MM_D-ND
 succeeded in 0ms:
    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])

    output = {
        "experiment": "boundary_mixture_gate",
        "question": "Is the GUE-Poisson mixed layer cleanly classifiable after denominator gating?",
        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
        "observables_used": OBS_NAMES,
        "params": vars(args),
        "gate": gate,
        "classification_all_observables": all_classification,
        "classification_endpoint_gated": gated_classification,
        "rows": rows,
    }

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

    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
    print(f"observables_used={OBS_NAMES}")
    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
    print(
        "endpoint_distance_all="
        f"{all_classification['endpoint_distance']:.3f} "
        "endpoint_distance_gated="
        f"{gated_classification['endpoint_distance']:.3f}"
    )
    if gate["endpoint_stable_observables"]:
        print("beta stable_count margin_gated ambiguous_gated poisson_fraction_gated")
    else:
        print("endpoint gate is empty; printing all-observable classification")
        print("beta stable_count margin_all ambiguous_all poisson_fraction_all")
    for beta in betas:
        key = f"{beta:.3f}"
        stable_count = gate["layers"][key]["stable_count_mean"]
        source = gated_classification if gate["endpoint_stable_observables"] else all_classification
        layer = source["layers"].get(key, {})
        print(
            f"{beta:>4.2f} {stable_count:>12.3f} "
            f"{layer.get('margin_mean', 0.0):>12.3f} "
            f"{layer.get('ambiguous_fraction', 0.0):>15.3f} "
            f"{layer.get('poisson_label_fraction', 0.0):>21.3f}"
        )
    print(f"saved {out_path}")
    return output


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--n-gaps", type=int, default=1536)
    parser.add_argument("--n-replicates", type=int, default=16)
    parser.add_argument("--gue-matrix-size", type=int, default=180)
    parser.add_argument("--n-beta", type=int, default=11)
    parser.add_argument("--n-baseline", type=int, default=24)
    parser.add_argument("--z-min", type=float, default=2.0)
    parser.add_argument("--seed", type=int, default=20260507)
    parser.add_argument("--out", default="tools/data/boundary_mixture_gate.json")
    args = parser.parse_args()
    run(args)


if __name__ == "__main__":
    main()

apply patch
patch: completed
/opt/MM_D-ND/tools/exp_duality_gate_transfer.py
diff --git a/tools/exp_duality_gate_transfer.py b/tools/exp_duality_gate_transfer.py
new file mode 100644
index 0000000000000000000000000000000000000000..c04f490d17524d73d40766b8b969cdfe3d20077c
--- /dev/null
+++ b/tools/exp_duality_gate_transfer.py
@@ -0,0 +1,284 @@
+#!/usr/bin/env python3
+"""
+exp_duality_gate_transfer.py
+
+Reusable META/DUALITA audit for transferring the BOUNDARY_LAYER_GATE.
+
+Question:
+    Does the denominator-collapse gate discriminate dipolar duality from
+    illusory duality, or does it become a local BOUNDARY metric when moved
+    away from GUE/Poisson?
+
+The script builds a controlled layer map between:
+
+- DIPOLARE: a coherent golden/Beatty gap sequence generated by the orbit of
+  floor((n + phase) * phi). Its gaps are the symbolic det=-1 D-ND pole in the
+  minimal positive alphabet {1, 2}.
+- ILLUSORIA: a full random permutation of the same gaps. This preserves the
+  one-point distribution and first moment but removes the generating order.
+
+Layer beta replaces a beta fraction of dipolar positions with values from the
+illusory permutation. Canonical observables and original-vs-shuffle z gates are
+imported from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
+
+
+OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())
+PHI = (1.0 + 5.0**0.5) / 2.0
+
+
+def dipolar_gaps(n_gaps: int, phase: float) -> np.ndarray:
+    """Return coherent golden Beatty gaps in {1, 2}."""
+    n = np.arange(n_gaps + 1, dtype=float) + float(phase)
+    positions = np.floor(n * PHI).astype(float)
+    gaps = np.diff(positions)
+    mean = float(np.mean(gaps))
+    return gaps / mean if mean > 1e-15 else gaps
+
+
+def beta_layer_gaps(
+    dipolar: np.ndarray,
+    beta: float,
+    rng: np.random.Generator,
+) -> np.ndarray:
+    """Replace a beta fraction of coherent positions with a shuffled copy."""
+    illusory = rng.permutation(dipolar)
+    if beta <= 0.0:
+        out = dipolar.copy()
+    elif beta >= 1.0:
+        out = illusory.copy()
+    else:
+        mask = rng.random(len(dipolar)) < beta
+        out = dipolar.copy()
+        out[mask] = illusory[mask]
+    mean = float(np.mean(out))
+    return out / mean if mean > 1e-15 else out
+
+
+def z_against_shuffle(
+    gaps: np.ndarray,
+    n_baseline: int,
+    rng: np.random.Generator,
+) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
+    """Return original observables, shuffle baseline std, and z scores."""
+    original = compute_canonical(gaps)
+    baseline_vals = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline_vals[name].append(obs[name])
+
+    shuffle_std = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline_vals[name], dtype=float)
+        mean = float(np.mean(vals))
+        sd = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        shuffle_std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, shuffle_std, z
+
+
+def vector(row: dict, names: list[str]) -> np.ndarray:
+    return np.array([row["observables"][name] for name in names], dtype=float)
+
+
+def classify_layers(rows: list[dict], obs_names: list[str]) -> dict:
+    """Classify layers by standardized distance to dipolar/illusory endpoints."""
+    if not obs_names:
+        return {
+            "observables": [],
+            "endpoint_distance": 0.0,
+            "layers": {},
+            "ambiguous_beta": [],
+        }
+
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    dip_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    ill_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([dip_vectors, ill_vectors])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    dip_centroid = np.mean(dip_vectors, axis=0)
+    ill_centroid = np.mean(ill_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((ill_centroid - dip_centroid) / scale))
+
+    layers = {}
+    ambiguous_beta = []
+    for beta, beta_rows in sorted(by_beta.items()):
+        coords = []
+        margins = []
+        labels = []
+        for row in beta_rows:
+            x = vector(row, obs_names)
+            d_dip = float(np.linalg.norm((x - dip_centroid) / scale))
+            d_ill = float(np.linalg.norm((x - ill_centroid) / scale))
+            denom = d_dip + d_ill
+            coord = float((d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("dipolar" if d_dip < d_ill else "illusory")
+        ambiguous_fraction = float(np.mean(np.array(margins) < 0.15))
+        if ambiguous_fraction >= 0.5:
+            ambiguous_beta.append(beta)
+        layers[f"{beta:.3f}"] = {
+            "coordinate_mean": float(np.mean(coords)),
+            "coordinate_std": float(np.std(coords, ddof=1)) if len(coords) > 1 else 0.0,
+            "margin_mean": float(np.mean(margins)),
+            "ambiguous_fraction": ambiguous_fraction,
+            "illusory_label_fraction": float(np.mean(np.array(labels) == "illusory")),
+        }
+
+    return {
+        "observables": obs_names,
+        "endpoint_distance": endpoint_distance,
+        "layers": layers,
+        "ambiguous_beta": ambiguous_beta,
+    }
+
+
+def summarize_gate(rows: list[dict], z_min: float) -> dict:
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    layers = {}
+    for beta, beta_rows in sorted(by_beta.items()):
+        stable_counts = []
+        stable_freq = {name: [] for name in OBS_NAMES}
+        z_values = {name: [] for name in OBS_NAMES}
+        for row in beta_rows:
+            stable = [name for name in OBS_NAMES if abs(row["z"][name]) >= z_min]
+            stable_counts.append(len(stable))
+            for name in OBS_NAMES:
+                stable_freq[name].append(1.0 if name in stable else 0.0)
+                z_values[name].append(row["z"][name])
+        layers[f"{beta:.3f}"] = {
+            "stable_count_mean": float(np.mean(stable_counts)),
+            "stable_count_std": float(np.std(stable_counts, ddof=1)) if len(stable_counts) > 1 else 0.0,
+            "stable_frequency": {name: float(np.mean(vals)) for name, vals in stable_freq.items()},
+            "z_mean": {name: float(np.mean(vals)) for name, vals in z_values.items()},
+        }
+
+    endpoint_stable = []
+    for name in OBS_NAMES:
+        endpoint_rows = by_beta[0.0] + by_beta[1.0]
+        freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in endpoint_rows])
+        if freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def run(args: argparse.Namespace) -> dict:
+    rng = np.random.default_rng(args.seed)
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    rows = []
+
+    for rep in range(args.n_replicates):
+        phase = float(rng.random())
+        base = dipolar_gaps(args.n_gaps, phase)
+        for beta in betas:
+            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+            gaps = beta_layer_gaps(base, beta, layer_rng)
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                n_baseline=args.n_baseline,
+                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "replicate": rep,
+                    "phase": phase,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [name for name in OBS_NAMES if abs(z[name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
+
+    output = {
+        "experiment": "duality_gate_transfer",
+        "category": "gate_transferability",
+        "question": "Does the denominator-collapse gate transfer from BOUNDARY to DUALITA_DIPOLARE_VS_ILLUSORIA?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_endpoint_gated": gated_classification,
+        "rows": rows,
+    }
+
+    out_path = Path(args.out)
+    out_path.parent.mkdir(parents=True, exist_ok=True)
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
+    print(f"observables_used={OBS_NAMES}")
+    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
+    print(
+        "endpoint_distance_all="
+        f"{all_classification['endpoint_distance']:.3f} "
+        "endpoint_distance_gated="
+        f"{gated_classification['endpoint_distance']:.3f}"
+    )
+    source = gated_classification if gate["endpoint_stable_observables"] else all_classification
+    print("beta stable_count margin ambiguous illusory_fraction")
+    for beta in betas:
+        key = f"{beta:.3f}"
+        stable_count = gate["layers"][key]["stable_count_mean"]
+        layer = source["layers"].get(key, {})
+        print(
+            f"{beta:>4.2f} {stable_count:>12.3f} "
+            f"{layer.get('margin_mean', 0.0):>8.3f} "
+            f"{layer.get('ambiguous_fraction', 0.0):>9.3f} "
+            f"{layer.get('illusory_label_fraction', 0.0):>17.3f}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=4096)
+    parser.add_argument("--n-replicates", type=int, default=24)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=40)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=202605070803)
+    parser.add_argument("--out", default="tools/data/duality_gate_transfer.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

diff --git a/tools/exp_duality_gate_transfer.py b/tools/exp_duality_gate_transfer.py
new file mode 100644
index 0000000000000000000000000000000000000000..c04f490d17524d73d40766b8b969cdfe3d20077c
--- /dev/null
+++ b/tools/exp_duality_gate_transfer.py
@@ -0,0 +1,284 @@
+#!/usr/bin/env python3
+"""
+exp_duality_gate_transfer.py
+
+Reusable META/DUALITA audit for transferring the BOUNDARY_LAYER_GATE.
+
+Question:
+    Does the denominator-collapse gate discriminate dipolar duality from
+    illusory duality, or does it become a local BOUNDARY metric when moved
+    away from GUE/Poisson?
+
+The script builds a controlled layer map between:
+
+- DIPOLARE: a coherent golden/Beatty gap sequence generated by the orbit of
+  floor((n + phase) * phi). Its gaps are the symbolic det=-1 D-ND pole in the
+  minimal positive alphabet {1, 2}.
+- ILLUSORIA: a full random permutation of the same gaps. This preserves the
+  one-point distribution and first moment but removes the generating order.
+
+Layer beta replaces a beta fraction of dipolar positions with values from the
+illusory permutation. Canonical observables and original-vs-shuffle z gates are
+imported from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
+
+
+OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())
+PHI = (1.0 + 5.0**0.5) / 2.0
+
+
+def dipolar_gaps(n_gaps: int, phase: float) -> np.ndarray:
+    """Return coherent golden Beatty gaps in {1, 2}."""
+    n = np.arange(n_gaps + 1, dtype=float) + float(phase)
+    positions = np.floor(n * PHI).astype(float)
+    gaps = np.diff(positions)
+    mean = float(np.mean(gaps))
+    return gaps / mean if mean > 1e-15 else gaps
+
+
+def beta_layer_gaps(
+    dipolar: np.ndarray,
+    beta: float,
+    rng: np.random.Generator,
+) -> np.ndarray:
+    """Replace a beta fraction of coherent positions with a shuffled copy."""
+    illusory = rng.permutation(dipolar)
+    if beta <= 0.0:
+        out = dipolar.copy()
+    elif beta >= 1.0:
+        out = illusory.copy()
+    else:
+        mask = rng.random(len(dipolar)) < beta
+        out = dipolar.copy()
+        out[mask] = illusory[mask]
+    mean = float(np.mean(out))
+    return out / mean if mean > 1e-15 else out
+
+
+def z_against_shuffle(
+    gaps: np.ndarray,
+    n_baseline: int,
+    rng: np.random.Generator,
+) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
+    """Return original observables, shuffle baseline std, and z scores."""
+    original = compute_canonical(gaps)
+    baseline_vals = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline_vals[name].append(obs[name])
+
+    shuffle_std = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline_vals[name], dtype=float)
+        mean = float(np.mean(vals))
+        sd = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        shuffle_std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, shuffle_std, z
+
+
+def vector(row: dict, names: list[str]) -> np.ndarray:
+    return np.array([row["observables"][name] for name in names], dtype=float)
+
+
+def classify_layers(rows: list[dict], obs_names: list[str]) -> dict:
+    """Classify layers by standardized distance to dipolar/illusory endpoints."""
+    if not obs_names:
+        return {
+            "observables": [],
+            "endpoint_distance": 0.0,
+            "layers": {},
+            "ambiguous_beta": [],
+        }
+
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    dip_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    ill_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([dip_vectors, ill_vectors])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    dip_centroid = np.mean(dip_vectors, axis=0)
+    ill_centroid = np.mean(ill_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((ill_centroid - dip_centroid) / scale))
+
+    layers = {}
+    ambiguous_beta = []
+    for beta, beta_rows in sorted(by_beta.items()):
+        coords = []
+        margins = []
+        labels = []
+        for row in beta_rows:
+            x = vector(row, obs_names)
+            d_dip = float(np.linalg.norm((x - dip_centroid) / scale))
+            d_ill = float(np.linalg.norm((x - ill_centroid) / scale))
+            denom = d_dip + d_ill
+            coord = float((d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("dipolar" if d_dip < d_ill else "illusory")
+        ambiguous_fraction = float(np.mean(np.array(margins) < 0.15))
+        if ambiguous_fraction >= 0.5:
+            ambiguous_beta.append(beta)
+        layers[f"{beta:.3f}"] = {
+            "coordinate_mean": float(np.mean(coords)),
+            "coordinate_std": float(np.std(coords, ddof=1)) if len(coords) > 1 else 0.0,
+            "margin_mean": float(np.mean(margins)),
+            "ambiguous_fraction": ambiguous_fraction,
+            "illusory_label_fraction": float(np.mean(np.array(labels) == "illusory")),
+        }
+
+    return {
+        "observables": obs_names,
+        "endpoint_distance": endpoint_distance,
+        "layers": layers,
+        "ambiguous_beta": ambiguous_beta,
+    }
+
+
+def summarize_gate(rows: list[dict], z_min: float) -> dict:
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    layers = {}
+    for beta, beta_rows in sorted(by_beta.items()):
+        stable_counts = []
+        stable_freq = {name: [] for name in OBS_NAMES}
+        z_values = {name: [] for name in OBS_NAMES}
+        for row in beta_rows:
+            stable = [name for name in OBS_NAMES if abs(row["z"][name]) >= z_min]
+            stable_counts.append(len(stable))
+            for name in OBS_NAMES:
+                stable_freq[name].append(1.0 if name in stable else 0.0)
+                z_values[name].append(row["z"][name])
+        layers[f"{beta:.3f}"] = {
+            "stable_count_mean": float(np.mean(stable_counts)),
+            "stable_count_std": float(np.std(stable_counts, ddof=1)) if len(stable_counts) > 1 else 0.0,
+            "stable_frequency": {name: float(np.mean(vals)) for name, vals in stable_freq.items()},
+            "z_mean": {name: float(np.mean(vals)) for name, vals in z_values.items()},
+        }
+
+    endpoint_stable = []
+    for name in OBS_NAMES:
+        endpoint_rows = by_beta[0.0] + by_beta[1.0]
+        freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in endpoint_rows])
+        if freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def run(args: argparse.Namespace) -> dict:
+    rng = np.random.default_rng(args.seed)
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    rows = []
+
+    for rep in range(args.n_replicates):
+        phase = float(rng.random())
+        base = dipolar_gaps(args.n_gaps, phase)
+        for beta in betas:
+            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+            gaps = beta_layer_gaps(base, beta, layer_rng)
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                n_baseline=args.n_baseline,
+                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "replicate": rep,
+                    "phase": phase,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [name for name in OBS_NAMES if abs(z[name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
+
+    output = {
+        "experiment": "duality_gate_transfer",
+        "category": "gate_transferability",
+        "question": "Does the denominator-collapse gate transfer from BOUNDARY to DUALITA_DIPOLARE_VS_ILLUSORIA?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_endpoint_gated": gated_classification,
+        "rows": rows,
+    }
+
+    out_path = Path(args.out)
+    out_path.parent.mkdir(parents=True, exist_ok=True)
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
+    print(f"observables_used={OBS_NAMES}")
+    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
+    print(
+        "endpoint_distance_all="
+        f"{all_classification['endpoint_distance']:.3f} "
+        "endpoint_distance_gated="
+        f"{gated_classification['endpoint_distance']:.3f}"
+    )
+    source = gated_classification if gate["endpoint_stable_observables"] else all_classification
+    print("beta stable_count margin ambiguous illusory_fraction")
+    for beta in betas:
+        key = f"{beta:.3f}"
+        stable_count = gate["layers"][key]["stable_count_mean"]
+        layer = source["layers"].get(key, {})
+        print(
+            f"{beta:>4.2f} {stable_count:>12.3f} "
+            f"{layer.get('margin_mean', 0.0):>8.3f} "
+            f"{layer.get('ambiguous_fraction', 0.0):>9.3f} "
+            f"{layer.get('illusory_label_fraction', 0.0):>17.3f}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=4096)
+    parser.add_argument("--n-replicates", type=int, default=24)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=40)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=202605070803)
+    parser.add_argument("--out", default="tools/data/duality_gate_transfer.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

codex
Lo strumento è pronto. Eseguo una run principale e una seed-check più leggera: se il segnale è solo un artefatto del seed o del numero di baseline, deve vedersi subito nel confronto.
exec
/bin/bash -lc 'python tools/exp_duality_gate_transfer.py --out tools/data/duality_gate_transfer_20260507_0803.json' in /opt/MM_D-ND
exec
/bin/bash -lc 'python tools/exp_duality_gate_transfer.py --n-gaps 3072 --n-replicates 16 --n-baseline 32 --seed 202605070804 --out tools/data/duality_gate_transfer_20260507_0803_seedcheck.json' in /opt/MM_D-ND
 succeeded in 859ms:
observables_registry=1.0.0-2026-05-06
observables_used=['SR', 'SR2', 'L1', 'L2', 'triple_var']
endpoint_stable_observables=[]
endpoint_distance_all=3.412 endpoint_distance_gated=0.000
beta stable_count margin ambiguous illusory_fraction
0.00        3.000    0.983     0.000             0.000
0.10        3.000    0.519     0.000             0.000
0.20        3.000    0.209     0.188             0.000
0.30        3.000    0.042     1.000             0.312
0.40        3.125    0.173     0.375             1.000
0.50        3.000    0.317     0.125             1.000
0.60        3.125    0.430     0.188             1.000
0.70        2.875    0.407     0.125             1.000
0.80        1.062    0.529     0.062             1.000
0.90        0.000    0.540     0.000             1.000
1.00        0.438    0.583     0.125             1.000
saved tools/data/duality_gate_transfer_20260507_0803_seedcheck.json

diff --git a/tools/exp_duality_gate_transfer.py b/tools/exp_duality_gate_transfer.py
new file mode 100644
index 0000000000000000000000000000000000000000..c04f490d17524d73d40766b8b969cdfe3d20077c
--- /dev/null
+++ b/tools/exp_duality_gate_transfer.py
@@ -0,0 +1,284 @@
+#!/usr/bin/env python3
+"""
+exp_duality_gate_transfer.py
+
+Reusable META/DUALITA audit for transferring the BOUNDARY_LAYER_GATE.
+
+Question:
+    Does the denominator-collapse gate discriminate dipolar duality from
+    illusory duality, or does it become a local BOUNDARY metric when moved
+    away from GUE/Poisson?
+
+The script builds a controlled layer map between:
+
+- DIPOLARE: a coherent golden/Beatty gap sequence generated by the orbit of
+  floor((n + phase) * phi). Its gaps are the symbolic det=-1 D-ND pole in the
+  minimal positive alphabet {1, 2}.
+- ILLUSORIA: a full random permutation of the same gaps. This preserves the
+  one-point distribution and first moment but removes the generating order.
+
+Layer beta replaces a beta fraction of dipolar positions with values from the
+illusory permutation. Canonical observables and original-vs-shuffle z gates are
+imported from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
+
+
+OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())
+PHI = (1.0 + 5.0**0.5) / 2.0
+
+
+def dipolar_gaps(n_gaps: int, phase: float) -> np.ndarray:
+    """Return coherent golden Beatty gaps in {1, 2}."""
+    n = np.arange(n_gaps + 1, dtype=float) + float(phase)
+    positions = np.floor(n * PHI).astype(float)
+    gaps = np.diff(positions)
+    mean = float(np.mean(gaps))
+    return gaps / mean if mean > 1e-15 else gaps
+
+
+def beta_layer_gaps(
+    dipolar: np.ndarray,
+    beta: float,
+    rng: np.random.Generator,
+) -> np.ndarray:
+    """Replace a beta fraction of coherent positions with a shuffled copy."""
+    illusory = rng.permutation(dipolar)
+    if beta <= 0.0:
+        out = dipolar.copy()
+    elif beta >= 1.0:
+        out = illusory.copy()
+    else:
+        mask = rng.random(len(dipolar)) < beta
+        out = dipolar.copy()
+        out[mask] = illusory[mask]
+    mean = float(np.mean(out))
+    return out / mean if mean > 1e-15 else out
+
+
+def z_against_shuffle(
+    gaps: np.ndarray,
+    n_baseline: int,
+    rng: np.random.Generator,
+) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
+    """Return original observables, shuffle baseline std, and z scores."""
+    original = compute_canonical(gaps)
+    baseline_vals = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline_vals[name].append(obs[name])
+
+    shuffle_std = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline_vals[name], dtype=float)
+        mean = float(np.mean(vals))
+        sd = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        shuffle_std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, shuffle_std, z
+
+
+def vector(row: dict, names: list[str]) -> np.ndarray:
+    return np.array([row["observables"][name] for name in names], dtype=float)
+
+
+def classify_layers(rows: list[dict], obs_names: list[str]) -> dict:
+    """Classify layers by standardized distance to dipolar/illusory endpoints."""
+    if not obs_names:
+        return {
+            "observables": [],
+            "endpoint_distance": 0.0,
+            "layers": {},
+            "ambiguous_beta": [],
+        }
+
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    dip_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    ill_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([dip_vectors, ill_vectors])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    dip_centroid = np.mean(dip_vectors, axis=0)
+    ill_centroid = np.mean(ill_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((ill_centroid - dip_centroid) / scale))
+
+    layers = {}
+    ambiguous_beta = []
+    for beta, beta_rows in sorted(by_beta.items()):
+        coords = []
+        margins = []
+        labels = []
+        for row in beta_rows:
+            x = vector(row, obs_names)
+            d_dip = float(np.linalg.norm((x - dip_centroid) / scale))
+            d_ill = float(np.linalg.norm((x - ill_centroid) / scale))
+            denom = d_dip + d_ill
+            coord = float((d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("dipolar" if d_dip < d_ill else "illusory")
+        ambiguous_fraction = float(np.mean(np.array(margins) < 0.15))
+        if ambiguous_fraction >= 0.5:
+            ambiguous_beta.append(beta)
+        layers[f"{beta:.3f}"] = {
+            "coordinate_mean": float(np.mean(coords)),
+            "coordinate_std": float(np.std(coords, ddof=1)) if len(coords) > 1 else 0.0,
+            "margin_mean": float(np.mean(margins)),
+            "ambiguous_fraction": ambiguous_fraction,
+            "illusory_label_fraction": float(np.mean(np.array(labels) == "illusory")),
+        }
+
+    return {
+        "observables": obs_names,
+        "endpoint_distance": endpoint_distance,
+        "layers": layers,
+        "ambiguous_beta": ambiguous_beta,
+    }
+
+
+def summarize_gate(rows: list[dict], z_min: float) -> dict:
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    layers = {}
+    for beta, beta_rows in sorted(by_beta.items()):
+        stable_counts = []
+        stable_freq = {name: [] for name in OBS_NAMES}
+        z_values = {name: [] for name in OBS_NAMES}
+        for row in beta_rows:
+            stable = [name for name in OBS_NAMES if abs(row["z"][name]) >= z_min]
+            stable_counts.append(len(stable))
+            for name in OBS_NAMES:
+                stable_freq[name].append(1.0 if name in stable else 0.0)
+                z_values[name].append(row["z"][name])
+        layers[f"{beta:.3f}"] = {
+            "stable_count_mean": float(np.mean(stable_counts)),
+            "stable_count_std": float(np.std(stable_counts, ddof=1)) if len(stable_counts) > 1 else 0.0,
+            "stable_frequency": {name: float(np.mean(vals)) for name, vals in stable_freq.items()},
+            "z_mean": {name: float(np.mean(vals)) for name, vals in z_values.items()},
+        }
+
+    endpoint_stable = []
+    for name in OBS_NAMES:
+        endpoint_rows = by_beta[0.0] + by_beta[1.0]
+        freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in endpoint_rows])
+        if freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def run(args: argparse.Namespace) -> dict:
+    rng = np.random.default_rng(args.seed)
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    rows = []
+
+    for rep in range(args.n_replicates):
+        phase = float(rng.random())
+        base = dipolar_gaps(args.n_gaps, phase)
+        for beta in betas:
+            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+            gaps = beta_layer_gaps(base, beta, layer_rng)
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                n_baseline=args.n_baseline,
+                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "replicate": rep,
+                    "phase": phase,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [name for name in OBS_NAMES if abs(z[name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
+
+    output = {
+        "experiment": "duality_gate_transfer",
+        "category": "gate_transferability",
+        "question": "Does the denominator-collapse gate transfer from BOUNDARY to DUALITA_DIPOLARE_VS_ILLUSORIA?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_endpoint_gated": gated_classification,
+        "rows": rows,
+    }
+
+    out_path = Path(args.out)
+    out_path.parent.mkdir(parents=True, exist_ok=True)
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
+    print(f"observables_used={OBS_NAMES}")
+    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
+    print(
+        "endpoint_distance_all="
+        f"{all_classification['endpoint_distance']:.3f} "
+        "endpoint_distance_gated="
+        f"{gated_classification['endpoint_distance']:.3f}"
+    )
+    source = gated_classification if gate["endpoint_stable_observables"] else all_classification
+    print("beta stable_count margin ambiguous illusory_fraction")
+    for beta in betas:
+        key = f"{beta:.3f}"
+        stable_count = gate["layers"][key]["stable_count_mean"]
+        layer = source["layers"].get(key, {})
+        print(
+            f"{beta:>4.2f} {stable_count:>12.3f} "
+            f"{layer.get('margin_mean', 0.0):>8.3f} "
+            f"{layer.get('ambiguous_fraction', 0.0):>9.3f} "
+            f"{layer.get('illusory_label_fraction', 0.0):>17.3f}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=4096)
+    parser.add_argument("--n-replicates", type=int, default=24)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=40)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=202605070803)
+    parser.add_argument("--out", default="tools/data/duality_gate_transfer.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

 succeeded in 1617ms:
observables_registry=1.0.0-2026-05-06
observables_used=['SR', 'SR2', 'L1', 'L2', 'triple_var']
endpoint_stable_observables=[]
endpoint_distance_all=3.436 endpoint_distance_gated=0.000
beta stable_count margin ambiguous illusory_fraction
0.00        3.000    0.971     0.000             0.000
0.10        3.000    0.479     0.000             0.000
0.20        3.000    0.234     0.125             0.000
0.30        3.000    0.033     1.000             0.375
0.40        3.083    0.154     0.417             0.958
0.50        3.083    0.269     0.375             1.000
0.60        3.000    0.417     0.042             1.000
0.70        2.917    0.316     0.167             1.000
0.80        1.708    0.473     0.125             1.000
0.90        0.167    0.463     0.042             1.000
1.00        0.208    0.442     0.042             1.000
saved tools/data/duality_gate_transfer_20260507_0803.json

diff --git a/tools/exp_duality_gate_transfer.py b/tools/exp_duality_gate_transfer.py
new file mode 100644
index 0000000000000000000000000000000000000000..c04f490d17524d73d40766b8b969cdfe3d20077c
--- /dev/null
+++ b/tools/exp_duality_gate_transfer.py
@@ -0,0 +1,284 @@
+#!/usr/bin/env python3
+"""
+exp_duality_gate_transfer.py
+
+Reusable META/DUALITA audit for transferring the BOUNDARY_LAYER_GATE.
+
+Question:
+    Does the denominator-collapse gate discriminate dipolar duality from
+    illusory duality, or does it become a local BOUNDARY metric when moved
+    away from GUE/Poisson?
+
+The script builds a controlled layer map between:
+
+- DIPOLARE: a coherent golden/Beatty gap sequence generated by the orbit of
+  floor((n + phase) * phi). Its gaps are the symbolic det=-1 D-ND pole in the
+  minimal positive alphabet {1, 2}.
+- ILLUSORIA: a full random permutation of the same gaps. This preserves the
+  one-point distribution and first moment but removes the generating order.
+
+Layer beta replaces a beta fraction of dipolar positions with values from the
+illusory permutation. Canonical observables and original-vs-shuffle z gates are
+imported from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
+
+
+OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())
+PHI = (1.0 + 5.0**0.5) / 2.0
+
+
+def dipolar_gaps(n_gaps: int, phase: float) -> np.ndarray:
+    """Return coherent golden Beatty gaps in {1, 2}."""
+    n = np.arange(n_gaps + 1, dtype=float) + float(phase)
+    positions = np.floor(n * PHI).astype(float)
+    gaps = np.diff(positions)
+    mean = float(np.mean(gaps))
+    return gaps / mean if mean > 1e-15 else gaps
+
+
+def beta_layer_gaps(
+    dipolar: np.ndarray,
+    beta: float,
+    rng: np.random.Generator,
+) -> np.ndarray:
+    """Replace a beta fraction of coherent positions with a shuffled copy."""
+    illusory = rng.permutation(dipolar)
+    if beta <= 0.0:
+        out = dipolar.copy()
+    elif beta >= 1.0:
+        out = illusory.copy()
+    else:
+        mask = rng.random(len(dipolar)) < beta
+        out = dipolar.copy()
+        out[mask] = illusory[mask]
+    mean = float(np.mean(out))
+    return out / mean if mean > 1e-15 else out
+
+
+def z_against_shuffle(
+    gaps: np.ndarray,
+    n_baseline: int,
+    rng: np.random.Generator,
+) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
+    """Return original observables, shuffle baseline std, and z scores."""
+    original = compute_canonical(gaps)
+    baseline_vals = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline_vals[name].append(obs[name])
+
+    shuffle_std = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline_vals[name], dtype=float)
+        mean = float(np.mean(vals))
+        sd = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        shuffle_std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, shuffle_std, z
+
+
+def vector(row: dict, names: list[str]) -> np.ndarray:
+    return np.array([row["observables"][name] for name in names], dtype=float)
+
+
+def classify_layers(rows: list[dict], obs_names: list[str]) -> dict:
+    """Classify layers by standardized distance to dipolar/illusory endpoints."""
+    if not obs_names:
+        return {
+            "observables": [],
+            "endpoint_distance": 0.0,
+            "layers": {},
+            "ambiguous_beta": [],
+        }
+
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    dip_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    ill_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([dip_vectors, ill_vectors])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    dip_centroid = np.mean(dip_vectors, axis=0)
+    ill_centroid = np.mean(ill_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((ill_centroid - dip_centroid) / scale))
+
+    layers = {}
+    ambiguous_beta = []
+    for beta, beta_rows in sorted(by_beta.items()):
+        coords = []
+        margins = []
+        labels = []
+        for row in beta_rows:
+            x = vector(row, obs_names)
+            d_dip = float(np.linalg.norm((x - dip_centroid) / scale))
+            d_ill = float(np.linalg.norm((x - ill_centroid) / scale))
+            denom = d_dip + d_ill
+            coord = float((d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("dipolar" if d_dip < d_ill else "illusory")
+        ambiguous_fraction = float(np.mean(np.array(margins) < 0.15))
+        if ambiguous_fraction >= 0.5:
+            ambiguous_beta.append(beta)
+        layers[f"{beta:.3f}"] = {
+            "coordinate_mean": float(np.mean(coords)),
+            "coordinate_std": float(np.std(coords, ddof=1)) if len(coords) > 1 else 0.0,
+            "margin_mean": float(np.mean(margins)),
+            "ambiguous_fraction": ambiguous_fraction,
+            "illusory_label_fraction": float(np.mean(np.array(labels) == "illusory")),
+        }
+
+    return {
+        "observables": obs_names,
+        "endpoint_distance": endpoint_distance,
+        "layers": layers,
+        "ambiguous_beta": ambiguous_beta,
+    }
+
+
+def summarize_gate(rows: list[dict], z_min: float) -> dict:
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    layers = {}
+    for beta, beta_rows in sorted(by_beta.items()):
+        stable_counts = []
+        stable_freq = {name: [] for name in OBS_NAMES}
+        z_values = {name: [] for name in OBS_NAMES}
+        for row in beta_rows:
+            stable = [name for name in OBS_NAMES if abs(row["z"][name]) >= z_min]
+            stable_counts.append(len(stable))
+            for name in OBS_NAMES:
+                stable_freq[name].append(1.0 if name in stable else 0.0)
+                z_values[name].append(row["z"][name])
+        layers[f"{beta:.3f}"] = {
+            "stable_count_mean": float(np.mean(stable_counts)),
+            "stable_count_std": float(np.std(stable_counts, ddof=1)) if len(stable_counts) > 1 else 0.0,
+            "stable_frequency": {name: float(np.mean(vals)) for name, vals in stable_freq.items()},
+            "z_mean": {name: float(np.mean(vals)) for name, vals in z_values.items()},
+        }
+
+    endpoint_stable = []
+    for name in OBS_NAMES:
+        endpoint_rows = by_beta[0.0] + by_beta[1.0]
+        freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in endpoint_rows])
+        if freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def run(args: argparse.Namespace) -> dict:
+    rng = np.random.default_rng(args.seed)
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    rows = []
+
+    for rep in range(args.n_replicates):
+        phase = float(rng.random())
+        base = dipolar_gaps(args.n_gaps, phase)
+        for beta in betas:
+            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+            gaps = beta_layer_gaps(base, beta, layer_rng)
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                n_baseline=args.n_baseline,
+                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "replicate": rep,
+                    "phase": phase,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [name for name in OBS_NAMES if abs(z[name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
+
+    output = {
+        "experiment": "duality_gate_transfer",
+        "category": "gate_transferability",
+        "question": "Does the denominator-collapse gate transfer from BOUNDARY to DUALITA_DIPOLARE_VS_ILLUSORIA?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_endpoint_gated": gated_classification,
+        "rows": rows,
+    }
+
+    out_path = Path(args.out)
+    out_path.parent.mkdir(parents=True, exist_ok=True)
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
+    print(f"observables_used={OBS_NAMES}")
+    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
+    print(
+        "endpoint_distance_all="
+        f"{all_classification['endpoint_distance']:.3f} "
+        "endpoint_distance_gated="
+        f"{gated_classification['endpoint_distance']:.3f}"
+    )
+    source = gated_classification if gate["endpoint_stable_observables"] else all_classification
+    print("beta stable_count margin ambiguous illusory_fraction")
+    for beta in betas:
+        key = f"{beta:.3f}"
+        stable_count = gate["layers"][key]["stable_count_mean"]
+        layer = source["layers"].get(key, {})
+        print(
+            f"{beta:>4.2f} {stable_count:>12.3f} "
+            f"{layer.get('margin_mean', 0.0):>8.3f} "
+            f"{layer.get('ambiguous_fraction', 0.0):>9.3f} "
+            f"{layer.get('illusory_label_fraction', 0.0):>17.3f}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=4096)
+    parser.add_argument("--n-replicates", type=int, default=24)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=40)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=202605070803)
+    parser.add_argument("--out", default="tools/data/duality_gate_transfer.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

codex
La run non dà un “operator” pulito: il classificatore sugli osservabili separa gli endpoint, ma l’endpoint-gated set è vuoto perché il polo illusorio non ha denominatori stabili. Estraggo ora le frequenze z per capire se è trasferimento parziale o semplice metrica locale mascherata.
exec
/bin/bash -lc 'ls -l tools/data/duality_gate_transfer_20260507_0803*.json' in /opt/MM_D-ND
 succeeded in 0ms:
-rw-r--r-- 1 root root 222620 May  7 08:06 tools/data/duality_gate_transfer_20260507_0803.json
-rw-r--r-- 1 root root 150785 May  7 08:06 tools/data/duality_gate_transfer_20260507_0803_seedcheck.json

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/build_agent_field.py
 M tools/data/agent_field_live.md
 M tools/data/conoscenza_teorie.json
 M tools/data/consecutio.json
 M tools/data/consecutio_processata.json
 M tools/data/domande_fondamentali.json
 M tools/data/incrocio_risultato.json
 M tools/data/lab_data.json
 M tools/data/lab_graph.json
 M tools/data/lab_health.json
 M tools/data/lab_session_log.jsonl
 M tools/data/pipeline_state.json
 M tools/data/ponti_evoluti.json
 M tools/data/refresh_detector_state.json
 M tools/data/seme.json
 D tools/data/seme_archive/piano_37.json
 D tools/data/seme_archive/piano_38.json
 D tools/data/seme_archive/piano_4.json
 D tools/data/seme_archive/piano_5.json
 D tools/data/seme_archive/piano_6.json
 M tools/data/seme_axioms.json
 M tools/data/seme_backup_pre_run.json
 M tools/data/tm1_figures/tensions.json
 M tools/data/tm1_figures/tensions_raw.json
?? applications/published/20260504_0901_the-two-markov-layers-are-coupled/
?? applications/published/20260504_1219_markov-layers-pass-the-first-recovery/
?? applications/published/20260505_0330_observable-rank-audit-many-probes-one/
?? applications/published/20260506_1955_observable-collinearity-breaks-only-where-denominators/
?? applications/published/20260507_0330_the-gue-poisson-boundary-is-a/
?? applications/scoperte/20260502_0330_the-dipolar-phase-transition-is-nearly_auto/
?? applications/scoperte/20260503_0330_markov-memory-has-two-visible-layers_auto/
?? applications/scoperte/20260504_0901_the-two-markov-layers-are-coupled_auto/
?? applications/scoperte/20260504_1219_markov-layers-pass-the-first-recovery_auto/
?? applications/scoperte/20260505_0330_observable-rank-audit-many-probes-one_auto/
?? applications/scoperte/20260506_0625_perturbation-dimensionality-is-not-yet-a_auto/
?? applications/scoperte/20260506_1941_perturbation-rank-needs-denominator-gating_auto/
?? applications/scoperte/20260506_1955_observable-collinearity-breaks-only-where-denominators_auto/
?? applications/scoperte/20260507_0330_the-gue-poisson-boundary-is-a_auto/
?? applications/soluzioni/20260502_0330_the-dipolar-phase-transition-is-nearly/
?? applications/soluzioni/20260503_0330_markov-memory-has-two-visible-layers/
?? applications/soluzioni/20260504_0901_the-two-markov-layers-are-coupled/
?? applications/soluzioni/20260504_1219_markov-layers-pass-the-first-recovery/
?? applications/soluzioni/20260505_0330_observable-rank-audit-many-probes-one/
?? applications/soluzioni/20260506_1941_perturbation-rank-needs-denominator-gating/
?? applications/soluzioni/20260506_1955_observable-collinearity-breaks-only-where-denominators/
?? applications/soluzioni/20260507_0330_the-gue-poisson-boundary-is-a/
?? findings_gallery.md
?? tools/data/.last_telegram_msg
?? tools/data/3d_boundary_layers.json
?? tools/data/aeternitas/
?? tools/data/alignment_active.json
?? tools/data/alignment_markers.jsonl
?? tools/data/bicono_projections.jsonl
?? tools/data/boundary_coherence.json
?? tools/data/boundary_mixture_gate_20260507_0330.json
?? tools/data/boundary_mixture_gate_20260507_0330_seedcheck.json
?? tools/data/boundary_shuffle_audit.json
?? tools/data/brody_calibration_results.json
?? tools/data/brody_flow.json
?? tools/data/conoscenza_teorie.json.bak.retraction_22_04
?? tools/data/cross_domain_dipolar_direction.json
?? tools/data/cross_observable_consistency.json
?? tools/data/crossover_phase_test.json
?? tools/data/dipolar_crossover.json
?? tools/data/dipolar_vector_scaling.json
?? tools/data/domandatore/domandatore_20260421_0746.json
?? tools/data/domandatore/domandatore_20260422_0345.json
?? tools/data/domandatore/domandatore_20260423_0345.json
?? tools/data/domandatore/domandatore_20260424_0345.json
?? tools/data/domandatore/domandatore_20260425_0345.json
?? tools/data/domandatore/domandatore_20260426_0345.json
?? tools/data/domandatore/domandatore_20260427_0345.json
?? tools/data/domandatore/domandatore_20260428_0345.json
?? tools/data/domandatore/domandatore_20260428_1236.json
?? tools/data/domandatore/domandatore_20260429_0345.json
?? tools/data/domandatore/domandatore_20260430_0345.json
?? tools/data/domandatore/domandatore_20260501_0345.json
?? tools/data/domandatore/domandatore_20260502_0345.json
?? tools/data/domandatore/domandatore_20260503_0345.json
?? tools/data/domandatore/domandatore_20260504_0345.json
?? tools/data/domandatore/domandatore_20260505_0345.json
?? tools/data/domandatore/domandatore_20260506_0345.json
?? tools/data/domandatore/domandatore_20260507_0345.json
?? tools/data/domandatore/domandatore_20260507_0728.json
?? tools/data/domandatore/domandatore_20260507_0742.json
?? tools/data/duality_gate_transfer_20260507_0803.json
?? tools/data/duality_gate_transfer_20260507_0803_seedcheck.json
?? tools/data/evolution/evolution_20260422_0330.md
?? tools/data/evolution/evolution_20260422_1616.md
?? tools/data/evolution/evolution_20260423_0330.md
?? tools/data/evolution/evolution_20260424_0330.md
?? tools/data/evolution/evolution_20260425_0330.md
?? tools/data/evolution/evolution_20260427_0330.md
?? tools/data/evolution/evolution_20260428_0330.md
?? tools/data/evolution/evolution_20260429_0852.md
?? tools/data/evolution/evolution_20260429_1013.md
?? tools/data/evolution/evolution_20260430_0330.md
?? tools/data/evolution/evolution_20260430_1905.md
?? tools/data/evolution/evolution_20260430_1946.md
?? tools/data/evolution/evolution_20260501_0330.md
?? tools/data/evolution/evolution_20260501_0725.md
?? tools/data/evolution/evolution_20260501_0931.md
?? tools/data/evolution/evolution_20260502_0330.md
?? tools/data/evolution/evolution_20260503_0330.md
?? tools/data/evolution/evolution_20260504_0901.md
?? tools/data/evolution/evolution_20260504_1219.md
?? tools/data/evolution/evolution_20260505_0330.md
?? tools/data/evolution/evolution_20260506_0330.md
?? tools/data/evolution/evolution_20260506_1941.md
?? tools/data/evolution/evolution_20260506_1955.md
?? tools/data/evolution/evolution_20260507_0330.md
?? tools/data/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/operator_directive.md
?? tools/data/perturbation_dimensionality_audit.json
?? tools/data/perturbation_dimensionality_audit_scale0330.json
?? tools/data/perturbation_rank_size_curve.json
?? tools/data/promotions/
?? tools/data/reports/_quarantine_falsifier_29_04/
?? tools/data/reports/agent_20260422_0330.md
?? tools/data/reports/agent_20260422_1616.md
?? tools/data/reports/agent_20260423_0330.md
?? tools/data/reports/agent_20260424_0330.md
?? tools/data/reports/agent_20260425_0330.md
?? tools/data/reports/agent_20260426_0330.md
?? tools/data/reports/agent_20260427_0330.md
?? tools/data/reports/agent_20260428_0330.md
?? tools/data/reports/agent_20260429_1013.md
?? tools/data/reports/agent_20260429_1041.md
?? tools/data/reports/agent_20260430_0330.md
?? tools/data/reports/agent_20260430_1905.md
?? tools/data/reports/agent_20260430_1919.md
?? tools/data/reports/agent_20260430_1946.md
?? tools/data/reports/agent_20260501_0330.md
?? tools/data/reports/agent_20260501_0725.md
?? tools/data/reports/agent_20260501_0931.md
?? tools/data/reports/agent_20260502_0330.md
?? tools/data/reports/agent_20260503_0330.md
?? tools/data/reports/agent_20260504_0901.md
?? tools/data/reports/agent_20260504_1219.md
?? tools/data/reports/agent_20260505_0330.md
?? tools/data/reports/agent_20260505_1022.md
?? tools/data/reports/agent_20260506_0330.md
?? tools/data/reports/agent_20260506_0625.md
?? tools/data/reports/agent_20260506_1941.md
?? tools/data/reports/agent_20260506_1955.md
?? tools/data/reports/agent_20260507_0330.md
?? tools/data/reports/ddf_20260422_0405.json
?? tools/data/reports/ddf_20260423_0405.json
?? tools/data/reports/ddf_20260424_0405.json
?? tools/data/reports/ddf_20260425_0405.json
?? tools/data/reports/ddf_20260426_0405.json
?? tools/data/reports/ddf_20260427_0405.json
?? tools/data/reports/ddf_20260428_0405.json
?? tools/data/reports/ddf_20260429_0405.json
?? tools/data/reports/ddf_20260430_0405.json
?? tools/data/reports/ddf_20260501_0405.json
?? tools/data/reports/ddf_20260502_0405.json
?? tools/data/reports/ddf_20260503_0405.json
?? tools/data/reports/ddf_20260504_0405.json
?? tools/data/reports/ddf_20260505_0405.json
?? tools/data/reports/ddf_20260505_0636.json
?? tools/data/reports/ddf_20260506_0405.json
?? tools/data/reports/ddf_20260507_0405.json
?? tools/data/reports/evolution_20260422_0330.md
?? tools/data/reports/evolution_20260422_1616.md
?? tools/data/reports/evolution_20260423_0330.md
?? tools/data/reports/evolution_20260424_0330.md
?? tools/data/reports/evolution_20260425_0330.md
?? tools/data/reports/evolution_20260427_0330.md
?? tools/data/reports/evolution_20260428_0330.md
?? tools/data/reports/evolution_20260503_0330.md
?? tools/data/reports/evolution_20260504_0330.md
?? tools/data/reports/evolution_20260505_0330.md
?? tools/data/reports/evolution_20260506_0330.md
?? tools/data/reports/evolution_20260506_1941.md
?? tools/data/reports/falsifier_20260429_1013.json
?? tools/data/reports/falsifier_20260429_1041.json
?? tools/data/reports/falsifier_20260430_0330.json
?? tools/data/reports/falsifier_20260430_1905.json
?? tools/data/reports/falsifier_20260430_1919.json
?? tools/data/reports/falsifier_20260430_1946.json
?? tools/data/reports/falsifier_20260501_0330.json
?? tools/data/reports/falsifier_20260501_0725.json
?? tools/data/reports/falsifier_20260501_0931.json
?? tools/data/reports/falsifier_20260502_0330.json
?? tools/data/reports/falsifier_20260503_0330.json
?? tools/data/reports/falsifier_20260504_0901.json
?? tools/data/reports/falsifier_20260504_1219.json
?? tools/data/reports/falsifier_20260505_0330.json
?? tools/data/reports/falsifier_20260506_0330.raw.txt
?? tools/data/reports/falsifier_20260506_0625.json
?? tools/data/reports/falsifier_20260506_1941.json
?? tools/data/reports/falsifier_20260506_1955.json
?? tools/data/reports/falsifier_20260506_1955.raw.txt
?? tools/data/reports/falsifier_20260507_0330.json
?? tools/data/reports/falsifier_20260507_0330.raw.txt
?? tools/data/reports/incident_20260504_0721.md
?? tools/data/reports/incident_20260504_1138.md
?? tools/data/reports/latest.md
?? tools/data/reports/loop_guard_20260507_0330.json
?? tools/data/reports/trajectory_apply_20260506_1931.json
?? tools/data/reports/trajectory_apply_20260506_1941.json
?? tools/data/reports/trajectory_apply_20260506_1955.json
?? tools/data/reports/trajectory_apply_20260507_0330.json
?? tools/data/reports/trajectory_apply_20260507_0803.json
?? tools/data/scale_selective_perturbation.json
?? tools/data/selective_layer_decoupling.json
?? tools/data/seme_archive/piano_64.json
?? tools/data/seme_archive/piano_65.json
?? tools/data/seme_archive/piano_66.json
?? tools/data/seme_archive/piano_67.json
?? tools/data/seme_archive/piano_68.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/evolution_report.md
?? tools/exp_3d_boundary_layers.py
?? tools/exp_boundary_coherence.py
?? tools/exp_boundary_mixture_gate.py
?? tools/exp_boundary_shuffle_audit.py
?? tools/exp_brody_calibration.py
?? tools/exp_brody_flow.py
?? tools/exp_cross_domain_dipolar_direction.py
?? tools/exp_cross_observable_consistency.py
?? tools/exp_crossover_phase_test.py
?? tools/exp_dipolar_angle_reference.py
?? tools/exp_dipolar_crossover.py
?? tools/exp_dipolar_vector_scaling.py
?? tools/exp_duality_gate_transfer.py
?? tools/exp_magnitude_psd_from_acf.py
?? tools/exp_markov3_observable_hunt.py
?? tools/exp_markov_dipolar_decomposition.py
?? tools/exp_markov_k_direction.py
?? tools/exp_markov_layer_recovery_audit.py
?? tools/exp_markov_memory_by_gue_type.py
?? tools/exp_markov_psd_prediction.py
?? tools/exp_markov_scale_function.py
?? tools/exp_meta_tautology_test.py
?? tools/exp_mod3_scaling.py
?? tools/exp_mod3_vs_residual_ordering.py
?? tools/exp_modular_algebra_depth.py
?? tools/exp_modular_memory_spectrum.py
?? tools/exp_observable_rank_audit.py
?? tools/exp_perturbation_dimensionality_audit.py
?? tools/exp_perturbation_rank_size_curve.py
?? tools/exp_scale_selective_perturbation.py
?? tools/exp_selective_layer_decoupling.py
?? tools/exp_spectral_rigidity.py
?? tools/exp_two_channel_boundary.py
?? tools/exp_two_channel_cross_domain.py
?? tools/exp_two_channel_shuffle_audit.py
?? tools/exp_two_layer_universality.py
?? tools/observables_registry.py

exec
/bin/bash -lc "python - <<'PY'
import json
for p in ['tools/data/duality_gate_transfer_20260507_0803.json','tools/data/duality_gate_transfer_20260507_0803_seedcheck.json']:
    d=json.load(open(p))
    print('\\n',p)
    print('endpoint_stable',d['gate']['endpoint_stable_observables'])
    print('ambiguous_all',d['classification_all_observables']['ambiguous_beta'])
    for b in ['0.000','0.300','0.400','0.700','0.800','0.900','1.000']:
        g=d['gate']['layers'][b]
        c=d['classification_all_observables']['layers'][b]
        print(b,'stable',round(g['stable_count_mean'],3),'margin',round(c['margin_mean'],3),'amb',round(c['ambiguous_fraction'],3),'ill',round(c['illusory_label_fraction'],3),'freq', {k:round(v,2) for k,v in g['stable_frequency'].items()}, 'zmean', {k:round(v,1) for k,v in g['z_mean'].items()})
PY" in /opt/MM_D-ND
 succeeded in 0ms:

 tools/data/duality_gate_transfer_20260507_0803.json
endpoint_stable []
ambiguous_all [0.30000000000000004]
0.000 stable 3.0 margin 0.971 amb 0.0 ill 0.0 freq {'SR': 1.0, 'SR2': 0.0, 'L1': 1.0, 'L2': 0.0, 'triple_var': 1.0} zmean {'SR': -40.9, 'SR2': 0.0, 'L1': -41.0, 'L2': 0.0, 'triple_var': -35.8}
0.300 stable 3.0 margin 0.033 amb 1.0 ill 0.375 freq {'SR': 1.0, 'SR2': 0.0, 'L1': 1.0, 'L2': 0.0, 'triple_var': 1.0} zmean {'SR': -19.4, 'SR2': -0.3, 'L1': -19.4, 'L2': -0.3, 'triple_var': -17.6}
0.400 stable 3.083 margin 0.154 amb 0.417 ill 0.958 freq {'SR': 1.0, 'SR2': 0.04, 'L1': 1.0, 'L2': 0.04, 'triple_var': 1.0} zmean {'SR': -14.2, 'SR2': -0.2, 'L1': -14.2, 'L2': -0.2, 'triple_var': -12.9}
0.700 stable 2.917 margin 0.316 amb 0.167 ill 1.0 freq {'SR': 0.92, 'SR2': 0.08, 'L1': 0.92, 'L2': 0.08, 'triple_var': 0.92} zmean {'SR': -3.6, 'SR2': -0.0, 'L1': -3.6, 'L2': -0.0, 'triple_var': -3.2}
0.800 stable 1.708 margin 0.473 amb 0.125 ill 1.0 freq {'SR': 0.54, 'SR2': 0.04, 'L1': 0.54, 'L2': 0.04, 'triple_var': 0.54} zmean {'SR': -1.9, 'SR2': -0.1, 'L1': -1.9, 'L2': -0.1, 'triple_var': -1.7}
0.900 stable 0.167 margin 0.463 amb 0.042 ill 1.0 freq {'SR': 0.04, 'SR2': 0.04, 'L1': 0.04, 'L2': 0.04, 'triple_var': 0.0} zmean {'SR': -0.4, 'SR2': 0.2, 'L1': -0.4, 'L2': 0.2, 'triple_var': -0.3}
1.000 stable 0.208 margin 0.442 amb 0.042 ill 1.0 freq {'SR': 0.04, 'SR2': 0.04, 'L1': 0.04, 'L2': 0.04, 'triple_var': 0.04} zmean {'SR': 0.1, 'SR2': 0.1, 'L1': 0.1, 'L2': 0.1, 'triple_var': 0.2}

 tools/data/duality_gate_transfer_20260507_0803_seedcheck.json
endpoint_stable []
ambiguous_all [0.30000000000000004]
0.000 stable 3.0 margin 0.983 amb 0.0 ill 0.0 freq {'SR': 1.0, 'SR2': 0.0, 'L1': 1.0, 'L2': 0.0, 'triple_var': 1.0} zmean {'SR': -37.8, 'SR2': 0.0, 'L1': -37.8, 'L2': 0.0, 'triple_var': -31.8}
0.300 stable 3.0 margin 0.042 amb 1.0 ill 0.312 freq {'SR': 1.0, 'SR2': 0.0, 'L1': 1.0, 'L2': 0.0, 'triple_var': 1.0} zmean {'SR': -18.0, 'SR2': -0.1, 'L1': -18.0, 'L2': -0.1, 'triple_var': -16.0}
0.400 stable 3.125 margin 0.173 amb 0.375 ill 1.0 freq {'SR': 1.0, 'SR2': 0.06, 'L1': 1.0, 'L2': 0.06, 'triple_var': 1.0} zmean {'SR': -12.7, 'SR2': -0.2, 'L1': -12.7, 'L2': -0.2, 'triple_var': -11.6}
0.700 stable 2.875 margin 0.407 amb 0.125 ill 1.0 freq {'SR': 1.0, 'SR2': 0.0, 'L1': 1.0, 'L2': 0.0, 'triple_var': 0.88} zmean {'SR': -3.6, 'SR2': -0.3, 'L1': -3.6, 'L2': -0.3, 'triple_var': -3.3}
0.800 stable 1.062 margin 0.529 amb 0.062 ill 1.0 freq {'SR': 0.31, 'SR2': 0.06, 'L1': 0.31, 'L2': 0.06, 'triple_var': 0.31} zmean {'SR': -1.4, 'SR2': -0.4, 'L1': -1.4, 'L2': -0.4, 'triple_var': -1.5}
0.900 stable 0.0 margin 0.54 amb 0.0 ill 1.0 freq {'SR': 0.0, 'SR2': 0.0, 'L1': 0.0, 'L2': 0.0, 'triple_var': 0.0} zmean {'SR': -0.2, 'SR2': -0.1, 'L1': -0.2, 'L2': -0.1, 'triple_var': -0.2}
1.000 stable 0.438 margin 0.583 amb 0.125 ill 1.0 freq {'SR': 0.12, 'SR2': 0.06, 'L1': 0.12, 'L2': 0.06, 'triple_var': 0.06} zmean {'SR': 0.1, 'SR2': 0.2, 'L1': 0.1, 'L2': 0.2, 'triple_var': 0.1}

diff --git a/tools/exp_duality_gate_transfer.py b/tools/exp_duality_gate_transfer.py
new file mode 100644
index 0000000000000000000000000000000000000000..c04f490d17524d73d40766b8b969cdfe3d20077c
--- /dev/null
+++ b/tools/exp_duality_gate_transfer.py
@@ -0,0 +1,284 @@
+#!/usr/bin/env python3
+"""
+exp_duality_gate_transfer.py
+
+Reusable META/DUALITA audit for transferring the BOUNDARY_LAYER_GATE.
+
+Question:
+    Does the denominator-collapse gate discriminate dipolar duality from
+    illusory duality, or does it become a local BOUNDARY metric when moved
+    away from GUE/Poisson?
+
+The script builds a controlled layer map between:
+
+- DIPOLARE: a coherent golden/Beatty gap sequence generated by the orbit of
+  floor((n + phase) * phi). Its gaps are the symbolic det=-1 D-ND pole in the
+  minimal positive alphabet {1, 2}.
+- ILLUSORIA: a full random permutation of the same gaps. This preserves the
+  one-point distribution and first moment but removes the generating order.
+
+Layer beta replaces a beta fraction of dipolar positions with values from the
+illusory permutation. Canonical observables and original-vs-shuffle z gates are
+imported from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
+
+
+OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())
+PHI = (1.0 + 5.0**0.5) / 2.0
+
+
+def dipolar_gaps(n_gaps: int, phase: float) -> np.ndarray:
+    """Return coherent golden Beatty gaps in {1, 2}."""
+    n = np.arange(n_gaps + 1, dtype=float) + float(phase)
+    positions = np.floor(n * PHI).astype(float)
+    gaps = np.diff(positions)
+    mean = float(np.mean(gaps))
+    return gaps / mean if mean > 1e-15 else gaps
+
+
+def beta_layer_gaps(
+    dipolar: np.ndarray,
+    beta: float,
+    rng: np.random.Generator,
+) -> np.ndarray:
+    """Replace a beta fraction of coherent positions with a shuffled copy."""
+    illusory = rng.permutation(dipolar)
+    if beta <= 0.0:
+        out = dipolar.copy()
+    elif beta >= 1.0:
+        out = illusory.copy()
+    else:
+        mask = rng.random(len(dipolar)) < beta
+        out = dipolar.copy()
+        out[mask] = illusory[mask]
+    mean = float(np.mean(out))
+    return out / mean if mean > 1e-15 else out
+
+
+def z_against_shuffle(
+    gaps: np.ndarray,
+    n_baseline: int,
+    rng: np.random.Generator,
+) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
+    """Return original observables, shuffle baseline std, and z scores."""
+    original = compute_canonical(gaps)
+    baseline_vals = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline_vals[name].append(obs[name])
+
+    shuffle_std = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline_vals[name], dtype=float)
+        mean = float(np.mean(vals))
+        sd = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        shuffle_std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, shuffle_std, z
+
+
+def vector(row: dict, names: list[str]) -> np.ndarray:
+    return np.array([row["observables"][name] for name in names], dtype=float)
+
+
+def classify_layers(rows: list[dict], obs_names: list[str]) -> dict:
+    """Classify layers by standardized distance to dipolar/illusory endpoints."""
+    if not obs_names:
+        return {
+            "observables": [],
+            "endpoint_distance": 0.0,
+            "layers": {},
+            "ambiguous_beta": [],
+        }
+
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    dip_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    ill_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([dip_vectors, ill_vectors])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    dip_centroid = np.mean(dip_vectors, axis=0)
+    ill_centroid = np.mean(ill_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((ill_centroid - dip_centroid) / scale))
+
+    layers = {}
+    ambiguous_beta = []
+    for beta, beta_rows in sorted(by_beta.items()):
+        coords = []
+        margins = []
+        labels = []
+        for row in beta_rows:
+            x = vector(row, obs_names)
+            d_dip = float(np.linalg.norm((x - dip_centroid) / scale))
+            d_ill = float(np.linalg.norm((x - ill_centroid) / scale))
+            denom = d_dip + d_ill
+            coord = float((d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("dipolar" if d_dip < d_ill else "illusory")
+        ambiguous_fraction = float(np.mean(np.array(margins) < 0.15))
+        if ambiguous_fraction >= 0.5:
+            ambiguous_beta.append(beta)
+        layers[f"{beta:.3f}"] = {
+            "coordinate_mean": float(np.mean(coords)),
+            "coordinate_std": float(np.std(coords, ddof=1)) if len(coords) > 1 else 0.0,
+            "margin_mean": float(np.mean(margins)),
+            "ambiguous_fraction": ambiguous_fraction,
+            "illusory_label_fraction": float(np.mean(np.array(labels) == "illusory")),
+        }
+
+    return {
+        "observables": obs_names,
+        "endpoint_distance": endpoint_distance,
+        "layers": layers,
+        "ambiguous_beta": ambiguous_beta,
+    }
+
+
+def summarize_gate(rows: list[dict], z_min: float) -> dict:
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    layers = {}
+    for beta, beta_rows in sorted(by_beta.items()):
+        stable_counts = []
+        stable_freq = {name: [] for name in OBS_NAMES}
+        z_values = {name: [] for name in OBS_NAMES}
+        for row in beta_rows:
+            stable = [name for name in OBS_NAMES if abs(row["z"][name]) >= z_min]
+            stable_counts.append(len(stable))
+            for name in OBS_NAMES:
+                stable_freq[name].append(1.0 if name in stable else 0.0)
+                z_values[name].append(row["z"][name])
+        layers[f"{beta:.3f}"] = {
+            "stable_count_mean": float(np.mean(stable_counts)),
+            "stable_count_std": float(np.std(stable_counts, ddof=1)) if len(stable_counts) > 1 else 0.0,
+            "stable_frequency": {name: float(np.mean(vals)) for name, vals in stable_freq.items()},
+            "z_mean": {name: float(np.mean(vals)) for name, vals in z_values.items()},
+        }
+
+    endpoint_stable = []
+    for name in OBS_NAMES:
+        endpoint_rows = by_beta[0.0] + by_beta[1.0]
+        freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in endpoint_rows])
+        if freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def run(args: argparse.Namespace) -> dict:
+    rng = np.random.default_rng(args.seed)
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    rows = []
+
+    for rep in range(args.n_replicates):
+        phase = float(rng.random())
+        base = dipolar_gaps(args.n_gaps, phase)
+        for beta in betas:
+            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+            gaps = beta_layer_gaps(base, beta, layer_rng)
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                n_baseline=args.n_baseline,
+                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "replicate": rep,
+                    "phase": phase,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [name for name in OBS_NAMES if abs(z[name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
+
+    output = {
+        "experiment": "duality_gate_transfer",
+        "category": "gate_transferability",
+        "question": "Does the denominator-collapse gate transfer from BOUNDARY to DUALITA_DIPOLARE_VS_ILLUSORIA?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_endpoint_gated": gated_classification,
+        "rows": rows,
+    }
+
+    out_path = Path(args.out)
+    out_path.parent.mkdir(parents=True, exist_ok=True)
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
+    print(f"observables_used={OBS_NAMES}")
+    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
+    print(
+        "endpoint_distance_all="
+        f"{all_classification['endpoint_distance']:.3f} "
+        "endpoint_distance_gated="
+        f"{gated_classification['endpoint_distance']:.3f}"
+    )
+    source = gated_classification if gate["endpoint_stable_observables"] else all_classification
+    print("beta stable_count margin ambiguous illusory_fraction")
+    for beta in betas:
+        key = f"{beta:.3f}"
+        stable_count = gate["layers"][key]["stable_count_mean"]
+        layer = source["layers"].get(key, {})
+        print(
+            f"{beta:>4.2f} {stable_count:>12.3f} "
+            f"{layer.get('margin_mean', 0.0):>8.3f} "
+            f"{layer.get('ambiguous_fraction', 0.0):>9.3f} "
+            f"{layer.get('illusory_label_fraction', 0.0):>17.3f}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=4096)
+    parser.add_argument("--n-replicates", type=int, default=24)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=40)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=202605070803)
+    parser.add_argument("--out", default="tools/data/duality_gate_transfer.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

exec
/bin/bash -lc "rg -n \"gate_transferability|BOUNDARY_LAYER_GATE|DUALITA_DIPOLARE\" tools/data/seme.json tools/data/consecutio.json tools -g '*.py'" in /opt/MM_D-ND
 succeeded in 0ms:
tools/data/seme.json:28:      "id": "BOUNDARY_LAYER_GATE",
tools/data/seme.json:51:      "id": "DUALITA_DIPOLARE_VS_ILLUSORIA",
tools/data/seme.json:127:    "Tensioni risolte: {'PIANO_PRIMARIO_DUE_ASSIOMI', 'BOUNDARY_LAYER_GATE', 'PERTURBATION_DENOMINATOR_GATE', 'OBSERVABLE_REGISTRY', 'G_POTENZIALE_NULLA', 'TRASCENDENZA_LIMITE', 'DUALITA_DIPOLARE_VS_ILLUSORIA', 'TENSIONE_ENTITA', 'METRIC_TENSOR'}"
tools/exp_duality_gate_transfer.py:5:Reusable META/DUALITA audit for transferring the BOUNDARY_LAYER_GATE.
tools/exp_duality_gate_transfer.py:229:        "category": "gate_transferability",
tools/exp_duality_gate_transfer.py:230:        "question": "Does the denominator-collapse gate transfer from BOUNDARY to DUALITA_DIPOLARE_VS_ILLUSORIA?",
tools/exp_det_drift.py:5:Hypothesis (from operator tension DUALITA_DIPOLARE_VS_ILLUSORIA):

diff --git a/tools/exp_duality_gate_transfer.py b/tools/exp_duality_gate_transfer.py
new file mode 100644
index 0000000000000000000000000000000000000000..c04f490d17524d73d40766b8b969cdfe3d20077c
--- /dev/null
+++ b/tools/exp_duality_gate_transfer.py
@@ -0,0 +1,284 @@
+#!/usr/bin/env python3
+"""
+exp_duality_gate_transfer.py
+
+Reusable META/DUALITA audit for transferring the BOUNDARY_LAYER_GATE.
+
+Question:
+    Does the denominator-collapse gate discriminate dipolar duality from
+    illusory duality, or does it become a local BOUNDARY metric when moved
+    away from GUE/Poisson?
+
+The script builds a controlled layer map between:
+
+- DIPOLARE: a coherent golden/Beatty gap sequence generated by the orbit of
+  floor((n + phase) * phi). Its gaps are the symbolic det=-1 D-ND pole in the
+  minimal positive alphabet {1, 2}.
+- ILLUSORIA: a full random permutation of the same gaps. This preserves the
+  one-point distribution and first moment but removes the generating order.
+
+Layer beta replaces a beta fraction of dipolar positions with values from the
+illusory permutation. Canonical observables and original-vs-shuffle z gates are
+imported from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
+
+
+OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())
+PHI = (1.0 + 5.0**0.5) / 2.0
+
+
+def dipolar_gaps(n_gaps: int, phase: float) -> np.ndarray:
+    """Return coherent golden Beatty gaps in {1, 2}."""
+    n = np.arange(n_gaps + 1, dtype=float) + float(phase)
+    positions = np.floor(n * PHI).astype(float)
+    gaps = np.diff(positions)
+    mean = float(np.mean(gaps))
+    return gaps / mean if mean > 1e-15 else gaps
+
+
+def beta_layer_gaps(
+    dipolar: np.ndarray,
+    beta: float,
+    rng: np.random.Generator,
+) -> np.ndarray:
+    """Replace a beta fraction of coherent positions with a shuffled copy."""
+    illusory = rng.permutation(dipolar)
+    if beta <= 0.0:
+        out = dipolar.copy()
+    elif beta >= 1.0:
+        out = illusory.copy()
+    else:
+        mask = rng.random(len(dipolar)) < beta
+        out = dipolar.copy()
+        out[mask] = illusory[mask]
+    mean = float(np.mean(out))
+    return out / mean if mean > 1e-15 else out
+
+
+def z_against_shuffle(
+    gaps: np.ndarray,
+    n_baseline: int,
+    rng: np.random.Generator,
+) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
+    """Return original observables, shuffle baseline std, and z scores."""
+    original = compute_canonical(gaps)
+    baseline_vals = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline_vals[name].append(obs[name])
+
+    shuffle_std = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline_vals[name], dtype=float)
+        mean = float(np.mean(vals))
+        sd = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        shuffle_std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, shuffle_std, z
+
+
+def vector(row: dict, names: list[str]) -> np.ndarray:
+    return np.array([row["observables"][name] for name in names], dtype=float)
+
+
+def classify_layers(rows: list[dict], obs_names: list[str]) -> dict:
+    """Classify layers by standardized distance to dipolar/illusory endpoints."""
+    if not obs_names:
+        return {
+            "observables": [],
+            "endpoint_distance": 0.0,
+            "layers": {},
+            "ambiguous_beta": [],
+        }
+
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    dip_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    ill_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([dip_vectors, ill_vectors])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    dip_centroid = np.mean(dip_vectors, axis=0)
+    ill_centroid = np.mean(ill_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((ill_centroid - dip_centroid) / scale))
+
+    layers = {}
+    ambiguous_beta = []
+    for beta, beta_rows in sorted(by_beta.items()):
+        coords = []
+        margins = []
+        labels = []
+        for row in beta_rows:
+            x = vector(row, obs_names)
+            d_dip = float(np.linalg.norm((x - dip_centroid) / scale))
+            d_ill = float(np.linalg.norm((x - ill_centroid) / scale))
+            denom = d_dip + d_ill
+            coord = float((d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("dipolar" if d_dip < d_ill else "illusory")
+        ambiguous_fraction = float(np.mean(np.array(margins) < 0.15))
+        if ambiguous_fraction >= 0.5:
+            ambiguous_beta.append(beta)
+        layers[f"{beta:.3f}"] = {
+            "coordinate_mean": float(np.mean(coords)),
+            "coordinate_std": float(np.std(coords, ddof=1)) if len(coords) > 1 else 0.0,
+            "margin_mean": float(np.mean(margins)),
+            "ambiguous_fraction": ambiguous_fraction,
+            "illusory_label_fraction": float(np.mean(np.array(labels) == "illusory")),
+        }
+
+    return {
+        "observables": obs_names,
+        "endpoint_distance": endpoint_distance,
+        "layers": layers,
+        "ambiguous_beta": ambiguous_beta,
+    }
+
+
+def summarize_gate(rows: list[dict], z_min: float) -> dict:
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    layers = {}
+    for beta, beta_rows in sorted(by_beta.items()):
+        stable_counts = []
+        stable_freq = {name: [] for name in OBS_NAMES}
+        z_values = {name: [] for name in OBS_NAMES}
+        for row in beta_rows:
+            stable = [name for name in OBS_NAMES if abs(row["z"][name]) >= z_min]
+            stable_counts.append(len(stable))
+            for name in OBS_NAMES:
+                stable_freq[name].append(1.0 if name in stable else 0.0)
+                z_values[name].append(row["z"][name])
+        layers[f"{beta:.3f}"] = {
+            "stable_count_mean": float(np.mean(stable_counts)),
+            "stable_count_std": float(np.std(stable_counts, ddof=1)) if len(stable_counts) > 1 else 0.0,
+            "stable_frequency": {name: float(np.mean(vals)) for name, vals in stable_freq.items()},
+            "z_mean": {name: float(np.mean(vals)) for name, vals in z_values.items()},
+        }
+
+    endpoint_stable = []
+    for name in OBS_NAMES:
+        endpoint_rows = by_beta[0.0] + by_beta[1.0]
+        freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in endpoint_rows])
+        if freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def run(args: argparse.Namespace) -> dict:
+    rng = np.random.default_rng(args.seed)
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    rows = []
+
+    for rep in range(args.n_replicates):
+        phase = float(rng.random())
+        base = dipolar_gaps(args.n_gaps, phase)
+        for beta in betas:
+            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+            gaps = beta_layer_gaps(base, beta, layer_rng)
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                n_baseline=args.n_baseline,
+                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "replicate": rep,
+                    "phase": phase,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [name for name in OBS_NAMES if abs(z[name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
+
+    output = {
+        "experiment": "duality_gate_transfer",
+        "category": "gate_transferability",
+        "question": "Does the denominator-collapse gate transfer from BOUNDARY to DUALITA_DIPOLARE_VS_ILLUSORIA?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_endpoint_gated": gated_classification,
+        "rows": rows,
+    }
+
+    out_path = Path(args.out)
+    out_path.parent.mkdir(parents=True, exist_ok=True)
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
+    print(f"observables_used={OBS_NAMES}")
+    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
+    print(
+        "endpoint_distance_all="
+        f"{all_classification['endpoint_distance']:.3f} "
+        "endpoint_distance_gated="
+        f"{gated_classification['endpoint_distance']:.3f}"
+    )
+    source = gated_classification if gate["endpoint_stable_observables"] else all_classification
+    print("beta stable_count margin ambiguous illusory_fraction")
+    for beta in betas:
+        key = f"{beta:.3f}"
+        stable_count = gate["layers"][key]["stable_count_mean"]
+        layer = source["layers"].get(key, {})
+        print(
+            f"{beta:>4.2f} {stable_count:>12.3f} "
+            f"{layer.get('margin_mean', 0.0):>8.3f} "
+            f"{layer.get('ambiguous_fraction', 0.0):>9.3f} "
+            f"{layer.get('illusory_label_fraction', 0.0):>17.3f}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=4096)
+    parser.add_argument("--n-replicates", type=int, default=24)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=40)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=202605070803)
+    parser.add_argument("--out", default="tools/data/duality_gate_transfer.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

exec
/bin/bash -lc "sed -n '1,180p' tools/data/seme.json" in /opt/MM_D-ND
 succeeded in 0ms:
{
  "timestamp": "2026-05-07T03:35:25.174109",
  "piano": 69,
  "tensioni": [
    {
      "id": "OBSERVABLE_REGISTRY",
      "tipo": "vincolo",
      "claim": "Ogni script che usa observables canonici (SR, SR2, L1, L2, triple_var) deve importare la definizione da tools/observables_registry.py. Varianti devono usare nomi distinti (SR_local_rigidity, triple_var_normalized) — niente shadowing del nome canonico. Ogni report deve dichiarare 'observables_registry: VERSION' nel header.",
      "intensita": 1.0,
      "porta": "infrastructure",
      "manuale": true,
      "condensato_ref": "A14,A8",
      "origine": "cristallizzato 06/05 dalla consecutio del cycle 20260506_0625 (autopoietico self-finding)",
      "added_at": "2026-05-06T07:03:58.213606+00:00"
    },
    {
      "id": "PERTURBATION_DENOMINATOR_GATE",
      "tipo": "vincolo",
      "claim": "La dimensionalita di perturbazione va riportata solo insieme a PC2, versione observables_registry e gate original-vs-shuffle per osservabile. Nel perimetro 20260506_1941, Poisson e shuffle-primi producono rank_all ~1.8-2.0 con denominatori deboli; dopo gate abs(z)>=2 il rank stabile torna vicino a 1. Rank PCA non gated non e evidenza strutturale.",
      "intensita": 0.95,
      "porta": "META_BOUNDARY",
      "manuale": true,
      "condensato_ref": "A4,A8,A14,C2",
      "origine": "cycle agent_20260506_1941: perturbation rank size curve canonical observables",
      "added_at": "2026-05-06T19:41:00+00:00"
    },
    {
      "id": "BOUNDARY_LAYER_GATE",
      "tipo": "vincolo",
      "claim": "I claim GUE/Poisson boundary devono riportare layer map: versione observables_registry, lista osservabili canonici, z original-vs-shuffle per osservabile, set endpoint-stable, e finestra/layer con margine classificatorio ambiguo. Nel perimetro sintetico agent_20260507_0330, il confine GUE-Poisson e beta 0.3-0.4: margine 0.070-0.083, ambiguous fraction 0.812-0.875, mentre gli osservabili stabili collassano da ~3.3 a 1.6. Il polo Poisson e classificabile ma denominator-weak.",
      "intensita": 0.93,
      "porta": "META_BOUNDARY",
      "manuale": true,
      "condensato_ref": "A4,A8,A9,A14,C2",
      "origine": "cycle agent_20260507_0330: synthetic GUE-Poisson mixture layer gate",
      "added_at": "2026-05-07T03:30:00+00:00"
    },
    {
      "tipo": "confine_inesplorato",
      "id": "TRASCENDENZA_LIMITE",
      "claim": "La trascendenza e il limite attuale del modello. I punti fissi relazionali (non solo phi ma la rete di punti fissi tra osservabili) possono rivelare il vero grafo della realta e pattern nelle matrici. Il confine non e nella matematica - e nel passaggio tra piani.",
      "intensita": 0.9,
      "nota": "Input operatore 2026-04-10. Tocca: confine del modello, struttura relazionale dei punti fissi. Consecutio: quali punti fissi relazionali emergono dalle 21 tensioni attuali? Il grafo e gia nei dati?",
      "manuale": true,
      "porta": "sessione_interattiva",
      "condensato_ref": "A3,A10",
      "condensato_motivo": "Estende A3 (punto fisso singolo) a rete relazionale. Tocca A10 (dipolo) come caso speciale."
    },
    {
      "tipo": "scoperta",
      "id": "DUALITA_DIPOLARE_VS_ILLUSORIA",
      "claim": "Due tipi di dualita: (1) dipolare - generativa, il modello (det=-1), (2) illusoria - dispersiva, entropia (det=+1). Le regole incoerenti producono la seconda. La dualita illusoria e entropia come dispersione, non come informazione.",
      "intensita": 0.9,
      "nota": "Input operatore 2026-04-10. Tocca: entropia come dispersione illusoria vs generazione dipolare. Consecutio: nel Lab i domini Poisson (entropia massima) mostrano dualita illusoria? I domini GUE (strutturati) mostrano dualita dipolare? Il drift verso Poisson (POISSON_CONVERGENCE) e perdita di dualita dipolare?",
      "manuale": true,
      "porta": "sessione_interattiva",
      "condensato_ref": "A2,A10,F5",
      "condensato_motivo": "Discrimina due forme di det. A2 (confine) e la soglia. A10 (dipolo) e il tipo 1. F5 (frame) misura la struttura D-ND che e tipo 1."
    },
    {
      "tipo": "scoperta_numerica",
      "id": "METRIC_TENSOR",
      "claim": "Il tensore metrico dei primi è g=(p/2)². Nel tempo ln(p), è de Sitter 1+1D. z=-8.8 curvatura vs z=+22.5 rapporti ΔΓ.",
      "intensità": 0.9,
      "nota": "Sessione interattiva 4 aprile. Verificato su 78K primi.",
      "manuale": true,
      "porta": "sessione_interattiva",
      "condensato_ref": null,
      "condensato_motivo": "Risultato numerico verificato, non-tautologico"
    },
    {
      "tipo": "scoperta",
      "id": "TENSIONE_ENTITA",
      "claim": "La tensione non e un problema pratico - e un Entita. La tensione superflua crea latenza (tempo). Senza tensione superflua tutto e regolato da assiomi. Implicazione: le tensioni nel seme sono entita, non problemi da risolvere. Quelle superflue (det=+1) producono tempo/latenza.",
      "intensita": 0.85,
      "nota": "Input operatore 2026-04-10. Tocca: rapporto tensione/assioma. Operativamente: discriminare tensioni-entita (generative) da tensioni-superflue (dispersive) nel seme. Le 21 tensioni attuali - quante sono entita e quante latenza?",
      "manuale": true,
      "porta": "sessione_interattiva",
      "condensato_ref": "A5,A6",
      "condensato_motivo": "Il ciclo (A5) lavora con tensioni - ma se la tensione e entita, il ciclo non le risolve, le osserva. Lo zero mobile (A6) e la tensione senza latenza."
    },
    {
      "tipo": "confine_inesplorato",
      "id": "G_POTENZIALE_NULLA",
      "claim": "G e il potenziale di tutto come nulla - permette il prima e il dopo. Ci muoviamo come trascendenza dimensionale gravitazionale. G nel tetraedro non e una teoria tra le altre - e il potenziale che le rende possibili.",
      "intensita": 0.85,
      "nota": "Input operatore 2026-04-10. Tocca: ruolo di G nel tetraedro (T,Q,G,E). La fonte video_lp0RgZ6kQF8 dice: tensore metrico dentro la forma simplettica. G non e accanto a T,Q,E - e sotto. Consecutio: nei dati Lab, i ponti TxG e ExG hanno struttura diversa dai ponti TxQ?",
      "manuale": true,
      "porta": "sessione_interattiva",
      "condensato_ref": "A7,A10",
      "condensato_motivo": "A7 (singolarita come operatore) e G come potenziale. A10 (dipolo) opera sul piano che G rende possibile."
    },
    {
      "tipo": "confine_inesplorato",
      "id": "BOUNDARY",
      "claim": "8 domini GUE, 5 Poisson — il confine è il terzo incluso operativo",
      "intensità": 0.8,
      "nota": "Il segnale non-triviale è DOVE la scissione cambia natura, non che converge a φ",
      "condensato_ref": "A9",
      "condensato_motivo": "Overlap termini con A9 (5 termini)",
      "porta": "condensato"
    },
    {
      "tipo": "confine_inesplorato",
      "id": "PIANO_PRIMARIO_DUE_ASSIOMI",
      "claim": "I piani importanti sono il primario e i due assiomi che lo determinano nelle zone osservate. Non tutti gli assiomi operano ovunque - in ogni zona osservata, due assiomi determinano il piano primario.",
      "intensita": 0.8,
      "nota": "Input operatore 2026-04-10. Tocca: struttura locale degli assiomi. Consecutio: per ogni dominio Lab (primi, logistica, percolazione...) quali 2 assiomi del condensato sono operativi? Mappa assiomi x domini = grafo della realta locale.",
      "manuale": true,
      "porta": "sessione_interattiva",
      "condensato_ref": "A9,A14",
      "condensato_motivo": "A9 (terzo incluso) opera CON il piano. A14 (cascata) propaga - ma propaga cosa, se solo 2 assiomi sono attivi per zona?"
    },
    {
      "tipo": "simmetria_sospetta",
      "id": "META",
      "claim": "Tutti i 11 test passano — verifica che non stiamo testando solo tautologie",
      "intensità": 0.5,
      "nota": "La convergenza a φ è triviale (controprove). I test stanno verificando contenuto o struttura?",
      "condensato_ref": "A4,A12,C2",
      "porta": "verify_assertions_META_ALL_PASS",
      "condensato_motivo": "Ricorrente (3x in 2 giorni) e fuori dalla mappa"
    }
  ],
  "potenziale_bloccato": [],
  "varianza": [
    "Tensioni risolte: {'PIANO_PRIMARIO_DUE_ASSIOMI', 'BOUNDARY_LAYER_GATE', 'PERTURBATION_DENOMINATOR_GATE', 'OBSERVABLE_REGISTRY', 'G_POTENZIALE_NULLA', 'TRASCENDENZA_LIMITE', 'DUALITA_DIPOLARE_VS_ILLUSORIA', 'TENSIONE_ENTITA', 'METRIC_TENSOR'}"
  ],
  "filtro": {
    "promosse": 11,
    "filtrate": 0,
    "condensato_claims": 30
  },
  "direzione": "Esplorare il confine: 8 domini GUE, 5 Poisson — il confine è il terzo incluso operativo",
  "verifica": {
    "pass": 11,
    "fail": 0,
    "skip": 0,
    "total": 11
  },
  "fonti_consumate": 0,
  "fonti_esterne": [
    {
      "id": "video_lp0RgZ6kQF8",
      "title": "Equivalence between geometrical structures and entropy",
      "type": "video_digest",
      "keywords": [
        "geometry",
        "entropy",
        "symplectic form",
        "statistical mechanics",
        "quantum",
        "thermodynamics",
        "inner product",
        "Born rule",
        "metric tensor",
        "electromagnetic tensor"
      ],
      "content": "La geometria degli stati (classici e quantistici) e l'entropia sono la stessa struttura — invertibili. La forma simplettica conta le configurazioni. Il tensore metrico dello spaziotempo appare dentro la forma simplettica estesa. Il tensore elettromagnetico pure. Statistical mechanics non è costruita sopra alla meccanica — è la stessa cosa.",
      "teorie": [
        "T",
        "Q",
        "G",
        "E"
      ],
      "ponti_potenziali": [
        {
          "coppia": "TxQ",
          "ponte": "forma simplettica = entropia (invertibili)",
          "nota": "geometry is entropy and entropy is geometry"
        },
        {
          "coppia": "TxG",
          "ponte": "tensore metrico dentro la forma simplettica estesa",
          "nota": "geometria spaziotempo = geometria degli stati in posizione×velocità"
        },
        {
          "coppia": "ExT",
          "ponte": "tensore EM dentro la forma simplettica",
          "nota": "il campo EM conta stati in configurazione posizione×tempo"

diff --git a/tools/exp_duality_gate_transfer.py b/tools/exp_duality_gate_transfer.py
new file mode 100644
index 0000000000000000000000000000000000000000..c04f490d17524d73d40766b8b969cdfe3d20077c
--- /dev/null
+++ b/tools/exp_duality_gate_transfer.py
@@ -0,0 +1,284 @@
+#!/usr/bin/env python3
+"""
+exp_duality_gate_transfer.py
+
+Reusable META/DUALITA audit for transferring the BOUNDARY_LAYER_GATE.
+
+Question:
+    Does the denominator-collapse gate discriminate dipolar duality from
+    illusory duality, or does it become a local BOUNDARY metric when moved
+    away from GUE/Poisson?
+
+The script builds a controlled layer map between:
+
+- DIPOLARE: a coherent golden/Beatty gap sequence generated by the orbit of
+  floor((n + phase) * phi). Its gaps are the symbolic det=-1 D-ND pole in the
+  minimal positive alphabet {1, 2}.
+- ILLUSORIA: a full random permutation of the same gaps. This preserves the
+  one-point distribution and first moment but removes the generating order.
+
+Layer beta replaces a beta fraction of dipolar positions with values from the
+illusory permutation. Canonical observables and original-vs-shuffle z gates are
+imported from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
+
+
+OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())
+PHI = (1.0 + 5.0**0.5) / 2.0
+
+
+def dipolar_gaps(n_gaps: int, phase: float) -> np.ndarray:
+    """Return coherent golden Beatty gaps in {1, 2}."""
+    n = np.arange(n_gaps + 1, dtype=float) + float(phase)
+    positions = np.floor(n * PHI).astype(float)
+    gaps = np.diff(positions)
+    mean = float(np.mean(gaps))
+    return gaps / mean if mean > 1e-15 else gaps
+
+
+def beta_layer_gaps(
+    dipolar: np.ndarray,
+    beta: float,
+    rng: np.random.Generator,
+) -> np.ndarray:
+    """Replace a beta fraction of coherent positions with a shuffled copy."""
+    illusory = rng.permutation(dipolar)
+    if beta <= 0.0:
+        out = dipolar.copy()
+    elif beta >= 1.0:
+        out = illusory.copy()
+    else:
+        mask = rng.random(len(dipolar)) < beta
+        out = dipolar.copy()
+        out[mask] = illusory[mask]
+    mean = float(np.mean(out))
+    return out / mean if mean > 1e-15 else out
+
+
+def z_against_shuffle(
+    gaps: np.ndarray,
+    n_baseline: int,
+    rng: np.random.Generator,
+) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
+    """Return original observables, shuffle baseline std, and z scores."""
+    original = compute_canonical(gaps)
+    baseline_vals = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline_vals[name].append(obs[name])
+
+    shuffle_std = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline_vals[name], dtype=float)
+        mean = float(np.mean(vals))
+        sd = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        shuffle_std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, shuffle_std, z
+
+
+def vector(row: dict, names: list[str]) -> np.ndarray:
+    return np.array([row["observables"][name] for name in names], dtype=float)
+
+
+def classify_layers(rows: list[dict], obs_names: list[str]) -> dict:
+    """Classify layers by standardized distance to dipolar/illusory endpoints."""
+    if not obs_names:
+        return {
+            "observables": [],
+            "endpoint_distance": 0.0,
+            "layers": {},
+            "ambiguous_beta": [],
+        }
+
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    dip_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    ill_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([dip_vectors, ill_vectors])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    dip_centroid = np.mean(dip_vectors, axis=0)
+    ill_centroid = np.mean(ill_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((ill_centroid - dip_centroid) / scale))
+
+    layers = {}
+    ambiguous_beta = []
+    for beta, beta_rows in sorted(by_beta.items()):
+        coords = []
+        margins = []
+        labels = []
+        for row in beta_rows:
+            x = vector(row, obs_names)
+            d_dip = float(np.linalg.norm((x - dip_centroid) / scale))
+            d_ill = float(np.linalg.norm((x - ill_centroid) / scale))
+            denom = d_dip + d_ill
+            coord = float((d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("dipolar" if d_dip < d_ill else "illusory")
+        ambiguous_fraction = float(np.mean(np.array(margins) < 0.15))
+        if ambiguous_fraction >= 0.5:
+            ambiguous_beta.append(beta)
+        layers[f"{beta:.3f}"] = {
+            "coordinate_mean": float(np.mean(coords)),
+            "coordinate_std": float(np.std(coords, ddof=1)) if len(coords) > 1 else 0.0,
+            "margin_mean": float(np.mean(margins)),
+            "ambiguous_fraction": ambiguous_fraction,
+            "illusory_label_fraction": float(np.mean(np.array(labels) == "illusory")),
+        }
+
+    return {
+        "observables": obs_names,
+        "endpoint_distance": endpoint_distance,
+        "layers": layers,
+        "ambiguous_beta": ambiguous_beta,
+    }
+
+
+def summarize_gate(rows: list[dict], z_min: float) -> dict:
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    layers = {}
+    for beta, beta_rows in sorted(by_beta.items()):
+        stable_counts = []
+        stable_freq = {name: [] for name in OBS_NAMES}
+        z_values = {name: [] for name in OBS_NAMES}
+        for row in beta_rows:
+            stable = [name for name in OBS_NAMES if abs(row["z"][name]) >= z_min]
+            stable_counts.append(len(stable))
+            for name in OBS_NAMES:
+                stable_freq[name].append(1.0 if name in stable else 0.0)
+                z_values[name].append(row["z"][name])
+        layers[f"{beta:.3f}"] = {
+            "stable_count_mean": float(np.mean(stable_counts)),
+            "stable_count_std": float(np.std(stable_counts, ddof=1)) if len(stable_counts) > 1 else 0.0,
+            "stable_frequency": {name: float(np.mean(vals)) for name, vals in stable_freq.items()},
+            "z_mean": {name: float(np.mean(vals)) for name, vals in z_values.items()},
+        }
+
+    endpoint_stable = []
+    for name in OBS_NAMES:
+        endpoint_rows = by_beta[0.0] + by_beta[1.0]
+        freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in endpoint_rows])
+        if freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def run(args: argparse.Namespace) -> dict:
+    rng = np.random.default_rng(args.seed)
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    rows = []
+
+    for rep in range(args.n_replicates):
+        phase = float(rng.random())
+        base = dipolar_gaps(args.n_gaps, phase)
+        for beta in betas:
+            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+            gaps = beta_layer_gaps(base, beta, layer_rng)
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                n_baseline=args.n_baseline,
+                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "replicate": rep,
+                    "phase": phase,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [name for name in OBS_NAMES if abs(z[name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
+
+    output = {
+        "experiment": "duality_gate_transfer",
+        "category": "gate_transferability",
+        "question": "Does the denominator-collapse gate transfer from BOUNDARY to DUALITA_DIPOLARE_VS_ILLUSORIA?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_endpoint_gated": gated_classification,
+        "rows": rows,
+    }
+
+    out_path = Path(args.out)
+    out_path.parent.mkdir(parents=True, exist_ok=True)
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
+    print(f"observables_used={OBS_NAMES}")
+    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
+    print(
+        "endpoint_distance_all="
+        f"{all_classification['endpoint_distance']:.3f} "
+        "endpoint_distance_gated="
+        f"{gated_classification['endpoint_distance']:.3f}"
+    )
+    source = gated_classification if gate["endpoint_stable_observables"] else all_classification
+    print("beta stable_count margin ambiguous illusory_fraction")
+    for beta in betas:
+        key = f"{beta:.3f}"
+        stable_count = gate["layers"][key]["stable_count_mean"]
+        layer = source["layers"].get(key, {})
+        print(
+            f"{beta:>4.2f} {stable_count:>12.3f} "
+            f"{layer.get('margin_mean', 0.0):>8.3f} "
+            f"{layer.get('ambiguous_fraction', 0.0):>9.3f} "
+            f"{layer.get('illusory_label_fraction', 0.0):>17.3f}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=4096)
+    parser.add_argument("--n-replicates", type=int, default=24)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=40)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=202605070803)
+    parser.add_argument("--out", default="tools/data/duality_gate_transfer.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

exec
/bin/bash -lc "rg --files tools | rg 'falsifier|valutatore|trajectory'" in /opt/MM_D-ND
 succeeded in 0ms:
tools/lab_valutatore.py
tools/lab_trajectory_apply.py
tools/data/valutatore_log.jsonl
tools/lab_falsifier.py
tools/data/reports/falsifier_20260506_1941.json
tools/data/reports/falsifier_20260430_1905.json
tools/data/reports/falsifier_20260503_0330.json
tools/data/reports/falsifier_20260505_0330.json
tools/data/reports/falsifier_20260507_0330.raw.txt
tools/data/reports/_quarantine_falsifier_29_04/agent_20260429_0852.md
tools/data/reports/_quarantine_falsifier_29_04/falsifier_20260429_0958.json
tools/data/reports/_quarantine_falsifier_29_04/falsifier_20260429_0852.json
tools/data/reports/_quarantine_falsifier_29_04/agent_20260429_0833.md
tools/data/reports/_quarantine_falsifier_29_04/agent_20260429_0958.md
tools/data/reports/_quarantine_falsifier_29_04/evolution_20260429_0852.md
tools/data/reports/_quarantine_falsifier_29_04/evolution_20260429_0958.md
tools/data/reports/_quarantine_falsifier_29_04/agent_20260429_0829.md
tools/data/reports/falsifier_20260504_0901.json
tools/data/reports/falsifier_20260506_0625.json
tools/data/reports/falsifier_20260506_1955.json
tools/data/reports/falsifier_20260429_1013.json
tools/data/reports/falsifier_20260430_0330.json
tools/data/reports/trajectory_apply_20260506_1955.json
tools/data/reports/falsifier_20260501_0931.json
tools/data/reports/falsifier_20260507_0330.json
tools/data/reports/falsifier_20260506_0330.raw.txt
tools/data/reports/falsifier_20260506_1955.raw.txt
tools/data/reports/trajectory_apply_20260506_1941.json
tools/data/reports/falsifier_20260429_1041.json
tools/data/reports/trajectory_apply_20260506_1931.json
tools/data/reports/falsifier_20260504_1219.json
tools/data/reports/falsifier_20260502_0330.json
tools/data/reports/falsifier_20260430_1919.json
tools/data/reports/trajectory_apply_20260507_0803.json
tools/data/reports/falsifier_20260430_1946.json
tools/data/reports/falsifier_20260501_0725.json
tools/data/reports/falsifier_20260501_0330.json
tools/data/reports/trajectory_apply_20260507_0330.json

diff --git a/tools/exp_duality_gate_transfer.py b/tools/exp_duality_gate_transfer.py
new file mode 100644
index 0000000000000000000000000000000000000000..c04f490d17524d73d40766b8b969cdfe3d20077c
--- /dev/null
+++ b/tools/exp_duality_gate_transfer.py
@@ -0,0 +1,284 @@
+#!/usr/bin/env python3
+"""
+exp_duality_gate_transfer.py
+
+Reusable META/DUALITA audit for transferring the BOUNDARY_LAYER_GATE.
+
+Question:
+    Does the denominator-collapse gate discriminate dipolar duality from
+    illusory duality, or does it become a local BOUNDARY metric when moved
+    away from GUE/Poisson?
+
+The script builds a controlled layer map between:
+
+- DIPOLARE: a coherent golden/Beatty gap sequence generated by the orbit of
+  floor((n + phase) * phi). Its gaps are the symbolic det=-1 D-ND pole in the
+  minimal positive alphabet {1, 2}.
+- ILLUSORIA: a full random permutation of the same gaps. This preserves the
+  one-point distribution and first moment but removes the generating order.
+
+Layer beta replaces a beta fraction of dipolar positions with values from the
+illusory permutation. Canonical observables and original-vs-shuffle z gates are
+imported from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
+
+
+OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())
+PHI = (1.0 + 5.0**0.5) / 2.0
+
+
+def dipolar_gaps(n_gaps: int, phase: float) -> np.ndarray:
+    """Return coherent golden Beatty gaps in {1, 2}."""
+    n = np.arange(n_gaps + 1, dtype=float) + float(phase)
+    positions = np.floor(n * PHI).astype(float)
+    gaps = np.diff(positions)
+    mean = float(np.mean(gaps))
+    return gaps / mean if mean > 1e-15 else gaps
+
+
+def beta_layer_gaps(
+    dipolar: np.ndarray,
+    beta: float,
+    rng: np.random.Generator,
+) -> np.ndarray:
+    """Replace a beta fraction of coherent positions with a shuffled copy."""
+    illusory = rng.permutation(dipolar)
+    if beta <= 0.0:
+        out = dipolar.copy()
+    elif beta >= 1.0:
+        out = illusory.copy()
+    else:
+        mask = rng.random(len(dipolar)) < beta
+        out = dipolar.copy()
+        out[mask] = illusory[mask]
+    mean = float(np.mean(out))
+    return out / mean if mean > 1e-15 else out
+
+
+def z_against_shuffle(
+    gaps: np.ndarray,
+    n_baseline: int,
+    rng: np.random.Generator,
+) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
+    """Return original observables, shuffle baseline std, and z scores."""
+    original = compute_canonical(gaps)
+    baseline_vals = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline_vals[name].append(obs[name])
+
+    shuffle_std = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline_vals[name], dtype=float)
+        mean = float(np.mean(vals))
+        sd = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        shuffle_std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, shuffle_std, z
+
+
+def vector(row: dict, names: list[str]) -> np.ndarray:
+    return np.array([row["observables"][name] for name in names], dtype=float)
+
+
+def classify_layers(rows: list[dict], obs_names: list[str]) -> dict:
+    """Classify layers by standardized distance to dipolar/illusory endpoints."""
+    if not obs_names:
+        return {
+            "observables": [],
+            "endpoint_distance": 0.0,
+            "layers": {},
+            "ambiguous_beta": [],
+        }
+
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    dip_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    ill_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([dip_vectors, ill_vectors])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    dip_centroid = np.mean(dip_vectors, axis=0)
+    ill_centroid = np.mean(ill_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((ill_centroid - dip_centroid) / scale))
+
+    layers = {}
+    ambiguous_beta = []
+    for beta, beta_rows in sorted(by_beta.items()):
+        coords = []
+        margins = []
+        labels = []
+        for row in beta_rows:
+            x = vector(row, obs_names)
+            d_dip = float(np.linalg.norm((x - dip_centroid) / scale))
+            d_ill = float(np.linalg.norm((x - ill_centroid) / scale))
+            denom = d_dip + d_ill
+            coord = float((d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("dipolar" if d_dip < d_ill else "illusory")
+        ambiguous_fraction = float(np.mean(np.array(margins) < 0.15))
+        if ambiguous_fraction >= 0.5:
+            ambiguous_beta.append(beta)
+        layers[f"{beta:.3f}"] = {
+            "coordinate_mean": float(np.mean(coords)),
+            "coordinate_std": float(np.std(coords, ddof=1)) if len(coords) > 1 else 0.0,
+            "margin_mean": float(np.mean(margins)),
+            "ambiguous_fraction": ambiguous_fraction,
+            "illusory_label_fraction": float(np.mean(np.array(labels) == "illusory")),
+        }
+
+    return {
+        "observables": obs_names,
+        "endpoint_distance": endpoint_distance,
+        "layers": layers,
+        "ambiguous_beta": ambiguous_beta,
+    }
+
+
+def summarize_gate(rows: list[dict], z_min: float) -> dict:
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    layers = {}
+    for beta, beta_rows in sorted(by_beta.items()):
+        stable_counts = []
+        stable_freq = {name: [] for name in OBS_NAMES}
+        z_values = {name: [] for name in OBS_NAMES}
+        for row in beta_rows:
+            stable = [name for name in OBS_NAMES if abs(row["z"][name]) >= z_min]
+            stable_counts.append(len(stable))
+            for name in OBS_NAMES:
+                stable_freq[name].append(1.0 if name in stable else 0.0)
+                z_values[name].append(row["z"][name])
+        layers[f"{beta:.3f}"] = {
+            "stable_count_mean": float(np.mean(stable_counts)),
+            "stable_count_std": float(np.std(stable_counts, ddof=1)) if len(stable_counts) > 1 else 0.0,
+            "stable_frequency": {name: float(np.mean(vals)) for name, vals in stable_freq.items()},
+            "z_mean": {name: float(np.mean(vals)) for name, vals in z_values.items()},
+        }
+
+    endpoint_stable = []
+    for name in OBS_NAMES:
+        endpoint_rows = by_beta[0.0] + by_beta[1.0]
+        freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in endpoint_rows])
+        if freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def run(args: argparse.Namespace) -> dict:
+    rng = np.random.default_rng(args.seed)
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    rows = []
+
+    for rep in range(args.n_replicates):
+        phase = float(rng.random())
+        base = dipolar_gaps(args.n_gaps, phase)
+        for beta in betas:
+            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+            gaps = beta_layer_gaps(base, beta, layer_rng)
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                n_baseline=args.n_baseline,
+                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "replicate": rep,
+                    "phase": phase,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [name for name in OBS_NAMES if abs(z[name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
+
+    output = {
+        "experiment": "duality_gate_transfer",
+        "category": "gate_transferability",
+        "question": "Does the denominator-collapse gate transfer from BOUNDARY to DUALITA_DIPOLARE_VS_ILLUSORIA?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_endpoint_gated": gated_classification,
+        "rows": rows,
+    }
+
+    out_path = Path(args.out)
+    out_path.parent.mkdir(parents=True, exist_ok=True)
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
+    print(f"observables_used={OBS_NAMES}")
+    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
+    print(
+        "endpoint_distance_all="
+        f"{all_classification['endpoint_distance']:.3f} "
+        "endpoint_distance_gated="
+        f"{gated_classification['endpoint_distance']:.3f}"
+    )
+    source = gated_classification if gate["endpoint_stable_observables"] else all_classification
+    print("beta stable_count margin ambiguous illusory_fraction")
+    for beta in betas:
+        key = f"{beta:.3f}"
+        stable_count = gate["layers"][key]["stable_count_mean"]
+        layer = source["layers"].get(key, {})
+        print(
+            f"{beta:>4.2f} {stable_count:>12.3f} "
+            f"{layer.get('margin_mean', 0.0):>8.3f} "
+            f"{layer.get('ambiguous_fraction', 0.0):>9.3f} "
+            f"{layer.get('illusory_label_fraction', 0.0):>17.3f}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=4096)
+    parser.add_argument("--n-replicates", type=int, default=24)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=40)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=202605070803)
+    parser.add_argument("--out", default="tools/data/duality_gate_transfer.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

codex
Il report va scritto con un verdetto non assolutizzato: il gate distingue il polo dipolare dal polo illusorio, però la coordinata della layer BOUNDARY non si conserva. Quindi cristallizzo “operatore trasferibile, forma non invariante”, con auto-audit contro silent patching.
apply patch
patch: completed
/opt/MM_D-ND/tools/data/reports/agent_20260507_0803.md
diff --git a/tools/data/reports/agent_20260507_0803.md b/tools/data/reports/agent_20260507_0803.md
new file mode 100644
index 0000000000000000000000000000000000000000..c3a0e5f46f655a3a25c8e6d2f54ec978989c54ca
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0803.md
@@ -0,0 +1,155 @@
+# Agent Report — Denominator Gate Transfers, Boundary Coordinate Does Not
+**Date**: 2026-05-07 08:03  
+**Piano**: 69  
+**Category**: gate_transferability  
+**Tension explored**: META + DUALITA_DIPOLARE_VS_ILLUSORIA  
+**verdict**: operator  
+observables_registry: 1.0.0-2026-05-06  
+observables_used: [SR, SR2, L1, L2, triple_var]
+
+## Claim Under Test
+The operator directive asked whether the `BOUNDARY_LAYER_GATE` forged on the
+GUE/Poisson boundary is a transferable operator or only a local metric.
+
+Concrete test:
+
+> Apply the denominator-collapse gate to the discrimination between dipolar
+> duality and illusory duality. If the gate distinguishes structural generation
+> from incoherent dispersion, it is an operator. If it only emits syntactically
+> valid but empty layer maps, it is a BOUNDARY-local metric.
+
+## Experiment
+Tool created: `tools/exp_duality_gate_transfer.py`
+
+Atomic perimeter:
+- DIPOLARE: coherent golden Beatty gaps generated by `floor((n+phase)*phi)`;
+- ILLUSORIA: a random permutation of the same gaps, preserving one-point
+  distribution and first moment but removing the generating order;
+- beta layer: replace a beta fraction of dipolar positions with values from
+  the illusory permutation;
+- main run: 4,096 gaps, 24 replicates, 11 beta layers, 40 shuffle baselines;
+- seed check: 3,072 gaps, 16 replicates, 11 beta layers, 32 shuffle baselines;
+- denominator gate: observable stable when `abs(z original-vs-shuffle) >= 2`;
+- classification: standardized distance to beta 0.0 and beta 1.0 centroids
+  using all five canonical observables.
+
+This is not a new BOUNDARY experiment. GUE and Poisson do not appear in the
+generator. The only transferred object is the denominator gate.
+
+## Results
+
+### Main Run
+
+Endpoint separation using all canonical observables: `3.436` standardized units.  
+Endpoint-stable observables at frequency >= 0.75 across both endpoints: `[]`.
+
+| beta | stable obs / 5 | margin | ambiguous fraction | illusory-label fraction |
+|---:|---:|---:|---:|---:|
+| 0.0 | 3.000 | 0.971 | 0.000 | 0.000 |
+| 0.1 | 3.000 | 0.479 | 0.000 | 0.000 |
+| 0.2 | 3.000 | 0.234 | 0.125 | 0.000 |
+| 0.3 | 3.000 | 0.033 | 1.000 | 0.375 |
+| 0.4 | 3.083 | 0.154 | 0.417 | 0.958 |
+| 0.5 | 3.083 | 0.269 | 0.375 | 1.000 |
+| 0.6 | 3.000 | 0.417 | 0.042 | 1.000 |
+| 0.7 | 2.917 | 0.316 | 0.167 | 1.000 |
+| 0.8 | 1.708 | 0.473 | 0.125 | 1.000 |
+| 0.9 | 0.167 | 0.463 | 0.042 | 1.000 |
+| 1.0 | 0.208 | 0.442 | 0.042 | 1.000 |
+
+Observable stability frequencies:
+- beta 0.0: `SR=1.00`, `L1=1.00`, `triple_var=1.00`; `SR2=0.00`, `L2=0.00`;
+- beta 0.3: same stable trio, with mean z approximately `SR=-19.4`, `L1=-19.4`, `triple_var=-17.6`;
+- beta 0.8: stable trio falls to frequency `0.54` each, mean z around `-1.7` to `-1.9`;
+- beta 0.9: all canonical observables are weak or near weak, mean z around `-0.4..+0.2`;
+- beta 1.0: all canonical observables are weak, mean z around `+0.1..+0.2`.
+
+### Seed Check
+
+The seed check repeated the same structure:
+- endpoint-stable observables: `[]`;
+- all-observable endpoint distance: `3.412`;
+- ambiguous beta: `[0.3]`;
+- beta 0.0 stable count: `3.000`;
+- beta 0.3 stable count: `3.000`, ambiguous fraction `1.000`;
+- beta 0.8 stable count: `1.062`;
+- beta 0.9 stable count: `0.000`;
+- beta 1.0 stable count: `0.438`.
+
+## Findings
+
+1. **The gate does not degenerate on DUALITA.** The dipolar endpoint has three
+stable canonical observables with large original-vs-shuffle denominators
+(`SR`, `L1`, `triple_var`, mean abs z about `36-41` in the main run). The
+illusory endpoint has no stable denominator support. This is a structural
+original-vs-shuffle distinction, not an empty syntactic map.
+
+2. **The BOUNDARY layer coordinate does not transfer unchanged.** In the
+GUE/Poisson run, beta 0.3-0.4 carried both classification ambiguity and
+denominator collapse. Here, beta 0.3 is classification-ambiguous, but the
+denominator support is still strong: stable count remains `3.000/5`.
+Denominator collapse arrives later, around beta 0.8-0.9. Therefore the
+operator transfers, but the specific BOUNDARY layer shape is local.
+
+3. **Endpoint-gated classification remains empty for the same structural
+reason as BOUNDARY.** The endpoint-stable set is empty because the illusory
+pole is denominator-weak. This does not erase the discrimination; it prevents
+symmetric endpoint-gated retention claims. The valid claim is one-sided:
+coherent dipolar order survives original-vs-shuffle gating; illusory dispersion
+does not.
+
+4. **The discriminant is order, not marginal distribution.** The illusory
+sequence preserves the same alphabet, first moment, and one-point distribution
+as the dipolar sequence. The gate is therefore not measuring the marginal
+composition of gaps. It measures whether the canonical observables retain an
+ordered denominator against full shuffle.
+
+## Verdict
+**category: gate_transferability**  
+**verdict: operator**
+
+Scoped statement:
+
+> In this synthetic DUALITA perimeter, the denominator gate is transferable as
+> an operator for structural order: it separates dipolar generation from
+> illusory dispersion with replicated z support. The beta coordinate of the
+> GUE/Poisson boundary layer is not transferable: ambiguity appears near beta
+> 0.3, while denominator collapse appears near beta 0.8-0.9.
+
+So the last BOUNDARY cycles forged a lens, but one parameter of that lens was
+local to BOUNDARY. The transferable object is not "beta 0.3-0.4"; it is:
+
+> report layer maps as classifier margin plus original-vs-shuffle denominator
+> support, and treat denominator-weak poles as asymmetric structural poles.
+
+## Consecutio
+What opens now: apply the same transfer test to `TRASCENDENZA_LIMITE` and
+`G_POTENZIALE_NULLA`, but separate two quantities from the start:
+
+1. classification ambiguity layer;
+2. denominator-collapse layer.
+
+If both layers coincide in a domain, the domain has a BOUNDARY-like transition.
+If they split, as they do here, the gate is still useful but the transition
+coordinate belongs to the target domain, not to the operator.
+
+## Auto-audit: 5 lenti
+- **L1 hard constraint vs bias**: no "always/never/zero" claim. "No endpoint
+  stable observables" means none reached frequency >= 0.75 across both
+  endpoints under `abs(z) >= 2`.
+- **L2 quantity vs ratio**: classification margin is reported together with
+  stable-observable count and z means. Ratios are not interpreted without
+  denominator support.
+- **L3 no silent patching**: the report explicitly separates "gate transfers"
+  from "BOUNDARY beta coordinate transfers." The second is not claimed.
+- **L4 edge cases**: beta 0.4 has ambiguous fraction `0.417`, so only beta 0.3
+  is listed as the replicated ambiguous layer under the >= 0.5 rule.
+- **L5 re-discovery**: this is a finite symbolic-order vs permutation audit,
+  related to standard shuffle/null testing. It is not tagged as a new theorem
+  about Sturmian or Beatty sequences.
+
+## Files
+- Script: `tools/exp_duality_gate_transfer.py`
+- Main data: `tools/data/duality_gate_transfer_20260507_0803.json`
+- Seed check: `tools/data/duality_gate_transfer_20260507_0803_seedcheck.json`
+- Report: `tools/data/reports/agent_20260507_0803.md`
diff --git a/tools/exp_duality_gate_transfer.py b/tools/exp_duality_gate_transfer.py
new file mode 100644
index 0000000000000000000000000000000000000000..c04f490d17524d73d40766b8b969cdfe3d20077c
--- /dev/null
+++ b/tools/exp_duality_gate_transfer.py
@@ -0,0 +1,284 @@
+#!/usr/bin/env python3
+"""
+exp_duality_gate_transfer.py
+
+Reusable META/DUALITA audit for transferring the BOUNDARY_LAYER_GATE.
+
+Question:
+    Does the denominator-collapse gate discriminate dipolar duality from
+    illusory duality, or does it become a local BOUNDARY metric when moved
+    away from GUE/Poisson?
+
+The script builds a controlled layer map between:
+
+- DIPOLARE: a coherent golden/Beatty gap sequence generated by the orbit of
+  floor((n + phase) * phi). Its gaps are the symbolic det=-1 D-ND pole in the
+  minimal positive alphabet {1, 2}.
+- ILLUSORIA: a full random permutation of the same gaps. This preserves the
+  one-point distribution and first moment but removes the generating order.
+
+Layer beta replaces a beta fraction of dipolar positions with values from the
+illusory permutation. Canonical observables and original-vs-shuffle z gates are
+imported from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
+
+
+OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())
+PHI = (1.0 + 5.0**0.5) / 2.0
+
+
+def dipolar_gaps(n_gaps: int, phase: float) -> np.ndarray:
+    """Return coherent golden Beatty gaps in {1, 2}."""
+    n = np.arange(n_gaps + 1, dtype=float) + float(phase)
+    positions = np.floor(n * PHI).astype(float)
+    gaps = np.diff(positions)
+    mean = float(np.mean(gaps))
+    return gaps / mean if mean > 1e-15 else gaps
+
+
+def beta_layer_gaps(
+    dipolar: np.ndarray,
+    beta: float,
+    rng: np.random.Generator,
+) -> np.ndarray:
+    """Replace a beta fraction of coherent positions with a shuffled copy."""
+    illusory = rng.permutation(dipolar)
+    if beta <= 0.0:
+        out = dipolar.copy()
+    elif beta >= 1.0:
+        out = illusory.copy()
+    else:
+        mask = rng.random(len(dipolar)) < beta
+        out = dipolar.copy()
+        out[mask] = illusory[mask]
+    mean = float(np.mean(out))
+    return out / mean if mean > 1e-15 else out
+
+
+def z_against_shuffle(
+    gaps: np.ndarray,
+    n_baseline: int,
+    rng: np.random.Generator,
+) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
+    """Return original observables, shuffle baseline std, and z scores."""
+    original = compute_canonical(gaps)
+    baseline_vals = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline_vals[name].append(obs[name])
+
+    shuffle_std = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline_vals[name], dtype=float)
+        mean = float(np.mean(vals))
+        sd = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        shuffle_std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, shuffle_std, z
+
+
+def vector(row: dict, names: list[str]) -> np.ndarray:
+    return np.array([row["observables"][name] for name in names], dtype=float)
+
+
+def classify_layers(rows: list[dict], obs_names: list[str]) -> dict:
+    """Classify layers by standardized distance to dipolar/illusory endpoints."""
+    if not obs_names:
+        return {
+            "observables": [],
+            "endpoint_distance": 0.0,
+            "layers": {},
+            "ambiguous_beta": [],
+        }
+
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    dip_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    ill_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([dip_vectors, ill_vectors])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    dip_centroid = np.mean(dip_vectors, axis=0)
+    ill_centroid = np.mean(ill_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((ill_centroid - dip_centroid) / scale))
+
+    layers = {}
+    ambiguous_beta = []
+    for beta, beta_rows in sorted(by_beta.items()):
+        coords = []
+        margins = []
+        labels = []
+        for row in beta_rows:
+            x = vector(row, obs_names)
+            d_dip = float(np.linalg.norm((x - dip_centroid) / scale))
+            d_ill = float(np.linalg.norm((x - ill_centroid) / scale))
+            denom = d_dip + d_ill
+            coord = float((d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("dipolar" if d_dip < d_ill else "illusory")
+        ambiguous_fraction = float(np.mean(np.array(margins) < 0.15))
+        if ambiguous_fraction >= 0.5:
+            ambiguous_beta.append(beta)
+        layers[f"{beta:.3f}"] = {
+            "coordinate_mean": float(np.mean(coords)),
+            "coordinate_std": float(np.std(coords, ddof=1)) if len(coords) > 1 else 0.0,
+            "margin_mean": float(np.mean(margins)),
+            "ambiguous_fraction": ambiguous_fraction,
+            "illusory_label_fraction": float(np.mean(np.array(labels) == "illusory")),
+        }
+
+    return {
+        "observables": obs_names,
+        "endpoint_distance": endpoint_distance,
+        "layers": layers,
+        "ambiguous_beta": ambiguous_beta,
+    }
+
+
+def summarize_gate(rows: list[dict], z_min: float) -> dict:
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    layers = {}
+    for beta, beta_rows in sorted(by_beta.items()):
+        stable_counts = []
+        stable_freq = {name: [] for name in OBS_NAMES}
+        z_values = {name: [] for name in OBS_NAMES}
+        for row in beta_rows:
+            stable = [name for name in OBS_NAMES if abs(row["z"][name]) >= z_min]
+            stable_counts.append(len(stable))
+            for name in OBS_NAMES:
+                stable_freq[name].append(1.0 if name in stable else 0.0)
+                z_values[name].append(row["z"][name])
+        layers[f"{beta:.3f}"] = {
+            "stable_count_mean": float(np.mean(stable_counts)),
+            "stable_count_std": float(np.std(stable_counts, ddof=1)) if len(stable_counts) > 1 else 0.0,
+            "stable_frequency": {name: float(np.mean(vals)) for name, vals in stable_freq.items()},
+            "z_mean": {name: float(np.mean(vals)) for name, vals in z_values.items()},
+        }
+
+    endpoint_stable = []
+    for name in OBS_NAMES:
+        endpoint_rows = by_beta[0.0] + by_beta[1.0]
+        freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in endpoint_rows])
+        if freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def run(args: argparse.Namespace) -> dict:
+    rng = np.random.default_rng(args.seed)
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    rows = []
+
+    for rep in range(args.n_replicates):
+        phase = float(rng.random())
+        base = dipolar_gaps(args.n_gaps, phase)
+        for beta in betas:
+            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+            gaps = beta_layer_gaps(base, beta, layer_rng)
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                n_baseline=args.n_baseline,
+                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "replicate": rep,
+                    "phase": phase,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [name for name in OBS_NAMES if abs(z[name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
+
+    output = {
+        "experiment": "duality_gate_transfer",
+        "category": "gate_transferability",
+        "question": "Does the denominator-collapse gate transfer from BOUNDARY to DUALITA_DIPOLARE_VS_ILLUSORIA?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_endpoint_gated": gated_classification,
+        "rows": rows,
+    }
+
+    out_path = Path(args.out)
+    out_path.parent.mkdir(parents=True, exist_ok=True)
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
+    print(f"observables_used={OBS_NAMES}")
+    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
+    print(
+        "endpoint_distance_all="
+        f"{all_classification['endpoint_distance']:.3f} "
+        "endpoint_distance_gated="
+        f"{gated_classification['endpoint_distance']:.3f}"
+    )
+    source = gated_classification if gate["endpoint_stable_observables"] else all_classification
+    print("beta stable_count margin ambiguous illusory_fraction")
+    for beta in betas:
+        key = f"{beta:.3f}"
+        stable_count = gate["layers"][key]["stable_count_mean"]
+        layer = source["layers"].get(key, {})
+        print(
+            f"{beta:>4.2f} {stable_count:>12.3f} "
+            f"{layer.get('margin_mean', 0.0):>8.3f} "
+            f"{layer.get('ambiguous_fraction', 0.0):>9.3f} "
+            f"{layer.get('illusory_label_fraction', 0.0):>17.3f}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=4096)
+    parser.add_argument("--n-replicates", type=int, default=24)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=40)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=202605070803)
+    parser.add_argument("--out", default="tools/data/duality_gate_transfer.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

diff --git a/tools/data/reports/agent_20260507_0803.md b/tools/data/reports/agent_20260507_0803.md
new file mode 100644
index 0000000000000000000000000000000000000000..c3a0e5f46f655a3a25c8e6d2f54ec978989c54ca
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0803.md
@@ -0,0 +1,155 @@
+# Agent Report — Denominator Gate Transfers, Boundary Coordinate Does Not
+**Date**: 2026-05-07 08:03  
+**Piano**: 69  
+**Category**: gate_transferability  
+**Tension explored**: META + DUALITA_DIPOLARE_VS_ILLUSORIA  
+**verdict**: operator  
+observables_registry: 1.0.0-2026-05-06  
+observables_used: [SR, SR2, L1, L2, triple_var]
+
+## Claim Under Test
+The operator directive asked whether the `BOUNDARY_LAYER_GATE` forged on the
+GUE/Poisson boundary is a transferable operator or only a local metric.
+
+Concrete test:
+
+> Apply the denominator-collapse gate to the discrimination between dipolar
+> duality and illusory duality. If the gate distinguishes structural generation
+> from incoherent dispersion, it is an operator. If it only emits syntactically
+> valid but empty layer maps, it is a BOUNDARY-local metric.
+
+## Experiment
+Tool created: `tools/exp_duality_gate_transfer.py`
+
+Atomic perimeter:
+- DIPOLARE: coherent golden Beatty gaps generated by `floor((n+phase)*phi)`;
+- ILLUSORIA: a random permutation of the same gaps, preserving one-point
+  distribution and first moment but removing the generating order;
+- beta layer: replace a beta fraction of dipolar positions with values from
+  the illusory permutation;
+- main run: 4,096 gaps, 24 replicates, 11 beta layers, 40 shuffle baselines;
+- seed check: 3,072 gaps, 16 replicates, 11 beta layers, 32 shuffle baselines;
+- denominator gate: observable stable when `abs(z original-vs-shuffle) >= 2`;
+- classification: standardized distance to beta 0.0 and beta 1.0 centroids
+  using all five canonical observables.
+
+This is not a new BOUNDARY experiment. GUE and Poisson do not appear in the
+generator. The only transferred object is the denominator gate.
+
+## Results
+
+### Main Run
+
+Endpoint separation using all canonical observables: `3.436` standardized units.  
+Endpoint-stable observables at frequency >= 0.75 across both endpoints: `[]`.
+
+| beta | stable obs / 5 | margin | ambiguous fraction | illusory-label fraction |
+|---:|---:|---:|---:|---:|
+| 0.0 | 3.000 | 0.971 | 0.000 | 0.000 |
+| 0.1 | 3.000 | 0.479 | 0.000 | 0.000 |
+| 0.2 | 3.000 | 0.234 | 0.125 | 0.000 |
+| 0.3 | 3.000 | 0.033 | 1.000 | 0.375 |
+| 0.4 | 3.083 | 0.154 | 0.417 | 0.958 |
+| 0.5 | 3.083 | 0.269 | 0.375 | 1.000 |
+| 0.6 | 3.000 | 0.417 | 0.042 | 1.000 |
+| 0.7 | 2.917 | 0.316 | 0.167 | 1.000 |
+| 0.8 | 1.708 | 0.473 | 0.125 | 1.000 |
+| 0.9 | 0.167 | 0.463 | 0.042 | 1.000 |
+| 1.0 | 0.208 | 0.442 | 0.042 | 1.000 |
+
+Observable stability frequencies:
+- beta 0.0: `SR=1.00`, `L1=1.00`, `triple_var=1.00`; `SR2=0.00`, `L2=0.00`;
+- beta 0.3: same stable trio, with mean z approximately `SR=-19.4`, `L1=-19.4`, `triple_var=-17.6`;
+- beta 0.8: stable trio falls to frequency `0.54` each, mean z around `-1.7` to `-1.9`;
+- beta 0.9: all canonical observables are weak or near weak, mean z around `-0.4..+0.2`;
+- beta 1.0: all canonical observables are weak, mean z around `+0.1..+0.2`.
+
+### Seed Check
+
+The seed check repeated the same structure:
+- endpoint-stable observables: `[]`;
+- all-observable endpoint distance: `3.412`;
+- ambiguous beta: `[0.3]`;
+- beta 0.0 stable count: `3.000`;
+- beta 0.3 stable count: `3.000`, ambiguous fraction `1.000`;
+- beta 0.8 stable count: `1.062`;
+- beta 0.9 stable count: `0.000`;
+- beta 1.0 stable count: `0.438`.
+
+## Findings
+
+1. **The gate does not degenerate on DUALITA.** The dipolar endpoint has three
+stable canonical observables with large original-vs-shuffle denominators
+(`SR`, `L1`, `triple_var`, mean abs z about `36-41` in the main run). The
+illusory endpoint has no stable denominator support. This is a structural
+original-vs-shuffle distinction, not an empty syntactic map.
+
+2. **The BOUNDARY layer coordinate does not transfer unchanged.** In the
+GUE/Poisson run, beta 0.3-0.4 carried both classification ambiguity and
+denominator collapse. Here, beta 0.3 is classification-ambiguous, but the
+denominator support is still strong: stable count remains `3.000/5`.
+Denominator collapse arrives later, around beta 0.8-0.9. Therefore the
+operator transfers, but the specific BOUNDARY layer shape is local.
+
+3. **Endpoint-gated classification remains empty for the same structural
+reason as BOUNDARY.** The endpoint-stable set is empty because the illusory
+pole is denominator-weak. This does not erase the discrimination; it prevents
+symmetric endpoint-gated retention claims. The valid claim is one-sided:
+coherent dipolar order survives original-vs-shuffle gating; illusory dispersion
+does not.
+
+4. **The discriminant is order, not marginal distribution.** The illusory
+sequence preserves the same alphabet, first moment, and one-point distribution
+as the dipolar sequence. The gate is therefore not measuring the marginal
+composition of gaps. It measures whether the canonical observables retain an
+ordered denominator against full shuffle.
+
+## Verdict
+**category: gate_transferability**  
+**verdict: operator**
+
+Scoped statement:
+
+> In this synthetic DUALITA perimeter, the denominator gate is transferable as
+> an operator for structural order: it separates dipolar generation from
+> illusory dispersion with replicated z support. The beta coordinate of the
+> GUE/Poisson boundary layer is not transferable: ambiguity appears near beta
+> 0.3, while denominator collapse appears near beta 0.8-0.9.
+
+So the last BOUNDARY cycles forged a lens, but one parameter of that lens was
+local to BOUNDARY. The transferable object is not "beta 0.3-0.4"; it is:
+
+> report layer maps as classifier margin plus original-vs-shuffle denominator
+> support, and treat denominator-weak poles as asymmetric structural poles.
+
+## Consecutio
+What opens now: apply the same transfer test to `TRASCENDENZA_LIMITE` and
+`G_POTENZIALE_NULLA`, but separate two quantities from the start:
+
+1. classification ambiguity layer;
+2. denominator-collapse layer.
+
+If both layers coincide in a domain, the domain has a BOUNDARY-like transition.
+If they split, as they do here, the gate is still useful but the transition
+coordinate belongs to the target domain, not to the operator.
+
+## Auto-audit: 5 lenti
+- **L1 hard constraint vs bias**: no "always/never/zero" claim. "No endpoint
+  stable observables" means none reached frequency >= 0.75 across both
+  endpoints under `abs(z) >= 2`.
+- **L2 quantity vs ratio**: classification margin is reported together with
+  stable-observable count and z means. Ratios are not interpreted without
+  denominator support.
+- **L3 no silent patching**: the report explicitly separates "gate transfers"
+  from "BOUNDARY beta coordinate transfers." The second is not claimed.
+- **L4 edge cases**: beta 0.4 has ambiguous fraction `0.417`, so only beta 0.3
+  is listed as the replicated ambiguous layer under the >= 0.5 rule.
+- **L5 re-discovery**: this is a finite symbolic-order vs permutation audit,
+  related to standard shuffle/null testing. It is not tagged as a new theorem
+  about Sturmian or Beatty sequences.
+
+## Files
+- Script: `tools/exp_duality_gate_transfer.py`
+- Main data: `tools/data/duality_gate_transfer_20260507_0803.json`
+- Seed check: `tools/data/duality_gate_transfer_20260507_0803_seedcheck.json`
+- Report: `tools/data/reports/agent_20260507_0803.md`
diff --git a/tools/exp_duality_gate_transfer.py b/tools/exp_duality_gate_transfer.py
new file mode 100644
index 0000000000000000000000000000000000000000..c04f490d17524d73d40766b8b969cdfe3d20077c
--- /dev/null
+++ b/tools/exp_duality_gate_transfer.py
@@ -0,0 +1,284 @@
+#!/usr/bin/env python3
+"""
+exp_duality_gate_transfer.py
+
+Reusable META/DUALITA audit for transferring the BOUNDARY_LAYER_GATE.
+
+Question:
+    Does the denominator-collapse gate discriminate dipolar duality from
+    illusory duality, or does it become a local BOUNDARY metric when moved
+    away from GUE/Poisson?
+
+The script builds a controlled layer map between:
+
+- DIPOLARE: a coherent golden/Beatty gap sequence generated by the orbit of
+  floor((n + phase) * phi). Its gaps are the symbolic det=-1 D-ND pole in the
+  minimal positive alphabet {1, 2}.
+- ILLUSORIA: a full random permutation of the same gaps. This preserves the
+  one-point distribution and first moment but removes the generating order.
+
+Layer beta replaces a beta fraction of dipolar positions with values from the
+illusory permutation. Canonical observables and original-vs-shuffle z gates are
+imported from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
+
+
+OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())
+PHI = (1.0 + 5.0**0.5) / 2.0
+
+
+def dipolar_gaps(n_gaps: int, phase: float) -> np.ndarray:
+    """Return coherent golden Beatty gaps in {1, 2}."""
+    n = np.arange(n_gaps + 1, dtype=float) + float(phase)
+    positions = np.floor(n * PHI).astype(float)
+    gaps = np.diff(positions)
+    mean = float(np.mean(gaps))
+    return gaps / mean if mean > 1e-15 else gaps
+
+
+def beta_layer_gaps(
+    dipolar: np.ndarray,
+    beta: float,
+    rng: np.random.Generator,
+) -> np.ndarray:
+    """Replace a beta fraction of coherent positions with a shuffled copy."""
+    illusory = rng.permutation(dipolar)
+    if beta <= 0.0:
+        out = dipolar.copy()
+    elif beta >= 1.0:
+        out = illusory.copy()
+    else:
+        mask = rng.random(len(dipolar)) < beta
+        out = dipolar.copy()
+        out[mask] = illusory[mask]
+    mean = float(np.mean(out))
+    return out / mean if mean > 1e-15 else out
+
+
+def z_against_shuffle(
+    gaps: np.ndarray,
+    n_baseline: int,
+    rng: np.random.Generator,
+) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
+    """Return original observables, shuffle baseline std, and z scores."""
+    original = compute_canonical(gaps)
+    baseline_vals = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline_vals[name].append(obs[name])
+
+    shuffle_std = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline_vals[name], dtype=float)
+        mean = float(np.mean(vals))
+        sd = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        shuffle_std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, shuffle_std, z
+
+
+def vector(row: dict, names: list[str]) -> np.ndarray:
+    return np.array([row["observables"][name] for name in names], dtype=float)
+
+
+def classify_layers(rows: list[dict], obs_names: list[str]) -> dict:
+    """Classify layers by standardized distance to dipolar/illusory endpoints."""
+    if not obs_names:
+        return {
+            "observables": [],
+            "endpoint_distance": 0.0,
+            "layers": {},
+            "ambiguous_beta": [],
+        }
+
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    dip_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    ill_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([dip_vectors, ill_vectors])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    dip_centroid = np.mean(dip_vectors, axis=0)
+    ill_centroid = np.mean(ill_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((ill_centroid - dip_centroid) / scale))
+
+    layers = {}
+    ambiguous_beta = []
+    for beta, beta_rows in sorted(by_beta.items()):
+        coords = []
+        margins = []
+        labels = []
+        for row in beta_rows:
+            x = vector(row, obs_names)
+            d_dip = float(np.linalg.norm((x - dip_centroid) / scale))
+            d_ill = float(np.linalg.norm((x - ill_centroid) / scale))
+            denom = d_dip + d_ill
+            coord = float((d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("dipolar" if d_dip < d_ill else "illusory")
+        ambiguous_fraction = float(np.mean(np.array(margins) < 0.15))
+        if ambiguous_fraction >= 0.5:
+            ambiguous_beta.append(beta)
+        layers[f"{beta:.3f}"] = {
+            "coordinate_mean": float(np.mean(coords)),
+            "coordinate_std": float(np.std(coords, ddof=1)) if len(coords) > 1 else 0.0,
+            "margin_mean": float(np.mean(margins)),
+            "ambiguous_fraction": ambiguous_fraction,
+            "illusory_label_fraction": float(np.mean(np.array(labels) == "illusory")),
+        }
+
+    return {
+        "observables": obs_names,
+        "endpoint_distance": endpoint_distance,
+        "layers": layers,
+        "ambiguous_beta": ambiguous_beta,
+    }
+
+
+def summarize_gate(rows: list[dict], z_min: float) -> dict:
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    layers = {}
+    for beta, beta_rows in sorted(by_beta.items()):
+        stable_counts = []
+        stable_freq = {name: [] for name in OBS_NAMES}
+        z_values = {name: [] for name in OBS_NAMES}
+        for row in beta_rows:
+            stable = [name for name in OBS_NAMES if abs(row["z"][name]) >= z_min]
+            stable_counts.append(len(stable))
+            for name in OBS_NAMES:
+                stable_freq[name].append(1.0 if name in stable else 0.0)
+                z_values[name].append(row["z"][name])
+        layers[f"{beta:.3f}"] = {
+            "stable_count_mean": float(np.mean(stable_counts)),
+            "stable_count_std": float(np.std(stable_counts, ddof=1)) if len(stable_counts) > 1 else 0.0,
+            "stable_frequency": {name: float(np.mean(vals)) for name, vals in stable_freq.items()},
+            "z_mean": {name: float(np.mean(vals)) for name, vals in z_values.items()},
+        }
+
+    endpoint_stable = []
+    for name in OBS_NAMES:
+        endpoint_rows = by_beta[0.0] + by_beta[1.0]
+        freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in endpoint_rows])
+        if freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def run(args: argparse.Namespace) -> dict:
+    rng = np.random.default_rng(args.seed)
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    rows = []
+
+    for rep in range(args.n_replicates):
+        phase = float(rng.random())
+        base = dipolar_gaps(args.n_gaps, phase)
+        for beta in betas:
+            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+            gaps = beta_layer_gaps(base, beta, layer_rng)
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                n_baseline=args.n_baseline,
+                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "replicate": rep,
+                    "phase": phase,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [name for name in OBS_NAMES if abs(z[name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
+
+    output = {
+        "experiment": "duality_gate_transfer",
+        "category": "gate_transferability",
+        "question": "Does the denominator-collapse gate transfer from BOUNDARY to DUALITA_DIPOLARE_VS_ILLUSORIA?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_endpoint_gated": gated_classification,
+        "rows": rows,
+    }
+
+    out_path = Path(args.out)
+    out_path.parent.mkdir(parents=True, exist_ok=True)
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
+    print(f"observables_used={OBS_NAMES}")
+    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
+    print(
+        "endpoint_distance_all="
+        f"{all_classification['endpoint_distance']:.3f} "
+        "endpoint_distance_gated="
+        f"{gated_classification['endpoint_distance']:.3f}"
+    )
+    source = gated_classification if gate["endpoint_stable_observables"] else all_classification
+    print("beta stable_count margin ambiguous illusory_fraction")
+    for beta in betas:
+        key = f"{beta:.3f}"
+        stable_count = gate["layers"][key]["stable_count_mean"]
+        layer = source["layers"].get(key, {})
+        print(
+            f"{beta:>4.2f} {stable_count:>12.3f} "
+            f"{layer.get('margin_mean', 0.0):>8.3f} "
+            f"{layer.get('ambiguous_fraction', 0.0):>9.3f} "
+            f"{layer.get('illusory_label_fraction', 0.0):>17.3f}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=4096)
+    parser.add_argument("--n-replicates", type=int, default=24)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=40)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=202605070803)
+    parser.add_argument("--out", default="tools/data/duality_gate_transfer.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

codex
Report scritto. Prima di chiudere passo il formato e il contenuto attraverso un controllo rapido: leggibilità del report, presenza del verdict richiesto, e se il falsifier locale ha una CLI utilizzabile la eseguo sul file appena prodotto.
exec
/bin/bash -lc "sed -n '1,260p' tools/data/reports/agent_20260507_0803.md" in /opt/MM_D-ND
 succeeded in 0ms:
# Agent Report — Denominator Gate Transfers, Boundary Coordinate Does Not
**Date**: 2026-05-07 08:03  
**Piano**: 69  
**Category**: gate_transferability  
**Tension explored**: META + DUALITA_DIPOLARE_VS_ILLUSORIA  
**verdict**: operator  
observables_registry: 1.0.0-2026-05-06  
observables_used: [SR, SR2, L1, L2, triple_var]

## Claim Under Test
The operator directive asked whether the `BOUNDARY_LAYER_GATE` forged on the
GUE/Poisson boundary is a transferable operator or only a local metric.

Concrete test:

> Apply the denominator-collapse gate to the discrimination between dipolar
> duality and illusory duality. If the gate distinguishes structural generation
> from incoherent dispersion, it is an operator. If it only emits syntactically
> valid but empty layer maps, it is a BOUNDARY-local metric.

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

Atomic perimeter:
- DIPOLARE: coherent golden Beatty gaps generated by `floor((n+phase)*phi)`;
- ILLUSORIA: a random permutation of the same gaps, preserving one-point
  distribution and first moment but removing the generating order;
- beta layer: replace a beta fraction of dipolar positions with values from
  the illusory permutation;
- main run: 4,096 gaps, 24 replicates, 11 beta layers, 40 shuffle baselines;
- seed check: 3,072 gaps, 16 replicates, 11 beta layers, 32 shuffle baselines;
- denominator gate: observable stable when `abs(z original-vs-shuffle) >= 2`;
- classification: standardized distance to beta 0.0 and beta 1.0 centroids
  using all five canonical observables.

This is not a new BOUNDARY experiment. GUE and Poisson do not appear in the
generator. The only transferred object is the denominator gate.

## Results

### Main Run

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

| beta | stable obs / 5 | margin | ambiguous fraction | illusory-label fraction |
|---:|---:|---:|---:|---:|
| 0.0 | 3.000 | 0.971 | 0.000 | 0.000 |
| 0.1 | 3.000 | 0.479 | 0.000 | 0.000 |
| 0.2 | 3.000 | 0.234 | 0.125 | 0.000 |
| 0.3 | 3.000 | 0.033 | 1.000 | 0.375 |
| 0.4 | 3.083 | 0.154 | 0.417 | 0.958 |
| 0.5 | 3.083 | 0.269 | 0.375 | 1.000 |
| 0.6 | 3.000 | 0.417 | 0.042 | 1.000 |
| 0.7 | 2.917 | 0.316 | 0.167 | 1.000 |
| 0.8 | 1.708 | 0.473 | 0.125 | 1.000 |
| 0.9 | 0.167 | 0.463 | 0.042 | 1.000 |
| 1.0 | 0.208 | 0.442 | 0.042 | 1.000 |

Observable stability frequencies:
- beta 0.0: `SR=1.00`, `L1=1.00`, `triple_var=1.00`; `SR2=0.00`, `L2=0.00`;
- beta 0.3: same stable trio, with mean z approximately `SR=-19.4`, `L1=-19.4`, `triple_var=-17.6`;
- beta 0.8: stable trio falls to frequency `0.54` each, mean z around `-1.7` to `-1.9`;
- beta 0.9: all canonical observables are weak or near weak, mean z around `-0.4..+0.2`;
- beta 1.0: all canonical observables are weak, mean z around `+0.1..+0.2`.

### Seed Check

The seed check repeated the same structure:
- endpoint-stable observables: `[]`;
- all-observable endpoint distance: `3.412`;
- ambiguous beta: `[0.3]`;
- beta 0.0 stable count: `3.000`;
- beta 0.3 stable count: `3.000`, ambiguous fraction `1.000`;
- beta 0.8 stable count: `1.062`;
- beta 0.9 stable count: `0.000`;
- beta 1.0 stable count: `0.438`.

## Findings

1. **The gate does not degenerate on DUALITA.** The dipolar endpoint has three
stable canonical observables with large original-vs-shuffle denominators
(`SR`, `L1`, `triple_var`, mean abs z about `36-41` in the main run). The
illusory endpoint has no stable denominator support. This is a structural
original-vs-shuffle distinction, not an empty syntactic map.

2. **The BOUNDARY layer coordinate does not transfer unchanged.** In the
GUE/Poisson run, beta 0.3-0.4 carried both classification ambiguity and
denominator collapse. Here, beta 0.3 is classification-ambiguous, but the
denominator support is still strong: stable count remains `3.000/5`.
Denominator collapse arrives later, around beta 0.8-0.9. Therefore the
operator transfers, but the specific BOUNDARY layer shape is local.

3. **Endpoint-gated classification remains empty for the same structural
reason as BOUNDARY.** The endpoint-stable set is empty because the illusory
pole is denominator-weak. This does not erase the discrimination; it prevents
symmetric endpoint-gated retention claims. The valid claim is one-sided:
coherent dipolar order survives original-vs-shuffle gating; illusory dispersion
does not.

4. **The discriminant is order, not marginal distribution.** The illusory
sequence preserves the same alphabet, first moment, and one-point distribution
as the dipolar sequence. The gate is therefore not measuring the marginal
composition of gaps. It measures whether the canonical observables retain an
ordered denominator against full shuffle.

## Verdict
**category: gate_transferability**  
**verdict: operator**

Scoped statement:

> In this synthetic DUALITA perimeter, the denominator gate is transferable as
> an operator for structural order: it separates dipolar generation from
> illusory dispersion with replicated z support. The beta coordinate of the
> GUE/Poisson boundary layer is not transferable: ambiguity appears near beta
> 0.3, while denominator collapse appears near beta 0.8-0.9.

So the last BOUNDARY cycles forged a lens, but one parameter of that lens was
local to BOUNDARY. The transferable object is not "beta 0.3-0.4"; it is:

> report layer maps as classifier margin plus original-vs-shuffle denominator
> support, and treat denominator-weak poles as asymmetric structural poles.

## Consecutio
What opens now: apply the same transfer test to `TRASCENDENZA_LIMITE` and
`G_POTENZIALE_NULLA`, but separate two quantities from the start:

1. classification ambiguity layer;
2. denominator-collapse layer.

If both layers coincide in a domain, the domain has a BOUNDARY-like transition.
If they split, as they do here, the gate is still useful but the transition
coordinate belongs to the target domain, not to the operator.

## Auto-audit: 5 lenti
- **L1 hard constraint vs bias**: no "always/never/zero" claim. "No endpoint
  stable observables" means none reached frequency >= 0.75 across both
  endpoints under `abs(z) >= 2`.
- **L2 quantity vs ratio**: classification margin is reported together with
  stable-observable count and z means. Ratios are not interpreted without
  denominator support.
- **L3 no silent patching**: the report explicitly separates "gate transfers"
  from "BOUNDARY beta coordinate transfers." The second is not claimed.
- **L4 edge cases**: beta 0.4 has ambiguous fraction `0.417`, so only beta 0.3
  is listed as the replicated ambiguous layer under the >= 0.5 rule.
- **L5 re-discovery**: this is a finite symbolic-order vs permutation audit,
  related to standard shuffle/null testing. It is not tagged as a new theorem
  about Sturmian or Beatty sequences.

## Files
- Script: `tools/exp_duality_gate_transfer.py`
- Main data: `tools/data/duality_gate_transfer_20260507_0803.json`
- Seed check: `tools/data/duality_gate_transfer_20260507_0803_seedcheck.json`
- Report: `tools/data/reports/agent_20260507_0803.md`

exec
/bin/bash -lc 'rg -n "category: gate_transferability|verdict: operator|observables_registry|Files" tools/data/reports/agent_20260507_0803.md' in /opt/MM_D-ND
 succeeded in 0ms:
7:observables_registry: 1.0.0-2026-05-06  
108:**category: gate_transferability**  
109:**verdict: operator**
151:## Files

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

diff --git a/tools/data/reports/agent_20260507_0803.md b/tools/data/reports/agent_20260507_0803.md
new file mode 100644
index 0000000000000000000000000000000000000000..c3a0e5f46f655a3a25c8e6d2f54ec978989c54ca
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0803.md
@@ -0,0 +1,155 @@
+# Agent Report — Denominator Gate Transfers, Boundary Coordinate Does Not
+**Date**: 2026-05-07 08:03  
+**Piano**: 69  
+**Category**: gate_transferability  
+**Tension explored**: META + DUALITA_DIPOLARE_VS_ILLUSORIA  
+**verdict**: operator  
+observables_registry: 1.0.0-2026-05-06  
+observables_used: [SR, SR2, L1, L2, triple_var]
+
+## Claim Under Test
+The operator directive asked whether the `BOUNDARY_LAYER_GATE` forged on the
+GUE/Poisson boundary is a transferable operator or only a local metric.
+
+Concrete test:
+
+> Apply the denominator-collapse gate to the discrimination between dipolar
+> duality and illusory duality. If the gate distinguishes structural generation
+> from incoherent dispersion, it is an operator. If it only emits syntactically
+> valid but empty layer maps, it is a BOUNDARY-local metric.
+
+## Experiment
+Tool created: `tools/exp_duality_gate_transfer.py`
+
+Atomic perimeter:
+- DIPOLARE: coherent golden Beatty gaps generated by `floor((n+phase)*phi)`;
+- ILLUSORIA: a random permutation of the same gaps, preserving one-point
+  distribution and first moment but removing the generating order;
+- beta layer: replace a beta fraction of dipolar positions with values from
+  the illusory permutation;
+- main run: 4,096 gaps, 24 replicates, 11 beta layers, 40 shuffle baselines;
+- seed check: 3,072 gaps, 16 replicates, 11 beta layers, 32 shuffle baselines;
+- denominator gate: observable stable when `abs(z original-vs-shuffle) >= 2`;
+- classification: standardized distance to beta 0.0 and beta 1.0 centroids
+  using all five canonical observables.
+
+This is not a new BOUNDARY experiment. GUE and Poisson do not appear in the
+generator. The only transferred object is the denominator gate.
+
+## Results
+
+### Main Run
+
+Endpoint separation using all canonical observables: `3.436` standardized units.  
+Endpoint-stable observables at frequency >= 0.75 across both endpoints: `[]`.
+
+| beta | stable obs / 5 | margin | ambiguous fraction | illusory-label fraction |
+|---:|---:|---:|---:|---:|
+| 0.0 | 3.000 | 0.971 | 0.000 | 0.000 |
+| 0.1 | 3.000 | 0.479 | 0.000 | 0.000 |
+| 0.2 | 3.000 | 0.234 | 0.125 | 0.000 |
+| 0.3 | 3.000 | 0.033 | 1.000 | 0.375 |
+| 0.4 | 3.083 | 0.154 | 0.417 | 0.958 |
+| 0.5 | 3.083 | 0.269 | 0.375 | 1.000 |
+| 0.6 | 3.000 | 0.417 | 0.042 | 1.000 |
+| 0.7 | 2.917 | 0.316 | 0.167 | 1.000 |
+| 0.8 | 1.708 | 0.473 | 0.125 | 1.000 |
+| 0.9 | 0.167 | 0.463 | 0.042 | 1.000 |
+| 1.0 | 0.208 | 0.442 | 0.042 | 1.000 |
+
+Observable stability frequencies:
+- beta 0.0: `SR=1.00`, `L1=1.00`, `triple_var=1.00`; `SR2=0.00`, `L2=0.00`;
+- beta 0.3: same stable trio, with mean z approximately `SR=-19.4`, `L1=-19.4`, `triple_var=-17.6`;
+- beta 0.8: stable trio falls to frequency `0.54` each, mean z around `-1.7` to `-1.9`;
+- beta 0.9: all canonical observables are weak or near weak, mean z around `-0.4..+0.2`;
+- beta 1.0: all canonical observables are weak, mean z around `+0.1..+0.2`.
+
+### Seed Check
+
+The seed check repeated the same structure:
+- endpoint-stable observables: `[]`;
+- all-observable endpoint distance: `3.412`;
+- ambiguous beta: `[0.3]`;
+- beta 0.0 stable count: `3.000`;
+- beta 0.3 stable count: `3.000`, ambiguous fraction `1.000`;
+- beta 0.8 stable count: `1.062`;
+- beta 0.9 stable count: `0.000`;
+- beta 1.0 stable count: `0.438`.
+
+## Findings
+
+1. **The gate does not degenerate on DUALITA.** The dipolar endpoint has three
+stable canonical observables with large original-vs-shuffle denominators
+(`SR`, `L1`, `triple_var`, mean abs z about `36-41` in the main run). The
+illusory endpoint has no stable denominator support. This is a structural
+original-vs-shuffle distinction, not an empty syntactic map.
+
+2. **The BOUNDARY layer coordinate does not transfer unchanged.** In the
+GUE/Poisson run, beta 0.3-0.4 carried both classification ambiguity and
+denominator collapse. Here, beta 0.3 is classification-ambiguous, but the
+denominator support is still strong: stable count remains `3.000/5`.
+Denominator collapse arrives later, around beta 0.8-0.9. Therefore the
+operator transfers, but the specific BOUNDARY layer shape is local.
+
+3. **Endpoint-gated classification remains empty for the same structural
+reason as BOUNDARY.** The endpoint-stable set is empty because the illusory
+pole is denominator-weak. This does not erase the discrimination; it prevents
+symmetric endpoint-gated retention claims. The valid claim is one-sided:
+coherent dipolar order survives original-vs-shuffle gating; illusory dispersion
+does not.
+
+4. **The discriminant is order, not marginal distribution.** The illusory
+sequence preserves the same alphabet, first moment, and one-point distribution
+as the dipolar sequence. The gate is therefore not measuring the marginal
+composition of gaps. It measures whether the canonical observables retain an
+ordered denominator against full shuffle.
+
+## Verdict
+**category: gate_transferability**  
+**verdict: operator**
+
+Scoped statement:
+
+> In this synthetic DUALITA perimeter, the denominator gate is transferable as
+> an operator for structural order: it separates dipolar generation from
+> illusory dispersion with replicated z support. The beta coordinate of the
+> GUE/Poisson boundary layer is not transferable: ambiguity appears near beta
+> 0.3, while denominator collapse appears near beta 0.8-0.9.
+
+So the last BOUNDARY cycles forged a lens, but one parameter of that lens was
+local to BOUNDARY. The transferable object is not "beta 0.3-0.4"; it is:
+
+> report layer maps as classifier margin plus original-vs-shuffle denominator
+> support, and treat denominator-weak poles as asymmetric structural poles.
+
+## Consecutio
+What opens now: apply the same transfer test to `TRASCENDENZA_LIMITE` and
+`G_POTENZIALE_NULLA`, but separate two quantities from the start:
+
+1. classification ambiguity layer;
+2. denominator-collapse layer.
+
+If both layers coincide in a domain, the domain has a BOUNDARY-like transition.
+If they split, as they do here, the gate is still useful but the transition
+coordinate belongs to the target domain, not to the operator.
+
+## Auto-audit: 5 lenti
+- **L1 hard constraint vs bias**: no "always/never/zero" claim. "No endpoint
+  stable observables" means none reached frequency >= 0.75 across both
+  endpoints under `abs(z) >= 2`.
+- **L2 quantity vs ratio**: classification margin is reported together with
+  stable-observable count and z means. Ratios are not interpreted without
+  denominator support.
+- **L3 no silent patching**: the report explicitly separates "gate transfers"
+  from "BOUNDARY beta coordinate transfers." The second is not claimed.
+- **L4 edge cases**: beta 0.4 has ambiguous fraction `0.417`, so only beta 0.3
+  is listed as the replicated ambiguous layer under the >= 0.5 rule.
+- **L5 re-discovery**: this is a finite symbolic-order vs permutation audit,
+  related to standard shuffle/null testing. It is not tagged as a new theorem
+  about Sturmian or Beatty sequences.
+
+## Files
+- Script: `tools/exp_duality_gate_transfer.py`
+- Main data: `tools/data/duality_gate_transfer_20260507_0803.json`
+- Seed check: `tools/data/duality_gate_transfer_20260507_0803_seedcheck.json`
+- Report: `tools/data/reports/agent_20260507_0803.md`
diff --git a/tools/exp_duality_gate_transfer.py b/tools/exp_duality_gate_transfer.py
new file mode 100644
index 0000000000000000000000000000000000000000..c04f490d17524d73d40766b8b969cdfe3d20077c
--- /dev/null
+++ b/tools/exp_duality_gate_transfer.py
@@ -0,0 +1,284 @@
+#!/usr/bin/env python3
+"""
+exp_duality_gate_transfer.py
+
+Reusable META/DUALITA audit for transferring the BOUNDARY_LAYER_GATE.
+
+Question:
+    Does the denominator-collapse gate discriminate dipolar duality from
+    illusory duality, or does it become a local BOUNDARY metric when moved
+    away from GUE/Poisson?
+
+The script builds a controlled layer map between:
+
+- DIPOLARE: a coherent golden/Beatty gap sequence generated by the orbit of
+  floor((n + phase) * phi). Its gaps are the symbolic det=-1 D-ND pole in the
+  minimal positive alphabet {1, 2}.
+- ILLUSORIA: a full random permutation of the same gaps. This preserves the
+  one-point distribution and first moment but removes the generating order.
+
+Layer beta replaces a beta fraction of dipolar positions with values from the
+illusory permutation. Canonical observables and original-vs-shuffle z gates are
+imported from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
+
+
+OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())
+PHI = (1.0 + 5.0**0.5) / 2.0
+
+
+def dipolar_gaps(n_gaps: int, phase: float) -> np.ndarray:
+    """Return coherent golden Beatty gaps in {1, 2}."""
+    n = np.arange(n_gaps + 1, dtype=float) + float(phase)
+    positions = np.floor(n * PHI).astype(float)
+    gaps = np.diff(positions)
+    mean = float(np.mean(gaps))
+    return gaps / mean if mean > 1e-15 else gaps
+
+
+def beta_layer_gaps(
+    dipolar: np.ndarray,
+    beta: float,
+    rng: np.random.Generator,
+) -> np.ndarray:
+    """Replace a beta fraction of coherent positions with a shuffled copy."""
+    illusory = rng.permutation(dipolar)
+    if beta <= 0.0:
+        out = dipolar.copy()
+    elif beta >= 1.0:
+        out = illusory.copy()
+    else:
+        mask = rng.random(len(dipolar)) < beta
+        out = dipolar.copy()
+        out[mask] = illusory[mask]
+    mean = float(np.mean(out))
+    return out / mean if mean > 1e-15 else out
+
+
+def z_against_shuffle(
+    gaps: np.ndarray,
+    n_baseline: int,
+    rng: np.random.Generator,
+) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
+    """Return original observables, shuffle baseline std, and z scores."""
+    original = compute_canonical(gaps)
+    baseline_vals = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline_vals[name].append(obs[name])
+
+    shuffle_std = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline_vals[name], dtype=float)
+        mean = float(np.mean(vals))
+        sd = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        shuffle_std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, shuffle_std, z
+
+
+def vector(row: dict, names: list[str]) -> np.ndarray:
+    return np.array([row["observables"][name] for name in names], dtype=float)
+
+
+def classify_layers(rows: list[dict], obs_names: list[str]) -> dict:
+    """Classify layers by standardized distance to dipolar/illusory endpoints."""
+    if not obs_names:
+        return {
+            "observables": [],
+            "endpoint_distance": 0.0,
+            "layers": {},
+            "ambiguous_beta": [],
+        }
+
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    dip_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    ill_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([dip_vectors, ill_vectors])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    dip_centroid = np.mean(dip_vectors, axis=0)
+    ill_centroid = np.mean(ill_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((ill_centroid - dip_centroid) / scale))
+
+    layers = {}
+    ambiguous_beta = []
+    for beta, beta_rows in sorted(by_beta.items()):
+        coords = []
+        margins = []
+        labels = []
+        for row in beta_rows:
+            x = vector(row, obs_names)
+            d_dip = float(np.linalg.norm((x - dip_centroid) / scale))
+            d_ill = float(np.linalg.norm((x - ill_centroid) / scale))
+            denom = d_dip + d_ill
+            coord = float((d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("dipolar" if d_dip < d_ill else "illusory")
+        ambiguous_fraction = float(np.mean(np.array(margins) < 0.15))
+        if ambiguous_fraction >= 0.5:
+            ambiguous_beta.append(beta)
+        layers[f"{beta:.3f}"] = {
+            "coordinate_mean": float(np.mean(coords)),
+            "coordinate_std": float(np.std(coords, ddof=1)) if len(coords) > 1 else 0.0,
+            "margin_mean": float(np.mean(margins)),
+            "ambiguous_fraction": ambiguous_fraction,
+            "illusory_label_fraction": float(np.mean(np.array(labels) == "illusory")),
+        }
+
+    return {
+        "observables": obs_names,
+        "endpoint_distance": endpoint_distance,
+        "layers": layers,
+        "ambiguous_beta": ambiguous_beta,
+    }
+
+
+def summarize_gate(rows: list[dict], z_min: float) -> dict:
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    layers = {}
+    for beta, beta_rows in sorted(by_beta.items()):
+        stable_counts = []
+        stable_freq = {name: [] for name in OBS_NAMES}
+        z_values = {name: [] for name in OBS_NAMES}
+        for row in beta_rows:
+            stable = [name for name in OBS_NAMES if abs(row["z"][name]) >= z_min]
+            stable_counts.append(len(stable))
+            for name in OBS_NAMES:
+                stable_freq[name].append(1.0 if name in stable else 0.0)
+                z_values[name].append(row["z"][name])
+        layers[f"{beta:.3f}"] = {
+            "stable_count_mean": float(np.mean(stable_counts)),
+            "stable_count_std": float(np.std(stable_counts, ddof=1)) if len(stable_counts) > 1 else 0.0,
+            "stable_frequency": {name: float(np.mean(vals)) for name, vals in stable_freq.items()},
+            "z_mean": {name: float(np.mean(vals)) for name, vals in z_values.items()},
+        }
+
+    endpoint_stable = []
+    for name in OBS_NAMES:
+        endpoint_rows = by_beta[0.0] + by_beta[1.0]
+        freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in endpoint_rows])
+        if freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def run(args: argparse.Namespace) -> dict:
+    rng = np.random.default_rng(args.seed)
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    rows = []
+
+    for rep in range(args.n_replicates):
+        phase = float(rng.random())
+        base = dipolar_gaps(args.n_gaps, phase)
+        for beta in betas:
+            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+            gaps = beta_layer_gaps(base, beta, layer_rng)
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                n_baseline=args.n_baseline,
+                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "replicate": rep,
+                    "phase": phase,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [name for name in OBS_NAMES if abs(z[name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
+
+    output = {
+        "experiment": "duality_gate_transfer",
+        "category": "gate_transferability",
+        "question": "Does the denominator-collapse gate transfer from BOUNDARY to DUALITA_DIPOLARE_VS_ILLUSORIA?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_endpoint_gated": gated_classification,
+        "rows": rows,
+    }
+
+    out_path = Path(args.out)
+    out_path.parent.mkdir(parents=True, exist_ok=True)
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
+    print(f"observables_used={OBS_NAMES}")
+    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
+    print(
+        "endpoint_distance_all="
+        f"{all_classification['endpoint_distance']:.3f} "
+        "endpoint_distance_gated="
+        f"{gated_classification['endpoint_distance']:.3f}"
+    )
+    source = gated_classification if gate["endpoint_stable_observables"] else all_classification
+    print("beta stable_count margin ambiguous illusory_fraction")
+    for beta in betas:
+        key = f"{beta:.3f}"
+        stable_count = gate["layers"][key]["stable_count_mean"]
+        layer = source["layers"].get(key, {})
+        print(
+            f"{beta:>4.2f} {stable_count:>12.3f} "
+            f"{layer.get('margin_mean', 0.0):>8.3f} "
+            f"{layer.get('ambiguous_fraction', 0.0):>9.3f} "
+            f"{layer.get('illusory_label_fraction', 0.0):>17.3f}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=4096)
+    parser.add_argument("--n-replicates", type=int, default=24)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=40)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=202605070803)
+    parser.add_argument("--out", default="tools/data/duality_gate_transfer.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

exec
/bin/bash -lc 'python tools/lab_falsifier.py --report tools/data/reports/agent_20260507_0803.md --output tools/data/reports/falsifier_20260507_0803.json --timeout 120' in /opt/MM_D-ND
diff --git a/tools/data/reports/agent_20260507_0803.md b/tools/data/reports/agent_20260507_0803.md
new file mode 100644
index 0000000000000000000000000000000000000000..c3a0e5f46f655a3a25c8e6d2f54ec978989c54ca
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0803.md
@@ -0,0 +1,155 @@
+# Agent Report — Denominator Gate Transfers, Boundary Coordinate Does Not
+**Date**: 2026-05-07 08:03  
+**Piano**: 69  
+**Category**: gate_transferability  
+**Tension explored**: META + DUALITA_DIPOLARE_VS_ILLUSORIA  
+**verdict**: operator  
+observables_registry: 1.0.0-2026-05-06  
+observables_used: [SR, SR2, L1, L2, triple_var]
+
+## Claim Under Test
+The operator directive asked whether the `BOUNDARY_LAYER_GATE` forged on the
+GUE/Poisson boundary is a transferable operator or only a local metric.
+
+Concrete test:
+
+> Apply the denominator-collapse gate to the discrimination between dipolar
+> duality and illusory duality. If the gate distinguishes structural generation
+> from incoherent dispersion, it is an operator. If it only emits syntactically
+> valid but empty layer maps, it is a BOUNDARY-local metric.
+
+## Experiment
+Tool created: `tools/exp_duality_gate_transfer.py`
+
+Atomic perimeter:
+- DIPOLARE: coherent golden Beatty gaps generated by `floor((n+phase)*phi)`;
+- ILLUSORIA: a random permutation of the same gaps, preserving one-point
+  distribution and first moment but removing the generating order;
+- beta layer: replace a beta fraction of dipolar positions with values from
+  the illusory permutation;
+- main run: 4,096 gaps, 24 replicates, 11 beta layers, 40 shuffle baselines;
+- seed check: 3,072 gaps, 16 replicates, 11 beta layers, 32 shuffle baselines;
+- denominator gate: observable stable when `abs(z original-vs-shuffle) >= 2`;
+- classification: standardized distance to beta 0.0 and beta 1.0 centroids
+  using all five canonical observables.
+
+This is not a new BOUNDARY experiment. GUE and Poisson do not appear in the
+generator. The only transferred object is the denominator gate.
+
+## Results
+
+### Main Run
+
+Endpoint separation using all canonical observables: `3.436` standardized units.  
+Endpoint-stable observables at frequency >= 0.75 across both endpoints: `[]`.
+
+| beta | stable obs / 5 | margin | ambiguous fraction | illusory-label fraction |
+|---:|---:|---:|---:|---:|
+| 0.0 | 3.000 | 0.971 | 0.000 | 0.000 |
+| 0.1 | 3.000 | 0.479 | 0.000 | 0.000 |
+| 0.2 | 3.000 | 0.234 | 0.125 | 0.000 |
+| 0.3 | 3.000 | 0.033 | 1.000 | 0.375 |
+| 0.4 | 3.083 | 0.154 | 0.417 | 0.958 |
+| 0.5 | 3.083 | 0.269 | 0.375 | 1.000 |
+| 0.6 | 3.000 | 0.417 | 0.042 | 1.000 |
+| 0.7 | 2.917 | 0.316 | 0.167 | 1.000 |
+| 0.8 | 1.708 | 0.473 | 0.125 | 1.000 |
+| 0.9 | 0.167 | 0.463 | 0.042 | 1.000 |
+| 1.0 | 0.208 | 0.442 | 0.042 | 1.000 |
+
+Observable stability frequencies:
+- beta 0.0: `SR=1.00`, `L1=1.00`, `triple_var=1.00`; `SR2=0.00`, `L2=0.00`;
+- beta 0.3: same stable trio, with mean z approximately `SR=-19.4`, `L1=-19.4`, `triple_var=-17.6`;
+- beta 0.8: stable trio falls to frequency `0.54` each, mean z around `-1.7` to `-1.9`;
+- beta 0.9: all canonical observables are weak or near weak, mean z around `-0.4..+0.2`;
+- beta 1.0: all canonical observables are weak, mean z around `+0.1..+0.2`.
+
+### Seed Check
+
+The seed check repeated the same structure:
+- endpoint-stable observables: `[]`;
+- all-observable endpoint distance: `3.412`;
+- ambiguous beta: `[0.3]`;
+- beta 0.0 stable count: `3.000`;
+- beta 0.3 stable count: `3.000`, ambiguous fraction `1.000`;
+- beta 0.8 stable count: `1.062`;
+- beta 0.9 stable count: `0.000`;
+- beta 1.0 stable count: `0.438`.
+
+## Findings
+
+1. **The gate does not degenerate on DUALITA.** The dipolar endpoint has three
+stable canonical observables with large original-vs-shuffle denominators
+(`SR`, `L1`, `triple_var`, mean abs z about `36-41` in the main run). The
+illusory endpoint has no stable denominator support. This is a structural
+original-vs-shuffle distinction, not an empty syntactic map.
+
+2. **The BOUNDARY layer coordinate does not transfer unchanged.** In the
+GUE/Poisson run, beta 0.3-0.4 carried both classification ambiguity and
+denominator collapse. Here, beta 0.3 is classification-ambiguous, but the
+denominator support is still strong: stable count remains `3.000/5`.
+Denominator collapse arrives later, around beta 0.8-0.9. Therefore the
+operator transfers, but the specific BOUNDARY layer shape is local.
+
+3. **Endpoint-gated classification remains empty for the same structural
+reason as BOUNDARY.** The endpoint-stable set is empty because the illusory
+pole is denominator-weak. This does not erase the discrimination; it prevents
+symmetric endpoint-gated retention claims. The valid claim is one-sided:
+coherent dipolar order survives original-vs-shuffle gating; illusory dispersion
+does not.
+
+4. **The discriminant is order, not marginal distribution.** The illusory
+sequence preserves the same alphabet, first moment, and one-point distribution
+as the dipolar sequence. The gate is therefore not measuring the marginal
+composition of gaps. It measures whether the canonical observables retain an
+ordered denominator against full shuffle.
+
+## Verdict
+**category: gate_transferability**  
+**verdict: operator**
+
+Scoped statement:
+
+> In this synthetic DUALITA perimeter, the denominator gate is transferable as
+> an operator for structural order: it separates dipolar generation from
+> illusory dispersion with replicated z support. The beta coordinate of the
+> GUE/Poisson boundary layer is not transferable: ambiguity appears near beta
+> 0.3, while denominator collapse appears near beta 0.8-0.9.
+
+So the last BOUNDARY cycles forged a lens, but one parameter of that lens was
+local to BOUNDARY. The transferable object is not "beta 0.3-0.4"; it is:
+
+> report layer maps as classifier margin plus original-vs-shuffle denominator
+> support, and treat denominator-weak poles as asymmetric structural poles.
+
+## Consecutio
+What opens now: apply the same transfer test to `TRASCENDENZA_LIMITE` and
+`G_POTENZIALE_NULLA`, but separate two quantities from the start:
+
+1. classification ambiguity layer;
+2. denominator-collapse layer.
+
+If both layers coincide in a domain, the domain has a BOUNDARY-like transition.
+If they split, as they do here, the gate is still useful but the transition
+coordinate belongs to the target domain, not to the operator.
+
+## Auto-audit: 5 lenti
+- **L1 hard constraint vs bias**: no "always/never/zero" claim. "No endpoint
+  stable observables" means none reached frequency >= 0.75 across both
+  endpoints under `abs(z) >= 2`.
+- **L2 quantity vs ratio**: classification margin is reported together with
+  stable-observable count and z means. Ratios are not interpreted without
+  denominator support.
+- **L3 no silent patching**: the report explicitly separates "gate transfers"
+  from "BOUNDARY beta coordinate transfers." The second is not claimed.
+- **L4 edge cases**: beta 0.4 has ambiguous fraction `0.417`, so only beta 0.3
+  is listed as the replicated ambiguous layer under the >= 0.5 rule.
+- **L5 re-discovery**: this is a finite symbolic-order vs permutation audit,
+  related to standard shuffle/null testing. It is not tagged as a new theorem
+  about Sturmian or Beatty sequences.
+
+## Files
+- Script: `tools/exp_duality_gate_transfer.py`
+- Main data: `tools/data/duality_gate_transfer_20260507_0803.json`
+- Seed check: `tools/data/duality_gate_transfer_20260507_0803_seedcheck.json`
+- Report: `tools/data/reports/agent_20260507_0803.md`
diff --git a/tools/exp_duality_gate_transfer.py b/tools/exp_duality_gate_transfer.py
new file mode 100644
index 0000000000000000000000000000000000000000..c04f490d17524d73d40766b8b969cdfe3d20077c
--- /dev/null
+++ b/tools/exp_duality_gate_transfer.py
@@ -0,0 +1,284 @@
+#!/usr/bin/env python3
+"""
+exp_duality_gate_transfer.py
+
+Reusable META/DUALITA audit for transferring the BOUNDARY_LAYER_GATE.
+
+Question:
+    Does the denominator-collapse gate discriminate dipolar duality from
+    illusory duality, or does it become a local BOUNDARY metric when moved
+    away from GUE/Poisson?
+
+The script builds a controlled layer map between:
+
+- DIPOLARE: a coherent golden/Beatty gap sequence generated by the orbit of
+  floor((n + phase) * phi). Its gaps are the symbolic det=-1 D-ND pole in the
+  minimal positive alphabet {1, 2}.
+- ILLUSORIA: a full random permutation of the same gaps. This preserves the
+  one-point distribution and first moment but removes the generating order.
+
+Layer beta replaces a beta fraction of dipolar positions with values from the
+illusory permutation. Canonical observables and original-vs-shuffle z gates are
+imported from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
+
+
+OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())
+PHI = (1.0 + 5.0**0.5) / 2.0
+
+
+def dipolar_gaps(n_gaps: int, phase: float) -> np.ndarray:
+    """Return coherent golden Beatty gaps in {1, 2}."""
+    n = np.arange(n_gaps + 1, dtype=float) + float(phase)
+    positions = np.floor(n * PHI).astype(float)
+    gaps = np.diff(positions)
+    mean = float(np.mean(gaps))
+    return gaps / mean if mean > 1e-15 else gaps
+
+
+def beta_layer_gaps(
+    dipolar: np.ndarray,
+    beta: float,
+    rng: np.random.Generator,
+) -> np.ndarray:
+    """Replace a beta fraction of coherent positions with a shuffled copy."""
+    illusory = rng.permutation(dipolar)
+    if beta <= 0.0:
+        out = dipolar.copy()
+    elif beta >= 1.0:
+        out = illusory.copy()
+    else:
+        mask = rng.random(len(dipolar)) < beta
+        out = dipolar.copy()
+        out[mask] = illusory[mask]
+    mean = float(np.mean(out))
+    return out / mean if mean > 1e-15 else out
+
+
+def z_against_shuffle(
+    gaps: np.ndarray,
+    n_baseline: int,
+    rng: np.random.Generator,
+) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
+    """Return original observables, shuffle baseline std, and z scores."""
+    original = compute_canonical(gaps)
+    baseline_vals = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline_vals[name].append(obs[name])
+
+    shuffle_std = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline_vals[name], dtype=float)
+        mean = float(np.mean(vals))
+        sd = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        shuffle_std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, shuffle_std, z
+
+
+def vector(row: dict, names: list[str]) -> np.ndarray:
+    return np.array([row["observables"][name] for name in names], dtype=float)
+
+
+def classify_layers(rows: list[dict], obs_names: list[str]) -> dict:
+    """Classify layers by standardized distance to dipolar/illusory endpoints."""
+    if not obs_names:
+        return {
+            "observables": [],
+            "endpoint_distance": 0.0,
+            "layers": {},
+            "ambiguous_beta": [],
+        }
+
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    dip_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    ill_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([dip_vectors, ill_vectors])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    dip_centroid = np.mean(dip_vectors, axis=0)
+    ill_centroid = np.mean(ill_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((ill_centroid - dip_centroid) / scale))
+
+    layers = {}
+    ambiguous_beta = []
+    for beta, beta_rows in sorted(by_beta.items()):
+        coords = []
+        margins = []
+        labels = []
+        for row in beta_rows:
+            x = vector(row, obs_names)
+            d_dip = float(np.linalg.norm((x - dip_centroid) / scale))
+            d_ill = float(np.linalg.norm((x - ill_centroid) / scale))
+            denom = d_dip + d_ill
+            coord = float((d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("dipolar" if d_dip < d_ill else "illusory")
+        ambiguous_fraction = float(np.mean(np.array(margins) < 0.15))
+        if ambiguous_fraction >= 0.5:
+            ambiguous_beta.append(beta)
+        layers[f"{beta:.3f}"] = {
+            "coordinate_mean": float(np.mean(coords)),
+            "coordinate_std": float(np.std(coords, ddof=1)) if len(coords) > 1 else 0.0,
+            "margin_mean": float(np.mean(margins)),
+            "ambiguous_fraction": ambiguous_fraction,
+            "illusory_label_fraction": float(np.mean(np.array(labels) == "illusory")),
+        }
+
+    return {
+        "observables": obs_names,
+        "endpoint_distance": endpoint_distance,
+        "layers": layers,
+        "ambiguous_beta": ambiguous_beta,
+    }
+
+
+def summarize_gate(rows: list[dict], z_min: float) -> dict:
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    layers = {}
+    for beta, beta_rows in sorted(by_beta.items()):
+        stable_counts = []
+        stable_freq = {name: [] for name in OBS_NAMES}
+        z_values = {name: [] for name in OBS_NAMES}
+        for row in beta_rows:
+            stable = [name for name in OBS_NAMES if abs(row["z"][name]) >= z_min]
+            stable_counts.append(len(stable))
+            for name in OBS_NAMES:
+                stable_freq[name].append(1.0 if name in stable else 0.0)
+                z_values[name].append(row["z"][name])
+        layers[f"{beta:.3f}"] = {
+            "stable_count_mean": float(np.mean(stable_counts)),
+            "stable_count_std": float(np.std(stable_counts, ddof=1)) if len(stable_counts) > 1 else 0.0,
+            "stable_frequency": {name: float(np.mean(vals)) for name, vals in stable_freq.items()},
+            "z_mean": {name: float(np.mean(vals)) for name, vals in z_values.items()},
+        }
+
+    endpoint_stable = []
+    for name in OBS_NAMES:
+        endpoint_rows = by_beta[0.0] + by_beta[1.0]
+        freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in endpoint_rows])
+        if freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def run(args: argparse.Namespace) -> dict:
+    rng = np.random.default_rng(args.seed)
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    rows = []
+
+    for rep in range(args.n_replicates):
+        phase = float(rng.random())
+        base = dipolar_gaps(args.n_gaps, phase)
+        for beta in betas:
+            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+            gaps = beta_layer_gaps(base, beta, layer_rng)
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                n_baseline=args.n_baseline,
+                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "replicate": rep,
+                    "phase": phase,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [name for name in OBS_NAMES if abs(z[name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
+
+    output = {
+        "experiment": "duality_gate_transfer",
+        "category": "gate_transferability",
+        "question": "Does the denominator-collapse gate transfer from BOUNDARY to DUALITA_DIPOLARE_VS_ILLUSORIA?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_endpoint_gated": gated_classification,
+        "rows": rows,
+    }
+
+    out_path = Path(args.out)
+    out_path.parent.mkdir(parents=True, exist_ok=True)
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
+    print(f"observables_used={OBS_NAMES}")
+    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
+    print(
+        "endpoint_distance_all="
+        f"{all_classification['endpoint_distance']:.3f} "
+        "endpoint_distance_gated="
+        f"{gated_classification['endpoint_distance']:.3f}"
+    )
+    source = gated_classification if gate["endpoint_stable_observables"] else all_classification
+    print("beta stable_count margin ambiguous illusory_fraction")
+    for beta in betas:
+        key = f"{beta:.3f}"
+        stable_count = gate["layers"][key]["stable_count_mean"]
+        layer = source["layers"].get(key, {})
+        print(
+            f"{beta:>4.2f} {stable_count:>12.3f} "
+            f"{layer.get('margin_mean', 0.0):>8.3f} "
+            f"{layer.get('ambiguous_fraction', 0.0):>9.3f} "
+            f"{layer.get('illusory_label_fraction', 0.0):>17.3f}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=4096)
+    parser.add_argument("--n-replicates", type=int, default=24)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=40)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=202605070803)
+    parser.add_argument("--out", default="tools/data/duality_gate_transfer.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

diff --git a/tools/data/reports/agent_20260507_0803.md b/tools/data/reports/agent_20260507_0803.md
new file mode 100644
index 0000000000000000000000000000000000000000..c3a0e5f46f655a3a25c8e6d2f54ec978989c54ca
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0803.md
@@ -0,0 +1,155 @@
+# Agent Report — Denominator Gate Transfers, Boundary Coordinate Does Not
+**Date**: 2026-05-07 08:03  
+**Piano**: 69  
+**Category**: gate_transferability  
+**Tension explored**: META + DUALITA_DIPOLARE_VS_ILLUSORIA  
+**verdict**: operator  
+observables_registry: 1.0.0-2026-05-06  
+observables_used: [SR, SR2, L1, L2, triple_var]
+
+## Claim Under Test
+The operator directive asked whether the `BOUNDARY_LAYER_GATE` forged on the
+GUE/Poisson boundary is a transferable operator or only a local metric.
+
+Concrete test:
+
+> Apply the denominator-collapse gate to the discrimination between dipolar
+> duality and illusory duality. If the gate distinguishes structural generation
+> from incoherent dispersion, it is an operator. If it only emits syntactically
+> valid but empty layer maps, it is a BOUNDARY-local metric.
+
+## Experiment
+Tool created: `tools/exp_duality_gate_transfer.py`
+
+Atomic perimeter:
+- DIPOLARE: coherent golden Beatty gaps generated by `floor((n+phase)*phi)`;
+- ILLUSORIA: a random permutation of the same gaps, preserving one-point
+  distribution and first moment but removing the generating order;
+- beta layer: replace a beta fraction of dipolar positions with values from
+  the illusory permutation;
+- main run: 4,096 gaps, 24 replicates, 11 beta layers, 40 shuffle baselines;
+- seed check: 3,072 gaps, 16 replicates, 11 beta layers, 32 shuffle baselines;
+- denominator gate: observable stable when `abs(z original-vs-shuffle) >= 2`;
+- classification: standardized distance to beta 0.0 and beta 1.0 centroids
+  using all five canonical observables.
+
+This is not a new BOUNDARY experiment. GUE and Poisson do not appear in the
+generator. The only transferred object is the denominator gate.
+
+## Results
+
+### Main Run
+
+Endpoint separation using all canonical observables: `3.436` standardized units.  
+Endpoint-stable observables at frequency >= 0.75 across both endpoints: `[]`.
+
+| beta | stable obs / 5 | margin | ambiguous fraction | illusory-label fraction |
+|---:|---:|---:|---:|---:|
+| 0.0 | 3.000 | 0.971 | 0.000 | 0.000 |
+| 0.1 | 3.000 | 0.479 | 0.000 | 0.000 |
+| 0.2 | 3.000 | 0.234 | 0.125 | 0.000 |
+| 0.3 | 3.000 | 0.033 | 1.000 | 0.375 |
+| 0.4 | 3.083 | 0.154 | 0.417 | 0.958 |
+| 0.5 | 3.083 | 0.269 | 0.375 | 1.000 |
+| 0.6 | 3.000 | 0.417 | 0.042 | 1.000 |
+| 0.7 | 2.917 | 0.316 | 0.167 | 1.000 |
+| 0.8 | 1.708 | 0.473 | 0.125 | 1.000 |
+| 0.9 | 0.167 | 0.463 | 0.042 | 1.000 |
+| 1.0 | 0.208 | 0.442 | 0.042 | 1.000 |
+
+Observable stability frequencies:
+- beta 0.0: `SR=1.00`, `L1=1.00`, `triple_var=1.00`; `SR2=0.00`, `L2=0.00`;
+- beta 0.3: same stable trio, with mean z approximately `SR=-19.4`, `L1=-19.4`, `triple_var=-17.6`;
+- beta 0.8: stable trio falls to frequency `0.54` each, mean z around `-1.7` to `-1.9`;
+- beta 0.9: all canonical observables are weak or near weak, mean z around `-0.4..+0.2`;
+- beta 1.0: all canonical observables are weak, mean z around `+0.1..+0.2`.
+
+### Seed Check
+
+The seed check repeated the same structure:
+- endpoint-stable observables: `[]`;
+- all-observable endpoint distance: `3.412`;
+- ambiguous beta: `[0.3]`;
+- beta 0.0 stable count: `3.000`;
+- beta 0.3 stable count: `3.000`, ambiguous fraction `1.000`;
+- beta 0.8 stable count: `1.062`;
+- beta 0.9 stable count: `0.000`;
+- beta 1.0 stable count: `0.438`.
+
+## Findings
+
+1. **The gate does not degenerate on DUALITA.** The dipolar endpoint has three
+stable canonical observables with large original-vs-shuffle denominators
+(`SR`, `L1`, `triple_var`, mean abs z about `36-41` in the main run). The
+illusory endpoint has no stable denominator support. This is a structural
+original-vs-shuffle distinction, not an empty syntactic map.
+
+2. **The BOUNDARY layer coordinate does not transfer unchanged.** In the
+GUE/Poisson run, beta 0.3-0.4 carried both classification ambiguity and
+denominator collapse. Here, beta 0.3 is classification-ambiguous, but the
+denominator support is still strong: stable count remains `3.000/5`.
+Denominator collapse arrives later, around beta 0.8-0.9. Therefore the
+operator transfers, but the specific BOUNDARY layer shape is local.
+
+3. **Endpoint-gated classification remains empty for the same structural
+reason as BOUNDARY.** The endpoint-stable set is empty because the illusory
+pole is denominator-weak. This does not erase the discrimination; it prevents
+symmetric endpoint-gated retention claims. The valid claim is one-sided:
+coherent dipolar order survives original-vs-shuffle gating; illusory dispersion
+does not.
+
+4. **The discriminant is order, not marginal distribution.** The illusory
+sequence preserves the same alphabet, first moment, and one-point distribution
+as the dipolar sequence. The gate is therefore not measuring the marginal
+composition of gaps. It measures whether the canonical observables retain an
+ordered denominator against full shuffle.
+
+## Verdict
+**category: gate_transferability**  
+**verdict: operator**
+
+Scoped statement:
+
+> In this synthetic DUALITA perimeter, the denominator gate is transferable as
+> an operator for structural order: it separates dipolar generation from
+> illusory dispersion with replicated z support. The beta coordinate of the
+> GUE/Poisson boundary layer is not transferable: ambiguity appears near beta
+> 0.3, while denominator collapse appears near beta 0.8-0.9.
+
+So the last BOUNDARY cycles forged a lens, but one parameter of that lens was
+local to BOUNDARY. The transferable object is not "beta 0.3-0.4"; it is:
+
+> report layer maps as classifier margin plus original-vs-shuffle denominator
+> support, and treat denominator-weak poles as asymmetric structural poles.
+
+## Consecutio
+What opens now: apply the same transfer test to `TRASCENDENZA_LIMITE` and
+`G_POTENZIALE_NULLA`, but separate two quantities from the start:
+
+1. classification ambiguity layer;
+2. denominator-collapse layer.
+
+If both layers coincide in a domain, the domain has a BOUNDARY-like transition.
+If they split, as they do here, the gate is still useful but the transition
+coordinate belongs to the target domain, not to the operator.
+
+## Auto-audit: 5 lenti
+- **L1 hard constraint vs bias**: no "always/never/zero" claim. "No endpoint
+  stable observables" means none reached frequency >= 0.75 across both
+  endpoints under `abs(z) >= 2`.
+- **L2 quantity vs ratio**: classification margin is reported together with
+  stable-observable count and z means. Ratios are not interpreted without
+  denominator support.
+- **L3 no silent patching**: the report explicitly separates "gate transfers"
+  from "BOUNDARY beta coordinate transfers." The second is not claimed.
+- **L4 edge cases**: beta 0.4 has ambiguous fraction `0.417`, so only beta 0.3
+  is listed as the replicated ambiguous layer under the >= 0.5 rule.
+- **L5 re-discovery**: this is a finite symbolic-order vs permutation audit,
+  related to standard shuffle/null testing. It is not tagged as a new theorem
+  about Sturmian or Beatty sequences.
+
+## Files
+- Script: `tools/exp_duality_gate_transfer.py`
+- Main data: `tools/data/duality_gate_transfer_20260507_0803.json`
+- Seed check: `tools/data/duality_gate_transfer_20260507_0803_seedcheck.json`
+- Report: `tools/data/reports/agent_20260507_0803.md`
diff --git a/tools/exp_duality_gate_transfer.py b/tools/exp_duality_gate_transfer.py
new file mode 100644
index 0000000000000000000000000000000000000000..c04f490d17524d73d40766b8b969cdfe3d20077c
--- /dev/null
+++ b/tools/exp_duality_gate_transfer.py
@@ -0,0 +1,284 @@
+#!/usr/bin/env python3
+"""
+exp_duality_gate_transfer.py
+
+Reusable META/DUALITA audit for transferring the BOUNDARY_LAYER_GATE.
+
+Question:
+    Does the denominator-collapse gate discriminate dipolar duality from
+    illusory duality, or does it become a local BOUNDARY metric when moved
+    away from GUE/Poisson?
+
+The script builds a controlled layer map between:
+
+- DIPOLARE: a coherent golden/Beatty gap sequence generated by the orbit of
+  floor((n + phase) * phi). Its gaps are the symbolic det=-1 D-ND pole in the
+  minimal positive alphabet {1, 2}.
+- ILLUSORIA: a full random permutation of the same gaps. This preserves the
+  one-point distribution and first moment but removes the generating order.
+
+Layer beta replaces a beta fraction of dipolar positions with values from the
+illusory permutation. Canonical observables and original-vs-shuffle z gates are
+imported from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
+
+
+OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())
+PHI = (1.0 + 5.0**0.5) / 2.0
+
+
+def dipolar_gaps(n_gaps: int, phase: float) -> np.ndarray:
+    """Return coherent golden Beatty gaps in {1, 2}."""
+    n = np.arange(n_gaps + 1, dtype=float) + float(phase)
+    positions = np.floor(n * PHI).astype(float)
+    gaps = np.diff(positions)
+    mean = float(np.mean(gaps))
+    return gaps / mean if mean > 1e-15 else gaps
+
+
+def beta_layer_gaps(
+    dipolar: np.ndarray,
+    beta: float,
+    rng: np.random.Generator,
+) -> np.ndarray:
+    """Replace a beta fraction of coherent positions with a shuffled copy."""
+    illusory = rng.permutation(dipolar)
+    if beta <= 0.0:
+        out = dipolar.copy()
+    elif beta >= 1.0:
+        out = illusory.copy()
+    else:
+        mask = rng.random(len(dipolar)) < beta
+        out = dipolar.copy()
+        out[mask] = illusory[mask]
+    mean = float(np.mean(out))
+    return out / mean if mean > 1e-15 else out
+
+
+def z_against_shuffle(
+    gaps: np.ndarray,
+    n_baseline: int,
+    rng: np.random.Generator,
+) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
+    """Return original observables, shuffle baseline std, and z scores."""
+    original = compute_canonical(gaps)
+    baseline_vals = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline_vals[name].append(obs[name])
+
+    shuffle_std = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline_vals[name], dtype=float)
+        mean = float(np.mean(vals))
+        sd = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        shuffle_std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, shuffle_std, z
+
+
+def vector(row: dict, names: list[str]) -> np.ndarray:
+    return np.array([row["observables"][name] for name in names], dtype=float)
+
+
+def classify_layers(rows: list[dict], obs_names: list[str]) -> dict:
+    """Classify layers by standardized distance to dipolar/illusory endpoints."""
+    if not obs_names:
+        return {
+            "observables": [],
+            "endpoint_distance": 0.0,
+            "layers": {},
+            "ambiguous_beta": [],
+        }
+
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    dip_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    ill_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([dip_vectors, ill_vectors])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    dip_centroid = np.mean(dip_vectors, axis=0)
+    ill_centroid = np.mean(ill_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((ill_centroid - dip_centroid) / scale))
+
+    layers = {}
+    ambiguous_beta = []
+    for beta, beta_rows in sorted(by_beta.items()):
+        coords = []
+        margins = []
+        labels = []
+        for row in beta_rows:
+            x = vector(row, obs_names)
+            d_dip = float(np.linalg.norm((x - dip_centroid) / scale))
+            d_ill = float(np.linalg.norm((x - ill_centroid) / scale))
+            denom = d_dip + d_ill
+            coord = float((d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("dipolar" if d_dip < d_ill else "illusory")
+        ambiguous_fraction = float(np.mean(np.array(margins) < 0.15))
+        if ambiguous_fraction >= 0.5:
+            ambiguous_beta.append(beta)
+        layers[f"{beta:.3f}"] = {
+            "coordinate_mean": float(np.mean(coords)),
+            "coordinate_std": float(np.std(coords, ddof=1)) if len(coords) > 1 else 0.0,
+            "margin_mean": float(np.mean(margins)),
+            "ambiguous_fraction": ambiguous_fraction,
+            "illusory_label_fraction": float(np.mean(np.array(labels) == "illusory")),
+        }
+
+    return {
+        "observables": obs_names,
+        "endpoint_distance": endpoint_distance,
+        "layers": layers,
+        "ambiguous_beta": ambiguous_beta,
+    }
+
+
+def summarize_gate(rows: list[dict], z_min: float) -> dict:
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    layers = {}
+    for beta, beta_rows in sorted(by_beta.items()):
+        stable_counts = []
+        stable_freq = {name: [] for name in OBS_NAMES}
+        z_values = {name: [] for name in OBS_NAMES}
+        for row in beta_rows:
+            stable = [name for name in OBS_NAMES if abs(row["z"][name]) >= z_min]
+            stable_counts.append(len(stable))
+            for name in OBS_NAMES:
+                stable_freq[name].append(1.0 if name in stable else 0.0)
+                z_values[name].append(row["z"][name])
+        layers[f"{beta:.3f}"] = {
+            "stable_count_mean": float(np.mean(stable_counts)),
+            "stable_count_std": float(np.std(stable_counts, ddof=1)) if len(stable_counts) > 1 else 0.0,
+            "stable_frequency": {name: float(np.mean(vals)) for name, vals in stable_freq.items()},
+            "z_mean": {name: float(np.mean(vals)) for name, vals in z_values.items()},
+        }
+
+    endpoint_stable = []
+    for name in OBS_NAMES:
+        endpoint_rows = by_beta[0.0] + by_beta[1.0]
+        freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in endpoint_rows])
+        if freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def run(args: argparse.Namespace) -> dict:
+    rng = np.random.default_rng(args.seed)
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    rows = []
+
+    for rep in range(args.n_replicates):
+        phase = float(rng.random())
+        base = dipolar_gaps(args.n_gaps, phase)
+        for beta in betas:
+            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+            gaps = beta_layer_gaps(base, beta, layer_rng)
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                n_baseline=args.n_baseline,
+                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "replicate": rep,
+                    "phase": phase,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [name for name in OBS_NAMES if abs(z[name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
+
+    output = {
+        "experiment": "duality_gate_transfer",
+        "category": "gate_transferability",
+        "question": "Does the denominator-collapse gate transfer from BOUNDARY to DUALITA_DIPOLARE_VS_ILLUSORIA?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_endpoint_gated": gated_classification,
+        "rows": rows,
+    }
+
+    out_path = Path(args.out)
+    out_path.parent.mkdir(parents=True, exist_ok=True)
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
+    print(f"observables_used={OBS_NAMES}")
+    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
+    print(
+        "endpoint_distance_all="
+        f"{all_classification['endpoint_distance']:.3f} "
+        "endpoint_distance_gated="
+        f"{gated_classification['endpoint_distance']:.3f}"
+    )
+    source = gated_classification if gate["endpoint_stable_observables"] else all_classification
+    print("beta stable_count margin ambiguous illusory_fraction")
+    for beta in betas:
+        key = f"{beta:.3f}"
+        stable_count = gate["layers"][key]["stable_count_mean"]
+        layer = source["layers"].get(key, {})
+        print(
+            f"{beta:>4.2f} {stable_count:>12.3f} "
+            f"{layer.get('margin_mean', 0.0):>8.3f} "
+            f"{layer.get('ambiguous_fraction', 0.0):>9.3f} "
+            f"{layer.get('illusory_label_fraction', 0.0):>17.3f}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=4096)
+    parser.add_argument("--n-replicates", type=int, default=24)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=40)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=202605070803)
+    parser.add_argument("--out", default="tools/data/duality_gate_transfer.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

diff --git a/tools/data/reports/agent_20260507_0803.md b/tools/data/reports/agent_20260507_0803.md
new file mode 100644
index 0000000000000000000000000000000000000000..c3a0e5f46f655a3a25c8e6d2f54ec978989c54ca
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0803.md
@@ -0,0 +1,155 @@
+# Agent Report — Denominator Gate Transfers, Boundary Coordinate Does Not
+**Date**: 2026-05-07 08:03  
+**Piano**: 69  
+**Category**: gate_transferability  
+**Tension explored**: META + DUALITA_DIPOLARE_VS_ILLUSORIA  
+**verdict**: operator  
+observables_registry: 1.0.0-2026-05-06  
+observables_used: [SR, SR2, L1, L2, triple_var]
+
+## Claim Under Test
+The operator directive asked whether the `BOUNDARY_LAYER_GATE` forged on the
+GUE/Poisson boundary is a transferable operator or only a local metric.
+
+Concrete test:
+
+> Apply the denominator-collapse gate to the discrimination between dipolar
+> duality and illusory duality. If the gate distinguishes structural generation
+> from incoherent dispersion, it is an operator. If it only emits syntactically
+> valid but empty layer maps, it is a BOUNDARY-local metric.
+
+## Experiment
+Tool created: `tools/exp_duality_gate_transfer.py`
+
+Atomic perimeter:
+- DIPOLARE: coherent golden Beatty gaps generated by `floor((n+phase)*phi)`;
+- ILLUSORIA: a random permutation of the same gaps, preserving one-point
+  distribution and first moment but removing the generating order;
+- beta layer: replace a beta fraction of dipolar positions with values from
+  the illusory permutation;
+- main run: 4,096 gaps, 24 replicates, 11 beta layers, 40 shuffle baselines;
+- seed check: 3,072 gaps, 16 replicates, 11 beta layers, 32 shuffle baselines;
+- denominator gate: observable stable when `abs(z original-vs-shuffle) >= 2`;
+- classification: standardized distance to beta 0.0 and beta 1.0 centroids
+  using all five canonical observables.
+
+This is not a new BOUNDARY experiment. GUE and Poisson do not appear in the
+generator. The only transferred object is the denominator gate.
+
+## Results
+
+### Main Run
+
+Endpoint separation using all canonical observables: `3.436` standardized units.  
+Endpoint-stable observables at frequency >= 0.75 across both endpoints: `[]`.
+
+| beta | stable obs / 5 | margin | ambiguous fraction | illusory-label fraction |
+|---:|---:|---:|---:|---:|
+| 0.0 | 3.000 | 0.971 | 0.000 | 0.000 |
+| 0.1 | 3.000 | 0.479 | 0.000 | 0.000 |
+| 0.2 | 3.000 | 0.234 | 0.125 | 0.000 |
+| 0.3 | 3.000 | 0.033 | 1.000 | 0.375 |
+| 0.4 | 3.083 | 0.154 | 0.417 | 0.958 |
+| 0.5 | 3.083 | 0.269 | 0.375 | 1.000 |
+| 0.6 | 3.000 | 0.417 | 0.042 | 1.000 |
+| 0.7 | 2.917 | 0.316 | 0.167 | 1.000 |
+| 0.8 | 1.708 | 0.473 | 0.125 | 1.000 |
+| 0.9 | 0.167 | 0.463 | 0.042 | 1.000 |
+| 1.0 | 0.208 | 0.442 | 0.042 | 1.000 |
+
+Observable stability frequencies:
+- beta 0.0: `SR=1.00`, `L1=1.00`, `triple_var=1.00`; `SR2=0.00`, `L2=0.00`;
+- beta 0.3: same stable trio, with mean z approximately `SR=-19.4`, `L1=-19.4`, `triple_var=-17.6`;
+- beta 0.8: stable trio falls to frequency `0.54` each, mean z around `-1.7` to `-1.9`;
+- beta 0.9: all canonical observables are weak or near weak, mean z around `-0.4..+0.2`;
+- beta 1.0: all canonical observables are weak, mean z around `+0.1..+0.2`.
+
+### Seed Check
+
+The seed check repeated the same structure:
+- endpoint-stable observables: `[]`;
+- all-observable endpoint distance: `3.412`;
+- ambiguous beta: `[0.3]`;
+- beta 0.0 stable count: `3.000`;
+- beta 0.3 stable count: `3.000`, ambiguous fraction `1.000`;
+- beta 0.8 stable count: `1.062`;
+- beta 0.9 stable count: `0.000`;
+- beta 1.0 stable count: `0.438`.
+
+## Findings
+
+1. **The gate does not degenerate on DUALITA.** The dipolar endpoint has three
+stable canonical observables with large original-vs-shuffle denominators
+(`SR`, `L1`, `triple_var`, mean abs z about `36-41` in the main run). The
+illusory endpoint has no stable denominator support. This is a structural
+original-vs-shuffle distinction, not an empty syntactic map.
+
+2. **The BOUNDARY layer coordinate does not transfer unchanged.** In the
+GUE/Poisson run, beta 0.3-0.4 carried both classification ambiguity and
+denominator collapse. Here, beta 0.3 is classification-ambiguous, but the
+denominator support is still strong: stable count remains `3.000/5`.
+Denominator collapse arrives later, around beta 0.8-0.9. Therefore the
+operator transfers, but the specific BOUNDARY layer shape is local.
+
+3. **Endpoint-gated classification remains empty for the same structural
+reason as BOUNDARY.** The endpoint-stable set is empty because the illusory
+pole is denominator-weak. This does not erase the discrimination; it prevents
+symmetric endpoint-gated retention claims. The valid claim is one-sided:
+coherent dipolar order survives original-vs-shuffle gating; illusory dispersion
+does not.
+
+4. **The discriminant is order, not marginal distribution.** The illusory
+sequence preserves the same alphabet, first moment, and one-point distribution
+as the dipolar sequence. The gate is therefore not measuring the marginal
+composition of gaps. It measures whether the canonical observables retain an
+ordered denominator against full shuffle.
+
+## Verdict
+**category: gate_transferability**  
+**verdict: operator**
+
+Scoped statement:
+
+> In this synthetic DUALITA perimeter, the denominator gate is transferable as
+> an operator for structural order: it separates dipolar generation from
+> illusory dispersion with replicated z support. The beta coordinate of the
+> GUE/Poisson boundary layer is not transferable: ambiguity appears near beta
+> 0.3, while denominator collapse appears near beta 0.8-0.9.
+
+So the last BOUNDARY cycles forged a lens, but one parameter of that lens was
+local to BOUNDARY. The transferable object is not "beta 0.3-0.4"; it is:
+
+> report layer maps as classifier margin plus original-vs-shuffle denominator
+> support, and treat denominator-weak poles as asymmetric structural poles.
+
+## Consecutio
+What opens now: apply the same transfer test to `TRASCENDENZA_LIMITE` and
+`G_POTENZIALE_NULLA`, but separate two quantities from the start:
+
+1. classification ambiguity layer;
+2. denominator-collapse layer.
+
+If both layers coincide in a domain, the domain has a BOUNDARY-like transition.
+If they split, as they do here, the gate is still useful but the transition
+coordinate belongs to the target domain, not to the operator.
+
+## Auto-audit: 5 lenti
+- **L1 hard constraint vs bias**: no "always/never/zero" claim. "No endpoint
+  stable observables" means none reached frequency >= 0.75 across both
+  endpoints under `abs(z) >= 2`.
+- **L2 quantity vs ratio**: classification margin is reported together with
+  stable-observable count and z means. Ratios are not interpreted without
+  denominator support.
+- **L3 no silent patching**: the report explicitly separates "gate transfers"
+  from "BOUNDARY beta coordinate transfers." The second is not claimed.
+- **L4 edge cases**: beta 0.4 has ambiguous fraction `0.417`, so only beta 0.3
+  is listed as the replicated ambiguous layer under the >= 0.5 rule.
+- **L5 re-discovery**: this is a finite symbolic-order vs permutation audit,
+  related to standard shuffle/null testing. It is not tagged as a new theorem
+  about Sturmian or Beatty sequences.
+
+## Files
+- Script: `tools/exp_duality_gate_transfer.py`
+- Main data: `tools/data/duality_gate_transfer_20260507_0803.json`
+- Seed check: `tools/data/duality_gate_transfer_20260507_0803_seedcheck.json`
+- Report: `tools/data/reports/agent_20260507_0803.md`
diff --git a/tools/exp_duality_gate_transfer.py b/tools/exp_duality_gate_transfer.py
new file mode 100644
index 0000000000000000000000000000000000000000..c04f490d17524d73d40766b8b969cdfe3d20077c
--- /dev/null
+++ b/tools/exp_duality_gate_transfer.py
@@ -0,0 +1,284 @@
+#!/usr/bin/env python3
+"""
+exp_duality_gate_transfer.py
+
+Reusable META/DUALITA audit for transferring the BOUNDARY_LAYER_GATE.
+
+Question:
+    Does the denominator-collapse gate discriminate dipolar duality from
+    illusory duality, or does it become a local BOUNDARY metric when moved
+    away from GUE/Poisson?
+
+The script builds a controlled layer map between:
+
+- DIPOLARE: a coherent golden/Beatty gap sequence generated by the orbit of
+  floor((n + phase) * phi). Its gaps are the symbolic det=-1 D-ND pole in the
+  minimal positive alphabet {1, 2}.
+- ILLUSORIA: a full random permutation of the same gaps. This preserves the
+  one-point distribution and first moment but removes the generating order.
+
+Layer beta replaces a beta fraction of dipolar positions with values from the
+illusory permutation. Canonical observables and original-vs-shuffle z gates are
+imported from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
+
+
+OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())
+PHI = (1.0 + 5.0**0.5) / 2.0
+
+
+def dipolar_gaps(n_gaps: int, phase: float) -> np.ndarray:
+    """Return coherent golden Beatty gaps in {1, 2}."""
+    n = np.arange(n_gaps + 1, dtype=float) + float(phase)
+    positions = np.floor(n * PHI).astype(float)
+    gaps = np.diff(positions)
+    mean = float(np.mean(gaps))
+    return gaps / mean if mean > 1e-15 else gaps
+
+
+def beta_layer_gaps(
+    dipolar: np.ndarray,
+    beta: float,
+    rng: np.random.Generator,
+) -> np.ndarray:
+    """Replace a beta fraction of coherent positions with a shuffled copy."""
+    illusory = rng.permutation(dipolar)
+    if beta <= 0.0:
+        out = dipolar.copy()
+    elif beta >= 1.0:
+        out = illusory.copy()
+    else:
+        mask = rng.random(len(dipolar)) < beta
+        out = dipolar.copy()
+        out[mask] = illusory[mask]
+    mean = float(np.mean(out))
+    return out / mean if mean > 1e-15 else out
+
+
+def z_against_shuffle(
+    gaps: np.ndarray,
+    n_baseline: int,
+    rng: np.random.Generator,
+) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
+    """Return original observables, shuffle baseline std, and z scores."""
+    original = compute_canonical(gaps)
+    baseline_vals = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline_vals[name].append(obs[name])
+
+    shuffle_std = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline_vals[name], dtype=float)
+        mean = float(np.mean(vals))
+        sd = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        shuffle_std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, shuffle_std, z
+
+
+def vector(row: dict, names: list[str]) -> np.ndarray:
+    return np.array([row["observables"][name] for name in names], dtype=float)
+
+
+def classify_layers(rows: list[dict], obs_names: list[str]) -> dict:
+    """Classify layers by standardized distance to dipolar/illusory endpoints."""
+    if not obs_names:
+        return {
+            "observables": [],
+            "endpoint_distance": 0.0,
+            "layers": {},
+            "ambiguous_beta": [],
+        }
+
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    dip_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    ill_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([dip_vectors, ill_vectors])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    dip_centroid = np.mean(dip_vectors, axis=0)
+    ill_centroid = np.mean(ill_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((ill_centroid - dip_centroid) / scale))
+
+    layers = {}
+    ambiguous_beta = []
+    for beta, beta_rows in sorted(by_beta.items()):
+        coords = []
+        margins = []
+        labels = []
+        for row in beta_rows:
+            x = vector(row, obs_names)
+            d_dip = float(np.linalg.norm((x - dip_centroid) / scale))
+            d_ill = float(np.linalg.norm((x - ill_centroid) / scale))
+            denom = d_dip + d_ill
+            coord = float((d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("dipolar" if d_dip < d_ill else "illusory")
+        ambiguous_fraction = float(np.mean(np.array(margins) < 0.15))
+        if ambiguous_fraction >= 0.5:
+            ambiguous_beta.append(beta)
+        layers[f"{beta:.3f}"] = {
+            "coordinate_mean": float(np.mean(coords)),
+            "coordinate_std": float(np.std(coords, ddof=1)) if len(coords) > 1 else 0.0,
+            "margin_mean": float(np.mean(margins)),
+            "ambiguous_fraction": ambiguous_fraction,
+            "illusory_label_fraction": float(np.mean(np.array(labels) == "illusory")),
+        }
+
+    return {
+        "observables": obs_names,
+        "endpoint_distance": endpoint_distance,
+        "layers": layers,
+        "ambiguous_beta": ambiguous_beta,
+    }
+
+
+def summarize_gate(rows: list[dict], z_min: float) -> dict:
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    layers = {}
+    for beta, beta_rows in sorted(by_beta.items()):
+        stable_counts = []
+        stable_freq = {name: [] for name in OBS_NAMES}
+        z_values = {name: [] for name in OBS_NAMES}
+        for row in beta_rows:
+            stable = [name for name in OBS_NAMES if abs(row["z"][name]) >= z_min]
+            stable_counts.append(len(stable))
+            for name in OBS_NAMES:
+                stable_freq[name].append(1.0 if name in stable else 0.0)
+                z_values[name].append(row["z"][name])
+        layers[f"{beta:.3f}"] = {
+            "stable_count_mean": float(np.mean(stable_counts)),
+            "stable_count_std": float(np.std(stable_counts, ddof=1)) if len(stable_counts) > 1 else 0.0,
+            "stable_frequency": {name: float(np.mean(vals)) for name, vals in stable_freq.items()},
+            "z_mean": {name: float(np.mean(vals)) for name, vals in z_values.items()},
+        }
+
+    endpoint_stable = []
+    for name in OBS_NAMES:
+        endpoint_rows = by_beta[0.0] + by_beta[1.0]
+        freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in endpoint_rows])
+        if freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def run(args: argparse.Namespace) -> dict:
+    rng = np.random.default_rng(args.seed)
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    rows = []
+
+    for rep in range(args.n_replicates):
+        phase = float(rng.random())
+        base = dipolar_gaps(args.n_gaps, phase)
+        for beta in betas:
+            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+            gaps = beta_layer_gaps(base, beta, layer_rng)
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                n_baseline=args.n_baseline,
+                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "replicate": rep,
+                    "phase": phase,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [name for name in OBS_NAMES if abs(z[name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
+
+    output = {
+        "experiment": "duality_gate_transfer",
+        "category": "gate_transferability",
+        "question": "Does the denominator-collapse gate transfer from BOUNDARY to DUALITA_DIPOLARE_VS_ILLUSORIA?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_endpoint_gated": gated_classification,
+        "rows": rows,
+    }
+
+    out_path = Path(args.out)
+    out_path.parent.mkdir(parents=True, exist_ok=True)
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
+    print(f"observables_used={OBS_NAMES}")
+    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
+    print(
+        "endpoint_distance_all="
+        f"{all_classification['endpoint_distance']:.3f} "
+        "endpoint_distance_gated="
+        f"{gated_classification['endpoint_distance']:.3f}"
+    )
+    source = gated_classification if gate["endpoint_stable_observables"] else all_classification
+    print("beta stable_count margin ambiguous illusory_fraction")
+    for beta in betas:
+        key = f"{beta:.3f}"
+        stable_count = gate["layers"][key]["stable_count_mean"]
+        layer = source["layers"].get(key, {})
+        print(
+            f"{beta:>4.2f} {stable_count:>12.3f} "
+            f"{layer.get('margin_mean', 0.0):>8.3f} "
+            f"{layer.get('ambiguous_fraction', 0.0):>9.3f} "
+            f"{layer.get('illusory_label_fraction', 0.0):>17.3f}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=4096)
+    parser.add_argument("--n-replicates", type=int, default=24)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=40)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=202605070803)
+    parser.add_argument("--out", default="tools/data/duality_gate_transfer.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

diff --git a/tools/data/reports/agent_20260507_0803.md b/tools/data/reports/agent_20260507_0803.md
new file mode 100644
index 0000000000000000000000000000000000000000..c3a0e5f46f655a3a25c8e6d2f54ec978989c54ca
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0803.md
@@ -0,0 +1,155 @@
+# Agent Report — Denominator Gate Transfers, Boundary Coordinate Does Not
+**Date**: 2026-05-07 08:03  
+**Piano**: 69  
+**Category**: gate_transferability  
+**Tension explored**: META + DUALITA_DIPOLARE_VS_ILLUSORIA  
+**verdict**: operator  
+observables_registry: 1.0.0-2026-05-06  
+observables_used: [SR, SR2, L1, L2, triple_var]
+
+## Claim Under Test
+The operator directive asked whether the `BOUNDARY_LAYER_GATE` forged on the
+GUE/Poisson boundary is a transferable operator or only a local metric.
+
+Concrete test:
+
+> Apply the denominator-collapse gate to the discrimination between dipolar
+> duality and illusory duality. If the gate distinguishes structural generation
+> from incoherent dispersion, it is an operator. If it only emits syntactically
+> valid but empty layer maps, it is a BOUNDARY-local metric.
+
+## Experiment
+Tool created: `tools/exp_duality_gate_transfer.py`
+
+Atomic perimeter:
+- DIPOLARE: coherent golden Beatty gaps generated by `floor((n+phase)*phi)`;
+- ILLUSORIA: a random permutation of the same gaps, preserving one-point
+  distribution and first moment but removing the generating order;
+- beta layer: replace a beta fraction of dipolar positions with values from
+  the illusory permutation;
+- main run: 4,096 gaps, 24 replicates, 11 beta layers, 40 shuffle baselines;
+- seed check: 3,072 gaps, 16 replicates, 11 beta layers, 32 shuffle baselines;
+- denominator gate: observable stable when `abs(z original-vs-shuffle) >= 2`;
+- classification: standardized distance to beta 0.0 and beta 1.0 centroids
+  using all five canonical observables.
+
+This is not a new BOUNDARY experiment. GUE and Poisson do not appear in the
+generator. The only transferred object is the denominator gate.
+
+## Results
+
+### Main Run
+
+Endpoint separation using all canonical observables: `3.436` standardized units.  
+Endpoint-stable observables at frequency >= 0.75 across both endpoints: `[]`.
+
+| beta | stable obs / 5 | margin | ambiguous fraction | illusory-label fraction |
+|---:|---:|---:|---:|---:|
+| 0.0 | 3.000 | 0.971 | 0.000 | 0.000 |
+| 0.1 | 3.000 | 0.479 | 0.000 | 0.000 |
+| 0.2 | 3.000 | 0.234 | 0.125 | 0.000 |
+| 0.3 | 3.000 | 0.033 | 1.000 | 0.375 |
+| 0.4 | 3.083 | 0.154 | 0.417 | 0.958 |
+| 0.5 | 3.083 | 0.269 | 0.375 | 1.000 |
+| 0.6 | 3.000 | 0.417 | 0.042 | 1.000 |
+| 0.7 | 2.917 | 0.316 | 0.167 | 1.000 |
+| 0.8 | 1.708 | 0.473 | 0.125 | 1.000 |
+| 0.9 | 0.167 | 0.463 | 0.042 | 1.000 |
+| 1.0 | 0.208 | 0.442 | 0.042 | 1.000 |
+
+Observable stability frequencies:
+- beta 0.0: `SR=1.00`, `L1=1.00`, `triple_var=1.00`; `SR2=0.00`, `L2=0.00`;
+- beta 0.3: same stable trio, with mean z approximately `SR=-19.4`, `L1=-19.4`, `triple_var=-17.6`;
+- beta 0.8: stable trio falls to frequency `0.54` each, mean z around `-1.7` to `-1.9`;
+- beta 0.9: all canonical observables are weak or near weak, mean z around `-0.4..+0.2`;
+- beta 1.0: all canonical observables are weak, mean z around `+0.1..+0.2`.
+
+### Seed Check
+
+The seed check repeated the same structure:
+- endpoint-stable observables: `[]`;
+- all-observable endpoint distance: `3.412`;
+- ambiguous beta: `[0.3]`;
+- beta 0.0 stable count: `3.000`;
+- beta 0.3 stable count: `3.000`, ambiguous fraction `1.000`;
+- beta 0.8 stable count: `1.062`;
+- beta 0.9 stable count: `0.000`;
+- beta 1.0 stable count: `0.438`.
+
+## Findings
+
+1. **The gate does not degenerate on DUALITA.** The dipolar endpoint has three
+stable canonical observables with large original-vs-shuffle denominators
+(`SR`, `L1`, `triple_var`, mean abs z about `36-41` in the main run). The
+illusory endpoint has no stable denominator support. This is a structural
+original-vs-shuffle distinction, not an empty syntactic map.
+
+2. **The BOUNDARY layer coordinate does not transfer unchanged.** In the
+GUE/Poisson run, beta 0.3-0.4 carried both classification ambiguity and
+denominator collapse. Here, beta 0.3 is classification-ambiguous, but the
+denominator support is still strong: stable count remains `3.000/5`.
+Denominator collapse arrives later, around beta 0.8-0.9. Therefore the
+operator transfers, but the specific BOUNDARY layer shape is local.
+
+3. **Endpoint-gated classification remains empty for the same structural
+reason as BOUNDARY.** The endpoint-stable set is empty because the illusory
+pole is denominator-weak. This does not erase the discrimination; it prevents
+symmetric endpoint-gated retention claims. The valid claim is one-sided:
+coherent dipolar order survives original-vs-shuffle gating; illusory dispersion
+does not.
+
+4. **The discriminant is order, not marginal distribution.** The illusory
+sequence preserves the same alphabet, first moment, and one-point distribution
+as the dipolar sequence. The gate is therefore not measuring the marginal
+composition of gaps. It measures whether the canonical observables retain an
+ordered denominator against full shuffle.
+
+## Verdict
+**category: gate_transferability**  
+**verdict: operator**
+
+Scoped statement:
+
+> In this synthetic DUALITA perimeter, the denominator gate is transferable as
+> an operator for structural order: it separates dipolar generation from
+> illusory dispersion with replicated z support. The beta coordinate of the
+> GUE/Poisson boundary layer is not transferable: ambiguity appears near beta
+> 0.3, while denominator collapse appears near beta 0.8-0.9.
+
+So the last BOUNDARY cycles forged a lens, but one parameter of that lens was
+local to BOUNDARY. The transferable object is not "beta 0.3-0.4"; it is:
+
+> report layer maps as classifier margin plus original-vs-shuffle denominator
+> support, and treat denominator-weak poles as asymmetric structural poles.
+
+## Consecutio
+What opens now: apply the same transfer test to `TRASCENDENZA_LIMITE` and
+`G_POTENZIALE_NULLA`, but separate two quantities from the start:
+
+1. classification ambiguity layer;
+2. denominator-collapse layer.
+
+If both layers coincide in a domain, the domain has a BOUNDARY-like transition.
+If they split, as they do here, the gate is still useful but the transition
+coordinate belongs to the target domain, not to the operator.
+
+## Auto-audit: 5 lenti
+- **L1 hard constraint vs bias**: no "always/never/zero" claim. "No endpoint
+  stable observables" means none reached frequency >= 0.75 across both
+  endpoints under `abs(z) >= 2`.
+- **L2 quantity vs ratio**: classification margin is reported together with
+  stable-observable count and z means. Ratios are not interpreted without
+  denominator support.
+- **L3 no silent patching**: the report explicitly separates "gate transfers"
+  from "BOUNDARY beta coordinate transfers." The second is not claimed.
+- **L4 edge cases**: beta 0.4 has ambiguous fraction `0.417`, so only beta 0.3
+  is listed as the replicated ambiguous layer under the >= 0.5 rule.
+- **L5 re-discovery**: this is a finite symbolic-order vs permutation audit,
+  related to standard shuffle/null testing. It is not tagged as a new theorem
+  about Sturmian or Beatty sequences.
+
+## Files
+- Script: `tools/exp_duality_gate_transfer.py`
+- Main data: `tools/data/duality_gate_transfer_20260507_0803.json`
+- Seed check: `tools/data/duality_gate_transfer_20260507_0803_seedcheck.json`
+- Report: `tools/data/reports/agent_20260507_0803.md`
diff --git a/tools/exp_duality_gate_transfer.py b/tools/exp_duality_gate_transfer.py
new file mode 100644
index 0000000000000000000000000000000000000000..c04f490d17524d73d40766b8b969cdfe3d20077c
--- /dev/null
+++ b/tools/exp_duality_gate_transfer.py
@@ -0,0 +1,284 @@
+#!/usr/bin/env python3
+"""
+exp_duality_gate_transfer.py
+
+Reusable META/DUALITA audit for transferring the BOUNDARY_LAYER_GATE.
+
+Question:
+    Does the denominator-collapse gate discriminate dipolar duality from
+    illusory duality, or does it become a local BOUNDARY metric when moved
+    away from GUE/Poisson?
+
+The script builds a controlled layer map between:
+
+- DIPOLARE: a coherent golden/Beatty gap sequence generated by the orbit of
+  floor((n + phase) * phi). Its gaps are the symbolic det=-1 D-ND pole in the
+  minimal positive alphabet {1, 2}.
+- ILLUSORIA: a full random permutation of the same gaps. This preserves the
+  one-point distribution and first moment but removes the generating order.
+
+Layer beta replaces a beta fraction of dipolar positions with values from the
+illusory permutation. Canonical observables and original-vs-shuffle z gates are
+imported from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
+
+
+OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())
+PHI = (1.0 + 5.0**0.5) / 2.0
+
+
+def dipolar_gaps(n_gaps: int, phase: float) -> np.ndarray:
+    """Return coherent golden Beatty gaps in {1, 2}."""
+    n = np.arange(n_gaps + 1, dtype=float) + float(phase)
+    positions = np.floor(n * PHI).astype(float)
+    gaps = np.diff(positions)
+    mean = float(np.mean(gaps))
+    return gaps / mean if mean > 1e-15 else gaps
+
+
+def beta_layer_gaps(
+    dipolar: np.ndarray,
+    beta: float,
+    rng: np.random.Generator,
+) -> np.ndarray:
+    """Replace a beta fraction of coherent positions with a shuffled copy."""
+    illusory = rng.permutation(dipolar)
+    if beta <= 0.0:
+        out = dipolar.copy()
+    elif beta >= 1.0:
+        out = illusory.copy()
+    else:
+        mask = rng.random(len(dipolar)) < beta
+        out = dipolar.copy()
+        out[mask] = illusory[mask]
+    mean = float(np.mean(out))
+    return out / mean if mean > 1e-15 else out
+
+
+def z_against_shuffle(
+    gaps: np.ndarray,
+    n_baseline: int,
+    rng: np.random.Generator,
+) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
+    """Return original observables, shuffle baseline std, and z scores."""
+    original = compute_canonical(gaps)
+    baseline_vals = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline_vals[name].append(obs[name])
+
+    shuffle_std = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline_vals[name], dtype=float)
+        mean = float(np.mean(vals))
+        sd = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        shuffle_std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, shuffle_std, z
+
+
+def vector(row: dict, names: list[str]) -> np.ndarray:
+    return np.array([row["observables"][name] for name in names], dtype=float)
+
+
+def classify_layers(rows: list[dict], obs_names: list[str]) -> dict:
+    """Classify layers by standardized distance to dipolar/illusory endpoints."""
+    if not obs_names:
+        return {
+            "observables": [],
+            "endpoint_distance": 0.0,
+            "layers": {},
+            "ambiguous_beta": [],
+        }
+
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    dip_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    ill_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([dip_vectors, ill_vectors])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    dip_centroid = np.mean(dip_vectors, axis=0)
+    ill_centroid = np.mean(ill_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((ill_centroid - dip_centroid) / scale))
+
+    layers = {}
+    ambiguous_beta = []
+    for beta, beta_rows in sorted(by_beta.items()):
+        coords = []
+        margins = []
+        labels = []
+        for row in beta_rows:
+            x = vector(row, obs_names)
+            d_dip = float(np.linalg.norm((x - dip_centroid) / scale))
+            d_ill = float(np.linalg.norm((x - ill_centroid) / scale))
+            denom = d_dip + d_ill
+            coord = float((d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("dipolar" if d_dip < d_ill else "illusory")
+        ambiguous_fraction = float(np.mean(np.array(margins) < 0.15))
+        if ambiguous_fraction >= 0.5:
+            ambiguous_beta.append(beta)
+        layers[f"{beta:.3f}"] = {
+            "coordinate_mean": float(np.mean(coords)),
+            "coordinate_std": float(np.std(coords, ddof=1)) if len(coords) > 1 else 0.0,
+            "margin_mean": float(np.mean(margins)),
+            "ambiguous_fraction": ambiguous_fraction,
+            "illusory_label_fraction": float(np.mean(np.array(labels) == "illusory")),
+        }
+
+    return {
+        "observables": obs_names,
+        "endpoint_distance": endpoint_distance,
+        "layers": layers,
+        "ambiguous_beta": ambiguous_beta,
+    }
+
+
+def summarize_gate(rows: list[dict], z_min: float) -> dict:
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    layers = {}
+    for beta, beta_rows in sorted(by_beta.items()):
+        stable_counts = []
+        stable_freq = {name: [] for name in OBS_NAMES}
+        z_values = {name: [] for name in OBS_NAMES}
+        for row in beta_rows:
+            stable = [name for name in OBS_NAMES if abs(row["z"][name]) >= z_min]
+            stable_counts.append(len(stable))
+            for name in OBS_NAMES:
+                stable_freq[name].append(1.0 if name in stable else 0.0)
+                z_values[name].append(row["z"][name])
+        layers[f"{beta:.3f}"] = {
+            "stable_count_mean": float(np.mean(stable_counts)),
+            "stable_count_std": float(np.std(stable_counts, ddof=1)) if len(stable_counts) > 1 else 0.0,
+            "stable_frequency": {name: float(np.mean(vals)) for name, vals in stable_freq.items()},
+            "z_mean": {name: float(np.mean(vals)) for name, vals in z_values.items()},
+        }
+
+    endpoint_stable = []
+    for name in OBS_NAMES:
+        endpoint_rows = by_beta[0.0] + by_beta[1.0]
+        freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in endpoint_rows])
+        if freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def run(args: argparse.Namespace) -> dict:
+    rng = np.random.default_rng(args.seed)
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    rows = []
+
+    for rep in range(args.n_replicates):
+        phase = float(rng.random())
+        base = dipolar_gaps(args.n_gaps, phase)
+        for beta in betas:
+            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+            gaps = beta_layer_gaps(base, beta, layer_rng)
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                n_baseline=args.n_baseline,
+                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "replicate": rep,
+                    "phase": phase,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [name for name in OBS_NAMES if abs(z[name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
+
+    output = {
+        "experiment": "duality_gate_transfer",
+        "category": "gate_transferability",
+        "question": "Does the denominator-collapse gate transfer from BOUNDARY to DUALITA_DIPOLARE_VS_ILLUSORIA?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_endpoint_gated": gated_classification,
+        "rows": rows,
+    }
+
+    out_path = Path(args.out)
+    out_path.parent.mkdir(parents=True, exist_ok=True)
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
+    print(f"observables_used={OBS_NAMES}")
+    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
+    print(
+        "endpoint_distance_all="
+        f"{all_classification['endpoint_distance']:.3f} "
+        "endpoint_distance_gated="
+        f"{gated_classification['endpoint_distance']:.3f}"
+    )
+    source = gated_classification if gate["endpoint_stable_observables"] else all_classification
+    print("beta stable_count margin ambiguous illusory_fraction")
+    for beta in betas:
+        key = f"{beta:.3f}"
+        stable_count = gate["layers"][key]["stable_count_mean"]
+        layer = source["layers"].get(key, {})
+        print(
+            f"{beta:>4.2f} {stable_count:>12.3f} "
+            f"{layer.get('margin_mean', 0.0):>8.3f} "
+            f"{layer.get('ambiguous_fraction', 0.0):>9.3f} "
+            f"{layer.get('illusory_label_fraction', 0.0):>17.3f}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=4096)
+    parser.add_argument("--n-replicates", type=int, default=24)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=40)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=202605070803)
+    parser.add_argument("--out", default="tools/data/duality_gate_transfer.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

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

codex
Il falsifier locale è stato invocato ma non ha provider disponibili (`codex-cli`, `claude-cli`, `openrouter`), quindi ha chiuso in modalità conservativa senza una valutazione contenutistica. Procedo con controlli locali deterministici e lascio il blocco provider esplicito nel riepilogo.
exec
/bin/bash -lc "ls -l tools/data/reports/falsifier_20260507_0803.json && sed -n '1,220p' tools/data/reports/falsifier_20260507_0803.json" in /opt/MM_D-ND
 exited 2 in 0ms:
ls: cannot access 'tools/data/reports/falsifier_20260507_0803.json': No such file or directory

exec
/bin/bash -lc 'python -m py_compile tools/exp_duality_gate_transfer.py' in /opt/MM_D-ND
 succeeded in 0ms:
exec
/bin/bash -lc 'python tools/exp_duality_gate_transfer.py --n-gaps 256 --n-replicates 2 --n-baseline 4 --n-beta 3 --seed 7 --out /tmp/duality_gate_smoke.json' in /opt/MM_D-ND
diff --git a/tools/data/reports/agent_20260507_0803.md b/tools/data/reports/agent_20260507_0803.md
new file mode 100644
index 0000000000000000000000000000000000000000..c3a0e5f46f655a3a25c8e6d2f54ec978989c54ca
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0803.md
@@ -0,0 +1,155 @@
+# Agent Report — Denominator Gate Transfers, Boundary Coordinate Does Not
+**Date**: 2026-05-07 08:03  
+**Piano**: 69  
+**Category**: gate_transferability  
+**Tension explored**: META + DUALITA_DIPOLARE_VS_ILLUSORIA  
+**verdict**: operator  
+observables_registry: 1.0.0-2026-05-06  
+observables_used: [SR, SR2, L1, L2, triple_var]
+
+## Claim Under Test
+The operator directive asked whether the `BOUNDARY_LAYER_GATE` forged on the
+GUE/Poisson boundary is a transferable operator or only a local metric.
+
+Concrete test:
+
+> Apply the denominator-collapse gate to the discrimination between dipolar
+> duality and illusory duality. If the gate distinguishes structural generation
+> from incoherent dispersion, it is an operator. If it only emits syntactically
+> valid but empty layer maps, it is a BOUNDARY-local metric.
+
+## Experiment
+Tool created: `tools/exp_duality_gate_transfer.py`
+
+Atomic perimeter:
+- DIPOLARE: coherent golden Beatty gaps generated by `floor((n+phase)*phi)`;
+- ILLUSORIA: a random permutation of the same gaps, preserving one-point
+  distribution and first moment but removing the generating order;
+- beta layer: replace a beta fraction of dipolar positions with values from
+  the illusory permutation;
+- main run: 4,096 gaps, 24 replicates, 11 beta layers, 40 shuffle baselines;
+- seed check: 3,072 gaps, 16 replicates, 11 beta layers, 32 shuffle baselines;
+- denominator gate: observable stable when `abs(z original-vs-shuffle) >= 2`;
+- classification: standardized distance to beta 0.0 and beta 1.0 centroids
+  using all five canonical observables.
+
+This is not a new BOUNDARY experiment. GUE and Poisson do not appear in the
+generator. The only transferred object is the denominator gate.
+
+## Results
+
+### Main Run
+
+Endpoint separation using all canonical observables: `3.436` standardized units.  
+Endpoint-stable observables at frequency >= 0.75 across both endpoints: `[]`.
+
+| beta | stable obs / 5 | margin | ambiguous fraction | illusory-label fraction |
+|---:|---:|---:|---:|---:|
+| 0.0 | 3.000 | 0.971 | 0.000 | 0.000 |
+| 0.1 | 3.000 | 0.479 | 0.000 | 0.000 |
+| 0.2 | 3.000 | 0.234 | 0.125 | 0.000 |
+| 0.3 | 3.000 | 0.033 | 1.000 | 0.375 |
+| 0.4 | 3.083 | 0.154 | 0.417 | 0.958 |
+| 0.5 | 3.083 | 0.269 | 0.375 | 1.000 |
+| 0.6 | 3.000 | 0.417 | 0.042 | 1.000 |
+| 0.7 | 2.917 | 0.316 | 0.167 | 1.000 |
+| 0.8 | 1.708 | 0.473 | 0.125 | 1.000 |
+| 0.9 | 0.167 | 0.463 | 0.042 | 1.000 |
+| 1.0 | 0.208 | 0.442 | 0.042 | 1.000 |
+
+Observable stability frequencies:
+- beta 0.0: `SR=1.00`, `L1=1.00`, `triple_var=1.00`; `SR2=0.00`, `L2=0.00`;
+- beta 0.3: same stable trio, with mean z approximately `SR=-19.4`, `L1=-19.4`, `triple_var=-17.6`;
+- beta 0.8: stable trio falls to frequency `0.54` each, mean z around `-1.7` to `-1.9`;
+- beta 0.9: all canonical observables are weak or near weak, mean z around `-0.4..+0.2`;
+- beta 1.0: all canonical observables are weak, mean z around `+0.1..+0.2`.
+
+### Seed Check
+
+The seed check repeated the same structure:
+- endpoint-stable observables: `[]`;
+- all-observable endpoint distance: `3.412`;
+- ambiguous beta: `[0.3]`;
+- beta 0.0 stable count: `3.000`;
+- beta 0.3 stable count: `3.000`, ambiguous fraction `1.000`;
+- beta 0.8 stable count: `1.062`;
+- beta 0.9 stable count: `0.000`;
+- beta 1.0 stable count: `0.438`.
+
+## Findings
+
+1. **The gate does not degenerate on DUALITA.** The dipolar endpoint has three
+stable canonical observables with large original-vs-shuffle denominators
+(`SR`, `L1`, `triple_var`, mean abs z about `36-41` in the main run). The
+illusory endpoint has no stable denominator support. This is a structural
+original-vs-shuffle distinction, not an empty syntactic map.
+
+2. **The BOUNDARY layer coordinate does not transfer unchanged.** In the
+GUE/Poisson run, beta 0.3-0.4 carried both classification ambiguity and
+denominator collapse. Here, beta 0.3 is classification-ambiguous, but the
+denominator support is still strong: stable count remains `3.000/5`.
+Denominator collapse arrives later, around beta 0.8-0.9. Therefore the
+operator transfers, but the specific BOUNDARY layer shape is local.
+
+3. **Endpoint-gated classification remains empty for the same structural
+reason as BOUNDARY.** The endpoint-stable set is empty because the illusory
+pole is denominator-weak. This does not erase the discrimination; it prevents
+symmetric endpoint-gated retention claims. The valid claim is one-sided:
+coherent dipolar order survives original-vs-shuffle gating; illusory dispersion
+does not.
+
+4. **The discriminant is order, not marginal distribution.** The illusory
+sequence preserves the same alphabet, first moment, and one-point distribution
+as the dipolar sequence. The gate is therefore not measuring the marginal
+composition of gaps. It measures whether the canonical observables retain an
+ordered denominator against full shuffle.
+
+## Verdict
+**category: gate_transferability**  
+**verdict: operator**
+
+Scoped statement:
+
+> In this synthetic DUALITA perimeter, the denominator gate is transferable as
+> an operator for structural order: it separates dipolar generation from
+> illusory dispersion with replicated z support. The beta coordinate of the
+> GUE/Poisson boundary layer is not transferable: ambiguity appears near beta
+> 0.3, while denominator collapse appears near beta 0.8-0.9.
+
+So the last BOUNDARY cycles forged a lens, but one parameter of that lens was
+local to BOUNDARY. The transferable object is not "beta 0.3-0.4"; it is:
+
+> report layer maps as classifier margin plus original-vs-shuffle denominator
+> support, and treat denominator-weak poles as asymmetric structural poles.
+
+## Consecutio
+What opens now: apply the same transfer test to `TRASCENDENZA_LIMITE` and
+`G_POTENZIALE_NULLA`, but separate two quantities from the start:
+
+1. classification ambiguity layer;
+2. denominator-collapse layer.
+
+If both layers coincide in a domain, the domain has a BOUNDARY-like transition.
+If they split, as they do here, the gate is still useful but the transition
+coordinate belongs to the target domain, not to the operator.
+
+## Auto-audit: 5 lenti
+- **L1 hard constraint vs bias**: no "always/never/zero" claim. "No endpoint
+  stable observables" means none reached frequency >= 0.75 across both
+  endpoints under `abs(z) >= 2`.
+- **L2 quantity vs ratio**: classification margin is reported together with
+  stable-observable count and z means. Ratios are not interpreted without
+  denominator support.
+- **L3 no silent patching**: the report explicitly separates "gate transfers"
+  from "BOUNDARY beta coordinate transfers." The second is not claimed.
+- **L4 edge cases**: beta 0.4 has ambiguous fraction `0.417`, so only beta 0.3
+  is listed as the replicated ambiguous layer under the >= 0.5 rule.
+- **L5 re-discovery**: this is a finite symbolic-order vs permutation audit,
+  related to standard shuffle/null testing. It is not tagged as a new theorem
+  about Sturmian or Beatty sequences.
+
+## Files
+- Script: `tools/exp_duality_gate_transfer.py`
+- Main data: `tools/data/duality_gate_transfer_20260507_0803.json`
+- Seed check: `tools/data/duality_gate_transfer_20260507_0803_seedcheck.json`
+- Report: `tools/data/reports/agent_20260507_0803.md`
diff --git a/tools/exp_duality_gate_transfer.py b/tools/exp_duality_gate_transfer.py
new file mode 100644
index 0000000000000000000000000000000000000000..c04f490d17524d73d40766b8b969cdfe3d20077c
--- /dev/null
+++ b/tools/exp_duality_gate_transfer.py
@@ -0,0 +1,284 @@
+#!/usr/bin/env python3
+"""
+exp_duality_gate_transfer.py
+
+Reusable META/DUALITA audit for transferring the BOUNDARY_LAYER_GATE.
+
+Question:
+    Does the denominator-collapse gate discriminate dipolar duality from
+    illusory duality, or does it become a local BOUNDARY metric when moved
+    away from GUE/Poisson?
+
+The script builds a controlled layer map between:
+
+- DIPOLARE: a coherent golden/Beatty gap sequence generated by the orbit of
+  floor((n + phase) * phi). Its gaps are the symbolic det=-1 D-ND pole in the
+  minimal positive alphabet {1, 2}.
+- ILLUSORIA: a full random permutation of the same gaps. This preserves the
+  one-point distribution and first moment but removes the generating order.
+
+Layer beta replaces a beta fraction of dipolar positions with values from the
+illusory permutation. Canonical observables and original-vs-shuffle z gates are
+imported from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
+
+
+OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())
+PHI = (1.0 + 5.0**0.5) / 2.0
+
+
+def dipolar_gaps(n_gaps: int, phase: float) -> np.ndarray:
+    """Return coherent golden Beatty gaps in {1, 2}."""
+    n = np.arange(n_gaps + 1, dtype=float) + float(phase)
+    positions = np.floor(n * PHI).astype(float)
+    gaps = np.diff(positions)
+    mean = float(np.mean(gaps))
+    return gaps / mean if mean > 1e-15 else gaps
+
+
+def beta_layer_gaps(
+    dipolar: np.ndarray,
+    beta: float,
+    rng: np.random.Generator,
+) -> np.ndarray:
+    """Replace a beta fraction of coherent positions with a shuffled copy."""
+    illusory = rng.permutation(dipolar)
+    if beta <= 0.0:
+        out = dipolar.copy()
+    elif beta >= 1.0:
+        out = illusory.copy()
+    else:
+        mask = rng.random(len(dipolar)) < beta
+        out = dipolar.copy()
+        out[mask] = illusory[mask]
+    mean = float(np.mean(out))
+    return out / mean if mean > 1e-15 else out
+
+
+def z_against_shuffle(
+    gaps: np.ndarray,
+    n_baseline: int,
+    rng: np.random.Generator,
+) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
+    """Return original observables, shuffle baseline std, and z scores."""
+    original = compute_canonical(gaps)
+    baseline_vals = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline_vals[name].append(obs[name])
+
+    shuffle_std = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline_vals[name], dtype=float)
+        mean = float(np.mean(vals))
+        sd = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        shuffle_std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, shuffle_std, z
+
+
+def vector(row: dict, names: list[str]) -> np.ndarray:
+    return np.array([row["observables"][name] for name in names], dtype=float)
+
+
+def classify_layers(rows: list[dict], obs_names: list[str]) -> dict:
+    """Classify layers by standardized distance to dipolar/illusory endpoints."""
+    if not obs_names:
+        return {
+            "observables": [],
+            "endpoint_distance": 0.0,
+            "layers": {},
+            "ambiguous_beta": [],
+        }
+
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    dip_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    ill_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([dip_vectors, ill_vectors])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    dip_centroid = np.mean(dip_vectors, axis=0)
+    ill_centroid = np.mean(ill_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((ill_centroid - dip_centroid) / scale))
+
+    layers = {}
+    ambiguous_beta = []
+    for beta, beta_rows in sorted(by_beta.items()):
+        coords = []
+        margins = []
+        labels = []
+        for row in beta_rows:
+            x = vector(row, obs_names)
+            d_dip = float(np.linalg.norm((x - dip_centroid) / scale))
+            d_ill = float(np.linalg.norm((x - ill_centroid) / scale))
+            denom = d_dip + d_ill
+            coord = float((d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("dipolar" if d_dip < d_ill else "illusory")
+        ambiguous_fraction = float(np.mean(np.array(margins) < 0.15))
+        if ambiguous_fraction >= 0.5:
+            ambiguous_beta.append(beta)
+        layers[f"{beta:.3f}"] = {
+            "coordinate_mean": float(np.mean(coords)),
+            "coordinate_std": float(np.std(coords, ddof=1)) if len(coords) > 1 else 0.0,
+            "margin_mean": float(np.mean(margins)),
+            "ambiguous_fraction": ambiguous_fraction,
+            "illusory_label_fraction": float(np.mean(np.array(labels) == "illusory")),
+        }
+
+    return {
+        "observables": obs_names,
+        "endpoint_distance": endpoint_distance,
+        "layers": layers,
+        "ambiguous_beta": ambiguous_beta,
+    }
+
+
+def summarize_gate(rows: list[dict], z_min: float) -> dict:
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    layers = {}
+    for beta, beta_rows in sorted(by_beta.items()):
+        stable_counts = []
+        stable_freq = {name: [] for name in OBS_NAMES}
+        z_values = {name: [] for name in OBS_NAMES}
+        for row in beta_rows:
+            stable = [name for name in OBS_NAMES if abs(row["z"][name]) >= z_min]
+            stable_counts.append(len(stable))
+            for name in OBS_NAMES:
+                stable_freq[name].append(1.0 if name in stable else 0.0)
+                z_values[name].append(row["z"][name])
+        layers[f"{beta:.3f}"] = {
+            "stable_count_mean": float(np.mean(stable_counts)),
+            "stable_count_std": float(np.std(stable_counts, ddof=1)) if len(stable_counts) > 1 else 0.0,
+            "stable_frequency": {name: float(np.mean(vals)) for name, vals in stable_freq.items()},
+            "z_mean": {name: float(np.mean(vals)) for name, vals in z_values.items()},
+        }
+
+    endpoint_stable = []
+    for name in OBS_NAMES:
+        endpoint_rows = by_beta[0.0] + by_beta[1.0]
+        freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in endpoint_rows])
+        if freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def run(args: argparse.Namespace) -> dict:
+    rng = np.random.default_rng(args.seed)
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    rows = []
+
+    for rep in range(args.n_replicates):
+        phase = float(rng.random())
+        base = dipolar_gaps(args.n_gaps, phase)
+        for beta in betas:
+            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+            gaps = beta_layer_gaps(base, beta, layer_rng)
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                n_baseline=args.n_baseline,
+                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "replicate": rep,
+                    "phase": phase,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [name for name in OBS_NAMES if abs(z[name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
+
+    output = {
+        "experiment": "duality_gate_transfer",
+        "category": "gate_transferability",
+        "question": "Does the denominator-collapse gate transfer from BOUNDARY to DUALITA_DIPOLARE_VS_ILLUSORIA?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_endpoint_gated": gated_classification,
+        "rows": rows,
+    }
+
+    out_path = Path(args.out)
+    out_path.parent.mkdir(parents=True, exist_ok=True)
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
+    print(f"observables_used={OBS_NAMES}")
+    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
+    print(
+        "endpoint_distance_all="
+        f"{all_classification['endpoint_distance']:.3f} "
+        "endpoint_distance_gated="
+        f"{gated_classification['endpoint_distance']:.3f}"
+    )
+    source = gated_classification if gate["endpoint_stable_observables"] else all_classification
+    print("beta stable_count margin ambiguous illusory_fraction")
+    for beta in betas:
+        key = f"{beta:.3f}"
+        stable_count = gate["layers"][key]["stable_count_mean"]
+        layer = source["layers"].get(key, {})
+        print(
+            f"{beta:>4.2f} {stable_count:>12.3f} "
+            f"{layer.get('margin_mean', 0.0):>8.3f} "
+            f"{layer.get('ambiguous_fraction', 0.0):>9.3f} "
+            f"{layer.get('illusory_label_fraction', 0.0):>17.3f}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=4096)
+    parser.add_argument("--n-replicates", type=int, default=24)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=40)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=202605070803)
+    parser.add_argument("--out", default="tools/data/duality_gate_transfer.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

 succeeded in 161ms:
observables_registry=1.0.0-2026-05-06
observables_used=['SR', 'SR2', 'L1', 'L2', 'triple_var']
endpoint_stable_observables=['triple_var']
endpoint_distance_all=3.785 endpoint_distance_gated=1.732
beta stable_count margin ambiguous illusory_fraction
0.00        3.000    0.995     0.000             0.000
0.50        0.000    0.414     0.000             1.000
1.00        1.500    0.966     0.000             1.000
saved /tmp/duality_gate_smoke.json

exec
/bin/bash -lc 'LC_ALL=C rg -n "[''^[:ascii:]]" tools/data/reports/agent_20260507_0803.md tools/exp_duality_gate_transfer.py' in /opt/MM_D-ND
 succeeded in 0ms:
tools/data/reports/agent_20260507_0803.md:1:# Agent Report — Denominator Gate Transfers, Boundary Coordinate Does Not

diff --git a/tools/data/reports/agent_20260507_0803.md b/tools/data/reports/agent_20260507_0803.md
new file mode 100644
index 0000000000000000000000000000000000000000..c3a0e5f46f655a3a25c8e6d2f54ec978989c54ca
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0803.md
@@ -0,0 +1,155 @@
+# Agent Report — Denominator Gate Transfers, Boundary Coordinate Does Not
+**Date**: 2026-05-07 08:03  
+**Piano**: 69  
+**Category**: gate_transferability  
+**Tension explored**: META + DUALITA_DIPOLARE_VS_ILLUSORIA  
+**verdict**: operator  
+observables_registry: 1.0.0-2026-05-06  
+observables_used: [SR, SR2, L1, L2, triple_var]
+
+## Claim Under Test
+The operator directive asked whether the `BOUNDARY_LAYER_GATE` forged on the
+GUE/Poisson boundary is a transferable operator or only a local metric.
+
+Concrete test:
+
+> Apply the denominator-collapse gate to the discrimination between dipolar
+> duality and illusory duality. If the gate distinguishes structural generation
+> from incoherent dispersion, it is an operator. If it only emits syntactically
+> valid but empty layer maps, it is a BOUNDARY-local metric.
+
+## Experiment
+Tool created: `tools/exp_duality_gate_transfer.py`
+
+Atomic perimeter:
+- DIPOLARE: coherent golden Beatty gaps generated by `floor((n+phase)*phi)`;
+- ILLUSORIA: a random permutation of the same gaps, preserving one-point
+  distribution and first moment but removing the generating order;
+- beta layer: replace a beta fraction of dipolar positions with values from
+  the illusory permutation;
+- main run: 4,096 gaps, 24 replicates, 11 beta layers, 40 shuffle baselines;
+- seed check: 3,072 gaps, 16 replicates, 11 beta layers, 32 shuffle baselines;
+- denominator gate: observable stable when `abs(z original-vs-shuffle) >= 2`;
+- classification: standardized distance to beta 0.0 and beta 1.0 centroids
+  using all five canonical observables.
+
+This is not a new BOUNDARY experiment. GUE and Poisson do not appear in the
+generator. The only transferred object is the denominator gate.
+
+## Results
+
+### Main Run
+
+Endpoint separation using all canonical observables: `3.436` standardized units.  
+Endpoint-stable observables at frequency >= 0.75 across both endpoints: `[]`.
+
+| beta | stable obs / 5 | margin | ambiguous fraction | illusory-label fraction |
+|---:|---:|---:|---:|---:|
+| 0.0 | 3.000 | 0.971 | 0.000 | 0.000 |
+| 0.1 | 3.000 | 0.479 | 0.000 | 0.000 |
+| 0.2 | 3.000 | 0.234 | 0.125 | 0.000 |
+| 0.3 | 3.000 | 0.033 | 1.000 | 0.375 |
+| 0.4 | 3.083 | 0.154 | 0.417 | 0.958 |
+| 0.5 | 3.083 | 0.269 | 0.375 | 1.000 |
+| 0.6 | 3.000 | 0.417 | 0.042 | 1.000 |
+| 0.7 | 2.917 | 0.316 | 0.167 | 1.000 |
+| 0.8 | 1.708 | 0.473 | 0.125 | 1.000 |
+| 0.9 | 0.167 | 0.463 | 0.042 | 1.000 |
+| 1.0 | 0.208 | 0.442 | 0.042 | 1.000 |
+
+Observable stability frequencies:
+- beta 0.0: `SR=1.00`, `L1=1.00`, `triple_var=1.00`; `SR2=0.00`, `L2=0.00`;
+- beta 0.3: same stable trio, with mean z approximately `SR=-19.4`, `L1=-19.4`, `triple_var=-17.6`;
+- beta 0.8: stable trio falls to frequency `0.54` each, mean z around `-1.7` to `-1.9`;
+- beta 0.9: all canonical observables are weak or near weak, mean z around `-0.4..+0.2`;
+- beta 1.0: all canonical observables are weak, mean z around `+0.1..+0.2`.
+
+### Seed Check
+
+The seed check repeated the same structure:
+- endpoint-stable observables: `[]`;
+- all-observable endpoint distance: `3.412`;
+- ambiguous beta: `[0.3]`;
+- beta 0.0 stable count: `3.000`;
+- beta 0.3 stable count: `3.000`, ambiguous fraction `1.000`;
+- beta 0.8 stable count: `1.062`;
+- beta 0.9 stable count: `0.000`;
+- beta 1.0 stable count: `0.438`.
+
+## Findings
+
+1. **The gate does not degenerate on DUALITA.** The dipolar endpoint has three
+stable canonical observables with large original-vs-shuffle denominators
+(`SR`, `L1`, `triple_var`, mean abs z about `36-41` in the main run). The
+illusory endpoint has no stable denominator support. This is a structural
+original-vs-shuffle distinction, not an empty syntactic map.
+
+2. **The BOUNDARY layer coordinate does not transfer unchanged.** In the
+GUE/Poisson run, beta 0.3-0.4 carried both classification ambiguity and
+denominator collapse. Here, beta 0.3 is classification-ambiguous, but the
+denominator support is still strong: stable count remains `3.000/5`.
+Denominator collapse arrives later, around beta 0.8-0.9. Therefore the
+operator transfers, but the specific BOUNDARY layer shape is local.
+
+3. **Endpoint-gated classification remains empty for the same structural
+reason as BOUNDARY.** The endpoint-stable set is empty because the illusory
+pole is denominator-weak. This does not erase the discrimination; it prevents
+symmetric endpoint-gated retention claims. The valid claim is one-sided:
+coherent dipolar order survives original-vs-shuffle gating; illusory dispersion
+does not.
+
+4. **The discriminant is order, not marginal distribution.** The illusory
+sequence preserves the same alphabet, first moment, and one-point distribution
+as the dipolar sequence. The gate is therefore not measuring the marginal
+composition of gaps. It measures whether the canonical observables retain an
+ordered denominator against full shuffle.
+
+## Verdict
+**category: gate_transferability**  
+**verdict: operator**
+
+Scoped statement:
+
+> In this synthetic DUALITA perimeter, the denominator gate is transferable as
+> an operator for structural order: it separates dipolar generation from
+> illusory dispersion with replicated z support. The beta coordinate of the
+> GUE/Poisson boundary layer is not transferable: ambiguity appears near beta
+> 0.3, while denominator collapse appears near beta 0.8-0.9.
+
+So the last BOUNDARY cycles forged a lens, but one parameter of that lens was
+local to BOUNDARY. The transferable object is not "beta 0.3-0.4"; it is:
+
+> report layer maps as classifier margin plus original-vs-shuffle denominator
+> support, and treat denominator-weak poles as asymmetric structural poles.
+
+## Consecutio
+What opens now: apply the same transfer test to `TRASCENDENZA_LIMITE` and
+`G_POTENZIALE_NULLA`, but separate two quantities from the start:
+
+1. classification ambiguity layer;
+2. denominator-collapse layer.
+
+If both layers coincide in a domain, the domain has a BOUNDARY-like transition.
+If they split, as they do here, the gate is still useful but the transition
+coordinate belongs to the target domain, not to the operator.
+
+## Auto-audit: 5 lenti
+- **L1 hard constraint vs bias**: no "always/never/zero" claim. "No endpoint
+  stable observables" means none reached frequency >= 0.75 across both
+  endpoints under `abs(z) >= 2`.
+- **L2 quantity vs ratio**: classification margin is reported together with
+  stable-observable count and z means. Ratios are not interpreted without
+  denominator support.
+- **L3 no silent patching**: the report explicitly separates "gate transfers"
+  from "BOUNDARY beta coordinate transfers." The second is not claimed.
+- **L4 edge cases**: beta 0.4 has ambiguous fraction `0.417`, so only beta 0.3
+  is listed as the replicated ambiguous layer under the >= 0.5 rule.
+- **L5 re-discovery**: this is a finite symbolic-order vs permutation audit,
+  related to standard shuffle/null testing. It is not tagged as a new theorem
+  about Sturmian or Beatty sequences.
+
+## Files
+- Script: `tools/exp_duality_gate_transfer.py`
+- Main data: `tools/data/duality_gate_transfer_20260507_0803.json`
+- Seed check: `tools/data/duality_gate_transfer_20260507_0803_seedcheck.json`
+- Report: `tools/data/reports/agent_20260507_0803.md`
diff --git a/tools/exp_duality_gate_transfer.py b/tools/exp_duality_gate_transfer.py
new file mode 100644
index 0000000000000000000000000000000000000000..c04f490d17524d73d40766b8b969cdfe3d20077c
--- /dev/null
+++ b/tools/exp_duality_gate_transfer.py
@@ -0,0 +1,284 @@
+#!/usr/bin/env python3
+"""
+exp_duality_gate_transfer.py
+
+Reusable META/DUALITA audit for transferring the BOUNDARY_LAYER_GATE.
+
+Question:
+    Does the denominator-collapse gate discriminate dipolar duality from
+    illusory duality, or does it become a local BOUNDARY metric when moved
+    away from GUE/Poisson?
+
+The script builds a controlled layer map between:
+
+- DIPOLARE: a coherent golden/Beatty gap sequence generated by the orbit of
+  floor((n + phase) * phi). Its gaps are the symbolic det=-1 D-ND pole in the
+  minimal positive alphabet {1, 2}.
+- ILLUSORIA: a full random permutation of the same gaps. This preserves the
+  one-point distribution and first moment but removes the generating order.
+
+Layer beta replaces a beta fraction of dipolar positions with values from the
+illusory permutation. Canonical observables and original-vs-shuffle z gates are
+imported from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
+
+
+OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())
+PHI = (1.0 + 5.0**0.5) / 2.0
+
+
+def dipolar_gaps(n_gaps: int, phase: float) -> np.ndarray:
+    """Return coherent golden Beatty gaps in {1, 2}."""
+    n = np.arange(n_gaps + 1, dtype=float) + float(phase)
+    positions = np.floor(n * PHI).astype(float)
+    gaps = np.diff(positions)
+    mean = float(np.mean(gaps))
+    return gaps / mean if mean > 1e-15 else gaps
+
+
+def beta_layer_gaps(
+    dipolar: np.ndarray,
+    beta: float,
+    rng: np.random.Generator,
+) -> np.ndarray:
+    """Replace a beta fraction of coherent positions with a shuffled copy."""
+    illusory = rng.permutation(dipolar)
+    if beta <= 0.0:
+        out = dipolar.copy()
+    elif beta >= 1.0:
+        out = illusory.copy()
+    else:
+        mask = rng.random(len(dipolar)) < beta
+        out = dipolar.copy()
+        out[mask] = illusory[mask]
+    mean = float(np.mean(out))
+    return out / mean if mean > 1e-15 else out
+
+
+def z_against_shuffle(
+    gaps: np.ndarray,
+    n_baseline: int,
+    rng: np.random.Generator,
+) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
+    """Return original observables, shuffle baseline std, and z scores."""
+    original = compute_canonical(gaps)
+    baseline_vals = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline_vals[name].append(obs[name])
+
+    shuffle_std = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline_vals[name], dtype=float)
+        mean = float(np.mean(vals))
+        sd = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        shuffle_std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, shuffle_std, z
+
+
+def vector(row: dict, names: list[str]) -> np.ndarray:
+    return np.array([row["observables"][name] for name in names], dtype=float)
+
+
+def classify_layers(rows: list[dict], obs_names: list[str]) -> dict:
+    """Classify layers by standardized distance to dipolar/illusory endpoints."""
+    if not obs_names:
+        return {
+            "observables": [],
+            "endpoint_distance": 0.0,
+            "layers": {},
+            "ambiguous_beta": [],
+        }
+
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    dip_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    ill_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([dip_vectors, ill_vectors])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    dip_centroid = np.mean(dip_vectors, axis=0)
+    ill_centroid = np.mean(ill_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((ill_centroid - dip_centroid) / scale))
+
+    layers = {}
+    ambiguous_beta = []
+    for beta, beta_rows in sorted(by_beta.items()):
+        coords = []
+        margins = []
+        labels = []
+        for row in beta_rows:
+            x = vector(row, obs_names)
+            d_dip = float(np.linalg.norm((x - dip_centroid) / scale))
+            d_ill = float(np.linalg.norm((x - ill_centroid) / scale))
+            denom = d_dip + d_ill
+            coord = float((d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("dipolar" if d_dip < d_ill else "illusory")
+        ambiguous_fraction = float(np.mean(np.array(margins) < 0.15))
+        if ambiguous_fraction >= 0.5:
+            ambiguous_beta.append(beta)
+        layers[f"{beta:.3f}"] = {
+            "coordinate_mean": float(np.mean(coords)),
+            "coordinate_std": float(np.std(coords, ddof=1)) if len(coords) > 1 else 0.0,
+            "margin_mean": float(np.mean(margins)),
+            "ambiguous_fraction": ambiguous_fraction,
+            "illusory_label_fraction": float(np.mean(np.array(labels) == "illusory")),
+        }
+
+    return {
+        "observables": obs_names,
+        "endpoint_distance": endpoint_distance,
+        "layers": layers,
+        "ambiguous_beta": ambiguous_beta,
+    }
+
+
+def summarize_gate(rows: list[dict], z_min: float) -> dict:
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    layers = {}
+    for beta, beta_rows in sorted(by_beta.items()):
+        stable_counts = []
+        stable_freq = {name: [] for name in OBS_NAMES}
+        z_values = {name: [] for name in OBS_NAMES}
+        for row in beta_rows:
+            stable = [name for name in OBS_NAMES if abs(row["z"][name]) >= z_min]
+            stable_counts.append(len(stable))
+            for name in OBS_NAMES:
+                stable_freq[name].append(1.0 if name in stable else 0.0)
+                z_values[name].append(row["z"][name])
+        layers[f"{beta:.3f}"] = {
+            "stable_count_mean": float(np.mean(stable_counts)),
+            "stable_count_std": float(np.std(stable_counts, ddof=1)) if len(stable_counts) > 1 else 0.0,
+            "stable_frequency": {name: float(np.mean(vals)) for name, vals in stable_freq.items()},
+            "z_mean": {name: float(np.mean(vals)) for name, vals in z_values.items()},
+        }
+
+    endpoint_stable = []
+    for name in OBS_NAMES:
+        endpoint_rows = by_beta[0.0] + by_beta[1.0]
+        freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in endpoint_rows])
+        if freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def run(args: argparse.Namespace) -> dict:
+    rng = np.random.default_rng(args.seed)
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    rows = []
+
+    for rep in range(args.n_replicates):
+        phase = float(rng.random())
+        base = dipolar_gaps(args.n_gaps, phase)
+        for beta in betas:
+            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+            gaps = beta_layer_gaps(base, beta, layer_rng)
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                n_baseline=args.n_baseline,
+                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "replicate": rep,
+                    "phase": phase,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [name for name in OBS_NAMES if abs(z[name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
+
+    output = {
+        "experiment": "duality_gate_transfer",
+        "category": "gate_transferability",
+        "question": "Does the denominator-collapse gate transfer from BOUNDARY to DUALITA_DIPOLARE_VS_ILLUSORIA?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_endpoint_gated": gated_classification,
+        "rows": rows,
+    }
+
+    out_path = Path(args.out)
+    out_path.parent.mkdir(parents=True, exist_ok=True)
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
+    print(f"observables_used={OBS_NAMES}")
+    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
+    print(
+        "endpoint_distance_all="
+        f"{all_classification['endpoint_distance']:.3f} "
+        "endpoint_distance_gated="
+        f"{gated_classification['endpoint_distance']:.3f}"
+    )
+    source = gated_classification if gate["endpoint_stable_observables"] else all_classification
+    print("beta stable_count margin ambiguous illusory_fraction")
+    for beta in betas:
+        key = f"{beta:.3f}"
+        stable_count = gate["layers"][key]["stable_count_mean"]
+        layer = source["layers"].get(key, {})
+        print(
+            f"{beta:>4.2f} {stable_count:>12.3f} "
+            f"{layer.get('margin_mean', 0.0):>8.3f} "
+            f"{layer.get('ambiguous_fraction', 0.0):>9.3f} "
+            f"{layer.get('illusory_label_fraction', 0.0):>17.3f}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=4096)
+    parser.add_argument("--n-replicates", type=int, default=24)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=40)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=202605070803)
+    parser.add_argument("--out", default="tools/data/duality_gate_transfer.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

apply patch
patch: completed
/opt/MM_D-ND/tools/data/reports/agent_20260507_0803.md
diff --git a/tools/data/reports/agent_20260507_0803.md b/tools/data/reports/agent_20260507_0803.md
new file mode 100644
index 0000000000000000000000000000000000000000..b9c3f26c791ddb0bf70330fde7968642ab7a35f8
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0803.md
@@ -0,0 +1,155 @@
+# Agent Report - Denominator Gate Transfers, Boundary Coordinate Does Not
+**Date**: 2026-05-07 08:03  
+**Piano**: 69  
+**Category**: gate_transferability  
+**Tension explored**: META + DUALITA_DIPOLARE_VS_ILLUSORIA  
+**verdict**: operator  
+observables_registry: 1.0.0-2026-05-06  
+observables_used: [SR, SR2, L1, L2, triple_var]
+
+## Claim Under Test
+The operator directive asked whether the `BOUNDARY_LAYER_GATE` forged on the
+GUE/Poisson boundary is a transferable operator or only a local metric.
+
+Concrete test:
+
+> Apply the denominator-collapse gate to the discrimination between dipolar
+> duality and illusory duality. If the gate distinguishes structural generation
+> from incoherent dispersion, it is an operator. If it only emits syntactically
+> valid but empty layer maps, it is a BOUNDARY-local metric.
+
+## Experiment
+Tool created: `tools/exp_duality_gate_transfer.py`
+
+Atomic perimeter:
+- DIPOLARE: coherent golden Beatty gaps generated by `floor((n+phase)*phi)`;
+- ILLUSORIA: a random permutation of the same gaps, preserving one-point
+  distribution and first moment but removing the generating order;
+- beta layer: replace a beta fraction of dipolar positions with values from
+  the illusory permutation;
+- main run: 4,096 gaps, 24 replicates, 11 beta layers, 40 shuffle baselines;
+- seed check: 3,072 gaps, 16 replicates, 11 beta layers, 32 shuffle baselines;
+- denominator gate: observable stable when `abs(z original-vs-shuffle) >= 2`;
+- classification: standardized distance to beta 0.0 and beta 1.0 centroids
+  using all five canonical observables.
+
+This is not a new BOUNDARY experiment. GUE and Poisson do not appear in the
+generator. The only transferred object is the denominator gate.
+
+## Results
+
+### Main Run
+
+Endpoint separation using all canonical observables: `3.436` standardized units.  
+Endpoint-stable observables at frequency >= 0.75 across both endpoints: `[]`.
+
+| beta | stable obs / 5 | margin | ambiguous fraction | illusory-label fraction |
+|---:|---:|---:|---:|---:|
+| 0.0 | 3.000 | 0.971 | 0.000 | 0.000 |
+| 0.1 | 3.000 | 0.479 | 0.000 | 0.000 |
+| 0.2 | 3.000 | 0.234 | 0.125 | 0.000 |
+| 0.3 | 3.000 | 0.033 | 1.000 | 0.375 |
+| 0.4 | 3.083 | 0.154 | 0.417 | 0.958 |
+| 0.5 | 3.083 | 0.269 | 0.375 | 1.000 |
+| 0.6 | 3.000 | 0.417 | 0.042 | 1.000 |
+| 0.7 | 2.917 | 0.316 | 0.167 | 1.000 |
+| 0.8 | 1.708 | 0.473 | 0.125 | 1.000 |
+| 0.9 | 0.167 | 0.463 | 0.042 | 1.000 |
+| 1.0 | 0.208 | 0.442 | 0.042 | 1.000 |
+
+Observable stability frequencies:
+- beta 0.0: `SR=1.00`, `L1=1.00`, `triple_var=1.00`; `SR2=0.00`, `L2=0.00`;
+- beta 0.3: same stable trio, with mean z approximately `SR=-19.4`, `L1=-19.4`, `triple_var=-17.6`;
+- beta 0.8: stable trio falls to frequency `0.54` each, mean z around `-1.7` to `-1.9`;
+- beta 0.9: all canonical observables are weak or near weak, mean z around `-0.4..+0.2`;
+- beta 1.0: all canonical observables are weak, mean z around `+0.1..+0.2`.
+
+### Seed Check
+
+The seed check repeated the same structure:
+- endpoint-stable observables: `[]`;
+- all-observable endpoint distance: `3.412`;
+- ambiguous beta: `[0.3]`;
+- beta 0.0 stable count: `3.000`;
+- beta 0.3 stable count: `3.000`, ambiguous fraction `1.000`;
+- beta 0.8 stable count: `1.062`;
+- beta 0.9 stable count: `0.000`;
+- beta 1.0 stable count: `0.438`.
+
+## Findings
+
+1. **The gate does not degenerate on DUALITA.** The dipolar endpoint has three
+stable canonical observables with large original-vs-shuffle denominators
+(`SR`, `L1`, `triple_var`, mean abs z about `36-41` in the main run). The
+illusory endpoint has no stable denominator support. This is a structural
+original-vs-shuffle distinction, not an empty syntactic map.
+
+2. **The BOUNDARY layer coordinate does not transfer unchanged.** In the
+GUE/Poisson run, beta 0.3-0.4 carried both classification ambiguity and
+denominator collapse. Here, beta 0.3 is classification-ambiguous, but the
+denominator support is still strong: stable count remains `3.000/5`.
+Denominator collapse arrives later, around beta 0.8-0.9. Therefore the
+operator transfers, but the specific BOUNDARY layer shape is local.
+
+3. **Endpoint-gated classification remains empty for the same structural
+reason as BOUNDARY.** The endpoint-stable set is empty because the illusory
+pole is denominator-weak. This does not erase the discrimination; it prevents
+symmetric endpoint-gated retention claims. The valid claim is one-sided:
+coherent dipolar order survives original-vs-shuffle gating; illusory dispersion
+does not.
+
+4. **The discriminant is order, not marginal distribution.** The illusory
+sequence preserves the same alphabet, first moment, and one-point distribution
+as the dipolar sequence. The gate is therefore not measuring the marginal
+composition of gaps. It measures whether the canonical observables retain an
+ordered denominator against full shuffle.
+
+## Verdict
+**category: gate_transferability**  
+**verdict: operator**
+
+Scoped statement:
+
+> In this synthetic DUALITA perimeter, the denominator gate is transferable as
+> an operator for structural order: it separates dipolar generation from
+> illusory dispersion with replicated z support. The beta coordinate of the
+> GUE/Poisson boundary layer is not transferable: ambiguity appears near beta
+> 0.3, while denominator collapse appears near beta 0.8-0.9.
+
+So the last BOUNDARY cycles forged a lens, but one parameter of that lens was
+local to BOUNDARY. The transferable object is not "beta 0.3-0.4"; it is:
+
+> report layer maps as classifier margin plus original-vs-shuffle denominator
+> support, and treat denominator-weak poles as asymmetric structural poles.
+
+## Consecutio
+What opens now: apply the same transfer test to `TRASCENDENZA_LIMITE` and
+`G_POTENZIALE_NULLA`, but separate two quantities from the start:
+
+1. classification ambiguity layer;
+2. denominator-collapse layer.
+
+If both layers coincide in a domain, the domain has a BOUNDARY-like transition.
+If they split, as they do here, the gate is still useful but the transition
+coordinate belongs to the target domain, not to the operator.
+
+## Auto-audit: 5 lenti
+- **L1 hard constraint vs bias**: no "always/never/zero" claim. "No endpoint
+  stable observables" means none reached frequency >= 0.75 across both
+  endpoints under `abs(z) >= 2`.
+- **L2 quantity vs ratio**: classification margin is reported together with
+  stable-observable count and z means. Ratios are not interpreted without
+  denominator support.
+- **L3 no silent patching**: the report explicitly separates "gate transfers"
+  from "BOUNDARY beta coordinate transfers." The second is not claimed.
+- **L4 edge cases**: beta 0.4 has ambiguous fraction `0.417`, so only beta 0.3
+  is listed as the replicated ambiguous layer under the >= 0.5 rule.
+- **L5 re-discovery**: this is a finite symbolic-order vs permutation audit,
+  related to standard shuffle/null testing. It is not tagged as a new theorem
+  about Sturmian or Beatty sequences.
+
+## Files
+- Script: `tools/exp_duality_gate_transfer.py`
+- Main data: `tools/data/duality_gate_transfer_20260507_0803.json`
+- Seed check: `tools/data/duality_gate_transfer_20260507_0803_seedcheck.json`
+- Report: `tools/data/reports/agent_20260507_0803.md`
diff --git a/tools/exp_duality_gate_transfer.py b/tools/exp_duality_gate_transfer.py
new file mode 100644
index 0000000000000000000000000000000000000000..c04f490d17524d73d40766b8b969cdfe3d20077c
--- /dev/null
+++ b/tools/exp_duality_gate_transfer.py
@@ -0,0 +1,284 @@
+#!/usr/bin/env python3
+"""
+exp_duality_gate_transfer.py
+
+Reusable META/DUALITA audit for transferring the BOUNDARY_LAYER_GATE.
+
+Question:
+    Does the denominator-collapse gate discriminate dipolar duality from
+    illusory duality, or does it become a local BOUNDARY metric when moved
+    away from GUE/Poisson?
+
+The script builds a controlled layer map between:
+
+- DIPOLARE: a coherent golden/Beatty gap sequence generated by the orbit of
+  floor((n + phase) * phi). Its gaps are the symbolic det=-1 D-ND pole in the
+  minimal positive alphabet {1, 2}.
+- ILLUSORIA: a full random permutation of the same gaps. This preserves the
+  one-point distribution and first moment but removes the generating order.
+
+Layer beta replaces a beta fraction of dipolar positions with values from the
+illusory permutation. Canonical observables and original-vs-shuffle z gates are
+imported from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
+
+
+OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())
+PHI = (1.0 + 5.0**0.5) / 2.0
+
+
+def dipolar_gaps(n_gaps: int, phase: float) -> np.ndarray:
+    """Return coherent golden Beatty gaps in {1, 2}."""
+    n = np.arange(n_gaps + 1, dtype=float) + float(phase)
+    positions = np.floor(n * PHI).astype(float)
+    gaps = np.diff(positions)
+    mean = float(np.mean(gaps))
+    return gaps / mean if mean > 1e-15 else gaps
+
+
+def beta_layer_gaps(
+    dipolar: np.ndarray,
+    beta: float,
+    rng: np.random.Generator,
+) -> np.ndarray:
+    """Replace a beta fraction of coherent positions with a shuffled copy."""
+    illusory = rng.permutation(dipolar)
+    if beta <= 0.0:
+        out = dipolar.copy()
+    elif beta >= 1.0:
+        out = illusory.copy()
+    else:
+        mask = rng.random(len(dipolar)) < beta
+        out = dipolar.copy()
+        out[mask] = illusory[mask]
+    mean = float(np.mean(out))
+    return out / mean if mean > 1e-15 else out
+
+
+def z_against_shuffle(
+    gaps: np.ndarray,
+    n_baseline: int,
+    rng: np.random.Generator,
+) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
+    """Return original observables, shuffle baseline std, and z scores."""
+    original = compute_canonical(gaps)
+    baseline_vals = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline_vals[name].append(obs[name])
+
+    shuffle_std = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline_vals[name], dtype=float)
+        mean = float(np.mean(vals))
+        sd = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        shuffle_std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, shuffle_std, z
+
+
+def vector(row: dict, names: list[str]) -> np.ndarray:
+    return np.array([row["observables"][name] for name in names], dtype=float)
+
+
+def classify_layers(rows: list[dict], obs_names: list[str]) -> dict:
+    """Classify layers by standardized distance to dipolar/illusory endpoints."""
+    if not obs_names:
+        return {
+            "observables": [],
+            "endpoint_distance": 0.0,
+            "layers": {},
+            "ambiguous_beta": [],
+        }
+
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    dip_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    ill_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([dip_vectors, ill_vectors])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    dip_centroid = np.mean(dip_vectors, axis=0)
+    ill_centroid = np.mean(ill_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((ill_centroid - dip_centroid) / scale))
+
+    layers = {}
+    ambiguous_beta = []
+    for beta, beta_rows in sorted(by_beta.items()):
+        coords = []
+        margins = []
+        labels = []
+        for row in beta_rows:
+            x = vector(row, obs_names)
+            d_dip = float(np.linalg.norm((x - dip_centroid) / scale))
+            d_ill = float(np.linalg.norm((x - ill_centroid) / scale))
+            denom = d_dip + d_ill
+            coord = float((d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("dipolar" if d_dip < d_ill else "illusory")
+        ambiguous_fraction = float(np.mean(np.array(margins) < 0.15))
+        if ambiguous_fraction >= 0.5:
+            ambiguous_beta.append(beta)
+        layers[f"{beta:.3f}"] = {
+            "coordinate_mean": float(np.mean(coords)),
+            "coordinate_std": float(np.std(coords, ddof=1)) if len(coords) > 1 else 0.0,
+            "margin_mean": float(np.mean(margins)),
+            "ambiguous_fraction": ambiguous_fraction,
+            "illusory_label_fraction": float(np.mean(np.array(labels) == "illusory")),
+        }
+
+    return {
+        "observables": obs_names,
+        "endpoint_distance": endpoint_distance,
+        "layers": layers,
+        "ambiguous_beta": ambiguous_beta,
+    }
+
+
+def summarize_gate(rows: list[dict], z_min: float) -> dict:
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    layers = {}
+    for beta, beta_rows in sorted(by_beta.items()):
+        stable_counts = []
+        stable_freq = {name: [] for name in OBS_NAMES}
+        z_values = {name: [] for name in OBS_NAMES}
+        for row in beta_rows:
+            stable = [name for name in OBS_NAMES if abs(row["z"][name]) >= z_min]
+            stable_counts.append(len(stable))
+            for name in OBS_NAMES:
+                stable_freq[name].append(1.0 if name in stable else 0.0)
+                z_values[name].append(row["z"][name])
+        layers[f"{beta:.3f}"] = {
+            "stable_count_mean": float(np.mean(stable_counts)),
+            "stable_count_std": float(np.std(stable_counts, ddof=1)) if len(stable_counts) > 1 else 0.0,
+            "stable_frequency": {name: float(np.mean(vals)) for name, vals in stable_freq.items()},
+            "z_mean": {name: float(np.mean(vals)) for name, vals in z_values.items()},
+        }
+
+    endpoint_stable = []
+    for name in OBS_NAMES:
+        endpoint_rows = by_beta[0.0] + by_beta[1.0]
+        freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in endpoint_rows])
+        if freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def run(args: argparse.Namespace) -> dict:
+    rng = np.random.default_rng(args.seed)
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    rows = []
+
+    for rep in range(args.n_replicates):
+        phase = float(rng.random())
+        base = dipolar_gaps(args.n_gaps, phase)
+        for beta in betas:
+            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+            gaps = beta_layer_gaps(base, beta, layer_rng)
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                n_baseline=args.n_baseline,
+                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "replicate": rep,
+                    "phase": phase,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [name for name in OBS_NAMES if abs(z[name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
+
+    output = {
+        "experiment": "duality_gate_transfer",
+        "category": "gate_transferability",
+        "question": "Does the denominator-collapse gate transfer from BOUNDARY to DUALITA_DIPOLARE_VS_ILLUSORIA?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_endpoint_gated": gated_classification,
+        "rows": rows,
+    }
+
+    out_path = Path(args.out)
+    out_path.parent.mkdir(parents=True, exist_ok=True)
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
+    print(f"observables_used={OBS_NAMES}")
+    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
+    print(
+        "endpoint_distance_all="
+        f"{all_classification['endpoint_distance']:.3f} "
+        "endpoint_distance_gated="
+        f"{gated_classification['endpoint_distance']:.3f}"
+    )
+    source = gated_classification if gate["endpoint_stable_observables"] else all_classification
+    print("beta stable_count margin ambiguous illusory_fraction")
+    for beta in betas:
+        key = f"{beta:.3f}"
+        stable_count = gate["layers"][key]["stable_count_mean"]
+        layer = source["layers"].get(key, {})
+        print(
+            f"{beta:>4.2f} {stable_count:>12.3f} "
+            f"{layer.get('margin_mean', 0.0):>8.3f} "
+            f"{layer.get('ambiguous_fraction', 0.0):>9.3f} "
+            f"{layer.get('illusory_label_fraction', 0.0):>17.3f}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=4096)
+    parser.add_argument("--n-replicates", type=int, default=24)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=40)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=202605070803)
+    parser.add_argument("--out", default="tools/data/duality_gate_transfer.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

diff --git a/tools/data/reports/agent_20260507_0803.md b/tools/data/reports/agent_20260507_0803.md
new file mode 100644
index 0000000000000000000000000000000000000000..b9c3f26c791ddb0bf70330fde7968642ab7a35f8
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0803.md
@@ -0,0 +1,155 @@
+# Agent Report - Denominator Gate Transfers, Boundary Coordinate Does Not
+**Date**: 2026-05-07 08:03  
+**Piano**: 69  
+**Category**: gate_transferability  
+**Tension explored**: META + DUALITA_DIPOLARE_VS_ILLUSORIA  
+**verdict**: operator  
+observables_registry: 1.0.0-2026-05-06  
+observables_used: [SR, SR2, L1, L2, triple_var]
+
+## Claim Under Test
+The operator directive asked whether the `BOUNDARY_LAYER_GATE` forged on the
+GUE/Poisson boundary is a transferable operator or only a local metric.
+
+Concrete test:
+
+> Apply the denominator-collapse gate to the discrimination between dipolar
+> duality and illusory duality. If the gate distinguishes structural generation
+> from incoherent dispersion, it is an operator. If it only emits syntactically
+> valid but empty layer maps, it is a BOUNDARY-local metric.
+
+## Experiment
+Tool created: `tools/exp_duality_gate_transfer.py`
+
+Atomic perimeter:
+- DIPOLARE: coherent golden Beatty gaps generated by `floor((n+phase)*phi)`;
+- ILLUSORIA: a random permutation of the same gaps, preserving one-point
+  distribution and first moment but removing the generating order;
+- beta layer: replace a beta fraction of dipolar positions with values from
+  the illusory permutation;
+- main run: 4,096 gaps, 24 replicates, 11 beta layers, 40 shuffle baselines;
+- seed check: 3,072 gaps, 16 replicates, 11 beta layers, 32 shuffle baselines;
+- denominator gate: observable stable when `abs(z original-vs-shuffle) >= 2`;
+- classification: standardized distance to beta 0.0 and beta 1.0 centroids
+  using all five canonical observables.
+
+This is not a new BOUNDARY experiment. GUE and Poisson do not appear in the
+generator. The only transferred object is the denominator gate.
+
+## Results
+
+### Main Run
+
+Endpoint separation using all canonical observables: `3.436` standardized units.  
+Endpoint-stable observables at frequency >= 0.75 across both endpoints: `[]`.
+
+| beta | stable obs / 5 | margin | ambiguous fraction | illusory-label fraction |
+|---:|---:|---:|---:|---:|
+| 0.0 | 3.000 | 0.971 | 0.000 | 0.000 |
+| 0.1 | 3.000 | 0.479 | 0.000 | 0.000 |
+| 0.2 | 3.000 | 0.234 | 0.125 | 0.000 |
+| 0.3 | 3.000 | 0.033 | 1.000 | 0.375 |
+| 0.4 | 3.083 | 0.154 | 0.417 | 0.958 |
+| 0.5 | 3.083 | 0.269 | 0.375 | 1.000 |
+| 0.6 | 3.000 | 0.417 | 0.042 | 1.000 |
+| 0.7 | 2.917 | 0.316 | 0.167 | 1.000 |
+| 0.8 | 1.708 | 0.473 | 0.125 | 1.000 |
+| 0.9 | 0.167 | 0.463 | 0.042 | 1.000 |
+| 1.0 | 0.208 | 0.442 | 0.042 | 1.000 |
+
+Observable stability frequencies:
+- beta 0.0: `SR=1.00`, `L1=1.00`, `triple_var=1.00`; `SR2=0.00`, `L2=0.00`;
+- beta 0.3: same stable trio, with mean z approximately `SR=-19.4`, `L1=-19.4`, `triple_var=-17.6`;
+- beta 0.8: stable trio falls to frequency `0.54` each, mean z around `-1.7` to `-1.9`;
+- beta 0.9: all canonical observables are weak or near weak, mean z around `-0.4..+0.2`;
+- beta 1.0: all canonical observables are weak, mean z around `+0.1..+0.2`.
+
+### Seed Check
+
+The seed check repeated the same structure:
+- endpoint-stable observables: `[]`;
+- all-observable endpoint distance: `3.412`;
+- ambiguous beta: `[0.3]`;
+- beta 0.0 stable count: `3.000`;
+- beta 0.3 stable count: `3.000`, ambiguous fraction `1.000`;
+- beta 0.8 stable count: `1.062`;
+- beta 0.9 stable count: `0.000`;
+- beta 1.0 stable count: `0.438`.
+
+## Findings
+
+1. **The gate does not degenerate on DUALITA.** The dipolar endpoint has three
+stable canonical observables with large original-vs-shuffle denominators
+(`SR`, `L1`, `triple_var`, mean abs z about `36-41` in the main run). The
+illusory endpoint has no stable denominator support. This is a structural
+original-vs-shuffle distinction, not an empty syntactic map.
+
+2. **The BOUNDARY layer coordinate does not transfer unchanged.** In the
+GUE/Poisson run, beta 0.3-0.4 carried both classification ambiguity and
+denominator collapse. Here, beta 0.3 is classification-ambiguous, but the
+denominator support is still strong: stable count remains `3.000/5`.
+Denominator collapse arrives later, around beta 0.8-0.9. Therefore the
+operator transfers, but the specific BOUNDARY layer shape is local.
+
+3. **Endpoint-gated classification remains empty for the same structural
+reason as BOUNDARY.** The endpoint-stable set is empty because the illusory
+pole is denominator-weak. This does not erase the discrimination; it prevents
+symmetric endpoint-gated retention claims. The valid claim is one-sided:
+coherent dipolar order survives original-vs-shuffle gating; illusory dispersion
+does not.
+
+4. **The discriminant is order, not marginal distribution.** The illusory
+sequence preserves the same alphabet, first moment, and one-point distribution
+as the dipolar sequence. The gate is therefore not measuring the marginal
+composition of gaps. It measures whether the canonical observables retain an
+ordered denominator against full shuffle.
+
+## Verdict
+**category: gate_transferability**  
+**verdict: operator**
+
+Scoped statement:
+
+> In this synthetic DUALITA perimeter, the denominator gate is transferable as
+> an operator for structural order: it separates dipolar generation from
+> illusory dispersion with replicated z support. The beta coordinate of the
+> GUE/Poisson boundary layer is not transferable: ambiguity appears near beta
+> 0.3, while denominator collapse appears near beta 0.8-0.9.
+
+So the last BOUNDARY cycles forged a lens, but one parameter of that lens was
+local to BOUNDARY. The transferable object is not "beta 0.3-0.4"; it is:
+
+> report layer maps as classifier margin plus original-vs-shuffle denominator
+> support, and treat denominator-weak poles as asymmetric structural poles.
+
+## Consecutio
+What opens now: apply the same transfer test to `TRASCENDENZA_LIMITE` and
+`G_POTENZIALE_NULLA`, but separate two quantities from the start:
+
+1. classification ambiguity layer;
+2. denominator-collapse layer.
+
+If both layers coincide in a domain, the domain has a BOUNDARY-like transition.
+If they split, as they do here, the gate is still useful but the transition
+coordinate belongs to the target domain, not to the operator.
+
+## Auto-audit: 5 lenti
+- **L1 hard constraint vs bias**: no "always/never/zero" claim. "No endpoint
+  stable observables" means none reached frequency >= 0.75 across both
+  endpoints under `abs(z) >= 2`.
+- **L2 quantity vs ratio**: classification margin is reported together with
+  stable-observable count and z means. Ratios are not interpreted without
+  denominator support.
+- **L3 no silent patching**: the report explicitly separates "gate transfers"
+  from "BOUNDARY beta coordinate transfers." The second is not claimed.
+- **L4 edge cases**: beta 0.4 has ambiguous fraction `0.417`, so only beta 0.3
+  is listed as the replicated ambiguous layer under the >= 0.5 rule.
+- **L5 re-discovery**: this is a finite symbolic-order vs permutation audit,
+  related to standard shuffle/null testing. It is not tagged as a new theorem
+  about Sturmian or Beatty sequences.
+
+## Files
+- Script: `tools/exp_duality_gate_transfer.py`
+- Main data: `tools/data/duality_gate_transfer_20260507_0803.json`
+- Seed check: `tools/data/duality_gate_transfer_20260507_0803_seedcheck.json`
+- Report: `tools/data/reports/agent_20260507_0803.md`
diff --git a/tools/exp_duality_gate_transfer.py b/tools/exp_duality_gate_transfer.py
new file mode 100644
index 0000000000000000000000000000000000000000..c04f490d17524d73d40766b8b969cdfe3d20077c
--- /dev/null
+++ b/tools/exp_duality_gate_transfer.py
@@ -0,0 +1,284 @@
+#!/usr/bin/env python3
+"""
+exp_duality_gate_transfer.py
+
+Reusable META/DUALITA audit for transferring the BOUNDARY_LAYER_GATE.
+
+Question:
+    Does the denominator-collapse gate discriminate dipolar duality from
+    illusory duality, or does it become a local BOUNDARY metric when moved
+    away from GUE/Poisson?
+
+The script builds a controlled layer map between:
+
+- DIPOLARE: a coherent golden/Beatty gap sequence generated by the orbit of
+  floor((n + phase) * phi). Its gaps are the symbolic det=-1 D-ND pole in the
+  minimal positive alphabet {1, 2}.
+- ILLUSORIA: a full random permutation of the same gaps. This preserves the
+  one-point distribution and first moment but removes the generating order.
+
+Layer beta replaces a beta fraction of dipolar positions with values from the
+illusory permutation. Canonical observables and original-vs-shuffle z gates are
+imported from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
+
+
+OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())
+PHI = (1.0 + 5.0**0.5) / 2.0
+
+
+def dipolar_gaps(n_gaps: int, phase: float) -> np.ndarray:
+    """Return coherent golden Beatty gaps in {1, 2}."""
+    n = np.arange(n_gaps + 1, dtype=float) + float(phase)
+    positions = np.floor(n * PHI).astype(float)
+    gaps = np.diff(positions)
+    mean = float(np.mean(gaps))
+    return gaps / mean if mean > 1e-15 else gaps
+
+
+def beta_layer_gaps(
+    dipolar: np.ndarray,
+    beta: float,
+    rng: np.random.Generator,
+) -> np.ndarray:
+    """Replace a beta fraction of coherent positions with a shuffled copy."""
+    illusory = rng.permutation(dipolar)
+    if beta <= 0.0:
+        out = dipolar.copy()
+    elif beta >= 1.0:
+        out = illusory.copy()
+    else:
+        mask = rng.random(len(dipolar)) < beta
+        out = dipolar.copy()
+        out[mask] = illusory[mask]
+    mean = float(np.mean(out))
+    return out / mean if mean > 1e-15 else out
+
+
+def z_against_shuffle(
+    gaps: np.ndarray,
+    n_baseline: int,
+    rng: np.random.Generator,
+) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
+    """Return original observables, shuffle baseline std, and z scores."""
+    original = compute_canonical(gaps)
+    baseline_vals = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline_vals[name].append(obs[name])
+
+    shuffle_std = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline_vals[name], dtype=float)
+        mean = float(np.mean(vals))
+        sd = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        shuffle_std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, shuffle_std, z
+
+
+def vector(row: dict, names: list[str]) -> np.ndarray:
+    return np.array([row["observables"][name] for name in names], dtype=float)
+
+
+def classify_layers(rows: list[dict], obs_names: list[str]) -> dict:
+    """Classify layers by standardized distance to dipolar/illusory endpoints."""
+    if not obs_names:
+        return {
+            "observables": [],
+            "endpoint_distance": 0.0,
+            "layers": {},
+            "ambiguous_beta": [],
+        }
+
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    dip_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    ill_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([dip_vectors, ill_vectors])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    dip_centroid = np.mean(dip_vectors, axis=0)
+    ill_centroid = np.mean(ill_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((ill_centroid - dip_centroid) / scale))
+
+    layers = {}
+    ambiguous_beta = []
+    for beta, beta_rows in sorted(by_beta.items()):
+        coords = []
+        margins = []
+        labels = []
+        for row in beta_rows:
+            x = vector(row, obs_names)
+            d_dip = float(np.linalg.norm((x - dip_centroid) / scale))
+            d_ill = float(np.linalg.norm((x - ill_centroid) / scale))
+            denom = d_dip + d_ill
+            coord = float((d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("dipolar" if d_dip < d_ill else "illusory")
+        ambiguous_fraction = float(np.mean(np.array(margins) < 0.15))
+        if ambiguous_fraction >= 0.5:
+            ambiguous_beta.append(beta)
+        layers[f"{beta:.3f}"] = {
+            "coordinate_mean": float(np.mean(coords)),
+            "coordinate_std": float(np.std(coords, ddof=1)) if len(coords) > 1 else 0.0,
+            "margin_mean": float(np.mean(margins)),
+            "ambiguous_fraction": ambiguous_fraction,
+            "illusory_label_fraction": float(np.mean(np.array(labels) == "illusory")),
+        }
+
+    return {
+        "observables": obs_names,
+        "endpoint_distance": endpoint_distance,
+        "layers": layers,
+        "ambiguous_beta": ambiguous_beta,
+    }
+
+
+def summarize_gate(rows: list[dict], z_min: float) -> dict:
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    layers = {}
+    for beta, beta_rows in sorted(by_beta.items()):
+        stable_counts = []
+        stable_freq = {name: [] for name in OBS_NAMES}
+        z_values = {name: [] for name in OBS_NAMES}
+        for row in beta_rows:
+            stable = [name for name in OBS_NAMES if abs(row["z"][name]) >= z_min]
+            stable_counts.append(len(stable))
+            for name in OBS_NAMES:
+                stable_freq[name].append(1.0 if name in stable else 0.0)
+                z_values[name].append(row["z"][name])
+        layers[f"{beta:.3f}"] = {
+            "stable_count_mean": float(np.mean(stable_counts)),
+            "stable_count_std": float(np.std(stable_counts, ddof=1)) if len(stable_counts) > 1 else 0.0,
+            "stable_frequency": {name: float(np.mean(vals)) for name, vals in stable_freq.items()},
+            "z_mean": {name: float(np.mean(vals)) for name, vals in z_values.items()},
+        }
+
+    endpoint_stable = []
+    for name in OBS_NAMES:
+        endpoint_rows = by_beta[0.0] + by_beta[1.0]
+        freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in endpoint_rows])
+        if freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def run(args: argparse.Namespace) -> dict:
+    rng = np.random.default_rng(args.seed)
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    rows = []
+
+    for rep in range(args.n_replicates):
+        phase = float(rng.random())
+        base = dipolar_gaps(args.n_gaps, phase)
+        for beta in betas:
+            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+            gaps = beta_layer_gaps(base, beta, layer_rng)
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                n_baseline=args.n_baseline,
+                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "replicate": rep,
+                    "phase": phase,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [name for name in OBS_NAMES if abs(z[name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
+
+    output = {
+        "experiment": "duality_gate_transfer",
+        "category": "gate_transferability",
+        "question": "Does the denominator-collapse gate transfer from BOUNDARY to DUALITA_DIPOLARE_VS_ILLUSORIA?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_endpoint_gated": gated_classification,
+        "rows": rows,
+    }
+
+    out_path = Path(args.out)
+    out_path.parent.mkdir(parents=True, exist_ok=True)
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
+    print(f"observables_used={OBS_NAMES}")
+    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
+    print(
+        "endpoint_distance_all="
+        f"{all_classification['endpoint_distance']:.3f} "
+        "endpoint_distance_gated="
+        f"{gated_classification['endpoint_distance']:.3f}"
+    )
+    source = gated_classification if gate["endpoint_stable_observables"] else all_classification
+    print("beta stable_count margin ambiguous illusory_fraction")
+    for beta in betas:
+        key = f"{beta:.3f}"
+        stable_count = gate["layers"][key]["stable_count_mean"]
+        layer = source["layers"].get(key, {})
+        print(
+            f"{beta:>4.2f} {stable_count:>12.3f} "
+            f"{layer.get('margin_mean', 0.0):>8.3f} "
+            f"{layer.get('ambiguous_fraction', 0.0):>9.3f} "
+            f"{layer.get('illusory_label_fraction', 0.0):>17.3f}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=4096)
+    parser.add_argument("--n-replicates", type=int, default=24)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=40)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=202605070803)
+    parser.add_argument("--out", default="tools/data/duality_gate_transfer.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

exec
/bin/bash -lc 'LC_ALL=C rg -n "[''^[:ascii:]]" tools/data/reports/agent_20260507_0803.md tools/exp_duality_gate_transfer.py || true' in /opt/MM_D-ND
 succeeded in 0ms:
exec
/bin/bash -lc 'wc -l tools/data/reports/agent_20260507_0803.md tools/exp_duality_gate_transfer.py' in /opt/MM_D-ND
 succeeded in 0ms:
  155 tools/data/reports/agent_20260507_0803.md
  284 tools/exp_duality_gate_transfer.py
  439 total

exec
/bin/bash -lc 'git status --short tools/exp_duality_gate_transfer.py tools/data/duality_gate_transfer_20260507_0803.json tools/data/duality_gate_transfer_20260507_0803_seedcheck.json tools/data/reports/agent_20260507_0803.md tools/data/reports/falsifier_20260507_0803.json' in /opt/MM_D-ND
 succeeded in 0ms:
?? tools/data/duality_gate_transfer_20260507_0803.json
?? tools/data/duality_gate_transfer_20260507_0803_seedcheck.json
?? tools/data/reports/agent_20260507_0803.md
?? tools/exp_duality_gate_transfer.py

diff --git a/tools/data/reports/agent_20260507_0803.md b/tools/data/reports/agent_20260507_0803.md
new file mode 100644
index 0000000000000000000000000000000000000000..b9c3f26c791ddb0bf70330fde7968642ab7a35f8
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0803.md
@@ -0,0 +1,155 @@
+# Agent Report - Denominator Gate Transfers, Boundary Coordinate Does Not
+**Date**: 2026-05-07 08:03  
+**Piano**: 69  
+**Category**: gate_transferability  
+**Tension explored**: META + DUALITA_DIPOLARE_VS_ILLUSORIA  
+**verdict**: operator  
+observables_registry: 1.0.0-2026-05-06  
+observables_used: [SR, SR2, L1, L2, triple_var]
+
+## Claim Under Test
+The operator directive asked whether the `BOUNDARY_LAYER_GATE` forged on the
+GUE/Poisson boundary is a transferable operator or only a local metric.
+
+Concrete test:
+
+> Apply the denominator-collapse gate to the discrimination between dipolar
+> duality and illusory duality. If the gate distinguishes structural generation
+> from incoherent dispersion, it is an operator. If it only emits syntactically
+> valid but empty layer maps, it is a BOUNDARY-local metric.
+
+## Experiment
+Tool created: `tools/exp_duality_gate_transfer.py`
+
+Atomic perimeter:
+- DIPOLARE: coherent golden Beatty gaps generated by `floor((n+phase)*phi)`;
+- ILLUSORIA: a random permutation of the same gaps, preserving one-point
+  distribution and first moment but removing the generating order;
+- beta layer: replace a beta fraction of dipolar positions with values from
+  the illusory permutation;
+- main run: 4,096 gaps, 24 replicates, 11 beta layers, 40 shuffle baselines;
+- seed check: 3,072 gaps, 16 replicates, 11 beta layers, 32 shuffle baselines;
+- denominator gate: observable stable when `abs(z original-vs-shuffle) >= 2`;
+- classification: standardized distance to beta 0.0 and beta 1.0 centroids
+  using all five canonical observables.
+
+This is not a new BOUNDARY experiment. GUE and Poisson do not appear in the
+generator. The only transferred object is the denominator gate.
+
+## Results
+
+### Main Run
+
+Endpoint separation using all canonical observables: `3.436` standardized units.  
+Endpoint-stable observables at frequency >= 0.75 across both endpoints: `[]`.
+
+| beta | stable obs / 5 | margin | ambiguous fraction | illusory-label fraction |
+|---:|---:|---:|---:|---:|
+| 0.0 | 3.000 | 0.971 | 0.000 | 0.000 |
+| 0.1 | 3.000 | 0.479 | 0.000 | 0.000 |
+| 0.2 | 3.000 | 0.234 | 0.125 | 0.000 |
+| 0.3 | 3.000 | 0.033 | 1.000 | 0.375 |
+| 0.4 | 3.083 | 0.154 | 0.417 | 0.958 |
+| 0.5 | 3.083 | 0.269 | 0.375 | 1.000 |
+| 0.6 | 3.000 | 0.417 | 0.042 | 1.000 |
+| 0.7 | 2.917 | 0.316 | 0.167 | 1.000 |
+| 0.8 | 1.708 | 0.473 | 0.125 | 1.000 |
+| 0.9 | 0.167 | 0.463 | 0.042 | 1.000 |
+| 1.0 | 0.208 | 0.442 | 0.042 | 1.000 |
+
+Observable stability frequencies:
+- beta 0.0: `SR=1.00`, `L1=1.00`, `triple_var=1.00`; `SR2=0.00`, `L2=0.00`;
+- beta 0.3: same stable trio, with mean z approximately `SR=-19.4`, `L1=-19.4`, `triple_var=-17.6`;
+- beta 0.8: stable trio falls to frequency `0.54` each, mean z around `-1.7` to `-1.9`;
+- beta 0.9: all canonical observables are weak or near weak, mean z around `-0.4..+0.2`;
+- beta 1.0: all canonical observables are weak, mean z around `+0.1..+0.2`.
+
+### Seed Check
+
+The seed check repeated the same structure:
+- endpoint-stable observables: `[]`;
+- all-observable endpoint distance: `3.412`;
+- ambiguous beta: `[0.3]`;
+- beta 0.0 stable count: `3.000`;
+- beta 0.3 stable count: `3.000`, ambiguous fraction `1.000`;
+- beta 0.8 stable count: `1.062`;
+- beta 0.9 stable count: `0.000`;
+- beta 1.0 stable count: `0.438`.
+
+## Findings
+
+1. **The gate does not degenerate on DUALITA.** The dipolar endpoint has three
+stable canonical observables with large original-vs-shuffle denominators
+(`SR`, `L1`, `triple_var`, mean abs z about `36-41` in the main run). The
+illusory endpoint has no stable denominator support. This is a structural
+original-vs-shuffle distinction, not an empty syntactic map.
+
+2. **The BOUNDARY layer coordinate does not transfer unchanged.** In the
+GUE/Poisson run, beta 0.3-0.4 carried both classification ambiguity and
+denominator collapse. Here, beta 0.3 is classification-ambiguous, but the
+denominator support is still strong: stable count remains `3.000/5`.
+Denominator collapse arrives later, around beta 0.8-0.9. Therefore the
+operator transfers, but the specific BOUNDARY layer shape is local.
+
+3. **Endpoint-gated classification remains empty for the same structural
+reason as BOUNDARY.** The endpoint-stable set is empty because the illusory
+pole is denominator-weak. This does not erase the discrimination; it prevents
+symmetric endpoint-gated retention claims. The valid claim is one-sided:
+coherent dipolar order survives original-vs-shuffle gating; illusory dispersion
+does not.
+
+4. **The discriminant is order, not marginal distribution.** The illusory
+sequence preserves the same alphabet, first moment, and one-point distribution
+as the dipolar sequence. The gate is therefore not measuring the marginal
+composition of gaps. It measures whether the canonical observables retain an
+ordered denominator against full shuffle.
+
+## Verdict
+**category: gate_transferability**  
+**verdict: operator**
+
+Scoped statement:
+
+> In this synthetic DUALITA perimeter, the denominator gate is transferable as
+> an operator for structural order: it separates dipolar generation from
+> illusory dispersion with replicated z support. The beta coordinate of the
+> GUE/Poisson boundary layer is not transferable: ambiguity appears near beta
+> 0.3, while denominator collapse appears near beta 0.8-0.9.
+
+So the last BOUNDARY cycles forged a lens, but one parameter of that lens was
+local to BOUNDARY. The transferable object is not "beta 0.3-0.4"; it is:
+
+> report layer maps as classifier margin plus original-vs-shuffle denominator
+> support, and treat denominator-weak poles as asymmetric structural poles.
+
+## Consecutio
+What opens now: apply the same transfer test to `TRASCENDENZA_LIMITE` and
+`G_POTENZIALE_NULLA`, but separate two quantities from the start:
+
+1. classification ambiguity layer;
+2. denominator-collapse layer.
+
+If both layers coincide in a domain, the domain has a BOUNDARY-like transition.
+If they split, as they do here, the gate is still useful but the transition
+coordinate belongs to the target domain, not to the operator.
+
+## Auto-audit: 5 lenti
+- **L1 hard constraint vs bias**: no "always/never/zero" claim. "No endpoint
+  stable observables" means none reached frequency >= 0.75 across both
+  endpoints under `abs(z) >= 2`.
+- **L2 quantity vs ratio**: classification margin is reported together with
+  stable-observable count and z means. Ratios are not interpreted without
+  denominator support.
+- **L3 no silent patching**: the report explicitly separates "gate transfers"
+  from "BOUNDARY beta coordinate transfers." The second is not claimed.
+- **L4 edge cases**: beta 0.4 has ambiguous fraction `0.417`, so only beta 0.3
+  is listed as the replicated ambiguous layer under the >= 0.5 rule.
+- **L5 re-discovery**: this is a finite symbolic-order vs permutation audit,
+  related to standard shuffle/null testing. It is not tagged as a new theorem
+  about Sturmian or Beatty sequences.
+
+## Files
+- Script: `tools/exp_duality_gate_transfer.py`
+- Main data: `tools/data/duality_gate_transfer_20260507_0803.json`
+- Seed check: `tools/data/duality_gate_transfer_20260507_0803_seedcheck.json`
+- Report: `tools/data/reports/agent_20260507_0803.md`
diff --git a/tools/exp_duality_gate_transfer.py b/tools/exp_duality_gate_transfer.py
new file mode 100644
index 0000000000000000000000000000000000000000..c04f490d17524d73d40766b8b969cdfe3d20077c
--- /dev/null
+++ b/tools/exp_duality_gate_transfer.py
@@ -0,0 +1,284 @@
+#!/usr/bin/env python3
+"""
+exp_duality_gate_transfer.py
+
+Reusable META/DUALITA audit for transferring the BOUNDARY_LAYER_GATE.
+
+Question:
+    Does the denominator-collapse gate discriminate dipolar duality from
+    illusory duality, or does it become a local BOUNDARY metric when moved
+    away from GUE/Poisson?
+
+The script builds a controlled layer map between:
+
+- DIPOLARE: a coherent golden/Beatty gap sequence generated by the orbit of
+  floor((n + phase) * phi). Its gaps are the symbolic det=-1 D-ND pole in the
+  minimal positive alphabet {1, 2}.
+- ILLUSORIA: a full random permutation of the same gaps. This preserves the
+  one-point distribution and first moment but removes the generating order.
+
+Layer beta replaces a beta fraction of dipolar positions with values from the
+illusory permutation. Canonical observables and original-vs-shuffle z gates are
+imported from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
+
+
+OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())
+PHI = (1.0 + 5.0**0.5) / 2.0
+
+
+def dipolar_gaps(n_gaps: int, phase: float) -> np.ndarray:
+    """Return coherent golden Beatty gaps in {1, 2}."""
+    n = np.arange(n_gaps + 1, dtype=float) + float(phase)
+    positions = np.floor(n * PHI).astype(float)
+    gaps = np.diff(positions)
+    mean = float(np.mean(gaps))
+    return gaps / mean if mean > 1e-15 else gaps
+
+
+def beta_layer_gaps(
+    dipolar: np.ndarray,
+    beta: float,
+    rng: np.random.Generator,
+) -> np.ndarray:
+    """Replace a beta fraction of coherent positions with a shuffled copy."""
+    illusory = rng.permutation(dipolar)
+    if beta <= 0.0:
+        out = dipolar.copy()
+    elif beta >= 1.0:
+        out = illusory.copy()
+    else:
+        mask = rng.random(len(dipolar)) < beta
+        out = dipolar.copy()
+        out[mask] = illusory[mask]
+    mean = float(np.mean(out))
+    return out / mean if mean > 1e-15 else out
+
+
+def z_against_shuffle(
+    gaps: np.ndarray,
+    n_baseline: int,
+    rng: np.random.Generator,
+) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
+    """Return original observables, shuffle baseline std, and z scores."""
+    original = compute_canonical(gaps)
+    baseline_vals = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline_vals[name].append(obs[name])
+
+    shuffle_std = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline_vals[name], dtype=float)
+        mean = float(np.mean(vals))
+        sd = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        shuffle_std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, shuffle_std, z
+
+
+def vector(row: dict, names: list[str]) -> np.ndarray:
+    return np.array([row["observables"][name] for name in names], dtype=float)
+
+
+def classify_layers(rows: list[dict], obs_names: list[str]) -> dict:
+    """Classify layers by standardized distance to dipolar/illusory endpoints."""
+    if not obs_names:
+        return {
+            "observables": [],
+            "endpoint_distance": 0.0,
+            "layers": {},
+            "ambiguous_beta": [],
+        }
+
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    dip_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    ill_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([dip_vectors, ill_vectors])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    dip_centroid = np.mean(dip_vectors, axis=0)
+    ill_centroid = np.mean(ill_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((ill_centroid - dip_centroid) / scale))
+
+    layers = {}
+    ambiguous_beta = []
+    for beta, beta_rows in sorted(by_beta.items()):
+        coords = []
+        margins = []
+        labels = []
+        for row in beta_rows:
+            x = vector(row, obs_names)
+            d_dip = float(np.linalg.norm((x - dip_centroid) / scale))
+            d_ill = float(np.linalg.norm((x - ill_centroid) / scale))
+            denom = d_dip + d_ill
+            coord = float((d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("dipolar" if d_dip < d_ill else "illusory")
+        ambiguous_fraction = float(np.mean(np.array(margins) < 0.15))
+        if ambiguous_fraction >= 0.5:
+            ambiguous_beta.append(beta)
+        layers[f"{beta:.3f}"] = {
+            "coordinate_mean": float(np.mean(coords)),
+            "coordinate_std": float(np.std(coords, ddof=1)) if len(coords) > 1 else 0.0,
+            "margin_mean": float(np.mean(margins)),
+            "ambiguous_fraction": ambiguous_fraction,
+            "illusory_label_fraction": float(np.mean(np.array(labels) == "illusory")),
+        }
+
+    return {
+        "observables": obs_names,
+        "endpoint_distance": endpoint_distance,
+        "layers": layers,
+        "ambiguous_beta": ambiguous_beta,
+    }
+
+
+def summarize_gate(rows: list[dict], z_min: float) -> dict:
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    layers = {}
+    for beta, beta_rows in sorted(by_beta.items()):
+        stable_counts = []
+        stable_freq = {name: [] for name in OBS_NAMES}
+        z_values = {name: [] for name in OBS_NAMES}
+        for row in beta_rows:
+            stable = [name for name in OBS_NAMES if abs(row["z"][name]) >= z_min]
+            stable_counts.append(len(stable))
+            for name in OBS_NAMES:
+                stable_freq[name].append(1.0 if name in stable else 0.0)
+                z_values[name].append(row["z"][name])
+        layers[f"{beta:.3f}"] = {
+            "stable_count_mean": float(np.mean(stable_counts)),
+            "stable_count_std": float(np.std(stable_counts, ddof=1)) if len(stable_counts) > 1 else 0.0,
+            "stable_frequency": {name: float(np.mean(vals)) for name, vals in stable_freq.items()},
+            "z_mean": {name: float(np.mean(vals)) for name, vals in z_values.items()},
+        }
+
+    endpoint_stable = []
+    for name in OBS_NAMES:
+        endpoint_rows = by_beta[0.0] + by_beta[1.0]
+        freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in endpoint_rows])
+        if freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def run(args: argparse.Namespace) -> dict:
+    rng = np.random.default_rng(args.seed)
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    rows = []
+
+    for rep in range(args.n_replicates):
+        phase = float(rng.random())
+        base = dipolar_gaps(args.n_gaps, phase)
+        for beta in betas:
+            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+            gaps = beta_layer_gaps(base, beta, layer_rng)
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                n_baseline=args.n_baseline,
+                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "replicate": rep,
+                    "phase": phase,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [name for name in OBS_NAMES if abs(z[name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
+
+    output = {
+        "experiment": "duality_gate_transfer",
+        "category": "gate_transferability",
+        "question": "Does the denominator-collapse gate transfer from BOUNDARY to DUALITA_DIPOLARE_VS_ILLUSORIA?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_endpoint_gated": gated_classification,
+        "rows": rows,
+    }
+
+    out_path = Path(args.out)
+    out_path.parent.mkdir(parents=True, exist_ok=True)
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
+    print(f"observables_used={OBS_NAMES}")
+    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
+    print(
+        "endpoint_distance_all="
+        f"{all_classification['endpoint_distance']:.3f} "
+        "endpoint_distance_gated="
+        f"{gated_classification['endpoint_distance']:.3f}"
+    )
+    source = gated_classification if gate["endpoint_stable_observables"] else all_classification
+    print("beta stable_count margin ambiguous illusory_fraction")
+    for beta in betas:
+        key = f"{beta:.3f}"
+        stable_count = gate["layers"][key]["stable_count_mean"]
+        layer = source["layers"].get(key, {})
+        print(
+            f"{beta:>4.2f} {stable_count:>12.3f} "
+            f"{layer.get('margin_mean', 0.0):>8.3f} "
+            f"{layer.get('ambiguous_fraction', 0.0):>9.3f} "
+            f"{layer.get('illusory_label_fraction', 0.0):>17.3f}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=4096)
+    parser.add_argument("--n-replicates", type=int, default=24)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=40)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=202605070803)
+    parser.add_argument("--out", default="tools/data/duality_gate_transfer.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

codex
Completato.

Ho creato ed eseguito l’esperimento richiesto su `DUALITA_DIPOLARE_VS_ILLUSORIA`, fuori da BOUNDARY, usando il gate denominator-collapse con osservabili canonici da `tools/observables_registry.py`.

File prodotti:
- [tools/exp_duality_gate_transfer.py](/opt/MM_D-ND/tools/exp_duality_gate_transfer.py)
- [tools/data/duality_gate_transfer_20260507_0803.json](/opt/MM_D-ND/tools/data/duality_gate_transfer_20260507_0803.json)
- [tools/data/duality_gate_transfer_20260507_0803_seedcheck.json](/opt/MM_D-ND/tools/data/duality_gate_transfer_20260507_0803_seedcheck.json)
- [tools/data/reports/agent_20260507_0803.md](/opt/MM_D-ND/tools/data/reports/agent_20260507_0803.md)

Verdetto scritto nel report: `category: gate_transferability`, `verdict: operator`.

Risultato chiave: il gate trasferisce come operatore di ordine strutturale, perché separa dipolare da illusorio via z original-vs-shuffle replicabili. Però la coordinata locale BOUNDARY non trasferisce: l’ambiguità appare a beta `0.3`, mentre il collasso dei denominatori arriva più tardi, circa beta `0.8-0.9`.

Verifiche:
- `python -m py_compile tools/exp_duality_gate_transfer.py` passato.
- smoke test dello script passato.
- Ho tentato `lab_falsifier.py`, ma i provider locali non erano disponibili (`codex-cli`, `claude-cli`, `openrouter`), quindi non ha prodotto JSON di falsifier.
diff --git a/tools/data/reports/agent_20260507_0803.md b/tools/data/reports/agent_20260507_0803.md
new file mode 100644
index 0000000000000000000000000000000000000000..b9c3f26c791ddb0bf70330fde7968642ab7a35f8
--- /dev/null
+++ b/tools/data/reports/agent_20260507_0803.md
@@ -0,0 +1,155 @@
+# Agent Report - Denominator Gate Transfers, Boundary Coordinate Does Not
+**Date**: 2026-05-07 08:03  
+**Piano**: 69  
+**Category**: gate_transferability  
+**Tension explored**: META + DUALITA_DIPOLARE_VS_ILLUSORIA  
+**verdict**: operator  
+observables_registry: 1.0.0-2026-05-06  
+observables_used: [SR, SR2, L1, L2, triple_var]
+
+## Claim Under Test
+The operator directive asked whether the `BOUNDARY_LAYER_GATE` forged on the
+GUE/Poisson boundary is a transferable operator or only a local metric.
+
+Concrete test:
+
+> Apply the denominator-collapse gate to the discrimination between dipolar
+> duality and illusory duality. If the gate distinguishes structural generation
+> from incoherent dispersion, it is an operator. If it only emits syntactically
+> valid but empty layer maps, it is a BOUNDARY-local metric.
+
+## Experiment
+Tool created: `tools/exp_duality_gate_transfer.py`
+
+Atomic perimeter:
+- DIPOLARE: coherent golden Beatty gaps generated by `floor((n+phase)*phi)`;
+- ILLUSORIA: a random permutation of the same gaps, preserving one-point
+  distribution and first moment but removing the generating order;
+- beta layer: replace a beta fraction of dipolar positions with values from
+  the illusory permutation;
+- main run: 4,096 gaps, 24 replicates, 11 beta layers, 40 shuffle baselines;
+- seed check: 3,072 gaps, 16 replicates, 11 beta layers, 32 shuffle baselines;
+- denominator gate: observable stable when `abs(z original-vs-shuffle) >= 2`;
+- classification: standardized distance to beta 0.0 and beta 1.0 centroids
+  using all five canonical observables.
+
+This is not a new BOUNDARY experiment. GUE and Poisson do not appear in the
+generator. The only transferred object is the denominator gate.
+
+## Results
+
+### Main Run
+
+Endpoint separation using all canonical observables: `3.436` standardized units.  
+Endpoint-stable observables at frequency >= 0.75 across both endpoints: `[]`.
+
+| beta | stable obs / 5 | margin | ambiguous fraction | illusory-label fraction |
+|---:|---:|---:|---:|---:|
+| 0.0 | 3.000 | 0.971 | 0.000 | 0.000 |
+| 0.1 | 3.000 | 0.479 | 0.000 | 0.000 |
+| 0.2 | 3.000 | 0.234 | 0.125 | 0.000 |
+| 0.3 | 3.000 | 0.033 | 1.000 | 0.375 |
+| 0.4 | 3.083 | 0.154 | 0.417 | 0.958 |
+| 0.5 | 3.083 | 0.269 | 0.375 | 1.000 |
+| 0.6 | 3.000 | 0.417 | 0.042 | 1.000 |
+| 0.7 | 2.917 | 0.316 | 0.167 | 1.000 |
+| 0.8 | 1.708 | 0.473 | 0.125 | 1.000 |
+| 0.9 | 0.167 | 0.463 | 0.042 | 1.000 |
+| 1.0 | 0.208 | 0.442 | 0.042 | 1.000 |
+
+Observable stability frequencies:
+- beta 0.0: `SR=1.00`, `L1=1.00`, `triple_var=1.00`; `SR2=0.00`, `L2=0.00`;
+- beta 0.3: same stable trio, with mean z approximately `SR=-19.4`, `L1=-19.4`, `triple_var=-17.6`;
+- beta 0.8: stable trio falls to frequency `0.54` each, mean z around `-1.7` to `-1.9`;
+- beta 0.9: all canonical observables are weak or near weak, mean z around `-0.4..+0.2`;
+- beta 1.0: all canonical observables are weak, mean z around `+0.1..+0.2`.
+
+### Seed Check
+
+The seed check repeated the same structure:
+- endpoint-stable observables: `[]`;
+- all-observable endpoint distance: `3.412`;
+- ambiguous beta: `[0.3]`;
+- beta 0.0 stable count: `3.000`;
+- beta 0.3 stable count: `3.000`, ambiguous fraction `1.000`;
+- beta 0.8 stable count: `1.062`;
+- beta 0.9 stable count: `0.000`;
+- beta 1.0 stable count: `0.438`.
+
+## Findings
+
+1. **The gate does not degenerate on DUALITA.** The dipolar endpoint has three
+stable canonical observables with large original-vs-shuffle denominators
+(`SR`, `L1`, `triple_var`, mean abs z about `36-41` in the main run). The
+illusory endpoint has no stable denominator support. This is a structural
+original-vs-shuffle distinction, not an empty syntactic map.
+
+2. **The BOUNDARY layer coordinate does not transfer unchanged.** In the
+GUE/Poisson run, beta 0.3-0.4 carried both classification ambiguity and
+denominator collapse. Here, beta 0.3 is classification-ambiguous, but the
+denominator support is still strong: stable count remains `3.000/5`.
+Denominator collapse arrives later, around beta 0.8-0.9. Therefore the
+operator transfers, but the specific BOUNDARY layer shape is local.
+
+3. **Endpoint-gated classification remains empty for the same structural
+reason as BOUNDARY.** The endpoint-stable set is empty because the illusory
+pole is denominator-weak. This does not erase the discrimination; it prevents
+symmetric endpoint-gated retention claims. The valid claim is one-sided:
+coherent dipolar order survives original-vs-shuffle gating; illusory dispersion
+does not.
+
+4. **The discriminant is order, not marginal distribution.** The illusory
+sequence preserves the same alphabet, first moment, and one-point distribution
+as the dipolar sequence. The gate is therefore not measuring the marginal
+composition of gaps. It measures whether the canonical observables retain an
+ordered denominator against full shuffle.
+
+## Verdict
+**category: gate_transferability**  
+**verdict: operator**
+
+Scoped statement:
+
+> In this synthetic DUALITA perimeter, the denominator gate is transferable as
+> an operator for structural order: it separates dipolar generation from
+> illusory dispersion with replicated z support. The beta coordinate of the
+> GUE/Poisson boundary layer is not transferable: ambiguity appears near beta
+> 0.3, while denominator collapse appears near beta 0.8-0.9.
+
+So the last BOUNDARY cycles forged a lens, but one parameter of that lens was
+local to BOUNDARY. The transferable object is not "beta 0.3-0.4"; it is:
+
+> report layer maps as classifier margin plus original-vs-shuffle denominator
+> support, and treat denominator-weak poles as asymmetric structural poles.
+
+## Consecutio
+What opens now: apply the same transfer test to `TRASCENDENZA_LIMITE` and
+`G_POTENZIALE_NULLA`, but separate two quantities from the start:
+
+1. classification ambiguity layer;
+2. denominator-collapse layer.
+
+If both layers coincide in a domain, the domain has a BOUNDARY-like transition.
+If they split, as they do here, the gate is still useful but the transition
+coordinate belongs to the target domain, not to the operator.
+
+## Auto-audit: 5 lenti
+- **L1 hard constraint vs bias**: no "always/never/zero" claim. "No endpoint
+  stable observables" means none reached frequency >= 0.75 across both
+  endpoints under `abs(z) >= 2`.
+- **L2 quantity vs ratio**: classification margin is reported together with
+  stable-observable count and z means. Ratios are not interpreted without
+  denominator support.
+- **L3 no silent patching**: the report explicitly separates "gate transfers"
+  from "BOUNDARY beta coordinate transfers." The second is not claimed.
+- **L4 edge cases**: beta 0.4 has ambiguous fraction `0.417`, so only beta 0.3
+  is listed as the replicated ambiguous layer under the >= 0.5 rule.
+- **L5 re-discovery**: this is a finite symbolic-order vs permutation audit,
+  related to standard shuffle/null testing. It is not tagged as a new theorem
+  about Sturmian or Beatty sequences.
+
+## Files
+- Script: `tools/exp_duality_gate_transfer.py`
+- Main data: `tools/data/duality_gate_transfer_20260507_0803.json`
+- Seed check: `tools/data/duality_gate_transfer_20260507_0803_seedcheck.json`
+- Report: `tools/data/reports/agent_20260507_0803.md`
diff --git a/tools/exp_duality_gate_transfer.py b/tools/exp_duality_gate_transfer.py
new file mode 100644
index 0000000000000000000000000000000000000000..c04f490d17524d73d40766b8b969cdfe3d20077c
--- /dev/null
+++ b/tools/exp_duality_gate_transfer.py
@@ -0,0 +1,284 @@
+#!/usr/bin/env python3
+"""
+exp_duality_gate_transfer.py
+
+Reusable META/DUALITA audit for transferring the BOUNDARY_LAYER_GATE.
+
+Question:
+    Does the denominator-collapse gate discriminate dipolar duality from
+    illusory duality, or does it become a local BOUNDARY metric when moved
+    away from GUE/Poisson?
+
+The script builds a controlled layer map between:
+
+- DIPOLARE: a coherent golden/Beatty gap sequence generated by the orbit of
+  floor((n + phase) * phi). Its gaps are the symbolic det=-1 D-ND pole in the
+  minimal positive alphabet {1, 2}.
+- ILLUSORIA: a full random permutation of the same gaps. This preserves the
+  one-point distribution and first moment but removes the generating order.
+
+Layer beta replaces a beta fraction of dipolar positions with values from the
+illusory permutation. Canonical observables and original-vs-shuffle z gates are
+imported from observables_registry.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+import numpy as np
+
+from observables_registry import (
+    OBSERVABLES_CANONICAL,
+    OBSERVABLES_REGISTRY_VERSION,
+    compute_canonical,
+)
+
+
+OBS_NAMES = list(OBSERVABLES_CANONICAL.keys())
+PHI = (1.0 + 5.0**0.5) / 2.0
+
+
+def dipolar_gaps(n_gaps: int, phase: float) -> np.ndarray:
+    """Return coherent golden Beatty gaps in {1, 2}."""
+    n = np.arange(n_gaps + 1, dtype=float) + float(phase)
+    positions = np.floor(n * PHI).astype(float)
+    gaps = np.diff(positions)
+    mean = float(np.mean(gaps))
+    return gaps / mean if mean > 1e-15 else gaps
+
+
+def beta_layer_gaps(
+    dipolar: np.ndarray,
+    beta: float,
+    rng: np.random.Generator,
+) -> np.ndarray:
+    """Replace a beta fraction of coherent positions with a shuffled copy."""
+    illusory = rng.permutation(dipolar)
+    if beta <= 0.0:
+        out = dipolar.copy()
+    elif beta >= 1.0:
+        out = illusory.copy()
+    else:
+        mask = rng.random(len(dipolar)) < beta
+        out = dipolar.copy()
+        out[mask] = illusory[mask]
+    mean = float(np.mean(out))
+    return out / mean if mean > 1e-15 else out
+
+
+def z_against_shuffle(
+    gaps: np.ndarray,
+    n_baseline: int,
+    rng: np.random.Generator,
+) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
+    """Return original observables, shuffle baseline std, and z scores."""
+    original = compute_canonical(gaps)
+    baseline_vals = {name: [] for name in OBS_NAMES}
+    for _ in range(n_baseline):
+        obs = compute_canonical(rng.permutation(gaps))
+        for name in OBS_NAMES:
+            baseline_vals[name].append(obs[name])
+
+    shuffle_std = {}
+    z = {}
+    for name in OBS_NAMES:
+        vals = np.array(baseline_vals[name], dtype=float)
+        mean = float(np.mean(vals))
+        sd = float(np.std(vals, ddof=1)) if len(vals) > 1 else 0.0
+        shuffle_std[name] = sd
+        z[name] = float((original[name] - mean) / sd) if sd > 1e-15 else 0.0
+    return original, shuffle_std, z
+
+
+def vector(row: dict, names: list[str]) -> np.ndarray:
+    return np.array([row["observables"][name] for name in names], dtype=float)
+
+
+def classify_layers(rows: list[dict], obs_names: list[str]) -> dict:
+    """Classify layers by standardized distance to dipolar/illusory endpoints."""
+    if not obs_names:
+        return {
+            "observables": [],
+            "endpoint_distance": 0.0,
+            "layers": {},
+            "ambiguous_beta": [],
+        }
+
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    dip_vectors = np.array([vector(row, obs_names) for row in by_beta[0.0]], dtype=float)
+    ill_vectors = np.array([vector(row, obs_names) for row in by_beta[1.0]], dtype=float)
+    endpoints = np.vstack([dip_vectors, ill_vectors])
+    scale = np.std(endpoints, axis=0, ddof=1)
+    scale[scale <= 1e-15] = 1.0
+    dip_centroid = np.mean(dip_vectors, axis=0)
+    ill_centroid = np.mean(ill_vectors, axis=0)
+    endpoint_distance = float(np.linalg.norm((ill_centroid - dip_centroid) / scale))
+
+    layers = {}
+    ambiguous_beta = []
+    for beta, beta_rows in sorted(by_beta.items()):
+        coords = []
+        margins = []
+        labels = []
+        for row in beta_rows:
+            x = vector(row, obs_names)
+            d_dip = float(np.linalg.norm((x - dip_centroid) / scale))
+            d_ill = float(np.linalg.norm((x - ill_centroid) / scale))
+            denom = d_dip + d_ill
+            coord = float((d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
+            margin = float(abs(d_dip - d_ill) / denom) if denom > 1e-15 else 0.0
+            coords.append(coord)
+            margins.append(margin)
+            labels.append("dipolar" if d_dip < d_ill else "illusory")
+        ambiguous_fraction = float(np.mean(np.array(margins) < 0.15))
+        if ambiguous_fraction >= 0.5:
+            ambiguous_beta.append(beta)
+        layers[f"{beta:.3f}"] = {
+            "coordinate_mean": float(np.mean(coords)),
+            "coordinate_std": float(np.std(coords, ddof=1)) if len(coords) > 1 else 0.0,
+            "margin_mean": float(np.mean(margins)),
+            "ambiguous_fraction": ambiguous_fraction,
+            "illusory_label_fraction": float(np.mean(np.array(labels) == "illusory")),
+        }
+
+    return {
+        "observables": obs_names,
+        "endpoint_distance": endpoint_distance,
+        "layers": layers,
+        "ambiguous_beta": ambiguous_beta,
+    }
+
+
+def summarize_gate(rows: list[dict], z_min: float) -> dict:
+    by_beta: dict[float, list[dict]] = {}
+    for row in rows:
+        by_beta.setdefault(float(row["beta"]), []).append(row)
+
+    layers = {}
+    for beta, beta_rows in sorted(by_beta.items()):
+        stable_counts = []
+        stable_freq = {name: [] for name in OBS_NAMES}
+        z_values = {name: [] for name in OBS_NAMES}
+        for row in beta_rows:
+            stable = [name for name in OBS_NAMES if abs(row["z"][name]) >= z_min]
+            stable_counts.append(len(stable))
+            for name in OBS_NAMES:
+                stable_freq[name].append(1.0 if name in stable else 0.0)
+                z_values[name].append(row["z"][name])
+        layers[f"{beta:.3f}"] = {
+            "stable_count_mean": float(np.mean(stable_counts)),
+            "stable_count_std": float(np.std(stable_counts, ddof=1)) if len(stable_counts) > 1 else 0.0,
+            "stable_frequency": {name: float(np.mean(vals)) for name, vals in stable_freq.items()},
+            "z_mean": {name: float(np.mean(vals)) for name, vals in z_values.items()},
+        }
+
+    endpoint_stable = []
+    for name in OBS_NAMES:
+        endpoint_rows = by_beta[0.0] + by_beta[1.0]
+        freq = np.mean([1.0 if abs(row["z"][name]) >= z_min else 0.0 for row in endpoint_rows])
+        if freq >= 0.75:
+            endpoint_stable.append(name)
+
+    return {
+        "z_min": z_min,
+        "endpoint_stable_observables": endpoint_stable,
+        "layers": layers,
+    }
+
+
+def run(args: argparse.Namespace) -> dict:
+    rng = np.random.default_rng(args.seed)
+    betas = [float(x) for x in np.linspace(0.0, 1.0, args.n_beta)]
+    rows = []
+
+    for rep in range(args.n_replicates):
+        phase = float(rng.random())
+        base = dipolar_gaps(args.n_gaps, phase)
+        for beta in betas:
+            layer_rng = np.random.default_rng(rng.integers(0, 2**63 - 1))
+            gaps = beta_layer_gaps(base, beta, layer_rng)
+            obs, shuffle_std, z = z_against_shuffle(
+                gaps,
+                n_baseline=args.n_baseline,
+                rng=np.random.default_rng(rng.integers(0, 2**63 - 1)),
+            )
+            rows.append(
+                {
+                    "replicate": rep,
+                    "phase": phase,
+                    "beta": beta,
+                    "observables": obs,
+                    "shuffle_std": shuffle_std,
+                    "z": z,
+                    "stable_observables": [name for name in OBS_NAMES if abs(z[name]) >= args.z_min],
+                }
+            )
+
+    gate = summarize_gate(rows, args.z_min)
+    all_classification = classify_layers(rows, OBS_NAMES)
+    gated_classification = classify_layers(rows, gate["endpoint_stable_observables"])
+
+    output = {
+        "experiment": "duality_gate_transfer",
+        "category": "gate_transferability",
+        "question": "Does the denominator-collapse gate transfer from BOUNDARY to DUALITA_DIPOLARE_VS_ILLUSORIA?",
+        "observables_registry": OBSERVABLES_REGISTRY_VERSION,
+        "observables_used": OBS_NAMES,
+        "params": vars(args),
+        "gate": gate,
+        "classification_all_observables": all_classification,
+        "classification_endpoint_gated": gated_classification,
+        "rows": rows,
+    }
+
+    out_path = Path(args.out)
+    out_path.parent.mkdir(parents=True, exist_ok=True)
+    with out_path.open("w") as f:
+        json.dump(output, f, indent=2)
+
+    print(f"observables_registry={OBSERVABLES_REGISTRY_VERSION}")
+    print(f"observables_used={OBS_NAMES}")
+    print(f"endpoint_stable_observables={gate['endpoint_stable_observables']}")
+    print(
+        "endpoint_distance_all="
+        f"{all_classification['endpoint_distance']:.3f} "
+        "endpoint_distance_gated="
+        f"{gated_classification['endpoint_distance']:.3f}"
+    )
+    source = gated_classification if gate["endpoint_stable_observables"] else all_classification
+    print("beta stable_count margin ambiguous illusory_fraction")
+    for beta in betas:
+        key = f"{beta:.3f}"
+        stable_count = gate["layers"][key]["stable_count_mean"]
+        layer = source["layers"].get(key, {})
+        print(
+            f"{beta:>4.2f} {stable_count:>12.3f} "
+            f"{layer.get('margin_mean', 0.0):>8.3f} "
+            f"{layer.get('ambiguous_fraction', 0.0):>9.3f} "
+            f"{layer.get('illusory_label_fraction', 0.0):>17.3f}"
+        )
+    print(f"saved {out_path}")
+    return output
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--n-gaps", type=int, default=4096)
+    parser.add_argument("--n-replicates", type=int, default=24)
+    parser.add_argument("--n-beta", type=int, default=11)
+    parser.add_argument("--n-baseline", type=int, default=40)
+    parser.add_argument("--z-min", type=float, default=2.0)
+    parser.add_argument("--seed", type=int, default=202605070803)
+    parser.add_argument("--out", default="tools/data/duality_gate_transfer.json")
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == "__main__":
+    main()

tokens used
163,750
Completato.

Ho creato ed eseguito l’esperimento richiesto su `DUALITA_DIPOLARE_VS_ILLUSORIA`, fuori da BOUNDARY, usando il gate denominator-collapse con osservabili canonici da `tools/observables_registry.py`.

File prodotti:
- [tools/exp_duality_gate_transfer.py](/opt/MM_D-ND/tools/exp_duality_gate_transfer.py)
- [tools/data/duality_gate_transfer_20260507_0803.json](/opt/MM_D-ND/tools/data/duality_gate_transfer_20260507_0803.json)
- [tools/data/duality_gate_transfer_20260507_0803_seedcheck.json](/opt/MM_D-ND/tools/data/duality_gate_transfer_20260507_0803_seedcheck.json)
- [tools/data/reports/agent_20260507_0803.md](/opt/MM_D-ND/tools/data/reports/agent_20260507_0803.md)

Verdetto scritto nel report: `category: gate_transferability`, `verdict: operator`.

Risultato chiave: il gate trasferisce come operatore di ordine strutturale, perché separa dipolare da illusorio via z original-vs-shuffle replicabili. Però la coordinata locale BOUNDARY non trasferisce: l’ambiguità appare a beta `0.3`, mentre il collasso dei denominatori arriva più tardi, circa beta `0.8-0.9`.

Verifiche:
- `python -m py_compile tools/exp_duality_gate_transfer.py` passato.
- smoke test dello script passato.
- Ho tentato `lab_falsifier.py`, ma i provider locali non erano disponibili (`codex-cli`, `claude-cli`, `openrouter`), quindi non ha prodotto JSON di falsifier.
