docs(phase4): implementation plan + tasks.json (5 data-type tier tasks)
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
# Phase 4 (data-type tier) — Driver data-type & robustness coverage — Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:subagent-driven-development to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Close five cleanly-achievable scalar data-type & robustness gaps from `stillpending.md` §2 across three disjoint driver projects (Modbus, FOCAS, Historian/AlarmHistorian).
|
||||
|
||||
**Architecture:** Per-driver-independent changes; no contract churn, no EF migration, no `IOpcUaAddressSpaceSink` touch. `DriverDataType.Int64`/`UInt64` already exist + already resolve correctly on the equipment-node path. S7 wide types and all array work are deferred (see design doc).
|
||||
|
||||
**Tech Stack:** .NET 10, xUnit + Shouldly (TDD red→green, **no bUnit**), Akka not touched, SQLite store-and-forward (item 5).
|
||||
|
||||
**Design:** `docs/plans/2026-06-16-stillpending-phase-4-driver-datatypes-design.md` (branch `feat/stillpending-phase-4-driver-datatypes` off master `7eeb9fb0`).
|
||||
|
||||
**Hard rules:** stage by path — never `git add .`; never stage `sql_login.txt` / `src/Server/.../Host/pki/` / `pending.md` / `current.md` / `docker-dev/docker-compose.yml` / `stillpending.md`; never echo/commit secrets; no force-push; no `--no-verify`; NO Configuration entity / EF migration.
|
||||
|
||||
---
|
||||
|
||||
### Task 0: Feature branch — DONE
|
||||
|
||||
Branch `feat/stillpending-phase-4-driver-datatypes` created off master `7eeb9fb0`; design doc committed `57d9f1b3`. No action.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Modbus Int64 / UInt64 node DataType
|
||||
|
||||
**Classification:** small
|
||||
**Estimated implement time:** ~3 min
|
||||
**Parallelizable with:** Task 2, Task 4, Task 5
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusDriver.cs` (`MapDataType` ~1508-1523; stale doc ~1496-1506)
|
||||
- Test: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests/ModbusDataTypeTests.cs`
|
||||
|
||||
**Context:** `DriverDataType.Int64`/`UInt64` already exist and `OtOpcUaNodeManager.ResolveBuiltInDataType` already maps `"Int64"`/`"UInt64"`. The wire codec round-trips `long`/`ulong` correctly — only `MapDataType` lies (returns `Int32`).
|
||||
|
||||
**Step 1 — Failing test.** In `ModbusDataTypeTests.cs` add:
|
||||
```csharp
|
||||
[Theory]
|
||||
[InlineData(ModbusDataType.Int64, DriverDataType.Int64)]
|
||||
[InlineData(ModbusDataType.UInt64, DriverDataType.UInt64)]
|
||||
public void MapDataType_64bit_surfaces_correct_DriverDataType(ModbusDataType wire, DriverDataType expected)
|
||||
=> ModbusDriver.MapDataType(wire).ShouldBe(expected);
|
||||
```
|
||||
(If `MapDataType` is `private`, either make it `internal` + ensure `[InternalsVisibleTo]` to the test project already exists — it does for `DecodeRegister`/`EncodeRegister` tests — or assert via a `DiscoverAsync` recording-builder test that an `Int64` tag's `DriverAttributeInfo.DriverDataType == DriverDataType.Int64`. Prefer matching the existing visibility pattern in this file.)
|
||||
|
||||
**Step 2 — Run, expect FAIL** (`Int64` currently maps to `Int32`):
|
||||
`dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests --filter MapDataType_64bit`
|
||||
|
||||
**Step 3 — Implement.** In `MapDataType` replace the combined line:
|
||||
```csharp
|
||||
ModbusDataType.Int64 or ModbusDataType.UInt64 => DriverDataType.Int32,
|
||||
```
|
||||
with:
|
||||
```csharp
|
||||
ModbusDataType.Int64 => DriverDataType.Int64,
|
||||
ModbusDataType.UInt64 => DriverDataType.UInt64,
|
||||
```
|
||||
Remove the stale `Driver.Modbus-007` XML-doc caveat block (~1496-1506) and the inline `// Driver.Modbus-007 …` comment (~1510-1516). Leave `Bcd16/Bcd32 => Int32` and the `_ => Int32` default unchanged.
|
||||
|
||||
**Step 4 — Run, expect PASS** + the full Modbus suite green:
|
||||
`dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests`
|
||||
|
||||
**Step 5 — Commit:**
|
||||
```bash
|
||||
git add src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusDriver.cs \
|
||||
tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests/ModbusDataTypeTests.cs
|
||||
git commit -m "fix(modbus): surface Int64/UInt64 node DataType (Driver.Modbus-007)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: FOCAS fail-fast factory
|
||||
|
||||
**Classification:** small
|
||||
**Estimated implement time:** ~4 min
|
||||
**Parallelizable with:** Task 1, Task 4, Task 5 (NOT Task 3 — both edit `FocasDriver.cs`)
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/IFocasClient.cs` (`IFocasClientFactory`, `UnimplementedFocasClientFactory`, `WireFocasClientFactory`)
|
||||
- Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasDriver.cs` (`InitializeAsync`)
|
||||
- Test: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasScaffoldingTests.cs`
|
||||
|
||||
**Context:** `UnimplementedFocasClientFactory.Create()` throws only on first read (lazy `Create()` at `FocasDriver.cs:1079`), so an `unimplemented`/`none`/`stub` backend reports **healthy** then faults every read. Make it fail at init.
|
||||
|
||||
**Step 1 — Failing test.** In `FocasScaffoldingTests.cs` add a test asserting that a driver built on `Backend:"unimplemented"` throws (or transitions the driver Faulted, matching how this suite already asserts init failures) from `InitializeAsync`, BEFORE any `ReadAsync`. Mirror the existing scaffolding-test setup. Assert the exception/health message contains `"unimplemented"`.
|
||||
|
||||
**Step 2 — Run, expect FAIL** (today init succeeds; the throw only happens on first read):
|
||||
`dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests --filter Scaffolding`
|
||||
|
||||
**Step 3 — Implement.**
|
||||
- Add to `IFocasClientFactory`: `void EnsureUsable();` (a config-time usability probe that must NOT create a live wire client).
|
||||
- `WireFocasClientFactory.EnsureUsable()` → no-op (`{ }`).
|
||||
- `UnimplementedFocasClientFactory.EnsureUsable()` → `throw new NotSupportedException(<same message as Create()>)`.
|
||||
- In `FocasDriver.InitializeAsync`, after the factory is in hand and before the probe/poll loops start, call `_clientFactory.EnsureUsable();` (let it propagate so the driver faults at init with the actionable message). Keep `Create()`'s own throw as the lazy backstop.
|
||||
|
||||
**Step 4 — Run, expect PASS** + full FOCAS suite green:
|
||||
`dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests`
|
||||
|
||||
**Step 5 — Commit:**
|
||||
```bash
|
||||
git add src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/IFocasClient.cs \
|
||||
src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasDriver.cs \
|
||||
tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasScaffoldingTests.cs
|
||||
git commit -m "fix(focas): fail-fast at init on unimplemented backend (operator footgun)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: FOCAS position scaling (config DecimalPlaces)
|
||||
|
||||
**Classification:** standard
|
||||
**Estimated implement time:** ~5 min
|
||||
**Parallelizable with:** Task 1, Task 4, Task 5 (NOT Task 2 — both edit `FocasDriver.cs`; sequence after Task 2)
|
||||
|
||||
**Files:**
|
||||
- Modify: the FOCAS device-config record (find it: `rg "record FocasDeviceOptions" src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/`; if config flows via a separate DTO `FocasDriverConfigDto`/device DTO, add the field there and thread it onto the device options) — add `PositionDecimalPlaces` (int, default `0`).
|
||||
- Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasDriver.cs` (`PublishAxisSnapshot` ~817-820)
|
||||
- Test: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasReadWriteTests.cs` (or a new `FocasPositionScalingTests.cs`)
|
||||
|
||||
**Context:** `cnc_rddynamic2` returns scaled integers; the `10^dp` divide is never applied. Axis variables are already `DriverDataType.Float64` (`FocasDriver.cs:519`) → **no node-type change**. Scale only the four position fields; FeedRate/SpindleSpeed/ServoLoad are untouched. Default `0` = today's behaviour (backward compatible).
|
||||
|
||||
**Step 1 — Failing test.** Assert: with the device configured `PositionDecimalPlaces:3`, a snapshot with `AbsolutePosition=12345` publishes `12.345d` (read it back via `ReadAsync` of the AbsolutePosition fixed-tree ref, mirroring `FocasReadWriteTests` setup which drives the poll loop / cache). Add a second case: `PositionDecimalPlaces:0` publishes `12345d` unchanged; and FeedRate is never scaled.
|
||||
|
||||
**Step 2 — Run, expect FAIL.**
|
||||
`dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests --filter Scaling`
|
||||
|
||||
**Step 3 — Implement.**
|
||||
- Add `PositionDecimalPlaces` (default 0) to the device options/DTO and thread it into `DeviceState.Options` (whatever `PublishAxisSnapshot` reads as `state.Options`).
|
||||
- In `PublishAxisSnapshot`, compute `var factor = state.Options.PositionDecimalPlaces > 0 ? Math.Pow(10, state.Options.PositionDecimalPlaces) : 1.0;` and store `snap.AbsolutePosition / factor` (etc.) for the **four position fields** (Absolute / Machine / Relative / DistanceToGo). Leave any FeedRate/SpindleSpeed publishes (and the `ServoLoad` field) unscaled. Publish as `double` (already Float64 nodes).
|
||||
- Guard a negative config value (treat `<0` as `0`, or validate in the config parse) so `Math.Pow` can't misbehave.
|
||||
|
||||
**Step 4 — Run, expect PASS** + full FOCAS suite green:
|
||||
`dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests`
|
||||
|
||||
**Step 5 — Commit:**
|
||||
```bash
|
||||
git add src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/ \
|
||||
tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/
|
||||
git commit -m "feat(focas): scale axis positions by 10^PositionDecimalPlaces (config-supplied)"
|
||||
```
|
||||
(Stage only the precise FOCAS files you edited — list them explicitly, do not blanket-add the dir if other work is present.)
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Historian `Total` aggregate (client-side)
|
||||
|
||||
**Classification:** standard
|
||||
**Estimated implement time:** ~5 min
|
||||
**Parallelizable with:** Task 1, Task 2, Task 3, Task 5
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client/WonderwareHistorianClient.cs` (`ReadProcessedAsync` ~102-130, `MapAggregate` ~513-522)
|
||||
- Test: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests/WonderwareHistorianClientTests.cs`
|
||||
|
||||
**Context:** `MapAggregate` throws for `Total` (Wonderware AnalogSummary has no Total column). For a time-weighted average, `Total = Average × interval-duration`. Compute client-side.
|
||||
|
||||
**Step 1 — Replace the throws-test.** Remove/repurpose `ReadProcessedAsync_TotalAggregate_ThrowsNotSupported`. Add `ReadProcessedAsync_TotalAggregate_ReturnsAverageTimesIntervalSeconds`: FakeSidecarServer `OnReadProcessed`/`OnAnalogSummary` returns Average buckets (e.g. value `2.0`); request `Total` with `interval = TimeSpan.FromMinutes(1)` (60 s) → client returns `120.0` for that bucket; quality carries through.
|
||||
|
||||
**Step 2 — Run, expect FAIL.**
|
||||
`dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests --filter TotalAggregate`
|
||||
|
||||
**Step 3 — Implement.** In `ReadProcessedAsync`:
|
||||
- Detect `aggregate == HistoryAggregateType.Total`. For the wire request, use `MapAggregate(HistoryAggregateType.Average)` as the `AggregateColumn` (i.e. substitute Average for the query).
|
||||
- After decoding the reply samples, when the original request was `Total`, multiply each sample's numeric value by `interval.TotalSeconds` (re-box at the same value type the Average decode produced; keep `StatusCode`/timestamps). Factor a small helper if cleaner.
|
||||
- Drop the `Total` throw from `MapAggregate` (or keep `MapAggregate` Average-only and never pass it `Total`). Add a `<remarks>` note: "Total is derived client-side as time-weighted Average × interval-seconds; Wonderware AnalogSummary exposes no Total column."
|
||||
|
||||
**Step 4 — Run, expect PASS** + full suite green:
|
||||
`dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests`
|
||||
|
||||
**Step 5 — Commit:**
|
||||
```bash
|
||||
git add src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client/WonderwareHistorianClient.cs \
|
||||
tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests/WonderwareHistorianClientTests.cs
|
||||
git commit -m "feat(historian): support Total aggregate (client-side Average x interval-seconds)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Historian poison-event dead-letter cap
|
||||
|
||||
**Classification:** standard
|
||||
**Estimated implement time:** ~5 min
|
||||
**Parallelizable with:** Task 1, Task 2, Task 3, Task 4
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/Core/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/SqliteStoreAndForwardSink.cs` (ctor ~119-135, `ReadBatch`/`QueueRow` ~555-586, drain outcome loop ~448-462)
|
||||
- Test: `tests/Core/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian.Tests/SqliteStoreAndForwardSinkTests.cs`
|
||||
- (If a wiring/options site passes the other knobs — `capacity`/`deadLetterRetention` — thread `maxAttempts` there too; grep `new SqliteStoreAndForwardSink(` and the `AddAlarmHistorian` DI to find call sites.)
|
||||
|
||||
**Context:** All failures map to `RetryPlease`; the drain bumps `AttemptCount` but never caps it → poison events retry forever. The `AttemptCount` column already exists (schema `:23,689`); `QueueRow` just doesn't surface it.
|
||||
|
||||
**Step 1 — Failing test.** In `SqliteStoreAndForwardSinkTests.cs`: a writer stub that always returns `RetryPlease`; enqueue one event; drain `maxAttempts` times (construct the sink with a small `maxAttempts`, e.g. 3). Assert after the 3rd drain the row is **dead-lettered** (`HistorianSinkStatus.DeadLetterDepth == 1`, `QueueDepth == 0`); assert before the cap it stays queued.
|
||||
|
||||
**Step 2 — Run, expect FAIL** (today it never dead-letters):
|
||||
`dotnet test tests/Core/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian.Tests --filter MaxAttempts`
|
||||
|
||||
**Step 3 — Implement.**
|
||||
- Add `DefaultMaxAttempts` const (e.g. `10`) + `int maxAttempts = DefaultMaxAttempts` ctor param; store `_maxAttempts`; guard `> 0` (throw `ArgumentOutOfRangeException` like `batchSize`/`capacity`, or clamp with a `_logger.Warning`). Add the `<param>` doc.
|
||||
- Extend `QueueRow` → `record struct QueueRow(long RowId, AlarmHistorianEvent? Event, long AttemptCount)`; add `AttemptCount` to the `ReadBatch` SELECT (`SELECT RowId, PayloadJson, AttemptCount …`) and read it (`reader.GetInt64(2)`).
|
||||
- In the drain outcome loop, `case HistorianWriteOutcome.RetryPlease:` → `if (liveRows[i].AttemptCount + 1 >= _maxAttempts) DeadLetterRow(conn, tx, rowId, $"max attempts ({_maxAttempts}) exceeded"); else BumpAttempt(conn, tx, rowId, "retry-please");` (and count a dead-lettered row as leaving the queue, mirroring the existing `PermanentFail` arm: `rowsLeavingQueue++`).
|
||||
|
||||
**Step 4 — Run, expect PASS** + full suite green:
|
||||
`dotnet test tests/Core/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian.Tests`
|
||||
|
||||
**Step 5 — Commit:**
|
||||
```bash
|
||||
git add src/Core/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/SqliteStoreAndForwardSink.cs \
|
||||
tests/Core/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian.Tests/SqliteStoreAndForwardSinkTests.cs
|
||||
git commit -m "fix(historian): dead-letter poison events after maxAttempts (finding 002)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Docs + bookkeeping
|
||||
|
||||
**Classification:** small
|
||||
**Estimated implement time:** ~4 min
|
||||
**Parallelizable with:** none (run after Tasks 1-5 land)
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/drivers/FOCAS.md` (fail-fast backend + `PositionDecimalPlaces` knob), the Modbus driver doc (Int64/UInt64 now surfaced), `docs/Historian.md` (Total aggregate now supported; poison-event dead-letter cap).
|
||||
- Modify (disk-only, do NOT commit): `pending.md`, `stillpending.md` annotations, memory `MEMORY.md` + `project_stillpending_backlog.md`.
|
||||
|
||||
**Steps:**
|
||||
1. Update each driver doc: Modbus Int64/UInt64 node-type now correct (remove the Driver.Modbus-007 caveat if mentioned); FOCAS unimplemented backend now fails at init + the `PositionDecimalPlaces` config field; Historian `Total` supported (note the Average×seconds derivation) + the `maxAttempts` dead-letter cap.
|
||||
2. Find the exact doc paths (`rg -l "Driver.Modbus-007|PositionDecimalPlaces|AnalogSummary" docs/` + the FOCAS/Modbus/Historian guide docs).
|
||||
3. Commit ONLY the `docs/**` files by path:
|
||||
```bash
|
||||
git add docs/drivers/FOCAS.md docs/Historian.md <modbus-doc-path>
|
||||
git commit -m "docs(phase4): Int64/UInt64 modbus, FOCAS fail-fast+scaling, Historian Total+dead-letter"
|
||||
```
|
||||
4. Update `pending.md` / `stillpending.md` / memory on disk (NOT staged) at the integration step.
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Full build + test + final integration review
|
||||
|
||||
**Classification:** high-risk (final gate)
|
||||
**Estimated implement time:** ~6 min
|
||||
**Parallelizable with:** none
|
||||
|
||||
**Steps:**
|
||||
1. `dotnet build ZB.MOM.WW.OtOpcUa.slnx` — expect 0 errors (production projects are `TreatWarningsAsErrors`).
|
||||
2. `dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests tests/Core/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian.Tests` — all green. (Then a full `dotnet test ZB.MOM.WW.OtOpcUa.slnx` if time allows.)
|
||||
3. **Final integration reviewer subagent** over `git diff 57d9f1b3..HEAD`: verify (a) no contract/`IOpcUaAddressSpaceSink`/`DriverDataType` change leaked in; (b) Modbus codec unchanged (only `MapDataType`); (c) FOCAS scaling applies to positions only + default 0 is byte-identical to old behaviour; (d) Historian Total quality/timestamp carry-through is correct; (e) the dead-letter cap counts the row as leaving the queue (no `_queuedRowCount` drift) and `maxAttempts<=0` is guarded; (f) every commit staged by path, no forbidden file staged.
|
||||
4. Address any blocking findings, re-review, then proceed.
|
||||
|
||||
---
|
||||
|
||||
### Task 8: Live `/run` — Modbus Int64 (the one live-verifiable item)
|
||||
|
||||
**Classification:** high-risk (acceptance gate, agent-driven on docker-dev — login disabled)
|
||||
**Estimated implement time:** ~8 min
|
||||
**Parallelizable with:** none
|
||||
|
||||
**Steps:**
|
||||
1. Rebuild the docker-dev central nodes onto this branch:
|
||||
`docker compose -f docker-dev/docker-compose.yml up -d --build central-1 central-2` (do NOT stage the compose file).
|
||||
2. Confirm healthy startup + clean deploy in the logs (`applied plan …`, OPC UA serving).
|
||||
3. Author (or seed) a Modbus equipment tag of type Int64 bound to the modbus sim (`10.100.0.35:5020`), deploy, and via Client.CLI confirm the node **advertises Int64** and a value outside the 32-bit range reads back correctly (browse the node's DataType + read). Drive the AdminUI/`/uns` if authoring through the UI.
|
||||
4. FOCAS (no CNC) and Historian (sidecar-gated on `10.100.0.48`) live gates: record honestly as unit-proven / deferred — do not fabricate a pass.
|
||||
5. Capture the evidence in the completion report.
|
||||
|
||||
---
|
||||
|
||||
## Execution notes (subagent-driven)
|
||||
|
||||
- **Dispatch order:** Tasks 1, 2, 4, 5 are mutually parallelizable (disjoint projects) — dispatch their implementers concurrently. Task 3 follows Task 2 (both edit `FocasDriver.cs`). Task 6 after 1-5; Task 7 then Task 8.
|
||||
- **Review chain by classification:** small → code-reviewer (Sonnet/Haiku); standard → spec ∥ code reviewers; Task 7 → final integration reviewer. Implementer model: opus for standard (3,4,5), sonnet for small (1,2,6).
|
||||
- Done = build clean + `dotnet test` green + final review SHIP + the Modbus live gate, then finish-a-development-branch → **merge to master + push**.
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"planPath": "docs/plans/2026-06-16-stillpending-phase-4-driver-datatypes.md",
|
||||
"branch": "feat/stillpending-phase-4-driver-datatypes",
|
||||
"tasks": [
|
||||
{"id": 433, "subject": "P4 Task 1: Modbus Int64/UInt64 node DataType (MapDataType split)", "status": "pending"},
|
||||
{"id": 434, "subject": "P4 Task 2: FOCAS fail-fast factory (EnsureUsable at init)", "status": "pending"},
|
||||
{"id": 435, "subject": "P4 Task 3: FOCAS position scaling (PositionDecimalPlaces)", "status": "pending", "blockedBy": [434]},
|
||||
{"id": 436, "subject": "P4 Task 4: Historian Total aggregate (client-side Average x interval-seconds)", "status": "pending"},
|
||||
{"id": 437, "subject": "P4 Task 5: Historian poison-event dead-letter cap (maxAttempts)", "status": "pending"},
|
||||
{"id": 438, "subject": "P4 Task 6: Docs + bookkeeping", "status": "pending", "blockedBy": [433, 434, 435, 436, 437]},
|
||||
{"id": 439, "subject": "P4 Task 7: Full build + test + final integration review", "status": "pending", "blockedBy": [433, 434, 435, 436, 437, 438]},
|
||||
{"id": 440, "subject": "P4 Task 8: Live /run — Modbus Int64 (acceptance gate)", "status": "pending", "blockedBy": [439]}
|
||||
],
|
||||
"lastUpdated": "2026-06-16"
|
||||
}
|
||||
Reference in New Issue
Block a user