Fix euristica BOM, unifica script/GUI su un'unica fonte, aggiungi test integrazione/E2E
Due bug reali trovati confrontando l'estrazione BOM con la BOM ufficiale Creo: un componente scartato per placement fallito in un file veniva bloccato per sempre anche quando il file giusto aveva un vero placement (perdendo interi sotto-alberi come 9256510047), e i codici con suffisso (_C01 ecc.) richiedevano sempre placement esplicito invece dell'eccezione per occorrenze ripetute già presente nella GUI. Entrambi corretti. AssemblyComponentScanner reso pubblico e scripts/BomViaDb.cs, ExtractCreoStrings.cs riscritti per referenziarlo via "#:project" invece di duplicare l'euristica: un'unica fonte di verità condivisa da GUI e script standalone. Aggiunti test di integrazione per Indexer.ScanAsync (mai coperto oltre al parsing del nome file) e test end-to-end che ricostruiscono l'intera pipeline disco -> indice -> risoluzione albero componenti senza toccare la UI. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
353a1028d9
commit
5c9661eb12
8 files changed
+514
-171
No files matched your search
+24
-54
@@ -1,17 +1,16 @@
|
||||
#:project ../PrepStep/PrepStep.csproj
|
||||
#:property TargetFramework=net10.0-windows
|
||||
#:package Microsoft.Data.Sqlite@10.0.12
|
||||
// dotnet run scripts/BomViaDb.cs -- <file.asm> [cad_index.db] [output.txt]
|
||||
//
|
||||
// PROVA: a differenza di ExtractCreoStrings.cs (che restringe i candidati a un solo
|
||||
// pattern di codice, tarato sui codici numerici Pro-E), qui l'estrazione è generica -
|
||||
// qualunque run di lettere/cifre/underscore/trattino - e il DB decide se è un codice
|
||||
// vero, così si intercettano anche i nomi alfabetici delle parti commerciali scaricate
|
||||
// come STEP (es. "ANELLO_ESTERNO-1-SOLID1"). L'essere nel DB da solo non basta però:
|
||||
// codici corti o generici (es. "907", "NONE") comparirebbero per puro rumore binario,
|
||||
// quindi si richiede comunque il placement esplicito nel file, come in
|
||||
// ExtractCreoStrings.cs. Stessa espansione ricorsiva sui sotto-assiemi.
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
// Stessa risoluzione ricorsiva dell'albero componenti di Form1.RisolviAlberoComponenti
|
||||
// nella GUI, da riga di comando: usa direttamente PrepStep.AssemblyComponentScanner (via
|
||||
// "#:project", niente ProjectReference separato) invece di duplicare l'euristica di
|
||||
// estrazione/placement. Un'unica fonte di verità - niente rischio che script e GUI
|
||||
// divergano silenziosamente, come accaduto prima di questa riscrittura (vedi
|
||||
// docs/formato-asm.md).
|
||||
using Microsoft.Data.Sqlite;
|
||||
using PrepStep;
|
||||
|
||||
if (args.Length < 1)
|
||||
{
|
||||
@@ -23,20 +22,12 @@ string asmIniziale = args[0];
|
||||
string dbPath = args.Length > 1 ? args[1] : "cad_index.db";
|
||||
string outPath = args.Length > 2 ? args[2] : "BOM.txt";
|
||||
|
||||
// Oltre il 90% dei codici in indice sono numerici Pro-E, ma una minoranza (parti
|
||||
// commerciali scaricate come STEP, es. "ANELLO_ESTERNO-1-SOLID1") ha nomi alfabetici
|
||||
// con trattini/underscore. Niente forma di codice ipotizzata a mano: si tokenizza in
|
||||
// modo generico (run di lettere/cifre/underscore/trattino) e si lascia al DB decidere
|
||||
// cos'è un codice vero - stesso principio già usato per il filtro, esteso anche
|
||||
// all'estrazione dei candidati. Cattura anche "UNI6592"/"CUSCI_ACBB7" dentro
|
||||
// "<UNI6592>" gratis: '<' e '>' non fanno parte del charset e delimitano il token.
|
||||
var codiceRegex = new Regex(@"[A-Za-z0-9_-]{3,}", RegexOptions.Compiled);
|
||||
|
||||
using var connection = new SqliteConnection($"Data Source={dbPath};Mode=ReadOnly");
|
||||
connection.Open();
|
||||
|
||||
// Tutti i codici prt/asm noti all'indice, caricati una volta sola: il lookup per ogni
|
||||
// candidato estratto dal binario diventa O(1) invece di una query per file.
|
||||
// candidato estratto dal binario diventa O(1) invece di una query per file, e serve anche
|
||||
// a EstraiCodiciReferenziati per validare i token generici delle parti commerciali.
|
||||
var codiciNoti = new Dictionary<string, List<(string Tipo, string Percorso)>>(StringComparer.OrdinalIgnoreCase);
|
||||
using (var command = connection.CreateCommand())
|
||||
{
|
||||
@@ -54,51 +45,31 @@ using (var command = connection.CreateCommand())
|
||||
}
|
||||
Console.WriteLine($"Codici prt/asm noti nell'indice: {codiciNoti.Count}");
|
||||
|
||||
// Stesso controllo di placement di ExtractCreoStrings.cs/AssemblyComponentScanner.cs:
|
||||
// la sola presenza del codice nel DB non basta, perché codici corti o generici
|
||||
// (es. "907", "NONE", "start_part") possono comparire per puro rumore binario. Un
|
||||
// componente vero è sempre referenziato in una di queste forme (vedi
|
||||
// docs/formato-asm.md): "Componente ID N (<codice>...)"/"Component id N (<codice>...)",
|
||||
// il codice ripetuto due volte separato da un byte NUL, il campo
|
||||
// "family_name\0<codice>\0", o il codice seguito da un bracket "<codice><GENERICO>".
|
||||
bool HaPlacement(string testo, string codice) =>
|
||||
Regex.IsMatch(testo, $@"Component(?:e)? id \d+ \([^()]*{Regex.Escape(codice)}[^()]*\)", RegexOptions.IgnoreCase)
|
||||
|| testo.Contains(codice + "\0" + codice + "\0")
|
||||
|| testo.Contains("family_name\0" + codice + "\0")
|
||||
|| testo.Contains(codice + "<");
|
||||
|
||||
var codiceProprio = Path.GetFileName(asmIniziale).Split('.')[0];
|
||||
// Stessa esclusione di ExtractCreoStrings.cs: un assieme "_D..." referenzia sempre al
|
||||
// suo interno anche l'assieme generale a cui appartiene (riferimento fantasma verso
|
||||
// l'alto), da escludere subito o si risale tutto l'albero del macchinario.
|
||||
var codiceRadice = codiceProprio.Split('_')[0];
|
||||
var codiceRadice = AssemblyComponentScanner.CodiceRadice(codiceProprio);
|
||||
|
||||
var componentiTrovati = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var scartatiNonInIndice = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var scartatiSenzaPlacement = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var nonTrovati = new List<string>();
|
||||
var codiciVisti = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { codiceProprio, codiceRadice };
|
||||
var daScansionare = new Queue<string>(new[] { asmIniziale });
|
||||
|
||||
while (daScansionare.Count > 0)
|
||||
{
|
||||
var asm = daScansionare.Dequeue();
|
||||
var testo = Encoding.Latin1.GetString(File.ReadAllBytes(asm));
|
||||
var candidati = codiceRegex.Matches(testo).Select(m => m.Value).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var candidato in candidati)
|
||||
foreach (var codice in AssemblyComponentScanner.EstraiCodiciReferenziati(asm, codiciNoti.Keys))
|
||||
{
|
||||
if (!codiciVisti.Add(candidato))
|
||||
// Solo i codici già confermati da EstraiCodiciReferenziati arrivano qui: nessun
|
||||
// rischio del bug "primo fallimento blocca per sempre" (vedi docs/formato-asm.md),
|
||||
// perché il placement è già stato verificato per questo file specifico al suo
|
||||
// interno, da zero, indipendentemente da cosa è successo negli altri file.
|
||||
if (!codiciVisti.Add(codice))
|
||||
continue;
|
||||
if (!codiciNoti.TryGetValue(candidato, out var componenti))
|
||||
if (!codiciNoti.TryGetValue(codice, out var componenti))
|
||||
{
|
||||
scartatiNonInIndice.Add(candidato);
|
||||
nonTrovati.Add(codice);
|
||||
continue;
|
||||
}
|
||||
if (!HaPlacement(testo, candidato))
|
||||
{
|
||||
scartatiSenzaPlacement.Add(candidato);
|
||||
continue;
|
||||
}
|
||||
componentiTrovati.Add(candidato);
|
||||
componentiTrovati.Add(codice);
|
||||
foreach (var (tipo, percorso) in componenti)
|
||||
if (tipo == "asm")
|
||||
daScansionare.Enqueue(percorso);
|
||||
@@ -107,7 +78,6 @@ while (daScansionare.Count > 0)
|
||||
|
||||
File.WriteAllLines(outPath, componentiTrovati);
|
||||
Console.WriteLine($"{componentiTrovati.Count} componenti scritti in {outPath}");
|
||||
Console.WriteLine($"Candidati nell'indice ma senza placement (scartati, {scartatiSenzaPlacement.Count}): {string.Join(", ", scartatiSenzaPlacement.Take(30))}"
|
||||
+ (scartatiSenzaPlacement.Count > 30 ? ", ..." : ""));
|
||||
Console.WriteLine($"Candidati non nell'indice (rumore binario): {scartatiNonInIndice.Count}");
|
||||
Console.WriteLine($"Candidati non nell'indice (scartati, {nonTrovati.Count}): {string.Join(", ", nonTrovati.Take(30))}"
|
||||
+ (nonTrovati.Count > 30 ? ", ..." : ""));
|
||||
return 0;
|
||||
Reference in new issue
Block a user