fix(template-engine): resolve TemplateEngine-006..010 — code-region-aware API/brace scanning, composed-alarm override validation, N+1 fix, doc correction

This commit is contained in:
Joseph Doherty
2026-05-16 21:44:11 -04:00
parent 5672502d83
commit 804697f873
11 changed files with 832 additions and 160 deletions

View File

@@ -8,7 +8,7 @@
| Last reviewed | 2026-05-16 |
| Reviewer | claude-agent |
| Commit reviewed | `9c60592` |
| Open findings | 9 |
| Open findings | 4 |
## Summary
@@ -263,7 +263,7 @@ Regression test: `CreateTemplate_WithParent_DoesNotRunDeadCollisionQuery`.
|--|--|
| Severity | Medium |
| Category | Security |
| Status | Open |
| Status | Resolved |
| Location | `src/ScadaLink.TemplateEngine/Validation/ScriptCompiler.cs:21`, `src/ScadaLink.TemplateEngine/Validation/ValidationService.cs:318` |
**Description**
@@ -289,7 +289,20 @@ limitation prominently and treat the substring scan as advisory, not authoritati
**Resolution**
_Unresolved._
Resolved 2026-05-16 (commit `pending`): fixed the false-positive half and
documented the (deferred) bypass half. Added `CSharpDelimiterScanner.ContainsInCode`,
a code-region-aware substring search that blanks out string/char-literal/comment
spans before matching, so the inert text `System.IO.` inside a string or comment is
no longer flagged. `ScriptCompiler.TryCompile` and `ValidationService.CheckExpressionSyntax`
now use it. The bypass half (namespace aliases, `using static`, `global::`) genuinely
requires Roslyn semantic symbol analysis, which the TemplateEngine project deliberately
does not reference — that authoritative check is deferred to the real script compiler /
Site Runtime sandbox. The limitation is now documented prominently as a `SECURITY
LIMITATION` note in the `ScriptCompiler` class summary and the `ForbiddenPatterns`
doc, and the scan is explicitly labelled advisory. Regression tests:
`TryCompile_ForbiddenApiTextInsideStringLiteral_NotFlagged`,
`TryCompile_ForbiddenApiTextInsideComment_NotFlagged`,
`TryCompile_ForbiddenApiInRealCode_StillFlagged`.
### TemplateEngine-007 — Brace-balance "compilation" misjudges verbatim / interpolated / raw strings
@@ -297,7 +310,7 @@ _Unresolved._
|--|--|
| Severity | Medium |
| Category | Correctness & logic bugs |
| Status | Open |
| Status | Resolved |
| Location | `src/ScadaLink.TemplateEngine/Validation/ScriptCompiler.cs:54`, `src/ScadaLink.TemplateEngine/SharedScriptService.cs:124` |
**Description**
@@ -321,7 +334,18 @@ to something that cannot false-positive.
**Resolution**
_Unresolved._
Resolved 2026-05-16 (commit `pending`): replaced both hand-rolled string trackers
with `CSharpDelimiterScanner`, a single string-/comment-aware scanner that correctly
skips regular strings (with `\` escapes), verbatim strings (`@"..."`, `""` escape),
interpolated strings (`$"..."` / `$@"..."`, interpolation holes `{...}` treated as
code, `{{`/`}}` as escaped braces), C# 11 raw string literals (`"""..."""`), char
literals, and line/block comments while tracking `{}`/`[]`/`()` depth. `ScriptCompiler
.TryCompile` and `SharedScriptService.ValidateSyntax` now delegate to it, so a valid
script containing a delimiter inside a literal/comment is no longer falsely rejected;
genuine mismatches are still caught. Regression tests in `ScriptCompilerTests`
(`TryCompile_VerbatimStringWithBrace_*`, `_VerbatimStringWithEscapedQuote_*`,
`_InterpolatedStringWithBraces_*`, `_RawStringLiteralWithBraces_*`, `_CharLiteralWithBrace_*`,
`_GenuineMismatchedBraces_StillDetected`) and `SharedScriptServiceTests.ValidateSyntax_DelimiterInsideStringOrComment_ReturnsNull`.
### TemplateEngine-008 — `SetAlarmOverrideAsync` accepts overrides for unknown / composed alarms with no validation
@@ -329,7 +353,7 @@ _Unresolved._
|--|--|
| Severity | Medium |
| Category | Error handling & resilience |
| Status | Open |
| Status | Resolved |
| Location | `src/ScadaLink.TemplateEngine/Services/InstanceService.cs:178` |
**Description**
@@ -351,7 +375,16 @@ the lock check to composed alarms too.
**Resolution**
_Unresolved._
Resolved 2026-05-16 (commit `pending`): `SetAlarmOverrideAsync` now resolves the
instance template's full effective alarm set via `TemplateResolver.ResolveAllMembers`
(loaded from `GetAllTemplatesAsync`) instead of looking up only the template's direct
alarms. An override whose canonical name is absent from that set is rejected with a
"does not exist" failure (mirroring `SetAttributeOverrideAsync`); the `IsLocked` check
now also applies to composed (path-qualified) and inherited alarms, closing the
lock-bypass. Regression tests: `SetAlarmOverride_NonExistentAlarm_ReturnsFailure`,
`SetAlarmOverride_ComposedLockedAlarm_ReturnsFailure`,
`SetAlarmOverride_ComposedUnlockedAlarm_ReturnsSuccess`,
`SetAlarmOverride_DirectLockedAlarm_ReturnsFailure`.
### TemplateEngine-009 — N+1 query in `TemplateDeletionService.CanDeleteTemplateAsync`
@@ -359,7 +392,7 @@ _Unresolved._
|--|--|
| Severity | Medium |
| Category | Performance & resource management |
| Status | Open |
| Status | Resolved |
| Location | `src/ScadaLink.TemplateEngine/Services/TemplateDeletionService.cs:75` |
**Description**
@@ -380,15 +413,24 @@ template.
**Resolution**
_Unresolved._
Resolved 2026-05-16 (commit `pending`): `CanDeleteTemplateAsync` Check 3 now reads
the `Compositions` navigation already loaded by `GetAllTemplatesAsync` (a single
`SelectMany`) instead of issuing one `GetCompositionsByTemplateIdAsync` round-trip
per template — the same source `TemplateService.DeleteTemplateAsync` uses for the
equivalent check. The per-delete cost no longer scales with template count.
Regression test: `CanDeleteTemplate_DoesNotIssuePerTemplateCompositionQuery`
(verifies `GetCompositionsByTemplateIdAsync` is never called); the existing
`CanDeleteTemplate_ComposedByOthers_ReturnsFailure` and
`CanDeleteTemplate_MultipleConstraints_AllErrorsReported` tests were updated to seed
the `Compositions` navigation, matching how EF's `GetAllTemplatesAsync` loads it.
### TemplateEngine-010 — `InstanceService` documents optimistic concurrency that is not implemented
| | |
|--|--|
| Severity | Medium |
| Severity | Low — re-triaged from Medium: this is a stale XML comment, not a behavioural defect. The code matches the design (last-write-wins); only the doc string was wrong. |
| Category | Documentation & comments |
| Status | Open |
| Status | Resolved |
| Location | `src/ScadaLink.TemplateEngine/Services/InstanceService.cs:9` |
**Description**
@@ -407,9 +449,24 @@ If last-write-wins is acceptable for instance state, correct the XML doc. If opt
concurrency is required, add a concurrency token to `Instance` and surface a conflict
result.
**Re-triage**
Verified against the design: `docs/requirements/Component-TemplateEngine.md` states
"Concurrent editing uses **last-write-wins** — no pessimistic locking or conflict
detection." The system's optimistic-concurrency decision (per CLAUDE.md) applies to
*deployment status records*, not instance state. The code is therefore correct — a
plain read-modify-write is the intended behaviour — and the only defect is the stale
"with optimistic concurrency" phrase in the class XML summary. Re-triaged from
Medium (Error handling) to Low (Documentation): doc-only fix, no behaviour change.
**Resolution**
_Unresolved._
Resolved 2026-05-16 (commit `pending`): corrected the `InstanceService` class XML
summary — replaced "Enabled/disabled state with optimistic concurrency" with an
explicit statement that instance-state edits are last-write-wins (no version token /
conflict detection), citing the design decision and noting that optimistic concurrency
in the system applies to deployment status records, not instance state. No code or
behaviour change; no regression test (documentation-only).
### TemplateEngine-011 — `SortedPropertiesConverterFactory` is dead code with a misleading comment