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 sotto-assiemi/parti (`RisolviAlberoComponenti`), copiandoli anch'essi in `raw/`. Usa
`AsmRecordReader` (parser esatto dei record binari, `docs/formato-asm-record.md`) e ripiega `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 su `AssemblyComponentScanner` (euristica testuale, `docs/formato-asm.md`) solo se il file
non rispetta il formato validato (`InvalidDataException`). ⚠️ Limite noto non coperto dal non rispetta il formato validato (`InvalidDataException`). Due punti da non confondere,
fallback: un **assieme** con family table (raro, non solo le parti normalizzate possono entrambi imparati facendo fallire Creo (`docs/formato-asm-record.md` §7.3 e §7.4):
averla) può dare un risultato silenziosamente incompleto senza lanciare eccezione — vedi i **componenti soppressi** non sono nella distinta ma il loro file serve per aprire
`docs/formato-asm-record.md` §7. 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 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 Creo per l'esportazione STEP (Distributed Batch, manuale, fuori dall'app); "Verifica
completezza assiemi" controlla ricorsivamente sull'indice (non su `raw/`, stessa completezza assiemi" controlla ricorsivamente sull'indice (non su `raw/`, stessa
+33
View File
@@ -196,6 +196,39 @@ public class AsmRecordReaderTests
Assert.Single(membri); 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] [Fact]
public void Sezione_members_assente_lancia_eccezione() public void Sezione_members_assente_lancia_eccezione()
{ {
+30
View File
@@ -201,4 +201,34 @@ public static class AsmRecordReader
var membri = LeggiMembri(File.ReadAllBytes(percorsoAsm)); var membri = LeggiMembri(File.ReadAllBytes(percorsoAsm));
return new HashSet<string>(membri.Select(m => m.Generico), StringComparer.OrdinalIgnoreCase); 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; 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 /// <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 /// 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 /// 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 /// <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 /// (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 /// 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( private (List<(string Codice, string Tipo, string Percorso)> Trovati, List<string> NonTrovati) RisolviAlberoComponenti(
IEnumerable<string> asmIniziali, IProgress<(int Fatti, int Totale)>? progress = null) IEnumerable<string> asmIniziali, IProgress<(int Fatti, int Totale)>? progress = null)
{ {
var daScansionare = new Queue<string>(asmIniziali); // Ogni assieme viaggia con la cartella della share da cui proviene: e' li' che Creo
var codiciIniziali = daScansionare.Select(f => Path.GetFileName(f).Split('.')[0]); // 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>( var codiciVisti = new HashSet<string>(
codiciIniziali.Concat(codiciIniziali.Select(AssemblyComponentScanner.CodiceRadice)), codiciIniziali.Concat(codiciIniziali.Select(AssemblyComponentScanner.CodiceRadice)),
StringComparer.OrdinalIgnoreCase); StringComparer.OrdinalIgnoreCase);
var codiciSoppressiVisti = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var trovati = new List<(string Codice, string Tipo, string Percorso)>(); var trovati = new List<(string Codice, string Tipo, string Percorso)>();
var codiciNonTrovati = new List<string>(); var codiciNonTrovati = new List<string>();
if (daScansionare.Count == 0 || _dbPath is null) if (daScansionare.Count == 0 || _dbPath is null)
return (trovati, codiciNonTrovati); return (trovati, codiciNonTrovati);
var codiciNoti = CaricaCodiciNoti(_dbPath); var codiciNoti = CaricaCodiciNoti(_dbPath);
var candidati = CaricaCandidati(_dbPath);
int processati = 0; int processati = 0;
while (daScansionare.Count > 0) while (daScansionare.Count > 0)
{ {
var asm = daScansionare.Dequeue(); var (asm, cartellaOrigine) = daScansionare.Dequeue();
cartellaOrigine ??= CartellaDiOrigine(asm, candidati);
processati++; processati++;
foreach (var codice in EstraiCodiciReferenziati(asm, codiciNoti.Keys)) foreach (var codice in EstraiCodiciReferenziati(asm, codiciNoti.Keys))
{ {
if (!codiciVisti.Add(codice)) if (!codiciVisti.Add(codice))
continue; continue;
if (!codiciNoti.TryGetValue(codice, out var componenti)) if (!candidati.TryGetValue(codice, out var perCodice))
{ {
codiciNonTrovati.Add(codice); codiciNonTrovati.Add(codice);
continue; continue;
} }
foreach (var (tipo, percorso) in componenti) foreach (var (tipo, percorso) in ScegliPerCartella(perCodice, cartellaOrigine))
{ {
CopiaFileTrovati(new[] { percorso }, _ultimaCartellaRaw!); CopiaFileTrovati(new[] { percorso }, _ultimaCartellaRaw!);
trovati.Add((codice, tipo, percorso)); trovati.Add((codice, tipo, percorso));
if (tipo == "asm") 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)); 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_tipo ON files(tipo_file);
CREATE INDEX IF NOT EXISTS idx_files_cartella ON files(cartella_radice); 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, SELECT percorso_completo, percorso_relativo, nome_file, cartella_radice, famiglia,
codice, tipo_file, versione, dimensione_byte, data_modifica, scansionato_il codice, tipo_file, versione, dimensione_byte, data_modifica, scansionato_il
FROM ( FROM (
SELECT f.*, ROW_NUMBER() OVER ( SELECT f.*, ROW_NUMBER() OVER (
PARTITION BY cartella_radice, codice, tipo_file 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 ) AS rn
FROM files f FROM files f
) WHERE rn = 1; ) 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 | | 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 | | 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 | | 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) | | 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`, 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 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`. 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 ## Import distinta base
- **DistintaBaseImporter.cs** — converte l'export del gestionale (SpreadsheetML XML con - **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 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() `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. OVER (PARTITION BY ... ORDER BY versione DESC, data_modifica DESC, percorso_completo)`. Tutte
TrovaPercorsiCad`, `Form1.CaricaCodiciNoti`) interrogano questa vista, non `files` le ricerche di componenti (`Form1.TrovaPercorsiCad`, `Form1.CaricaCodiciNoti`) interrogano
direttamente, così un componente con più versioni sulla share risolve sempre all'ultima. questa vista, non `files` direttamente, così un componente con più versioni sulla share
Deliberatamente una `ROW_NUMBER()` invece di un self-join con `GROUP BY`: su ~200k righe un risolve sempre all'ultima. Deliberatamente una `ROW_NUMBER()` invece di un self-join con
self-join degenera in tempi quasi quadratici, `ROW_NUMBER()` resta lineare. `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 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 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 ciascuna — non una sola in assoluto. `TrovaPercorsiCad` gestisce questo caso deduplicando per
`tipo_file` lato C# (vedi sotto). `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` ## Come viene popolato: `Indexer.ScanAsync`
Chiamato da "Aggiorna indice" (fase 1) e riutilizza lo stesso `dbPath` già caricato. 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 `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 di `\xf8 04`). Nessun marcatore di stato da gestire: `CreoBom.cs` sul file risalvato
riproduce esattamente la BOM di riferimento senza modifiche al parser. 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 - **Family table di assiemi — rilevamento.** Non solo le parti normalizzate (§4) possono
essere family table: un intero **assieme** può esserlo. Osservato su `2546220101.asm` 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` (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 quale situazione divergerebbe, né se il codice dovrebbe fare qualcosa se succedesse (oggi
non la usa in nessun modo, legge solo `name`). 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 ## 8. Uso
```powershell ```powershell