fix(verify): PUNCT_END generico e frasi spezzate come warning

PUNCT_END esteso con terminatori comuni in documenti tecnici:
- $ e } per formule LaTeX inline e blocchi
- > per tag HTML (</table>, </td>, ecc.)
- \\ per line break LaTeX
- · (punto centrato, U+00B7) per notazione matematica
Caratteri unicode scritti con chr() per evitare SyntaxError Python 3.12+
con curly quotes nel sorgente.

Frasi spezzate spostate da blockers a warnings: il chunker le risolve
automaticamente con merge_broken_sentences; i casi residui sono
perlopiù falsi positivi (frontespizi, entità proprie senza punto finale).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 14:18:27 +02:00
parent f0c6bad046
commit c93e2e8494
+28 -16
View File
@@ -32,13 +32,27 @@ import config as cfg
MIN_CHARS = cfg.MIN_CHARS MIN_CHARS = cfg.MIN_CHARS
MAX_CHARS = cfg.MAX_CHARS MAX_CHARS = cfg.MAX_CHARS
_PUNCT_CLS = (
"[.!?"
+ chr(0xBB) + ")\\]"
+ chr(0x2018) + chr(0x2019)
+ chr(0x201C) + chr(0x201D)
+ "\"'"
+ chr(0x2014) + chr(0x2013) + chr(0x2026)
+ chr(0xB7) # punto centrato LaTeX
+ "]$"
)
PUNCT_END = re.compile( PUNCT_END = re.compile(
r"[.!?\xbb)\]'\"“”‘—–…]$" _PUNCT_CLS
r"|/$" # URL che finisce con / + r"|/$"
r"|\|$" # riga di tabella Markdown + r"|\|$"
r"|;$" # fine clausola legale + r"|;$"
r"|:$" # introduzione a lista o formula + r"|:$"
r"|\d[\d.,/]*$" # numero, anno, versione, riferimento normativo + r"|\d[\d.,/]*$"
+ r"|\$$"
+ r"|\}$"
+ r"|>$"
+ r"|\\\\$"
) )
_HEX_END = re.compile(r"[0-9a-fA-F]{8,}$") _HEX_END = re.compile(r"[0-9a-fA-F]{8,}$")
_URL_TAIL = re.compile(r"(https?://|www\.)\S+(\s+\S+){0,3}$") _URL_TAIL = re.compile(r"(https?://|www\.)\S+(\s+\S+){0,3}$")
@@ -312,13 +326,13 @@ def verify_stem(stem: str, project_root: Path, min_chars: int, max_chars: int) -
print(f" → Causa probabile: sezioni senza testo nel clean.md") print(f" → Causa probabile: sezioni senza testo nel clean.md")
if incomplete: if incomplete:
print(f"\n 🔴 {len(incomplete)} chunk con FRASE SPEZZATA:") print(f"\n 🟡 {len(incomplete)} chunk con FRASE SPEZZATA (warning):")
for c in incomplete[:5]: for c in incomplete[:5]:
last_line = c.get("text", "").rstrip().split("\n")[-1][-80:] last_line = c.get("text", "").rstrip().split("\n")[-1][-80:]
print(f" [{c.get('chunk_id', '?')}] ...{last_line!r}") print(f" [{c.get('chunk_id', '?')}] ...{last_line!r}")
if len(incomplete) > 5: if len(incomplete) > 5:
print(f" ... e altri {len(incomplete) - 5}") print(f" ... e altri {len(incomplete) - 5}")
print(f" → Soluzione: python chunks/fix_chunks.py --stem {stem}") print(f" → Soluzione: correggi il sorgente .md e rigenera con chunker.py --force")
# ── Warnings ────────────────────────────────────────────────────────────── # ── Warnings ──────────────────────────────────────────────────────────────
@@ -350,12 +364,12 @@ def verify_stem(stem: str, project_root: Path, min_chars: int, max_chars: int) -
print(f" [{e['chunk_id']}] ≡ [{e['duplicate_of']}] «{e['last_text'][:60]}»") print(f" [{e['chunk_id']}] ≡ [{e['duplicate_of']}] «{e['last_text'][:60]}»")
if len(duplicates) > 5: if len(duplicates) > 5:
print(f" ... e altri {len(duplicates) - 5}") print(f" ... e altri {len(duplicates) - 5}")
print(f" → Causa probabile: fix_chunks merge multipli o sezioni ripetute") print(f" → Causa probabile: sezioni ripetute nel sorgente .md")
# ── Report.json ─────────────────────────────────────────────────────────── # ── Report.json ───────────────────────────────────────────────────────────
blockers = empty_chunks + no_prefix + malformed_prefix + body_empty + incomplete blockers = empty_chunks + no_prefix + malformed_prefix + body_empty
warnings = too_short + too_long + incomplete_math + broken_tables warnings = too_short + too_long + incomplete + incomplete_math + broken_tables
verdict = "blocked" if blockers else ("warnings_only" if (warnings or duplicates) else "ok") verdict = "blocked" if blockers else ("warnings_only" if (warnings or duplicates) else "ok")
@@ -418,18 +432,16 @@ def verify_stem(stem: str, project_root: Path, min_chars: int, max_chars: int) -
print(f" python ingestion/ingest.py --stem {stem}") print(f" python ingestion/ingest.py --stem {stem}")
if too_short or too_long: if too_short or too_long:
print() print()
print(f" Per ottimizzare prima:") print(f" Per ottimizzare: correggi il sorgente .md e rigenera con --force")
print(f" python chunks/fix_chunks.py --stem {stem} --dry-run")
print(f" python chunks/fix_chunks.py --stem {stem}")
else: else:
print(f" 🔴 {len(blockers)} problemi bloccanti — correggi prima di procedere:") print(f" 🔴 {len(blockers)} problemi bloccanti — correggi prima di procedere:")
if empty_chunks or body_empty: if empty_chunks or body_empty:
print(f" • chunk vuoti/senza corpo → controlla sources/{stem}/auto/{stem}_clean.md") print(f" • chunk vuoti/senza corpo → controlla sources/{stem}/auto/{stem}_clean.md")
if no_prefix or malformed_prefix: if no_prefix or malformed_prefix:
print(f" • prefisso mancante/malformato → controlla gli heading in {stem}_clean.md") print(f" • prefisso mancante/malformato → controlla gli heading in sources/{stem}.md")
if incomplete: if incomplete:
print(f" • frasi spezzate → python chunks/fix_chunks.py --stem {stem}") print(f" • frasi spezzate → correggi il sorgente e rigenera con --force")
print() print()
print(f" Dopo le correzioni:") print(f" Dopo le correzioni:")
print(f" python chunks/chunker.py --stem {stem} --force") print(f" python chunks/chunker.py --stem {stem} --force")