From 54f267b5089556e04790bec5691241048d9e8983 Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Tue, 22 Sep 2026 11:52:16 +0200 Subject: [PATCH] Copia in raw i file che servono a Creo, non solo quelli della distinta MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Creo Distributed Batch falliva su 8 dei 22 assiemi con "[-4] Dati di input non completi", un messaggio che non nomina il file mancante. Erano due cause distinte, entrambe emerse aprendo gli assiemi da raw/ e cercando nell'albero il componente con l'icona rossa. Copia sbagliata del componente. Lo stesso codice esiste spesso in piu' cartelle della share con pari versione e contenuto diverso (13% dei codici): non sono revisioni, sono istantanee parallele lasciate da studi, allestimenti fiera e cartelle temporanee. La vista dell'indice rompeva il pareggio in modo arbitrario, e poteva vincere una copia obsoleta - nascondendo del tutto quella buona. Creo poi caricava un modello diverso da quello atteso, non risolveva i vincoli e non apriva l'assieme. Il .asm non dice quale copia usa (ne' percorso, ne' UOBJ_ID del figlio, ne' revisione attesa), quindi si replica la regola di Creo: vince la copia nella stessa cartella dell'assieme che la referenzia. La vista ora rompe il pareggio in modo deterministico e viene ricreata a ogni scan, cosi' un DB esistente prende la definizione nuova senza reindicizzare. Componenti soppressi. Spariscono da "members" ma restano citati dalla feature che li posiziona, e il loro file serve comunque per aprire l'assieme. Da quando la lettura usa il parser esatto - che legge solo "members", ed e' la scelta giusta per la BOM - non venivano piu' copiati. Ora si copiano a parte, senza entrare nel conteggio dei "non trovati": non sono componenti della distinta, segnalarli come mancanti sarebbe un falso allarme. Documentate entrambe in docs/formato-asm-record.md §7.3 e §7.4, insieme alla distinzione che le accomuna: la lista della distinta e la lista dei file necessari a Creo non coincidono, e la seconda e' piu' lunga. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 12 ++- PrepStep.Tests/AsmRecordReaderTests.cs | 33 ++++++ PrepStep/AsmRecordReader.cs | 30 ++++++ PrepStep/Form1.cs | 138 +++++++++++++++++++++++-- PrepStep/Indexer.cs | 13 ++- docs/architettura.md | 19 +++- docs/db-cad-index.md | 26 ++++- docs/formato-asm-record.md | 63 +++++++++++ 8 files changed, 316 insertions(+), 18 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 493d793..9a476f0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -63,10 +63,14 @@ designer visuale): sotto-assiemi/parti (`RisolviAlberoComponenti`), copiandoli anch'essi in `raw/`. Usa `AsmRecordReader` (parser esatto dei record binari, `docs/formato-asm-record.md`) e ripiega su `AssemblyComponentScanner` (euristica testuale, `docs/formato-asm.md`) solo se il file - non rispetta il formato validato (`InvalidDataException`). ⚠️ Limite noto non coperto dal - fallback: un **assieme** con family table (raro, non solo le parti normalizzate possono - averla) può dare un risultato silenziosamente incompleto senza lanciare eccezione — vedi - `docs/formato-asm-record.md` §7. + non rispetta il formato validato (`InvalidDataException`). Due punti da non confondere, + entrambi imparati facendo fallire Creo (`docs/formato-asm-record.md` §7.3 e §7.4): + i **componenti soppressi** non sono nella distinta ma il loro file serve per aprire + l'assieme (`EstraiCodiciSoppressi`, copiati senza finire fra i "non trovati"); e quando lo + stesso codice esiste in più cartelle della share — succede per il 13% dei codici — va + copiata la copia nella **stessa cartella dell'assieme che la referenzia** + (`ScegliPerCartella`), perché il `.asm` non registra quale usa e sbagliarla impedisce a + Creo di aprire l'assieme. 5. **Verifica completezza** — mostra i `.asm` della distinta già in `raw/`, trascinabili su Creo per l'esportazione STEP (Distributed Batch, manuale, fuori dall'app); "Verifica completezza assiemi" controlla ricorsivamente sull'indice (non su `raw/`, stessa diff --git a/PrepStep.Tests/AsmRecordReaderTests.cs b/PrepStep.Tests/AsmRecordReaderTests.cs index f5f43e7..8875761 100644 --- a/PrepStep.Tests/AsmRecordReaderTests.cs +++ b/PrepStep.Tests/AsmRecordReaderTests.cs @@ -196,6 +196,39 @@ public class AsmRecordReaderTests Assert.Single(membri); } + [Fact] + public void Componente_soppresso_non_e_fra_i_membri_ma_e_segnalato_a_parte() + { + // Un componente soppresso sparisce da "members" (il conteggio scende) ma resta citato + // dalla feature che lo posiziona. Non va nella BOM, però senza il suo file Creo non + // apre l'assieme, quindi va copiato lo stesso. + byte[] dati = Asm(1, Membro(1, 2, "9244206098"), + extra: Bytes("Componente ID 52 (9230100043.PRT)", 0)); + + Assert.Equal(new[] { "9244206098" }, AsmRecordReader.LeggiMembri(dati).Select(m => m.Nome)); + + string percorso = ScriviFile(dati); + try + { + Assert.Equal(new[] { "9230100043" }, AsmRecordReader.EstraiCodiciSoppressi(percorso)); + Assert.DoesNotContain("9230100043", AsmRecordReader.EstraiCodiciReferenziati(percorso)); + } + finally { File.Delete(percorso); } + } + + [Fact] + public void Componente_gia_fra_i_membri_non_e_segnalato_come_soppresso() + { + byte[] dati = Asm(1, Membro(1, 2, "9244206098"), + extra: Bytes("Componente ID 7 (9244206098.PRT)", 0)); + string percorso = ScriviFile(dati); + try + { + Assert.Empty(AsmRecordReader.EstraiCodiciSoppressi(percorso)); + } + finally { File.Delete(percorso); } + } + [Fact] public void Sezione_members_assente_lancia_eccezione() { diff --git a/PrepStep/AsmRecordReader.cs b/PrepStep/AsmRecordReader.cs index a770cb7..1e31588 100644 --- a/PrepStep/AsmRecordReader.cs +++ b/PrepStep/AsmRecordReader.cs @@ -201,4 +201,34 @@ public static class AsmRecordReader var membri = LeggiMembri(File.ReadAllBytes(percorsoAsm)); return new HashSet(membri.Select(m => m.Generico), StringComparer.OrdinalIgnoreCase); } + + /// Codici che compaiono come piazzamento ma non fra i membri: sono i componenti + /// **soppressi**, che spariscono da "members" senza lasciare traccia (§7.1) ma restano + /// citati dalla feature che li posiziona. + /// + /// Non fanno parte della BOM - Creo non li elenca - ma il loro file serve comunque per + /// **aprire** l'assieme: senza, il caricamento fallisce con "dati di input non completi". + /// Sono quindi due domande diverse: cosa compare nella distinta () e cosa serve avere su disco (questo metodo in + /// aggiunta), e la seconda lista e' piu' lunga della prima. + public static HashSet EstraiCodiciSoppressi(string percorsoAsm) + { + byte[] dati = File.ReadAllBytes(percorsoAsm); + var membri = LeggiMembri(dati); + var noti = new HashSet( + membri.Select(m => m.Generico).Concat(membri.Select(m => m.Nome)), StringComparer.OrdinalIgnoreCase); + + var soppressi = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (Match m in PlacementRegex.Matches(Encoding.Latin1.GetString(dati))) + if (!noti.Contains(m.Groups[1].Value)) + soppressi.Add(m.Groups[1].Value); + return soppressi; + } + + // "Componente ID 52 (9230100043.PRT)" - la stringa cambia con la localizzazione Creo del + // file, come in AssemblyComponentScanner.HaPlacement. Minimo 3 caratteri per lo stesso + // motivo per cui li' i token generici partono da 3: sotto quella soglia si raccoglie + // rumore (sulla share esistono davvero modelli chiamati "1.prt", "2.prt"...). + private static readonly Regex PlacementRegex = new( + @"Component(?:e)? [Ii][Dd] \d+ \(([A-Za-z0-9_\-]{3,})\.(?:PRT|ASM)\)", RegexOptions.Compiled); } diff --git a/PrepStep/Form1.cs b/PrepStep/Form1.cs index 667b829..e483d28 100644 --- a/PrepStep/Form1.cs +++ b/PrepStep/Form1.cs @@ -743,6 +743,64 @@ public partial class Form1 : Form return codiciNoti; } + /// Tutti i file prt/asm dell'indice raggruppati per codice, tenendo per ogni + /// cartella la sola versione più alta. A differenza di non + /// sceglie un vincitore fra le cartelle: lo stesso codice può esistere in più punti della + /// share (copie in cartelle di studio, di lavoro, temporanee) con contenuto diverso, e + /// quale sia quello giusto dipende da chi lo referenzia - vedi + /// . + private static Dictionary> CaricaCandidati(string dbPath) + { + var candidati = new Dictionary>(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, versione, data_modifica FROM files " + + "WHERE LOWER(tipo_file) IN ('prt', 'asm')"; + using var reader = command.ExecuteReader(); + while (reader.Read()) + { + string codice = reader.GetString(0); + string tipo = reader.GetString(1).ToLowerInvariant(); + string percorso = reader.GetString(2); + int versione = reader.GetInt32(3); + var lista = candidati.TryGetValue(codice, out var l) ? l : candidati[codice] = new(); + + string cartella = Path.GetDirectoryName(percorso) ?? ""; + int esistente = lista.FindIndex(x => x.Tipo == tipo + && string.Equals(Path.GetDirectoryName(x.Percorso), cartella, StringComparison.OrdinalIgnoreCase)); + if (esistente < 0) + lista.Add((tipo, percorso, versione, reader.GetString(4))); + else if (versione > lista[esistente].Versione) + lista[esistente] = (tipo, percorso, versione, reader.GetString(4)); + } + return candidati; + } + + /// Fra più copie dello stesso codice sulla share sceglie quella che userebbe Creo: + /// prima cerca nella cartella dell'assieme che la referenzia (Creo risolve i componenti + /// partendo da lì), poi ripiega sulla versione più alta e, a parità, sulla più recente. + /// Senza questo criterio si finisce per copiare in "raw" una copia omonima ma di un altro + /// modello - Creo la carica e i vincoli di accoppiamento non si risolvono, quindi l'assieme + /// non si apre affatto. + private static List<(string Tipo, string Percorso)> ScegliPerCartella( + List<(string Tipo, string Percorso, int Versione, string Data)> candidati, string? cartellaPadre) + { + return candidati + .GroupBy(c => c.Tipo) + .Select(perTipo => + { + var scelto = perTipo + .OrderByDescending(c => cartellaPadre is not null + && string.Equals(Path.GetDirectoryName(c.Percorso), cartellaPadre, StringComparison.OrdinalIgnoreCase)) + .ThenByDescending(c => c.Versione) + .ThenByDescending(c => c.Data) + .First(); + return (scelto.Tipo, scelto.Percorso); + }) + .ToList(); + } + /// Codici referenziati direttamente in un assieme: prova prima il parser esatto /// dei record binari (, vedi docs/formato-asm-record.md), che /// non ha i falsi positivi/negativi noti dell'euristica testuale (docs/formato-asm.md); se @@ -763,6 +821,50 @@ public partial class Form1 : Form } } + /// Componenti soppressi di un assieme (): + /// non sono nella distinta, ma senza il loro file Creo non apre l'assieme. Vuoto per i file + /// che il parser esatto non sa leggere: lì si usa l'euristica testuale, che i piazzamenti li + /// riconosce già per conto suo (). + private static IEnumerable CodiciSoppressi(string percorsoAsm) + { + try + { + return AsmRecordReader.EstraiCodiciSoppressi(percorsoAsm); + } + catch (InvalidDataException) + { + return Array.Empty(); + } + } + + /// Cartella della share da cui proviene un file già copiato in "raw": fra le copie + /// omonime indicizzate è quella di pari dimensione. Serve per gli assiemi di partenza, che + /// arrivano dalla fase precedente senza portarsi dietro l'origine. Null se non si riesce a + /// stabilirla (il file non è nell'indice, o la share non risponde): in quel caso la scelta + /// del componente ricade sul criterio generale. + private static string? CartellaDiOrigine(string fileInRaw, + Dictionary> candidati) + { + string nome = Path.GetFileName(fileInRaw); + if (!candidati.TryGetValue(nome.Split('.')[0], out var perCodice)) + return null; + try + { + long dimensione = new FileInfo(fileInRaw).Length; + foreach (var c in perCodice.Where(c => string.Equals(Path.GetFileName(c.Percorso), nome, StringComparison.OrdinalIgnoreCase))) + { + var info = new FileInfo(c.Percorso); + if (info.Exists && info.Length == dimensione) + return Path.GetDirectoryName(c.Percorso); + } + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + // share non raggiungibile: si prosegue col criterio generale + } + return null; + } + /// 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 @@ -771,45 +873,69 @@ public partial class Form1 : Form private (List<(string Codice, string Tipo, string Percorso)> Trovati, List NonTrovati) RisolviAlberoComponenti( IEnumerable asmIniziali, IProgress<(int Fatti, int Totale)>? progress = null) { - var daScansionare = new Queue(asmIniziali); - var codiciIniziali = daScansionare.Select(f => Path.GetFileName(f).Split('.')[0]); + // Ogni assieme viaggia con la cartella della share da cui proviene: e' li' che Creo + // cerca per primi i suoi componenti, quindi e' li' che li cerchiamo anche noi. + var daScansionare = new Queue<(string File, string? CartellaOrigine)>( + asmIniziali.Select(f => (f, (string?)null))); + var codiciIniziali = asmIniziali.Select(f => Path.GetFileName(f).Split('.')[0]).ToList(); var codiciVisti = new HashSet( codiciIniziali.Concat(codiciIniziali.Select(AssemblyComponentScanner.CodiceRadice)), StringComparer.OrdinalIgnoreCase); + var codiciSoppressiVisti = new HashSet(StringComparer.OrdinalIgnoreCase); var trovati = new List<(string Codice, string Tipo, string Percorso)>(); var codiciNonTrovati = new List(); if (daScansionare.Count == 0 || _dbPath is null) return (trovati, codiciNonTrovati); var codiciNoti = CaricaCodiciNoti(_dbPath); + var candidati = CaricaCandidati(_dbPath); int processati = 0; while (daScansionare.Count > 0) { - var asm = daScansionare.Dequeue(); + var (asm, cartellaOrigine) = daScansionare.Dequeue(); + cartellaOrigine ??= CartellaDiOrigine(asm, candidati); processati++; foreach (var codice in EstraiCodiciReferenziati(asm, codiciNoti.Keys)) { if (!codiciVisti.Add(codice)) continue; - if (!codiciNoti.TryGetValue(codice, out var componenti)) + if (!candidati.TryGetValue(codice, out var perCodice)) { codiciNonTrovati.Add(codice); continue; } - foreach (var (tipo, percorso) in componenti) + foreach (var (tipo, percorso) in ScegliPerCartella(perCodice, cartellaOrigine)) { CopiaFileTrovati(new[] { percorso }, _ultimaCartellaRaw!); trovati.Add((codice, tipo, percorso)); if (tipo == "asm") { - daScansionare.Enqueue(Path.Combine(_ultimaCartellaRaw!, Path.GetFileName(percorso))); + daScansionare.Enqueue((Path.Combine(_ultimaCartellaRaw!, Path.GetFileName(percorso)), + Path.GetDirectoryName(percorso))); } } } + + // I componenti soppressi non compaiono nella BOM, ma Creo ne vuole comunque il file + // per aprire l'assieme: si copiano senza entrare nel conteggio dei "non trovati" + // (non essendo componenti della distinta, segnalarli come mancanti sarebbe un falso + // allarme) e senza ricorrervi dentro. + foreach (var codice in CodiciSoppressi(asm)) + { + if (!codiciSoppressiVisti.Add(codice) || codiciVisti.Contains(codice)) + continue; + if (!candidati.TryGetValue(codice, out var perCodice)) + continue; + foreach (var (tipo, percorso) in ScegliPerCartella(perCodice, cartellaOrigine)) + { + CopiaFileTrovati(new[] { percorso }, _ultimaCartellaRaw!); + trovati.Add((codice, tipo, percorso)); + } + } progress?.Report((processati, processati + daScansionare.Count)); } diff --git a/PrepStep/Indexer.cs b/PrepStep/Indexer.cs index 860ff88..cfe6c6a 100644 --- a/PrepStep/Indexer.cs +++ b/PrepStep/Indexer.cs @@ -40,13 +40,22 @@ public static class Indexer CREATE INDEX IF NOT EXISTS idx_files_tipo ON files(tipo_file); CREATE INDEX IF NOT EXISTS idx_files_cartella ON files(cartella_radice); - CREATE VIEW IF NOT EXISTS files_ultima_versione AS + -- DROP+CREATE invece di "IF NOT EXISTS": la vista e' ricalcolata a ogni avvio di scan, + -- cosi' un DB gia' esistente prende la definizione corrente senza reindicizzare. + DROP VIEW IF EXISTS files_ultima_versione; + CREATE VIEW files_ultima_versione AS SELECT percorso_completo, percorso_relativo, nome_file, cartella_radice, famiglia, codice, tipo_file, versione, dimensione_byte, data_modifica, scansionato_il FROM ( SELECT f.*, ROW_NUMBER() OVER ( PARTITION BY cartella_radice, codice, tipo_file - ORDER BY versione DESC + -- A parita' di versione (stesso codice in due sottocartelle della stessa + -- radice, es. una copia vecchia in una cartella di lavoro) vince il file + -- modificato piu' di recente: senza questo criterio il pareggio e' rotto in + -- modo arbitrario e puo' vincere la copia obsoleta, nascondendo del tutto + -- quella buona. Il file copiato in "raw" sarebbe allora un modello diverso da + -- quello che l'assieme si aspetta, e Creo non riesce ad aprirlo. + ORDER BY versione DESC, data_modifica DESC, percorso_completo ) AS rn FROM files f ) WHERE rn = 1; diff --git a/docs/architettura.md b/docs/architettura.md index 0a91640..fb63bed 100644 --- a/docs/architettura.md +++ b/docs/architettura.md @@ -44,7 +44,7 @@ Distinta (.xls/.xml) → CSV (componente;descrizione) → raw/ (CAD copiati | 1 | Database | "Aggiorna indice" → `Indexer.ScanAsync`; "Cambia cartella..." → `ScegliCartellaLavoro` | Crea/aggiorna `cad_index.db` scansionando la share; sceglie/mostra la cartella di lavoro | | 2 | Importa Distinta Base | selezione file → `ImportaDistinta` → `DistintaBaseImporter.ConvertiInCsv` | Xls/Xml gestionale → CSV componente;descrizione, scritto nella cartella di lavoro | | 3 | Crea cartella raw | "Crea cartella raw" → `CreaCartellaRaw` | `TrovaPercorsiCad` + `CopiaFileTrovati` (parallela) → `raw/` dentro la cartella di lavoro | - | 4 | Trova componenti referenziati | "Trova componenti referenziati" → `TrovaPrtReferenziati` → `RisolviAlberoComponenti` | Legge i `.asm` in `raw/` con `AssemblyComponentScanner`, risolve ricorsivamente sotto-assiemi/parti nell'indice, li copia anch'essi in `raw/` | + | 4 | Trova componenti referenziati | "Trova componenti referenziati" → `TrovaPrtReferenziati` → `RisolviAlberoComponenti` | Legge i `.asm` in `raw/`, risolve ricorsivamente sotto-assiemi/parti nell'indice e li copia anch'essi in `raw/`, insieme ai componenti soppressi che servono a Creo per aprire l'assieme | | 5 | Verifica completezza | "Verifica completezza assiemi" → `VerificaCompletezzaAssiemi` | Completezza ricorsiva sui percorsi in indice. All'ingresso nella fase, `AggiornaAnteprimaAsm` mostra le icone trascinabili dei `.asm` della distinta già in `raw/`, da trascinare su Creo per l'esportazione STEP (Distributed Batch) | Stato condiviso tra le fasi (campi privati di `Form1`): `_dbPath`, `_cartellaLavoro`, @@ -100,6 +100,23 @@ Distinta (.xls/.xml) → CSV (componente;descrizione) → raw/ (CAD copiati positivi noti in `docs/formato-asm.md`. Resta il fallback per i file che il parser esatto non copre, e la fonte di verità per `ExtractCreoStrings.cs`/`BomViaDb.cs`. +Quella lista è **la distinta**, e non coincide con i file che servono a Creo per aprire +l'assieme: i componenti **soppressi** ne restano fuori (non sono nella BOM) ma il loro file +serve lo stesso, altrimenti il caricamento fallisce. `AsmRecordReader.EstraiCodiciSoppressi` +li restituisce a parte, e la fase 4 li copia in `raw/` senza contarli fra i "non trovati" — +vedi `docs/formato-asm-record.md` §7.3. + +## Quale copia di un componente si copia in `raw/` + +Lo stesso codice esiste spesso in più cartelle della share, con contenuto diverso e pari +versione (13% dei codici, vedi `docs/formato-asm-record.md` §7.4). Il `.asm` non registra +quale copia usa, quindi `RisolviAlberoComponenti` replica la regola di Creo: +`Form1.ScegliPerCartella` preferisce la copia nella **stessa cartella dell'assieme che la +referenzia** (`Form1.CartellaDiOrigine` ricostruisce quella cartella per gli assiemi di +partenza), e solo in mancanza ripiega su versione più alta e data più recente. Sbagliare copia +non dà un errore comprensibile: Creo non apre l'assieme e dice solo "dati di input non +completi". + ## Import distinta base - **DistintaBaseImporter.cs** — converte l'export del gestionale (SpreadsheetML XML con diff --git a/docs/db-cad-index.md b/docs/db-cad-index.md index 4f188e3..6f294f8 100644 --- a/docs/db-cad-index.md +++ b/docs/db-cad-index.md @@ -29,17 +29,33 @@ componenti e nel filtro per cartella). Vista `files_ultima_versione`: stessa struttura di `files`, filtrata alla sola riga con `versione` più alta per ogni `(cartella_radice, codice, tipo_file)`, tramite `ROW_NUMBER() -OVER (PARTITION BY ... ORDER BY versione DESC)`. Tutte le ricerche di componenti (`Form1. -TrovaPercorsiCad`, `Form1.CaricaCodiciNoti`) interrogano questa vista, non `files` -direttamente, così un componente con più versioni sulla share risolve sempre all'ultima. -Deliberatamente una `ROW_NUMBER()` invece di un self-join con `GROUP BY`: su ~200k righe un -self-join degenera in tempi quasi quadratici, `ROW_NUMBER()` resta lineare. +OVER (PARTITION BY ... ORDER BY versione DESC, data_modifica DESC, percorso_completo)`. Tutte +le ricerche di componenti (`Form1.TrovaPercorsiCad`, `Form1.CaricaCodiciNoti`) interrogano +questa vista, non `files` direttamente, così un componente con più versioni sulla share +risolve sempre all'ultima. Deliberatamente una `ROW_NUMBER()` invece di un self-join con +`GROUP BY`: su ~200k righe un self-join degenera in tempi quasi quadratici, `ROW_NUMBER()` +resta lineare. + +La vista è ricreata (`DROP` + `CREATE`, non `CREATE IF NOT EXISTS`) a ogni "Aggiorna indice", +così un DB già esistente prende la definizione corrente senza dover reindicizzare la share. Nota: `cartella_radice` fa parte della partition, quindi se lo stesso codice+tipo esiste in **due cartelle radice diverse** (duplicati sulla share), la vista restituisce una riga per ciascuna — non una sola in assoluto. `TrovaPercorsiCad` gestisce questo caso deduplicando per `tipo_file` lato C# (vedi sotto). +> **I criteri di pareggio non sono decorativi.** Lo stesso codice può esistere **nella stessa +> cartella radice, con la stessa `versione`**, in due sottocartelle diverse (una copia in una +> cartella di studio, di lavoro o temporanea). Senza `data_modifica` nell'`ORDER BY` il +> pareggio veniva rotto in modo arbitrario, e poteva vincere la copia obsoleta — nascondendo +> del tutto quella buona, che non compariva più nella vista. Il file che finiva in `raw/` era +> allora un modello diverso da quello che l'assieme si aspetta: Creo lo carica, i vincoli di +> accoppiamento non si risolvono e **l'assieme non si apre affatto**. +> +> Il criterio deterministico però non basta a scegliere *bene*: è il minimo per non sbagliare +> a caso. Quale copia sia quella giusta dipende da chi la referenzia, non dalla data — vedi +> [`formato-asm-record.md`](formato-asm-record.md) §7.4 e `Form1.ScegliPerCartella`. + ## Come viene popolato: `Indexer.ScanAsync` Chiamato da "Aggiorna indice" (fase 1) e riutilizza lo stesso `dbPath` già caricato. diff --git a/docs/formato-asm-record.md b/docs/formato-asm-record.md index 18e1c72..9dac344 100644 --- a/docs/formato-asm-record.md +++ b/docs/formato-asm-record.md @@ -394,6 +394,16 @@ di sbagliare in silenzio. `members`: sparisce e basta, il conteggio dichiarato scende di conseguenza (`\xf8 03` invece di `\xf8 04`). Nessun marcatore di stato da gestire: `CreoBom.cs` sul file risalvato riproduce esattamente la BOM di riferimento senza modifiche al parser. + + > **Sparisce da `members`, non dal file.** La feature che lo posiziona resta, e con essa la + > stringa `Componente ID (.PRT)` (localizzata come in + > `AssemblyComponentScanner.HaPlacement`). Per la BOM va ignorata - Creo non elenca i + > soppressi - ma **il file di quel componente serve comunque per aprire l'assieme**: se manca, + > Creo fallisce il caricamento con "dati di input non completi" (`[-4]`, cioè + > `PRO_TK_E_NOT_FOUND`). Verificato su un caso reale: un assieme che non si apriva dalla + > cartella `raw/`, e che si è aperto senza errori copiandoci dentro quel solo file. + > `AsmRecordReader.EstraiCodiciSoppressi` li restituisce a parte, apposta per questo - vedi + > §7.3. - **Family table di assiemi — rilevamento.** Non solo le parti normalizzate (§4) possono essere family table: un intero **assieme** può esserlo. Osservato su `2546220101.asm` (dentro l'albero di `9284218024b.asm`): il file generico ha una propria sezione `members` @@ -476,6 +486,59 @@ di sbagliare in silenzio. quale situazione divergerebbe, né se il codice dovrebbe fare qualcosa se succedesse (oggi non la usa in nessun modo, legge solo `name`). +### 7.3 Due domande diverse: cosa va in distinta, cosa serve per aprire + +È la lezione che costa di più riscoprire, quindi sta qui in chiaro: **la lista dei componenti +della BOM e la lista dei file che servono a Creo per aprire l'assieme non coincidono.** La +seconda è più lunga. + +| Lista | Serve per | Si ottiene con | +| --- | --- | --- | +| Componenti della distinta | BOM, verifica completezza | `LeggiMembri` / `EstraiCodiciReferenziati` (sezione `members`) | +| File necessari al caricamento | popolare `raw/` per l'export STEP | i precedenti **più** `EstraiCodiciSoppressi` | + +La differenza sono i **componenti soppressi** (§7.1): fuori dalla BOM, ma senza il loro file +Creo non apre l'assieme. Chi popola `raw/` deve quindi usare l'unione; chi genera la BOM o +verifica la completezza deve usare **solo** i membri, altrimenti i soppressi diventano falsi +"componenti mancanti". + +Il sintomo, se si sbaglia, non assomiglia per niente alla causa: Creo Distributed Batch +fallisce con `Carica modello in Creo Pro | Non riuscito | [-4] Errore: Dati di input non +completi` - un messaggio che non nomina il file mancante. Il modo più rapido per trovarlo è +aprire l'assieme in Creo **dalla cartella `raw/`** (non dalla share, dove i file ci sono tutti +e quindi si apre senza errori) e cercare nell'albero la voce con l'icona rossa. + +### 7.4 Lo stesso codice esiste in più cartelle della share + +Sulla share **13% dei codici** (5.193 su 39.274 fra prt e asm) ha la versione più alta in più +cartelle, con contenuto diverso. Non sono revisioni: sono copie parallele, lasciate da studi +preliminari, allestimenti fiera, cartelle di lavoro datate, raccolte temporanee. + +Il `.asm` **non dice quale copia usa**: verificato che non registra né il percorso, né +l'`UOBJ_ID` del figlio (che pure distingue le copie), né la revisione attesa. Cita solo il +nome del modello. Quale copia venga caricata lo decide Creo al momento dell'apertura, e la +regola pratica è: **prima la cartella del modello che la referenzia**. + +Non esiste quindi un criterio globale corretto - e i numeri lo confermano: nel 79% dei +conflitti nessuna delle copie sta in una cartella "ufficiale" (`…\M_B`, `…\A`), perché il +conflitto è fra due istantanee parallele, ognuna autoconsistente con i propri componenti. Per +data, per versione e per posizione sono equivalenti: solo il contesto di chi referenzia +discrimina. + +Conseguenze per il codice: + +- `Form1.ScegliPerCartella` risolve ogni componente preferendo la copia nella **stessa cartella + dell'assieme che lo referenzia**, e solo in mancanza ripiega su versione più alta e data più + recente. `Form1.CartellaDiOrigine` ricostruisce quella cartella per gli assiemi di partenza, + che arrivano dalla fase precedente senza portarsela dietro. +- La vista `files_ultima_versione` (vedi [`db-cad-index.md`](db-cad-index.md)) rompe il + pareggio fra copie di pari versione in modo deterministico invece che arbitrario. +- Resta un rischio noto: quando il componente **non** sta nella cartella del suo assieme, la + scelta torna a essere una scommessa. Se un assieme non si apre da `raw/`, è la prima cosa da + verificare - `9244206098` è il caso reale che ha portato a questa sezione: due copie di pari + versione, una del 2011 in una cartella di file sciolti e una del 2026 accanto all'assieme, + e in `raw/` finiva quella del 2011. + ## 8. Uso ```powershell