Add proposed cable-sizing module

Isolated module (mirrors src/external-model/ dependency direction) that
adds an on-demand, DIN VDE 0298-4 referenced cable cross-section
calculator: a click-on-a-circuit input mask for laying method,
insulation, ambient temperature, grouping and voltage-drop limit that
suggests a cross-section for the circuit cableCrossSection/cableLength
fields. Applying a suggestion goes through the existing circuit.update
command (expectedRevision, undo/redo) unchanged - nothing here writes
to circuits directly or touches the revision/command system. See
docs/cable-sizing-module.md for the full rationale, API contract,
verification status and maintenance plan. Proposal, not yet reviewed
by the project owner - see the docs file.

423 existing + 11 new tests pass, build:api/build:web/typecheck:scripts
clean.
This commit is contained in:
Grovy311 2026-08-07 17:13:59 +02:00
parent a17e2e3f4b
commit 1d030971dd
17 changed files with 4179 additions and 2 deletions

View file

@ -0,0 +1,152 @@
import path from "node:path";
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { eq } from "drizzle-orm";
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
import { createDatabaseContext, type DatabaseContext } from "../src/db/database-context.js";
import { CableSizingCalculationRepository } from "../src/db/repositories/cable-sizing-calculation.repository.js";
import { circuitLists } from "../src/db/schema/circuit-lists.js";
import { circuitSections } from "../src/db/schema/circuit-sections.js";
import { circuits } from "../src/db/schema/circuits.js";
import { projects } from "../src/db/schema/projects.js";
import { DistributionBoardFixtureRepository } from "./support/distribution-board-fixture.js";
function createRepository() {
const context = createDatabaseContext(":memory:");
migrate(context.db, { migrationsFolder: path.resolve("src", "db", "migrations") });
return new CableSizingCalculationRepository(context.db);
}
function createRepositoryWithCircuits(): {
repository: CableSizingCalculationRepository;
context: DatabaseContext;
circuitOneId: string;
circuitTwoId: string;
} {
const context = createDatabaseContext(":memory:");
migrate(context.db, { migrationsFolder: path.resolve("src", "db", "migrations") });
context.db.insert(projects).values({ id: "project-1", name: "Test project" }).run();
const board = new DistributionBoardFixtureRepository(context.db).createWithCircuitListAndDefaultSections(
"project-1",
"UV-01"
);
const circuitList = context.db
.select()
.from(circuitLists)
.where(eq(circuitLists.distributionBoardId, board.id))
.get();
if (!circuitList) {
throw new Error("fixture did not create a circuit list");
}
const section = context.db
.select()
.from(circuitSections)
.where(eq(circuitSections.circuitListId, circuitList.id))
.get();
if (!section) {
throw new Error("fixture did not create a section");
}
const circuitOneId = "circuit-1";
const circuitTwoId = "circuit-2";
context.db
.insert(circuits)
.values([
{
id: circuitOneId,
circuitListId: section.circuitListId,
sectionId: section.id,
equipmentIdentifier: "-1F1.1",
},
{
id: circuitTwoId,
circuitListId: section.circuitListId,
sectionId: section.id,
equipmentIdentifier: "-1F1.2",
},
])
.run();
return {
repository: new CableSizingCalculationRepository(context.db),
context,
circuitOneId,
circuitTwoId,
};
}
describe("CableSizingCalculationRepository", () => {
it("creates an entry with input/result JSON and defaults appliedToCircuit to false", () => {
const repository = createRepository();
const entry = repository.create({
id: "calc-1",
projectId: null,
circuitId: null,
equipmentIdentifier: "-1F1.1",
input: { layingMethod: "C" },
result: { recommendedCrossSectionMm2: 4 },
appliedToCircuit: 0,
});
assert.equal(entry.id, "calc-1");
assert.equal(entry.appliedToCircuit, 0);
assert.deepEqual(entry.input, { layingMethod: "C" });
assert.deepEqual(entry.result, { recommendedCrossSectionMm2: 4 });
});
it("lists entries by circuitId, most recent first", () => {
// createdAt defaults to SQLite's unixepoch() (second resolution), so two
// inserts in the same test can tie - pass explicit, distinct timestamps
// instead of racing the clock. circuitId has a real foreign key into
// circuits (foreign_keys = ON), so this needs actual circuit rows, not
// arbitrary strings.
const { repository, circuitOneId, circuitTwoId } = createRepositoryWithCircuits();
repository.create({
id: "calc-a",
projectId: null,
circuitId: circuitOneId,
equipmentIdentifier: null,
input: {},
result: {},
appliedToCircuit: 0,
createdAt: new Date(1000),
});
repository.create({
id: "calc-b",
projectId: null,
circuitId: circuitOneId,
equipmentIdentifier: null,
input: {},
result: {},
appliedToCircuit: 0,
createdAt: new Date(2000),
});
repository.create({
id: "calc-other-circuit",
projectId: null,
circuitId: circuitTwoId,
equipmentIdentifier: null,
input: {},
result: {},
appliedToCircuit: 0,
createdAt: new Date(3000),
});
const rows = repository.listByCircuit(circuitOneId);
assert.equal(rows.length, 2);
assert.equal(rows[0].id, "calc-b");
assert.equal(rows[1].id, "calc-a");
});
it("marks an entry as applied", () => {
const repository = createRepository();
repository.create({
id: "calc-1",
projectId: null,
circuitId: null,
equipmentIdentifier: null,
input: {},
result: {},
appliedToCircuit: 0,
});
const updated = repository.markApplied("calc-1");
assert.equal(updated?.appliedToCircuit, 1);
});
});