Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions core.py
Original file line number Diff line number Diff line change
Expand Up @@ -527,10 +527,18 @@ async def analizza_async(


def _bias_linguistico_score(bias_pats: list[Patologia]) -> float:
"""1.0 - max severita patologie hedging/booster (cap a 1.0)."""
"""1.0 - max severita patologie BOOSTER (cap a 1.0).

Solo il boosting (assolutismo / petitio: «ovviamente», «indubbiamente»)
erode ε: è la mossa che la critica ai dogmi punisce. L'HEDGING
(«forse», «sembra», «potrebbe») è il marcatore della provvisorietà
fallibilista che l'asse ऋ⁷ *loda* — non è un bias e NON abbassa ε
(B1, 2026-07). Resta rilevato e visibile come segnale descrittivo; la
distinzione cautela↔evasività (weasel) spetta all'induttivo
(diagnosi_malafede), non al conteggio deterministico.
"""
rilevanti = [p.severita for p in bias_pats
if p.tipo in {TipoPatologia.HEDGING_ECCESSIVO,
TipoPatologia.BOOSTER_ECCESSIVO}]
if p.tipo is TipoPatologia.BOOSTER_ECCESSIVO]
if not rilevanti:
return 1.0
return round(max(0.0, 1.0 - max(rilevanti)), 4)
Expand All @@ -546,7 +554,7 @@ def _bias_linguistico_score(bias_pats: list[Patologia]) -> float:
TipoPatologia.DENSITA_CRITICA},
"coerenza_tematica": {TipoPatologia.INCOERENZA_TEMATICA, TipoPatologia.DERIVA_REGISTRO},
"coesione_semantica": {TipoPatologia.INCOERENZA_LOCALE},
"bias_linguistico": {TipoPatologia.HEDGING_ECCESSIVO, TipoPatologia.BOOSTER_ECCESSIVO},
"bias_linguistico": {TipoPatologia.BOOSTER_ECCESSIVO}, # hedging non erode ε (B1)
"credibilita_fonte": {TipoPatologia.APPELLO_AUTORITA},
"integrita_obiettivo": {TipoPatologia.OBIETTIVO_CONTRADDITTORIO, TipoPatologia.OBIETTIVO_DISPERSO},
}
Expand Down
264 changes: 264 additions & 0 deletions docs/analisi_bias.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion epsilon.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
coesione_semantica 0.12 (coerenza.coesione_locale)
coerenza_tematica 0.08 (coerenza.coerenza_tematica_score)
qualita_sintattica 0.10 (profilo_linguistico.qualita_sintattica)
bias_linguistico 0.08 (1 - max(hedge_ratio, booster_ratio)/0.1)
bias_linguistico 0.08 (1 - booster_ratio/0.1; hedging NON erode — B1)
credibilita_fonte 0.07 (autorita.credibilita)

NB: `validita_formale` (entailment) e `assenza_fallacie` (MAFALDA) sono assi
Expand Down
29 changes: 19 additions & 10 deletions gamma/bias_autorita.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"""resh/bias_autorita.py — NER + lessico hedging/booster + ad verecundiam.

Rileva:
- hedging_ratio = #hedges / n_token → bias verso indeterminatezza
- booster_ratio = #boosters / n_token → bias verso assolutismo (petitio)
- hedging_ratio = #hedges / n_token → SEGNALE DESCRITTIVO (provvisorietà
fallibilista, non un bias): rilevato e visibile, ma NON erode ε (B1, 2026-07)
- booster_ratio = #boosters / n_token → bias verso assolutismo (petitio) → ε
- pattern ad_verecundiam non-citato (NER PER + verbo dicendi + no quote)
- bias multipli accumulati → flag su `AutoritaCriteri.bias_rilevati`

Expand Down Expand Up @@ -156,16 +157,24 @@ def analizza_bias_autorita(testo: str, doc: AnnotatedDoc) -> tuple[AutoritaCrite
# AutoritaCriteri legacy
persone = _persone_da_doc(doc)
fonte_label = persone[0] if persone else "sconosciuta"
# expertise: euristica conservativa — true solo se citata 1+ PER nominata
expertise = bool(persone)

# credibilità: baseline neutro-positivo, penalità per bias, bonus per testo pulito
# expertise (A5, 2026-07): un nome proprio da solo NON è expertise — anche il
# soggetto del testo («Berkeley», «Socrate») è un nome. Serve un nome NOMINATO
# con un segnale di citazione (virgolette o riferimento bibliografico), non la
# sua mera presenza. Riusa i marcatori già rilevati sul testo.
ha_citazione = bool(re.search(r"[\"«»“”']", testo) or
re.search(r"\((?:cf|cfr|vedi|in)\.?\s+", testo, re.I))
expertise = bool(persone) and ha_citazione

# credibilità: baseline neutro-positivo, penalità SOLO per autorità non citata
# (ad_verecundiam), bonus per testo pulito. NON reagisce più a hedge/booster
# (B1+A1, 2026-07): l'hedging è fallibilismo (non bias), e il booster è già
# penalizzato una volta sola in `bias_linguistico` — qui era doppio conteggio
# su due componenti di ε. `credibilita_fonte` misura ora SOLO l'autorità/fonte,
# coerente col suo nome.
credibilita = 0.65
credibilita -= 0.15 * (hedging_ratio > 0.06)
credibilita -= 0.20 * (booster_ratio > 0.04)
credibilita -= 0.20 * (1 if fonti_invocate else 0)
if not (hedging_ratio > 0.06 or booster_ratio > 0.04 or fonti_invocate):
credibilita += 0.10 # testo senza bias rilevati → premio epistemico
if not fonti_invocate:
credibilita += 0.10 # nessuna autorità non citata → premio epistemico
credibilita = max(0.05, min(0.95, credibilita))

autorita = AutoritaCriteri(
Expand Down
62 changes: 59 additions & 3 deletions gamma/fallacie.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import logging
import re
from pathlib import Path
from typing import Optional

from . import _nli
from .annotazione import AnnotatedDoc
Expand Down Expand Up @@ -122,13 +123,65 @@ def _regex_fallacies(testo: str) -> list[Patologia]:
"fallacia_l2": meta["tipo"],
"match": m.group(0),
"fonte": f"regex_{lang}",
"confermata": True, # regex ad alta precisione
# A2 (2026-07): un match regex NON è più un verdetto per sé.
# `confermata` viene deciso a valle da `_conferma_via_nli`
# (co-occorrenza con l'NLI). Default False finché non confermato.
"confermata": False,
},
origine_modulo = "fallacie",
))
return found


def _conferma_via_nli(regex_pats: list[Patologia], nli_pats: list[Patologia],
sentence_spans: list[tuple[int, int]]) -> None:
"""Marca in-place `confermata` sui regex (A2, 2026-07).

Un regex è `confermata=True` **iff** l'NLI ha rilevato la STESSA `fallacia_l2`
nella STESSA frase — il criterio è la conferma indipendente (README: «verdetti
= più segnali indipendenti»), non l'essere-regex. Era l'intento originale del
file: «i regex vengono confermate/integrate dal classifier NLI».

Senza NLI (backend degradato / nessun candidato → `nli_pats` vuoto) nessun
regex è confermato: niente modello, niente verdetti di fallacia (coerente con
l'onestà dichiarata di resh in modalità degradata).

`sentence_spans`: [(start_char, end_char), …] da `doc.sentences`, per mappare lo
span del match alla frase. Se il mapping fallisce (offset ignoti, es. fallback
con span 0), si ricade su un confronto doc-level per tipo.
"""
nli_by_frase: set[tuple[int, str]] = set()
nli_types: set[str] = set()
for p in nli_pats:
l2 = p.dettaglio.get("fallacia_l2")
if not l2:
continue
nli_types.add(l2)
idx = p.dettaglio.get("idx_frase")
if idx is not None:
nli_by_frase.add((int(idx), l2))

def _frase_di(span: Optional[tuple[int, int]]) -> Optional[int]:
if not span:
return None
start = span[0]
for i, (s, e) in enumerate(sentence_spans):
if e > s and s <= start < e:
return i
return None

for p in regex_pats:
l2 = p.dettaglio.get("fallacia_l2")
if not l2:
p.dettaglio["confermata"] = False
continue
idx = _frase_di(p.span_char)
if idx is not None:
p.dettaglio["confermata"] = (idx, l2) in nli_by_frase
else:
p.dettaglio["confermata"] = l2 in nli_types # fallback doc-level


def _frasi_con_marker(doc: AnnotatedDoc) -> list[tuple[int, str]]:
"""Ritorna [(idx_frase, text)] solo per frasi che contengono marker
argomentativi — riduce drasticamente il carico NLI."""
Expand Down Expand Up @@ -220,9 +273,12 @@ def rileva_fallacie(doc: AnnotatedDoc, *, threshold: float = 0.55) -> list[Patol
"""Ritorna lista deduplicata di patologie FALLACIA_LOGICA.

threshold: soglia confidence per accettare risultato NLI (default 0.55,
calibrato per deberta-v3-base-zeroshot-v2.0). Regex sono sempre inclusi
(precisione alta per costruzione).
calibrato per deberta-v3-base-zeroshot-v2.0). Regex sono sempre inclusi come
CANDIDATI; diventano `confermata` solo se l'NLI conferma la stessa fallacia
nella stessa frase (`_conferma_via_nli`, A2).
"""
regex_pats = _regex_fallacies(doc.text)
nli_pats = _dedup_per_frase(_nli_fallacies(doc, threshold=threshold))
sentence_spans = [(s.start_char, s.end_char) for s in doc.sentences]
_conferma_via_nli(regex_pats, nli_pats, sentence_spans)
return _dedup(regex_pats + nli_pats)
24 changes: 22 additions & 2 deletions gamma/profilo_linguistico.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,18 @@ def qualita_sintattica(profilo: dict) -> Optional[float]:
Usato come componente 'qualità_sintattica' in epsilon.py. Curva ad
arco: testi troppo semplici (Gulpease>80, MTLD<30) e troppo barocchi
(depth>8, sub_ratio>1.5) sono entrambi penalizzati.

REGISTER-SENSITIVE (limite noto, come `struttura_argomentativa`): gli ottimi
depth≈4.5 / sub≈0.6 sono di un registro espositivo contemporaneo; la prosa
classica a periodi lunghi (alta subordinazione/profondità) è in parte
penalizzata da artefatto di segmentazione, non da difetto reale — va letta con
gli altri componenti. Il retuning richiede un gold set di calibrazione (non si
inventano numeri a priori: il sistema si raffina nel tempo). Il report dichiara
questa provvisorietà (B2).

LINGUA: il Gulpease è una formula tarata sull'ITALIANO — su testi EN è
miscalibrato, quindi viene ESCLUSO e i pesi rinormalizzati sui tre restanti
(A4, 2026-07).
"""
# Evidenza insufficiente: la stilometria (MTLD, depth, sub_ratio) non è
# affidabile sotto ~30 token → ritorna None = «non misurabile», così
Expand All @@ -172,15 +184,23 @@ def qualita_sintattica(profilo: dict) -> Optional[float]:
if profilo.get("n_token", 0) < 30:
return None

from .. import config
lang = config.LANG.get()

mtld = profilo.get("mtld", 0.0)
sub = profilo.get("subordination_ratio", 0.0)
depth = profilo.get("profondita_media_albero", 0.0)
gulpease = profilo.get("gulpease", 50.0)

mtld_score = min(1.0, max(0.0, (mtld - 30.0) / 60.0)) # 30→0, 90→1
depth_score = 1.0 - min(1.0, abs(depth - 4.5) / 4.5) # ottimo @ ~4.5
sub_score = 1.0 - min(1.0, abs(sub - 0.6) / 0.6) # ottimo @ ~0.6
gulp_score = 1.0 - min(1.0, abs(gulpease - 55.0) / 35.0) # ottimo @ ~55

if lang == "en":
# Gulpease escluso (formula IT): rinormalizza 0.35/0.25/0.20 → Σ=1.
return round((0.35 * mtld_score + 0.25 * depth_score
+ 0.20 * sub_score) / 0.80, 4)

gulpease = profilo.get("gulpease", 50.0)
gulp_score = 1.0 - min(1.0, abs(gulpease - 55.0) / 35.0) # ottimo @ ~55
return round(0.35 * mtld_score + 0.25 * depth_score
+ 0.20 * sub_score + 0.20 * gulp_score, 4)
11 changes: 6 additions & 5 deletions lessici/booster_en.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# A3 (Mill, A System of Logic): logical quantifiers/modals — all, none, always,
# never, necessarily — do NOT belong here. They are constitutive of valid argument
# (a syllogism uses them by construction), not rhetorical emphasis. The circularity
# Mill ascribes to universal quantification (the syllogism is a petitio: the major
# premise already contains the conclusion) is the business of the circularity axis
# (Trilemma C₂ vicious/virtuous, circular_reasoning), not of rhetorical bias.
obviously
certainly
undoubtedly
Expand All @@ -20,11 +26,6 @@ incontrovertibly
surely
notoriously
indisputably
necessarily
always
never
all
none
totally
completely
absolutely certain
Expand Down
11 changes: 6 additions & 5 deletions lessici/booster_it.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# A3 (Mill, A System of Logic): i quantificatori/modali logici — tutti, nessuno,
# sempre, mai, necessariamente — NON stanno qui. Sono costitutivi dell'argomentazione
# valida (un sillogismo li usa per costruzione), non enfasi retorica. La circolarità
# che Mill attribuisce alla quantificazione universale (il sillogismo è petitio: la
# premessa maggiore include già la conclusione) è competenza dell'asse circolarità
# (Trilemma C₂ viziosa/virtuosa, circular_reasoning), non del bias retorico.
ovviamente
certamente
indubbiamente
Expand All @@ -21,11 +27,6 @@ sicuramente
notoriamente
indiscutibilmente
pacificamente
necessariamente
sempre
mai
tutti
nessuno
totalmente
completamente
assolutamente certo
Expand Down
23 changes: 23 additions & 0 deletions report.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,23 @@
from typing import Optional


# B3 — la lente di resh dichiarata in testa al report, come resh esige dai testi.
POSTURA_RESH = (
"> **Postura di resh:** analisi non-fondazionalista; il framework SA{ऋ} "
"(ऋ¹ infondabilità operativa, ऋ² vuoto ontologico, ऋ⁶ contingenza dei "
"significati) è un **C₃ strumentale dichiarato**, non un fondamento — resh è "
"soggetto agli stessi assi che applica."
)

# B2 — la provvisorietà che vive nei commenti-codice, resa visibile: niente
# precisione spuria (è la «certezza operativa non dichiarata» che resh segnala altrove).
CAVEAT_EPS = (
"*Pesi di ε provvisori, non ancora calibrati su gold set; «qualità sintattica» "
"e «struttura argomentativa» risentono del registro d'epoca — leggere ε come "
"indicativo, non assoluto.*"
)


_COMP_LABELS = {
"trasparenza_premesse": "Trasparenza premesse",
"validita_formale": "Validità formale (sequitur)",
Expand Down Expand Up @@ -278,12 +295,15 @@ def genera_report_documento(rap_doc, *, run_uid: str = "") -> str:
f"**Call LLM:** {meta.get('call_eseguite')} · {meta.get('ts')}"
+ (f" · **Run:** `{run_uid}`" if run_uid else ""),
"",
POSTURA_RESH,
"",
]

# Executive summary in testa
head.append("## Riepilogo\n")
fascia = _fascia_eps(eps_doc)
head.append(f"**Epsilon documento: {eps_doc}** (tenuta epistemica: {fascia})")
head.append(CAVEAT_EPS)
if O and "errore" not in O:
head.append(f"\n**Obiettivo:** «{O.get('dichiarato', '?')}»")
if sd:
Expand Down Expand Up @@ -371,12 +391,15 @@ def genera_report(det: Optional[dict] = None, ind: Optional[dict] = None,
+ (f" · **Run:** `{run_uid}`" if run_uid else "")
+ (f" · {ts}" if ts else ""),
"",
POSTURA_RESH,
"",
]

# Executive summary
if eps is not None:
head.append("## Riepilogo\n")
head.append(f"**Epsilon: {eps}** (tenuta epistemica: {fascia})")
head.append(CAVEAT_EPS)
pat = (det or {}).get("patologie", [])
if pat:
head.append(f" · Patologie rilevate: {len(pat)}")
Expand Down
Loading