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:
1 parent
b136e7f49b
commit
0ae2a5e7cf
3 files changed
+100
-76
No files matched your search
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user