diff --git a/CLAUDE.md b/CLAUDE.md
index f605ee1..79f3111 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -56,7 +56,8 @@ entrambi rimossi: ora l'unico programma è questo, con GUI e indicizzazione inte
che contiene `CLAUDE.md`), indipendentemente da dove si trova il file `.xls`/`.xml`
di input scelto dall'utente.
3. **Crea cartella raw** — cerca nell'indice il percorso CAD di ogni componente del CSV
- (`TrovaPercorsiCad`) e lo copia in `raw/` (`CopiaFileTrovati`).
+ (`TrovaPercorsiCad`) e lo copia in `raw/` (`CopiaFileTrovati`, parallela — vedi
+ `docs/copia-raw-parallela.md`).
4. **Trova componenti referenziati** — legge euristicamente i metadati binari dei `.asm`
già in `raw/` (`AssemblyComponentScanner`) per risolvere ricorsivamente i
sotto-assiemi/parti che contengono (`RisolviAlberoComponenti`), copiandoli anch'essi
diff --git a/CadSearchGui/Form1.cs b/CadSearchGui/Form1.cs
index 7952ef9..1c7f444 100644
--- a/CadSearchGui/Form1.cs
+++ b/CadSearchGui/Form1.cs
@@ -954,23 +954,25 @@ public partial class Form1 : Form
}
/// Copia i file trovati nella cartella "raw" indicata, creandola se necessario.
- /// Ritorna il numero di file copiati con successo.
+ /// Ritorna il numero di file copiati con successo. Parallelizzata (grado limitato): il
+ /// collo di bottiglia è la latenza di rete verso la share, non la CPU, quindi poche copie
+ /// concorrenti bastano a sovrapporre le attese senza saturare la share.
internal static int CopiaFileTrovati(IEnumerable percorsi, string cartellaRaw)
{
+ Directory.CreateDirectory(cartellaRaw);
int copiati = 0;
- foreach (var percorso in percorsi)
+ Parallel.ForEach(percorsi, new ParallelOptions { MaxDegreeOfParallelism = 6 }, percorso =>
{
try
{
- Directory.CreateDirectory(cartellaRaw);
File.Copy(percorso, Path.Combine(cartellaRaw, Path.GetFileName(percorso)), overwrite: true);
- copiati++;
+ Interlocked.Increment(ref copiati);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
// ponytail: file non raggiungibile (share offline, permessi) -> lo si salta, non blocca gli altri
}
- }
+ });
return copiati;
}
diff --git a/docs/architettura.md b/docs/architettura.md
new file mode 100644
index 0000000..949a874
--- /dev/null
+++ b/docs/architettura.md
@@ -0,0 +1,146 @@
+# Architettura di CadSearchGui
+
+Panoramica di tutti i file sorgente e di come collaborano. Per il "come si usa" vedi il
+`README.md` in root; per il formato binario `.asm` vedi `docs/formato-asm.md`; per la
+parallelizzazione della copia in `raw/` vedi `docs/copia-raw-parallela.md`.
+
+## Vista d'insieme
+
+Un solo eseguibile WinForms (nessuna CLI, nessun processo esterno): indicizza i file CAD di
+una share di rete in un DB SQLite locale, poi guida l'utente in un wizard a 5 fasi che, a
+partire da una distinta base esportata dal gestionale, raccoglie in locale (`raw/`) tutti i
+file CAD dei componenti e dei loro sotto-assiemi, e verifica/raccoglie gli STEP corrispondenti
+(`step_finali/`).
+
+```
+Distinta (.xls/.xml) → CSV (componente;descrizione) → raw/ (CAD copiati dall'indice)
+ │
+ scansione .asm ─────┘ (sotto-assiemi/parti mancanti)
+ │
+ step/ (esportati a mano da Creo) → step_finali/ (solo i codici distinta)
+```
+
+## Entry point
+
+- **Program.cs** — `Main()` standard WinForms: `Application.Run(new Form1())`. Nessuna logica.
+
+## Finestra principale (wizard)
+
+- **Form1.cs** — tutta la UI è costruita in codice (`BuildUi`/`BuildFase*`), non con il
+ designer visuale. È un wizard: `_panelFasi[]` contiene un `Panel` per fase, uno solo
+ visibile alla volta (`VaiAllaFase`); si avanza con "Avanti" solo se `AvantiConsentito`
+ ritorna vero per la fase corrente. `Form1.Designer.cs` contiene solo lo scheletro minimo
+ generato dal template (`InitializeComponent`/`Dispose`), non è usato per costruire i
+ controlli.
+
+ Le 5 fasi e i metodi principali dietro ciascuna:
+
+ | # | Fase | Bottone → metodo | Cosa fa |
+ |---|---|---|---|
+ | 1 | Database | "Aggiorna indice" → `Indexer.ScanAsync` | Crea/aggiorna `cad_index.db` scansionando la share |
+ | 2 | Importa Distinta Base | selezione file → `ImportaDistinta` → `DistintaBaseImporter.ConvertiInCsv` | Xls/Xml gestionale → CSV componente;descrizione, scritto nella root del repo (`TrovaRadiceRepo`) |
+ | 3 | Crea cartella raw | "Crea cartella raw" → `CreaCartellaRaw` | `TrovaPercorsiCad` + `CopiaFileTrovati` (parallela) → `raw/` |
+ | 4 | Trova componenti referenziati | "Trova componenti referenziati" → `TrovaPrtReferenziati` → `RisolviAlberoComponenti` | Legge i `.asm` in `raw/` con `AssemblyComponentScanner`, risolve ricorsivamente sotto-assiemi/parti nell'indice, li copia anch'essi in `raw/` |
+ | 5 | Verifica completezza e STEP | "Verifica completezza assiemi" → `VerificaCompletezzaAssiemi`; "Crea cartella step_finali" → `GeneraReportStep` | Completezza ricorsiva sui percorsi in indice; confronto con `step/` → `step_finali/`. All'ingresso nella fase, `AggiornaAnteprimaAsm` mostra le icone trascinabili dei `.asm` della distinta già in `raw/` |
+
+ Stato condiviso tra le fasi (campi privati di `Form1`): `_dbPath`, `_percorsoInputCorrente`,
+ `_righeCorrenti` (righe del CSV), `_ultimaVerifica` (cache percorsi CAD), `_ultimaCartellaRaw`.
+
+ Altre responsabilità in Form1.cs:
+ - `TrovaPercorsiCad` / `CaricaCodiciNoti` — query sul DB (`files_ultima_versione`), la
+ seconda carica tutti i codici prt/asm noti in un colpo solo (usata sia per risolvere i
+ componenti sia da `AssemblyComponentScanner` per validare i token generici).
+ - `CopiaFileTrovati` — copia (in parallelo, grado 6) i file trovati in `raw/`, saltando
+ silenziosamente quelli irraggiungibili (share offline/permessi).
+ - `PulisciRawEStep` — svuota `raw/`, `step/`, `step_finali/` (manutenzione, richiede conferma).
+ - `InizializzaDatabase` / `ScegliDatabase` / `TrovaRadiceRepo` — localizzazione del DB e
+ della root del repo (quest'ultima risalendo da `AppContext.BaseDirectory` fino alla
+ cartella che contiene `CLAUDE.md`).
+
+## Indicizzazione
+
+- **Indexer.cs** — `ScanAsync` cammina tutte le cartelle di primo livello sotto la radice
+ (scoperte dinamicamente, non un elenco fisso) con un walk manuale a stack (`Walk`, più
+ veloce di `EnumerateFiles(..., AllDirectories)` su share con centinaia di migliaia di
+ file), esclude le cartelle `temp_*`, estrae da ogni nome file il pattern Pro-E
+ `..` (`ParseFilename`, regex), e fa un upsert incrementale riga per
+ riga confrontando `percorso_completo` + dimensione + data modifica. Progresso riportato via
+ `IProgress` (marshalato sul thread UI). Contiene anche lo schema SQL: tabella
+ `files` e vista `files_ultima_versione` (ultima versione per `codice`+`tipo_file` via
+ `ROW_NUMBER() OVER (...)`, non self-join+`GROUP BY` per evitare tempi quasi quadratici su
+ ~200k righe).
+
+## Scansione binaria dei `.asm`
+
+- **AssemblyComponentScanner.cs** — estrae euristicamente i codici componente referenziati
+ dentro un `.asm` (formato per lo più binario, codici come stringhe ASCII leggibili al suo
+ interno). Due strategie: codici numerici Pro-E riconosciuti da soli via regex (come nei nomi
+ file), e nomi alfabetici di parti commerciali tokenizzati genericamente e validati contro
+ l'elenco di codici noti del DB (altrimenti il rumore binario sarebbe ingestibile). Dettagli
+ sul formato, l'euristica e i falsi positivi noti in `docs/formato-asm.md`.
+
+## Import distinta base
+
+- **DistintaBaseImporter.cs** — converte l'export del gestionale (SpreadsheetML XML con
+ estensione `.xls`/`.xml`, oppure `.xls`/`.xlsx` binario vero, distinti dai primi byte del
+ file) nelle sole colonne Componente/Descrizione, foglio `"Distinta Base"`. Se il nome del
+ file segue la convenzione `DBP__`, aggiunge l'assieme generale come riga
+ in più (il foglio elenca solo i componenti sotto l'assieme). Scrive il CSV nella cartella
+ passata da `Form1` (root del repo), non necessariamente accanto al file di input.
+
+## Finestre di dialogo (modali, mostrate a fine step)
+
+- **ProgressForm.cs** — durante l'aggiornamento indice: barra di progresso, conteggi
+ nuovi/aggiornati/invariati/rimossi, tempo trascorso e stima del tempo rimanente (dal ritmo
+ file/secondo rispetto al totale atteso, preso dal conteggio della scansione precedente
+ salvato nel DB).
+- **ProgressoComponentiForm.cs** — dialog minimale (etichetta + barra) durante
+ `RisolviAlberoComponenti` in fase 4.
+- **PrtMancantiForm.cs** — esito della risoluzione ricorsiva dell'albero componenti: PRT e
+ sotto-assiemi trovati e copiati in `raw/`, più i codici referenziati che non corrispondono a
+ nessun file indicizzato (possono essere disegni, componenti davvero mancanti, o falsi
+ positivi della scansione `.asm`).
+- **ReportCompletezzaForm.cs** — per ogni assieme della distinta, se tutti i suoi componenti
+ (ricorsivamente fino ai PRT) sono nell'indice (✓/✗/? per "non trovato").
+- **ReportStepForm.cs** — per ogni componente della distinta, `EsitoStep`: `Creato` (STEP
+ trovato e copiato), `StepMancante` (CAD in indice ma STEP non esportato),
+ `ComponenteNonTrovato` (nemmeno il CAD è in indice).
+
+## Persistenza
+
+- **DB** (`cad_index.db`, SQLite) — vedi tabella `files`/vista `files_ultima_versione` sopra.
+ Percorso: cercato accanto all'eseguibile o nella working directory all'avvio; altrimenti
+ selezionabile con "Apri DB...". Radice disco di scansione di default `X:\`
+ (`Form1.RadiceDiscoDefault`, hardcoded).
+- **File su disco**, tutti nella root del repo (non nella cartella dell'eseguibile): il CSV
+ della distinta, `raw/` (CAD dei componenti e sotto-componenti), `step/` (STEP esportati a
+ mano da Creo, input), `step_finali/` (STEP dei soli codici della distinta, output).
+
+## Script standalone (`scripts/`)
+
+C# file-based apps (.NET 10, `dotnet run scripts/Nome.cs -- arg1 arg2`, nessun `.csproj`,
+dipendenze extra con `#:package Nome@versione` in testa al file):
+
+- **ExtractCreoStrings.cs** — BOM di un `.asm` risolvendo ricorsivamente i sotto-assiemi
+ contro `cad_index.db` (stessa logica di `AssiemeCompleto` in Form1, solo codici numerici
+ Pro-E).
+- **BomViaDb.cs** — come sopra ma con la tokenizzazione generica validata contro il DB, per
+ intercettare anche i nomi alfabetici delle parti commerciali.
+- **Analyze.cs** — dump del contesto testo/hex attorno a un pattern in un file binario, per
+ investigare a mano nuovi falsi positivi prima di toccare la regex di
+ `AssemblyComponentScanner`.
+- **Query.cs** — query SQL ad-hoc contro `cad_index.db` da riga di comando.
+
+> ⚠️ Nota importante di sicurezza: se `dotnet run` viene lanciato dentro `CadSearchGui/`
+> (che contiene un `.csproj` di tipo `WinExe`), esegue l'app GUI vera invece dello script
+> standalone e resta bloccato in attesa che una finestra si chiuda. Lanciare gli script da
+> una cartella senza `.csproj` (es. la root del repo o `scripts/` stessa).
+
+## Test
+
+- **CadSearchGui.Tests/** (xUnit) — riferimento al progetto principale via `ProjectReference`
+ + `InternalsVisibleTo` (in `CadSearchGui.csproj`), così i test chiamano direttamente
+ `Indexer.ParseFilename` e `AssemblyComponentScanner.EstraiCodiciReferenziati` senza esporli
+ come API pubblica. File: `IndexerParseFilenameTests.cs`,
+ `AssemblyComponentScannerTests.cs`, `DistintaBaseImporterTests.cs`,
+ `CaseInsensitiveLookupIntegrationTests.cs`.
diff --git a/docs/copia-raw-parallela.md b/docs/copia-raw-parallela.md
new file mode 100644
index 0000000..d412e1b
--- /dev/null
+++ b/docs/copia-raw-parallela.md
@@ -0,0 +1,46 @@
+# Copia parallela dei file in `raw/`
+
+`Form1.CopiaFileTrovati` (usata in fase 3 "Crea cartella raw" e in fase 4 "Trova componenti
+referenziati" per copiare i file trovati nell'indice dentro `raw/`) copia i file in parallelo
+invece che uno alla volta.
+
+## Perché
+
+I file vengono letti da una share di rete (`X:\`, vedi CLAUDE.md). Il tempo per ogni copia è
+dominato dalla latenza di rete (round-trip SMB per aprire/leggere/chiudere il file), non dalla
+CPU: un thread passa la maggior parte del tempo in attesa della risposta del server. Copiare
+più file in parallelo sovrappone queste attese invece di sommarle, con un guadagno quasi
+proporzionale al grado di parallelismo.
+
+## Implementazione
+
+`Parallel.ForEach` con `MaxDegreeOfParallelism = 6` (non un thread per file: troppa
+concorrenza sulla stessa share rischia di saturare le connessioni SMB o subire throttling dal
+server, risultando più lento invece che più veloce). Il contatore dei file copiati usa
+`Interlocked.Increment` per essere thread-safe. La logica di skip per file irraggiungibili
+(share offline, permessi — `catch (IOException or UnauthorizedAccessException)`) resta
+invariata: essendo dentro il corpo della lambda, ogni iterazione ha il proprio try/catch
+indipendente.
+
+Il valore 6 è una stima di partenza, non misurata sulla share reale: se in pratica risulta
+troppo aggressivo o troppo conservativo per la vostra rete, va aggiustato empiricamente.
+
+## Benchmark (sintetico)
+
+Non essendoci accesso diretto alla share `X:\` da questo ambiente, il confronto prima/dopo è
+stato fatto con uno script standalone (`dotnet run .cs`, pattern già usato in `scripts/`)
+che copia file locali fittizi aggiungendo un ritardo artificiale di 80 ms per file, per simulare
+la latenza di un round-trip SMB (il disco locale da solo è troppo veloce per mostrare qualunque
+differenza).
+
+50 file, ritardo simulato 80 ms/file:
+
+| Versione | Tempo totale |
+|---|---|
+| Prima (sequenziale) | 4751 ms |
+| Ora (parallela, grado 6) | 961 ms |
+
+~5x più veloce, coerente con l'aspettativa teorica (limite di ~6x dato il grado di
+parallelismo, scontato l'overhead di scheduling). Essendo una simulazione, il guadagno reale
+sulla share va verificato sul campo: la latenza effettiva, la banda disponibile e il
+comportamento del server sotto più connessioni concorrenti possono cambiare il risultato.