25 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/appwith reusable editor modules undersrc/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-*.tsmodules. - 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.tsfor the atomic domain-write, revision and history transition boundary. Its applied forward-command variant preserves derived CircuitDeviceRow override metadata. - Low-level
appendProjectRevisionpersistence 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.tsowns 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:
ProjectDeviceDistributionBoardCircuitSectionCircuitCircuitDeviceRowequipmentIdentifierdisplayNamephaseTypequantitypowerPerUnitsimultaneityFactorcosPhirowTotalPowercircuitTotalPower
Avoid ambiguous names like:
itemthingentryrowData
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.
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.
IfcGUIDidentifies the external Revit object and must never replace an internal UUID orequipmentIdentifier. - 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
CircuitDeviceRowonly 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
IfcGUIDas 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-sqlite3or 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
0000is 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.mdis 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.mdis the setup entry point.docs/README.mdis the documentation map.docs/current-architecture.mddescribes 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 testnpm run build:apinpm run build:webnpm run typecheck:scriptsnpx 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.