Add the 7 per-domain design+implementation plans (archreview/plans/) with an index, produced from the 2026-07-08 architecture review. Fix two confirmed doc drifts the review flagged (theme #5): - CLAUDE.md KNOWN LIMITATION 2: the continuous-historization historized-ref feed IS wired (AddressSpaceApplier.FeedHistorizedRefs -> UpdateHistorizedRefs -> recorder); rewrite to reflect that value-capture is code-complete and only the live end-to-end + restart-convergence verification remains. - CLAUDE.md ScriptAnalysis gating: endpoints use Roles=Administrator,Designer via RequireAuthorization, not the FleetAdmin policy.
26 KiB
Architecture Review — Core Composition Pipeline and Core Libraries
Date: 2026-07-08
Commit: 9cad9ed0 (master)
Reviewer scope:
src/Core/ZB.MOM.WW.OtOpcUa.Core(driver hosting, resilience, stability, authorization trie)src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions(driver capability interfaces,PollGroupEngine, historian seams)src/Core/ZB.MOM.WW.OtOpcUa.Commons(cluster message contracts, deferred sinks, NodeId scheme, telemetry)src/Core/ZB.MOM.WW.OtOpcUa.Configuration(EF config persistence, generation-sealed local cache, draft validation)src/Core/ZB.MOM.WW.OtOpcUa.Cluster(Akka bootstrap, role info, ServiceLevel)- Plus the address-space composition pipeline itself —
AddressSpaceComposer/AddressSpacePlanner/AddressSpaceApplier
Scope note (location drift). The review brief places the composition pipeline in
ZB.MOM.WW.OtOpcUa.Core, but since the Phase7→AddressSpace rename it lives in
src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/ (AddressSpaceComposer.cs, AddressSpacePlan.cs,
AddressSpaceApplier.cs), with its tests under tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/.
The Core project's own OpcUa/ folder now holds only the retired EquipmentNodeWalker and an
unused GenericDriverNodeManager (see U-1). The pipeline was reviewed in full regardless.
Architecture Overview
Composition pipeline (deploy → address space)
The pipeline is a clean three-stage compose/diff/apply design:
-
AddressSpaceComposer.Compose(OpcUaServer/AddressSpaceComposer.cs:290) — a pure static projection from EF config entities (UnsArea,UnsLine,Equipment,DriverInstance,Tag,VirtualTag,Script,ScriptedAlarm,Device,Namespace) into anAddressSpaceComposition: sorted lists ofUnsAreaProjection/UnsLineProjection/EquipmentNode/DriverInstancePlan/EquipmentTagPlan/EquipmentVirtualTagPlan/EquipmentScriptedAlarmPlan. Tag intent (driverFullName, native-alarm object, historize flags, array shape) is parsed out of the schemalessTag.TagConfigJSON here. VirtualTag scripts get{{equip}}token substitution and dependency-ref extraction via the sharedCommons.Types.EquipmentScriptPathshelper. Everything is ordinally sorted so the composition is deterministic — the foundation of the "byte-parity" contract with the artifact-decode mirror inRuntime/Drivers/DeploymentArtifact.cs. -
AddressSpacePlanner.Compute(OpcUaServer/AddressSpacePlan.cs:96) — a pure diff of two compositions keyed on stable logical ids, using record value-equality as the changed check (with hand-writtenEquals/GetHashCodeon the plans carryingIReadOnlyListmembers so a no-op redeploy diffs empty). Emits Added/Removed/Changed sets per entity class plus UNS folder renames.AddressSpacePlan.IsEmptyis the short-circuit gateOpcUaPublishActoruses. -
AddressSpaceApplier.Apply(OpcUaServer/AddressSpaceApplier.cs:73) — the only side-effecting stage. Decides between a full structural rebuild (any topology change) and a surgical in-place update (ISurgicalAddressSpaceSink.UpdateTagAttributes/UpdateFolderDisplayNamefor whitelisted node-irrelevant deltas — the F10b optimization), falling back to rebuild when the sink lacks the capability or any surgical call fails. After the address-space work it dispatches two non-blocking hooks: fire-and-forget historian tag provisioning (IHistorianProvisioning.EnsureTagsAsync) and the historized-ref delta feed to the continuous-historization recorder (IHistorizedTagSubscriptionSink.UpdateHistorizedRefs). SeparateMaterialise*passes (Hierarchy / EquipmentTags / VirtualTags / ScriptedAlarms / DiscoveredNodes) re-derive nodes from the composition after a rebuild; NodeIds are folder-scoped (Commons.OpcUa.EquipmentNodeIds:{equipment}/{folder}/{name}), never the driverFullName, to avoid collisions across identical machines.
The sink boundary (Commons.OpcUa.IOpcUaAddressSpaceSink) keeps the whole pipeline SDK-free:
production binds SdkAddressSpaceSink via the late-swap DeferredAddressSpaceSink (actors resolve
the wrapper at DI time, the OPC UA hosted service swaps the real sink in after StandardServer
starts); dev/tests bind the NullOpcUaAddressSpaceSink no-op.
Supporting Core libraries
Core/Hosting—DriverFactoryRegistry(driver-type → factory),DriverFactoryRegistryAdapter(v1 registry → v2IDriverFactory),DriverHost(id →IDriverlifecycle registry).Core/Resilience— Polly pipelines cached per(DriverInstanceId, HostName, Capability)(DriverResiliencePipelineBuilder), executed viaCapabilityInvoker(with the non-idempotent-write no-retry override), fanned out per host byAlarmSurfaceInvoker, observed byDriverResilienceStatusTrackerfor Admin/hosts.Core/Stability—MemoryTracking(median-baseline soft/hard breach),MemoryRecycleandScheduledRecycleScheduler(Tier C recycle viaIDriverSupervisor),WedgeDetector(demand-aware stall detection).Core/Authorization— generation-sealedPermissionTrieper(ClusterId, GenerationId)cached inPermissionTrieCache(CAS-pruned), walked byTriePermissionEvaluatoragainst per-sessionUserAuthorizationState(freshness window + fail-closed staleness ceiling).Commons— immutable record message contracts for the Akka DPS topics (deploy dispatch/ack, alertsAlarmTransitionEvent, redundancy state, admin ops, script log), strongly-typed ids (DeploymentId,RevisionHash,CorrelationId,NodeId), the deferred sink/publisher pair,EquipmentScriptPaths(shared script-path/dependency extraction), andOtOpcUaTelemetry(central Meter/ActivitySource).Configuration—OtOpcUaConfigDbContext(26 entities, logical-id unique indexes, JSON check constraints, RowVersion concurrency), the generation-sealed LiteDB fallback cache (GenerationSealedCache+ atomicCURRENTpointer, fail-closed on corruption),ResilientConfigReader(timeout→retry→cache-fallback with secret-scrubbed logging),DraftValidator(all-errors-in-one-pass pre-publish rules).Cluster—AddOtOpcUaCluster/WithOtOpcUaClusterBootstrap(Akka.Hosting bootstrap with embedded HOCON + Serilog logger wiring),ClusterRoleInfo(lock-guarded role topology snapshot fed by a subscriber actor),ServiceLevelCalculator(pure 0–255 tiering),RoleParser.
Dependency direction: Core.Abstractions (leaf) ← Configuration ← Core; Commons (leaf,
Akka+Audit packages) ← Cluster. The composer/applier in OpcUaServer reference both
Configuration (entities) and Commons (sink contracts).
Findings
1. Stability
S-1 — Applier swallows every sink failure; a deploy is reported applied even when the address space is broken — High
AddressSpaceApplier wraps every sink call in catch-all "Safe" helpers that log and continue:
SafeEnsureFolder/SafeEnsureVariable (AddressSpaceApplier.cs:612–622), SafeRebuild
(AddressSpaceApplier.cs:373–383), SafeWriteAlarmCondition/SafeMaterialiseAlarmCondition
(AddressSpaceApplier.cs:677–687). AddressSpaceApplyOutcome (AddressSpaceApplier.cs:691)
carries only Added/Removed/Changed counts and RebuildCalled — no failure count. If
RebuildAddressSpace() throws, rebuilt is still reported true (AddressSpaceApplier.cs:150–153)
and the outcome flows back to the deploy coordinator as a success; the deployment seals while the
running server holds a partially-materialised (or entirely stale) address space. Individual
EnsureVariable failures during a materialise pass likewise vanish into per-node Warnings.
The never-fail-a-deploy posture is defensible for the detached hooks (provisioning, historized-ref feed — both correctly isolated), but structural materialisation failures are the deploy's core contract.
Recommendation: add FailedNodes/RebuildFailed to AddressSpaceApplyOutcome, propagate into
the ApplyAck/DeploymentFailed decision and the audit log, and escalate SafeRebuild failure to
at least a degraded ack. Tests should assert the failure surface (the fixture sink can throw).
S-2 — PollGroupEngine.Unsubscribe blocks the calling thread up to 5 s per subscription — Medium
StopState does a synchronous task.Wait(TimeSpan.FromSeconds(5))
(Core.Abstractions/PollGroupEngine.cs:99–112). Unsubscribe is the teardown path drivers call
from OPC UA subscription-management callbacks and actor message handlers; a reader stuck in a slow
network call turns every unsubscribe into a 5-second stall of the calling thread (and N stalls when
tearing down N subscriptions serially). DisposeAsync gets this right (parallel cancel, awaited
WhenAll with one shared timeout, PollGroupEngine.cs:213–236); the single-subscription path does
not.
Recommendation: add an UnsubscribeAsync (or make Unsubscribe cancel + hand the await to a
background drain), keeping the "no callback after teardown" guarantee via the existing
LoopTask/CTS-dispose ordering.
S-3 — PollGroupEngine.Subscribe has no disposed guard — Low
Subscribe racing DisposeAsync can insert a fresh SubscriptionState after the dispose loop has
snapshotted _subscriptions.Values (PollGroupEngine.cs:73–84 vs 213–236), leaking a live poll
loop with no owner. Same pattern in DriverHost.RegisterAsync vs DisposeAsync
(Core/Hosting/DriverHost.cs:53–71 vs 92–106): a registration that wins the race after the
snapshot+clear is initialized but never shut down. Both are shutdown-window races, unlikely in
practice but cheap to close with a _disposed flag checked inside the lock / before insert.
S-4 — ScheduledRecycleScheduler catch-up recycle storm after a stall — Low
TickAsync advances _nextRecycleUtc by exactly one interval per fire
(Core/Stability/ScheduledRecycleScheduler.cs:72–84). If the host was suspended (VM pause, long
outage) across K intervals, the next K ticks each trigger a full Tier C process recycle
back-to-back. Recommendation: fast-forward _nextRecycleUtc past utcNow after a fire.
S-5 — GenericDriverNodeManager re-walk teardown is not synchronized — Low
BuildAddressSpaceAsync tears down the previous alarm forwarder and clears _alarmSinks
non-atomically (Core/OpcUa/GenericDriverNodeManager.cs:58–71); a concurrent driver alarm event
during the window between unsubscribe and re-subscribe is dropped, and two concurrent Build calls
would interleave badly. Currently moot because the class has no production caller (see U-1) — fold
this into whatever decision is made there.
Positive observations. The failure-mode engineering in Configuration/LocalCache is exemplary:
GenerationSealedCache writes temp-file + atomic File.Replace pointer swaps, fails closed on any
corruption rather than silently serving an older generation (GenerationSealedCache.cs:111–155);
LiteDbConfigCache documents and defuses two real LiteDB concurrency traps (global BsonMapper
races, find-then-insert upsert races) (LiteDbConfigCache.cs:16–36). ResilientConfigReader
correctly distinguishes SQL-command TaskCanceledException from genuine caller cancellation
(ResilientConfigReader.cs:117–138) and scrubs connection-string secrets from log messages.
PermissionTrieCache.Prune is a correct reference-equality CAS loop (PermissionTrieCache.cs:73–102).
The applier's two historian hooks are properly detached (continuation observes the task, never
.Result on a fault — AddressSpaceApplier.cs:259–286). PollGroupEngine classifies fatal
exceptions and hardens the onError callback (PollGroupEngine.cs:153–169).
2. Performance
P-1 — Compose parses each tag's TagConfig JSON four times — Medium
ExtractTagFullName, ExtractTagAlarm, ExtractTagHistorize and ExtractTagArray each do their
own JsonDocument.Parse of the same blob per tag (AddressSpaceComposer.cs:418–433 calling
:536–686). A compose over a 10k-tag fleet performs 40k JSON parses (plus the same again on the
artifact-decode side). Compose runs on every deploy and every diff baseline, not on the data hot
path, so this is a scalability tax rather than a latency bug today.
Recommendation: parse once per tag into a small TagConfigIntent record (which also collapses
the byte-parity duplication — see C-1).
P-2 — Per-operation allocations on the authorization hot path — Medium
PermissionTrie.CollectMatches allocates a fresh HashSet<string> from the session's LDAP groups
plus a List<MatchedGrant> on every call (Core/Authorization/PermissionTrie.cs:42–45), and
TriePermissionEvaluator.Authorize runs per node-operation. A recursive browse or a large
CreateMonitoredItems batch turns this into per-node garbage. UserAuthorizationState.LdapGroups is
immutable per membership refresh — the case-insensitive set can be computed once per session
version and reused.
P-3 — PollGroupEngine: one background task + independent timer per subscription, no cross-subscription batching — Medium
Each Subscribe spawns a dedicated Task.Run loop (PollGroupEngine.cs:73–84). Drivers that map
each OPC UA monitored-item group (or each poll group) to a subscription get linear task/timer
growth, and two subscriptions polling overlapping tag sets at the same interval issue duplicate
protocol reads — the engine has no interval-bucketed scheduler or read coalescing. Fine at dozens
of subscriptions; a scaling wall at hundreds per driver instance. Worth a note in the driver-facing
docs at minimum; an interval-bucketed shared loop is the structural fix if tag counts grow.
P-4 — Per-write allocation in the non-idempotent write path — Low
CapabilityInvoker.ExecuteWriteAsync builds a new options record + Dictionary per non-idempotent
write (Core/Resilience/CapabilityInvoker.cs:135–152) even though GetOrCreate keys only on
(id, host, capability) and ignores the options after the first build — the comment even cites the
"1% pipeline budget". Cache the no-retry snapshot per options generation, or key the fast path on a
precomputed pipeline.
Positive observations. Pipeline resolution is lock-free ConcurrentDictionary reads
(DriverResiliencePipelineBuilder.cs:58–71). The planner's diff is O(N) dictionary passes with
deterministic sorted outputs. The surgical-apply path exists precisely to avoid the expensive full
rebuild + subscription teardown for attribute-only edits, and its whitelist-via-with-expression
technique (AddressSpaceApplier.cs:630–663) is future-field-safe (unknown new fields force the
safe rebuild). Telemetry instruments are no-op until a listener attaches (OtOpcUaTelemetry.cs).
3. Conventions
C-1 — "Byte-parity by convention": TagConfig parsing replicated in four projects with comment-enforced sync — High
ExtractTagFullName (and friends) exist as deliberate copies in:
OpcUaServer/AddressSpaceComposer.cs:536–551(+ExtractTagAlarm/ExtractTagHistorize/ExtractTagArray/TryExtractDeviceHost)Runtime/Drivers/DeploymentArtifact.cs(the artifact-decode mirror, per its own comments)Core/OpcUa/EquipmentNodeWalker.cs:193–208Configuration/Validation/DraftValidator.cs:60–71(a fourth "small local copy, consistent with this codebase where the composer keeps its own")
Each copy carries a "MUST parse identically (byte-parity)" comment. The equality of the deployed
address space across the live-compose and artifact-decode seams — and therefore the correctness of
the diff/no-op-redeploy behavior — rests on humans keeping four JSON parsers in sync. The codebase
already demonstrated the fix once: EquipmentScriptPaths (Commons/Types/EquipmentScriptPaths.cs)
was created exactly to de-duplicate the ctx.GetTag extraction "those two seams used to duplicate".
Recommendation: move the TagConfig/DeviceConfig intent parsing into Commons (it has no EF or
SDK dependency; every current copy's host project already references Commons or could) as a single
TagConfigIntent.Parse(string) — also resolving P-1 — and keep one cross-seam parity test instead
of N replicated implementations.
C-2 — Core depends on Configuration: EF entities and enums are the domain model — Medium
ZB.MOM.WW.OtOpcUa.Core.csproj references Configuration; PermissionTrie consumes
Configuration.Enums.NodePermissions/NodeAclScopeKind (Core/Authorization/PermissionTrie.cs:1),
EquipmentNodeWalker consumes Configuration.Entities directly, and the composer's plan records
are projections of EF entities. There is no separate domain layer — persistence types are the
model. This is a deliberate, consistent choice (and the plan records do decouple the applier), but
it means: (a) any EF-driven entity change ripples straight into authorization and composition
logic; (b) Configuration (with EF Core, SqlServer, LiteDB, DataProtection packages) is pulled
into anything referencing Core. Flagging as accepted-risk to document rather than refactor —
new code should keep preferring the plan-record boundary the composer established.
C-3 — DraftValidator re-derives the NodeId scheme and hard-codes a driver-type string — Medium
ValidateNoEquipmentSignalNameCollision re-implements the folder-scoped NodeId key by hand
(Configuration/Validation/DraftValidator.cs:75–97) because Configuration cannot reference
Commons.OpcUa.EquipmentNodeIds (dependency direction) — a third copy of the
{equipment}/{folder}/{name} scheme whose drift would silently break the collision check.
ValidateGalaxyTagFullName hard-codes dtype != "GalaxyMxGateway"
(DraftValidator.cs:41–55) rather than a shared driver-type constant. Both are symptoms of the
same missing shared-contract home as C-1; a Commons-level scheme/constants type (or moving
EquipmentNodeIds down to a project Configuration can see) fixes both.
C-4 — Composer violates its own purity contract with Trace.TraceWarning — Low
The class doc says "Same inputs → same outputs, no logging" (AddressSpaceComposer.cs:280–283),
but the dangling-predicate-script skip emits Trace.TraceWarning (AddressSpaceComposer.cs:493–496)
— the only System.Diagnostics.Trace usage in a Serilog/ILogger codebase, invisible in the
production sinks. Either return skipped-alarm ids in the composition for the caller to log, or
accept an injected logger and fix the doc.
C-5 — Inconsistent duplicate-key defensiveness inside Compose — Low
deviceHostById is built with a deliberate last-wins foreach and a comment explaining why
ToDictionary (throw-on-dupe) would diverge from the decode side (AddressSpaceComposer.cs:352–364),
yet three lines later driversById/namespacesById/scriptsById use plain ToDictionary
(AddressSpaceComposer.cs:401–402, 453) — a duplicate logical id (only possible on DB-constraint
bypass) crashes the whole compose. Pick one posture; the defensive one is already argued for.
C-6 — Vestigial Akka package reference in Commons — Low
Commons.csproj references the Akka package but no Commons source uses an Akka type (only doc
comments mention actors). Harmless today because every consumer is Akka-hosted anyway, but it
contradicts Commons' role as the dependency-light contracts layer. Remove it.
C-7 — Stale scaffold-era XML docs on load-bearing classes — Low
AddressSpaceApplier's class doc still describes the F14 scaffold ("For now we record the work",
"the SDK adapter that lands in F10b will decide…" — AddressSpaceApplier.cs:7–26) although both
features shipped. DriverHost's doc promises "per-process isolation for Tier C … implemented in
Phase 2 via named-pipe RPC" (Core/Hosting/DriverHost.cs:5–10), an architecture superseded by the
out-of-repo mxaccessgw gateway. In a codebase whose XML documentation is otherwise a genuine asset
(and evidently relied on), stale top-of-class narratives are actively misleading.
Positive observations. Naming and structure are highly consistent (logical-id + RowVersion
entity pattern, Null*/Deferred* sink pattern, options records with Validate(), message
records per DPS topic). Zero TODO/HACK/FIXME/Obsolete markers across all five projects. The
DbContext matches a documented schema with an introspection compliance test
(SchemaComplianceTests). The DeferredAddressSpaceSink explicitly forwards the surgical
capability with a comment memorializing the F10b prod-inertness bug (DeferredAddressSpaceSink.cs:52–70)
— institutional memory encoded where the next person will trip.
4. Underdeveloped Areas
U-1 — Dead/dormant code retained in Core: GenericDriverNodeManager, EquipmentNodeWalker, TryParseRelayBody — Medium
GenericDriverNodeManager(Core/OpcUa/GenericDriverNodeManager.cs) has no production references — only its own test file. The production path is the composer→applier→sink chain.EquipmentNodeWalker(Core/OpcUa/EquipmentNodeWalker.cs:160–165) self-declares "retained for unit-test support only; real server deployments never invoke Walk" — ~280 lines plusIdentificationFolderBuilderand a full test suite exercising a code path that cannot run in production. ItsExtractFullNamecopy is also one of the four C-1 duplicates.EquipmentScriptPaths.TryParseRelayBody(Commons/Types/EquipmentScriptPaths.cs:164–172) serviced the relay→alias converter that was deleted when Galaxy became a standard driver (2026-06-12); only tests call it now.
Dormant code with green tests is worse than deleted code: it passes every gate while silently diverging from the live path. Recommendation: delete the walker + GDNM + relay parser (git preserves them), or, if the walker must survive as a test fixture, move it into the test project.
U-2 — The entire Tier C recycle machinery has no IDriverSupervisor implementation — Medium
MemoryRecycle, ScheduledRecycleScheduler, and DriverResilienceStatusTracker.RecordRecycle
all drive IDriverSupervisor.RecycleAsync, but a repo-wide search finds zero concrete
implementations of IDriverSupervisor (Core.Abstractions/IDriverSupervisor.cs + the two
Stability consumers are the only references). Since the out-of-process driver story moved to the
external mxaccessgw/HistorianGateway sidecars, no in-repo driver is Tier C, so the recycle path is
compiled, tested, and unreachable. Either document it as speculative infrastructure for a future
tier-migration workflow (the MemoryRecycle doc hints at one) or prune it alongside U-1.
U-3 — Continuous-historization interest set: delta feed landed, initial set still empty — Medium (known-limitation debt)
The applier now pushes add/remove historized-ref deltas per deploy
(AddressSpaceApplier.cs:310–353 → IHistorizedTagSubscriptionSink), which is the convergent
design for subsequent deploys — but per the documented KNOWN LIMITATION 2 (CLAUDE.md), the
recorder is still spawned with an empty ref set, so after a process restart the recorder historizes
nothing until the next deploy produces a delta. The seam contract
(Core.Abstractions/Historian/IHistorizedTagSubscriptionSink.cs) supports the fix; the missing
piece is a full-set replay at spawn/rebuild (e.g. have the applier feed the complete current set
on RebuildCalled, not just the diff). This is the highest-leverage half-finished feature in the
subsystem.
U-4 — Additive-only ACL model (no Deny) is a documented v2.0 gap — Low
PermissionTrie is explicitly pure-union ("no explicit Deny in v2.0",
Core/Authorization/PermissionTrie.cs:13–17): a broad cluster-root grant cannot be carved back at
a sub-scope. Acceptable for the current flat-role deployments (AdminUI roles are global by design),
but it bounds how fine-grained OT authorization can get without a trie-walk semantics change —
worth keeping visible in the security docs.
U-5 — Test coverage is strong but unevenly distributed — Low
Test method counts: Core.Tests 208, Configuration.Tests 94, Core.Abstractions.Tests 65,
Commons.Tests 45, Cluster.Tests 21. The composition pipeline itself is thoroughly covered (10+
dedicated files in tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpace*). Gaps:
ClusterRoleInfo — the only concurrency-bearing class in the Cluster project (lock + subscriber
actor + event fan-out) — has no test (Cluster.Tests covers only the three pure classes);
DriverHost dispose/register races (S-3) and the applier failure surface (S-1) are untested
because the surface doesn't exist. An Akka TestKit harness for ClusterRoleInfo leader-change
sequencing would close the riskiest untested seam.
Maturity Ratings
| Dimension | Rating (1–5) | Justification |
|---|---|---|
| Stability | 4 | Deliberate, well-commented failure-mode engineering throughout (fail-closed caches, atomic pointer swaps, detached hooks, fatal-exception classification); docked for the applier's swallow-everything success reporting (S-1) and the blocking unsubscribe (S-2). |
| Performance | 3 | Deploy-path costs are deterministic and diffed to near-zero for no-op redeploys, and the surgical-apply path avoids rebuild churn; but hot paths carry avoidable per-operation allocations (P-2), redundant JSON parsing scales 4× with tag count (P-1), and the poll engine is task-per-subscription with no coalescing (P-3). |
| Conventions | 4 | Exceptionally consistent naming, patterns, and XML docs with zero TODO debt; docked for the systemic replicate-and-comment "byte-parity" duplication (C-1/C-3) and the Core→Configuration persistence coupling (C-2). |
| Underdeveloped areas | 3 | The live pipeline is feature-complete and well-tested, but Core retains three dead/dormant code paths with green tests (U-1), a fully-built-but-unreachable Tier C recycle subsystem (U-2), and the known continuous-historization initial-set gap (U-3). |