leistungsbilanz-ts/docs/cable-sizing-module.md

9.3 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.

Coupling to the grid UI (the one fragile point)

Everything described above - the calculation, the audit table, applying a result via circuit.update - depends only on stable, explicitly documented domain fields (cableType/cableCrossSection/cableLength/ circuitTotalPower/voltage/protectionDevice.ratedCurrentA), the same ones AGENTS.md already treats as protected ("Protection and cable data belong to the circuit"). None of that breaks if the grid UI changes.

The one place that is coupled to a UI implementation detail: the click trigger in circuit-tree-editor.tsx matches grid column keys (cableSummary and cableCrossSection, defined in circuit-grid-model.ts) to decide where to show the calculator icon. Column visibility and order in this app are a per-browser user preference, not fixed - so this is checked against both known cable-related columns to reduce (not eliminate) the chance a user's personal column layout hides the trigger entirely. If a future column rename removes both keys, the failure mode is purely cosmetic: the calculator icon/click stops appearing on that cell, nothing crashes, no data is affected, and the rest of the app (including manual cable-field editing) is completely unaffected - the module's actual logic and persistence never depend on this column key.

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.