docs(plans): split Task 14 into 14a-14f (entity-model rewrite)

The original Task 14 (5-min EF migration that "drops ConfigGeneration") was
under-scoped: the design doc (live-edit model, ~line 208) requires removing
GenerationId from 13 entities (Equipment, DriverInstance, Device, Tag,
PollGroup, Namespace, UnsArea, UnsLine, NodeAcl, Script, VirtualTag,
ScriptedAlarm) and adding RowVersion columns for last-write-wins detection.
That cascades into GenerationApplier / GenerationDiff / GenerationSealedCache
and the legacy Server/Admin CRUD services.

New decomposition (~85 min total, replacing the original 5-min estimate):

  14a  standard   10m  Add RowVersion to live-edit entities
  14b  high-risk  30m  Drop GenerationId FK from those entities
  14c  high-risk  20m  Obsolete GenerationApplier/Diff/SealedCache
  14d  standard   5m   Drop ClusterNode.RedundancyRole
  14e  small      5m   Delete ConfigGeneration + ClusterNodeGenerationState
  14f  high-risk  15m  Consolidator: generate V2HostingAlignment migration

Policy decision (recorded with user): OtOpcUa.Server + OtOpcUa.Admin are
allowed to fail-to-compile between 14b and Task 56 - only the new v2 projects
need to stay green. Task 56 deletes the legacy projects.

Plan markdown: replaces the original Task 14 section with the 6-task
decomposition + a header explaining the rewrite. Task index table at the
bottom of the plan updated.

Tasks JSON: replaces the single Task 14 row with 6 string-id rows
("14a", "14b", ..., "14f"). Task 15 (Migrate-To-V2.ps1) and downstream
consumers re-pointed at "14f".

Verification step in 14f rewritten to use the shared docker host at
10.100.0.35 per CLAUDE.md (Docker is not installed on this Mac dev VM).
This commit is contained in:
Joseph Doherty
2026-05-26 03:55:48 -04:00
parent 8e2c4f2835
commit 990ce343fe
2 changed files with 167 additions and 40 deletions

View File

@@ -464,56 +464,173 @@ Commit: `feat(configdb): persist DataProtection keys in ConfigDb`.
---
### Task 14: EF migration — drop `ConfigGeneration` and `ClusterNode.RedundancyRole`, add new tables
### Tasks 14a-14f: Entity-model rewrite + V2HostingAlignment migration
> **Plan rewrite, 2026-05-26**: the original single Task 14 (5-min EF migration) was
> under-scoped — it only listed the schema drops/adds without addressing the 13+ entities
> whose foreign keys + indexes are keyed on `GenerationId`. The design doc (§ live-edit
> model) requires removing `GenerationId` from `Equipment`, `Driver`, `DriverInstance`,
> `Namespace`, `UnsArea`, `UnsLine`, `Device`, `Tag`, `PollGroup`, `NodeAcl`, `Script`,
> `VirtualTag`, `ScriptedAlarm` and adding `RowVersion` columns for last-write-wins
> stale-write detection. That cascades into `GenerationApplier`/`GenerationDiff`/
> `GenerationSealedCache` and the legacy Server/Admin CRUD services. Policy decision
> (recorded with the user): the legacy `OtOpcUa.Server` + `OtOpcUa.Admin` projects are
> allowed to fail-to-compile between Task 14c and Task 56 — only the new v2 projects need
> to stay green.
#### Task 14a: Add `RowVersion` to live-edit entities
**Classification:** standard
**Estimated implement time:** ~10 min
**Parallelizable with:** none (foundation for 14b)
**Files:** every live-edit entity class — `Equipment`, `DriverInstance`, `Device`, `Tag`,
`PollGroup`, `Namespace`, `UnsArea`, `UnsLine`, `NodeAcl`, `Script`, `VirtualTag`,
`ScriptedAlarm`. Add `public byte[] RowVersion { get; set; } = Array.Empty<byte>();` and a
`e.Property(x => x.RowVersion).IsRowVersion();` mapping in `OtOpcUaConfigDbContext`.
Commit: `feat(configdb): add RowVersion to live-edit entities for last-write-wins detection`.
---
#### Task 14b: Decouple live-edit entities from `ConfigGeneration`
**Classification:** high-risk
**Estimated implement time:** ~30 min
**Parallelizable with:** none
Remove `GenerationId` property, `Generation` navigation property, and the
`HasOne(x => x.Generation).WithMany().HasForeignKey(x => x.GenerationId)` mapping from each
of the 13 live-edit entities listed above. Rewrite the `UX_<Table>_Generation_LogicalId`
indexes to drop the `GenerationId` column (logical IDs become globally unique). Drop
`UX_*_Generation_*` filtered indexes where the filter referenced generation scope.
Will break `OtOpcUa.Server` + `OtOpcUa.Admin` compilation — that is accepted (Task 56
deletes them).
Commit: `refactor(configdb): drop GenerationId FK from live-edit entities`.
---
#### Task 14c: Mark `GenerationApplier` / `GenerationDiff` / `GenerationSealedCache` obsolete
**Classification:** high-risk
**Estimated implement time:** ~20 min
**Parallelizable with:** none
`src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Apply/` contains `GenerationApplier.cs`,
`GenerationDiff.cs`, `ApplyCallbacks.cs`, `ChangeKind.cs`, `IGenerationApplier.cs`. These
implement the v1 draft/publish lifecycle that v2 replaces with `AdminOperationsActor` +
`ConfigComposer`.
Inventory callers via `grep -rln 'GenerationApplier\|GenerationDiff' src tests`. Either:
- Mark types `[Obsolete("Replaced by AdminOperationsActor in v2", error: true)]` so
surviving call sites become hard build errors (cleaner; surfaces the Server-breakage),
- Or delete the files and accept the Server-side build break.
Sweep `GenerationSealedCache` similarly. Keep the LiteDb cache concept (it's repurposed
in Task 39 for stale-config fallback) but rename references to use `DeploymentArtifact`.
Commit: `refactor(configdb): obsolete GenerationApplier/Diff/SealedCache (replaced by AdminOperationsActor)`.
---
#### Task 14d: Drop `RedundancyRole` from `ClusterNode`
**Classification:** standard
**Estimated implement time:** ~5 min
**Parallelizable with:** none (depends on Tasks 10-13)
**Parallelizable with:** none
Remove `ClusterNode.RedundancyRole` property + the
`e.Property(x => x.RedundancyRole).HasConversion<string>()` mapping + the
`UX_ClusterNode_Primary_Per_Cluster` filtered unique index from
`OtOpcUaConfigDbContext.ConfigureClusterNode`. Akka cluster leader-of-driver-role becomes
the source of truth (Phase 5, Task 35).
Commit: `refactor(configdb): drop ClusterNode.RedundancyRole (replaced by Akka leader)`.
---
#### Task 14e: Delete `ConfigGeneration` + `ClusterNodeGenerationState`
**Classification:** small
**Estimated implement time:** ~5 min
**Parallelizable with:** none (depends on 14b clearing the FKs)
Delete `Entities/ConfigGeneration.cs` and `Entities/ClusterNodeGenerationState.cs`. Remove
the corresponding `DbSet<>` entries and `Configure*` methods from
`OtOpcUaConfigDbContext`. Drop `GenerationStatus` and `NodeApplyStatus` enums.
Commit: `refactor(configdb): delete ConfigGeneration + ClusterNodeGenerationState`.
---
#### Task 14f: Generate `V2HostingAlignment` EF migration
**Classification:** high-risk
**Estimated implement time:** ~15 min
**Parallelizable with:** none (consolidates 14a-14e)
**Files:**
- Create: `src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/<timestamp>_V2HostingAlignment.cs`
- Create: `src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/<timestamp>_V2HostingAlignment.Designer.cs`
- Modify: `OtOpcUaConfigDbContext.cs` — remove `DbSet<ConfigGeneration>` and `DbSet<ClusterNodeGenerationState>`; remove `ClusterNode.RedundancyRole` property
- Delete: `src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ConfigGeneration.cs`
- Delete: `src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ClusterNodeGenerationState.cs`
- Modify: `tests/Core/ZB.MOM.WW.OtOpcUa.Configuration.Tests/SchemaComplianceTests.cs` — update
the `expected` table list (remove ConfigGeneration + ClusterNodeGenerationState; add
Deployment + NodeDeploymentState + ConfigEdit + DataProtectionKeys).
**Step 1: Generate migration**
Run: `dotnet ef migrations add V2HostingAlignment --project src/Core/ZB.MOM.WW.OtOpcUa.Configuration --startup-project src/Server/ZB.MOM.WW.OtOpcUa.Host`
```bash
dotnet ef migrations add V2HostingAlignment \
--project src/Core/ZB.MOM.WW.OtOpcUa.Configuration \
--startup-project src/Server/ZB.MOM.WW.OtOpcUa.Host
```
If `dotnet-ef` isn't installed: `dotnet tool install --global dotnet-ef --version 10.0.7`.
**Step 2: Audit the generated migration** — it should:
- `DropTable("ConfigGeneration")`
- `DropTable("ClusterNodeGenerationState")`
- `DropTable("ConfigGeneration")` and `DropTable("ClusterNodeGenerationState")`
- `DropColumn("RedundancyRole", "ClusterNode")`
- `CreateTable("Deployment", ...)`
- `CreateTable("NodeDeploymentState", ...)`
- `CreateTable("ConfigEdit", ...)`
- `CreateTable("DataProtectionKeys", ...)`
- For each of the 13 live-edit tables: `DropForeignKey` on `GenerationId`,
`DropIndex` on `UX_*_Generation_LogicalId` (and any `UX_*_Generation_*`), `DropColumn` on
`GenerationId`, `AddColumn("RowVersion", "rowversion")`, `CreateIndex` on the new
globally-unique logical-id pattern.
- `CreateTable("Deployment", ...)`, `CreateTable("NodeDeploymentState", ...)`,
`CreateTable("ConfigEdit", ...)`, `CreateTable("DataProtectionKeys", ...)`.
If extra changes appear (e.g., column-type drift), reconcile by editing the entity classes — do not edit the migration directly.
If extra changes appear (e.g., column-type drift), reconcile by editing the entity classes
— do not edit the migration directly.
**Step 3: Verify on a scratch SQL Server**
**Step 3: Verify on a scratch SQL Server** (per CLAUDE.md, Docker is on the shared host
`10.100.0.35`, not local).
```bash
docker run --rm -d --name v2-migration-test -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=Pass@word123" -p 14333:1433 mcr.microsoft.com/mssql/server:2022-latest
# from this Mac dev:
ssh dohertj2@10.100.0.35 'docker run --rm -d --name v2-migration-test \
-e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=Pass@word123" \
-p 14333:1433 mcr.microsoft.com/mssql/server:2022-latest'
# Wait ~10s for SQL Server to start
ConnectionStrings__ConfigDb="Server=localhost,14333;Database=OtOpcUaV2Test;User Id=sa;Password=Pass@word123;TrustServerCertificate=true" \
dotnet ef database update --project src/Core/ZB.MOM.WW.OtOpcUa.Configuration --startup-project src/Server/ZB.MOM.WW.OtOpcUa.Host
ConnectionStrings__ConfigDb="Server=10.100.0.35,14333;Database=OtOpcUaV2Test;User Id=sa;Password=Pass@word123;TrustServerCertificate=true" \
dotnet ef database update \
--project src/Core/ZB.MOM.WW.OtOpcUa.Configuration \
--startup-project src/Server/ZB.MOM.WW.OtOpcUa.Host
ssh dohertj2@10.100.0.35 'docker exec v2-migration-test /opt/mssql-tools/bin/sqlcmd \
-S localhost -U sa -P Pass@word123 -d OtOpcUaV2Test \
-Q "SELECT name FROM sys.tables ORDER BY name"'
ssh dohertj2@10.100.0.35 'docker stop v2-migration-test'
```
Expected: completes without error. Verify with `docker exec v2-migration-test /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P Pass@word123 -d OtOpcUaV2Test -Q "SELECT name FROM sys.tables ORDER BY name"`.
Expected: migration completes; sys.tables contains the 4 new tables and not the 2 dropped
ones; live-edit tables have `RowVersion` column.
**Step 4: Tear down**
`docker stop v2-migration-test`.
**Step 4: Update `SchemaComplianceTests`** so its `expected` array matches the new schema.
**Step 5: Commit**
```bash
git add src/Core/ZB.MOM.WW.OtOpcUa.Configuration/
git commit -m "feat(configdb): V2HostingAlignment migration — drop ConfigGeneration, add Deployment+NodeDeploymentState+ConfigEdit"
git add src/Core/ZB.MOM.WW.OtOpcUa.Configuration/ \
tests/Core/ZB.MOM.WW.OtOpcUa.Configuration.Tests/SchemaComplianceTests.cs
git commit -m "feat(configdb): V2HostingAlignment migration — drop generation lifecycle, add deploy tables"
```
---
@@ -1888,7 +2005,12 @@ After Task 65:
| 11 | NodeDeploymentState entity | standard | 5m | 10,12,13 |
| 12 | ConfigEdit entity | small | 4m | 10,11,13 |
| 13 | DataProtection keys | small | 3m | 10-12 |
| 14 | V2 migration | high-risk | 5m | — |
| 14a | RowVersion on live-edit entities | standard | 10m | — |
| 14b | Drop GenerationId FK from entities | high-risk | 30m | — |
| 14c | Obsolete GenerationApplier/Diff/Cache | high-risk | 20m | — |
| 14d | Drop ClusterNode.RedundancyRole | standard | 5m | — |
| 14e | Delete ConfigGeneration + ClusterNodeGenerationState | small | 5m | — |
| 14f | V2HostingAlignment migration (consolidator) | high-risk | 15m | — |
| 15 | Migrate-To-V2.ps1 | standard | 5m | 16-18 |
| 16 | Common types | standard | 5m | 17,18 |
| 17 | Message contracts | standard | 5m | 16,18 |