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:
1 parent
0ae2a5e7cf
commit
54f267b508
8 files changed
+316
-18
No files matched your search
@@ -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
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user