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.
|
geparst und ihr Hash gegen die Vorschau geprüft.
|
||||||
|
|
||||||
Abnahme: fokussierte Tests, vollständige Tests und alle vorgeschriebenen
|
Abnahme: fokussierte Tests, vollständige Tests und alle vorgeschriebenen
|
||||||
Build-/Typecheck-Schritte. Für die exakte Abnahme mit 897 Objekt- und 211
|
Build-/Typecheck-Schritte. Die lokal bereitgestellte Referenz-CSV wird mit
|
||||||
Summenzeilen wird noch die anonymisierte Referenz-CSV benötigt; sie liegt nicht
|
`npm run revit:verify-reference` auf 897 Objektzeilen, 211 nichtleere
|
||||||
im Repository.
|
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
|
### 14.2 – Persistente Objekte und Erstimport
|
||||||
|
|
||||||
@@ -322,8 +324,9 @@ festgelegt.
|
|||||||
nicht durch indirekte oder nachträglich neu berechnete Payloads optimiert.
|
nicht durch indirekte oder nachträglich neu berechnete Payloads optimiert.
|
||||||
- **BMK-Drift:** Neue BMKs werden einmal bei der Planung berechnet und im
|
- **BMK-Drift:** Neue BMKs werden einmal bei der Planung berechnet und im
|
||||||
Command gespeichert; Import, Sortierung und Redo nummerieren nie implizit.
|
Command gespeichert; Import, Sortierung und Redo nummerieren nie implizit.
|
||||||
- **Unvollständige Referenzabnahme:** Die reale beziehungsweise anonymisierte
|
- **Vertrauliche Referenzdaten:** Der Referenzprüfer läuft lokal gegen die
|
||||||
Referenz-CSV muss vor Abschluss von 14.1 als lokale Testgrundlage vorliegen.
|
bereitgestellte CSV. Vor einer Aufnahme dieser Datei in Git ist gesondert zu
|
||||||
|
klären, ob sie ausreichend anonymisiert und zur Weitergabe freigegeben ist.
|
||||||
|
|
||||||
## Freigabepunkt
|
## Freigabepunkt
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -20,7 +20,8 @@
|
|||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
"db:migrate": "drizzle-kit migrate",
|
"db:migrate": "drizzle-kit migrate",
|
||||||
"db:backup": "tsx scripts/db-backup.ts",
|
"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": [],
|
"keywords": [],
|
||||||
"author": "",
|
"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",
|
"selectionMarker",
|
||||||
"circuitIdentifier",
|
"circuitIdentifier",
|
||||||
"power",
|
"power",
|
||||||
"quantity",
|
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export function assertExternalCsvConfiguration(
|
export function assertExternalCsvConfiguration(
|
||||||
@@ -56,6 +55,16 @@ export function assertExternalCsvConfiguration(
|
|||||||
}
|
}
|
||||||
mappedColumns.add(columnName);
|
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)) {
|
if (!Array.isArray(value.additionalSourceMappings)) {
|
||||||
throw new Error("External CSV additional source mappings must be an array.");
|
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.`);
|
throw new Error(`familyTypeRules.${index}.category is unsupported.`);
|
||||||
}
|
}
|
||||||
assertQuantityRule(rule.quantityRule, index);
|
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);
|
assertDisplayNameSuggestion(rule.displayNameSuggestion, index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export interface ExternalCsvColumnMapping {
|
|||||||
selectionMarker: string;
|
selectionMarker: string;
|
||||||
circuitIdentifier: string;
|
circuitIdentifier: string;
|
||||||
power: string;
|
power: string;
|
||||||
quantity: string;
|
quantity: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExternalCsvAdditionalSourceMapping {
|
export interface ExternalCsvAdditionalSourceMapping {
|
||||||
|
|||||||
@@ -59,7 +59,9 @@ export function parseExternalCsv(
|
|||||||
|
|
||||||
const headerRowIndex = headerMatches[0];
|
const headerRowIndex = headerMatches[0];
|
||||||
const configuredColumnNames = [
|
const configuredColumnNames = [
|
||||||
...Object.values(configuration.columns),
|
...Object.values(configuration.columns).filter(
|
||||||
|
(column): column is string => column !== null
|
||||||
|
),
|
||||||
...configuration.additionalSourceMappings.map((mapping) => mapping.sourceColumn),
|
...configuration.additionalSourceMappings.map((mapping) => mapping.sourceColumn),
|
||||||
];
|
];
|
||||||
const headerValues = parsed.rows[headerRowIndex].map((cell) => cell.value);
|
const headerValues = parsed.rows[headerRowIndex].map((cell) => cell.value);
|
||||||
@@ -81,7 +83,9 @@ export function parseExternalCsv(
|
|||||||
configuration.columns.familyAndType,
|
configuration.columns.familyAndType,
|
||||||
configuration.columns.selectionMarker,
|
configuration.columns.selectionMarker,
|
||||||
configuration.columns.power,
|
configuration.columns.power,
|
||||||
configuration.columns.quantity,
|
...(configuration.columns.quantity === null
|
||||||
|
? []
|
||||||
|
: [configuration.columns.quantity]),
|
||||||
...configuration.additionalSourceMappings.map((mapping) => mapping.sourceColumn),
|
...configuration.additionalSourceMappings.map((mapping) => mapping.sourceColumn),
|
||||||
].map((column) => headerLookup.get(column)!);
|
].map((column) => headerLookup.get(column)!);
|
||||||
const seenIfcGuids = new Set<string>();
|
const seenIfcGuids = new Set<string>();
|
||||||
@@ -274,7 +278,9 @@ function findHeaderRows(
|
|||||||
configuration: ExternalCsvConfiguration
|
configuration: ExternalCsvConfiguration
|
||||||
) {
|
) {
|
||||||
const requiredColumns = new Set([
|
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),
|
...configuration.additionalSourceMappings.map((mapping) => mapping.sourceColumn),
|
||||||
]);
|
]);
|
||||||
const matches: number[] = [];
|
const matches: number[] = [];
|
||||||
|
|||||||
@@ -103,6 +103,25 @@ describe("external CSV configuration", () => {
|
|||||||
});
|
});
|
||||||
assert.match(validateFailure(invalidQuantity), /positive finite number/);
|
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) {
|
function validateFailure(value: unknown) {
|
||||||
|
|||||||
@@ -51,6 +51,28 @@ describe("external CSV transport", () => {
|
|||||||
assert.deepEqual(serializeExternalCsv(document), source);
|
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", () => {
|
it("preserves embedded delimiters and line endings inside quoted cells", () => {
|
||||||
const csv = quotedRevitCsv.replace(
|
const csv = quotedRevitCsv.replace(
|
||||||
'"Steckdose: Doppelsteckdose"',
|
'"Steckdose: Doppelsteckdose"',
|
||||||
|
|||||||
Reference in New Issue
Block a user