Plan circuit group moves
This commit is contained in:
@@ -320,6 +320,12 @@ Gruppen, Stromkreise und Gruppenkomponenten exakt dem erwarteten Zustand
|
|||||||
entsprechen. Nummerntausch verwendet kollisionsfreie temporäre Präfixe und
|
entsprechen. Nummerntausch verwendet kollisionsfreie temporäre Präfixe und
|
||||||
BMKs über beide BMK-Tabellen; anschließend werden alle Zielwerte finalisiert.
|
BMKs über beide BMK-Tabellen; anschließend werden alle Zielwerte finalisiert.
|
||||||
Der vollständige inverse Plan ermöglicht dauerhaftes Undo/Redo.
|
Der vollständige inverse Plan ermöglicht dauerhaftes Undo/Redo.
|
||||||
|
`src/domain/services/circuit-group-move-planning.ts` plant den Wechsel eines
|
||||||
|
vollständigen Stromkreises zwischen zwei verschiedenen Gruppen derselben
|
||||||
|
Kategorie. Die Ziel-BMK verwendet die höchste vorhandene Stromkreis-Endnummer
|
||||||
|
der Zielgruppe plus eins; Lücken werden nicht gefüllt. Quell-/Zielgruppe,
|
||||||
|
Quell-/Zielposition und beide BMKs werden festgeschrieben, damit eine spätere
|
||||||
|
Wiederholung nichts neu berechnet. Die persistente Ausführung folgt separat.
|
||||||
`distribution-board.update` versioniert Etage, Netzart und den
|
`distribution-board.update` versioniert Etage, Netzart und den
|
||||||
verteilerweiten Gleichzeitigkeitsfaktor gemeinsam und stellt alle Werte über
|
verteilerweiten Gleichzeitigkeitsfaktor gemeinsam und stellt alle Werte über
|
||||||
dauerhaftes Undo/Redo wieder her. Der Faktor liegt zwischen `0` und `1` und
|
dauerhaftes Undo/Redo wieder her. Der Faktor liegt zwischen `0` und `1` und
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ requirements and intended sequencing, not proof of implementation.
|
|||||||
- [x] Phase C2b2: complete group reorder command.
|
- [x] Phase C2b2: complete group reorder command.
|
||||||
- [x] Phase D1a: deterministic collision-aware group-renumber plan.
|
- [x] Phase D1a: deterministic collision-aware group-renumber plan.
|
||||||
- [x] Phase D1b: persistent collision-safe group-renumber command.
|
- [x] Phase D1b: persistent collision-safe group-renumber command.
|
||||||
- [ ] Phase D2: same-category cross-group circuit moves.
|
- [x] Phase D2a: deterministic same-category circuit-move plan.
|
||||||
|
- [ ] Phase D2b: persistent same-category circuit-move command.
|
||||||
- [ ] Phase D3: confirmed populated-group deletion.
|
- [ ] Phase D3: confirmed populated-group deletion.
|
||||||
- [ ] Phase E: editor projection and editing.
|
- [ ] Phase E: editor projection and editing.
|
||||||
- [ ] Phase F: documentation and full GUI verification.
|
- [ ] Phase F: documentation and full GUI verification.
|
||||||
|
|||||||
@@ -662,8 +662,9 @@ Acceptance:
|
|||||||
|
|
||||||
### D. Group Numbering and Circuit Moves
|
### D. Group Numbering and Circuit Moves
|
||||||
|
|
||||||
Status: In progress. Deterministic planning D1a and persistent execution D1b
|
Status: In progress. Group renumbering D1 and deterministic circuit-move
|
||||||
are complete; cross-group moves D2 and populated deletion D3 remain pending.
|
planning D2a are complete; persistent moves D2b and populated deletion D3
|
||||||
|
remain pending.
|
||||||
|
|
||||||
- implement nested identifier generation
|
- implement nested identifier generation
|
||||||
- support same-category cross-group circuit moves
|
- support same-category cross-group circuit moves
|
||||||
@@ -690,6 +691,15 @@ Implemented in D1b:
|
|||||||
- Undo/Redo uses the exact inverse plan; late revision/history failures roll
|
- Undo/Redo uses the exact inverse plan; late revision/history failures roll
|
||||||
back every group, prefix and BMK
|
back every group, prefix and BMK
|
||||||
|
|
||||||
|
Implemented in D2a:
|
||||||
|
|
||||||
|
- a pure planner permits moves only between distinct groups of the same
|
||||||
|
category
|
||||||
|
- it validates the source BMK and stores source/target group and sort position
|
||||||
|
- the target BMK uses the highest existing target-group circuit suffix plus one
|
||||||
|
and never fills gaps
|
||||||
|
- the computed target BMK is part of the plan and is not recalculated for Redo
|
||||||
|
|
||||||
Acceptance:
|
Acceptance:
|
||||||
|
|
||||||
- target identifiers use highest suffix plus one
|
- target identifiers use highest suffix plus one
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import type { CircuitGroupCategory } from "../../shared/constants/circuit-group.js";
|
||||||
|
import {
|
||||||
|
formatCircuitGroupPrefix,
|
||||||
|
getNextGroupedCircuitIdentifier,
|
||||||
|
parseGroupedEquipmentIdentifier,
|
||||||
|
} from "./circuit-group-numbering.js";
|
||||||
|
|
||||||
|
export interface CircuitMoveGroupIdentity {
|
||||||
|
id: string;
|
||||||
|
category: CircuitGroupCategory;
|
||||||
|
groupNumber: number;
|
||||||
|
prefix: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CircuitGroupMovePlan {
|
||||||
|
circuitId: string;
|
||||||
|
circuitListId: string;
|
||||||
|
expectedSectionId: string;
|
||||||
|
targetSectionId: string;
|
||||||
|
expectedEquipmentIdentifier: string;
|
||||||
|
targetEquipmentIdentifier: string;
|
||||||
|
expectedSortOrder: number;
|
||||||
|
targetSortOrder: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createCircuitGroupMovePlan(input: {
|
||||||
|
circuit: {
|
||||||
|
id: string;
|
||||||
|
circuitListId: string;
|
||||||
|
sectionId: string;
|
||||||
|
equipmentIdentifier: string;
|
||||||
|
sortOrder: number;
|
||||||
|
};
|
||||||
|
sourceGroup: CircuitMoveGroupIdentity;
|
||||||
|
targetGroup: CircuitMoveGroupIdentity;
|
||||||
|
targetCircuitEquipmentIdentifiers: Iterable<string>;
|
||||||
|
targetSortOrder: number;
|
||||||
|
}): CircuitGroupMovePlan {
|
||||||
|
assertGroupIdentity(input.sourceGroup);
|
||||||
|
assertGroupIdentity(input.targetGroup);
|
||||||
|
if (
|
||||||
|
input.sourceGroup.id === input.targetGroup.id ||
|
||||||
|
input.sourceGroup.category !== input.targetGroup.category
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Circuits can move only between different groups of the same category."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
input.circuit.sectionId !== input.sourceGroup.id ||
|
||||||
|
!input.circuit.id.trim() ||
|
||||||
|
!input.circuit.circuitListId.trim() ||
|
||||||
|
!Number.isFinite(input.circuit.sortOrder) ||
|
||||||
|
!Number.isFinite(input.targetSortOrder)
|
||||||
|
) {
|
||||||
|
throw new Error("Circuit move source or target position is invalid.");
|
||||||
|
}
|
||||||
|
const parsed = parseGroupedEquipmentIdentifier(
|
||||||
|
input.circuit.equipmentIdentifier
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
parsed?.kind !== "circuit" ||
|
||||||
|
parsed.category !== input.sourceGroup.category ||
|
||||||
|
parsed.groupNumber !== input.sourceGroup.groupNumber
|
||||||
|
) {
|
||||||
|
throw new Error("Circuit BMK does not match its source group.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
circuitId: input.circuit.id,
|
||||||
|
circuitListId: input.circuit.circuitListId,
|
||||||
|
expectedSectionId: input.sourceGroup.id,
|
||||||
|
targetSectionId: input.targetGroup.id,
|
||||||
|
expectedEquipmentIdentifier:
|
||||||
|
input.circuit.equipmentIdentifier,
|
||||||
|
targetEquipmentIdentifier:
|
||||||
|
getNextGroupedCircuitIdentifier(
|
||||||
|
input.targetGroup.category,
|
||||||
|
input.targetGroup.groupNumber,
|
||||||
|
input.targetCircuitEquipmentIdentifiers
|
||||||
|
),
|
||||||
|
expectedSortOrder: input.circuit.sortOrder,
|
||||||
|
targetSortOrder: input.targetSortOrder,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertGroupIdentity(group: CircuitMoveGroupIdentity) {
|
||||||
|
if (
|
||||||
|
!group.id.trim() ||
|
||||||
|
!Number.isInteger(group.groupNumber) ||
|
||||||
|
group.groupNumber < 1 ||
|
||||||
|
group.prefix !==
|
||||||
|
formatCircuitGroupPrefix(group.category, group.groupNumber)
|
||||||
|
) {
|
||||||
|
throw new Error("Circuit move group identity is invalid.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
import "./circuit-group-structure-project-command.repository.test.js";
|
import "./circuit-group-structure-project-command.repository.test.js";
|
||||||
import { createCircuitGroupRenumberPlan } from "../src/domain/services/circuit-group-renumbering.js";
|
import { createCircuitGroupRenumberPlan } from "../src/domain/services/circuit-group-renumbering.js";
|
||||||
import "./circuit-group-renumber-project-command.repository.test.js";
|
import "./circuit-group-renumber-project-command.repository.test.js";
|
||||||
|
import { createCircuitGroupMovePlan } from "../src/domain/services/circuit-group-move-planning.js";
|
||||||
|
|
||||||
describe("circuit group numbering", () => {
|
describe("circuit group numbering", () => {
|
||||||
it("formats the agreed identifiers including the leading hyphen", () => {
|
it("formats the agreed identifiers including the leading hyphen", () => {
|
||||||
@@ -240,3 +241,117 @@ describe("circuit group renumber planning", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("circuit group move planning", () => {
|
||||||
|
const sourceGroup = {
|
||||||
|
id: "lighting-1",
|
||||||
|
category: "lighting" as const,
|
||||||
|
groupNumber: 1,
|
||||||
|
prefix: "-1F1.",
|
||||||
|
};
|
||||||
|
const targetGroup = {
|
||||||
|
id: "lighting-2",
|
||||||
|
category: "lighting" as const,
|
||||||
|
groupNumber: 2,
|
||||||
|
prefix: "-1F2.",
|
||||||
|
};
|
||||||
|
|
||||||
|
it("uses the highest target suffix plus one and stores both positions", () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
createCircuitGroupMovePlan({
|
||||||
|
circuit: {
|
||||||
|
id: "circuit-1",
|
||||||
|
circuitListId: "list-1",
|
||||||
|
sectionId: "lighting-1",
|
||||||
|
equipmentIdentifier: "-1F1.7",
|
||||||
|
sortOrder: 20,
|
||||||
|
},
|
||||||
|
sourceGroup,
|
||||||
|
targetGroup,
|
||||||
|
targetCircuitEquipmentIdentifiers: [
|
||||||
|
"-1F2.1",
|
||||||
|
"-1F2.4",
|
||||||
|
"-1F2.9",
|
||||||
|
],
|
||||||
|
targetSortOrder: 40,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
circuitId: "circuit-1",
|
||||||
|
circuitListId: "list-1",
|
||||||
|
expectedSectionId: "lighting-1",
|
||||||
|
targetSectionId: "lighting-2",
|
||||||
|
expectedEquipmentIdentifier: "-1F1.7",
|
||||||
|
targetEquipmentIdentifier: "-1F2.10",
|
||||||
|
expectedSortOrder: 20,
|
||||||
|
targetSortOrder: 40,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not fill target gaps", () => {
|
||||||
|
assert.equal(
|
||||||
|
createCircuitGroupMovePlan({
|
||||||
|
circuit: {
|
||||||
|
id: "circuit-1",
|
||||||
|
circuitListId: "list-1",
|
||||||
|
sectionId: "lighting-1",
|
||||||
|
equipmentIdentifier: "-1F1.2",
|
||||||
|
sortOrder: 10,
|
||||||
|
},
|
||||||
|
sourceGroup,
|
||||||
|
targetGroup,
|
||||||
|
targetCircuitEquipmentIdentifiers: ["-1F2.1", "-1F2.3"],
|
||||||
|
targetSortOrder: 20,
|
||||||
|
}).targetEquipmentIdentifier,
|
||||||
|
"-1F2.4"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects cross-category, same-group and mismatched source moves", () => {
|
||||||
|
const base = {
|
||||||
|
circuit: {
|
||||||
|
id: "circuit-1",
|
||||||
|
circuitListId: "list-1",
|
||||||
|
sectionId: "lighting-1",
|
||||||
|
equipmentIdentifier: "-1F1.1",
|
||||||
|
sortOrder: 10,
|
||||||
|
},
|
||||||
|
sourceGroup,
|
||||||
|
targetGroup,
|
||||||
|
targetCircuitEquipmentIdentifiers: [] as string[],
|
||||||
|
targetSortOrder: 10,
|
||||||
|
};
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
createCircuitGroupMovePlan({
|
||||||
|
...base,
|
||||||
|
targetGroup: {
|
||||||
|
id: "single-1",
|
||||||
|
category: "single_phase",
|
||||||
|
groupNumber: 1,
|
||||||
|
prefix: "-2F1.",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
/same category/
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
createCircuitGroupMovePlan({
|
||||||
|
...base,
|
||||||
|
targetGroup: sourceGroup,
|
||||||
|
}),
|
||||||
|
/different groups/
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
createCircuitGroupMovePlan({
|
||||||
|
...base,
|
||||||
|
circuit: {
|
||||||
|
...base.circuit,
|
||||||
|
equipmentIdentifier: "-1F9.1",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
/does not match/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user