From 9dfabd279f839c23fb4f623816641d2f951ac45e Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 16 Jun 2026 21:10:50 -0400 Subject: [PATCH] docs(phase4c): implementation plan + tasks.json (12 tasks, big-bang all 5 drivers) --- ...-16-stillpending-phase-4c-array-support.md | 375 ++++++++++++++++++ ...nding-phase-4c-array-support.md.tasks.json | 25 ++ 2 files changed, 400 insertions(+) create mode 100644 docs/plans/2026-06-16-stillpending-phase-4c-array-support.md create mode 100644 docs/plans/2026-06-16-stillpending-phase-4c-array-support.md.tasks.json diff --git a/docs/plans/2026-06-16-stillpending-phase-4c-array-support.md b/docs/plans/2026-06-16-stillpending-phase-4c-array-support.md new file mode 100644 index 00000000..803d54a2 --- /dev/null +++ b/docs/plans/2026-06-16-stillpending-phase-4c-array-support.md @@ -0,0 +1,375 @@ +# Phase 4c — cross-driver OPC UA array support — Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:subagent-driven-development to implement this plan task-by-task. + +**Goal:** an authored equipment tag marked `isArray` materializes as a 1-D OPC UA array variable +node and its backing driver reads + publishes array values — across Modbus, S7, AbCip, TwinCAT, +AbLegacy. + +**Architecture:** driver-agnostic plumbing core (sink contract → plan metadata → applier → authoring +UI) first, then per-driver array read fanned out across the 5 disjoint driver projects. Array-ness +rides in the existing `TagConfig` JSON blob (`isArray`/`arrayLength`) — NO EF migration. All new +fields default to scalar ⇒ byte-parity unchanged for every existing tag. + +**Tech Stack:** .NET 10, OPC UA Foundation UA-.NETStandard (`BaseDataVariableState`, `ValueRanks`, +`UInt32Collection`), Akka.NET, EF Core (read-only here), Blazor Server (AdminUI), S7.Net, libplctag +(AbCip/AbLegacy), TwinCAT ADS. + +**Design:** `docs/plans/2026-06-16-stillpending-phase-4c-array-support-design.md` (committed +`efccd8d1`). Branch `feat/stillpending-phase-4c-array-support` off master `050164b2`. + +**Hard rules (every task):** stage by explicit path, never `git add .`; never stage `sql_login.txt` +/ `src/Server/ZB.MOM.WW.OtOpcUa.Host/pki/` / `pending.md` / `current.md` / +`docker-dev/docker-compose.yml` / `stillpending.md`; no force-push / no `--no-verify`; NO EF +migration; NO wire/proto contract change (the `IOpcUaAddressSpaceSink` edit is an in-process +abstraction — fine); NO bUnit; `dangerouslyDisableSandbox: true` for all build/test/rig commands. +Concurrent sibling implementers touch disjoint projects — build ONLY your own project(s); a +half-written sibling driver breaking the whole-solution build is NOT your bug. + +**Dependency graph:** `T1 → T2 → T3`; then `{T4 ∥ T5 ∥ T6 ∥ T7 ∥ T8 ∥ T9}` (all after T1) → +`T10 → T11 → T12`. + +--- + +## Task 1: Sink contract — `EnsureVariable` array params (§A) + +**Classification:** high-risk +**Estimated implement time:** ~5 min +**Parallelizable with:** none (foundation) + +**Files:** +- Modify: `src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/IOpcUaAddressSpaceSink.cs:74` (interface) + `:107` (`NullOpcUaAddressSpaceSink`) +- Modify: `src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/DeferredAddressSpaceSink.cs:65` +- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/SdkAddressSpaceSink.cs:63` +- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs` (`EnsureVariable:1299-1349`, `CreateVariable` fallback near `:2049`) +- Test: `tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/` (the node-manager test class that covers `EnsureVariable`/`WriteValue`) + +**Step 1 — failing tests.** In the OpcUaServer test project, add: +- `EnsureVariable_isArray_creates_OneDimension_node_with_ArrayDimensions` — call + `EnsureVariable(node, parent, "arr", "Int32", writable:false, isArray:true, arrayLength:8)`; assert + the created `BaseDataVariableState` has `ValueRank == ValueRanks.OneDimension` and + `ArrayDimensions` == `[8]`. +- `EnsureVariable_scalar_default_unchanged` — existing scalar call still yields + `ValueRank == ValueRanks.Scalar` and null/empty `ArrayDimensions`. +- `WriteValue_array_roundtrips` — `EnsureVariable(..., isArray:true, arrayLength:3)` then + `WriteValue(node, new int[]{1,2,3}, Good, ts)`; assert the node's `Value` is the `int[]` and (if + the harness exposes it) reads back as an array Variant. + +**Step 2 — run, verify fail.** `dotnet test tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests --filter FullyQualifiedName~EnsureVariable` (compile error — new params). `dangerouslyDisableSandbox:true`. + +**Step 3 — implement.** Add the two trailing optional params to all four implementers: +```csharp +void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, + string dataType, bool writable, string? historianTagname = null, + bool isArray = false, uint? arrayLength = null); +``` +`DeferredAddressSpaceSink` delegates them through; `NullOpcUaAddressSpaceSink` ignores them. In +`OtOpcUaNodeManager.EnsureVariable` change the node initializer (`:1327`): +```csharp +ValueRank = isArray ? ValueRanks.OneDimension : ValueRanks.Scalar, +ArrayDimensions = isArray ? new UInt32Collection(new[] { arrayLength ?? 0u }) : null, +``` +Apply the same two lines in the `CreateVariable` fallback (near `:2049`). `ResolveBuiltInDataType` +unchanged. `WriteValue` (`:257`) unchanged. + +**Step 4 — run, verify pass.** Same filter. Then the full OpcUaServer suite. + +**Step 5 — commit.** `git add` the 4 sink files + node manager + the test file; `git commit -m "feat(opcua): EnsureVariable array params (ValueRank=OneDimension + ArrayDimensions)"`. + +--- + +## Task 2: Plan metadata — `EquipmentTagPlan.IsArray/ArrayLength` + composer + applier (§B) + +**Classification:** high-risk +**Estimated implement time:** ~5 min +**Parallelizable with:** none (blockedBy T1) + +**Files:** +- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Composer.cs` (record `:87-98`; `Compose` construction `:354-366`; add `ExtractTagArray` mirroring `ExtractTagHistorize:519`) +- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Applier.cs:214` (`SafeEnsureVariable` call + signature) +- Test: `tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/` (composer test class) + +**Step 1 — failing tests.** Composer test: a Tag whose `TagConfig` has `"isArray":true,"arrayLength":16` +produces an `EquipmentTagPlan` with `IsArray==true`, `ArrayLength==16`; absent keys ⇒ `false/null`; +malformed JSON ⇒ `false/null`. A changed-array-flag diff is a `ChangedEquipmentTags` entry (record +equality). + +**Step 2 — verify fail.** + +**Step 3 — implement.** Extend the record (trailing optional, after `HistorianTagname`): +```csharp +bool IsHistorized = false, +string? HistorianTagname = null, +bool IsArray = false, +uint? ArrayLength = null); +``` +Add `ExtractTagArray(string? tagConfig)` modeled exactly on `ExtractTagHistorize` (`:519`): parse +`TagConfig`, read `isArray` (bool; absent/non-bool/non-object/blank/malformed ⇒ false) and +`arrayLength` (uint; only when `isArray` and a number). In `Compose` (`:354`): +```csharp +var (isArray, arrayLength) = ExtractTagArray(t.TagConfig); +return new EquipmentTagPlan( + ..., + IsHistorized: isHistorized, + HistorianTagname: historianTagname, + IsArray: isArray, + ArrayLength: arrayLength); +``` +In `Phase7Applier` (`:214` + the `SafeEnsureVariable` helper `:312`): thread `tag.IsArray`, +`tag.ArrayLength` into `EnsureVariable`. (The other `SafeEnsureVariable` caller at `:267` — +VirtualTags/non-equipment — passes `isArray:false`.) + +**Step 4 — verify pass.** Composer suite. + +**Step 5 — commit.** `git add` Phase7Composer + Phase7Applier + tests; `git commit -m "feat(opcua): EquipmentTagPlan IsArray/ArrayLength + composer ExtractTagArray + applier wire-in"`. + +--- + +## Task 3: Artifact decode — byte-parity mirror (§B) + +**Classification:** high-risk +**Estimated implement time:** ~4 min +**Parallelizable with:** none (blockedBy T2) + +**Files:** +- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DeploymentArtifact.cs` (`BuildEquipmentTagPlans:404-452`; add `ExtractTagArray` mirroring its `ExtractTagHistorize:700`) +- Test: `tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/` (DeploymentArtifact tests) + +**Step 1 — failing test.** `BuildEquipmentTagPlans` decodes `isArray`/`arrayLength` from the +serialized tag JSON identically to the composer; a **byte-parity round-trip** test (compose → snapshot +JSON → decode) yields `IsArray`/`ArrayLength` equal on both sides for a tag with the keys set. + +**Step 2 — verify fail.** + +**Step 3 — implement.** Add `ExtractTagArray` modeled on this file's `ExtractTagHistorize` (`:700`). +In `BuildEquipmentTagPlans` (`:440`): +```csharp +var (isArray, arrayLength) = ExtractTagArray(tagConfig); +result.Add(new EquipmentTagPlan( + ..., + IsHistorized: isHistorized, + HistorianTagname: historianTagname, + IsArray: isArray, + ArrayLength: arrayLength)); +``` +The two `ExtractTagArray` copies (composer + artifact) MUST parse identically (byte-parity invariant). + +**Step 4 — verify pass.** DeploymentArtifact suite + the byte-parity test. + +**Step 5 — commit.** `git add` DeploymentArtifact + tests; `git commit -m "feat(runtime): decode IsArray/ArrayLength byte-parity in DeploymentArtifact"`. + +--- + +## Task 4: Authoring UI — driver-agnostic `isArray`/`arrayLength` (§C) + +**Classification:** standard +**Estimated implement time:** ~5 min +**Parallelizable with:** T5, T6, T7, T8, T9 (blockedBy T1) + +**Files:** +- Create: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagArrayConfig.cs` (mirror `TagHistorizeConfig.cs`) +- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/TagModal.razor` (add the control beside the historize control) +- Test: `tests/.../AdminUI.Tests/Uns/` (the test class covering `TagHistorizeConfig`) + +**Step 1 — failing tests.** `TagArrayConfig` merge: reading a `TagConfig` with `isArray`/`arrayLength` +populates the model; writing back preserves unknown keys + emits the two keys; clearing `isArray` +removes `arrayLength`. Validate: `arrayLength > 0` required when `isArray`. + +**Step 2 — verify fail.** + +**Step 3 — implement.** `TagArrayConfig` = pure merge helper exactly like `TagHistorizeConfig` +(`ParseOrNew` → read/write `isArray` bool + `arrayLength` uint via `TagConfigJson`, preserve-unknown). +In `TagModal.razor`, add an `IsArray` checkbox + an `ArrayLength` numeric (shown when checked), +driver-agnostic (alongside the historize fields, applied to the raw `TagConfig` for both typed +editors and the raw-JSON fallback). Client-side validation message when `isArray && arrayLength<=0`. + +**Step 4 — verify pass.** AdminUI.Tests (cores only; NO bUnit). + +**Step 5 — commit.** `git add` TagArrayConfig + TagModal.razor + tests; `git commit -m "feat(adminui): driver-agnostic isArray/arrayLength Tag-modal control"`. + +--- + +## Task 5: Modbus — String/BitInRegister array decode + resolver (§D) + +**Classification:** small +**Estimated implement time:** ~5 min +**Parallelizable with:** T4, T6, T7, T8, T9 (blockedBy T1) + +**Files:** +- Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusDriver.cs` (array decode `:482-484` throw; the equipment-tag resolver path that builds a transient `ModbusTagDefinition` — thread `arrayLength → ArrayCount`) +- Test: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests/` + +**Context:** numeric arrays already decode (`:454-481` build `int[]`/`ulong[]`/`float[]`/`double[]`). +Only String + BitInRegister throw (`:482-484`). + +**Step 1 — failing tests.** Array decode for `String` (N contiguous string blocks → `string[]`) and +`BitInRegister` (a register block → per-bit `bool[]`); the equipment-tag resolver maps `arrayLength` +into `ModbusTagDefinition.ArrayCount` so an array-config equipment tag reads an array. + +**Step 2 — verify fail.** + +**Step 3 — implement.** Replace the `:482-484` `default: throw` with `String` + `BitInRegister` +cases (loop over `count`, decode each element with the existing scalar decoder, return the typed +array). Thread `arrayLength` through the Modbus equipment-tag resolver into `ArrayCount`. + +**Step 4 — verify pass.** `dotnet test ...Driver.Modbus.Tests` (`dangerouslyDisableSandbox:true`). + +**Step 5 — commit.** `git add` ModbusDriver.cs + tests; `git commit -m "feat(modbus): String + BitInRegister array decode + equipment-tag arrayLength"`. + +--- + +## Task 6: AbCip — libplctag array read (§D) + +**Classification:** standard +**Estimated implement time:** ~5 min +**Parallelizable with:** T4, T5, T7, T8, T9 (blockedBy T1) + +**Files:** +- Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs` (`IsArray:false` sites `:948-949`, `:1002-1003`; read path; resolver) +- Test: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/` + +**Step 1 — failing tests.** Discovery flips `IsArray` for an array-typed atomic tag + UDT member; the +read path returns a CLR array (`object[]`/typed) for an array tag (against the fake libplctag +backend); resolver threads `arrayLength`. + +**Step 2 — verify fail.** + +**Step 3 — implement.** libplctag reads array elements natively — read `arrayLength` elements, box to +the element-typed array; flip the `IsArray:false` discovery hard-wires using the element count. + +**Step 4 — verify pass.** `...Driver.AbCip.Tests`. + +**Step 5 — commit.** `git commit -m "feat(abcip): 1-D array read via libplctag + IsArray discovery"`. + +--- + +## Task 7: TwinCAT — ADS array symbol read (§D) + +**Classification:** standard +**Estimated implement time:** ~5 min +**Parallelizable with:** T4, T5, T6, T8, T9 (blockedBy T1) + +**Files:** +- Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs:370-371`; `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/AdsTwinCATClient.cs` (read path) +- Test: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests/` + +**Step 1 — failing tests.** Discovery flips `IsArray` for array symbols (pre-declared + discovered); +the ADS read returns a CLR array (against the fake ADS client); resolver threads `arrayLength`. + +**Step 2 — verify fail.** + +**Step 3 — implement.** ADS reads array symbols natively — read + box to the element-typed array; +flip the `IsArray:false` discovery hard-wires from the symbol's array metadata. + +**Step 4 — verify pass.** `...Driver.TwinCAT.Tests`. + +**Step 5 — commit.** `git commit -m "feat(twincat): 1-D array symbol read via ADS + IsArray discovery"`. + +--- + +## Task 8: S7 — block-read array (§D) + +**Classification:** high-risk +**Estimated implement time:** ~5 min +**Parallelizable with:** T4, T5, T6, T7, T9 (blockedBy T1) + +**Files:** +- Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs:642` (IsArray) + the read path (block read + decode loop) + resolver +- Test: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/` + +**Step 1 — failing tests.** Discovery flips `IsArray` for an array tag; the read path reads +`arrayLength` contiguous elements (block read) and decodes them to a CLR array (against the fake S7 +backend); resolver threads `arrayLength`. + +**Step 2 — verify fail.** + +**Step 3 — implement.** Read a contiguous block sized `arrayLength × elementBytes` from the tag's +start address, decode each element with the existing per-type decoder into a typed array; flip the +`:642` `IsArray:false`. Keep the scalar path unchanged when `arrayLength` is null/1. + +**Step 4 — verify pass.** `...Driver.S7.Tests`. + +**Step 5 — commit.** `git commit -m "feat(s7): 1-D array block read + decode loop + IsArray discovery"`. + +--- + +## Task 9: AbLegacy — PCCC multi-element array read (§D, heaviest) + +**Classification:** high-risk +**Estimated implement time:** ~5 min +**Parallelizable with:** T4, T5, T6, T7, T8 (blockedBy T1) + +**Files:** +- Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs:431-438` (IsArray hard-wire) + the read path + resolver +- Test: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/` + +**Context:** a PCCC data file (e.g. `N7`) is inherently an array of up to 256 words; the driver +addresses one word at a time today. Array support = multi-element file read via libplctag +(`N7:0` reading `arrayLength` words). + +**Step 1 — failing tests.** Discovery flips `IsArray` for a multi-element file tag; the read path +reads `arrayLength` words from the file base element and returns a CLR array (against the fake +libplctag backend); resolver threads `arrayLength`. Cap at the PCCC file max (256). + +**Step 2 — verify fail.** + +**Step 3 — implement.** libplctag reads PCCC file arrays natively — read `arrayLength` elements from +the base file address, box to the element-typed array; flip the `:431-438` hard-wire. If the +addressing rework proves materially larger than estimated, STOP and surface it to the parent (do not +thrash) — the honest fallback is flag-wired + a clear `BadNotSupported` read (FOCAS-4b pattern), but +attempt the full read first. + +**Step 4 — verify pass.** `...Driver.AbLegacy.Tests`. + +**Step 5 — commit.** `git commit -m "feat(ablegacy): PCCC multi-element file array read + IsArray discovery"`. + +--- + +## Task 10: Docs + bookkeeping + +**Classification:** small +**Estimated implement time:** ~4 min +**Parallelizable with:** none (blockedBy T1–T9) + +**Files:** +- Modify: `docs/drivers/Modbus.md`, `docs/drivers/Galaxy.md`-style notes for S7/AbCip/AbLegacy/TwinCAT array support (whichever driver docs exist), a short array section (the `isArray`/`arrayLength` `TagConfig` keys + the per-driver coverage matrix + the read-only/1-D/no-write scope). +- Modify: `docs/plans/2026-06-16-stillpending-phase-4c-array-support.md.tasks.json` (bookkeeping at close-out). + +Document: the two new `TagConfig` keys; the per-driver coverage matrix (all 5 read; live-verify status +per driver); record the 3 stale-done `§2` items (Modbus-Int64-node-type, Historian-Total, +Historian-poison-dead-letter — all Phase 4); the named out-of-scope deferrals (array writes, +multi-dim, array historization). Do NOT edit `stillpending.md`. + +**Commit:** `git add` the doc files; `git commit -m "docs(phase4c): cross-driver array support + TagConfig keys + coverage matrix"`. + +--- + +## Task 11: Full build + test + final integration review + +**Classification:** standard +**Estimated implement time:** ~6 min +**Parallelizable with:** none (blockedBy T1–T10) + +`dotnet build ZB.MOM.WW.OtOpcUa.slnx` clean (`dangerouslyDisableSandbox:true`); run OpcUaServer + +Runtime + AdminUI.Tests + all 5 driver test suites green. Dispatch a **final integration reviewer** +over `git diff 050164b2..HEAD` checking: byte-parity (composer↔artifact `ExtractTagArray` identical); +defaults preserve scalar (no regression); the sink contract change is additive-only across all 4 +implementers; no EF migration / no wire-proto change / no bUnit; each driver's array read is gated on +`isArray`/`arrayLength` and the scalar path is untouched. Apply any Critical/Important findings via a +fresh implementer. + +--- + +## Task 12: Live `/run` acceptance + +**Classification:** standard +**Estimated implement time:** ~6 min +**Parallelizable with:** none (blockedBy T11) + +Rebuild docker-dev central-1 from the branch (`docker compose -f docker-dev/docker-compose.yml build central-1` +then `up -d --force-recreate central-1 traefik`, `dangerouslyDisableSandbox:true`). Drive AdminUI +`http://localhost:9200` (login disabled) via the Chrome tools: author a **Modbus array** equipment tag +(`isArray` + `arrayLength`) on the seeded `MAIN-modbus-eq` → deploy → browse via Client.CLI and +confirm the node is an array (`ValueRank`/`ArrayDimensions`) + an array value flows from the Modbus +sim (`10.100.0.35:5020`). S7 / AbCip / TwinCAT / AbLegacy are **unit-proven** (fixtures down — honest, +operator-gated live). Record results; then finish the branch (merge to master + push). diff --git a/docs/plans/2026-06-16-stillpending-phase-4c-array-support.md.tasks.json b/docs/plans/2026-06-16-stillpending-phase-4c-array-support.md.tasks.json new file mode 100644 index 00000000..e50685a8 --- /dev/null +++ b/docs/plans/2026-06-16-stillpending-phase-4c-array-support.md.tasks.json @@ -0,0 +1,25 @@ +{ + "planPath": "docs/plans/2026-06-16-stillpending-phase-4c-array-support.md", + "designPath": "docs/plans/2026-06-16-stillpending-phase-4c-array-support-design.md", + "designCommit": "efccd8d1", + "baseMaster": "050164b2", + "branch": "feat/stillpending-phase-4c-array-support", + "executionState": "PENDING — subagent-driven; T1->T2->T3 foundation serial, then {T4..T9} concurrent across disjoint projects, then T10->T11->T12.", + "scope": "Big-bang all 5 drivers (AskUserQuestion) + full AbLegacy array read (AskUserQuestion). 1-D read-surface only; array writes / multi-dim / array historization out of scope.", + "nativeTaskIds": {"1": 495, "2": 496, "3": 497, "4": 498, "5": 499, "6": 500, "7": 501, "8": 502, "9": 503, "10": 504, "11": 505, "12": 506}, + "tasks": [ + {"id": 1, "subject": "Sink contract — EnsureVariable array params", "classification": "high-risk", "status": "pending"}, + {"id": 2, "subject": "EquipmentTagPlan IsArray/ArrayLength + composer + applier", "classification": "high-risk", "status": "pending", "blockedBy": [1]}, + {"id": 3, "subject": "DeploymentArtifact decode byte-parity", "classification": "high-risk", "status": "pending", "blockedBy": [2]}, + {"id": 4, "subject": "AdminUI driver-agnostic isArray/arrayLength control", "classification": "standard", "status": "pending", "blockedBy": [1]}, + {"id": 5, "subject": "Modbus String/BitInRegister array decode + resolver", "classification": "small", "status": "pending", "blockedBy": [1]}, + {"id": 6, "subject": "AbCip libplctag array read + IsArray", "classification": "standard", "status": "pending", "blockedBy": [1]}, + {"id": 7, "subject": "TwinCAT ADS array read + IsArray", "classification": "standard", "status": "pending", "blockedBy": [1]}, + {"id": 8, "subject": "S7 block-read array + decode loop + IsArray", "classification": "high-risk", "status": "pending", "blockedBy": [1]}, + {"id": 9, "subject": "AbLegacy PCCC multi-element array read + IsArray", "classification": "high-risk", "status": "pending", "blockedBy": [1]}, + {"id": 10, "subject": "Docs + bookkeeping", "classification": "small", "status": "pending", "blockedBy": [3, 4, 5, 6, 7, 8, 9]}, + {"id": 11, "subject": "Full build + test + final integration review", "classification": "standard", "status": "pending", "blockedBy": [10]}, + {"id": 12, "subject": "Live /run acceptance + finish branch", "classification": "standard", "status": "pending", "blockedBy": [11]} + ], + "lastUpdated": "2026-06-16" +}