# Decodifica strutturale del .asm Creo: record, membri, family table Questo documento descrive il formato **record per record** del `.asm` Creo, ricavato per reverse engineering su un campione di assiemi reali e verificato confrontando l'output con le BOM esportate da Creo per gli stessi file. Sostituisce, per l'estrazione dei componenti, l'euristica testuale ("codice che compare almeno due volte") descritta in [`formato-asm.md`](formato-asm.md): quella era una statistica sui nomi, questa legge la struttura effettiva dei record. Implementazione: lettura in [`PrepStep/AsmRecordReader.cs`](../PrepStep/AsmRecordReader.cs) (condivisa con la GUI, `Form1.EstraiCodiciReferenziati` in fase 4 "Trova componenti referenziati" - con fallback su `AssemblyComponentScanner` per i file che non rispettano il formato validato), resa in formato BOM e ricorsione sui sottoassiemi in [`scripts/CreoBom.cs`](../scripts/CreoBom.cs) (via `#:project`). **Stato della validazione.** Due livelli di prova, da non confondere: 1. **Byte-per-byte contro BOM Creo di riferimento** (la prova forte): 3 assiemi del reverse engineering iniziale, più 5 casi reali aggiunti in seguito, ciascuno con la sua BOM esportata da Creo messa a confronto riga per riga con l'output di `CreoBom.cs` — `9284218024b.asm` (albero profondo, multi-livello), `2546220101.asm`/`2546220101_c04.asm.1` (istanza di family table isolata), `2546220101.asm` risalvato con un componente soppresso, `9244218080_c01.asm` (sottoassieme montato due volte), `9284208092_D110.asm` (revisione del figlio). Ognuno di questi è citato per nome nella sezione del documento a cui si riferisce, così resta verificabile quale affermazione poggia su quale file. 2. **Su tutti i 350 `.asm` reali di `raw/`**, solo conteggi interni e coerenza con l'euristica testuale (`AssemblyComponentScanner`), **non** contro BOM di riferimento: usato per calibrare le soglie dei controlli di sicurezza (§7) su scala, non per dimostrare la correttezza dei valori estratti file per file. Un'affermazione verificata solo al livello 2 (conteggi su scala) è più debole di una verificata al livello 1 (BOM reale) - il testo lo distingue sempre esplicitamente. --- ## 1. Intestazione Il file si apre con un header ASCII a righe terminate da `\` + newline: ``` #UGC:2 ASSEMBLY 2038 1760 805 1 1 15 4000 2022414 000002d0 \ #- VERS 0 0 \ #- HOST \ #- LINK \ #- DBID \ #- REVS 0, \ ``` `ASSEMBLY` distingue il tipo di modello. Gli altri campi numerici non sono stati decodificati e non servono per la BOM. Dopo l'header inizia il corpo binario. **Il corpo non è compresso**: i nomi dei modelli sono in chiaro, 1 byte per carattere (Latin1 — `Encoding.Latin1.GetString`, stesso metodo già usato in `AssemblyComponentScanner.cs` per leggere il file come testo senza alterarne i byte). ## 2. Il formato dei record Il corpo è una serializzazione a marcatori. I byte `\xe0`-`\xff` sono marcatori di struttura, tutti gli altri sono dati. I marcatori osservati: | byte | significato dedotto | |------|---------------------| | `\xe0` | inizio campo: `\xe0 \0 ` | | `\xe1` | valore nullo / vuoto | | `\xe2` | inizio struttura o unione (per i parametri: il byte dopo è il discriminante) | | `\xe3` | inizio struttura, usato per gli elementi di un array | | `\xf1` | apre un contenitore (visto prima di un array) | | `\xf7` | id di schema: `\xf7 ` identifica il layout dei record che seguono | | `\xf8` | array: `\xf8 ... ` | | `\xfb` | lo schema è definito in linea nel primo elemento dell'array | | `\xf4` | chiude una sezione (seguito da `#\n#\n`) | Il byte `` dopo `\xe0` codifica il tipo del valore. Osservati: `\x00` struttura/nullable, `\x01` intero, `\x02` array numerico, `\x0a` stringa terminata da `\0`. ### 2.1 Interi Gli interi sono big-endian con la **lunghezza codificata nei bit alti del primo byte**, per non collidere con i marcatori `\xe0`+: | primo byte | byte totali | valore | |------------|-------------|--------| | `0x00`-`0x7f` | 1 | il byte stesso | | `0x80`-`0xbf` | 2 | `(b0 & 0x3f) << 8 \| b1` | | `0xc0`-`0xdf` | 3 | `(b0 & 0x1f) << 16 \| b1 << 8 \| b2` | Esempi verificati: `\x2e` = 46; `\x80\x81` = 129; `\xc0\x4d\x51` = 19793. Questa regola è stata **ricavata** dalla necessità di far quadrare il conteggio dei membri: con la lettura a 1 byte fisso i campioni grandi si disallineavano. Non è confermata da una specifica; regge sui campioni disponibili al momento del reverse engineering. Nel codice è `AsmRecordReader.AsInt` (privata, in `PrepStep/AsmRecordReader.cs`), applicata al testo decodificato Latin1 (ogni `char` della stringa corrisponde 1:1 a un byte, quindi indicizzare la stringa o l'array di byte originale dà lo stesso offset). ### 2.2 Compressione per schema Un array `\xf8 \xf7 \xfb` scrive il **primo** elemento con tutti i nomi di campo per esteso; gli elementi successivi sono **posizionali**, senza nomi. È il motivo per cui nel file si vedono i nomi dei componenti "nudi", uno dopo l'altro, senza etichetta. Per leggere l'elemento N bisogna conoscere lo schema definito dall'elemento 1. --- ## 3. L'elenco dei componenti: la chiave `members` La chiave `members` è l'elenco dei **componenti diretti** dell'assieme (livello 1), nell'ordine di montaggio. ``` \xe0 \x00 "members" \0 \xf1 \xf8 \xf7 \xfb \xe3 \xe0\x01 "id"\0 \xe0\x01 "type"\0 \xe0\x0a "name"\0 \0 \xe0\x0a "family_name"\0 \0 \xe0\x00 "tha_ref"\0 \xe1 \xe0\x02 "e1"\0 ... "e2" ... "e3" ... "origin" ... \xe3 \0 \0 \xe3 \0 \0 ... ``` Campi utili: - **`id`** — identificatore del componente **dentro questo assieme**. È lo stesso numero che Creo mostra come "Componente ID N". Serve da chiave per collegare il membro ad altre sezioni (vedi family table, §4). - **`type`** — `1` = sottoassieme (`.asm`), `2` = parte (`.prt`). Sui campioni osservati compaiono solo questi due valori. - **`name`** — il **file su disco**, senza estensione. Per le family table è il **generico** (`UNI6592`), non l'istanza. - **`family_name`** — sui campioni disponibili è **sempre uguale a `name`**. Non porta quindi l'informazione sull'istanza; il nome è fuorviante. - `e1`, `e2`, `e3`, `origin` — la matrice di posizionamento (assi + origine). Non decodificati: non servono per la BOM, ma occupano il resto del record e hanno lunghezza variabile. `N` (il conteggio dichiarato) è il numero totale di **occorrenze**, non di codici distinti: una parte montata 4 volte conta 4 e compare in 4 record con id diversi. Il conteggio va usato come controllo di correttezza: se i record trovati non sono esattamente `N`, la lettura si è disallineata e va segnalata un'eccezione invece di restituire un risultato parziale silenzioso — vedi `AsmRecordReader.LeggiMembri`, che lancia `InvalidDataException` in questo caso. La sezione termina a `\xf4\x04#\n#MdlStatus\n`. ### 3.1 Come lo legge `AsmRecordReader.LeggiMembri` Il parser non ricostruisce lo schema completo (servirebbe decodificare le matrici). Si ancora invece al pattern `\xe3 \0 \0` (`AsmRecordReader.MemberRegex`, una regex .NET sul testo Latin1 del file) e salta il resto del record fino al `\xe3` successivo che soddisfa il pattern. La doppia stringa terminata da `\0` rende i falsi positivi improbabili, e il conteggio dichiarato `N` fa da rete di sicurezza. > `ponytail:` scansione a pattern invece del parser di schema — il punto debole > è che si appoggia al conteggio per accorgersi di un errore. Da sostituire con > la lettura dello schema se dovessero comparire campioni che non quadrano. --- ## 4. Family table: dove sta il nome dell'istanza Le parti normalizzate (viti, rondelle, spine, boccole) sono **istanze di family table**. In Creo esiste un solo file, il **generico** (`UNI6592.prt`), che contiene una tabella di varianti; ogni variante è un'**istanza** (`R8--_-_U6592_AISI304`). La BOM elenca le istanze, il disco contiene il generico. La sezione `members` cita **solo il generico**: senza risolvere `ConfigName` (sotto), il livello 1 produrrebbe `UNI6592` dove la BOM dice `R8--_-_U6592_AISI304`. Il collegamento sta in un record di **parametro** applicato al membro: ``` \xe3 "ConfigName"\0 \xe2 \x33 [\xf7 ] \0 \x82\x7b \xe1\xe1 ``` - `\xe2\x33` — parametro di tipo stringa (`\x33` = `'3'`). Confronto: `\xe2\x32` è un parametro numerico, es. `DENSITA`. - `` — il nome esatto che la BOM stampa. - `` — l'`id` del record `members` a cui il parametro si applica, con la codifica intera di §2.1. **È questo il collegamento.** - `\xf7 ` compare solo sulla prima occorrenza (schema definito in linea), poi è omesso. - `\x82\x7b` — costante (= 635) identica su tutte le occorrenze di tutti i campioni. Significato ignoto, presumibilmente attributi/flag del parametro. `AsmRecordReader.ConfigNameRegex` la usa come ancora di fine record; se un campione futuro avesse un valore diverso, il match salterebbe silenziosamente (nessun contatore di sicurezza analogo a quello di `members` copre questo caso). `ConfigName` compare **esattamente** per i membri che sono istanze, e per 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.Generico`. > **Avvertenza.** `ConfigName` sta in mezzo a parametri palesemente aziendali > (`TIPO_PARTE`, `CLASSE_RICAR`, `GR_PRODOTTO`, `TECN_RIORDINO`), quindi > potrebbe non essere un meccanismo standard di Creo ma una convenzione > 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.0.2 Istanza risolta direttamente in `members`, senza passare da `ConfigName` Non tutti i membri che sono istanze di family table hanno un record `ConfigName` associato. Per alcune parti commerciali/normalizzate non-UNI, il campo `name` (e allo stesso modo `family_name`) del record `members` (§3) porta già per intero la notazione `` invece del solo nome generico: ``` \xe3 ""\0 ""\0 ``` È la stessa notazione istanza→generico della §4.1 sotto, ma qui compare **dentro** il record `members` stesso, come valore diretto del componente - non nell'elenco di riferimenti geometrici di cui parla la §4.1 (quello sì da ignorare per la BOM). Il generico fra `<` e `>` è il file realmente indicizzato sul disco; il nome prima di `<` è quello che la BOM mostrerebbe. `AsmRecordReader.LeggiMembri` lo riconosce per i membri **senza** un `ConfigName` corrispondente il cui campo `name` rispetta per intero il pattern ``: il testo dopo l'ultima `<` (fino alla `>` finale) diventa il generico da cercare nell'indice, quello prima diventa il nome mostrato. Senza questo split, il codice cercato nell'indice sarebbe la stringa intera con le parentesi comprese, mai presente nell'indice - il componente risulterebbe erroneamente "non trovato". ### 4.1 Una seconda pista, che NON va usata per la BOM Altrove nel file compaiono stringhe nella forma `istanza`: ``` \xe2\x02 "R8--_-_U6592_AISI304"\0 ``` Documenta la stessa relazione istanza→generico in modo autonomo ed è utile come riscontro incrociato (è lo stesso pattern `` già sfruttato dall'euristica testuale in `formato-asm.md` per le family table non-UNI). **Non è però una lista dei componenti**: su un campione produceva 26 voci contro 17 membri reali, includeva alcuni nipoti (figli dei sottoassiemi) ma non tutti, e ripeteva le stesse voci più volte. È la lista dei riferimenti geometrici dei vincoli di accoppiamento, non una distinta. Usarla per la BOM dà risultati sbagliati sia in eccesso sia in difetto. --- ## 5. Il nome proprio del modello La BOM intesta i blocchi col nome **del modello**, non del file: il file `9244248550b.asm.11` produce `Assieme 9244248550B contiene:`. Il nome sta nel primo record `objname` del file: ``` \x00 "objname"\0 \xe2 \xe0\x01 "object_type"\0 \xe0\x0a "name"\0 \0 ``` La **prima** occorrenza è il modello stesso; quelle successive sono altri oggetti referenziati (disegni, modelli citati). `AsmRecordReader.NomeProprio` cerca solo la prima occorrenza (`Regex.Match`, non `Matches`), verificato su tutti i campioni disponibili durante il reverse engineering. --- ## 6. Generare la BOM: ricorsione e formato di output `members` dà un solo livello. La BOM di Creo è ricorsiva: apre anche i `.asm` dei sottoassiemi. Implementato in `CreoAsm.GeneraBom`/`Render` in [`scripts/CreoBom.cs`](../scripts/CreoBom.cs). ### 6.1 Struttura del file BOM - Codifica **UTF-8 con BOM** (`EF BB BF`), fine riga **CRLF**. - Due righe vuote in testa, poi il primo blocco. - **Tre** righe vuote fra un blocco e il successivo, **due** prima del riepilogo. Il file termina con un solo CRLF. - Riga voce: `{qta:6} {tipo:>12} {nome}` — cioè quantità allineata a destra su 6 caratteri, spazio, tipo allineato a destra su 12, due spazi, nome. - Intestazioni: `Assieme contiene:` per la radice, `Sottoassieme contiene:` per gli altri. ### 6.2 Ordine dei blocchi Visita **depth-first pre-order**: si emette il blocco di un sottoassieme e subito dopo, ricorsivamente, quelli dei suoi sottoassiemi, prima di passare al fratello successivo. Ogni modello compare **una volta sola** anche se montato più volte (deduplica per nome, `CreoAsm.Blocchi`). > **Verificato.** `9244218080_C01.asm` monta `2605410010_C01` due volte > (`scripts/9244218080_c01.asm.1` + `scripts/9244218080_c01.bom.2` di riferimento): la BOM > Creo mostra un solo blocco `Sottoassieme 2605410010_C01 contiene:` con quantità 2 in testa, > non due blocchi separati, e `CreoBom.cs` riproduce l'output esatto senza modifiche. Confermata > anche la moltiplicazione delle quantità nel riepilogo (§6.3): le parti di `2605410010_C01` > compaiono raddoppiate (1→2, 1→2, 16→32). ### 6.3 Riepilogo finale `Summary of items for :` elenca **solo le parti**, con la quantità totale moltiplicata lungo l'albero (una parte in 4 esemplari dentro un sottoassieme montato 2 volte conta 8), nell'ordine di **prima comparsa** nella visita depth-first (`CreoAsm.Sommario`). Una parte che compare a più livelli occupa la posizione della sua prima comparsa e somma le quantità. ### 6.4 Risoluzione dei file dei sottoassiemi È il punto che il formato **non** risolve: il `.asm` registra il nome del modello, non il percorso. Servono quindi cartelle di ricerca fornite dall'esterno (`-p`/`--path`). `CreoAsm.CreaFinder` indicizza `.asm.` senza distinzione di maiuscole e, a parità di nome, prende la **revisione più alta** presente. > **Verificato: è la scelta giusta.** Il `.asm` padre non registra la revisione attesa del > figlio (cercata e non trovata, vedi §7), ma non serve: confermato che **Creo stesso apre > sempre la revisione più alta disponibile** di un componente, indipendentemente da quale > fosse presente quando l'assieme padre fu salvato l'ultima volta. Caso reale controllato: > `9284208092_D110.asm` (salvato 2018-08-01) referenzia `9284202481.prt`, che sulla share ha > revisioni sia del 2014 (v1-v3, presenti all'epoca) sia del 2024 (v4-v6, arrivate anni dopo) - > Creo carica v6, non v3. `CreaFinder` che prende sempre la revisione più alta in cartella > replica esattamente questo comportamento, non è un'assunzione azzardata. Vedi `formato- > asm.md` §"Risoluzione ricorsiva dell'albero" per le trappole storiche su questo punto (con > l'euristica testuale - restano valide, sono su un problema diverso: trovare la cartella > giusta, non quale revisione preferire). Se un sottoassieme non si trova, il blocco viene emesso con `*** NON RISOLTO: file .asm non trovato ***` e le sue parti restano fuori dal riepilogo. Il risultato è dichiaratamente incompleto invece che silenziosamente sbagliato. --- ## 7. Stato di ogni punto aperto Tabella di riferimento: cosa è provato, come, e cosa resta genuinamente ignoto. Il dettaglio di ciascun punto segue sotto, in due gruppi. | Punto | Stato | Prova | |---|---|---| | Deduplica blocchi + moltiplicazione quantità (§6.2/§6.3) | Risolto | BOM reale (§7.1) | | Revisione del figlio che il padre si aspetta (§6.4) | Risolto | BOM reale (§7.1) | | Stato soppresso/escluso in `members` | Risolto | BOM reale (§7.1) | | Family table di **assiemi** (rilevamento) | Risolto: serve la conferma del nome proprio (`objname`), il solo `model_name` dava falsi positivi | BOM reali + albero `raw/` completo (§7.1) | | Family table di **assiemi** (contenuto di ogni istanza) | Non risolto, nessun impatto sulla BOM (fallback già corretto) | Scan sull'intera share (§7.2) | | Costante `\x82\x7b` in `ConfigName` | Significato ignoto, rischio coperto | Scala 350 file (§7.2) | | Occorrenze multiple di `members` (simplified rep) | Mai osservato, rischio coperto | Scala 350 file (§7.2) | | Matrice di posizionamento (`e1`/`e2`/`e3`/`origin`) | Non letto, nessun impatto sulla BOM | — (§7.2) | | Campi numerici dell'header dopo `ASSEMBLY` | Non letto, nessun impatto sulla BOM | — (§7.2) | | Divergenza `family_name` ≠ `name` | Mai osservata, non letta | — (§7.2) | "Rischio coperto" vuol dire: il codice non sa cosa significhi il byte, ma verifica un invariante che lo renderebbe visibile (eccezione) se mai smettesse di valere - non è una prova che il byte non conti mai, è la garanzia che se contasse il programma lo segnalerebbe invece di sbagliare in silenzio. ### 7.1 Risolti con una BOM Creo di riferimento - **Deduplica blocchi e moltiplicazione quantità.** `9244218080_C01.asm` monta `2605410010_C01` due volte (`scripts/9244218080_c01.asm.1` + `scripts/9244218080_c01.bom.2`): la BOM Creo mostra un solo blocco `Sottoassieme 2605410010_C01 contiene:` con quantità 2 in testa (non due blocchi separati), e le sue parti compaiono raddoppiate nel riepilogo (1→2, 1→2, 16→32). `CreoBom.cs` riproduce l'output esatto senza modifiche: la deduplica per nome (`CreoAsm.Blocchi`) e la moltiplicazione lungo l'albero (`CreoAsm.Sommario`) sono il comportamento reale di Creo, non solo un'assunzione. - **Revisione del figlio.** `9284208092_D110.asm` (salvato 2018-08-01) referenzia `9284202481.prt`, che sulla share ha revisioni sia del 2014 (v1-v3, presenti quando il padre fu salvato) sia del 2024 (v4-v6, arrivate anni dopo). Confermato direttamente in Creo: apre v6, non v3 - **sempre la revisione più alta disponibile**, indipendentemente da quale fosse presente all'ultimo salvataggio del padre. `CreoAsm.CreaFinder`, che prende sempre la revisione più alta trovata in cartella, replica esattamente questo comportamento. - **Stato soppresso/escluso.** Stesso assieme `2546220101.asm` risalvato da Creo con `2546220101_C02` soppresso (`scripts/2546220101.asm.1` + `scripts/2546220101.bom.1`, con le parti/sottoassiemi correlati). Un componente soppresso **non produce alcun record** in `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` che elenca il contenuto di UNA rappresentazione (3 cuscinetti + 1 sotto-assieme, dati arbitrari), 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 contenuto diverso. Il nome istanza qui **non** passa da `ConfigName` (verificato: nessuna occorrenza letterale nel file), ma da una coppia di record distinta subito dopo l'header del modello: ``` \xe0\x01model_t_type\0\xe0\x0amodel_name\0_C0n\0\xe0\x0ageneric_name\0\0 ...stamp\0\xe2\xf2\xf7 (\xe3\x02_C0n\0\0\xe2)* -- un'occorrenza per ogni istanza sorella \xf4 ``` Quando questo succede il file generico è già di per sé un'istanza (`model_name` porta il suffisso `_C0n`, non è neutro) e `stamp` elenca le sorelle. > **Il record `model_name` da solo non è un'identità.** Su file reali quel record nomina > quasi sempre **un altro modello** - tipicamente uno dei membri del file stesso, a volte un > codice imparentato - e in quei casi `members` è già corretto e va usato. Serve una > conferma indipendente: il **nome proprio del modello** (`objname`, §5). `LeggiMembri` > segnala la family table solo se `objname` esiste **e** coincide con `model_name` privato > del suffisso `_C0n`; negli altri casi legge `members` normalmente. > > Il controllo più largo (senza la conferma di `objname`) era la causa di > **falsi "componenti mancanti"**: il parser esatto rifiutava file perfettamente leggibili, > si ripiegava sull'euristica testuale e quella estraeva codici che non esistono, riportati > poi come componenti non trovati nell'indice. Verificato contro BOM Creo di riferimento: > sui file dove il segnale scattava senza conferma di `objname`, `members` elenca > **esattamente** i componenti della BOM reale (anche nello stesso ordine); uno di questi > file è stato inoltre aperto in Creo, e l'albero del modello mostra gli stessi componenti, > senza alcuna family table. Con la conferma di `objname` richiesta, tutti i `.asm` di una > cartella `raw/` reale (351 file, alberi completi di più macchine) si leggono col parser > esatto, zero ricadute sull'euristica. Da tenere presente: il sintomo dipende da **come il file è stato salvato**, non solo dal modello. Rileggendo una revisione più recente dello stesso assieme generico, il record di identità può non esserci più e `members` elencare direttamente tutte le istanze, col tipo giusto per ciascuna - cioè esattamente quello che mostra la BOM. Il blocco quindi non è una proprietà stabile del codice: va valutato file per file, revisione per revisione. Verificato anche che il fallback su `AssemblyComponentScanner` **risolve già correttamente** il caso vero: i nomi delle istanze compaiono come testo puro nel campo `stamp`, e l'euristica ("codice che compare almeno due volte") li trova senza bisogno di decodificare `stamp`. E che una singola istanza, se esportata da Creo come file a sé (Family Table → istanza → "Salva una copia"), è un `.asm`/`.prt` normale senza traccia di family table al suo interno - letto correttamente dal parser esatto (`2546220101_c04.asm.1`, verificato contro `2546220101.bom.1`). ### 7.2 Non risolti - **Family table di assiemi — contenuto di ogni istanza.** Come recuperare il contenuto specifico di un'istanza **senza** che esista già un file separato per essa sul disco (il caso comune, se nessuno l'ha mai esportata a parte con "Salva una copia"): il meccanismo dietro `stamp` non è stato decodificato oltre a nome+tipo di ciascuna istanza sorella: `stamp` porta **solo i nomi** delle sorelle, non il loro contenuto, quindi non è la struttura da cui partire per risolvere questo punto. Resta aperto solo per completezza: sui file dove il segnale scatta davvero (con conferma del nome proprio, §7.1) il parser si ferma invece di restituire un contenuto sbagliato, e il chiamante ripiega sull'euristica testuale, che i nomi delle istanze li trova comunque come testo puro. Nessun comportamento dell'app dipende da questa decodifica. - **Costante `\x82\x7b`** nei record `ConfigName`: significato ignoto. Rischio coperto - `LeggiMembri` conta le occorrenze grezze di `ConfigName` e le confronta col numero di match del regex; se un campione avesse un valore diverso, il regex non farebbe più match (prima: in silenzio) e ora lancia eccezione invece di restituire il nome generico al posto dell'istanza senza dirlo. Verificato su tutti i 350 `.asm` di `raw/`: 215 usano davvero `ConfigName`, 0 mismatch. - **Occorrenze multiple di `members`** (rappresentazioni semplificate, mai osservate). Rischio coperto - `LeggiMembri` lancia eccezione se ne trova più di una invece di prendere silenziosamente la prima senza sapere se sia quella giusta. Verificato su tutti i 350 `.asm` di `raw/`: sempre esattamente una occorrenza. - **La matrice di posizionamento** (`e1`, `e2`, `e3`, `origin`): formato numerico non decodificato, nessun impatto sulla BOM (non letta da nessuna parte del codice). - **I campi numerici dell'header** dopo `ASSEMBLY`: non letti, nessun impatto sulla BOM. - **`family_name`**: sempre uguale a `name` su tutti i campioni disponibili; non si sa in 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` e `EstraiCodiciRiferimentiEsterni` (§7.5) | La differenza sono i **componenti soppressi** (§7.1) e i **riferimenti esterni** (§7.5): 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 quelle voci 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. ### 7.5 Riferimenti esterni: la tabella dei modelli citati Terzo modo in cui un `.asm` dipende da un file che non è fra i suoi membri, dopo i soppressi (§7.1) e le copie ambigue (§7.4). Un assieme che porta **assembly feature** o **datum copiati** da un altro modello lo cita in una tabella a parte, che non è `members` e non lascia una stringa `Componente ID …`: i due lettori esistenti non la vedevano. Struttura: un array aperto da `\xf2\xf7\x05` e chiuso da `\xf3`, con elementi `\xe3\xe2\x00`. Il nome può portare il suffisso `#AF` di §4.0.1 e/o la notazione `istanza` di §4.0.2 - il file su disco è il generico: ``` f2 f7 05 e3 e2 01 "9255708017#AF2<9255708017>" 00 ... -> 9255708017 e3 e2 02 "9255700181" 00 ... -> 9255700181 f3 ``` Va letta con `EstraiCodiciRiferimentiEsterni`, e - a differenza dei soppressi - **si ricorre dentro**: Creo carica il modello citato con tutti i suoi componenti, che quindi servono anch'essi in `raw/`. La tabella contiene anche nomi di *feature* (`MIRROR3`, `CHAMFER1`, `M10_TAPPED_HOLE1`), indistinguibili dai modelli guardando il record: non è un problema perché chi la usa cerca ogni nome nell'indice e ignora quelli che non ci sono - per lo stesso motivo questi codici non devono finire fra i "non trovati". Il caso reale: `9255728001` falliva in Distributed Batch con lo stesso `[-4] Dati di input non completi` di §7.3 perché il suo sottoassieme `9255728001_200` referenzia per assembly feature `9255708017` e `9255708018` (e con loro `9255700181/182/183`), e monta una vite `V6-12--_-_U5933_C` il cui generico `uni5933.prt` non era in `raw/`. Nessuno dei sei file compariva in `members` né fra i piazzamenti. ## 8. Uso ```powershell dotnet run scripts/CreoBom.cs -- raw/9244228558.asm.20 -p M:\modelli -o out\9244228558.bom.1 dotnet run scripts/CreoBom.cs -- raw/9244228558.asm.20 -p M:\modelli # stampa a video dotnet run scripts/Analyze.cs -- raw/9244228558.asm.20 "ConfigName" # dump dei byte per investigare a mano ``` Senza `-p`/`--path`, la ricerca dei sottoassiemi usa la cartella del file di partenza. `-p` è ripetibile per più cartelle. `-o` scrive il file col BOM UTF-8 e CRLF descritti al §6.1; senza `-o` il testo va su stdout (con `\n` al posto di `\r\n`, per leggibilità in console).