leistungsbilanz-ts/docs/cable-sizing-module.md
Grovy311 1d030971dd 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.
2026-08-07 17:13:59 +02:00

8 KiB

Cable Sizing Module (Proposal)

Status: proposal, not yet reviewed or merged by the project owner. This document and the accompanying src/cable-sizing/, src/db/schema/cable-sizing-calculations.ts, src/server/{controllers,routes}/cable-sizing.* and src/frontend/components/cable-sizing-* files were written by a third party (see git history/authorship) against the project's own stated direction in docs/spec/06-future-sizing-and-calculations.md ("The app should later support rule-based protection and cable sizing") and AGENTS.md ("... and later electrical sizing logic"). Nothing here has been pushed to the project's own repository; it exists as a local branch for review, testing and discussion.

Why a separate module instead of extending core domain code

AGENTS.md is explicit that critical multi-write commands, the revision/undo system and the Circuit-First domain model are the supported architecture, and that changes should be small and reviewable. A cable-sizing calculation is not a project mutation - it doesn't need expectedRevision, doesn't belong in the undo/redo stack, and shouldn't grow the circuit.update command's switch statement or the DTOs it doesn't already have.

So this module is built exactly like the existing src/external-model/ adapter (the Revit/CSV import foundation): a one-way dependency boundary.

src/cable-sizing/domain/     <- pure functions and types, zero imports from
                                 db/, server/ or frontend/
src/db/schema/                <- one new, fully additive table
src/db/repositories/          <- one plain repository (no revision semantics)
src/server/{controllers,routes}/cable-sizing.*  <- one new, isolated route
                                 group, mounted with a single app.use() line
src/frontend/components/cable-sizing-*          <- one new modal + one small
                                 API client, following the existing
                                 FormModal / CircuitProtectionModal pattern

Nothing outside these files imports from them except the two required one-line hooks described below. If this module is rejected or needs to move out again, removing it is a matter of deleting these files, the two one-line hooks, and running a DROP TABLE migration - it never touches circuits, project_revisions or any command/history table.

The two required hooks into existing code

  1. src/server/index.ts: one import + one app.use("/api/cable-sizing", cableSizingRouter) line, next to the existing app.use("/api/projects", ...) etc.
  2. src/frontend/components/circuit-tree-editor.tsx: one new useState for the open modal, one new trigger condition (mirroring the existing isProtectionTrigger / protectionEditorCircuit pattern almost exactly), and one new conditionally-rendered <CableSizingModal /> alongside the existing <CircuitProtectionModal />. See the diff for the exact lines.

No changes to circuit-project-command.model.ts, project-command.service.ts, any Zod command schema, any migration for circuits/project_revisions, or any existing test.

How a suggestion is applied

The module never writes to circuits directly. The modal's "Empfehlung übernehmen" button calls the existing, unmodified frontend helper:

updateCircuitById(projectId, expectedRevision, circuitId, {
  cableCrossSection: "4 mm²",
  cableLength: 30,
});

which is the same circuit.update command the grid already uses for manual cell edits. This means:

  • optimistic concurrency (expectedRevision) is respected automatically
  • the change appears in the project's revision history and is undoable/ redoable exactly like a manual edit
  • manually typing a value into the cableCrossSection cell keeps working unchanged - the module only adds a small calculator trigger next to the cell, per docs/spec/06-future-sizing-and-calculations.md's explicit requirement that "users must remain able to manually override suggestions"

API contract

POST /api/cable-sizing/calculate

{
  "phase": 1,                    // 1 | 3
  "mode": "power",                // "power" | "current"
  "powerKw": 4.2,                  // circuit.circuitTotalPower, when mode="power"
  "cosPhi": 1,
  "voltage": 230,                  // circuit.voltage (already project-derived, read-only)
  "lengthM": 23.5,                 // circuit.cableLength
  "layingMethod": "C",             // DIN VDE 0298-4 reference method, A1|A2|B1|B2|C|D1|D2|E|F|G
  "conductorMaterial": "copper",   // "copper" | "aluminum"
  "insulation": "pvc",             // "pvc" | "xlpe"
  "ambientTemperatureC": 30,
  "groupingCircuits": 1,
  "maxVoltageDropPercent": 3,
  "harmonicNeutralLoad": "none",   // "none" | "15to33Percent" | "over33Percent", three-phase only
  "existingProtectionRatedCurrentA": 16,  // optional, from circuit.protectionDevice
  "context": { "projectId": "...", "circuitId": "...", "equipmentIdentifier": "-1F1.1" }
}

Response: { calculationId, result: CableSizingResult, alerts: CableSizingAlert[] } (see src/cable-sizing/domain/cable-sizing-calculation.ts for the exact shape). GET /api/cable-sizing/laying-methods and .../insulation-materials expose the pick-list metadata (including which combinations are verified) so a future non-modal UI could build its own form without hard-coding the enum.

Verification status - important

The reference current-carrying-capacity tables are ported from a sibling project's Kabelliste module, which was itself only verified against ~8 public sources for six of the ten DIN VDE 0298-4 reference laying methods (A1, B2, C, E, D1, D2) and PVC insulation only. The other four methods (A2, B1, F, G) and XLPE/VPE insulation could not be verified without contradiction across sources during that earlier work, so calculateCableSizing deliberately returns dataVerified: false and no numeric recommendation for those combinations, rather than a guessed value. The UI surfaces this as a plain critical alert. Anyone with access to the actual norm text can extend LAYING_METHOD_VERIFIED and CURRENT_CAPACITY_A in src/cable-sizing/domain/cable-sizing-calculation.ts once the missing tables are confirmed.

The protection-coordination check (existingProtectionRatedCurrentA vs. the recommended cross-section's corrected capacity) is a simplified In <= Iz check only - it is not a full IEC 60364-4-43 overload (I2 <= 1.45 x Iz) or short-circuit withstand check.

Relationship to the project's own future-sizing spec

docs/spec/06-future-sizing-and-calculations.md separately describes simple category-based defaults (e.g. lighting circuits -> 10 A / 1.5 mm²) as "common planning defaults, not a replacement for full norm-compliant calculation". This module is the latter: an on-demand, norm-referenced calculation for one circuit at a time, not a bulk default-filling tool. The two are complementary and could later be wired together (e.g. the category defaults pre-fill this module's laying-method/insulation fields), but that integration is out of scope here.

The spec's isPublicBuilding halogen-free rule and the Control Requirement (DALI/KNX/core-count) field are not implemented by this module - both concern cable type selection, not cross-section sizing, and the latter isn't in the current schema yet. They're natural follow-ups once this module is reviewed.

Maintenance plan

The src/cable-sizing/domain/ folder has zero imports from the rest of the app by design (see the dependency-direction note above), so it can be extracted into an independently versioned/published package later (e.g. a private npm package or a git subtree) without touching anything outside the five files/folders listed above - "separately maintained but part of the app for now", per the intent of this proposal.

Local testing

Deployed and manually exercised on an internal test host at http://192.168.0.133:3220 (a fork of this repository's main branch, not pushed anywhere). npm test, npm run build:api, npm run build:web and npm run typecheck:scripts all pass with this module included; see the accompanying diff/branch for the exact commit.