Files
prep-step/docs/formato-asm-record.md
T
davideandClaude Opus 5 13f6de6402 Copia in raw anche i modelli citati per riferimento esterno
Terzo caso di "[-4] Dati di input non completi" in Creo Distributed
Batch, dopo i soppressi e le copie ambigue del commit precedente: falliva
9255728001, e solo lui fra i sei assiemi del batch.

Il file in raw/ era identico bit per bit a quello sulla share, quindi non
era il modello: mancava il contorno. Il suo sottoassieme _200 e' legato
ad altri due (9255708017, 9255708018) da assembly feature e datum
copiati, e monta una vite V6-12--_-_U5933_C il cui generico uni5933.prt
non era in raw/. Nessuno dei sei file compariva in "members" ne' fra i
piazzamenti: i due lettori esistenti non potevano vederli.

Quei modelli stanno in una tabella a parte del .asm - array \xf2\xf7\x05
… \xf3 con elementi \xe3\xe2<tipo><nome>\0 - che ora si legge con
EstraiCodiciRiferimentiEsterni, riusando la normalizzazione di #AF<n> e
istanza<generico> gia' fatta per i membri. A differenza dei soppressi si
ricorre dentro: Creo carica il modello citato con tutto il suo albero, e
infatti 9255700182 arriva solo per quella via.

La tabella contiene anche nomi di feature (MIRROR3, CHAMFER1) che dal
record non si distinguono dai modelli. Non e' un problema: come per i
soppressi si cerca ogni nome nell'indice e si ignora quello che non c'e'.
Per lo stesso motivo questi codici restano fuori dai "non trovati", e la
coda di scansione porta ora un flag che tiene fuori anche i rami
raggiunti per riferimento esterno - la lista dei componenti mancanti
continua a parlare solo della distinta.

Documentato in docs/formato-asm-record.md §7.5.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 14:09:48 +02:00

584 lines
33 KiB
Markdown

# 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 "Trova componenti
referenziati"/"Verifica completezza" - 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 <tipo> <nome campo>\0 <valore>` |
| `\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 <n>` identifica il layout dei record che seguono |
| `\xf8` | array: `\xf8 <conteggio> ... <elementi>` |
| `\xfb` | lo schema è definito in linea nel primo elemento dell'array |
| `\xf4` | chiude una sezione (seguito da `#\n#<NomeSezione>\n`) |
Il byte `<tipo>` 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.
In `CreoBom.cs` è `AsmRecordReader.AsInt`, 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 <n> \xf7 <idSchema> \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 <N> \xf7 <idSchema> \xfb
\xe3 \xe0\x01 "id"\0 <int>
\xe0\x01 "type"\0 <int>
\xe0\x0a "name"\0 <stringa>\0
\xe0\x0a "family_name"\0 <stringa>\0
\xe0\x00 "tha_ref"\0 \xe1
\xe0\x02 "e1"\0 ... "e2" ... "e3" ... "origin" ...
\xe3 <id> <type> <name>\0 <family_name>\0 <resto posizionale>
\xe3 <id> <type> <name>\0 <family_name>\0 <resto posizionale>
...
```
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 <int> <int> <stringa>\0 <stringa>\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 <idSchema>] <nome istanza>\0 <id membro> \x82\x7b \xe1\xe1
```
- `\xe2\x33` — parametro di tipo stringa (`\x33` = `'3'`). Confronto: `\xe2\x32`
è un parametro numerico, es. `DENSITA`.
- `<nome istanza>` — il nome esatto che la BOM stampa.
- `<id membro>` — l'`id` del record `members` a cui il parametro si applica,
con la codifica intera di §2.1. **È questo il collegamento.**
- `\xf7 <idSchema>` 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<n>`)
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 `<codice>#AF<n>` (`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
`<nome istanza><generico>` invece del solo nome generico:
```
\xe3 <id> <type> "<istanza><generico>"\0 "<istanza><generico>"\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 `<testo><testo>`: 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<generico>`:
```
\xe2\x02 "R8--_-_U6592_AISI304<UNI6592>"\0
```
Documenta la stessa relazione istanza→generico in modo autonomo ed è utile come
riscontro incrociato (è lo stesso pattern `<codice><GENERICO>` 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 <int> \xe0\x0a "name"\0 <nome>\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 <nome> contiene:` per la radice,
`Sottoassieme <nome> 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 <radice>:` 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 `<nome>.asm.<rev>`
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 <n> (<codice>.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<int>\xe0\x0amodel_name\0<codice>_C0n\0\xe0\x0ageneric_name\0\0
...stamp\0\xe2\xf2\xf7<idSchema>
(\xe3<type>\x02<codice>_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<tipo><nome>\x00`. Il nome può portare il suffisso `#AF<n>` di §4.0.1 e/o la notazione
`istanza<generico>` 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<UNI5933>` 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).