Elimina i falsi "componenti mancanti" dal rilevamento delle family table

Il record "model_name" veniva preso per l'identita' del file, ma sui file
reali nomina quasi sempre un altro modello - tipicamente uno dei membri del
file stesso. Il parser esatto rifiutava percio' file perfettamente
leggibili, il chiamante ripiegava sull'euristica testuale e quella
estraeva codici inesistenti, riportati poi come componenti non trovati
nell'indice.

Il segnale ora richiede la conferma del nome proprio del modello
(objname): scatta solo se il file dichiara davvero se stesso come istanza.

Verificato contro le BOM esportate da Creo per 7 assiemi reali: 6 vengono
riprodotte byte per byte (3 di queste prima non si generavano affatto), e
la settima differisce per una sola parte, presente nella revisione di un
sotto-assieme che Creo ha caricato ma assente in quella piu' recente. La
verifica di completezza passa su tutti e 7 (prima 3 risultavano
incompleti), e tutti i 351 .asm di una cartella raw/ reale si leggono col
parser esatto, senza mai ricadere sull'euristica.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
davideandClaude Opus 5 committed 2026-09-22 10:47:08 +02:00
1 parent b136e7f49b
commit 0ae2a5e7cf
3 files changed
+100 -76

No files matched your search

+45 -10
View File
@@ -37,6 +37,17 @@ public class AsmRecordReaderTests
private static byte[] ConfigName(string istanza, int idMembro) =>
Bytes(0xe3, "ConfigName", 0, 0xe2, (byte)'3', istanza, 0, idMembro, 0x82, 0x7b);
// \0objname\0 \xe2 \xe0\x01 object_type\0 <int> \xe0\x0a name\0 <nome>\0 - il nome proprio
// del modello (quello che la BOM intesta), vedi docs/formato-asm-record.md §5
private static byte[] ObjName(string nome) =>
Bytes(0, "objname", 0, 0xe2, 0xe0, 0x01, "object_type", 0, 2, 0xe0, 0x0a, "name", 0, nome, 0);
// identita' dichiarata dal file: \xe0\x01 model_t_type\0 <int> \xe0\x0a model_name\0 <nome>\0
// \xe0\x0a generic_name\0 \0 (vuoto)
private static byte[] ModelName(string nome) =>
Bytes(0xe0, 0x01, "model_t_type", 0, 2, 0xe0, 0x0a, "model_name", 0, nome, 0,
0xe0, 0x0a, "generic_name", 0, 0);
private static byte[] Asm(int conteggioDichiarato, byte[] membri, byte[]? extra = null) =>
Bytes("header ", extra ?? Array.Empty<byte>(), "\0members\0", 0xf1, 0xf8, conteggioDichiarato, membri, "#MdlStatus");
@@ -131,18 +142,44 @@ public class AsmRecordReaderTests
[Fact]
public void Assieme_family_table_con_generic_name_lancia_eccezione()
{
// Regressione: 2546220101.asm (docs/formato-asm-record.md §7) e' esso stesso
// un'istanza di family table (model_name "2546220101_C01" + generic_name vuoto); il
// suo "members" descrive solo questa rappresentazione, non le 4 istanze reali della
// BOM Creo. Va segnalato invece di restituire silenziosamente il contenuto sbagliato.
// Regressione: un file che e' esso stesso un'istanza di family table (il suo nome
// proprio "2546220101" piu' il suffisso "_C01" in model_name) descrive in "members"
// solo quella rappresentazione, non le istanze sorelle. Va segnalato invece di
// restituire silenziosamente il contenuto sbagliato (docs/formato-asm-record.md §7).
byte[] dati = Asm(1, Membro(1, 2, "9244206098"),
extra: Bytes(0xe0, 0x01, "model_t_type", 0, 2,
0xe0, 0x0a, "model_name", 0, "2546220101_C01", 0,
0xe0, 0x0a, "generic_name", 0, 0));
extra: Bytes(ObjName("2546220101"), ModelName("2546220101_C01")));
Assert.Throws<InvalidDataException>(() => AsmRecordReader.LeggiMembri(dati));
}
[Fact]
public void Model_name_di_un_altro_modello_non_lancia_eccezione()
{
// Regressione: sui file reali della share il record "model_name" nomina spesso un
// modello diverso dal file (tipicamente un suo membro), non l'identita' del file. Il
// nome proprio (objname) non conferma, quindi non e' il sintomo della family table:
// "members" e' corretto e va usato. Verificato contro BOM Creo reali - senza questo
// controllo il file cadeva sull'euristica testuale, che produceva codici inesistenti
// segnalati poi come "componenti mancanti".
byte[] dati = Asm(1, Membro(1, 2, "9044207999A_C01"),
extra: Bytes(ObjName("9244207999A"), ModelName("9044207999A_C01")));
var membri = AsmRecordReader.LeggiMembri(dati);
Assert.Equal("9044207999A_C01", membri.Single().Nome);
}
[Fact]
public void Model_name_senza_nome_proprio_non_lancia_eccezione()
{
// Stesso caso del test sopra ma senza record "objname" (formato osservato su tutti i
// file reali che facevano scattare il falso positivo): senza conferma del nome proprio
// il record non e' un'identita', quindi "members" va letto normalmente.
byte[] dati = Asm(1, Membro(1, 2, "9244206098"), extra: ModelName("2546220101_C01"));
Assert.Single(AsmRecordReader.LeggiMembri(dati));
}
[Fact]
public void Generic_name_senza_suffisso_istanza_non_lancia_eccezione()
{
@@ -152,9 +189,7 @@ public class AsmRecordReaderTests
// correlato al nome del file). In quei casi "members" e' letto correttamente e va
// usato normalmente, non bloccato per un falso positivo.
byte[] dati = Asm(1, Membro(1, 2, "9244206098"),
extra: Bytes(0xe0, 0x01, "model_t_type", 0, 2,
0xe0, 0x0a, "model_name", 0, "9244230117", 0,
0xe0, 0x0a, "generic_name", 0, 0));
extra: Bytes(ObjName("9244230117"), ModelName("9244230117")));
var membri = AsmRecordReader.LeggiMembri(dati);
+22 -13
View File
@@ -67,12 +67,13 @@ public static class AsmRecordReader
// codice cercato nell'indice sarebbe la stringa intera con le parentesi, mai trovata.
private static readonly Regex IstanzaConGenericoRegex = new(@"^([\x20-\x7e]+?)<([\x20-\x7e]+)>$", RegexOptions.Compiled);
// sintomo vero e proprio (docs/formato-asm-record.md §7): il file e' esso stesso
// un'istanza di family table (il suo "model_name" ha il suffisso "_C0n" delle istanze,
// stessa convenzione di SuffissoArrayFeatureRegex/ConfigName sopra) - "members" descrive
// solo questa rappresentazione, non le istanze sorelle elencate in "stamp". Verificato su
// 2546220101.asm: la BOM Creo reale mostra 4 istanze diverse, "members" ne restituisce
// silenziosamente solo una.
// Suffisso "_C0n" delle istanze di family table (stessa convenzione di
// SuffissoArrayFeatureRegex/ConfigName sopra). Usato insieme a OwnModelIdentityRegex per
// riconoscere un file che e' esso stesso un'istanza, il cui "members" descriverebbe solo
// quella rappresentazione invece delle istanze sorelle (docs/formato-asm-record.md §7).
// Da solo NON basta: su file reali quel record nomina spesso un altro modello (un membro
// del file, o un codice imparentato), non il file stesso - per quello serve la conferma
// del nome proprio (objname).
private static readonly Regex SuffissoIstanzaFamilyTableRegex = new(@"_C\d{2,}$", RegexOptions.Compiled);
// primo elemento dell'array "members": porta le chiavi per esteso.
@@ -118,13 +119,6 @@ public static class AsmRecordReader
{
string testo = Encoding.Latin1.GetString(dati);
var identita = OwnModelIdentityRegex.Match(testo);
if (identita.Success && SuffissoIstanzaFamilyTableRegex.IsMatch(identita.Groups[2].Value))
throw new InvalidDataException(
$"assieme con family table: il file e' esso stesso l'istanza '{identita.Groups[2].Value}', " +
"'members' descriverebbe solo questa rappresentazione, non le istanze sorelle - vedi " +
"docs/formato-asm-record.md §7");
var configNameMatches = ConfigNameRegex.Matches(testo);
int configNameGrezzi = ContaOccorrenze(testo, (char)0xe3 + "ConfigName\0");
if (configNameMatches.Count != configNameGrezzi)
@@ -171,6 +165,21 @@ public static class AsmRecordReader
if (grezzi.Count != conteggio)
throw new InvalidDataException($"attesi {conteggio} membri, trovati {grezzi.Count}: lettura disallineata");
// Il record "model_name" e' l'identita' del file solo se il nome proprio del modello
// (objname, §5) lo conferma: da solo nomina spesso un altro modello - un membro del
// file, o un modello imparentato - e in quei casi "members" e' gia' corretto. Vedi
// docs/formato-asm-record.md §7.
var identita = OwnModelIdentityRegex.Match(testo);
var nomeProprio = ObjNameRegex.Match(testo);
if (identita.Success && nomeProprio.Success
&& SuffissoIstanzaFamilyTableRegex.IsMatch(identita.Groups[2].Value)
&& string.Equals(SuffissoIstanzaFamilyTableRegex.Replace(identita.Groups[2].Value, ""),
nomeProprio.Groups[1].Value, StringComparison.OrdinalIgnoreCase))
throw new InvalidDataException(
$"assieme con family table: il file e' esso stesso l'istanza '{identita.Groups[2].Value}', " +
"'members' descriverebbe solo questa rappresentazione, non le istanze sorelle - vedi " +
"docs/formato-asm-record.md §7");
return grezzi.Select(g =>
{
if (istanze.TryGetValue(g.Id, out var nomeIstanza))
+33 -53
View File
@@ -359,7 +359,7 @@ di ciascun punto segue sotto, in due gruppi.
| 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 lato sicurezza (mai un falso negativo pericoloso), segnale non affidabile al 100% | BOM reale + scala intera share (§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) |
@@ -411,24 +411,32 @@ di sbagliare in silenzio.
\xf4
```
Il file generico è già di per sé un'istanza (`model_name` porta il suffisso `_C0n`, non è
neutro), e `stamp` elenca le sorelle. Il segnale usato per riconoscerlo è la combinazione
`model_t_type`+`model_name` (con suffisso `_C0n`)+`generic_name` vuoto
(`AsmRecordReader.OwnModelIdentityRegex` + `SuffissoIstanzaFamilyTableRegex`) - **non** la
sola presenza di `generic_name`, che su altri 119 file del corpus da 350 compare in un
contesto diverso e non correlato (`model_name` lì è un codice slegato dal nome del file, e
`members` in quei casi è già corretto). Verificato: solo 25/350 file rispettano il segnale
affidabile, e su quei 25 `AsmRecordReader.LeggiMembri` lancia `InvalidDataException` invece
di restituire il contenuto sbagliato della rappresentazione di default.
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.
> **Non è un segnale affidabile al 100%.** Un controllo su scala più ampia (tutta la share,
> §7.2) ha trovato un caso in cui il segnale scatta ma **non c'è nessuna family table**:
> `9053005060.asm` è un assieme ordinario con due parti figlie `_C01`/`_C02` (geometrie
> diverse, nessuna family table nell'albero Creo - verificato dall'utente). Il perché il
> record di identità propria porti comunque il suffisso `_C0n` in quel caso resta ignoto.
> Non cambia la sicurezza del comportamento (`InvalidDataException` → fallback su
> `AssemblyComponentScanner`, che legge comunque il file correttamente), ma il segnale va
> trattato come "candidato da verificare", non come conferma di family table.
> **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
@@ -443,42 +451,14 @@ di sbagliare in silenzio.
- **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.
Aperto anche cosa sia davvero il record `generic_name` sui 119 file dove non è questo
sintomo.
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.
**Aggiornamento (scan sull'intera share, non solo sul campione da 350 file).** Applicando
il segnale (`OwnModelIdentityRegex` + `SuffissoIstanzaFamilyTableRegex`) a tutti i `.asm`
indicizzati su `X:\` (7157 combinazioni cartella+codice uniche, ultima revisione ciascuna):
**95 candidati**, non solo i 25/350 del campione originale.
Sul primo candidato controllato (`9053005060.asm.1`, un'unica istanza sorella secondo il
segnale), `stamp` si decodifica a mano byte per byte:
```
\xe0\x00 "stamp"\0 \xe2\xf2\xf7\x07 \xe3\x02 "9053005060_C02"\0\0 \xe2
```
Confermato: **`stamp` porta solo i nomi** delle istanze sorelle, non il loro contenuto -
non è quindi la struttura giusta da cui partire per risolvere questo punto in generale.
**Ma il candidato stesso era un falso positivo.** Verificato in Creo (albero del modello,
screenshot dell'utente): `9053005060.asm` **non** è una family table - è un assieme
ordinario con due parti figlie distinte `9053005060_C01.prt` e `9053005060_C02.prt` (pesi e
geometrie diverse, nessuna icona di family table nell'albero). Il segnale ha comunque
acceso perché, in quella revisione, il record di identità propria del file portava
`model_name = 9053005060_C01` con `generic_name` vuoto - non si sa ancora perché (forse
legato all'ultimo componente attivo/rigenerato quando il file fu salvato, non a una vera
family table). **Conseguenza pratica: il segnale, validato con BOM reale solo su
`2546220101.asm` (§7.1), non è affidabile su larga scala senza verifica caso per caso** - i
95 candidati trovati dallo scan sono solo *papabili*, non confermati; non c'è motivo di
credere che la maggioranza sia una vera family table di assieme.
**Resta comunque un punto aperto solo per completezza documentale, non un problema da
correggere**: qualunque sia la causa del falso positivo, il fallback su
`AssemblyComponentScanner` gestisce comunque correttamente il file (legge i due membri via
euristica testuale, non tenta di interpretare `stamp`), quindi nessun comportamento della
app dipende da questa decodifica.
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: