Files

26 KiB

AGENTS.md

Project Goal

Maintain and extend a spreadsheet-like electrical distribution board circuit list editor.

The editor is used for electrical planning in execution design.

It must support circuits, device rows, project devices, drag-and-drop restructuring, stable equipment identifiers and later electrical sizing logic.

Current Supported Architecture

  • Frontend: Next.js App Router under src/app with reusable editor modules under src/frontend.
  • API: Express composition starts in src/server/index.ts.
  • Domain rules: src/domain.
  • Persistence: SQLite/Drizzle schemas, repositories and migrations under src/db.
  • Primary editor route: src/app/projects/[projectId]/circuit-lists/[circuitListId]/tree-edit/page.tsx.
  • Primary editor component: src/frontend/components/circuit-tree-editor.tsx.
  • Grid ownership, projection, insertion and safety rules live in the pure src/frontend/components/circuit-grid-*.ts modules.
  • Critical multi-write commands use injected transaction repositories with real SQLite commit/rollback tests.
  • All supported runtime project-command stores, including full snapshot restoration, share src/db/repositories/project-command-transaction.persistence.ts for the atomic domain-write, revision and history transition boundary. Its applied forward-command variant preserves derived CircuitDeviceRow override metadata.
  • Low-level appendProjectRevision persistence is adapter-internal and tested directly; do not reintroduce a standalone runtime revision repository.
  • Runtime domain services receive narrow reader/store dependencies explicitly; concrete SQLite repositories are instantiated only under src/server/composition.
  • General application repositories also require an explicit AppDatabase; src/server/composition/application-repositories.ts owns their runtime instances, and controllers never import the global SQLite client.
  • General Circuit, CircuitDeviceRow, CircuitList and DistributionBoard repositories expose only active reads. Runtime writes belong in typed command repositories; direct integration fixtures belong under tests/support.

The supported runtime model is Circuit-First. The former Consumer UI, API, tables and upgrade tooling are removed; do not reintroduce them.

See docs/current-architecture.md for the complete module and request flow.

Critical Domain Rules

  • A circuit is not the same thing as one device row.
  • A circuit contains zero, one or multiple device rows.
  • A single-device circuit may be displayed as one compact row.
  • A multi-device circuit must be displayed with a circuit summary row and indented device rows.
  • Equipment identifiers belong to circuits, not to every device row.
  • Device rows inside a circuit do not have their own equipment identifiers.
  • Protection and cable data belong to the circuit, not to individual devices.
  • Device-level values include quantity, power per unit, simultaneity factor, cosPhi, room data, cost group and category.
  • Circuit total power is the sum of all device rows in that circuit.
  • Existing equipment identifiers must never be changed automatically.
  • Renumbering is always an explicit user action.

Existing Codebase Rule

There is an existing codebase.

Do not blindly continue old assumptions.

Do not blindly delete the codebase.

First audit current code against the specification.

Keep working parts if they match the target model.

Refactor or replace parts that conflict with the target model.

Before large changes, summarize:

  • what is kept
  • what is changed
  • what is removed
  • why

Implementation Discipline

Work in phases.

Do not implement unrelated future features while working on a phase.

Before coding, briefly state:

  • files to change
  • data model impact
  • UI impact
  • risks

Prefer small, reviewable changes.

Do not rewrite the whole app unless the current architecture blocks the required domain model.

Naming

Use English names in code.

Use clear domain names:

  • ProjectDevice
  • DistributionBoard
  • CircuitSection
  • Circuit
  • CircuitDeviceRow
  • equipmentIdentifier
  • displayName
  • phaseType
  • quantity
  • powerPerUnit
  • simultaneityFactor
  • cosPhi
  • rowTotalPower
  • circuitTotalPower

Avoid ambiguous names like:

  • item
  • thing
  • entry
  • rowData

Use them only for local UI variables where the context is obvious.

Numbering Rules

Default sections:

  • Lighting: prefix -1F
  • Single-phase circuits: prefix -2F
  • Three-phase circuits: prefix -3F

New circuit identifier:

  • highest existing number in the section + 1

Do not fill gaps automatically.

Do not renumber after insert, delete, move, sort or drag-and-drop.

Renumber only when the user explicitly triggers "Renumber section".

UI Rules

The circuit list table should behave like a spreadsheet.

User-facing frontend text must be German unless a domain-standard technical term is intentionally retained.

Cells show static text by default.

Inline edit starts by:

  • double click
  • Enter
  • typing
  • F2

Keyboard behavior:

  • Enter confirms
  • Escape cancels
  • Tab / Shift+Tab navigates editable cells
  • Arrow keys navigate cells when not editing

Support Ctrl+Plus and Ctrl+Shift+Plus for insertion.

Bootstrap / Styling Rule

Bootstrap may be used for the general application UI, such as navigation, page layout, buttons, forms, cards, alerts and modals.

Do not use Bootstrap as the core table/grid framework for the circuit list editor.

The circuit list editor must remain a custom spreadsheet-like component with controlled cell selection, inline edit mode, row grouping, drag-and-drop indicators and keyboard behavior.

Avoid permanent Bootstrap form controls inside table cells. Table cells should show static text by default and switch to inputs only while editing.

Drag-and-Drop Rules

Dragging from the circuit identifier / circuit handle moves the whole circuit.

Dragging from the device area moves device rows.

Project devices can be dragged from a sidebar into the circuit list.

Drop onto a free placeholder creates a new circuit.

Drop onto an existing circuit adds the device to that circuit.

Moving a device recalculates affected circuit totals.

Moving rows never renumbers circuits automatically.

Show clear visual drop indicators.

Reject invalid drop targets or require confirmation.

Sorting and Filtering

Filtering should work through column headers.

Sorting should work through column headers.

Sorting moves complete circuits as blocks.

Sorting must not split device rows away from their circuit.

Sorting does not renumber.

The user may explicitly renumber after sorting.

Linked Project Devices

Circuit device rows may be linked to project devices.

displayName is copied on insert but not synchronized automatically.

When a project device changes, show affected linked rows and let the user choose which fields to sync.

Never silently overwrite local changes.

Allow disconnecting linked rows from project devices.

Manual Rows

Manual rows are allowed and common.

Manual rows can later be saved as project devices.

After saving, the row becomes linked to the new project device.

Undo / Redo

The editor reads undo/redo eligibility from the project-wide server history on initial load and after every tree reload. Undo/redo therefore remains available after a page refresh or application restart. All currently supported Circuit and CircuitDeviceRow writes execute persistent project commands. Applying a sorted view across multiple sections is one atomic circuit.reorder-sections command and one undo step. Explicit renumbering uses the collision-safe circuit.renumber-section command and is never triggered implicitly. Immutable revision metadata is available through the paginated GET /api/projects/:projectId/history/revisions endpoint; it does not expose stored command payloads. Named logical snapshots can be created and listed through project-scoped API endpoints. Their schema-versioned payload contains the complete supported project runtime state and a SHA-256, excludes global data outside the project and does not change the project revision or undo/redo stacks. Restoring a server-stored snapshot verifies its checksum and the current-state hash, replaces supported project data atomically and records a new restore revision with a complete inverse command. Restore can therefore be undone and redone after a restart. Current snapshot schema version 5 additionally stores the manual quantity share of every CircuitDeviceRow. Version 4 snapshots are upgraded with manualQuantity = quantity; they already contain the complete confirmed external-model source, import batches, room mappings and objects. Version 3 snapshots are upgraded with an empty external-model state; version 2 snapshots are additionally upgraded with an empty external CSV configuration. Supported baseline version 1 snapshots are additionally upgraded with isPublicBuilding = false; pre-baseline snapshots remain unsupported. All supported versions contain circuit-group identity, distribution-board components and the separate circuit/component protection records. Portable duplicate imports remap component ids and protection-owner references together with the existing project graph. Project-wide Revit CSV configuration uses external-csv-configuration.update. Its complete expected/target snapshot is persisted through the shared project-command transaction, so create, update, delete, Undo and Redo are revision-safe. Snapshot schema 5, restore and portable project duplication preserve the configuration; duplication remaps its internal id and project link. The project page exposes the Revit CSV modal. It edits the complete transport/column/family-rule configuration through the revision-safe endpoint, requests a stateless preview and then shows the Phase 14.2 initial-import decisions. Preview responses include all mapped objects, while the UI renders only the first 25. No preview or plan creates external objects, rows, revisions or server-side drafts. Phase 14.2 migration 0003 provides one implicit Revit CSV source per project, immutable import batches with original bytes plus the classified cell matrix, normalized source-room mappings and external objects unique by (sourceId, ifcGuid). Source values, planning values and overrides are separate. The general external-model repository is read-only; runtime writes belong only to typed command adapters. Snapshot/restore and portable project transfer schema version 5 capture the complete external state and remap every internal UUID and link while preserving IFCGUIDs, source values, original bytes and the classified matrix. Import batches additionally store the monotonic project CSV configuration version introduced by additive migration 0004; it is distinct from the CSV configuration JSON schema version. Phase 14.3 migration 0005 adds CircuitDeviceRow.manualQuantity, backfills existing rows from quantity and keeps quantity as the materialized total. Normal manual row inserts and quantity edits update both values together. external-object.update-row-assignment atomically assigns external objects to existing rows, moves them between existing rows or detaches them. It preserves the manual quantity, validates the materialized total against every target link, rejects project/board/category/room/selection-marker mixing and requires explicitly listed confirmations for other planning-value conflicts. Its exact inverse supports persistent Undo/Redo, and detach never deletes the row. external-object.assign-to-new-row atomically inserts unassigned external objects as one external-only row (manualQuantity = 0) into an existing circuit. Its history-only inverse removes the row only while its complete snapshot and exact object-link set remain unchanged. Ordinary detach continues to preserve the row. external-object.assign-to-new-circuit atomically inserts a preplanned circuit with stable BMK/UUIDs, derived voltage, one external-only row, required default protection and its external object links. Its history-only inverse requires the complete unchanged circuit snapshot and exact link set. Redo never recalculates the BMK. All three external assignment stores share external-object-assignment.persistence.ts for compatibility and link safety. The read-only GET /api/projects/:projectId/circuit-lists/:circuitListId/external-objects projection resolves the board through the owned CircuitList and returns only that board's compact external objects, unassigned first. It exposes no original CSV bytes or command payloads and returns an empty list before optional import. Confirmed initial state is written only through external-import.apply-initial. The command rechecks configuration version, original-byte SHA-256, parsed matrix, complete IFCGUID/source values, explicit planning overrides and internal project links in the shared transaction. It never creates or links a CircuitDeviceRow. Its exact inverse removes the whole unchanged external state, and Redo restores the same UUIDs and bytes. POST /api/projects/:projectId/external-csv/initial-import/plan is a stateless read path for the first wizard stage. It rejects projects with an existing external source and returns source-room groups, exact room-number suggestions, exact family/type groups, projected object values, issue counts and the current floor/room/board/ProjectDevice catalogs. It creates no draft, revision or domain row. The wide modal renders these groups and catalogs as explicit decisions and identifies exact, ambiguous and new room matches. Missing rooms and ProjectDevices are created through the reused project forms with mapped Revit values as visible defaults. Each catalog creation remains its own project command; the wizard adds and selects the result without discarding its plan. The dedicated POST /api/projects/:projectId/external-csv/initial-import/apply endpoint retransmits and replans the file against the expected hash, configuration version and project revision. It requires one explicit decision per source-room and exact family/type group, blocks unclassified families, creates all stable external UUIDs server-side and invokes external-import.apply-initial. Room/default-board and optional ProjectDevice links are supported; CSV circuit values remain source-only and every circuitDeviceRowId remains null. The UI blocks unclassified families, requires a final confirmation and applies the whole import as one persistent Undo/Redo step. The central revision boundary creates an automatic logical snapshot after each 25 new revisions and retains only the newest 12 automatic snapshots per project. Named snapshots are never removed by this retention policy. The project page exposes persistent project-wide Undo/Redo in the header of an initially collapsed German snapshot/timeline UI, with explicit restore confirmation and cursor-based loading of older revision metadata. Insertions and generated move targets use client-generated stable UUIDs, and undo restores the same ids from complete server snapshots. The tree response supplies the optimistic currentRevision; obsolete direct field PATCH, structure POST, move, reorder, renumber, identifier-restore, Circuit and CircuitDeviceRow DELETE routes are removed. CircuitDeviceRow moves between existing circuits and moves that create one new placeholder target circuit are persisted. The latter stores the complete empty target snapshot so undo can restore the rows and remove only the unchanged generated circuit. Complete in-section Circuit reorders are persisted separately and change sort positions without changing equipment identifiers. Explicit complete-section renumbering is persisted through a separate collision-safe command and is never triggered by sorting or moving. Project-device synchronization, disconnect and reconnect are persisted as one atomic multi-row command with complete expected/target row snapshots, including link and override metadata. Canonical ProjectDevice field updates are also persisted and never synchronize linked rows implicitly. ProjectDevice insertion/deletion preserves stable device ids. Deletion captures complete disconnected snapshots of linked rows so undo can restore only rows that have remained unchanged. ProjectDevice create, update, delete and global-to-project copy API/UI paths use these persistent commands and track the returned project revision. ProjectDevice synchronization/disconnect API and UI paths do the same; their undo action uses the project-wide history endpoint. Project settings use the persistent project.update-settings command. Project metadata, the public-building classification, both voltage defaults and the enabled distribution-board supply types change in one revision and Undo/Redo restores them together; the project PUT route requires expectedRevision. The public-building flag is the future sizing input for requiring halogen-free cables and lines. The system catalog is AV, SV, EV, USV, MSR, SiBe; at least one must be enabled and a type used by a board cannot be disabled. Distribution-board setup uses distribution-board.insert schema version 1 with a complete stable snapshot of the board, circuit list, three default groups, main switch -Q0 and surge protective device -FA. Its inverse removes only the same unchanged and still-empty structure; the POST route requires expectedRevision and returns the updated history state. Complete populated distribution-board copying and deletion use distribution-board.insert-subtree and distribution-board.delete-subtree. Their exact snapshots include the board, circuit list, groups, circuits, device rows, components and all one-to-one protection data. Copy remaps all owning ids but preserves project-device and room links. Both operations are atomic persistent commands with restart-safe Undo/Redo; deletion requires an explicit UI warning. Mutable group-protection and auxiliary distribution-board components use distribution-board-component.insert and distribution-board-component.delete; edits and footer reordering use distribution-board-component.update. Their complete expected/target snapshots include the optional one-to-one protection-device state and reject stale data. Updates preserve id, ownership, role and placement. Fixed main-switch and surge-protection header roles are excluded from these general component commands. Empty circuit-group creation, display-name updates and deletion use circuit-group.insert, circuit-group.update and circuit-group.delete. Their snapshots keep category, positive group number and derived prefix consistent. General deletion rejects groups containing circuits or group components; populated deletion remains a separate confirmed command. circuit-group.reorder requires a complete expected/target sort assignment for every group in one circuit list. It changes only sort positions and never renumbers groups, prefixes or circuits. Explicit group-number changes use circuit-group.renumber. The command carries the complete expected/target group, circuit-BMK and optional group-component BMK plan, applies swaps through collision-safe temporary values and preserves circuit suffixes. Undo/Redo uses the exact inverse plan. Cross-group circuit moves use circuit.move-group and are limited to distinct groups of the same category. The stored target BMK is highest target suffix plus one at planning time and is never recalculated for Redo. Only circuit group, BMK and sort order change; device rows and circuit protection retain the stable circuit id. Confirmed populated-group deletion uses circuit-group.delete-subtree/circuit-group.restore-subtree. The exact snapshot contains the group, optional component protections, complete circuits, circuit protections and all device-row link/override metadata. Delete re-captures and compares the subtree before cascading; restore preserves every UUID in foreign-key-safe order. The circuit-tree read model exposes fixed headerComponents, group-owned components, footerComponents, optional group category/number and optional one-to-one protection DTOs for circuits and group components. These are the only supported circuit-protection source; no flat compatibility fields remain. Distribution-board floor assignment and a project-enabled supply type use distribution-board.update; both values are snapshot/export fields and one persistent undo step. Floor and room setup uses persistent insert, update and delete commands with complete stable snapshots. Their inverses remove or restore only unchanged records and reject floors with assigned rooms/distribution boards or rooms referenced by device rows. All write routes require expectedRevision and return the updated history state.

Required operations:

  • insert circuit
  • insert device
  • delete circuit
  • delete device
  • move circuit
  • move device
  • multi-row move
  • renumber section
  • edit cell value
  • edit equipment identifier
  • synchronization changes

Revit / CSV External Model Work

When explicitly working on Phase 14, use docs/spec/revit-csv-integration-requirements.md as the detailed source of truth together with the current architecture documents.

Critical rules:

  • Revit integration is optional. Do not make imported external data mandatory for projects, rooms, project devices, circuits or device rows.
  • Parse imports into staging first. Never write CSV rows directly into circuits or device rows.
  • Keep external object identity separate from internal UUIDs and circuit BMKs. IfcGUID identifies the external Revit object and must never replace an internal UUID or equipmentIdentifier.
  • One imported Revit object may exist without a CircuitDeviceRow. Import, room assignment, distribution-board assignment, classification and ProjectDevice linking do not create a row.
  • Create or change a CircuitDeviceRow only after an explicit user action in the circuit-list editor.
  • Several external objects may link to one CircuitDeviceRow, while every external object remains individually traceable for follow-up imports and object-specific export.
  • Multi-socket Revit objects are indivisible. Their effective quantity may be greater than one, but one external object must never be split across circuits.
  • Existing ProjectDevices are canonical project templates and must not be overwritten from Revit values. When creating a new ProjectDevice, mapped Revit values may prefill the creation form.
  • Store imported source values separately from local planning values and local overrides. Imports never silently overwrite local values.
  • Repeated imports match deterministically by project/source and IfcGUID. New, changed, missing and conflicting objects require preview and explicit user decisions.
  • The first implementation assumes one complete Revit model export per project. Do not add visible multi-source, linked-model or partial-import management.
  • CSV configuration is project-scoped. Preserve title, header, blank, subtotal and unknown rows and columns for the return export.
  • Treat rows without IfcGUID as passthrough when they contain no object data. Do not report Revit subtotal rows as missing-identity objects.
  • Apply each confirmed import diff through a typed project command with expectedRevision, complete inverse data and the shared project command transaction boundary.
  • External-model state must be added to logical project snapshots and portable project transfer when it becomes part of the supported runtime.
  • Keep CSV parsing/serialization in a transport adapter. Domain services must not depend on CSV syntax, React, better-sqlite3 or concrete repositories.
  • Do not implement automatic circuit planning, Dynamo/API exchange, PostgreSQL, background jobs or permissions as part of the initial CSV phase.

Before implementation, audit the current ProjectDevice, CircuitDeviceRow, command, snapshot, transfer and editor drag-and-drop paths. Propose concrete domain objects and commands before adding migrations.

Future Sizing

Do not implement full cable/protection sizing unless explicitly requested.

Keep the structure ready for it.

Future sizing will need:

  • circuit total power
  • phase type
  • voltage
  • cosPhi
  • cable length
  • cable type
  • cable cross-section
  • protection rated current
  • control requirement such as DALI

Users must be able to override sizing suggestions.

Persistence and Migration Rules

  • SQLite is the currently supported database.
  • Migration 0000 is the clean release baseline for empty databases.
  • Pre-baseline development databases, snapshots and stored commands are not supported.
  • After the first release, never edit an already applied migration.
  • Back up an existing database before applying a new migration.
  • Inspect generated SQL; it must contain only the intended schema change.
  • Keep database backups separate from logical project snapshots.
  • Do not import the global SQLite singleton into domain services.
  • Keep synchronous SQLite transaction behavior inside persistence adapters.
  • Preserve stable UUIDs and explicit transaction boundaries for a later PostgreSQL adapter.

Current Deferred Work

  • Revit/CSV/IFCGUID round-trip, except when Phase 14 is explicitly requested and docs/spec/revit-csv-integration-requirements.md is being followed
  • full electrical sizing
  • multi-user/PostgreSQL operation
  • supported production deployment

Do not implement these while working on an unrelated phase.

Documentation and Verification

  • README.md is the setup entry point.
  • docs/README.md is the documentation map.
  • docs/current-architecture.md describes current code paths.
  • docs/spec/ contains requirements and roadmap, not proof of implementation.
  • docs/archive/ is historical and must not drive current implementation.

For a normal code change run the relevant focused tests plus:

  • npm test
  • npm run build:api
  • npm run build:web
  • npm run typecheck:scripts
  • npx tsc --noEmit -p tsconfig.next.json

Use a concise imperative commit message for each completed, verified work package.

Response Style for Codex

Be concise.

Do not restate the whole specification.

Reference the relevant file and section.

When uncertain, choose the simplest implementation that preserves the domain model.

Do not add libraries unless there is a clear reason.

Do not introduce global state management unless needed.