diff --git a/CLAUDE.md b/CLAUDE.md index 7f73cdd..493d793 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -63,7 +63,10 @@ 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`). + 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. 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 74ef976..9579063 100644 --- a/PrepStep.Tests/AsmRecordReaderTests.cs +++ b/PrepStep.Tests/AsmRecordReaderTests.cs @@ -68,6 +68,25 @@ public class AsmRecordReaderTests Assert.Equal("UNI6592", membri[0].Generico); } + [Fact] + public void Rimuove_il_suffisso_array_feature_dal_nome_istanza() + { + // Regressione: caso reale 9244250136.asm -> ConfigName con suffisso "#AF" per + // componenti creati da un pattern/array feature (id 45 e id 78, entrambi variante + // "_C03" con suffisso diverso "#AF0"/"#AF1"): non un'istanza di family table vera, + // la BOM di Creo mostra il codice pulito e unisce le occorrenze duplicate. + byte[] dati = Asm(2, Bytes( + Membro(45, 2, "9244250136_C03"), + Membro(78, 2, "9244250136_C03")), + extra: Bytes( + ConfigName("9244250136_C03#AF0", idMembro: 45), + ConfigName("9244250136_C03#AF1", idMembro: 78))); + + var membri = AsmRecordReader.LeggiMembri(dati); + + Assert.All(membri, m => Assert.Equal("9244250136_C03", m.Nome)); + } + [Fact] public void Componente_senza_ConfigName_non_e_toccato_da_istanze_di_altri_membri() { diff --git a/PrepStep/AsmRecordReader.cs b/PrepStep/AsmRecordReader.cs index 02aa00a..ccbe67e 100644 --- a/PrepStep/AsmRecordReader.cs +++ b/PrepStep/AsmRecordReader.cs @@ -41,6 +41,14 @@ public static class AsmRecordReader @"\x00objname\x00\xe2\xe0\x01object_type\x00[\x00-\x7f]\xe0\x0aname\x00([^\x00]+)\x00", RegexOptions.Compiled); + // Un componente creato da un pattern/array feature ha un ConfigName con un tag interno + // "#AF" (Array Feature, n = indice nel pattern), non un'istanza di family + // table vera: la BOM reale di Creo lo ignora e mostra il codice pulito - osservato su + // 9244250136.asm (4 dei suoi 8 membri, due dei quali stesso codice "_C03" con "#AF0" e + // "#AF1" diversi, che la BOM unisce in una sola riga con quantita' 2). Senza questa + // normalizzazione ogni istanza del pattern risulterebbe un componente diverso. + private static readonly Regex SuffissoArrayFeatureRegex = new(@"#AF\d+$", RegexOptions.Compiled); + // primo elemento dell'array "members": porta le chiavi per esteso. private static readonly Regex FirstMemberRegex = new( @"\x01id\x00" + Int + @"\xe0\x01type\x00" + Int @@ -78,7 +86,7 @@ public static class AsmRecordReader var istanze = new Dictionary(); foreach (Match m in ConfigNameRegex.Matches(testo)) - istanze[AsInt(m.Groups[2].Value)] = m.Groups[1].Value; + istanze[AsInt(m.Groups[2].Value)] = SuffissoArrayFeatureRegex.Replace(m.Groups[1].Value, ""); const string chiaveMembers = "\0members\0"; int start = testo.IndexOf(chiaveMembers, StringComparison.Ordinal); diff --git a/docs/formato-asm-record.md b/docs/formato-asm-record.md index f90ef02..324005e 100644 --- a/docs/formato-asm-record.md +++ b/docs/formato-asm-record.md @@ -184,7 +184,7 @@ Il collegamento sta in un record di **parametro** applicato al membro: nessun altro. L'algoritmo è quindi: leggere `members`, costruire la mappa `id membro → nome istanza` dai record `ConfigName` (`AsmRecordReader.LeggiMembri`, dizionario `istanze`), e sostituire il nome dove la -mappa ha una voce. Il generico resta disponibile come `Membro.File`. +mappa ha una voce. Il generico resta disponibile come `Membro.Generico`. > **Avvertenza.** `ConfigName` sta in mezzo a parametri palesemente aziendali > (`TIPO_PARTE`, `CLASSE_RICAR`, `GR_PRODOTTO`, `TECN_RIORDINO`), quindi @@ -192,6 +192,25 @@ mappa ha una voce. Il generico resta disponibile come `Membro.File`. > interna. Regge sui campioni disponibili, ma su file provenienti da > un'altra installazione va riverificato prima di fidarsene. +### 4.0.1 Eccezione: componenti da pattern/array feature (`#AF`) + +Un componente creato da un **pattern/array feature** (stessa geometria ripetuta N volte da +una feature di pattern, non N placement manuali distinti) ha un `ConfigName` con un nome +nella forma `#AF` (`n` = indice dell'istanza nel pattern), es. +`9244250136_C01#AF0`. **Non è un'istanza di family table**: è un tag interno di +disambiguazione fra le ripetizioni del pattern. La BOM reale di Creo lo **ignora** e mostra +il codice pulito, unendo le occorrenze duplicate in una sola riga con la quantità totale. + +Osservato su `9244250136.asm`: 4 dei suoi 8 membri hanno `ConfigName` con suffisso `#AF`, +due dei quali (`_C03#AF0` e `_C03#AF1`) sono lo **stesso codice** con `n` diverso — la BOM li +mostra come un'unica riga `9244250136_C03` con quantità 2. Senza normalizzare il suffisso, +ogni istanza del pattern risulterebbe un componente distinto (e con nome sbagliato). +`AsmRecordReader` rimuove `#AF\d+$` da ogni valore di `ConfigName` prima di usarlo +(`SuffissoArrayFeatureRegex`). Scoperto durante un test end-to-end su un albero reale più +ampio degli 8 campioni originali (`9284218024b.asm`, confrontato con la sua BOM Creo di +riferimento) — non era presente in nessuno degli 8 campioni usati per il reverse engineering +iniziale. + ### 4.1 Una seconda pista, che NON va usata per la BOM Altrove nel file compaiono stringhe nella forma `istanza`: @@ -291,6 +310,24 @@ sbagliato. Elencato esplicitamente per non far passare per dimostrato ciò che non lo è: +- **⚠️ Family table di ASSIEMI (non solo di parti), risultato silenziosamente sbagliato.** + 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` che elenca il contenuto di UNA rappresentazione + (nell'esempio, 3 cuscinetti + 1 sotto-assieme), ma la BOM Creo per lo stesso file mostra 4 + **istanze diverse** (`2546220101_C01`.."_C04", tre "Parte" e una "Sottoassieme" - tipi + diversi fra loro!), ciascuna con un proprio contenuto potenzialmente diverso. Il nome + istanza qui **non** passa da un record `ConfigName` (verificato: nessuna occorrenza + letterale di `ConfigName` nel file) ma da un meccanismo diverso, imparentato con la + "seconda pista" del §4.1 (`\xe3\xe2\x02\0`, senza il bracket + `` in questo caso) - non decodificato. **`AsmRecordReader.LeggiMembri` non lo + rileva e non lancia eccezione**: restituisce silenziosamente il contenuto della + rappresentazione di default sotto un solo nome, invece delle 4 istanze corrette - a + differenza degli altri limiti di questa sezione, qui il fallimento non è segnalato in + nessun modo al chiamante (né un conteggio che non torna, né un'eccezione). Scoperto sullo + stesso test end-to-end del punto `#AF` sopra, mai osservato negli 8 campioni originali. + **Prima di fidarsi del parser esatto su un albero con assiemi a family table, verificare a + mano contro la BOM Creo.** - **Stato soppresso / escluso.** Nessuna evidenza cercata o trovata. Non si sa se un componente soppresso compaia in `members`, né con quale marcatore. Sui campioni disponibili il totale coincideva con la BOM, il che *suggerisce* che