Copia in raw i file che servono a Creo, non solo quelli della distinta

Creo Distributed Batch falliva su 8 dei 22 assiemi con "[-4] Dati di
input non completi", un messaggio che non nomina il file mancante. Erano
due cause distinte, entrambe emerse aprendo gli assiemi da raw/ e
cercando nell'albero il componente con l'icona rossa.

Copia sbagliata del componente. Lo stesso codice esiste spesso in piu'
cartelle della share con pari versione e contenuto diverso (13% dei
codici): non sono revisioni, sono istantanee parallele lasciate da studi,
allestimenti fiera e cartelle temporanee. La vista dell'indice rompeva il
pareggio in modo arbitrario, e poteva vincere una copia obsoleta -
nascondendo del tutto quella buona. Creo poi caricava un modello diverso
da quello atteso, non risolveva i vincoli e non apriva l'assieme. Il
.asm non dice quale copia usa (ne' percorso, ne' UOBJ_ID del figlio, ne'
revisione attesa), quindi si replica la regola di Creo: vince la copia
nella stessa cartella dell'assieme che la referenzia. La vista ora rompe
il pareggio in modo deterministico e viene ricreata a ogni scan, cosi' un
DB esistente prende la definizione nuova senza reindicizzare.

Componenti soppressi. Spariscono da "members" ma restano citati dalla
feature che li posiziona, e il loro file serve comunque per aprire
l'assieme. Da quando la lettura usa il parser esatto - che legge solo
"members", ed e' la scelta giusta per la BOM - non venivano piu' copiati.
Ora si copiano a parte, senza entrare nel conteggio dei "non trovati":
non sono componenti della distinta, segnalarli come mancanti sarebbe un
falso allarme.

Documentate entrambe in docs/formato-asm-record.md §7.3 e §7.4, insieme
alla distinzione che le accomuna: la lista della distinta e la lista dei
file necessari a Creo non coincidono, e la seconda e' piu' lunga.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
davideandClaude Opus 5 committed 2026-09-22 11:52:16 +02:00
1 parent 0ae2a5e7cf
commit 54f267b508
8 files changed
+316 -18

No files matched your search

+8 -4
View File
@@ -63,10 +63,14 @@ designer visuale):
sotto-assiemi/parti (`RisolviAlberoComponenti`), copiandoli anch'essi in `raw/`. Usa
`AsmRecordReader` (parser esatto dei record binari, `docs/formato-asm-record.md`) e ripiega
su `AssemblyComponentScanner` (euristica testuale, `docs/formato-asm.md`) solo se il file
non rispetta il formato validato (`InvalidDataException`). ⚠️ Limite noto non coperto dal
fallback: un **assieme** con family table (raro, non solo le parti normalizzate possono
averla) può dare un risultato silenziosamente incompleto senza lanciare eccezione — vedi
`docs/formato-asm-record.md` §7.
non rispetta il formato validato (`InvalidDataException`). Due punti da non confondere,
entrambi imparati facendo fallire Creo (`docs/formato-asm-record.md` §7.3 e §7.4):
i **componenti soppressi** non sono nella distinta ma il loro file serve per aprire
l'assieme (`EstraiCodiciSoppressi`, copiati senza finire fra i "non trovati"); e quando lo
stesso codice esiste in più cartelle della share — succede per il 13% dei codici — va
copiata la copia nella **stessa cartella dell'assieme che la referenzia**
(`ScegliPerCartella`), perché il `.asm` non registra quale usa e sbagliarla impedisce a
Creo di aprire l'assieme.
5. **Verifica completezza** — mostra i `.asm` della distinta già in `raw/`, trascinabili su
Creo per l'esportazione STEP (Distributed Batch, manuale, fuori dall'app); "Verifica
completezza assiemi" controlla ricorsivamente sull'indice (non su `raw/`, stessa
+33
View File
@@ -196,6 +196,39 @@ public class AsmRecordReaderTests
Assert.Single(membri);
}
[Fact]
public void Componente_soppresso_non_e_fra_i_membri_ma_e_segnalato_a_parte()
{
// Un componente soppresso sparisce da "members" (il conteggio scende) ma resta citato
// dalla feature che lo posiziona. Non va nella BOM, però senza il suo file Creo non
// apre l'assieme, quindi va copiato lo stesso.
byte[] dati = Asm(1, Membro(1, 2, "9244206098"),
extra: Bytes("Componente ID 52 (9230100043.PRT)", 0));
Assert.Equal(new[] { "9244206098" }, AsmRecordReader.LeggiMembri(dati).Select(m => m.Nome));
string percorso = ScriviFile(dati);
try
{
Assert.Equal(new[] { "9230100043" }, AsmRecordReader.EstraiCodiciSoppressi(percorso));
Assert.DoesNotContain("9230100043", AsmRecordReader.EstraiCodiciReferenziati(percorso));
}
finally { File.Delete(percorso); }
}
[Fact]
public void Componente_gia_fra_i_membri_non_e_segnalato_come_soppresso()
{
byte[] dati = Asm(1, Membro(1, 2, "9244206098"),
extra: Bytes("Componente ID 7 (9244206098.PRT)", 0));
string percorso = ScriviFile(dati);
try
{
Assert.Empty(AsmRecordReader.EstraiCodiciSoppressi(percorso));
}
finally { File.Delete(percorso); }
}
[Fact]
public void Sezione_members_assente_lancia_eccezione()
{
+30
View File
@@ -201,4 +201,34 @@ public static class AsmRecordReader
var membri = LeggiMembri(File.ReadAllBytes(percorsoAsm));
return new HashSet<string>(membri.Select(m => m.Generico), StringComparer.OrdinalIgnoreCase);
}
/// <summary>Codici che compaiono come piazzamento ma non fra i membri: sono i componenti
/// **soppressi**, che spariscono da "members" senza lasciare traccia (§7.1) ma restano
/// citati dalla feature che li posiziona.
///
/// Non fanno parte della BOM - Creo non li elenca - ma il loro file serve comunque per
/// **aprire** l'assieme: senza, il caricamento fallisce con "dati di input non completi".
/// Sono quindi due domande diverse: cosa compare nella distinta (<see
/// cref="EstraiCodiciReferenziati"/>) e cosa serve avere su disco (questo metodo in
/// aggiunta), e la seconda lista e' piu' lunga della prima.</summary>
public static HashSet<string> EstraiCodiciSoppressi(string percorsoAsm)
{
byte[] dati = File.ReadAllBytes(percorsoAsm);
var membri = LeggiMembri(dati);
var noti = new HashSet<string>(
membri.Select(m => m.Generico).Concat(membri.Select(m => m.Nome)), StringComparer.OrdinalIgnoreCase);
var soppressi = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (Match m in PlacementRegex.Matches(Encoding.Latin1.GetString(dati)))
if (!noti.Contains(m.Groups[1].Value))
soppressi.Add(m.Groups[1].Value);
return soppressi;
}
// "Componente ID 52 (9230100043.PRT)" - la stringa cambia con la localizzazione Creo del
// file, come in AssemblyComponentScanner.HaPlacement. Minimo 3 caratteri per lo stesso
// motivo per cui li' i token generici partono da 3: sotto quella soglia si raccoglie
// rumore (sulla share esistono davvero modelli chiamati "1.prt", "2.prt"...).
private static readonly Regex PlacementRegex = new(
@"Component(?:e)? [Ii][Dd] \d+ \(([A-Za-z0-9_\-]{3,})\.(?:PRT|ASM)\)", RegexOptions.Compiled);
}
+132 -6
View File
@@ -743,6 +743,64 @@ public partial class Form1 : Form
return codiciNoti;
}
/// <summary>Tutti i file prt/asm dell'indice raggruppati per codice, tenendo per ogni
/// cartella la sola versione più alta. A differenza di <see cref="CaricaCodiciNoti"/> non
/// sceglie un vincitore fra le cartelle: lo stesso codice può esistere in più punti della
/// share (copie in cartelle di studio, di lavoro, temporanee) con contenuto diverso, e
/// quale sia quello giusto dipende da chi lo referenzia - vedi
/// <see cref="ScegliPerCartella"/>.</summary>
private static Dictionary<string, List<(string Tipo, string Percorso, int Versione, string Data)>> CaricaCandidati(string dbPath)
{
var candidati = new Dictionary<string, List<(string Tipo, string Percorso, int Versione, string Data)>>(StringComparer.OrdinalIgnoreCase);
using var connection = new SqliteConnection($"Data Source={dbPath};Mode=ReadOnly");
connection.Open();
using var command = connection.CreateCommand();
command.CommandText = "SELECT codice, tipo_file, percorso_completo, versione, data_modifica FROM files "
+ "WHERE LOWER(tipo_file) IN ('prt', 'asm')";
using var reader = command.ExecuteReader();
while (reader.Read())
{
string codice = reader.GetString(0);
string tipo = reader.GetString(1).ToLowerInvariant();
string percorso = reader.GetString(2);
int versione = reader.GetInt32(3);
var lista = candidati.TryGetValue(codice, out var l) ? l : candidati[codice] = new();
string cartella = Path.GetDirectoryName(percorso) ?? "";
int esistente = lista.FindIndex(x => x.Tipo == tipo
&& string.Equals(Path.GetDirectoryName(x.Percorso), cartella, StringComparison.OrdinalIgnoreCase));
if (esistente < 0)
lista.Add((tipo, percorso, versione, reader.GetString(4)));
else if (versione > lista[esistente].Versione)
lista[esistente] = (tipo, percorso, versione, reader.GetString(4));
}
return candidati;
}
/// <summary>Fra più copie dello stesso codice sulla share sceglie quella che userebbe Creo:
/// prima cerca nella cartella dell'assieme che la referenzia (Creo risolve i componenti
/// partendo da lì), poi ripiega sulla versione più alta e, a parità, sulla più recente.
/// Senza questo criterio si finisce per copiare in "raw" una copia omonima ma di un altro
/// modello - Creo la carica e i vincoli di accoppiamento non si risolvono, quindi l'assieme
/// non si apre affatto.</summary>
private static List<(string Tipo, string Percorso)> ScegliPerCartella(
List<(string Tipo, string Percorso, int Versione, string Data)> candidati, string? cartellaPadre)
{
return candidati
.GroupBy(c => c.Tipo)
.Select(perTipo =>
{
var scelto = perTipo
.OrderByDescending(c => cartellaPadre is not null
&& string.Equals(Path.GetDirectoryName(c.Percorso), cartellaPadre, StringComparison.OrdinalIgnoreCase))
.ThenByDescending(c => c.Versione)
.ThenByDescending(c => c.Data)
.First();
return (scelto.Tipo, scelto.Percorso);
})
.ToList();
}
/// <summary>Codici referenziati direttamente in un assieme: prova prima il parser esatto
/// dei record binari (<see cref="AsmRecordReader"/>, vedi docs/formato-asm-record.md), che
/// non ha i falsi positivi/negativi noti dell'euristica testuale (docs/formato-asm.md); se
@@ -763,6 +821,50 @@ public partial class Form1 : Form
}
}
/// <summary>Componenti soppressi di un assieme (<see cref="AsmRecordReader.EstraiCodiciSoppressi"/>):
/// non sono nella distinta, ma senza il loro file Creo non apre l'assieme. Vuoto per i file
/// che il parser esatto non sa leggere: lì si usa l'euristica testuale, che i piazzamenti li
/// riconosce già per conto suo (<see cref="AssemblyComponentScanner"/>).</summary>
private static IEnumerable<string> CodiciSoppressi(string percorsoAsm)
{
try
{
return AsmRecordReader.EstraiCodiciSoppressi(percorsoAsm);
}
catch (InvalidDataException)
{
return Array.Empty<string>();
}
}
/// <summary>Cartella della share da cui proviene un file già copiato in "raw": fra le copie
/// omonime indicizzate è quella di pari dimensione. Serve per gli assiemi di partenza, che
/// arrivano dalla fase precedente senza portarsi dietro l'origine. Null se non si riesce a
/// stabilirla (il file non è nell'indice, o la share non risponde): in quel caso la scelta
/// del componente ricade sul criterio generale.</summary>
private static string? CartellaDiOrigine(string fileInRaw,
Dictionary<string, List<(string Tipo, string Percorso, int Versione, string Data)>> candidati)
{
string nome = Path.GetFileName(fileInRaw);
if (!candidati.TryGetValue(nome.Split('.')[0], out var perCodice))
return null;
try
{
long dimensione = new FileInfo(fileInRaw).Length;
foreach (var c in perCodice.Where(c => string.Equals(Path.GetFileName(c.Percorso), nome, StringComparison.OrdinalIgnoreCase)))
{
var info = new FileInfo(c.Percorso);
if (info.Exists && info.Length == dimensione)
return Path.GetDirectoryName(c.Percorso);
}
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
// share non raggiungibile: si prosegue col criterio generale
}
return null;
}
/// <summary>Risolve ricorsivamente l'albero dei componenti a partire dai file .asm indicati
/// (già copiati in "raw"): per ogni assieme legge i codici referenziati al suo interno, li
/// cerca nell'indice, copia in "raw" quelli trovati e, se sono a loro volta assiemi, li
@@ -771,45 +873,69 @@ public partial class Form1 : Form
private (List<(string Codice, string Tipo, string Percorso)> Trovati, List<string> NonTrovati) RisolviAlberoComponenti(
IEnumerable<string> asmIniziali, IProgress<(int Fatti, int Totale)>? progress = null)
{
var daScansionare = new Queue<string>(asmIniziali);
var codiciIniziali = daScansionare.Select(f => Path.GetFileName(f).Split('.')[0]);
// Ogni assieme viaggia con la cartella della share da cui proviene: e' li' che Creo
// cerca per primi i suoi componenti, quindi e' li' che li cerchiamo anche noi.
var daScansionare = new Queue<(string File, string? CartellaOrigine)>(
asmIniziali.Select(f => (f, (string?)null)));
var codiciIniziali = asmIniziali.Select(f => Path.GetFileName(f).Split('.')[0]).ToList();
var codiciVisti = new HashSet<string>(
codiciIniziali.Concat(codiciIniziali.Select(AssemblyComponentScanner.CodiceRadice)),
StringComparer.OrdinalIgnoreCase);
var codiciSoppressiVisti = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var trovati = new List<(string Codice, string Tipo, string Percorso)>();
var codiciNonTrovati = new List<string>();
if (daScansionare.Count == 0 || _dbPath is null)
return (trovati, codiciNonTrovati);
var codiciNoti = CaricaCodiciNoti(_dbPath);
var candidati = CaricaCandidati(_dbPath);
int processati = 0;
while (daScansionare.Count > 0)
{
var asm = daScansionare.Dequeue();
var (asm, cartellaOrigine) = daScansionare.Dequeue();
cartellaOrigine ??= CartellaDiOrigine(asm, candidati);
processati++;
foreach (var codice in EstraiCodiciReferenziati(asm, codiciNoti.Keys))
{
if (!codiciVisti.Add(codice))
continue;
if (!codiciNoti.TryGetValue(codice, out var componenti))
if (!candidati.TryGetValue(codice, out var perCodice))
{
codiciNonTrovati.Add(codice);
continue;
}
foreach (var (tipo, percorso) in componenti)
foreach (var (tipo, percorso) in ScegliPerCartella(perCodice, cartellaOrigine))
{
CopiaFileTrovati(new[] { percorso }, _ultimaCartellaRaw!);
trovati.Add((codice, tipo, percorso));
if (tipo == "asm")
{
daScansionare.Enqueue(Path.Combine(_ultimaCartellaRaw!, Path.GetFileName(percorso)));
daScansionare.Enqueue((Path.Combine(_ultimaCartellaRaw!, Path.GetFileName(percorso)),
Path.GetDirectoryName(percorso)));
}
}
}
// I componenti soppressi non compaiono nella BOM, ma Creo ne vuole comunque il file
// per aprire l'assieme: si copiano senza entrare nel conteggio dei "non trovati"
// (non essendo componenti della distinta, segnalarli come mancanti sarebbe un falso
// allarme) e senza ricorrervi dentro.
foreach (var codice in CodiciSoppressi(asm))
{
if (!codiciSoppressiVisti.Add(codice) || codiciVisti.Contains(codice))
continue;
if (!candidati.TryGetValue(codice, out var perCodice))
continue;
foreach (var (tipo, percorso) in ScegliPerCartella(perCodice, cartellaOrigine))
{
CopiaFileTrovati(new[] { percorso }, _ultimaCartellaRaw!);
trovati.Add((codice, tipo, percorso));
}
}
progress?.Report((processati, processati + daScansionare.Count));
}
+11 -2
View File
@@ -40,13 +40,22 @@ public static class Indexer
CREATE INDEX IF NOT EXISTS idx_files_tipo ON files(tipo_file);
CREATE INDEX IF NOT EXISTS idx_files_cartella ON files(cartella_radice);
CREATE VIEW IF NOT EXISTS files_ultima_versione AS
-- DROP+CREATE invece di "IF NOT EXISTS": la vista e' ricalcolata a ogni avvio di scan,
-- cosi' un DB gia' esistente prende la definizione corrente senza reindicizzare.
DROP VIEW IF EXISTS files_ultima_versione;
CREATE VIEW files_ultima_versione AS
SELECT percorso_completo, percorso_relativo, nome_file, cartella_radice, famiglia,
codice, tipo_file, versione, dimensione_byte, data_modifica, scansionato_il
FROM (
SELECT f.*, ROW_NUMBER() OVER (
PARTITION BY cartella_radice, codice, tipo_file
ORDER BY versione DESC
-- A parita' di versione (stesso codice in due sottocartelle della stessa
-- radice, es. una copia vecchia in una cartella di lavoro) vince il file
-- modificato piu' di recente: senza questo criterio il pareggio e' rotto in
-- modo arbitrario e puo' vincere la copia obsoleta, nascondendo del tutto
-- quella buona. Il file copiato in "raw" sarebbe allora un modello diverso da
-- quello che l'assieme si aspetta, e Creo non riesce ad aprirlo.
ORDER BY versione DESC, data_modifica DESC, percorso_completo
) AS rn
FROM files f
) WHERE rn = 1;
+18 -1
View File
@@ -44,7 +44,7 @@ Distinta (.xls/.xml) → CSV (componente;descrizione) → raw/ (CAD copiati
| 1 | Database | "Aggiorna indice" → `Indexer.ScanAsync`; "Cambia cartella..." → `ScegliCartellaLavoro` | Crea/aggiorna `cad_index.db` scansionando la share; sceglie/mostra la cartella di lavoro |
| 2 | Importa Distinta Base | selezione file → `ImportaDistinta` → `DistintaBaseImporter.ConvertiInCsv` | Xls/Xml gestionale → CSV componente;descrizione, scritto nella cartella di lavoro |
| 3 | Crea cartella raw | "Crea cartella raw" → `CreaCartellaRaw` | `TrovaPercorsiCad` + `CopiaFileTrovati` (parallela) → `raw/` dentro la cartella di lavoro |
| 4 | Trova componenti referenziati | "Trova componenti referenziati" → `TrovaPrtReferenziati` → `RisolviAlberoComponenti` | Legge i `.asm` in `raw/` con `AssemblyComponentScanner`, risolve ricorsivamente sotto-assiemi/parti nell'indice, li copia anch'essi in `raw/` |
| 4 | Trova componenti referenziati | "Trova componenti referenziati" → `TrovaPrtReferenziati` → `RisolviAlberoComponenti` | Legge i `.asm` in `raw/`, risolve ricorsivamente sotto-assiemi/parti nell'indice e li copia anch'essi in `raw/`, insieme ai componenti soppressi che servono a Creo per aprire l'assieme |
| 5 | Verifica completezza | "Verifica completezza assiemi" → `VerificaCompletezzaAssiemi` | Completezza ricorsiva sui percorsi in indice. All'ingresso nella fase, `AggiornaAnteprimaAsm` mostra le icone trascinabili dei `.asm` della distinta già in `raw/`, da trascinare su Creo per l'esportazione STEP (Distributed Batch) |
Stato condiviso tra le fasi (campi privati di `Form1`): `_dbPath`, `_cartellaLavoro`,
@@ -100,6 +100,23 @@ Distinta (.xls/.xml) → CSV (componente;descrizione) → raw/ (CAD copiati
positivi noti in `docs/formato-asm.md`. Resta il fallback per i file che il parser esatto
non copre, e la fonte di verità per `ExtractCreoStrings.cs`/`BomViaDb.cs`.
Quella lista è **la distinta**, e non coincide con i file che servono a Creo per aprire
l'assieme: i componenti **soppressi** ne restano fuori (non sono nella BOM) ma il loro file
serve lo stesso, altrimenti il caricamento fallisce. `AsmRecordReader.EstraiCodiciSoppressi`
li restituisce a parte, e la fase 4 li copia in `raw/` senza contarli fra i "non trovati" —
vedi `docs/formato-asm-record.md` §7.3.
## Quale copia di un componente si copia in `raw/`
Lo stesso codice esiste spesso in più cartelle della share, con contenuto diverso e pari
versione (13% dei codici, vedi `docs/formato-asm-record.md` §7.4). Il `.asm` non registra
quale copia usa, quindi `RisolviAlberoComponenti` replica la regola di Creo:
`Form1.ScegliPerCartella` preferisce la copia nella **stessa cartella dell'assieme che la
referenzia** (`Form1.CartellaDiOrigine` ricostruisce quella cartella per gli assiemi di
partenza), e solo in mancanza ripiega su versione più alta e data più recente. Sbagliare copia
non dà un errore comprensibile: Creo non apre l'assieme e dice solo "dati di input non
completi".
## Import distinta base
- **DistintaBaseImporter.cs** — converte l'export del gestionale (SpreadsheetML XML con
+21 -5
View File
@@ -29,17 +29,33 @@ componenti e nel filtro per cartella).
Vista `files_ultima_versione`: stessa struttura di `files`, filtrata alla sola riga con
`versione` più alta per ogni `(cartella_radice, codice, tipo_file)`, tramite `ROW_NUMBER()
OVER (PARTITION BY ... ORDER BY versione DESC)`. Tutte le ricerche di componenti (`Form1.
TrovaPercorsiCad`, `Form1.CaricaCodiciNoti`) interrogano questa vista, non `files`
direttamente, così un componente con più versioni sulla share risolve sempre all'ultima.
Deliberatamente una `ROW_NUMBER()` invece di un self-join con `GROUP BY`: su ~200k righe un
self-join degenera in tempi quasi quadratici, `ROW_NUMBER()` resta lineare.
OVER (PARTITION BY ... ORDER BY versione DESC, data_modifica DESC, percorso_completo)`. Tutte
le ricerche di componenti (`Form1.TrovaPercorsiCad`, `Form1.CaricaCodiciNoti`) interrogano
questa vista, non `files` direttamente, così un componente con più versioni sulla share
risolve sempre all'ultima. Deliberatamente una `ROW_NUMBER()` invece di un self-join con
`GROUP BY`: su ~200k righe un self-join degenera in tempi quasi quadratici, `ROW_NUMBER()`
resta lineare.
La vista è ricreata (`DROP` + `CREATE`, non `CREATE IF NOT EXISTS`) a ogni "Aggiorna indice",
così un DB già esistente prende la definizione corrente senza dover reindicizzare la share.
Nota: `cartella_radice` fa parte della partition, quindi se lo stesso codice+tipo esiste
in **due cartelle radice diverse** (duplicati sulla share), la vista restituisce una riga per
ciascuna — non una sola in assoluto. `TrovaPercorsiCad` gestisce questo caso deduplicando per
`tipo_file` lato C# (vedi sotto).
> **I criteri di pareggio non sono decorativi.** Lo stesso codice può esistere **nella stessa
> cartella radice, con la stessa `versione`**, in due sottocartelle diverse (una copia in una
> cartella di studio, di lavoro o temporanea). Senza `data_modifica` nell'`ORDER BY` il
> pareggio veniva rotto in modo arbitrario, e poteva vincere la copia obsoleta — nascondendo
> del tutto quella buona, che non compariva più nella vista. Il file che finiva in `raw/` era
> allora un modello diverso da quello che l'assieme si aspetta: Creo lo carica, i vincoli di
> accoppiamento non si risolvono e **l'assieme non si apre affatto**.
>
> Il criterio deterministico però non basta a scegliere *bene*: è il minimo per non sbagliare
> a caso. Quale copia sia quella giusta dipende da chi la referenzia, non dalla data — vedi
> [`formato-asm-record.md`](formato-asm-record.md) §7.4 e `Form1.ScegliPerCartella`.
## Come viene popolato: `Indexer.ScanAsync`
Chiamato da "Aggiorna indice" (fase 1) e riutilizza lo stesso `dbPath` già caricato.
+63
View File
@@ -394,6 +394,16 @@ di sbagliare in silenzio.
`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`
@@ -476,6 +486,59 @@ di sbagliare in silenzio.
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` |
La differenza sono i **componenti soppressi** (§7.1): 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 i soppressi 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.
## 8. Uso
```powershell