From 0ae2a5e7cf4c6b350eb46635d54740dda496c111 Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Tue, 22 Sep 2026 10:47:08 +0200 Subject: [PATCH] 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) --- PrepStep.Tests/AsmRecordReaderTests.cs | 55 +++++++++++++--- PrepStep/AsmRecordReader.cs | 35 +++++++---- docs/formato-asm-record.md | 86 ++++++++++---------------- 3 files changed, 100 insertions(+), 76 deletions(-) diff --git a/PrepStep.Tests/AsmRecordReaderTests.cs b/PrepStep.Tests/AsmRecordReaderTests.cs index 5165be2..f5f43e7 100644 --- a/PrepStep.Tests/AsmRecordReaderTests.cs +++ b/PrepStep.Tests/AsmRecordReaderTests.cs @@ -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 \xe0\x0a name\0 \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 \xe0\x0a model_name\0 \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(), "\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(() => 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); diff --git a/PrepStep/AsmRecordReader.cs b/PrepStep/AsmRecordReader.cs index 7bf9e9b..a770cb7 100644 --- a/PrepStep/AsmRecordReader.cs +++ b/PrepStep/AsmRecordReader.cs @@ -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)) diff --git a/docs/formato-asm-record.md b/docs/formato-asm-record.md index c1b4543..18e1c72 100644 --- a/docs/formato-asm-record.md +++ b/docs/formato-asm-record.md @@ -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: