docs(historian): Phase C HistoryRead implementation plan + tasks
This commit is contained in:
@@ -0,0 +1,300 @@
|
||||
# Galaxy Phase C — Server-side HistoryRead — Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: use superpowers-extended-cc:subagent-driven-development (or executing-plans) to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Answer OPC UA HistoryRead (Raw / Processed / AtTime / Events) for historized equipment
|
||||
tags by dispatching to the existing `IHistorianDataSource` (Wonderware TCP client). Authoring via a
|
||||
`"isHistorized"` flag in the existing `TagConfig` blob — **no UI, no EF migration**.
|
||||
|
||||
**Architecture:** Carry `isHistorized`/`historianTagname` through the Phase-B `alarm`-object seam
|
||||
(`TagConfig` → `EquipmentTagPlan` → byte-parity artifact → `EnsureVariable`), set `Historizing` +
|
||||
`AccessLevels.HistoryRead`, register a NodeId→tagname map, and override `CustomNodeManager2`'s
|
||||
HistoryRead surface to block-bridge onto the async data source. Config-gated DI mirrors
|
||||
`AddAlarmHistorian`; the Host supplies the Wonderware client.
|
||||
|
||||
**Tech Stack:** .NET 10, OPC Foundation `Opc.Ua.Server` (`CustomNodeManager2`), Akka-free node
|
||||
manager, xUnit + Shouldly.
|
||||
|
||||
**Design:** `docs/plans/2026-06-14-galaxy-phase-c-historian-design.md` (read for the locked decisions).
|
||||
|
||||
---
|
||||
|
||||
## Execution notes (hard rules — every task)
|
||||
|
||||
- **Branch:** `feat/galaxy-phase-c-historian` (already created off master `c9a0f627`). Do NOT work on
|
||||
master.
|
||||
- **Stage by path; never `git add .`.** Never stage `sql_login.txt`,
|
||||
`src/Server/ZB.MOM.WW.OtOpcUa.Host/pki/`, `pending.md`, `current.md`, or
|
||||
`docker-dev/docker-compose.yml`. Never echo the gateway API key or historian `SharedSecret` into a
|
||||
tracked file. No force-push, no `--no-verify`.
|
||||
- **NO EF/Configuration entity or migration change.** The flag rides in `TagConfig` JSON.
|
||||
- **NO bUnit** — there is no UI in Phase C; everything here is offline-unit-testable. The live `/run`
|
||||
is deferred (Task 7).
|
||||
- Production projects are `TreatWarningsAsErrors` — keep the build at **0 warnings** in `src/`.
|
||||
- Each task: write the failing test, see it fail, implement minimally, see it pass, then commit by path.
|
||||
|
||||
---
|
||||
|
||||
### Task 0: Feature branch (already created)
|
||||
|
||||
**Classification:** trivial · **Estimated implement time:** ~0 min · **Parallelizable with:** none
|
||||
|
||||
Precondition met — `feat/galaxy-phase-c-historian` exists off master `c9a0f627`, design committed
|
||||
`5e42c1bc`. No action.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Carry `isHistorized` + `historianTagname` (composer + artifact, byte-parity)
|
||||
|
||||
**Classification:** standard · **Estimated implement time:** ~5 min · **Parallelizable with:** none
|
||||
|
||||
Mirror the Phase B `alarm`-object carrier exactly.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Composer.cs`
|
||||
- `EquipmentTagPlan` record (`:82-91`): add `bool IsHistorized` and `string? HistorianTagname` as
|
||||
the last two positional fields (update the doc-comment to mention they parse identically on the
|
||||
artifact side, like `Alarm`).
|
||||
- Add `internal static (bool IsHistorized, string? HistorianTagname) ExtractTagHistorize(string?
|
||||
tagConfig)` next to `ExtractTagAlarm` (`:461`): parse `isHistorized` (bool; absent/non-bool ⇒
|
||||
false) and `historianTagname` (string; absent/blank/non-string ⇒ null). Never throws (catch
|
||||
`JsonException` ⇒ `(false, null)`). Carry the **byte-parity** comment naming
|
||||
`DeploymentArtifact.ExtractTagHistorize`.
|
||||
- In the `equipmentTags` `.Select(...)` (`:331-340`): set `IsHistorized` / `HistorianTagname` from
|
||||
`ExtractTagHistorize(t.TagConfig)`.
|
||||
- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DeploymentArtifact.cs`
|
||||
- Add a **byte-parity** `private static (bool, string?) ExtractTagHistorize(string? tagConfig)` next
|
||||
to `ExtractTagAlarm` (`:658`), same parse, carrying the byte-parity comment naming the composer.
|
||||
- In `BuildEquipmentTagPlans` (`:440-449`): thread the two new fields onto the `new
|
||||
EquipmentTagPlan(...)`.
|
||||
- Test: `tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/ExtractTagHistorizeTests.cs` (new; model on
|
||||
`ExtractTagAlarmTests.cs`).
|
||||
- Test: `tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DeploymentArtifactHistorizeParityTests.cs`
|
||||
(new; model on `DeploymentArtifactAliasParityTests.cs`).
|
||||
|
||||
**Steps:**
|
||||
1. Failing test `ExtractTagHistorizeTests`: `{"FullName":"T.A","isHistorized":true}` ⇒ `(true,null)`;
|
||||
`+"historianTagname":"WW.Tag"` ⇒ `(true,"WW.Tag")`; absent ⇒ `(false,null)`; `"isHistorized":false`
|
||||
⇒ `(false,null)`; malformed JSON / blank-tagname ⇒ `(false,null)` / `(true,null)`; never throws.
|
||||
2. Run → fail (method missing). Implement `ExtractTagHistorize` + record fields + Select wiring. Run →
|
||||
pass.
|
||||
3. Failing test `DeploymentArtifactHistorizeParityTests`: build a snapshot with a historized tag (and
|
||||
one with an override tagname), run it through `ConfigComposer`→artifact→`DeploymentArtifact`
|
||||
decode, and assert the decoded `EquipmentTagPlan.IsHistorized`/`HistorianTagname` equal the
|
||||
composer's for the same input (parity). Run → fail. Implement the artifact side. Run → pass.
|
||||
4. `dotnet build` clean (0 warn); `dotnet test` the two projects green. Commit by path
|
||||
(`Phase7Composer.cs`, `DeploymentArtifact.cs`, the two new test files).
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Materialize historized — `Historizing` + `HistoryRead` bit + NodeId→tagname registry
|
||||
|
||||
**Classification:** standard · **Estimated implement time:** ~5 min · **Parallelizable with:** none
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/IOpcUaAddressSpaceSink.cs` — `EnsureVariable`
|
||||
signature gains a trailing `string? historianTagname` param (doc: null ⇒ not historized). Update the
|
||||
`NullOpcUaAddressSpaceSink` no-op impl in the same file.
|
||||
- Modify: `src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/DeferredAddressSpaceSink.cs` — pass the new param
|
||||
through.
|
||||
- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/SdkAddressSpaceSink.cs` — pass-through.
|
||||
- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Applier.cs` — `SafeEnsureVariable` (`:294`)
|
||||
gains the param; `MaterialiseEquipmentTags` (`:200`) computes
|
||||
`string? historianTagname = tag.IsHistorized ? (string.IsNullOrWhiteSpace(tag.HistorianTagname) ?
|
||||
tag.FullName : tag.HistorianTagname) : null;` and passes it.
|
||||
- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs`
|
||||
- Add `private readonly ConcurrentDictionary<string,string> _historizedTagnames = new(StringComparer.Ordinal);`
|
||||
- `EnsureVariable` (`:820`): take `string? historianTagname`; when non-null set `Historizing = true`,
|
||||
OR `AccessLevels.HistoryRead` into both `AccessLevel`+`UserAccessLevel` (keep the writable
|
||||
ReadWrite composite), and `_historizedTagnames[variableNodeId] = historianTagname`. When null,
|
||||
behavior is exactly as today (`Historizing=false`).
|
||||
- `RebuildAddressSpace` (`:889`): `_historizedTagnames.Clear();` alongside the other clears.
|
||||
- Test: `tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/SdkAddressSpaceSinkTests.cs` (extend) — or a
|
||||
new `NodeManagerHistorizeTests.cs` if cleaner. Reuse the node-manager harness in `SdkAddressSpaceSinkTests`
|
||||
/ `EquipmentWriteGateTests`.
|
||||
|
||||
**Steps:**
|
||||
1. Failing test: materialize a variable with `historianTagname:"WW.Tag"` ⇒ the node's `Historizing`
|
||||
is true and `(AccessLevel & AccessLevels.HistoryRead) != 0`; a null tagname ⇒ `Historizing` false,
|
||||
no History bit. (Expose a read path: the existing tests already reach `BaseDataVariableState` via
|
||||
the node manager — assert on that, or add a small `TryGetVariable` test accessor if none exists.)
|
||||
2. Run → fail. Thread the param through all five seam files + node manager. Run → pass.
|
||||
3. Failing test (applier): a historized `EquipmentTagPlan` with no override ⇒ the sink receives
|
||||
`historianTagname == FullName`; with an override ⇒ receives the override. (Use a recording fake
|
||||
`IOpcUaAddressSpaceSink`.) Run → fail → implement the applier resolve → pass.
|
||||
4. Build clean; the `OpcUaServer.Tests` suite green (existing `EnsureVariable` callers updated for the
|
||||
new param). Commit by path.
|
||||
|
||||
---
|
||||
|
||||
### Task 3: HistoryRead override — Raw / Processed / AtTime + `NullHistorianDataSource` + wiring prop
|
||||
|
||||
**Classification:** high-risk · **Estimated implement time:** ~6 min · **Parallelizable with:** none
|
||||
|
||||
This is the load-bearing OPC UA service-level task.
|
||||
|
||||
**Files:**
|
||||
- Create: `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/Historian/NullHistorianDataSource.cs` —
|
||||
singleton `Instance`; all reads return empty `HistoryReadResult`/`HistoricalEventsResult` (empty
|
||||
list, null continuation); `GetHealthSnapshot()` returns a disabled snapshot; `Dispose()` no-op.
|
||||
- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs`
|
||||
- Add `private volatile IHistorianDataSource _historianDataSource = NullHistorianDataSource.Instance;`
|
||||
+ `public IHistorianDataSource HistorianDataSource { get => _historianDataSource; set =>
|
||||
_historianDataSource = value ?? NullHistorianDataSource.Instance; }` (doc mirrors `NodeWriteGateway`).
|
||||
- **Confirm the override surface** of the installed `OPCFoundation.NetStandard.Opc.Ua.Server` first:
|
||||
query the DeepWiki MCP (`OPCFoundation/UA-.NETStandard`, per CLAUDE.md) AND inspect the actual
|
||||
base members (e.g. `grep`/decompile the referenced `Opc.Ua.Server.CustomNodeManager2` for
|
||||
`HistoryRead`). RECOMMENDED: override the per-details protected virtuals
|
||||
(`HistoryReadRawModified`, `HistoryReadProcessed`, `HistoryReadAtTime`); FALLBACK: override the
|
||||
single public `HistoryRead(...)` and switch on `details`. Whatever the chosen surface, do **not**
|
||||
invoke base for nodes we own (`_historizedTagnames` hit); mark `Processed`/fill `results[i]`.
|
||||
- Per node: resolve `NodeId.Identifier` (string) in `_historizedTagnames`; miss ⇒
|
||||
`BadHistoryOperationUnsupported`. On hit, dispatch to `_historianDataSource` (block with
|
||||
`.GetAwaiter().GetResult()` in `try`/`catch` → per-node `BadHistoryOperationUnsupported`/
|
||||
`BadUnexpectedError` + log; never fault the batch). Map `DataValueSnapshot` →
|
||||
`Opc.Ua.DataValue`; wrap in `HistoryData`; `results[i].HistoryData = new ExtensionObject(data)`;
|
||||
`StatusCode = samples.Count == 0 ? GoodNoData : Good`; `ContinuationPoint = null`. Handle
|
||||
`releaseContinuationPoints == true` ⇒ `Good` no-op.
|
||||
- `MapAggregate(NodeId)` helper → `HistoryAggregateType`; unknown ⇒ `BadAggregateNotSupported`.
|
||||
- `DataValueSnapshot→DataValue` mapping helper (value Variant, StatusCode, source+server ts).
|
||||
- Test: `tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerHistoryReadTests.cs` (new) — a
|
||||
recording/fake `IHistorianDataSource`.
|
||||
- Test: `tests/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests/Historian/NullHistorianDataSourceTests.cs`
|
||||
(new).
|
||||
|
||||
**Steps:**
|
||||
1. Confirm the override surface (DeepWiki + assembly). Write `NullHistorianDataSource` + its test
|
||||
(empty results). Run → pass.
|
||||
2. Failing test (Raw): materialize a historized node; set a fake source returning 2 samples; invoke
|
||||
the HistoryRead override with a `ReadRawModifiedDetails` + one `HistoryReadValueId` for the node;
|
||||
assert the fake got `(tagname == FullName, start, end, numValues)`, the result decodes to a
|
||||
`HistoryData` of 2 `DataValue`s with mapped value/status/timestamps, `StatusCode == Good`.
|
||||
3. Run → fail. Add the prop + override + Raw path + mappers. Run → pass.
|
||||
4. Add tests + impl for: Processed (aggregate mapping, `BadAggregateNotSupported` on unknown), AtTime
|
||||
(request times passed through), default-vs-override tagname, empty ⇒ `GoodNoData`, non-historized
|
||||
node ⇒ `BadHistoryOperationUnsupported`, backend-throw ⇒ per-node bad + batch survives.
|
||||
5. Build clean; `OpcUaServer.Tests` + `Core.Abstractions.Tests` green. Commit by path.
|
||||
|
||||
---
|
||||
|
||||
### Task 4: HistoryRead override — Events over equipment-folder notifiers
|
||||
|
||||
**Classification:** high-risk · **Estimated implement time:** ~6 min · **Parallelizable with:** Task 5
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs`
|
||||
- Add `private readonly ConcurrentDictionary<string,string> _eventNotifierSources = new(StringComparer.Ordinal);`
|
||||
- `EnsureFolderIsEventNotifier` (`:749`): when `_historianDataSource is not NullHistorianDataSource`,
|
||||
OR `EventNotifiers.HistoryRead` into `folder.EventNotifier` (keep `SubscribeToEvents`); register
|
||||
`_eventNotifierSources[folder.NodeId.Identifier?.ToString()] = folder.NodeId.Identifier?.ToString()`
|
||||
(the equipment id = `sourceName`). (Reads the data-source field — fine; it's set before
|
||||
materialization in the Host. If a deployment materializes before wiring, the bit is simply absent
|
||||
until the next rebuild — acceptable; document it.)
|
||||
- `RebuildAddressSpace`: `_eventNotifierSources.Clear();`.
|
||||
- Events dispatch (the `ReadEventDetails` arm of the chosen override surface): resolve the target
|
||||
folder NodeId in `_eventNotifierSources` (miss ⇒ `BadHistoryOperationUnsupported`); call
|
||||
`_historianDataSource.ReadEventsAsync(sourceName, start, end, maxEvents, ct)` (block + guard);
|
||||
project each `HistoricalEvent` → `HistoryEventFieldList` per `ReadEventDetails.Filter.SelectClauses`
|
||||
— support `BaseEventType` operands `EventId`, `SourceName`, `Time`, `Message`, `Severity`; an
|
||||
unsupported operand ⇒ `null` field; wrap in `HistoryEvent { Events = … }`; `StatusCode = Good`
|
||||
(or `GoodNoData` when empty).
|
||||
- `ProjectEventField(HistoricalEvent, SimpleAttributeOperand)` helper.
|
||||
- Test: extend `NodeManagerHistoryReadTests.cs` — events arm.
|
||||
|
||||
**Steps:**
|
||||
1. Failing test: promote a folder to a notifier with a wired (fake non-Null) source; invoke the
|
||||
override with a `ReadEventDetails` (a select clause for EventId/SourceName/Time/Message/Severity)
|
||||
on the folder; assert the fake got `(sourceName == equipmentId, …)` and each returned
|
||||
`HistoryEventFieldList` carries the projected fields in select-clause order; an unsupported operand
|
||||
⇒ null field; empty ⇒ `GoodNoData`; a non-notifier node ⇒ `BadHistoryOperationUnsupported`.
|
||||
2. Run → fail. Implement the `_eventNotifierSources` map + folder-promotion bit + the events arm +
|
||||
projector. Run → pass.
|
||||
3. Build clean; `OpcUaServer.Tests` green. Commit by path.
|
||||
|
||||
---
|
||||
|
||||
### Task 5: DI + config + Host wiring (`AddServerHistorian`, `SetHistorianDataSource`)
|
||||
|
||||
**Classification:** standard · **Estimated implement time:** ~5 min · **Parallelizable with:** Task 4
|
||||
|
||||
**Files:**
|
||||
- Create: `src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Historian/ServerHistorianOptions.cs` — `SectionName =
|
||||
"ServerHistorian"`; `Enabled`, `Host`, `Port`, `UseTls`, `ServerCertThumbprint`, `SharedSecret`;
|
||||
`Validate()` warnings mirroring `AlarmHistorianOptions` (empty SharedSecret, etc.).
|
||||
- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ServiceCollectionExtensions.cs`
|
||||
- `AddOtOpcUaRuntime`: `services.TryAddSingleton<IHistorianDataSource>(NullHistorianDataSource.Instance);`
|
||||
- Add `AddServerHistorian(this IServiceCollection, IConfiguration, Func<ServerHistorianOptions,
|
||||
IServiceProvider, IHistorianDataSource> factory)` — bind the section; `Enabled != true` ⇒ no-op
|
||||
(Null stays); else log `Validate()` warnings + `AddSingleton<IHistorianDataSource>(sp =>
|
||||
factory(opts, sp))`. Mirror `AddAlarmHistorian`.
|
||||
- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaSdkServer.cs` — add
|
||||
`public bool SetHistorianDataSource(IHistorianDataSource? source)` mirroring `SetNodeWriteGateway`.
|
||||
- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs` — after `AddAlarmHistorian` (`:94`), add
|
||||
`AddServerHistorian(config, (opts,sp) => new WonderwareHistorianClient(new
|
||||
WonderwareHistorianClientOptions(opts.Host, opts.Port, opts.SharedSecret){ UseTls=opts.UseTls,
|
||||
ServerCertThumbprint=opts.ServerCertThumbprint }, sp.GetService<ILogger<WonderwareHistorianClient>>()))`.
|
||||
- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.Host/OpcUa/OtOpcUaServerHostedService.cs` — after
|
||||
`SetNodeWriteGateway` (`:147`), resolve `IHistorianDataSource` from DI and
|
||||
`_server.SetHistorianDataSource(source)`; in `StopAsync` (`:166`) `_server?.SetHistorianDataSource(null)`.
|
||||
(The hosted service must be able to resolve `IHistorianDataSource` — inject it or pull from the
|
||||
provider exactly as the other sinks are obtained.)
|
||||
- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.Host/appsettings.json` — add a disabled `ServerHistorian`
|
||||
block (documented defaults). **Do not** put a real `SharedSecret` in a tracked file (leave `""`).
|
||||
- Test: `tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Historian/AddServerHistorianTests.cs` (new;
|
||||
model on the existing `AddAlarmHistorian` DI test) — disabled ⇒ resolves `NullHistorianDataSource`;
|
||||
enabled ⇒ resolves the factory's source.
|
||||
|
||||
**Steps:**
|
||||
1. Failing DI test: `ServerHistorian:Enabled=false` ⇒ `IHistorianDataSource` is the Null instance;
|
||||
`Enabled=true` ⇒ the factory's fake. Run → fail.
|
||||
2. Implement `ServerHistorianOptions` + `AddServerHistorian` + the `TryAddSingleton` default + the
|
||||
`SetHistorianDataSource` setter. Run → pass.
|
||||
3. Wire the Host (`Program.cs` + hosted service + appsettings). Build the **Host** project to prove
|
||||
the Wonderware factory + DI compile (no unit test for the Host wiring — it's covered by the live
|
||||
gate). Build clean.
|
||||
4. Commit by path (do NOT stage appsettings.json secrets — the block is disabled with empty secret).
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Docs + bookkeeping
|
||||
|
||||
**Classification:** small · **Estimated implement time:** ~4 min · **Parallelizable with:** none
|
||||
|
||||
**Files:**
|
||||
- Create: `docs/Historian.md` — the historized-tag `TagConfig` schema (`isHistorized`,
|
||||
`historianTagname` default=FullName), the `ServerHistorian` config section, HistoryRead behavior
|
||||
(the four variants, graceful degrade to `GoodNoData`, the single-shot/no-continuation limitation,
|
||||
events-on-equipment-folders), and a `Client.CLI historyread` example.
|
||||
- Modify: `CLAUDE.md` — a short pointer under a Historian/HistoryRead note (one paragraph + the doc link).
|
||||
- Modify (disk-only, **do NOT commit**): `pending.md` — mark #2 Phase C done with the merge SHA +
|
||||
the deferred live gate.
|
||||
- Update: the plan's `.tasks.json` statuses.
|
||||
|
||||
**Steps:** Write the docs; commit `docs/Historian.md` + `CLAUDE.md` by path. Update `pending.md` on
|
||||
disk only (never staged).
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Live docker-dev `/run` verification — DEFERRED (user-driven)
|
||||
|
||||
**Classification:** n/a (live) · **Parallelizable with:** none · **Depends on:** Task 5
|
||||
|
||||
**Deferred — the agent does NOT sign in and the local docker-dev rig has no Wonderware sidecar.** The
|
||||
historian backend is the WW Historian VM (`10.100.0.48`) + AVEVA, so this gate is operator-driven.
|
||||
When run: author a historized Galaxy equipment tag (`TagConfig` `"isHistorized":true`), set
|
||||
`ServerHistorian:Enabled=true` pointed at the sidecar (`Host`/`Port`/`SharedSecret`/TLS), deploy on
|
||||
`MAIN-galaxy-eq` (`POST http://localhost:9200/api/deployments`, `X-Api-Key: docker-dev-deploy-key`),
|
||||
then `Client.CLI historyread -u opc.tcp://localhost:4840 -n "ns=2;s=<equip>/<tag>" --start … --end …`
|
||||
→ samples return; a non-historized tag ⇒ `BadHistoryOperationUnsupported`.
|
||||
|
||||
---
|
||||
|
||||
## Dependency graph
|
||||
|
||||
`0 → 1 → 2 → 3 → (4 ∥ 5) → 6 → 7(deferred)`. Tasks 4 and 5 touch disjoint files
|
||||
(node-manager events arm vs. DI/Host/SdkServer) and may be dispatched concurrently after Task 3.
|
||||
|
||||
## Done
|
||||
|
||||
Build clean (0 warnings in `src/`) + `dotnet test` green (new + existing suites) for Tasks 1–6. The
|
||||
live `/run` (Task 7) is the deferred user-driven gate.
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"planPath": "docs/plans/2026-06-14-galaxy-phase-c-historian-plan.md",
|
||||
"tasks": [
|
||||
{"id": 0, "subject": "Task 0: Feature branch feat/galaxy-phase-c-historian (created)", "status": "completed"},
|
||||
{"id": 1, "subject": "Task 1: Carry isHistorized + historianTagname (composer + artifact byte-parity)", "status": "pending", "blockedBy": [0]},
|
||||
{"id": 2, "subject": "Task 2: Materialize historized — Historizing + HistoryRead bit + NodeId->tagname registry", "status": "pending", "blockedBy": [1]},
|
||||
{"id": 3, "subject": "Task 3: HistoryRead override — Raw/Processed/AtTime + NullHistorianDataSource + prop", "status": "pending", "blockedBy": [2]},
|
||||
{"id": 4, "subject": "Task 4: HistoryRead override — Events over equipment-folder notifiers", "status": "pending", "blockedBy": [3]},
|
||||
{"id": 5, "subject": "Task 5: DI + config + Host wiring (AddServerHistorian, SetHistorianDataSource)", "status": "pending", "blockedBy": [3]},
|
||||
{"id": 6, "subject": "Task 6: Docs + bookkeeping", "status": "pending", "blockedBy": [4, 5]},
|
||||
{"id": 7, "subject": "Task 7: Live docker-dev /run verification — DEFERRED (user-driven)", "status": "pending", "blockedBy": [5]}
|
||||
],
|
||||
"lastUpdated": "2026-06-14"
|
||||
}
|
||||
Reference in New Issue
Block a user