step-4: add revise.py, step4-review skill, README update
- revise.py: automatic pre-processing (ALL-CAPS→##, numbered sections→###, TOC removal, broken paragraph merging, whitespace normalization); supports N. and Na. numbering patterns; universal heuristics - .claude/commands/step4-review.md: Claude Code skill for qualitative review of clean.md (🔴/🟡/🟢 report + interactive fixes) - README: document step-4 workflow with revise.py and /step4-review - .gitignore: exclude step-4/*/ and step-4/revision_log.md
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
---
|
||||
description: Revisione qualitativa del clean.md dopo il pre-processing automatico (step 4). Trova artefatti residui, paragrafi spezzati e header errati, poi propone le correzioni.
|
||||
allowed-tools: Read Bash Grep Edit
|
||||
argument-hint: <stem>
|
||||
---
|
||||
|
||||
Esegui la revisione qualitativa di `step-4/$ARGUMENTS/clean.md`.
|
||||
|
||||
**Profilo struttura:**
|
||||
!`cat step-4/$ARGUMENTS/structure_profile.json 2>/dev/null || echo "ERRORE: step-4/$ARGUMENTS/structure_profile.json non trovato — verifica che lo stem sia corretto"`
|
||||
|
||||
**Statistiche file:**
|
||||
!`wc -l step-4/$ARGUMENTS/clean.md 2>/dev/null && grep -c "^## " step-4/$ARGUMENTS/clean.md 2>/dev/null | xargs -I{} echo "## headers: {}" && grep -c "^### " step-4/$ARGUMENTS/clean.md 2>/dev/null | xargs -I{} echo "### headers: {}"`
|
||||
|
||||
---
|
||||
|
||||
Analizza `step-4/$ARGUMENTS/clean.md` seguendo questi passi. Usa Grep e Read per raccogliere i casi concreti prima di riportarli.
|
||||
|
||||
## Passo 1 — Righe orfane in corpo testo
|
||||
|
||||
Cerca blocchi di testo standalone (separati da righe vuote, non header `#`) più corti di 80 caratteri che sembrino artefatti da PDF:
|
||||
- Firme o nomi di persone isolati (tutto maiuscolo o iniziali maiuscole seguite da punto)
|
||||
- Date o luoghi isolati
|
||||
- Titoli del documento ripetuti nel corpo
|
||||
- Numeri di pagina isolati
|
||||
- Riferimenti bibliografici fuori contesto
|
||||
|
||||
Per ogni caso trovato: riporta numero di riga e testo.
|
||||
|
||||
## Passo 2 — Paragrafi spezzati residui
|
||||
|
||||
Cerca paragrafi che finiscono senza punteggiatura di fine frase (`.?!»)`), escludendo quelli che terminano con ` -` (interruzione stilistica intenzionale).
|
||||
|
||||
Riporta i primi 5 casi con numero di riga e la fine del testo.
|
||||
|
||||
## Passo 3 — Header sospetti
|
||||
|
||||
Controlla i `##` e `###`:
|
||||
- `### N.` con solo numero e punto → **corretto**, non segnalare
|
||||
- `##` o `###` con contenuto ALL-CAPS non convertito in sentence-case → segnalare
|
||||
- `##` o `###` duplicati (stesso titolo che appare due volte) → segnalare
|
||||
- `##` con testo molto lungo (> 80 char) → segnalare come sospetto
|
||||
|
||||
## Passo 4 — Sezioni quasi vuote
|
||||
|
||||
Cerca `### ` header seguiti da un blocco di testo inferiore a 60 caratteri (sezione priva di contenuto utile per il RAG). Riporta i casi.
|
||||
|
||||
## Report finale
|
||||
|
||||
```
|
||||
🔴 BLOCCANTI (compromettono il chunking o la qualità del RAG)
|
||||
...
|
||||
|
||||
🟡 MINORI (artefatti visibili ma non bloccanti)
|
||||
...
|
||||
|
||||
🟢 OK
|
||||
...
|
||||
```
|
||||
|
||||
Chiedi all'utente: "Vuoi che applichi le correzioni per i problemi 🔴? E per i 🟡?"
|
||||
Applica solo le correzioni esplicitamente approvate.
|
||||
@@ -33,3 +33,7 @@ step-2/*/
|
||||
# Output step-3 — profilo struttura generato da detect_structure.py
|
||||
step-3/*/
|
||||
|
||||
# Output step-4 — MD revisionato e log generati da revise.py
|
||||
step-4/*/
|
||||
step-4/revision_log.md
|
||||
|
||||
|
||||
@@ -285,19 +285,59 @@ Livello 0 — testo piatto
|
||||
|
||||
### Step 4 — Revisione manuale
|
||||
|
||||
**Tipo:** manuale
|
||||
**Input:** `processed/documento/raw.md` + profilo struttura
|
||||
**Output:** `processed/documento/clean.md`
|
||||
**Tool:** il tuo editor di testo
|
||||
**Tipo:** manuale (con pre-processing automatico)
|
||||
**Input:** `step-3/<stem>/clean.md` + `step-3/<stem>/structure_profile.json`
|
||||
**Output:** `step-4/<stem>/clean.md` — MD revisionato
|
||||
**Script:** `step-4/revise.py`
|
||||
|
||||
> Questo è lo step più importante dell'intera pipeline.
|
||||
> La qualità del RAG dipende da questo step più di qualsiasi
|
||||
> parametro tecnico o scelta di modello.
|
||||
|
||||
Apri `clean.md` nel tuo editor e lavora sezione per sezione,
|
||||
guidato dal report dello step 1 e dal profilo dello step 3.
|
||||
#### Pre-processing automatico
|
||||
|
||||
**Struttura target del MD pulito:**
|
||||
Prima di qualsiasi revisione manuale, esegui lo script di revisione automatica:
|
||||
|
||||
```bash
|
||||
python step-4/revise.py --stem documento
|
||||
```
|
||||
|
||||
Lo script applica le seguenti trasformazioni euristiche, valide per qualsiasi documento:
|
||||
|
||||
| Trasformazione | Descrizione |
|
||||
|---|---|
|
||||
| Rimozione TOC | Righe che iniziano con `INDICE`, `INDEX`, `CONTENTS`, ecc. |
|
||||
| ALL-CAPS → `##` | Righe standalone in maiuscolo convertite in header section-case |
|
||||
| `N. testo` → `### N.` | Sezioni numerate (con 1+ spazio dopo il punto) convertite in h3 |
|
||||
| Unione paragrafi | Blocchi spezzati da salti pagina PDF uniti automaticamente |
|
||||
| Whitespace | Spazi multipli normalizzati, righe vuote ridotte |
|
||||
|
||||
Il profilo strutturale aggiornato viene salvato in `step-4/<stem>/structure_profile.json`.
|
||||
|
||||
#### Revisione assistita da Claude Code
|
||||
|
||||
Dopo il pre-processing, usa la skill integrata per una revisione qualitativa:
|
||||
|
||||
```
|
||||
/step4-review documento
|
||||
```
|
||||
|
||||
La skill analizza `step-4/<stem>/clean.md` e produce un report strutturato:
|
||||
|
||||
```
|
||||
🔴 BLOCCANTI — problemi che compromettono il chunking
|
||||
🟡 MINORI — artefatti visibili ma non bloccanti
|
||||
🟢 OK — categorie senza problemi
|
||||
```
|
||||
|
||||
Poi propone le correzioni e le applica solo su tua approvazione.
|
||||
|
||||
#### Revisione manuale residua
|
||||
|
||||
Apri `step-4/<stem>/clean.md` nel tuo editor e verifica quanto segnalato
|
||||
dalla skill. Il criterio di qualità rimane lo stesso:
|
||||
|
||||
**Struttura target:**
|
||||
|
||||
```markdown
|
||||
# Titolo del documento
|
||||
@@ -309,31 +349,8 @@ guidato dal report dello step 1 e dal profilo dello step 3.
|
||||
Testo fluente, frasi complete, nessun artefatto.
|
||||
Ogni paragrafo è semanticamente autonomo.
|
||||
Una riga vuota separa le sezioni.
|
||||
|
||||
### Sottosezione successiva
|
||||
|
||||
Testo della sottosezione successiva...
|
||||
```
|
||||
|
||||
**Cosa rimuovi:**
|
||||
|
||||
- Numeri di pagina isolati (una riga con solo `42`)
|
||||
- Intestazioni e piè di pagina ripetitivi
|
||||
- Righe vuote multiple — massimo una tra sezioni
|
||||
|
||||
**Cosa correggi:**
|
||||
|
||||
- Sillabazioni residue: `estra-\ntto` → `estratto`
|
||||
- Righe spezzate artificialmente: unisci le righe
|
||||
che appartengono alla stessa frase
|
||||
- Parole unite per errore: `ilbene` → `il bene`
|
||||
|
||||
**Cosa sistemi:**
|
||||
|
||||
- Ogni sezione ha il suo titolo al livello corretto
|
||||
- Nessuna sezione manca di `###` se ne aveva uno nell'originale
|
||||
- Nessun titolo è duplicato o malformato
|
||||
|
||||
**Il criterio di qualità:**
|
||||
Leggi ogni sezione ad alta voce. Se suona naturale e fluente è corretta.
|
||||
Se si interrompe c'è una riga spezzata. Se suona ripetitiva c'è un artefatto.
|
||||
@@ -341,11 +358,11 @@ Se si interrompe c'è una riga spezzata. Se suona ripetitiva c'è un artefatto.
|
||||
**Traccia il lavoro:**
|
||||
|
||||
```bash
|
||||
# Aggiorna notes/revision_log.md con ogni correzione rilevante
|
||||
# Commita spesso per poter tornare indietro
|
||||
# step-4/revision_log.md viene aggiornato automaticamente da revise.py
|
||||
# Commita dopo la revisione manuale
|
||||
|
||||
git add processed/documento/clean.md notes/revision_log.md
|
||||
git commit -m "revisione: sezioni 1-50 corrette"
|
||||
git add step-4/revision_log.md
|
||||
git commit -m "step-4: revisione nietzsche completata"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,433 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Step 4 — Revisione automatica del Markdown
|
||||
|
||||
Trasforma clean.md da step-3 rivelando la struttura latente del documento.
|
||||
Le trasformazioni sono euristiche universali che funzionano su qualsiasi PDF:
|
||||
|
||||
- Normalizza whitespace multiplo (artefatto PDF)
|
||||
- Riduce righe vuote multiple
|
||||
- Rimuove marcatori **bold** nelle intestazioni esistenti
|
||||
- Converte righe ALL-CAPS standalone → ## header (euristico, qualsiasi lingua)
|
||||
- Converte sezioni numerate "N. testo" → ### N. (qualsiasi numerazione)
|
||||
- Rimuove blocchi TOC (righe che iniziano con parole-chiave indice)
|
||||
|
||||
Per ogni documento viene ricalcolato il profilo strutturale: il livello può
|
||||
salire (es. livello 1 → 3) se le strutture latenti vengono rilevate.
|
||||
|
||||
Output in step-4/<stem>/:
|
||||
raw.md — copia da step-3 (non modificare mai)
|
||||
clean.md — MD revisionato
|
||||
structure_profile.json — profilo aggiornato dopo la revisione
|
||||
|
||||
Uso:
|
||||
python step-4/revise.py # tutti i documenti in step-3/
|
||||
python step-4/revise.py --stem nietzsche # un solo documento
|
||||
python step-4/revise.py --force # riesegui anche se già presente
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
# Riusa la funzione analyze() già scritta nello step 3
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "step-3"))
|
||||
from detect_structure import analyze # noqa: E402
|
||||
|
||||
|
||||
# ─── Costanti ─────────────────────────────────────────────────────────────────
|
||||
|
||||
# Parole-chiave che identificano blocchi TOC (da rimuovere)
|
||||
_TOC_KEYWORDS = frozenset([
|
||||
"indice", "index", "contents", "table of contents",
|
||||
"sommario", "inhaltsverzeichnis", "inhalt",
|
||||
])
|
||||
|
||||
# Preposizioni/articoli da non capitalizzare nel title-case
|
||||
_STOP_IT_EN = frozenset([
|
||||
# italiano
|
||||
"di", "del", "della", "dei", "delle", "da", "in", "e", "il", "la",
|
||||
"lo", "le", "gli", "un", "una", "per", "a", "al", "alla", "ai",
|
||||
"alle", "con", "su", "sul", "sulla", "che", "o",
|
||||
# inglese
|
||||
"of", "the", "a", "an", "and", "or", "but", "in", "on", "at",
|
||||
"to", "for", "with", "by", "from", "as",
|
||||
])
|
||||
|
||||
# Ordinali italiani → romani (per titoli come "CAPITOLO PRIMO")
|
||||
_ORDINALS_IT = {
|
||||
"PRIMO": "I", "SECONDO": "II", "TERZO": "III", "QUARTO": "IV",
|
||||
"QUINTO": "V", "SESTO": "VI", "SETTIMO": "VII", "OTTAVO": "VIII",
|
||||
"NONO": "IX", "DECIMO": "X",
|
||||
}
|
||||
|
||||
# Ordinali inglesi → arabici (per "CHAPTER ONE")
|
||||
_ORDINALS_EN = {
|
||||
"ONE": "1", "TWO": "2", "THREE": "3", "FOUR": "4", "FIVE": "5",
|
||||
"SIX": "6", "SEVEN": "7", "EIGHT": "8", "NINE": "9", "TEN": "10",
|
||||
}
|
||||
|
||||
|
||||
# ─── Utilità ──────────────────────────────────────────────────────────────────
|
||||
|
||||
def _sentence_case(s: str) -> str:
|
||||
"""
|
||||
Sentence-case: prima lettera maiuscola, resto minuscolo.
|
||||
Corretto per l'italiano e accettabile per l'inglese accademico.
|
||||
"""
|
||||
if not s:
|
||||
return s
|
||||
lower = s.lower()
|
||||
return lower[0].upper() + lower[1:]
|
||||
|
||||
|
||||
def _is_allcaps_line(line: str) -> bool:
|
||||
"""
|
||||
True se la riga è una candidata per conversione a ## header.
|
||||
Criterio: tutti i caratteri alfabetici sono maiuscoli, lunghezza >= 3.
|
||||
"""
|
||||
stripped = line.strip()
|
||||
letters = [c for c in stripped if c.isalpha()]
|
||||
return (
|
||||
len(letters) >= 3
|
||||
and all(c.isupper() for c in letters)
|
||||
and not stripped.startswith("#")
|
||||
)
|
||||
|
||||
|
||||
def _allcaps_to_header(raw_line: str) -> str:
|
||||
"""
|
||||
Converte una riga ALL-CAPS in un ## header title-case.
|
||||
Riconosce pattern specifici (CAPITOLO ORDINE, CHAPTER N) come bonus,
|
||||
ma funziona in modalità generica su qualsiasi testo.
|
||||
"""
|
||||
text = raw_line.strip().rstrip('.').rstrip('?').strip()
|
||||
|
||||
# ── Pattern italiano: "CAPITOLO PRIMO. TITOLO DEL CAPITOLO"
|
||||
_ORD_IT_PAT = "|".join(_ORDINALS_IT.keys())
|
||||
m = re.match(rf'^CAPITOLO ({_ORD_IT_PAT})\. (.+)', text)
|
||||
if m:
|
||||
roman = _ORDINALS_IT[m.group(1)]
|
||||
titolo = m.group(2).rstrip('.').rstrip('?').strip()
|
||||
return f"## Capitolo {roman} — {_sentence_case(titolo)}"
|
||||
|
||||
# ── Pattern inglese: "CHAPTER ONE. TITLE" o "CHAPTER 1. TITLE"
|
||||
_ORD_EN_PAT = "|".join(_ORDINALS_EN.keys())
|
||||
m = re.match(rf'^CHAPTER ({_ORD_EN_PAT}|\d+)\.? (.+)', text)
|
||||
if m:
|
||||
n = _ORDINALS_EN.get(m.group(1), m.group(1))
|
||||
titolo = m.group(2).rstrip('.').rstrip('?').strip()
|
||||
return f"## Chapter {n} — {_sentence_case(titolo)}"
|
||||
|
||||
# ── Pattern generico con numerazione romana o arabica nel prefisso
|
||||
m = re.match(r'^([IVXLCDM]+|[0-9]+)\. (.+)', text)
|
||||
if m:
|
||||
n = m.group(1)
|
||||
titolo = m.group(2).rstrip('.').strip()
|
||||
return f"## {n}. {_sentence_case(titolo)}"
|
||||
|
||||
# ── Caso generico: tutto maiuscolo senza pattern riconoscibile
|
||||
return f"## {_sentence_case(text)}"
|
||||
|
||||
|
||||
def _is_toc_line(line: str) -> bool:
|
||||
"""True se la riga è l'intestazione di un blocco indice/TOC."""
|
||||
first_word = line.strip().split('.')[0].strip().lower()
|
||||
return first_word in _TOC_KEYWORDS
|
||||
|
||||
|
||||
# ─── Trasformazioni ────────────────────────────────────────────────────────────
|
||||
|
||||
def apply_transforms(text: str) -> tuple[str, dict]:
|
||||
"""
|
||||
Applica tutte le trasformazioni strutturali al testo MD.
|
||||
Restituisce (testo_modificato, statistiche).
|
||||
"""
|
||||
stats = {
|
||||
"toc_rimosso": False,
|
||||
"n_header_allcaps": 0,
|
||||
"n_sezioni_numerate": 0,
|
||||
"n_paragrafi_uniti": 0,
|
||||
}
|
||||
|
||||
# ── 1. Rimuovi marcatori **bold** nelle intestazioni esistenti
|
||||
# ## **Titolo** → ## Titolo
|
||||
text = re.sub(
|
||||
r'^(#{1,6})\s+\*\*(.+?)\*\*\s*$',
|
||||
r'\1 \2',
|
||||
text, flags=re.MULTILINE,
|
||||
)
|
||||
|
||||
# ── 1b. Normalizza header esistenti con contenuto ALL-CAPS → sentence-case
|
||||
# ## AL DI LA' DEL BENE E DEL MALE → ## Al di la' del bene e del male
|
||||
def _norm_allcaps_header(m: re.Match) -> str:
|
||||
hashes = m.group(1)
|
||||
content = m.group(2).strip()
|
||||
letters = [c for c in content if c.isalpha()]
|
||||
if letters and all(c.isupper() for c in letters):
|
||||
return f"{hashes} {_sentence_case(content)}"
|
||||
return m.group(0)
|
||||
|
||||
text = re.sub(
|
||||
r'^(#{1,6}) (.+)$',
|
||||
_norm_allcaps_header,
|
||||
text, flags=re.MULTILINE,
|
||||
)
|
||||
|
||||
# ── 2. Rimuovi blocco TOC (riga indice + contenuto inline sulla stessa riga)
|
||||
# "INDICE. Capitolo 1 Capitolo 2 ..." → rimossa
|
||||
lines = text.split('\n')
|
||||
new_lines = []
|
||||
for line in lines:
|
||||
if _is_toc_line(line):
|
||||
stats["toc_rimosso"] = True
|
||||
else:
|
||||
new_lines.append(line)
|
||||
text = '\n'.join(new_lines)
|
||||
|
||||
# ── 3. Converti righe ALL-CAPS standalone → ## header
|
||||
# Una riga è "standalone" se è preceduta/seguita da riga vuota
|
||||
# oppure si trova all'inizio/fine del documento.
|
||||
blocks = text.split('\n\n')
|
||||
new_blocks = []
|
||||
for block in blocks:
|
||||
stripped = block.strip()
|
||||
# Blocco standalone = un'unica riga (nessun \n interno rilevante)
|
||||
if '\n' not in stripped and _is_allcaps_line(stripped):
|
||||
new_blocks.append(_allcaps_to_header(stripped))
|
||||
stats["n_header_allcaps"] += 1
|
||||
else:
|
||||
# Controlla riga per riga per righe ALL-CAPS seguite da altri contenuti
|
||||
sub_lines = block.split('\n')
|
||||
converted = []
|
||||
for ln in sub_lines:
|
||||
if _is_allcaps_line(ln) and len(ln.strip()) > 3:
|
||||
converted.append(_allcaps_to_header(ln))
|
||||
stats["n_header_allcaps"] += 1
|
||||
else:
|
||||
converted.append(ln)
|
||||
new_blocks.append('\n'.join(converted))
|
||||
text = '\n\n'.join(new_blocks)
|
||||
|
||||
# ── 4. Converti sezioni numerate "N. testo" → "### N.\n\ntesto"
|
||||
# Riconosce: "1. Testo", "42. Testo" (due o più spazi dopo il punto)
|
||||
def _num_repl(m: re.Match) -> str:
|
||||
num = m.group(1)
|
||||
testo = m.group(2).strip()
|
||||
stats["n_sezioni_numerate"] += 1
|
||||
return f"### {num}.\n\n{testo}"
|
||||
|
||||
# Pattern standard: "1. testo" o "1. testo"
|
||||
text = re.sub(
|
||||
r'^(\d+)\.\s+(.+)$',
|
||||
_num_repl,
|
||||
text, flags=re.MULTILINE,
|
||||
)
|
||||
|
||||
# Pattern con lettera-suffisso: "65 a. testo" o "65a. testo"
|
||||
def _num_letter_repl(m: re.Match) -> str:
|
||||
num = m.group(1) + m.group(2)
|
||||
testo = m.group(3).strip()
|
||||
stats["n_sezioni_numerate"] += 1
|
||||
return f"### {num}.\n\n{testo}"
|
||||
|
||||
text = re.sub(
|
||||
r'^(\d+)\s*([a-z])\.\s+(.+)$',
|
||||
_num_letter_repl,
|
||||
text, flags=re.MULTILINE,
|
||||
)
|
||||
|
||||
# ── 5. Unisci paragrafi spezzati da salti pagina PDF
|
||||
# Criterio: blocco A non finisce con punteggiatura di fine frase,
|
||||
# blocco B non inizia con maiuscola "di sezione" né è un header.
|
||||
# Unione sicura: mai attraverso confini ###/##.
|
||||
_SENTENCE_END = set('.?!»)\'"')
|
||||
blocks = text.split('\n\n')
|
||||
merged = []
|
||||
i = 0
|
||||
while i < len(blocks):
|
||||
b = blocks[i]
|
||||
stripped = b.strip()
|
||||
# Prova a unire con il successivo se la frase è spezzata
|
||||
while (
|
||||
i + 1 < len(blocks)
|
||||
and stripped
|
||||
and not stripped.startswith('#')
|
||||
and stripped[-1] not in _SENTENCE_END
|
||||
):
|
||||
nxt = blocks[i + 1].strip()
|
||||
# Non unire se il successivo è un header o è vuoto
|
||||
if not nxt or nxt.startswith('#'):
|
||||
break
|
||||
# Non unire se il successivo inizia con una cifra seguita da punto
|
||||
# (sarebbe l'inizio di un nuovo aforisma non ancora convertito)
|
||||
if re.match(r'^\d+\.', nxt):
|
||||
break
|
||||
b = stripped + ' ' + nxt
|
||||
stripped = b.strip()
|
||||
stats["n_paragrafi_uniti"] += 1
|
||||
i += 1
|
||||
merged.append(b)
|
||||
i += 1
|
||||
text = '\n\n'.join(merged)
|
||||
|
||||
# ── 6. Normalizza whitespace multiplo interno alle righe
|
||||
# "parola parola" → "parola parola" (inclusi gli header)
|
||||
lines = text.split('\n')
|
||||
normalized = []
|
||||
for line in lines:
|
||||
if not line.strip():
|
||||
normalized.append(line)
|
||||
else:
|
||||
normalized.append(re.sub(r' +', ' ', line))
|
||||
text = '\n'.join(normalized)
|
||||
|
||||
# ── 7. Riduci righe vuote multiple a doppie
|
||||
text = re.sub(r'\n{3,}', '\n\n', text)
|
||||
|
||||
return text, stats
|
||||
|
||||
|
||||
# ─── Aggiornamento revision log ────────────────────────────────────────────────
|
||||
|
||||
def update_revision_log(
|
||||
log_path: Path,
|
||||
stem: str,
|
||||
profile_before: dict,
|
||||
profile_after: dict,
|
||||
t_stats: dict,
|
||||
) -> None:
|
||||
header_exists = log_path.exists() and log_path.stat().st_size > 0
|
||||
|
||||
avv = profile_after.get("avvertenze", [])
|
||||
avv_str = "; ".join(avv) if avv else "nessuna"
|
||||
|
||||
entry = f"""
|
||||
## {stem} — {date.today().isoformat()}
|
||||
|
||||
**Trasformazioni automatiche:**
|
||||
- Normalizzazione whitespace multiplo e righe vuote
|
||||
- Blocco TOC rimosso: {'sì' if t_stats['toc_rimosso'] else 'no'}
|
||||
- Righe ALL-CAPS → ## header: {t_stats['n_header_allcaps']}
|
||||
- Sezioni numerate → ### header: {t_stats['n_sezioni_numerate']}
|
||||
- Paragrafi uniti (salti pagina PDF): {t_stats['n_paragrafi_uniti']}
|
||||
- Livello struttura: {profile_before.get('livello_struttura', '?')} → {profile_after.get('livello_struttura', '?')}
|
||||
|
||||
**Avvertenze residue:** {avv_str}
|
||||
|
||||
**Revisioni manuali pendenti:**
|
||||
- [ ] Verificare conversioni ALL-CAPS errate
|
||||
- [ ] Controllare sezioni troppo corte o troppo lunghe
|
||||
"""
|
||||
|
||||
if not header_exists:
|
||||
log_path.write_text("# Revision log\n" + entry, encoding="utf-8")
|
||||
else:
|
||||
existing = log_path.read_text(encoding="utf-8")
|
||||
log_path.write_text(existing + entry, encoding="utf-8")
|
||||
|
||||
|
||||
# ─── Per-document processing ─────────────────────────────────────────────────
|
||||
|
||||
def process_stem(stem: str, project_root: Path, force: bool) -> bool:
|
||||
src_dir = project_root / "step-3" / stem
|
||||
out_dir = project_root / "step-4" / stem
|
||||
raw_src = src_dir / "raw.md"
|
||||
clean_src = src_dir / "clean.md"
|
||||
profile_src = src_dir / "structure_profile.json"
|
||||
clean_out = out_dir / "clean.md"
|
||||
profile_out = out_dir / "structure_profile.json"
|
||||
|
||||
print(f"\nDocumento: {stem}")
|
||||
|
||||
if not clean_src.exists():
|
||||
print(f" ✗ clean.md non trovato in step-3/{stem}/ — skip")
|
||||
return False
|
||||
|
||||
if clean_out.exists() and not force:
|
||||
print(f" ⚠️ clean.md già presente — skip")
|
||||
print(f" (usa --force per rieseguire)")
|
||||
return True
|
||||
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Copia raw.md immutabile (riferimento)
|
||||
if raw_src.exists():
|
||||
shutil.copy2(raw_src, out_dir / "raw.md")
|
||||
print(f" Copiato raw.md da step-3/{stem}/")
|
||||
|
||||
# Leggi profilo step-3 (per confronto nel report)
|
||||
profile_before: dict = {}
|
||||
if profile_src.exists():
|
||||
profile_before = json.loads(profile_src.read_text(encoding="utf-8"))
|
||||
|
||||
# Applica trasformazioni
|
||||
print(f" Applicazione trasformazioni strutturali...")
|
||||
text = clean_src.read_text(encoding="utf-8")
|
||||
text_revised, t_stats = apply_transforms(text)
|
||||
|
||||
# Salva clean.md revisionato
|
||||
clean_out.write_text(text_revised, encoding="utf-8")
|
||||
|
||||
# Ricalcola profilo sul nuovo clean.md
|
||||
profile_after = analyze(clean_out)
|
||||
profile_out.write_text(
|
||||
json.dumps(profile_after, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
# Report
|
||||
lv_b = profile_before.get("livello_struttura", "?")
|
||||
lv_a = profile_after["livello_struttura"]
|
||||
_STRAT = {3: "h3_aware", 2: "h2_paragraph_split", 1: "paragraph", 0: "sliding_window"}
|
||||
print(f" ✅ Livello struttura: {lv_b} → {lv_a} ({_STRAT.get(lv_a, '?')})")
|
||||
print(f" h2: {profile_before.get('n_h2','?')} → {profile_after['n_h2']}")
|
||||
print(f" h3: {profile_before.get('n_h3','?')} → {profile_after['n_h3']}")
|
||||
print(f" TOC rimosso: {'sì' if t_stats['toc_rimosso'] else 'no'}")
|
||||
print(f" Righe ALL-CAPS → ##: {t_stats['n_header_allcaps']}")
|
||||
print(f" Sezioni numerate → ###: {t_stats['n_sezioni_numerate']}")
|
||||
print(f" Paragrafi uniti (salti pagina): {t_stats['n_paragrafi_uniti']}")
|
||||
for w in profile_after["avvertenze"]:
|
||||
print(f" ⚠️ {w}")
|
||||
|
||||
# Aggiorna revision log (direttamente in step-4/, non in sottocartella)
|
||||
log_path = project_root / "step-4" / "revision_log.md"
|
||||
update_revision_log(log_path, stem, profile_before, profile_after, t_stats)
|
||||
print(f" ✅ step-4/revision_log.md aggiornato")
|
||||
print(f" ✅ structure_profile.json salvato")
|
||||
return True
|
||||
|
||||
|
||||
# ─── Entry point ─────────────────────────────────────────────────────────────
|
||||
|
||||
if __name__ == "__main__":
|
||||
project_root = Path(__file__).parent.parent
|
||||
|
||||
parser = argparse.ArgumentParser(description="Step 4 — Revisione automatica Markdown")
|
||||
parser.add_argument("--stem", help="Nome del documento (sottocartella di step-3/)")
|
||||
parser.add_argument("--force", action="store_true", help="Riesegui anche se già presente")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.stem:
|
||||
stems = [args.stem]
|
||||
else:
|
||||
step3_dir = project_root / "step-3"
|
||||
if not step3_dir.exists():
|
||||
print(f"Errore: cartella step-3/ non trovata in {project_root}")
|
||||
sys.exit(1)
|
||||
stems = sorted(p.name for p in step3_dir.iterdir() if p.is_dir())
|
||||
if not stems:
|
||||
print(f"Errore: nessun documento trovato in step-3/")
|
||||
sys.exit(1)
|
||||
|
||||
results = [process_stem(s, project_root, args.force) for s in stems]
|
||||
|
||||
ok = sum(results)
|
||||
total = len(results)
|
||||
print(f"\n{'✅' if all(results) else '⚠️ '} {ok}/{total} documenti revisionati")
|
||||
|
||||
sys.exit(0 if all(results) else 1)
|
||||
Reference in New Issue
Block a user