# 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 `` alongside the existing ``. 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. ## Feature overview - **Calculate a recommendation**: laying method, insulation, conductor material, ambient temperature, grouping, cos phi and max voltage drop - same VDE 0298-4 reference-method model as the sibling Kabelliste tool (see Verification status below). - **Breaker-aware sizing**: if the circuit already has a protection device (`circuit.protectionDevice`), the cross-section is selected against `designCurrentA = max(operatingCurrentA, protectionDevice.ratedCurrentA)`, not the load current alone - the standard `In <= Iz` rule (a breaker only trips at its own rated current, so a cable sized for the actual load alone could overheat under sustained current below that threshold). If no cross-section can cover an oversized breaker, that is reported as the limiting factor by name, not a generic capacity error. - **Maximum length for the voltage-drop limit**: each cross-section option also reports the longest single run that still meets the requested `maxVoltageDropPercent` at the given load - the inverse of the voltage-drop check, shown for the recommended cross-section. - **Practical minimum for socket circuits**: `single_phase`-category circuits are raised to at least 2.5 mm² if the calculation alone would recommend less - see the dedicated note under Verification status. Never lowers a calculation that already needs more. - **Manual entry, always available**: the modal has a plain text Kabeltyp/ Querschnitt field. Running a calculation pre-fills it as a suggestion, but the field is the single source of truth for what gets applied, and stays freely editable - satisfying `docs/spec/06-future-sizing-and-calculations.md`'s explicit requirement that "users must remain able to manually override suggestions" without requiring a calculation to have been run first. A non-blocking warning appears if the typed value is not a standard cross-section, is smaller than the last calculation, or is below the practical minimum above - informational only, never forced, matching the same spec doc's "shown as a warning or status indicator, not as an automatic forced change." ## How a suggestion or manual entry is applied The module never writes to `circuits` directly. The modal's "Übernehmen" button calls the **existing, unmodified** frontend helper with whatever is currently in the Kabeltyp/Querschnitt fields, calculated or hand-typed: ```ts updateCircuitById(projectId, expectedRevision, circuitId, { cableCrossSection: "4 mm²", cableType: "NYM-J 3x2.5", 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 - nothing is ever written automatically - calculating only fills the modal's own fields, applying is always an explicit, separate click ## API contract `POST /api/cable-sizing/calculate` ```jsonc { "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 "circuitCategory": "single_phase", // optional, from the circuit's section.category - // enables the practical-minimum convention below "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. `PRACTICAL_MINIMUM_CROSS_SECTION_MM2` (2.5 mm² for `single_phase`) is **not** a verified norm value at all - it is a named planning convention, sourced directly from this project's own `docs/spec/06-future-sizing-and-calculations.md` ("Standard Single-Phase Circuits ... usually use ... cable cross-section: 2.5 mm²"), applied as a floor on top of the calculated recommendation. It intentionally only covers the one category and one convention that document already states; it is not a general substitute for norm-compliant calculation. ## 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 end-to-end on an internal test host at `http://192.168.0.133:3220` (own docker deployment, own subnet/ports to avoid clashing with ~50 other containers on that host - see the two commits marked "Local-only" in this branch, which are not part of this proposal and should not be carried over if it is ever proposed upstream). Exercised against seeded real project/circuit data created through the actual command API (not direct DB writes), including a deliberately long (85 m) circuit to trigger the voltage-drop-critical path. `npm test`, `npm run build:api`, `npm run build:web` and `npm run typecheck:scripts` all pass with this module included at every commit in this branch. This branch (`feature/cable-sizing-module`) is pushed to a mirror of this repository under this homelab's own Forgejo instance, not to `git.jappel.io` - no write access to the upstream repository was available or used. See the branch's commit history for the incremental history of this module, including fixes made during manual testing (trigger column, icon rendering, breaker-aware sizing).