Compare commits

10 Commits
Author SHA1 Message Date
davideandClaude Sonnet 5 8a8a7d0417 Recognize commercial parts with alphabetic names in .asm scanning
Pro-E numeric codes cover most components, but parts downloaded as STEP
from online catalogs (rings, bushings, knobs...) get alphabetic Creo names
with no predictable shape. Add a second extraction path that tokenizes the
binary generically and validates candidates against the known codes in
cad_index.db, applying the same placement/occurrence rule used for bare
numeric codes. Numeric extraction is unchanged, so missing-from-index
reporting for renamed/moved Pro-E codes still works.

Also load all known prt/asm codes once per scan instead of one query per
candidate, in both RisolviAlberoComponenti and VerificaCompletezzaAssiemi.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 08:41:01 +02:00
davideandClaude Sonnet 5 c75dd21551 Exclude temp_* root folders from the CAD index
temp_paris, temp_davide, temp_studi esterni and the other temp_* folders
are personal/scratch work areas on the share: they hold stale or unrelated
copies of codes that also exist in the official project folders, and a real
.asm can numerically coincide with an unrelated file living in one of these
(see docs/formato-asm.md, the 2502020809 false-positive pulling in an
unrelated machine's assembly from temp_studi esterni/Slitter CD549).

Scan now deletes any already-indexed temp_* rows up front and skips walking
those folders going forward, so downstream queries (Form1.cs, ExtractCreoStrings.cs)
never need to filter them out case by case.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 16:19:37 +02:00
davideandClaude Sonnet 5 e7b5d85f6b Add family_name and bracket-adjacent placement signals for .asm scanning
Two more real placement forms found by comparing against Creo BOM reports:

- A sub-assembly can be referenced only through a "family_name\0<code>\0"
  metadata field instead of a textual "Componente ID" placement or the
  doubled-NUL pattern (case 9284202452 inside 9284208188_D400.asm, which
  contains 9284202452_C01/_C02/_C03 - previously dropped entirely, along
  with its three children).
- A bare numeric code immediately followed by a family-table bracket
  (<code><GENERIC>, e.g. bearing instances like 2510060303<CUSCI_ACBB7>)
  is a real reference to that instance, even though only the generic file
  is indexed on disk. Previously the instance code silently vanished before
  ever reaching the DB lookup; now it survives placement filtering and ends
  up in the "scartati" (discarded) list instead, so it's traceable rather
  than invisible.

docs/formato-asm.md documents both, bringing the total to four recognized
placement forms.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 16:14:52 +02:00
davideandClaude Sonnet 5 a7e0df0247 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>
2026-09-17 15:32:55 +02:00
davideandClaude Sonnet 5 9660c4883d 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 <noreply@anthropic.com>
2026-09-17 14:59:23 +02:00
davideandClaude Sonnet 5 e6de563f60 Distinguish missing STEP from missing CAD source in the final report
A component with no .stp could mean two different things: the CAD source
was never found in the index at all, or it's indexed but nobody exported
the STEP yet. Cross-references the missing codes against cad_index.db
(reusing the phase-2 verification cache when available) and reports each
as EsitoStep.StepMancante vs ComponenteNonTrovato, colored differently.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 13:24:25 +02:00
davideandClaude Sonnet 5 1f6cb2da00 Limit step_finali to the BOM's own components
Sub-components discovered recursively in phase 4 exist only to complete
raw/, not to define what final STEP files are expected - including them
made step_finali report unrelated sub-parts as missing/created. Also drops
the now-unused _componentiRisolti field.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 12:26:39 +02:00
davideandClaude Sonnet 5 338a7327a0 Resolve UNI family-table hardware (screws/nuts/washers) as components
These appear in the .asm binary as a family-table instance name (e.g.
"R6--_-_U6592<UNI6592>"), not a separate file on disk - only the generic
"uni6592.prt" is actually indexed. Extract just the "<UNI...>" content
instead of the full instance name, in both the standalone script and the
production scanner, so standard hardware stops being silently dropped as
"not found in index".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 12:26:31 +02:00
davideandClaude Sonnet 5 52d126372d Redesign the final STEP phase to build a step_finali folder
Previously it only compared the top-level BOM against step/ and reported
matches, without copying anything. Now it unions the BOM codes with the
sub-components resolved in phase 4, copies every matching .stp into a new
step_finali/ folder, and reports what got created vs what's still missing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 10:23:35 +02:00
davideandClaude Sonnet 5 ee408219bd Show a progress dialog while resolving referenced components
"Trova componenti referenziati" ran the recursive BFS synchronously on the
UI thread with no feedback. Runs it on a background task instead and shows
a modal dialog with a percentage bar, updated as each .asm in the queue is
processed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 09:55:14 +02:00
12 changed files with 847 additions and 157 deletions

No files matched your search

+1
View File
@@ -28,6 +28,7 @@ publish/
/*.csv
/raw/
/step/
/step_finali/
*.dxc
# Report BOM generati manualmente da Creo per debug/validazione: mai una dipendenza
+12 -5
View File
@@ -59,8 +59,12 @@ entrambi rimossi: ora l'unico programma è questo, con GUI e indicizzazione inte
sotto-assiemi/parti che contengono (`RisolviAlberoComponenti`), copiandoli anch'essi
in `raw/`.
5. **Verifica completezza e STEP** — "Verifica completezza assiemi" (ricorsivo sui
percorsi originali in indice, non su `raw/`) e "Report STEP realizzati" (confronto
con `step/`).
percorsi originali in indice, non su `raw/`) e "Crea cartella step_finali": solo per i
codici della distinta importata dal CSV (non i sotto-componenti di fase 4, che servono
solo a completare `raw/`) cerca in `step/` il file `<codice>.stp` e lo copia in
`step_finali/` se lo trova. Per i mancanti distingue, incrociando l'indice CAD,
`EsitoStep.StepMancante` (il CAD c'è, manca solo l'esportazione STEP) da
`ComponenteNonTrovato` (il componente non è nemmeno nell'indice) — vedi `ReportStepForm`.
- **Indexer.cs** — logica di scansione del disco e scrittura/aggiornamento del DB
SQLite (`Indexer.ScanAsync`). Cammina tutte le cartelle di primo livello trovate sotto
la radice (scoperte dinamicamente con `Directory.GetDirectories`, non un elenco fisso)
@@ -101,9 +105,12 @@ disco di default `X:\`, hardcoded in `Form1.RadiceDiscoDefault`).
`scripts/` contiene C# file-based apps (.NET 10, `dotnet run scripts/Nome.cs -- arg1 arg2`),
senza csproj — dipendenze NuGet extra si aggiungono con `#:package Nome@versione` in cima
al file. `ExtractCreoStrings.cs` estrae la BOM di un `.asm` risolvendo ricorsivamente i
sotto-assiemi contro `cad_index.db` (stessa logica di `Form1.AssiemeCompleto`); `Analyze.cs`
dumpa il contesto testo/hex attorno a un pattern in un file binario, per investigare a mano
nuovi falsi positivi prima di toccare la regex.
sotto-assiemi contro `cad_index.db` (stessa logica di `Form1.AssiemeCompleto`, solo codici
numerici Pro-E); `BomViaDb.cs` fa lo stesso ma con la tokenizzazione generica validata contro
il DB, usata per intercettare anche i nomi alfabetici delle parti commerciali (vedi
`docs/formato-asm.md`); `Analyze.cs` dumpa il contesto testo/hex attorno a un pattern in un
file binario, per investigare a mano nuovi falsi positivi prima di toccare la regex; `Query.cs`
esegue una query SQL ad-hoc contro `cad_index.db` da riga di comando.
Il formato binario `.asm`, l'euristica di estrazione codici e i falsi positivi noti (ID vista
`ModelView#`, self-reference, riferimenti fantasma a codici superati/rinominati — sintatticamente
@@ -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()
{
@@ -67,8 +83,69 @@ public class AssemblyComponentScannerTests
finally { File.Delete(percorso); }
}
[Fact]
public void Estrae_il_codice_generico_da_istanze_di_family_table_UNI()
{
// Visto su 9244228576.asm: le viti normalizzate compaiono come istanza di family
// table "R12--_-_U6592<UNI6592>", ma il file reale indicizzato è "uni6592.prt".
string percorso = ScriviAsmFinto(
"Componente ID 1 (R12--_-_U6592<UNI6592>.PRT) x Componente ID 2 (R12--_-_U6592<UNI6592>.PRT)");
try
{
var codici = AssemblyComponentScanner.EstraiCodiciReferenziati(percorso, codiciNoti: new[] { "UNI6592" });
Assert.Contains("UNI6592", codici);
Assert.DoesNotContain("R12--_-_U6592<UNI6592>", codici);
}
finally { File.Delete(percorso); }
}
[Fact]
public void File_inesistente_lancia_eccezione_IO() =>
Assert.Throws<DirectoryNotFoundException>(() =>
AssemblyComponentScanner.EstraiCodiciReferenziati(@"C:\non\esiste\pippo.asm.1"));
[Fact]
public void Riconosce_parte_commerciale_con_nome_alfabetico_se_nel_db()
{
// Visto su un caso reale: le parti commerciali scaricate come STEP (es. anelli,
// boccole) hanno nomi Creo alfabetici, non un codice numerico Pro-E - si
// riconoscono solo se il nome esiste davvero come codice indicizzato.
string percorso = ScriviAsmFinto(
"blah Componente ID 11 (ANELLO_ESTERNO-1-SOLID1.PRT) blah RUMORE_CASUALE blah");
try
{
var codiciNoti = new[] { "ANELLO_ESTERNO-1-SOLID1" };
var codici = AssemblyComponentScanner.EstraiCodiciReferenziati(percorso, codiciNoti);
Assert.Contains("ANELLO_ESTERNO-1-SOLID1", codici);
}
finally { File.Delete(percorso); }
}
[Fact]
public void Scarta_token_alfabetico_nel_db_ma_senza_placement()
{
// Un token che coincide per puro caso con un codice DB corto/generico (es. una
// keyword interna di Creo) non basta: senza un vero placement resta rumore.
string percorso = ScriviAsmFinto("start_part blah blah start_part");
try
{
var codiciNoti = new[] { "start_part" };
var codici = AssemblyComponentScanner.EstraiCodiciReferenziati(percorso, codiciNoti);
Assert.DoesNotContain("start_part", codici);
}
finally { File.Delete(percorso); }
}
[Fact]
public void Ignora_token_alfabetici_non_presenti_nel_db()
{
string percorso = ScriviAsmFinto(
"blah Componente ID 1 (QUALCOSA_DI_INESISTENTE-1.PRT) blah");
try
{
var codici = AssemblyComponentScanner.EstraiCodiciReferenziati(percorso, codiciNoti: new[] { "ALTRO_CODICE" });
Assert.DoesNotContain("QUALCOSA_DI_INESISTENTE-1", codici);
}
finally { File.Delete(percorso); }
}
}
+126 -16
View File
@@ -6,35 +6,145 @@ namespace CadSearchGui;
/// <summary>
/// Estrae i codici componente referenziati dentro un file .asm Pro-E/Creo.
/// Il formato è per lo più binario, ma i codici dei sotto-componenti compaiono
/// come stringhe ASCII leggibili al suo interno: si scansiona il file come testo
/// cercando pattern che assomigliano a un codice (stesso stile usato nei nomi file,
/// vedi Indexer.FilenameRegex) e si lascia poi al chiamante il compito di verificarli
/// contro l'indice DB, che scarta automaticamente i falsi positivi (numeri che non
/// corrispondono a nessun codice reale).
/// come stringhe ASCII leggibili al suo interno.
///
/// Verificato su un caso reale confrontando con un report BOM generato da Creo: i
/// Due famiglie di codici, due strategie:
///
/// 1) La maggioranza (&gt;90%) sono codici numerici Pro-E del gestionale, con una forma
/// nota (stesso stile usato nei nomi file, vedi Indexer.FilenameRegex): si riconoscono
/// da soli, senza bisogno del DB, e possono quindi essere segnalati come "referenziati
/// ma non trovati nell'indice" quando un codice risulta rinominato/spostato - il
/// chiamante fa la ricerca nel DB e riporta i mancanti.
///
/// 2) Una minoranza sono parti commerciali scaricate come STEP da cataloghi online
/// (viti, anelli, boccole...), che in Creo hanno nomi alfabetici senza una forma
/// riconoscibile a priori (es. "ANELLO_ESTERNO-1-SOLID1", non un numero). Per questi
/// non esiste un pattern da indovinare: si tokenizza in modo generico (qualunque run
/// di lettere/cifre/underscore/trattino) e si lascia decidere al DB, già caricato dal
/// chiamante, se il token è un codice vero - altrimenti il rumore binario sarebbe
/// ingestibile. Un token così "convalidato" è per costruzione già nell'indice, quindi
/// non contribuisce mai ai "non trovati": per le parti commerciali mancanti dall'indice
/// non c'è modo di distinguerle dal rumore, restano semplicemente invisibili come lo
/// erano prima.
///
/// Verificato su un caso reale confrontando con l'albero componenti di Creo: i
/// riferimenti veri compaiono più volte nel file (una volta per feature/placement),
/// mentre il rumore binario casuale che assomiglia a un codice compare quasi sempre
/// una sola volta. Si scartano quindi i candidati con una sola occorrenza.
/// una sola volta. Si scartano quindi, per entrambe le famiglie, i candidati "nudi" (senza
/// un suffisso di revisione/variante o una forma family-table nota) che non abbiano anche
/// un vero placement testuale nel binario.
///
/// Falso positivo noto ed escluso: gli ID interni di viste salvate, nel formato
/// "ModelView#&lt;numero&gt;", hanno la stessa forma di un codice (8-12 cifre) e
/// Falso positivo noto ed escluso (famiglia 1): gli ID interni di viste salvate, nel
/// formato "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 (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;". Con la tokenizzazione generica "UNI6592"/
/// "CUSCI_ACBB7" vengono catturati da soli: "&lt;" e "&gt;" non fanno parte del charset del
/// token e ne delimitano naturalmente i confini. 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 forma family-table nota) 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, oppure - per la
/// tokenizzazione generica - di puro rumore binario che coincide con un codice DB corto o
/// generico (vedi docs/formato-asm.md). Per questi si richiede anche un vero placement, in
/// una delle quattro 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); 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_..."); il campo "family_name\0&lt;codice&gt;\0" (osservato una
/// sola volta per file, su un sotto-assieme referenziato come family table generica invece
/// che con un placement testuale esplicito); oppure il codice seguito immediatamente da un
/// bracket "&lt;codice&gt;&lt;GENERICO&gt;" - l'istanza di una family table non-UNI vista
/// nel nome (es. "2510060303&lt;CUSCI_ACBB7&gt;", un cuscinetto): il codice dell'istanza
/// non è mai indicizzato a sé (solo il generico tra "&lt;" e "&gt;" lo è), ma questo
/// segnale lo fa comunque sopravvivere fino alla ricerca nel DB invece di scomparire in
/// silenzio come "nudo senza placement" - lì verrà comunque scartato come "non in indice",
/// ma in modo tracciabile.
/// </summary>
static class AssemblyComponentScanner
{
private static readonly Regex CodiceRegex = new(
@"(?<!ModelView#)\b\d{8,12}(?:_[A-Za-z]\d{2,4}|[A-Za-z]{0,2})\b", RegexOptions.Compiled);
private static readonly Regex CodiceNumericoRegex = new(
@"(?<!ModelView#)\b\d{8,12}[A-Za-z]{0,2}(?:_[A-Za-z]?\d{2,4})?\b",
RegexOptions.Compiled);
private static readonly Regex CodiceNudoRegex = new(@"^\d{8,12}[A-Za-z]{0,2}$", RegexOptions.Compiled);
// Nessuna forma ipotizzata a mano per le parti commerciali: qualunque run di
// lettere/cifre/underscore/trattino è un candidato, validato poi contro il DB.
private static readonly Regex TokenGenericoRegex = new(@"[A-Za-z0-9_-]{3,}", RegexOptions.Compiled);
// Suffisso di revisione/variante (es. "_C01", "_D100", "_8188") o prefisso di family
// table nota (UNI/CUSCI_): segnale forte abbastanza da non richiedere un placement
// esplicito, come per i codici numerici nudi con lo stesso suffisso.
private static readonly Regex SuffissoRegex = new(@"_[A-Za-z]?\d{2,4}$", RegexOptions.Compiled);
private const int OccorrenzeMinime = 2;
public static HashSet<string> EstraiCodiciReferenziati(string percorsoAsm)
/// <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];
/// <param name="codiciNoti">Tutti i codici prt/asm noti all'indice (caricati una sola
/// volta dal chiamante, non per-file): usati per validare i token generici della
/// famiglia 2 (parti commerciali). Non serve per la famiglia 1 (codici numerici), che
/// si riconosce dalla forma senza bisogno del DB.</param>
public static HashSet<string> EstraiCodiciReferenziati(string percorsoAsm, ICollection<string>? codiciNoti = null)
{
var testo = Encoding.Latin1.GetString(File.ReadAllBytes(percorsoAsm));
var occorrenze = new Dictionary<string, int>();
foreach (Match m in CodiceRegex.Matches(testo))
bool HaPlacement(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 risultato = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var occorrenzeNumeriche = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
foreach (Match m in CodiceNumericoRegex.Matches(testo))
occorrenzeNumeriche[m.Value] = occorrenzeNumeriche.GetValueOrDefault(m.Value) + 1;
foreach (var (codice, occorrenze) in occorrenzeNumeriche)
{
occorrenze[m.Value] = occorrenze.GetValueOrDefault(m.Value) + 1;
if (occorrenze < OccorrenzeMinime)
continue;
if (CodiceNudoRegex.IsMatch(codice) && !HaPlacement(codice))
continue;
risultato.Add(codice);
}
return occorrenze.Where(kv => kv.Value >= OccorrenzeMinime).Select(kv => kv.Key).ToHashSet();
if (codiciNoti is { Count: > 0 })
{
var occorrenzeGeneriche = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
foreach (Match m in TokenGenericoRegex.Matches(testo))
{
if (codiciNoti.Contains(m.Value))
occorrenzeGeneriche[m.Value] = occorrenzeGeneriche.GetValueOrDefault(m.Value) + 1;
}
foreach (var (codice, occorrenze) in occorrenzeGeneriche)
{
if (risultato.Contains(codice))
continue;
bool formaNota = SuffissoRegex.IsMatch(codice)
|| codice.StartsWith("UNI", StringComparison.OrdinalIgnoreCase)
|| codice.StartsWith("CUSCI_", StringComparison.OrdinalIgnoreCase);
if (formaNota ? occorrenze >= OccorrenzeMinime : HaPlacement(codice))
risultato.Add(codice);
}
}
return risultato;
}
}
+109 -82
View File
@@ -432,13 +432,13 @@ public partial class Form1 : Form
AutoSize = true,
ForeColor = SystemColors.GrayText,
Margin = new Padding(0, 0, 0, 8),
Text = "Dopo aver esportato gli STEP con Creo, premi qui per confrontare i "
+ "componenti della distinta con i file .stp presenti in 'step' e vedere quali "
+ "sono già stati realizzati e quali mancano ancora.",
Text = "Dopo aver esportato gli STEP con Creo, premi qui per confrontare i componenti "
+ "della distinta (e i sotto-componenti trovati in fase 4) con i file .stp presenti "
+ "in 'step', copiare quelli trovati in 'step_finali' e vedere quali mancano ancora.",
};
contenuto.Controls.Add(lblIstruzioniStep);
_btnReport.Text = "Report STEP realizzati";
_btnReport.Text = "Crea cartella step_finali";
_btnReport.Size = new Size(220, 32);
_btnReport.Click += (_, _) => GeneraReportStep();
contenuto.Controls.Add(_btnReport);
@@ -551,33 +551,65 @@ public partial class Form1 : Form
if (_percorsoInputCorrente is null || _righeCorrenti is null)
return;
var cartellaStep = Path.Combine(Path.GetDirectoryName(Path.GetFullPath(_percorsoInputCorrente))!, "step");
string radice = Path.GetDirectoryName(Path.GetFullPath(_percorsoInputCorrente))!;
var cartellaStep = Path.Combine(radice, "step");
if (!Directory.Exists(cartellaStep))
{
ImpostaStatoImport($"Cartella 'step' non trovata in {cartellaStep}.", Color.DarkRed);
return;
}
var codiciConStep = new HashSet<string>(
Directory.GetFiles(cartellaStep, "*.stp").Select(Path.GetFileNameWithoutExtension)!);
var descrizioni = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var r in _righeCorrenti)
descrizioni[r.Componente] = r.Descrizione;
var righeReport = _righeCorrenti
.Select(r => (r.Componente, r.Descrizione, Realizzato: codiciConStep.Contains(r.Componente)))
// Solo i componenti della distinta importata dal CSV: i sotto-componenti trovati in
// fase 4 (_componentiRisolti) servono a completare "raw", non entrano nello STEP finale.
var tuttiICodici = _righeCorrenti.Select(r => r.Componente)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
int realizzati = righeReport.Count(r => r.Realizzato);
// Per i codici senza STEP, distingue "il CAD c'è ma lo STEP non è stato esportato" da
// "il componente non è nemmeno nell'indice CAD" (riusa la verifica già fatta in fase 2/3
// se disponibile, altrimenti la rifà).
var cadTrovati = _ultimaVerifica ?? TrovaPercorsiCad(tuttiICodici);
var cartellaStepFinali = Path.Combine(radice, "step_finali");
Directory.CreateDirectory(cartellaStepFinali);
var righeReport = new List<(string Componente, string Descrizione, EsitoStep Esito)>();
foreach (var codice in tuttiICodici)
{
var percorsoStp = Path.Combine(cartellaStep, codice + ".stp");
EsitoStep esito;
if (File.Exists(percorsoStp))
{
File.Copy(percorsoStp, Path.Combine(cartellaStepFinali, codice + ".stp"), overwrite: true);
esito = EsitoStep.Creato;
}
else
{
esito = cadTrovati.ContainsKey(codice) ? EsitoStep.StepMancante : EsitoStep.ComponenteNonTrovato;
}
righeReport.Add((codice, descrizioni.GetValueOrDefault(codice, ""), esito));
}
int creati = righeReport.Count(r => r.Esito == EsitoStep.Creato);
int stepMancanti = righeReport.Count(r => r.Esito == EsitoStep.StepMancante);
int nonTrovati = righeReport.Count(r => r.Esito == EsitoStep.ComponenteNonTrovato);
ImpostaStatoImport(
$"Report STEP: {realizzati} componenti realizzati su {righeReport.Count} "
+ $"({righeReport.Count - realizzati} mancanti).",
$"STEP finali: {creati} copiati, {stepMancanti} da esportare (CAD presente), "
+ $"{nonTrovati} componenti non nell'indice, su {righeReport.Count} totali. "
+ $"Cartella: {cartellaStepFinali}.",
Color.DarkGreen);
using var dialog = new ReportStepForm(righeReport, cartellaStep);
using var dialog = new ReportStepForm(righeReport, cartellaStepFinali);
dialog.ShowDialog(this);
}
/// <summary>Svuota in un colpo solo le cartelle "raw" e "step" nella root (stessa cartella
/// base del file distinta importato), previa un'unica conferma. Non tocca l'indice CAD: solo
/// le copie locali generate dalla pipeline.</summary>
/// <summary>Svuota in un colpo solo le cartelle "raw", "step" e "step_finali" nella root
/// (stessa cartella base del file distinta importato), previa un'unica conferma. Non tocca
/// l'indice CAD: solo le copie locali generate dalla pipeline.</summary>
private void PulisciRawEStep()
{
string radice;
@@ -595,12 +627,12 @@ public partial class Form1 : Form
radice = dialog.SelectedPath;
}
var cartelle = new[] { Path.Combine(radice, "raw"), Path.Combine(radice, "step") }
var cartelle = new[] { Path.Combine(radice, "raw"), Path.Combine(radice, "step"), Path.Combine(radice, "step_finali") }
.Where(Directory.Exists)
.ToList();
if (cartelle.Count == 0)
{
ImpostaStatoImport($"Nessuna cartella 'raw' o 'step' trovata in {radice}.", Color.DarkRed);
ImpostaStatoImport($"Nessuna cartella 'raw', 'step' o 'step_finali' trovata in {radice}.", Color.DarkRed);
return;
}
@@ -630,57 +662,63 @@ public partial class Form1 : Form
ImpostaStatoImport($"{eliminati} file eliminati da {string.Join(" e ", cartelle)}.", Color.DarkGreen);
}
/// <summary>Tutti i codici prt/asm noti all'indice, con i relativi percorsi (ultima
/// versione, un solo file per tipo - stesso criterio di dedup per data_modifica DESC
/// usato ovunque in questa classe). Caricato in un'unica query invece che una per
/// codice: serve sia a risolvere i percorsi dei componenti trovati, sia a
/// AssemblyComponentScanner per validare i token generici (parti commerciali).</summary>
private static Dictionary<string, List<(string Tipo, string Percorso)>> CaricaCodiciNoti(string dbPath)
{
var codiciNoti = new Dictionary<string, List<(string Tipo, string Percorso)>>(StringComparer.OrdinalIgnoreCase);
using var connection = new SqliteConnection($"Data Source={dbPath};Mode=ReadOnly");
connection.Open();
using var command = connection.CreateCommand();
command.CommandText = "SELECT codice, tipo_file, percorso_completo FROM files_ultima_versione "
+ "WHERE LOWER(tipo_file) IN ('prt', 'asm') ORDER BY codice, tipo_file, data_modifica DESC";
using var reader = command.ExecuteReader();
while (reader.Read())
{
string codice = reader.GetString(0);
string tipo = reader.GetString(1).ToLowerInvariant();
var lista = codiciNoti.TryGetValue(codice, out var l) ? l : codiciNoti[codice] = new();
if (!lista.Any(x => x.Tipo == tipo))
lista.Add((tipo, reader.GetString(2)));
}
return codiciNoti;
}
/// <summary>Risolve ricorsivamente l'albero dei componenti a partire dai file .asm indicati
/// (già copiati in "raw"): per ogni assieme legge i codici referenziati al suo interno, li
/// cerca nell'indice, copia in "raw" quelli trovati e, se sono a loro volta assiemi, li
/// accoda per essere scansionati finché non restano solo PRT (foglie) o codici non presenti
/// nell'indice. Usata dal bottone "Trova componenti referenziati".</summary>
private (List<(string Codice, string Tipo, string Percorso)> Trovati, List<string> NonTrovati) RisolviAlberoComponenti(
IEnumerable<string> asmIniziali)
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>();
if (daScansionare.Count == 0 || _dbPath is null)
return (trovati, codiciNonTrovati);
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') "
+ "ORDER BY tipo_file, data_modifica DESC";
var pCodice = command.Parameters.Add("@codice", Microsoft.Data.Sqlite.SqliteType.Text);
var codiciNoti = CaricaCodiciNoti(_dbPath);
int processati = 0;
while (daScansionare.Count > 0)
{
var asm = daScansionare.Dequeue();
foreach (var codice in AssemblyComponentScanner.EstraiCodiciReferenziati(asm))
processati++;
foreach (var codice in AssemblyComponentScanner.EstraiCodiciReferenziati(asm, codiciNoti.Keys))
{
if (!codiciVisti.Add(codice))
continue;
pCodice.Value = codice;
// Lo stesso codice+tipo può esistere in più cartelle radice (duplicati sulla
// share): ordinato per data_modifica DESC, si tiene solo il più recente per tipo.
var componenti = new List<(string Tipo, string Percorso)>();
var tipiVisti = new HashSet<string>();
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
string tipoLower = reader.GetString(0).ToLowerInvariant();
if (tipiVisti.Add(tipoLower))
{
componenti.Add((tipoLower, reader.GetString(1)));
}
}
}
if (componenti.Count == 0)
if (!codiciNoti.TryGetValue(codice, out var componenti))
{
codiciNonTrovati.Add(codice);
continue;
@@ -696,12 +734,13 @@ public partial class Form1 : Form
}
}
}
progress?.Report((processati, processati + daScansionare.Count));
}
return (trovati, codiciNonTrovati);
}
private void TrovaPrtReferenziati()
private async void TrovaPrtReferenziati()
{
if (_ultimaCartellaRaw is null || !Directory.Exists(_ultimaCartellaRaw))
return;
@@ -714,8 +753,19 @@ public partial class Form1 : Form
// L'assieme generale è già in raw come qualsiasi altro assieme importato (vedi
// CreaCartellaRaw), quindi rientra qui senza bisogno di un caso speciale.
var asmIniziali = Directory.GetFiles(_ultimaCartellaRaw).Where(f => Path.GetFileName(f).Contains(".asm."));
var (trovati, codiciNonTrovati) = RisolviAlberoComponenti(asmIniziali);
var asmIniziali = Directory.GetFiles(_ultimaCartellaRaw).Where(f => Path.GetFileName(f).Contains(".asm.")).ToList();
using var progressForm = new ProgressoComponentiForm();
var progress = new Progress<(int Fatti, int Totale)>(p => progressForm.Aggiorna(p.Fatti, p.Totale));
var task = Task.Run(() => RisolviAlberoComponenti(asmIniziali, progress));
progressForm.Shown += async (_, _) =>
{
await task;
progressForm.Close();
};
progressForm.ShowDialog(this);
var (trovati, codiciNonTrovati) = await task;
int numPrt = trovati.Count(r => r.Tipo == "prt");
int numAsm = trovati.Count(r => r.Tipo == "asm");
@@ -742,46 +792,23 @@ public partial class Form1 : Form
return;
}
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') "
+ "ORDER BY tipo_file, data_modifica DESC";
var pCodice = command.Parameters.Add("@codice", Microsoft.Data.Sqlite.SqliteType.Text);
var cacheComponenti = new Dictionary<string, List<(string Tipo, string Percorso)>>();
var codiciNoti = CaricaCodiciNoti(_dbPath);
var cacheCodiciFile = new Dictionary<string, HashSet<string>>();
List<(string Tipo, string Percorso)> CercaComponenti(string codice)
{
if (cacheComponenti.TryGetValue(codice, out var giaTrovati))
return giaTrovati;
pCodice.Value = codice;
var risultato = new List<(string Tipo, string Percorso)>();
var tipiVisti = new HashSet<string>();
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
string tipo = reader.GetString(0).ToLowerInvariant();
if (tipiVisti.Add(tipo))
{
risultato.Add((tipo, reader.GetString(1)));
}
}
}
cacheComponenti[codice] = risultato;
return risultato;
}
List<(string Tipo, string Percorso)> CercaComponenti(string codice) =>
codiciNoti.TryGetValue(codice, out var trovati) ? trovati : new List<(string, string)>();
bool AssiemeCompleto(string percorsoAsmIniziale)
{
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)
{
@@ -790,7 +817,7 @@ public partial class Form1 : Form
{
try
{
codiciReferenziati = AssemblyComponentScanner.EstraiCodiciReferenziati(file);
codiciReferenziati = AssemblyComponentScanner.EstraiCodiciReferenziati(file, codiciNoti.Keys);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
+17 -1
View File
@@ -65,9 +65,25 @@ public static class Indexer
schemaCmd.ExecuteNonQuery();
}
// Le cartelle "temp_*" (temp_paris, temp_davide, temp_studi esterni, ...) sono lavori
// personali/temporanei: contengono spesso copie datate o versioni scollegate di codici
// che esistono ANCHE nel resto della share, con lo stesso codice ma contenuto diverso.
// Un .asm nella struttura "ufficiale" può referenziare per coincidenza numerica un file
// dentro una di queste cartelle (vedi docs/formato-asm.md, falso positivo su
// "2502020809"), trascinando dentro un intero assieme di un progetto scollegato. Vanno
// escluse a monte, non solo filtrate caso per caso nelle singole query.
using (var deleteTempCmd = connection.CreateCommand())
{
deleteTempCmd.CommandText = @"DELETE FROM files WHERE cartella_radice LIKE 'temp\_%' ESCAPE '\'";
deleteTempCmd.ExecuteNonQuery();
}
// Indicizza tutte le cartelle di primo livello sotto la radice (non un elenco fisso),
// così una nuova cartella creata sulla share entra nell'indice senza modificare il codice.
var cartelleRadice = Directory.GetDirectories(root).Select(p => Path.GetFileName(p)!).ToArray();
var cartelleRadice = Directory.GetDirectories(root)
.Select(p => Path.GetFileName(p)!)
.Where(c => !c.StartsWith("temp_", StringComparison.OrdinalIgnoreCase))
.ToArray();
var previsti = new Dictionary<string, long>();
foreach (var cartella in cartelleRadice)
+37
View File
@@ -0,0 +1,37 @@
namespace CadSearchGui;
public sealed class ProgressoComponentiForm : Form
{
private readonly Label _lbl = new();
private readonly ProgressBar _bar = new();
public ProgressoComponentiForm()
{
Text = "Ricerca componenti referenziati in corso...";
FormBorderStyle = FormBorderStyle.FixedDialog;
StartPosition = FormStartPosition.CenterParent;
MinimizeBox = false;
MaximizeBox = false;
ControlBox = false;
ClientSize = new Size(400, 88);
_lbl.Text = "Preparazione...";
_lbl.AutoSize = true;
_lbl.Location = new Point(16, 16);
_bar.Location = new Point(16, 44);
_bar.Size = new Size(368, 24);
_bar.Minimum = 0;
_bar.Maximum = 100;
Controls.Add(_lbl);
Controls.Add(_bar);
}
public void Aggiorna(int fatti, int totale)
{
int percento = totale > 0 ? (int)Math.Clamp(100.0 * fatti / totale, 0, 100) : 0;
_bar.Value = percento;
_lbl.Text = $"{fatti} / {totale} componenti analizzati ({percento}%)";
}
}
+31 -12
View File
@@ -2,15 +2,28 @@ using System.Diagnostics;
namespace CadSearchGui;
/// <summary>Mostra, per ogni componente della distinta base, se lo STEP corrispondente
/// è già stato esportato (file .stp presente in "step") o manca ancora.</summary>
public enum EsitoStep
{
/// <summary>File .stp trovato in "step" e copiato in "step_finali".</summary>
Creato,
/// <summary>Il componente è nell'indice CAD (asm/prt trovato), ma nessuno STEP esportato.</summary>
StepMancante,
/// <summary>Il componente non è nemmeno nell'indice CAD.</summary>
ComponenteNonTrovato,
}
/// <summary>Mostra, per ogni componente della distinta base, se lo STEP corrispondente è stato
/// trovato in "step" e copiato in "step_finali", se manca solo lo STEP (il CAD sorgente c'è,
/// ma non è ancora stato esportato) o se manca anche il CAD sorgente nell'indice.</summary>
public class ReportStepForm : Form
{
public ReportStepForm(List<(string Componente, string Descrizione, bool Realizzato)> righe, string cartellaStep)
public ReportStepForm(List<(string Componente, string Descrizione, EsitoStep Esito)> righe, string cartellaStepFinali)
{
int realizzati = righe.Count(r => r.Realizzato);
int creati = righe.Count(r => r.Esito == EsitoStep.Creato);
int stepMancanti = righe.Count(r => r.Esito == EsitoStep.StepMancante);
int nonTrovati = righe.Count(r => r.Esito == EsitoStep.ComponenteNonTrovato);
Text = "Report STEP realizzati";
Text = "Report STEP finali";
ClientSize = new Size(720, 440);
StartPosition = FormStartPosition.CenterParent;
Font = new Font("Segoe UI", 9.5f);
@@ -21,8 +34,8 @@ public class ReportStepForm : Form
AutoSize = false,
Height = 32,
Padding = new Padding(12, 8, 12, 8),
Text = $"{realizzati} componenti realizzati su {righe.Count} ({righe.Count - realizzati} mancanti), "
+ $"confrontati con \"{cartellaStep}\".",
Text = $"{creati} copiati, {stepMancanti} da esportare (CAD presente), "
+ $"{nonTrovati} non nell'indice, su {righe.Count} totali. In \"{cartellaStepFinali}\".",
};
var grid = new DataGridView
@@ -42,16 +55,22 @@ public class ReportStepForm : Form
grid.Columns["descrizione"]!.FillWeight = 55;
grid.Columns["step"]!.FillWeight = 25;
foreach (var (componente, descrizione, realizzato) in righe.OrderBy(r => r.Realizzato).ThenBy(r => r.Componente))
foreach (var (componente, descrizione, esito) in righe.OrderBy(r => r.Esito).ThenBy(r => r.Componente))
{
int i = grid.Rows.Add(componente, descrizione, realizzato ? "Realizzato" : "Mancante");
grid.Rows[i].DefaultCellStyle.ForeColor = realizzato ? Color.DarkGreen : Color.DarkRed;
var (testo, colore) = esito switch
{
EsitoStep.Creato => ("Creato", Color.DarkGreen),
EsitoStep.StepMancante => ("Da esportare (CAD presente)", Color.DarkOrange),
_ => ("Componente non trovato nel server", Color.DarkRed),
};
int i = grid.Rows.Add(componente, descrizione, testo);
grid.Rows[i].DefaultCellStyle.ForeColor = colore;
}
var btnApriCartella = new Button { Text = "Apri cartella step", Dock = DockStyle.Bottom, Height = 34 };
var btnApriCartella = new Button { Text = "Apri cartella step_finali", Dock = DockStyle.Bottom, Height = 34 };
btnApriCartella.Click += (_, _) =>
{
try { Process.Start("explorer.exe", cartellaStep); }
try { Process.Start("explorer.exe", cartellaStepFinali); }
catch (Exception ex)
{
MessageBox.Show(this, $"Impossibile aprire Esplora risorse:\n{ex.Message}", "Errore",
+198 -5
View File
@@ -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#<numero>` (es.
@@ -54,6 +65,71 @@ 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, in una delle quattro
forme osservate nel binario:
- `Componente ID N (<codice>...)`/`Component id N (<codice>...)` (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,
`<codice>\0<codice>\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ì);
- il campo `family_name\0<codice>\0` (con `name` e `family_name` uguali,
entrambi pari al codice). Osservato una sola volta per file, su un
sotto-assieme referenziato come family table generica invece che con un
placement testuale esplicito - caso `9284202452` dentro
`9284208188_D400.asm`: il sotto-assieme (che contiene a sua volta
`9284202452_C01/_C02/_C03`) non ha nessun `Componente ID (...)` né coppia
NUL, solo questo campo `family_name`. Verificato su 5 file diversi
(`9284208188_D400/D600/D610/D620/D630.asm`): l'unica occorrenza di
`family_name\0<codice>\0` in ciascuno corrisponde sempre a un componente
reale confermato dalla BOM Creo, mai al riferimento fantasma verso l'alto;
- il codice seguito immediatamente da un bracket, `<codice><GENERICO>`
(senza NUL né spazio in mezzo) - l'istanza di una family table non-UNI
(es. `2510060303<CUSCI_ACBB7>`, un cuscinetto, confermato aprendo l'assieme
in Creo: descrizione "CUSC.OBL.SFERE 2COR 17X40"). Il codice dell'istanza
non è mai indicizzato a sé sulla share (solo il generico tra `<` e `>` lo
è - stesso meccanismo di UNI, vedi sotto), quindi anche riconosciuto come
placement reale finisce comunque negli "scartati" alla ricerca nel DB - ma
in modo tracciabile, invece di sparire in silenzio prima ancora di essere
cercato.
Un riferimento incidentale al nome di un altro assieme, anche se ripetuto più
volte, non compare mai in nessuna delle quattro 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 uno dei quattro segnali
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
di una rinumerazione) con la stessa identica struttura sintattica di un
@@ -70,10 +146,120 @@ 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 (<codice>.PRT)`/`Component id N (<codice>.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 `<UNI6592>` 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.
## Parti commerciali con nome alfabetico (validazione via DB)
Oltre il 90% dei codici indicizzati sono numerici Pro-E (regex sopra), ma una minoranza
sono parti commerciali scaricate come STEP da cataloghi online (anelli, boccole, guarnizioni,
manopole...) che in Creo hanno un nome **alfabetico**, assegnato dal file STEP di origine, non
un codice del gestionale — es. `NOME_PARTE-1-SOLID1`, senza nessuna cifra riconoscibile. Non
esiste un pattern comune da ipotizzare a priori per questi nomi: variano da fornitore a
fornitore.
Per questa famiglia l'estrazione si inverte: invece di restringere la regex a una forma nota
(come per i codici numerici), si tokenizza il binario in modo generico —
```
regex: [A-Za-z0-9_-]{3,}
```
— e si lascia decidere al DB (`cad_index.db`, già caricato in memoria una sola volta dal
chiamante, non per-file) se il token è un codice reale. Senza questo filtro il rumore binario
sarebbe ingestibile: su un assieme con poche migliaia di stringhe, la tokenizzazione generica
produce tipicamente qualche migliaio di candidati, di cui solo una manciata sopravvive al
controllo DB.
L'essere nel DB da solo non basta, per lo stesso motivo dei codici numerici "nudi": codici
corti o generici (es. una keyword interna di Creo come `start_part`, o un codice indicizzato
per errore/caso limite) possono comparire per puro rumore. Si applica quindi la stessa regola
di validazione della famiglia numerica:
- un token con un suffisso di revisione/variante (`_[A-Za-z]?\d{2,4}` in coda, es. `_C01`,
`_D100`) o un prefisso di family table nota (`UNI`, `CUSCI_`) è considerato "forma nota":
basta che occorra almeno 2 volte nel file;
- un token "nudo" (nessuno dei due segnali) richiede invece un vero placement testuale, come
per i codici numerici nudi (stesse quattro forme viste sopra).
Una parte commerciale referenziata ma **non ancora indicizzata** non è distinguibile dal
rumore con questo metodo: resta invisibile, come lo era prima che questa famiglia venisse
gestita. La famiglia numerica invece resta rilevabile anche quando manca dall'indice
(rinominata/spostata), perché si riconosce dalla forma senza bisogno del DB — è per questo
che le due famiglie restano due passaggi distinti invece di un unico tokenizer generico
sempre validato contro il DB: unificarle farebbe perdere la segnalazione dei codici numerici
mancanti (vedi "Riferimenti fantasma" sopra).
`AssemblyComponentScanner.EstraiCodiciReferenziati` accetta quindi un parametro opzionale
`codiciNoti` (l'insieme di tutti i codici prt/asm noti all'indice) usato solo per questa
seconda famiglia; se omesso, si estraggono solo i codici numerici. `scripts/BomViaDb.cs`
è il prototipo standalone usato per validare l'approccio prima di portarlo nella GUI.
## Componenti normalizzati (family table UNI, CUSCI, ...)
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<UNI6592>.PRT)
2510060303<CUSCI_ACBB7>
```
`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.<versione>`, `cusci_acbb7.prt.<versione>`,
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+|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). 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
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`
@@ -82,11 +268,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 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.
- [`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) + `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, e caricano una volta sola tutti i codici noti all'indice (`Form1.CaricaCodiciNoti`) sia per validare i token generici delle parti commerciali sia per risolvere i percorsi dei componenti trovati, senza una query SQL per candidato.
- [`scripts/ExtractCreoStrings.cs`](../scripts/ExtractCreoStrings.cs) e [`scripts/BomViaDb.cs`](../scripts/BomViaDb.cs) — stessa BFS ricorsiva standalone da riga di comando (copia indipendente della stessa euristica, nessun `ProjectReference` verso la GUI). Usati 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ù
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
+113
View File
@@ -0,0 +1,113 @@
#: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;
using Microsoft.Data.Sqlite;
if (args.Length < 1)
{
Console.WriteLine("Uso: dotnet run BomViaDb.cs -- <file.asm> [cad_index.db] [output.txt]");
return 1;
}
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.
var codiciNoti = new Dictionary<string, List<(string Tipo, string Percorso)>>(StringComparer.OrdinalIgnoreCase);
using (var command = connection.CreateCommand())
{
command.CommandText = "SELECT codice, tipo_file, percorso_completo FROM files_ultima_versione "
+ "WHERE LOWER(tipo_file) IN ('prt', 'asm') ORDER BY codice, tipo_file, data_modifica DESC";
using var reader = command.ExecuteReader();
while (reader.Read())
{
string codice = reader.GetString(0);
string tipo = reader.GetString(1).ToLowerInvariant();
var lista = codiciNoti.TryGetValue(codice, out var l) ? l : codiciNoti[codice] = new();
if (!lista.Any(x => x.Tipo == tipo))
lista.Add((tipo, reader.GetString(2)));
}
}
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 componentiTrovati = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
var scartatiNonInIndice = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
var scartatiSenzaPlacement = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
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)
{
if (!codiciVisti.Add(candidato))
continue;
if (!codiciNoti.TryGetValue(candidato, out var componenti))
{
scartatiNonInIndice.Add(candidato);
continue;
}
if (!HaPlacement(testo, candidato))
{
scartatiSenzaPlacement.Add(candidato);
continue;
}
componentiTrovati.Add(candidato);
foreach (var (tipo, percorso) in componenti)
if (tipo == "asm")
daScansionare.Enqueue(percorso);
}
}
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}");
return 0;
+106 -34
View File
@@ -1,13 +1,12 @@
#:package Microsoft.Data.Sqlite@10.0.12
// dotnet run scripts/ExtractCreoStrings.cs -- <file.asm> [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#<numero>" 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, 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#<numero>" 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;
@@ -22,7 +21,20 @@ string asmIniziale = args[0];
string dbPath = args.Length > 1 ? args[1] : "cad_index.db";
string outPath = args.Length > 2 ? args[2] : "BOM.txt";
var codiceRegex = new Regex(@"(?<!ModelView#)\b\d{8,12}(?:_[A-Za-z]\d{2,4}|[A-Za-z]{0,2})\b", RegexOptions.Compiled);
// 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<UNI6592>.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.<versione>"/"cusci_acbb7.prt.<versione>" - 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(
@"(?<!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);
const int occorrenzeMinime = 2;
HashSet<string> EstraiCodiciReferenziati(string percorsoAsm)
@@ -31,56 +43,116 @@ HashSet<string> EstraiCodiciReferenziati(string percorsoAsm)
var occorrenze = new Dictionary<string, int>();
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, in una delle quattro forme osservate nel binario: "Componente ID N
// (<codice>...)"/"Component id N (<codice>...)" (la stringa cambia con la
// localizzazione Creo del file); 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_..."); il campo "family_name\0<codice>\0"
// (osservato una sola volta per file, su un sotto-assieme referenziato come family
// table generica invece che con un placement testuale esplicito); oppure il codice
// seguito immediatamente da un bracket "<codice><GENERICO>" - l'istanza di una family
// table non-UNI vista nel nome (es. "2510060303<CUSCI_ACBB7>", un cuscinetto): il
// codice dell'istanza non è mai indicizzato a sé (solo il generico tra "<" e ">" lo è,
// vedi sotto), ma questo segnale lo fa comunque sopravvivere fino alla ricerca nel DB
// invece di scomparire in silenzio come "nudo senza placement" - lì verrà comunque
// scartato come "non in indice", ma in modo tracciabile. 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)
|| testo.Contains(codice + "\0" + codice + "\0")
|| testo.Contains("family_name\0" + codice + "\0")
|| testo.Contains(codice + "<");
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<string, List<(string Tipo, string Percorso)>> CercaComponenti(IReadOnlyCollection<string> codici)
{
pCodice.Value = codice;
var risultato = new List<(string, string)>();
var tipiVisti = new HashSet<string>();
var risultato = new Dictionary<string, List<(string, string)>>(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();
// Lo stesso codice+tipo può esistere in più cartelle radice (duplicati/copie storiche
// sulla share, es. un generico family table ripetuto in 5 progetti scollegati): ordinato
// per data_modifica DESC, si tiene solo il più recente per codice+tipo (stessa regola di
// Form1.RisolviAlberoComponenti, vedi docs/formato-asm.md).
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') "
+ "ORDER BY codice, tipo_file, data_modifica DESC";
int i = 0;
foreach (var codice in codici)
command.Parameters.AddWithValue(nomiParametri[i++], codice.ToLowerInvariant());
var tipiVisti = new Dictionary<string, HashSet<string>>(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<string>();
if (visti.Add(tipo))
risultato[codiceDb].Add((tipo, reader.GetString(2)));
}
return risultato;
}
var codiciVisti = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { Path.GetFileName(asmIniziale).Split('.')[0] };
var daVisitare = new Queue<string>();
daVisitare.Enqueue(asmIniziale);
// 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<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 (daVisitare.Count > 0)
while (daScansionare.Count > 0)
{
var file = daVisitare.Dequeue();
foreach (var codice in EstraiCodiciReferenziati(file))
var asm = daScansionare.Dequeue();
var candidati = EstraiCodiciReferenziati(asm)
.Where(codiciVisti.Add)
.ToList();
var trovatiPerCodice = CercaComponenti(candidati);
foreach (var codice in candidati)
{
if (!codiciVisti.Add(codice))
continue;
var componenti = CercaComponenti(codice);
var componenti = trovatiPerCodice[codice];
if (componenti.Count == 0)
{
nonTrovati.Add(codice);
continue;
}
componentiTrovati.Add(codice);
var percorsoAsm = componenti.FirstOrDefault(c => c.Tipo == "asm").Percorso;
if (percorsoAsm is not null)
daVisitare.Enqueue(percorsoAsm);
foreach (var (tipo, percorso) in componenti)
if (tipo == "asm")
daScansionare.Enqueue(percorso);
}
}
+18
View File
@@ -0,0 +1,18 @@
#:package Microsoft.Data.Sqlite@10.0.12
// dotnet run scripts/Query.cs -- <cad_index.db> <sql>
using Microsoft.Data.Sqlite;
using var connection = new SqliteConnection($"Data Source={args[0]};Mode=ReadOnly");
connection.Open();
using var command = connection.CreateCommand();
command.CommandText = args[1];
using var reader = command.ExecuteReader();
int righe = 0;
while (reader.Read())
{
righe++;
var valori = Enumerable.Range(0, reader.FieldCount).Select(i => reader.IsDBNull(i) ? "" : reader.GetValue(i).ToString());
Console.WriteLine(string.Join(" | ", valori));
}
Console.WriteLine($"({righe} righe)");
return 0;