From 9660c4883dc747ece667b5efc861a74fa83a94ef Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Thu, 17 Sep 2026 14:59:23 +0200 Subject: [PATCH] Refine .asm component extraction: batch DB queries, wider suffix regex, placement check - Batch the DB lookup per file instead of per code in scripts/ExtractCreoStrings.cs: files_ultima_versione is an unmaterialized view, so each query rescanned the whole files table (~230ms) - batching cuts a multi-minute BFS to seconds. - Widen the code regex to accept a revision letter and a "_..." suffix together (e.g. "9284200366A_C01"), and purely numeric "_NNNN" suffixes (e.g. dimensional variants like "2702220182_8188"), not just letter+digits. - Require bare digit-only candidates to also appear inside a real "Componente ID N (...)"/"Component id N (...)" placement, filtering out incidental references to a containing assembly's own code. - scripts/ExtractCreoStrings.cs now reports only the direct references of the given .asm (one level, matching Creo's own tree for that file) instead of recursing into sub-assemblies. - Port the regex and placement-check refinements to the production AssemblyComponentScanner.cs used by the app's "Trova componenti referenziati" and "Verifica completezza" phases. - Document the new false-positive categories (parent/container self-reference, suppressed/removed components with residual placement, placement missing suffix/bracket detail) in docs/formato-asm.md, generalized beyond the specific assemblies used to find them. Co-Authored-By: Claude Sonnet 5 --- .../AssemblyComponentScannerTests.cs | 20 +++- CadSearchGui/AssemblyComponentScanner.cs | 26 ++++- docs/formato-asm.md | 82 +++++++++++++- scripts/ExtractCreoStrings.cs | 107 +++++++++++------- 4 files changed, 185 insertions(+), 50 deletions(-) diff --git a/CadSearchGui.Tests/AssemblyComponentScannerTests.cs b/CadSearchGui.Tests/AssemblyComponentScannerTests.cs index ba32bf1..b3ff19c 100644 --- a/CadSearchGui.Tests/AssemblyComponentScannerTests.cs +++ b/CadSearchGui.Tests/AssemblyComponentScannerTests.cs @@ -14,8 +14,10 @@ public class AssemblyComponentScannerTests [Fact] public void Scarta_codici_con_una_sola_occorrenza() { - // "11112222" compare 2 volte (riferimento vero), "99998888" una sola (rumore). - string percorso = ScriviAsmFinto("blah 11112222 blah 99998888 blah 11112222 fine"); + // "11112222" compare 2 volte con un vero placement (riferimento vero), "99998888" + // una sola volta senza placement (rumore). + string percorso = ScriviAsmFinto( + "blah Componente ID 1 (11112222.PRT) blah 99998888 blah 11112222 fine"); try { var codici = AssemblyComponentScanner.EstraiCodiciReferenziati(percorso); @@ -25,6 +27,20 @@ public class AssemblyComponentScannerTests finally { File.Delete(percorso); } } + [Fact] + public void Scarta_codice_ripetuto_senza_un_vero_placement() + { + // Riferimento al nome di un altro assieme (es. il padre che lo contiene): ripetuto + // più volte ma mai dentro un placement "Componente ID N (...)" - va scartato. + string percorso = ScriviAsmFinto("padre 33334444 assieme 33334444_D200 33334444"); + try + { + var codici = AssemblyComponentScanner.EstraiCodiciReferenziati(percorso); + Assert.DoesNotContain("33334444", codici); + } + finally { File.Delete(percorso); } + } + [Fact] public void Riconosce_sotto_assiemi_con_suffisso_stile_D100() { diff --git a/CadSearchGui/AssemblyComponentScanner.cs b/CadSearchGui/AssemblyComponentScanner.cs index 793b30c..e1ad7aa 100644 --- a/CadSearchGui/AssemblyComponentScanner.cs +++ b/CadSearchGui/AssemblyComponentScanner.cs @@ -25,11 +25,27 @@ namespace CadSearchGui; /// binario (es. "R6--_-_U6592<UNI6592>") è la singola istanza, non un file separato /// su disco - il file reale indicizzato è il generico "uni6592.prt.<versione>", quindi /// va estratto solo il contenuto tra "<" e ">". Vedi docs/formato-asm.md. +/// +/// Lettera di revisione e suffisso "_..." possono comparire insieme sullo stesso codice +/// (es. "9284200366A_C01"), e il suffisso dopo "_" può essere puramente numerico (es. +/// "2702220182_8188", una variante dimensionale, non lettera+cifre come "_C01"/"_D100"). +/// +/// Un codice "nudo" (senza suffisso "_..." e senza bracket UNI) ripetuto più volte non +/// basta: può trattarsi del riferimento al nome di un altro assieme che lo contiene (non +/// noto a priori, quindi non escludibile per confronto diretto come il proprio codice) o +/// di un residuo di feature soppresse/superate (vedi docs/formato-asm.md). Per questi si +/// richiede anche un vero placement "Componente ID N (<codice>...)"/ +/// "Component id N (<codice>...)" (la stringa cambia con la localizzazione Creo del +/// file). Non si applica alle varianti con suffisso o bracket UNI: in alcuni file il +/// placement riporta solo il nome istanza/generico senza il dettaglio del suffisso o del +/// bracket, che vive altrove nei metadati - lì resta necessaria la sola euristica delle +/// occorrenze ripetute. /// static class AssemblyComponentScanner { private static readonly Regex CodiceRegex = new( - @"(?)", RegexOptions.Compiled); + @"(?)", RegexOptions.Compiled); + private static readonly Regex CodiceNudoRegex = new(@"^\d{8,12}[A-Za-z]{0,2}$", RegexOptions.Compiled); private const int OccorrenzeMinime = 2; public static HashSet EstraiCodiciReferenziati(string percorsoAsm) @@ -40,6 +56,12 @@ static class AssemblyComponentScanner { occorrenze[m.Value] = occorrenze.GetValueOrDefault(m.Value) + 1; } - return occorrenze.Where(kv => kv.Value >= OccorrenzeMinime).Select(kv => kv.Key).ToHashSet(); + var candidati = occorrenze.Where(kv => kv.Value >= OccorrenzeMinime).Select(kv => kv.Key).ToHashSet(); + + bool HaPlacement(string codice) => + Regex.IsMatch(testo, $@"Component(?:e)? id \d+ \([^()]*{Regex.Escape(codice)}[^()]*\)", RegexOptions.IgnoreCase); + + candidati.RemoveWhere(c => CodiceNudoRegex.IsMatch(c) && !HaPlacement(c)); + return candidati; } } diff --git a/docs/formato-asm.md b/docs/formato-asm.md index 70cd7ae..f514f67 100644 --- a/docs/formato-asm.md +++ b/docs/formato-asm.md @@ -41,6 +41,17 @@ regex: \b\d{8,12}(?:_[A-Za-z]\d{2,4}|[A-Za-z]{0,2})\b occorrenze: >= 2 ``` +`scripts/ExtractCreoStrings.cs` ha una versione più raffinata della regex e un filtro +aggiuntivo (vedi sotto), non ancora portati in `AssemblyComponentScanner.cs`: + +``` +regex: \b\d{8,12}[A-Za-z]{0,2}(?:_[A-Za-z]?\d{2,4})?\b +``` + +(lettera di revisione e suffisso "_..." possono comparire anche insieme, es. +`9284200366A_C01`; il suffisso dopo "_" può essere puramente numerico, es. +`2702220182_8188`, non solo lettera+cifre come `_C01`/`_D100`.) + ## Falsi positivi noti **ID di viste salvate.** Il pattern `ModelView#` (es. @@ -54,6 +65,26 @@ volte nei metadati (`CODICE`, `part_table`, `STAMPIGLIATURA`, ecc.) e supera quindi la soglia di occorrenze. Va escluso esplicitamente confrontandolo con il codice derivato dal nome del file in ingresso (`nomefile.split('.')[0]`). +**Il codice dell'assieme padre (o di qualunque altro assieme che lo +contiene).** Variante dello stesso problema: un sotto-assieme può contenere +anche il codice dell'assieme che lo referenzia (riferimento al nome +sessione/file salvato nei metadati), che supera la soglia di occorrenze pur +non essendo un componente. A differenza del proprio codice, non è noto a +priori: dipende da dove/come il file è stato aperto o da chi lo referenzia, +quindi non si può escludere per confronto diretto come per il caso +precedente. La regola generale che discrimina questo caso: un componente +vero compare **sempre** anche dentro un vero placement +`Componente ID N (...)`/`Component id N (...)` (la stringa +cambia con la localizzazione Creo con cui il file è stato salvato - osservate +sia italiana sia inglese); un riferimento incidentale al nome di un altro +assieme, anche se ripetuto più volte, non compare mai in quel contesto. +`scripts/ExtractCreoStrings.cs` applica questa regola ai codici "nudi" (senza +suffisso `_...` e senza bracket UNI - vedi sotto perché limitata a questi): +un codice ripetuto ma mai trovato dentro un placement viene scartato. Questo +NON copre i casi fantasma/soppressi sotto, che hanno un placement +sintatticamente identico a uno vero e restano quindi indistinguibili a +livello testuale. + **Riferimenti fantasma a codici superati/rinominati.** Caso osservato: `9244207061.asm` contiene un riferimento a `9044207061` (stesso numero, prima di una rinumerazione) con la stessa identica struttura sintattica di un @@ -70,6 +101,39 @@ riesce a discriminarlo in modo affidabile senza rischiare di scartare componenti veri con pattern simili altrove. Se ricompare, l'unica verifica sicura resta l'apertura manuale in Creo del sotto-assieme sospetto. +**Componenti soppressi/rimossi con placement residuo.** Stessa natura del +caso precedente: capita che un codice compaia con un vero +`Componente ID N (.PRT)`/`Component id N (.PRT)` - con un ID +numerico nello stesso intervallo di quelli dei componenti realmente visibili +nell'albero Creo, quindi non distinguibile per posizione o formato - ma il +codice non compare nell'albero/BOM che Creo mostra per quell'assieme. +Osservato su più assiemi in questa sessione (`9044207061` dentro +`9244207061.asm`, e diversi codici dentro `9284208188_D200.asm`). Ipotesi più +probabile: feature soppresse, cancellate o superate in una revisione +successiva, il cui placement resta comunque nella storia binaria del file +(Creo non riscrive da zero il file a ogni salvataggio, accumula stato). Nella +pratica, quando compaiono, sono un numero ridotto di codici extra rispetto al +reale (falsi positivi, non falsi negativi): la BOM generata resta un +sovrainsieme sicuro dei componenti veri, mai un sottoinsieme. Nessuna +euristica testuale può escluderli con certezza; se serve un elenco esatto +l'unica verifica è aprire l'assieme in Creo e controllare a mano quali ID +sono effettivamente presenti/attivi. + +**Placement senza il dettaglio del suffisso/bracket.** Il testo dentro +`Componente ID N (...)` a volte riporta solo il nome generico/istanza, senza +il suffisso di variante dimensionale o il bracket family-table, che compaiono +invece solo altrove nei metadati dello stesso file (non vicino al placement). +Osservato in due forme sullo stesso assieme (`9284208188_D200.asm`): +`Component id 82 (R8--_-_U6592.PRT)` (nessun `` nel placement, pure +presente nel file) e `Component id 150 (2702220182.PRT)` (nessun `_8188` +nel placement, mentre la variante dimensionale reale `2702220182_8188` +compare solo come stringa ripetuta altrove). Per questo il requisito del +placement esplicito (caso sopra) si applica solo ai codici "nudi" (senza +suffisso `_...` e senza bracket UNI): per le varianti con suffisso o family +table il placement può mancare del dettaglio, quindi resta necessaria la +sola euristica delle occorrenze ripetute - meno precisa, ma l'unica +applicabile senza rischiare falsi negativi su casi come questi. + ## Viti/dadi/rosette normalizzati (family table UNI) I componenti di minuteria standard (viti, dadi, rosette UNI) sono definiti in @@ -98,7 +162,8 @@ referenziati" dell'app). ## Risoluzione ricorsiva dell'albero Un `.asm` referenzia solo i propri sotto-componenti diretti, non l'intero -albero annidato. Per ricostruire la distinta base completa serve una BFS: +albero annidato. Per ricostruire la distinta base completa (non solo il primo +livello) serve una BFS: 1. Estrarre i codici candidati dal file corrente (euristica sopra). 2. Per ogni codice, cercarlo nell'indice `cad_index.db` @@ -107,11 +172,18 @@ albero annidato. Per ricostruire la distinta base completa serve una BFS: 3. Un codice non trovato nell'indice va scartato/segnalato: o è rumore binario, o è un componente non ancora indicizzato. -Stessa logica in tre punti del codice, con scopi diversi: +Stessa logica di estrazione candidati in due punti del codice, con scopi +diversi: -- [`AssemblyComponentScanner.EstraiCodiciReferenziati`](../CadSearchGui/AssemblyComponentScanner.cs) — solo estrazione dei candidati da un file. -- [`Form1.AssiemeCompleto`](../CadSearchGui/Form1.cs) — BFS per verificare se un assieme è completo nell'indice (fase "Verifica completezza"). -- [`scripts/ExtractCreoStrings.cs`](../scripts/ExtractCreoStrings.cs) — stesso BFS, standalone, per generare un `BOM.txt` da riga di comando. +- [`AssemblyComponentScanner.EstraiCodiciReferenziati`](../CadSearchGui/AssemblyComponentScanner.cs) — solo estrazione dei candidati da un file, usata dalla BFS in [`Form1.RisolviAlberoComponenti`](../CadSearchGui/Form1.cs) (fase "Trova componenti referenziati") e [`Form1.AssiemeCompleto`](../CadSearchGui/Form1.cs) (fase "Verifica completezza"). +- [`scripts/ExtractCreoStrings.cs`](../scripts/ExtractCreoStrings.cs) — estrazione standalone da riga di comando, ma **un solo livello**: riporta solo i riferimenti diretti del file passato (come li mostra Creo per quell'assieme), senza scendere ricorsivamente nei sotto-assiemi. Usato per confrontare l'euristica contro l'albero Creo di un singolo file durante il reverse engineering; non genera una distinta base completa multi-livello. + +Nota: le query contro `files_ultima_versione` filtrano solo per +`(cartella_radice, codice, tipo_file)` - se lo stesso codice esiste in più +cartelle radice sulla share (duplicati/copie storiche), va sempre ordinato +per `data_modifica DESC` e tenuta solo la riga più recente per tipo, altrimenti +si rischia di risolvere un codice contro una copia diversa (e potenzialmente +diversa nel contenuto) da quella realmente referenziata. ## Strumento di analisi manuale diff --git a/scripts/ExtractCreoStrings.cs b/scripts/ExtractCreoStrings.cs index db93aa9..b964f68 100644 --- a/scripts/ExtractCreoStrings.cs +++ b/scripts/ExtractCreoStrings.cs @@ -1,13 +1,12 @@ #:package Microsoft.Data.Sqlite@10.0.12 // dotnet run scripts/ExtractCreoStrings.cs -- [cad_index.db] [output.txt] // -// Estrae dal file .asm i codici dei componenti referenziati (stessa euristica di -// CadSearchGui/AssemblyComponentScanner.cs: i codici veri compaiono più volte nel -// file, il rumore binario quasi sempre una sola volta; gli ID interni -// "ModelView#" sono esclusi via lookbehind - vedi scripts/Analyze.cs) e poi -// risolve ricorsivamente i sotto-assiemi cercandoli nell'indice cad_index.db, con lo -// stesso BFS di Form1.AssiemeCompleto: un assieme può referenziare solo i propri -// sotto-assiemi diretti, non l'intero albero, quindi va camminato livello per livello. +// Estrae dal file .asm i codici dei componenti referenziati DIRETTAMENTE (un solo +// livello, come li mostra Creo per quell'assieme - nessuna espansione ricorsiva nei +// sotto-assiemi). Stessa euristica di CadSearchGui/AssemblyComponentScanner.cs: i codici +// veri compaiono più volte nel file, il rumore binario quasi sempre una sola volta; gli +// ID interni "ModelView#" sono esclusi via lookbehind - vedi scripts/Analyze.cs. +// Ogni codice trovato viene validato contro l'indice cad_index.db. using System.Text; using System.Text.RegularExpressions; using Microsoft.Data.Sqlite; @@ -27,8 +26,13 @@ string outPath = args.Length > 2 ? args[2] : "BOM.txt"; // il nome della singola istanza (riga della family table, non un file separato su disco), // il file reale indicizzato è il generico "uni6592.prt." - va quindi estratto // solo il contenuto tra "<" e ">". +// Terza alternativa: varianti dimensionali con suffisso puramente numerico (es. cinghie +// "2702220182_8188", dove "8188" è una misura, non una lettera di revisione/sotto-assieme +// come "_C01"/"_D100") - la lettera dopo "_" è quindi opzionale, non obbligatoria. La +// lettera di revisione e il suffisso "_" possono anche comparire insieme sullo stesso +// codice (es. "9284200366A_C01", componente di un sotto-assieme già di per sé revisionato). var codiceRegex = new Regex( - @"(?)", + @"(?)", RegexOptions.Compiled); const int occorrenzeMinime = 2; @@ -38,57 +42,78 @@ HashSet EstraiCodiciReferenziati(string percorsoAsm) var occorrenze = new Dictionary(); foreach (Match m in codiceRegex.Matches(testo)) occorrenze[m.Value] = occorrenze.GetValueOrDefault(m.Value) + 1; - return occorrenze.Where(kv => kv.Value >= occorrenzeMinime).Select(kv => kv.Key).ToHashSet(); + var candidati = occorrenze.Where(kv => kv.Value >= occorrenzeMinime).Select(kv => kv.Key).ToHashSet(); + + // Ripetersi >=2 volte nel file non basta per un codice "nudo" (senza suffisso "_..."): + // riferimenti al proprio assieme padre (nome sessione/file) o a codici superati/ + // rinominati (caso "fantasma", docs/formato-asm.md) ripetono comunque il codice più + // volte pur non essendo mai un componente vero. Per questi si richiede un vero + // placement "Componente ID N (...)". "Componente ID"/"Component id": la + // stringa cambia con la localizzazione Creo con cui il file è stato salvato (osservate + // sia italiana sia inglese). Non si applica alle varianti con suffisso "_..." né alle + // istanze family table UNI: in alcuni file il placement riporta solo il nome + // istanza/generico senza il dettaglio del suffisso o del bracket "<...>", che vive + // altrove nei metadati - lì l'occorrenza ripetuta resta l'unico segnale disponibile. + bool HaPlacement(string codice) => + Regex.IsMatch(testo, $@"Component(?:e)? id \d+ \([^()]*{Regex.Escape(codice)}[^()]*\)", RegexOptions.IgnoreCase); + + candidati.RemoveWhere(c => Regex.IsMatch(c, @"^\d{8,12}[A-Za-z]{0,2}$") && !HaPlacement(c)); + return candidati; } using var connection = new SqliteConnection($"Data Source={dbPath};Mode=ReadOnly"); connection.Open(); -using var command = connection.CreateCommand(); -command.CommandText = "SELECT tipo_file, percorso_completo FROM files_ultima_versione " - + "WHERE LOWER(codice) = LOWER(@codice) AND LOWER(tipo_file) IN ('prt', 'asm')"; -var pCodice = command.Parameters.Add("@codice", SqliteType.Text); -List<(string Tipo, string Percorso)> CercaComponenti(string codice) +// "files_ultima_versione" è una VIEW con ROW_NUMBER() OVER (...), che SQLite non +// materializza: ogni query la ricalcola sull'intera tabella "files" (~230ms anche per un +// solo codice, su ~300k righe). Batchare tutti i codici trovati in un file in un'unica +// query paga quel costo una volta per file invece che una volta per codice - decisivo +// sugli assiemi con centinaia di sotto-componenti referenziati. +Dictionary> CercaComponenti(IReadOnlyCollection codici) { - pCodice.Value = codice; - var risultato = new List<(string, string)>(); - var tipiVisti = new HashSet(); + var risultato = new Dictionary>(StringComparer.OrdinalIgnoreCase); + foreach (var codice in codici) + risultato[codice] = new List<(string, string)>(); + if (codici.Count == 0) + return risultato; + + using var command = connection.CreateCommand(); + var nomiParametri = codici.Select((_, i) => $"@c{i}").ToList(); + command.CommandText = "SELECT codice, tipo_file, percorso_completo FROM files_ultima_versione " + + $"WHERE LOWER(codice) IN ({string.Join(",", nomiParametri)}) AND LOWER(tipo_file) IN ('prt', 'asm')"; + int i = 0; + foreach (var codice in codici) + command.Parameters.AddWithValue(nomiParametri[i++], codice.ToLowerInvariant()); + + var tipiVisti = new Dictionary>(StringComparer.OrdinalIgnoreCase); using var reader = command.ExecuteReader(); while (reader.Read()) { - string tipo = reader.GetString(0).ToLowerInvariant(); - if (tipiVisti.Add(tipo)) - risultato.Add((tipo, reader.GetString(1))); + string codiceDb = reader.GetString(0); + string tipo = reader.GetString(1).ToLowerInvariant(); + var visti = tipiVisti.TryGetValue(codiceDb, out var v) ? v : tipiVisti[codiceDb] = new HashSet(); + if (visti.Add(tipo)) + risultato[codiceDb].Add((tipo, reader.GetString(2))); } return risultato; } -var codiciVisti = new HashSet(StringComparer.OrdinalIgnoreCase) { Path.GetFileName(asmIniziale).Split('.')[0] }; -var daVisitare = new Queue(); -daVisitare.Enqueue(asmIniziale); +// Solo i riferimenti diretti del file indicato (un livello), come li mostra Creo per +// quell'assieme - nessuna espansione ricorsiva nei sotto-assiemi. +var codiceProprio = Path.GetFileName(asmIniziale).Split('.')[0]; var componentiTrovati = new SortedSet(StringComparer.OrdinalIgnoreCase); var nonTrovati = new List(); -while (daVisitare.Count > 0) +var candidati = EstraiCodiciReferenziati(asmIniziale) + .Where(c => !string.Equals(c, codiceProprio, StringComparison.OrdinalIgnoreCase)) + .ToList(); +var trovatiPerCodice = CercaComponenti(candidati); +foreach (var codice in candidati) { - var file = daVisitare.Dequeue(); - foreach (var codice in EstraiCodiciReferenziati(file)) - { - if (!codiciVisti.Add(codice)) - continue; - - var componenti = CercaComponenti(codice); - if (componenti.Count == 0) - { - nonTrovati.Add(codice); - continue; - } - + if (trovatiPerCodice[codice].Count == 0) + nonTrovati.Add(codice); + else componentiTrovati.Add(codice); - var percorsoAsm = componenti.FirstOrDefault(c => c.Tipo == "asm").Percorso; - if (percorsoAsm is not null) - daVisitare.Enqueue(percorsoAsm); - } } File.WriteAllLines(outPath, componentiTrovati);