From a7e0df024749d81162a02238140b16a1013b3f81 Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Thu, 17 Sep 2026 15:32:46 +0200 Subject: [PATCH] Fix runaway recursion and two missed-component gaps in .asm scanning The bare root code of an assembly (e.g. 9284208188, without a _D... suffix) is always referenced inside its own suffixed sub-assemblies as a genuine placement pointing back to the general assembly. Following it during recursive resolution pulled in the entire product tree instead of just the requested sub-assembly (598 components collected instead of ~65 on 9284208188_D600). Fixed by seeding the visited-codes set with each starting assembly's root code, in both scripts/ExtractCreoStrings.cs and the two BFS call sites in Form1.cs (RisolviAlberoComponenti, AssiemeCompleto). Also closes two recall gaps found by comparing against a real Creo BOM report: some real placements only show up as the code repeated twice separated by a NUL byte rather than the usual "Componente ID N (...)" text, and family-table bearing codes (CUSCI_...) use the same <...> bracket convention as UNI hardware but weren't recognized. docs/formato-asm.md documents all three findings. Co-Authored-By: Claude Sonnet 5 --- CadSearchGui/AssemblyComponentScanner.cs | 39 +++++++---- CadSearchGui/Form1.cs | 11 ++- docs/formato-asm.md | 85 ++++++++++++++++-------- scripts/ExtractCreoStrings.cs | 81 ++++++++++++++-------- 4 files changed, 147 insertions(+), 69 deletions(-) diff --git a/CadSearchGui/AssemblyComponentScanner.cs b/CadSearchGui/AssemblyComponentScanner.cs index e1ad7aa..cdb2d70 100644 --- a/CadSearchGui/AssemblyComponentScanner.cs +++ b/CadSearchGui/AssemblyComponentScanner.cs @@ -21,10 +21,12 @@ namespace CadSearchGui; /// "ModelView#<numero>", hanno la stessa forma di un codice (8-12 cifre) e /// compaiono più volte come un riferimento vero. Vedi docs/formato-asm.md. /// -/// Viti/dadi/rosette normalizzati UNI sono family table Creo: il nome che compare nel -/// 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. +/// Viti/dadi/rosette normalizzati UNI (e cuscinetti CUSCI_...) sono family table Creo: il +/// nome che compare nel binario (es. "R6--_-_U6592<UNI6592>", +/// "2510060303<CUSCI_ACBB7>") è la singola istanza, non un file separato su disco - il +/// file reale indicizzato è il generico "uni6592.prt.<versione>"/ +/// "cusci_acbb7.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. @@ -34,20 +36,32 @@ namespace CadSearchGui; /// 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. +/// richiede anche un vero placement, in una delle due forme osservate nel binario: +/// "Componente ID N (<codice>...)"/"Component id N (<codice>...)" (la stringa +/// cambia con la localizzazione Creo del file), oppure il codice ripetuto due volte di +/// seguito separato da un singolo byte NUL ("<codice>\0<codice>\0", osservato per +/// componenti di family table non-UNI, es. cuscinetti "CUSCI_..."). 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; + /// Codice radice (parte prima del primo "_") di un codice componente. Un + /// assieme con suffisso "_D.../_C..." referenzia sempre al suo interno anche l'assieme + /// generale a cui appartiene (codice radice, senza suffisso) - riferimento fantasma + /// verso l'alto, non un componente vero (vedi docs/formato-asm.md). Chi fa una + /// risoluzione ricorsiva dell'albero deve seminare l'insieme dei codici già visitati + /// anche con la radice del codice di partenza, non solo col codice stesso, altrimenti + /// risale fino all'assieme generale e riporta dentro l'intero albero del macchinario. + public static string CodiceRadice(string codice) => codice.Split('_')[0]; + public static HashSet EstraiCodiciReferenziati(string percorsoAsm) { var testo = Encoding.Latin1.GetString(File.ReadAllBytes(percorsoAsm)); @@ -59,7 +73,8 @@ static class AssemblyComponentScanner 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); + Regex.IsMatch(testo, $@"Component(?:e)? id \d+ \([^()]*{Regex.Escape(codice)}[^()]*\)", RegexOptions.IgnoreCase) + || testo.Contains(codice + "\0" + codice + "\0"); candidati.RemoveWhere(c => CodiceNudoRegex.IsMatch(c) && !HaPlacement(c)); return candidati; diff --git a/CadSearchGui/Form1.cs b/CadSearchGui/Form1.cs index 12c5a35..ce8be92 100644 --- a/CadSearchGui/Form1.cs +++ b/CadSearchGui/Form1.cs @@ -671,8 +671,10 @@ public partial class Form1 : Form IEnumerable asmIniziali, IProgress<(int Fatti, int Totale)>? progress = null) { var daScansionare = new Queue(asmIniziali); + var codiciIniziali = daScansionare.Select(f => Path.GetFileName(f).Split('.')[0]); var codiciVisti = new HashSet( - daScansionare.Select(f => Path.GetFileName(f).Split('.')[0]), StringComparer.OrdinalIgnoreCase); + codiciIniziali.Concat(codiciIniziali.Select(AssemblyComponentScanner.CodiceRadice)), + StringComparer.OrdinalIgnoreCase); var trovati = new List<(string Codice, string Tipo, string Percorso)>(); var codiciNonTrovati = new List(); @@ -827,7 +829,12 @@ public partial class Form1 : Form var daVisitare = new Queue(); daVisitare.Enqueue(percorsoAsmIniziale); var fileVisitati = new HashSet { percorsoAsmIniziale }; - var codiciVisti = new HashSet { Path.GetFileName(percorsoAsmIniziale).Split('.')[0] }; + var codiceIniziale = Path.GetFileName(percorsoAsmIniziale).Split('.')[0]; + var codiciVisti = new HashSet + { + codiceIniziale, + AssemblyComponentScanner.CodiceRadice(codiceIniziale), + }; while (daVisitare.Count > 0) { diff --git a/docs/formato-asm.md b/docs/formato-asm.md index f514f67..961d839 100644 --- a/docs/formato-asm.md +++ b/docs/formato-asm.md @@ -73,17 +73,43 @@ 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. +vero compare **sempre** anche dentro un vero placement, in una delle due +forme osservate nel binario: + +- `Componente ID N (...)`/`Component id N (...)` (la stringa + cambia con la localizzazione Creo con cui il file è stato salvato - + osservate sia italiana sia inglese); +- il codice ripetuto due volte di seguito separato da un singolo byte NUL, + `\0\0` (osservato per componenti di family table non-UNI, + es. cuscinetti `CUSCI_...` - vedi sotto; il placement testuale + "Componente ID" non li copre, ma questa coppia doppia sì). + +Un riferimento incidentale al nome di un altro assieme, anche se ripetuto più +volte, non compare mai in nessuna delle due forme. `scripts/ExtractCreoStrings.cs` +e `AssemblyComponentScanner.cs` applicano questa regola ai codici "nudi" (senza +suffisso `_...` e senza bracket family table - vedi sotto perché limitata a +questi): un codice ripetuto ma mai trovato dentro un placement/coppia NUL +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. + +**Il codice radice del proprio assieme (assieme generale, senza suffisso +`_D.../_C...`).** Caso particolare e sistematico del precedente, non solo +incidentale: un assieme suddiviso in sotto-assiemi con suffisso +(`9284208188_D600.asm`, `_D610.asm`, ...) referenzia **sempre** al suo interno +anche il codice radice dell'assieme generale (`9284208188`, senza suffisso) - +il "padre" a cui tutti i `_D...` appartengono nella struttura Creo - con un +vero placement sintatticamente identico a un componente reale. Non è un +componente di quel sotto-assieme, è la risalita verso l'assieme che lo +contiene. Osservato su `9284208188_D600.asm`: `Componente ID N (9284208188...)`. +Se seguito in una risoluzione ricorsiva, riporta dentro l'intero albero del +prodotto invece del solo sotto-assieme richiesto: 598 componenti raccolti +invece dei ~65 reali. A differenza del caso generico sopra, qui il codice +"padre" _è_ prevedibile a priori (è la parte del proprio codice prima del +primo `_`), quindi va escluso esplicitamente seminando l'insieme dei codici +già visitati anche con la radice del codice di partenza, non solo con il +codice stesso — non basta il controllo generico sul placement, perché il +placement qui è reale. Vedi `AssemblyComponentScanner.CodiceRadice`. **Riferimenti fantasma a codici superati/rinominati.** Caso osservato: `9244207061.asm` contiene un riferimento a `9044207061` (stesso numero, prima @@ -134,30 +160,35 @@ 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) +## Componenti normalizzati (family table UNI, CUSCI, ...) -I componenti di minuteria standard (viti, dadi, rosette UNI) sono definiti in -Creo come **family table**: un file generico (es. `uni6592.prt`) con tante -righe/istanze quante sono le varianti dimensionali. Nel binario dell'assieme -compaiono così: +I componenti di minuteria standard (viti, dadi, rosette UNI) e i cuscinetti +(`CUSCI_...`) sono definiti in Creo come **family table**: un file generico +(es. `uni6592.prt`, `cusci_acbb7.prt`) con tante righe/istanze quante sono le +varianti dimensionali. Nel binario dell'assieme compaiono così: ``` Componente ID 28755 (R6--_-_U6592.PRT) +2510060303 ``` -`R6--_-_U6592` è il nome della singola istanza (riga della family table), **non -un file separato su disco** — sulla share esiste solo `uni6592.prt.` -(verificato in `Unificati/`). Cercare l'istanza intera nell'indice fallisce -sempre; va estratto solo il contenuto tra `<` e `>` (`UNI6592`), che coincide -col nome del file generico realmente indicizzato: +`R6--_-_U6592` e `2510060303` sono il nome/codice della singola istanza (riga +della family table), **non un file separato su disco** — sulla share esiste +solo il generico (`uni6592.prt.`, `cusci_acbb7.prt.`, +verificato in `Unificati/`). Cercare l'istanza intera nell'indice fallisce +sempre; va estratto solo il contenuto tra `<` e `>` (`UNI6592`, `CUSCI_ACBB7`), +che coincide col nome del file generico realmente indicizzato: ``` -regex aggiuntiva: (?<=<)UNI\d+(?=>) +regex aggiuntiva: (?<=<)(?:UNI\d+|CUSCI_[A-Za-z0-9]+)(?=>) ``` Implementata sia in [`scripts/ExtractCreoStrings.cs`](../scripts/ExtractCreoStrings.cs) sia in [`AssemblyComponentScanner.cs`](../CadSearchGui/AssemblyComponentScanner.cs) (fase "Trova componenti -referenziati" dell'app). +referenziati" dell'app). Il prefisso family table non è garantito essere solo `UNI`/`CUSCI`: +se ricompaiono famiglie diverse (altri componenti normalizzati), vanno aggiunte allo stesso modo, +verificando prima con `scripts/Analyze.cs` che il contenuto tra `<` e `>` corrisponda a un codice +realmente indicizzato in `Unificati/`. ## Risoluzione ricorsiva dell'albero @@ -172,11 +203,11 @@ livello) serve una BFS: 3. Un codice non trovato nell'indice va scartato/segnalato: o è rumore binario, o è un componente non ancora indicizzato. -Stessa logica di estrazione candidati in due punti del codice, con scopi -diversi: +Stessa logica di estrazione candidati ed euristica del codice radice (sopra), +duplicata in due punti del codice per scopi diversi: -- [`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. +- [`AssemblyComponentScanner.EstraiCodiciReferenziati`](../CadSearchGui/AssemblyComponentScanner.cs) + `CodiceRadice` — 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"). Entrambe seminano l'insieme dei codici visitati con codice + radice di ogni file di partenza. +- [`scripts/ExtractCreoStrings.cs`](../scripts/ExtractCreoStrings.cs) — stessa BFS ricorsiva standalone da riga di comando (copia indipendente della stessa euristica, nessun `ProjectReference` verso la GUI). Usato per confrontare l'euristica contro l'albero Creo di un singolo file durante il reverse engineering, senza dover passare dalla UI/dal DB `raw`. Nota: le query contro `files_ultima_versione` filtrano solo per `(cartella_radice, codice, tipo_file)` - se lo stesso codice esiste in più diff --git a/scripts/ExtractCreoStrings.cs b/scripts/ExtractCreoStrings.cs index b964f68..f1de61d 100644 --- a/scripts/ExtractCreoStrings.cs +++ b/scripts/ExtractCreoStrings.cs @@ -1,11 +1,11 @@ #: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 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. +// Estrae dal file .asm i codici dei componenti referenziati, espandendo ricorsivamente +// ogni sotto-assieme (.asm) trovato nell'indice, finché non restano solo PRT (foglie) o +// codici non presenti nell'indice. 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; @@ -21,18 +21,19 @@ string asmIniziale = args[0]; string dbPath = args.Length > 1 ? args[1] : "cad_index.db"; string outPath = args.Length > 2 ? args[2] : "BOM.txt"; -// Seconda alternativa: viti/dadi/rosette normalizzati UNI, definiti come family table in -// Creo. Compaiono come "Componente ID N (R12--_-_U6592.PRT)": "R12--_-_U6592" è -// 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 ">". +// Seconda alternativa: componenti normalizzati definiti come family table in Creo (viti/ +// dadi/rosette UNI, ma anche cuscinetti CUSCI_...). Compaiono come "Componente ID N +// (R12--_-_U6592.PRT)": "R12--_-_U6592" è il nome della singola istanza (riga +// della family table, non un file separato su disco), il file reale indicizzato è il +// generico "uni6592.prt."/"cusci_acbb7.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; @@ -48,14 +49,18 @@ HashSet EstraiCodiciReferenziati(string percorsoAsm) // 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. + // placement, in una delle due forme osservate nel binario: "Componente ID N + // (...)"/"Component id N (...)" (la stringa cambia con la + // localizzazione Creo del file), oppure il codice ripetuto due volte di seguito + // separato da un singolo byte NUL ("\0\0", osservato per componenti di + // family table non-UNI, es. cuscinetti "CUSCI_..."). 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); + Regex.IsMatch(testo, $@"Component(?:e)? id \d+ \([^()]*{Regex.Escape(codice)}[^()]*\)", RegexOptions.IgnoreCase) + || testo.Contains(codice + "\0" + codice + "\0"); candidati.RemoveWhere(c => Regex.IsMatch(c, @"^\d{8,12}[A-Za-z]{0,2}$") && !HaPlacement(c)); return candidati; @@ -98,22 +103,42 @@ Dictionary> CercaComponenti(IReadOn return risultato; } -// Solo i riferimenti diretti del file indicato (un livello), come li mostra Creo per -// quell'assieme - nessuna espansione ricorsiva nei sotto-assiemi. +// Espansione ricorsiva: ogni sotto-assieme (.asm) trovato viene a sua volta letto e i suoi +// componenti aggiunti alla coda, finché non restano solo PRT (foglie) o codici non presenti +// nell'indice - stessa logica di Form1.RisolviAlberoComponenti. var codiceProprio = Path.GetFileName(asmIniziale).Split('.')[0]; +// Un assieme con suffisso "_D..." referenzia sempre al suo interno anche l'assieme +// generale (codice nudo, senza suffisso) a cui appartiene - riferimento fantasma verso +// l'alto, non un componente vero (vedi docs/formato-asm.md). Va escluso subito: se +// seguito, riporta dentro l'intero albero del macchinario invece del solo sotto-assieme +// richiesto (visto su 9284208188_D600 -> 9284208188, che da solo genera 598 falsi +// positivi invece di ~90 componenti reali). +var codiceRadice = codiceProprio.Split('_')[0]; var componentiTrovati = new SortedSet(StringComparer.OrdinalIgnoreCase); var nonTrovati = new List(); +var codiciVisti = new HashSet(StringComparer.OrdinalIgnoreCase) { codiceProprio, codiceRadice }; +var daScansionare = new Queue(new[] { asmIniziale }); -var candidati = EstraiCodiciReferenziati(asmIniziale) - .Where(c => !string.Equals(c, codiceProprio, StringComparison.OrdinalIgnoreCase)) - .ToList(); -var trovatiPerCodice = CercaComponenti(candidati); -foreach (var codice in candidati) +while (daScansionare.Count > 0) { - if (trovatiPerCodice[codice].Count == 0) - nonTrovati.Add(codice); - else + var asm = daScansionare.Dequeue(); + var candidati = EstraiCodiciReferenziati(asm) + .Where(codiciVisti.Add) + .ToList(); + var trovatiPerCodice = CercaComponenti(candidati); + foreach (var codice in candidati) + { + var componenti = trovatiPerCodice[codice]; + if (componenti.Count == 0) + { + nonTrovati.Add(codice); + continue; + } componentiTrovati.Add(codice); + foreach (var (tipo, percorso) in componenti) + if (tipo == "asm") + daScansionare.Enqueue(percorso); + } } File.WriteAllLines(outPath, componentiTrovati);