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 <noreply@anthropic.com>
This commit is contained in:
davideandClaude Sonnet 5 committed 2026-09-17 15:32:55 +02:00
1 parent 9660c4883d
commit a7e0df0247
4 files changed
+147 -69

No files matched your search

+27 -12
View File
@@ -21,10 +21,12 @@ namespace CadSearchGui;
/// "ModelView#&lt;numero&gt;", 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&lt;UNI6592&gt;") è la singola istanza, non un file separato
/// su disco - il file reale indicizzato è il generico "uni6592.prt.&lt;versione&gt;", quindi
/// va estratto solo il contenuto tra "&lt;" e "&gt;". 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&lt;UNI6592&gt;",
/// "2510060303&lt;CUSCI_ACBB7&gt;") è la singola istanza, non un file separato su disco - il
/// file reale indicizzato è il generico "uni6592.prt.&lt;versione&gt;"/
/// "cusci_acbb7.prt.&lt;versione&gt;", quindi va estratto solo il contenuto tra "&lt;" e
/// "&gt;". 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 (&lt;codice&gt;...)"/
/// "Component id N (&lt;codice&gt;...)" (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 (&lt;codice&gt;...)"/"Component id N (&lt;codice&gt;...)" (la stringa
/// cambia con la localizzazione Creo del file), oppure il codice ripetuto due volte di
/// seguito separato da un singolo byte NUL ("&lt;codice&gt;\0&lt;codice&gt;\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.
/// </summary>
static class AssemblyComponentScanner
{
private static readonly Regex CodiceRegex = new(
@"(?<!ModelView#)\b\d{8,12}[A-Za-z]{0,2}(?:_[A-Za-z]?\d{2,4})?\b|(?<=<)UNI\d+(?=>)", RegexOptions.Compiled);
@"(?<!ModelView#)\b\d{8,12}[A-Za-z]{0,2}(?:_[A-Za-z]?\d{2,4})?\b|(?<=<)(?:UNI\d+|CUSCI_[A-Za-z0-9]+)(?=>)",
RegexOptions.Compiled);
private static readonly Regex CodiceNudoRegex = new(@"^\d{8,12}[A-Za-z]{0,2}$", RegexOptions.Compiled);
private const int OccorrenzeMinime = 2;
/// <summary>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.</summary>
public static string CodiceRadice(string codice) => codice.Split('_')[0];
public static HashSet<string> 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;
+9 -2
View File
@@ -671,8 +671,10 @@ public partial class Form1 : Form
IEnumerable<string> asmIniziali, IProgress<(int Fatti, int Totale)>? progress = null)
{
var daScansionare = new Queue<string>(asmIniziali);
var codiciIniziali = daScansionare.Select(f => Path.GetFileName(f).Split('.')[0]);
var codiciVisti = new HashSet<string>(
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<string>();
@@ -827,7 +829,12 @@ public partial class Form1 : Form
var daVisitare = new Queue<string>();
daVisitare.Enqueue(percorsoAsmIniziale);
var fileVisitati = new HashSet<string> { percorsoAsmIniziale };
var codiciVisti = new HashSet<string> { Path.GetFileName(percorsoAsmIniziale).Split('.')[0] };
var codiceIniziale = Path.GetFileName(percorsoAsmIniziale).Split('.')[0];
var codiciVisti = new HashSet<string>
{
codiceIniziale,
AssemblyComponentScanner.CodiceRadice(codiceIniziale),
};
while (daVisitare.Count > 0)
{