Verify Revit reference CSV
This commit is contained in:
@@ -253,9 +253,11 @@ keine Projektrevision und laufen über einen eigenen Audit-Store.
|
||||
geparst und ihr Hash gegen die Vorschau geprüft.
|
||||
|
||||
Abnahme: fokussierte Tests, vollständige Tests und alle vorgeschriebenen
|
||||
Build-/Typecheck-Schritte. Für die exakte Abnahme mit 897 Objekt- und 211
|
||||
Summenzeilen wird noch die anonymisierte Referenz-CSV benötigt; sie liegt nicht
|
||||
im Repository.
|
||||
Build-/Typecheck-Schritte. Die lokal bereitgestellte Referenz-CSV wird mit
|
||||
`npm run revit:verify-reference` auf 897 Objektzeilen, 211 nichtleere
|
||||
Passthrough-Summenzeilen und einen bytegleichen Round-trip geprüft. Da die
|
||||
Referenzdatei reale Modelldaten enthalten kann, wird sie nicht automatisch in
|
||||
einen Commit aufgenommen.
|
||||
|
||||
### 14.2 – Persistente Objekte und Erstimport
|
||||
|
||||
@@ -322,8 +324,9 @@ festgelegt.
|
||||
nicht durch indirekte oder nachträglich neu berechnete Payloads optimiert.
|
||||
- **BMK-Drift:** Neue BMKs werden einmal bei der Planung berechnet und im
|
||||
Command gespeichert; Import, Sortierung und Redo nummerieren nie implizit.
|
||||
- **Unvollständige Referenzabnahme:** Die reale beziehungsweise anonymisierte
|
||||
Referenz-CSV muss vor Abschluss von 14.1 als lokale Testgrundlage vorliegen.
|
||||
- **Vertrauliche Referenzdaten:** Der Referenzprüfer läuft lokal gegen die
|
||||
bereitgestellte CSV. Vor einer Aufnahme dieser Datei in Git ist gesondert zu
|
||||
klären, ob sie ausreichend anonymisiert und zur Weitergabe freigegeben ist.
|
||||
|
||||
## Freigabepunkt
|
||||
|
||||
|
||||
+2
-1
@@ -20,7 +20,8 @@
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "drizzle-kit migrate",
|
||||
"db:backup": "tsx scripts/db-backup.ts",
|
||||
"db:verify:circuit-schema": "node scripts/db-verify-circuit-schema.js"
|
||||
"db:verify:circuit-schema": "node scripts/db-verify-circuit-schema.js",
|
||||
"revit:verify-reference": "tsx scripts/verify-revit-reference-csv.ts"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { createHash } from "node:crypto";
|
||||
import { createDefaultExternalCsvConfiguration } from "../src/external-model/csv/external-csv-contracts.js";
|
||||
import {
|
||||
parseExternalCsv,
|
||||
serializeExternalCsv,
|
||||
} from "../src/external-model/csv/external-csv-transport.js";
|
||||
|
||||
const filePath = resolve(
|
||||
process.argv[2] ?? "docs/spec/ELT Stromkreisnummernvergabe-Check_DIV.csv"
|
||||
);
|
||||
const source = readFileSync(filePath);
|
||||
const configuration = createDefaultExternalCsvConfiguration({
|
||||
ifcGuid: "IfcGUID",
|
||||
roomNumber: "MEP-Raum: Nummer",
|
||||
roomName: "MEP-Raum: Name",
|
||||
familyAndType: "Familie und Typ",
|
||||
selectionMarker: "CAx_Auswahlkenner",
|
||||
circuitIdentifier: "kbp_Stromkreisnummer",
|
||||
power: "kbp-E-Elektrische Leistung",
|
||||
quantity: null,
|
||||
});
|
||||
configuration.additionalSourceMappings = [
|
||||
{ sourceColumn: "CAx_Anmerkung", targetField: "sourceRemark" },
|
||||
{ sourceColumn: "kbp-E-Spannung", targetField: "sourceVoltage" },
|
||||
{ sourceColumn: "kbp-E-Stromstärke", targetField: "sourceCurrent" },
|
||||
{ sourceColumn: "kbp-E-Versorgung von ELT", targetField: "sourceElectricalSupply" },
|
||||
{ sourceColumn: "kbp-E-Versorgung von MSR/GLT", targetField: "sourceControlSupply" },
|
||||
];
|
||||
|
||||
const document = parseExternalCsv(source, configuration);
|
||||
const counts = Object.fromEntries(
|
||||
["metadata", "header", "passthrough", "object", "suspect-object"].map(
|
||||
(classification) => [
|
||||
classification,
|
||||
document.rows.filter((row) => row.classification === classification).length,
|
||||
]
|
||||
)
|
||||
);
|
||||
const nonEmptyPassthroughRows = document.rows.filter(
|
||||
(row) =>
|
||||
row.classification === "passthrough" &&
|
||||
row.cells.some((cell) => cell.value !== "")
|
||||
).length;
|
||||
const serialized = serializeExternalCsv(document);
|
||||
|
||||
assert.equal(counts.object, 897, "Expected 897 object rows.");
|
||||
assert.equal(nonEmptyPassthroughRows, 211, "Expected 211 non-empty passthrough rows.");
|
||||
assert.equal(counts["suspect-object"], 0, "Expected no suspect object rows.");
|
||||
assert.equal(
|
||||
Buffer.compare(Buffer.from(serialized), source),
|
||||
0,
|
||||
"Expected a byte-identical CSV round-trip."
|
||||
);
|
||||
|
||||
const sha256 = createHash("sha256").update(source).digest("hex");
|
||||
process.stdout.write(
|
||||
`${JSON.stringify(
|
||||
{
|
||||
file: filePath,
|
||||
bytes: source.length,
|
||||
sha256,
|
||||
dialect: document.dialect,
|
||||
rows: counts,
|
||||
nonEmptyPassthroughRows,
|
||||
roundTripByteIdentical: true,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}\n`
|
||||
);
|
||||
@@ -19,7 +19,6 @@ const requiredColumnKeys = [
|
||||
"selectionMarker",
|
||||
"circuitIdentifier",
|
||||
"power",
|
||||
"quantity",
|
||||
] as const;
|
||||
|
||||
export function assertExternalCsvConfiguration(
|
||||
@@ -56,6 +55,16 @@ export function assertExternalCsvConfiguration(
|
||||
}
|
||||
mappedColumns.add(columnName);
|
||||
}
|
||||
if (value.columns.quantity !== null) {
|
||||
const quantityColumn = assertTrimmedString(
|
||||
value.columns.quantity,
|
||||
"columns.quantity"
|
||||
);
|
||||
if (mappedColumns.has(quantityColumn)) {
|
||||
throw new Error(`External CSV column is mapped more than once: ${quantityColumn}`);
|
||||
}
|
||||
mappedColumns.add(quantityColumn);
|
||||
}
|
||||
|
||||
if (!Array.isArray(value.additionalSourceMappings)) {
|
||||
throw new Error("External CSV additional source mappings must be an array.");
|
||||
@@ -115,6 +124,11 @@ export function assertExternalCsvConfiguration(
|
||||
throw new Error(`familyTypeRules.${index}.category is unsupported.`);
|
||||
}
|
||||
assertQuantityRule(rule.quantityRule, index);
|
||||
if (rule.quantityRule.kind === "mapped-column" && value.columns.quantity === null) {
|
||||
throw new Error(
|
||||
`familyTypeRules.${index}.quantityRule requires a configured quantity column.`
|
||||
);
|
||||
}
|
||||
assertDisplayNameSuggestion(rule.displayNameSuggestion, index);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export interface ExternalCsvColumnMapping {
|
||||
selectionMarker: string;
|
||||
circuitIdentifier: string;
|
||||
power: string;
|
||||
quantity: string;
|
||||
quantity: string | null;
|
||||
}
|
||||
|
||||
export interface ExternalCsvAdditionalSourceMapping {
|
||||
|
||||
@@ -59,7 +59,9 @@ export function parseExternalCsv(
|
||||
|
||||
const headerRowIndex = headerMatches[0];
|
||||
const configuredColumnNames = [
|
||||
...Object.values(configuration.columns),
|
||||
...Object.values(configuration.columns).filter(
|
||||
(column): column is string => column !== null
|
||||
),
|
||||
...configuration.additionalSourceMappings.map((mapping) => mapping.sourceColumn),
|
||||
];
|
||||
const headerValues = parsed.rows[headerRowIndex].map((cell) => cell.value);
|
||||
@@ -81,7 +83,9 @@ export function parseExternalCsv(
|
||||
configuration.columns.familyAndType,
|
||||
configuration.columns.selectionMarker,
|
||||
configuration.columns.power,
|
||||
configuration.columns.quantity,
|
||||
...(configuration.columns.quantity === null
|
||||
? []
|
||||
: [configuration.columns.quantity]),
|
||||
...configuration.additionalSourceMappings.map((mapping) => mapping.sourceColumn),
|
||||
].map((column) => headerLookup.get(column)!);
|
||||
const seenIfcGuids = new Set<string>();
|
||||
@@ -274,7 +278,9 @@ function findHeaderRows(
|
||||
configuration: ExternalCsvConfiguration
|
||||
) {
|
||||
const requiredColumns = new Set([
|
||||
...Object.values(configuration.columns),
|
||||
...Object.values(configuration.columns).filter(
|
||||
(column): column is string => column !== null
|
||||
),
|
||||
...configuration.additionalSourceMappings.map((mapping) => mapping.sourceColumn),
|
||||
]);
|
||||
const matches: number[] = [];
|
||||
|
||||
@@ -103,6 +103,25 @@ describe("external CSV configuration", () => {
|
||||
});
|
||||
assert.match(validateFailure(invalidQuantity), /positive finite number/);
|
||||
});
|
||||
|
||||
it("allows a missing quantity column only for fixed quantity rules", () => {
|
||||
const configuration = createDefaultExternalCsvConfiguration({
|
||||
...columns,
|
||||
quantity: null,
|
||||
});
|
||||
configuration.familyTypeRules.push({
|
||||
exactFamilyAndType: "Steckdose: Standard",
|
||||
internalDeviceType: "Steckdose",
|
||||
connectionKind: null,
|
||||
category: "single_phase",
|
||||
quantityRule: { kind: "fixed", quantity: 1 },
|
||||
displayNameSuggestion: null,
|
||||
});
|
||||
assert.equal(validateExternalCsvConfiguration(configuration).success, true);
|
||||
|
||||
configuration.familyTypeRules[0].quantityRule = { kind: "mapped-column" };
|
||||
assert.match(validateFailure(configuration), /requires a configured quantity column/);
|
||||
});
|
||||
});
|
||||
|
||||
function validateFailure(value: unknown) {
|
||||
|
||||
@@ -51,6 +51,28 @@ describe("external CSV transport", () => {
|
||||
assert.deepEqual(serializeExternalCsv(document), source);
|
||||
});
|
||||
|
||||
it("parses exports without an optional quantity column", () => {
|
||||
const configuration = {
|
||||
...externalCsvTestConfiguration,
|
||||
columns: {
|
||||
...externalCsvTestConfiguration.columns,
|
||||
quantity: null,
|
||||
},
|
||||
};
|
||||
const csv = [
|
||||
'"Projekt-Export";"";"";"";"";"";""',
|
||||
'"Stromkreis";"Raumnummer";"Raumname";"Familie und Typ";"CAx_Auswahlkenner";"Elektrische Leistung";"IfcGUID"',
|
||||
`"UV_AV_01-2F1";"01/101";"Technik";"Steckdose: Standard";"Arbeitsplatz";"100";"${firstIfcGuid}"`,
|
||||
].join("\r\n") + "\r\n";
|
||||
|
||||
const document = parseExternalCsv(utf8Bytes(csv, true), configuration);
|
||||
|
||||
assert.equal(
|
||||
document.rows.filter((row) => row.classification === "object").length,
|
||||
1
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves embedded delimiters and line endings inside quoted cells", () => {
|
||||
const csv = quotedRevitCsv.replace(
|
||||
'"Steckdose: Doppelsteckdose"',
|
||||
|
||||
Reference in New Issue
Block a user