docs(code-reviews): re-review batch 4 at 39d737e — SiteEventLogging, SiteRuntime, StoreAndForward, TemplateEngine
11 new findings: SiteEventLogging-012..014, SiteRuntime-017..019, StoreAndForward-015..017, TemplateEngine-015..016.
This commit is contained in:
@@ -5,10 +5,10 @@
|
||||
| Module | `src/ScadaLink.TemplateEngine` |
|
||||
| Design doc | `docs/requirements/Component-TemplateEngine.md` |
|
||||
| Status | Reviewed |
|
||||
| Last reviewed | 2026-05-16 |
|
||||
| Last reviewed | 2026-05-17 |
|
||||
| Reviewer | claude-agent |
|
||||
| Commit reviewed | `9c60592` |
|
||||
| Open findings | 0 |
|
||||
| Commit reviewed | `39d737e` |
|
||||
| Open findings | 2 |
|
||||
|
||||
## Summary
|
||||
|
||||
@@ -29,11 +29,30 @@ create, optimistic concurrency on instance state) are claimed but not implemente
|
||||
Themes: validation that is weaker than the design promises, and asymmetric handling
|
||||
of attributes vs. alarms vs. scripts throughout the resolve/flatten/derive paths.
|
||||
|
||||
#### Re-review 2026-05-17 (commit `39d737e`)
|
||||
|
||||
Re-reviewed the whole module against all ten checklist categories at commit
|
||||
`39d737e`. All fourteen prior findings remain closed — the batch-4 fixes
|
||||
(`bc88a36`/`804697f` and predecessors) hold up: the recursive composition walk,
|
||||
the per-slot alarm override mechanism, the code-region-aware delimiter scanner,
|
||||
and the single-source deletion-constraint logic are all correctly in place. Two
|
||||
new Medium findings surfaced, both in the composition-cascade path and both
|
||||
affecting **nested** (depth ≥ 2) compositions specifically — the same blind spot
|
||||
that produced TemplateEngine-001. **TemplateEngine-015**: `RenameCompositionAsync`
|
||||
renames only the directly slot-owned derived template, leaving cascaded inner
|
||||
derived templates with a stale dotted-path name. **TemplateEngine-016**:
|
||||
`FlatteningService` hard-codes `ScriptScope.ParentPath` to the empty string for
|
||||
every composed script regardless of nesting depth, so a script two or more
|
||||
levels deep cannot resolve `Parent.X` references to its real parent module.
|
||||
Both are limited-impact (nested compositions are the less common case and there
|
||||
is design-time visibility) but represent genuine drift from the recursive-nesting
|
||||
design promise.
|
||||
|
||||
## Checklist coverage
|
||||
|
||||
| # | Category | Examined | Notes |
|
||||
|---|----------|----------|-------|
|
||||
| 1 | Correctness & logic bugs | ✓ | Multiple real bugs: deep composed-member loss, derived alarms omitted, granularity bypass, no-op create-time collision block. |
|
||||
| 1 | Correctness & logic bugs | ✓ | Prior bugs (001–005, 013) all resolved and verified. Re-review 2026-05-17 found two new nested-composition defects: rename does not cascade (TemplateEngine-015), composed-script `ParentPath` always empty (TemplateEngine-016). |
|
||||
| 2 | Akka.NET conventions | ✓ | No actors in this module (`AddTemplateEngineActors` is an empty placeholder). Nothing to assess. |
|
||||
| 3 | Concurrency & thread safety | ✓ | Services are stateless, scoped per request; static helpers hold no mutable state. Design says template editing is last-write-wins; that is honoured. See TemplateEngine-010 re: a doc claim of optimistic concurrency that is not implemented. |
|
||||
| 4 | Error handling & resilience | ✓ | `Result<T>` used consistently; repository nulls guarded. `FlatteningService` wraps in try/catch. No store-and-forward or failover surface in this module. |
|
||||
@@ -648,3 +667,102 @@ reports all blocking reasons and uses `TemplateDeletionService`'s phrasing — t
|
||||
affected `TemplateServiceTests` delete tests were updated to the unified messages,
|
||||
and a regression test `DeleteTemplate_MultipleConstraints_ReportsAllNotJustFirst`
|
||||
verifies all three constraint categories are surfaced together.
|
||||
|
||||
### TemplateEngine-015 — `RenameCompositionAsync` does not cascade-rename nested derived templates
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Open |
|
||||
| Location | `src/ScadaLink.TemplateEngine/TemplateService.cs:680` |
|
||||
|
||||
**Description**
|
||||
|
||||
`AddCompositionAsync` builds a cascade of derived templates whose names follow a
|
||||
dotted path: composing `$Sensor` (which itself composes `$Probe` as `Probe1`)
|
||||
into `$Pump` as `TempSensor` produces `$Pump.TempSensor` **and** the nested
|
||||
`$Pump.TempSensor.Probe1` (see `CreateCascadedCompositionAsync` and the
|
||||
`AddComposition_CascadesChildCompositions` test). `RenameCompositionAsync`,
|
||||
however, renames only the **directly** slot-owned derived template:
|
||||
|
||||
```csharp
|
||||
var derived = await _repository.GetTemplateByIdAsync(composition.ComposedTemplateId, ...);
|
||||
if (derived != null && derived.IsDerived && derived.OwnerCompositionId == compositionId)
|
||||
{
|
||||
var newDerivedName = $"{owner.Name}.{newInstanceName}";
|
||||
...
|
||||
derived.Name = newDerivedName;
|
||||
await _repository.UpdateTemplateAsync(derived, ...);
|
||||
}
|
||||
```
|
||||
|
||||
There is no recursion into `derived.Compositions`. After renaming the `TempSensor`
|
||||
slot to `MainSensor`, the parent derived becomes `$Pump.MainSensor` but the
|
||||
cascaded child stays `$Pump.TempSensor.Probe1` — its name no longer reflects the
|
||||
slot path it lives under, breaking the dotted-path naming invariant the cascade
|
||||
otherwise maintains. `DeleteCompositionAsync` correctly recurses
|
||||
(`CascadeDeleteDerivedAsync`), so rename is the asymmetric outlier. The
|
||||
`RenameComposition_RenamesSlotAndDerivedTemplate` test only exercises a
|
||||
single-level derived, so the gap is untested. The stale name also breaks the
|
||||
`AddComposition_DerivedNameCollision_Fails` / cascade-name pre-check on any
|
||||
subsequent compose that walks the now-inconsistent name tree.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Recurse over `derived.Compositions` (mirroring `CascadeDeleteDerivedAsync`),
|
||||
re-deriving each cascaded child's name from the renamed parent
|
||||
(`$"{parentDerivedName}.{childComposition.InstanceName}"`), and run the
|
||||
existing same-name collision pre-check across every name the cascade will
|
||||
produce — not just the top-level one. Add a regression test covering a
|
||||
two-level cascade rename.
|
||||
|
||||
**Resolution**
|
||||
|
||||
_Unresolved._
|
||||
|
||||
### TemplateEngine-016 — Composed-script `ScriptScope.ParentPath` is always empty, breaking `Parent.X` resolution for nested modules
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Open |
|
||||
| Location | `src/ScadaLink.TemplateEngine/Flattening/FlatteningService.cs:750` |
|
||||
|
||||
**Description**
|
||||
|
||||
`ResolveComposedScriptsRecursive` assigns each composed script a `ScriptScope`:
|
||||
|
||||
```csharp
|
||||
Scope = new Commons.Types.Scripts.ScriptScope(SelfPath: prefix, ParentPath: "")
|
||||
```
|
||||
|
||||
`prefix` is the accumulated path-qualified module path (`Outer` at depth 1,
|
||||
`Outer.Inner` at depth 2, etc.), so `SelfPath` is correct. `ParentPath`, however,
|
||||
is hard-coded to the empty string at every depth. Per `ScriptScope`'s own XML
|
||||
doc, `ParentPath` is "computed at flattening time and seeded into the script's
|
||||
globals … so `Attributes["X"]` / `Parent.X` can prepend the right path-prefix."
|
||||
For a script directly composed at depth 1 the parent is the root and `""` is
|
||||
correct, but for a script in a nested module (`Outer.Inner.Foo`) the parent
|
||||
module is `Outer` — yet `ParentPath` is still `""`. A nested composed script
|
||||
that references `Parent.X` will therefore resolve the reference against the root
|
||||
flat namespace instead of its actual parent module, reading the wrong attribute
|
||||
(or failing to find one). This is the same depth-≥2 nesting blind spot as
|
||||
TemplateEngine-001; the recursive walk was added there but the `Scope`
|
||||
construction was not updated to carry the parent path. `ResolveComposedScripts`
|
||||
for direct (root-template) scripts leaves `Scope` at the default `ScriptScope.Root`,
|
||||
which is correct.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Thread the parent module path through `ResolveComposedScriptsRecursive` (the
|
||||
caller already knows it — it is the `prefix` of the enclosing recursion frame,
|
||||
or `""` for a depth-1 composition) and set
|
||||
`ParentPath` to that value, so `SelfPath = "Outer.Inner"` pairs with
|
||||
`ParentPath = "Outer"`. Add a flattening test asserting the `Scope` of a
|
||||
two-level composed script.
|
||||
|
||||
**Resolution**
|
||||
|
||||
_Unresolved._
|
||||
|
||||
Reference in New Issue
Block a user