fix(audit): populate ParentExecutionId on alarm-triggered script runs

M5.4 T4 threaded a `parentExecutionId` parameter through
AlarmActor.SpawnAlarmExecution → AlarmExecutionActor → ScriptRuntimeContext,
but every call site passed null — so alarm on-trigger runs were silently always
execution-tree roots, contradicting the "tag-cascade coverage is complete"
claim in CLAUDE.md and Component-AuditLog.md.

Source the id where a spawner genuinely exists: a static attribute write issued
by a site script (`Instance.SetAttribute`) or by an inbound API request
(`Route.To(...).SetAttributes(...)`, whose ParentExecutionId was already carried
to the site and then dropped). The id rides site-locally through three additive,
nullable fields — no wire, proto or central schema change:

  ScriptRuntimeContext.SetAttribute / RouteToSetAttributesRequest.ParentExecutionId
    → SetStaticAttributeCommand.SourceExecutionId
    → AttributeValueChanged.SourceExecutionId   (InstanceActor static-write path)
    → AlarmActor.SpawnAlarmExecution → AlarmExecutionActor → ScriptRuntimeContext

All four computed trigger types participate. Expression triggers evaluate off
the dispatcher, so the writer of the newest value folded into the snapshot is
captured *with* the snapshot and echoed home on ExpressionEvalResult /
ExpressionEvalFailed — a change arriving mid-flight cannot mis-attribute the
raise.

Deliberately still roots (documented, not deferred): alarms fired by Data
Connection Layer values (external device data has no spawning execution — this
includes the device echo of a script write to a *data-sourced* attribute, so
only static writes cascade), and ScriptActor value-change/conditional/
expression/timer trigger runs (a timer tick has no spawner; a WhileTrue/interval
run has no single identifiable write).

Tests: new SiteRuntime.Tests/Actors/AlarmCascadeParentExecutionTests pins all
three hops — SetAttribute stamps the run's ExecutionId, InstanceActor publishes
it on the change (and publishes null when absent), and ValueMatch/HiLo/
Expression alarms parent the on-trigger run to the writer while a DCL-originated
change leaves it a root.

Docs: CLAUDE.md and Component-AuditLog.md corrected from "complete" to the true
behaviour; Component-SiteRuntime.md gains an "Audit correlation of an on-trigger
run" section with the hop table and the by-design root cases.
This commit is contained in:
Joseph Doherty
2026-08-01 11:21:29 -04:00
parent 88638d774a
commit 8aa6bf2270
12 changed files with 562 additions and 58 deletions
@@ -4,12 +4,28 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Instance;
/// Command to set a static attribute value on an Instance Actor.
/// Updates in-memory state and persists the override to SQLite.
/// </summary>
/// <param name="CorrelationId">Per-operation correlation id.</param>
/// <param name="InstanceUniqueName">Unique name of the target instance.</param>
/// <param name="AttributeName">Canonical name of the attribute to write.</param>
/// <param name="Value">Canonical string form of the new value.</param>
/// <param name="Timestamp">UTC timestamp of the command.</param>
/// <param name="SourceExecutionId">
/// Audit Log #23 (ParentExecutionId tag-cascade): the <c>ExecutionId</c> of the
/// execution issuing this write — a site script run
/// (<c>ScriptRuntimeContext.SetAttribute</c>) or the inbound API request behind a
/// <c>Route.To(...).SetAttributes(...)</c>. Additive and nullable; <c>null</c>
/// when the write has no audited originating execution (central Test Run,
/// deployment tooling, tests). The Instance Actor stamps it onto the resulting
/// <c>AttributeValueChanged</c> so an alarm the write trips can record it as the
/// on-trigger script run's <c>ParentExecutionId</c>.
/// </param>
public record SetStaticAttributeCommand(
string CorrelationId,
string InstanceUniqueName,
string AttributeName,
string Value,
DateTimeOffset Timestamp);
DateTimeOffset Timestamp,
Guid? SourceExecutionId = null);
/// <summary>
/// Response confirming that a static attribute was set.