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
@@ -259,6 +259,41 @@ When the Instance Actor is stopped (due to disable, delete, or redeployment), Ak
- **Can** call instance scripts via `Instance.CallScript()` — sends an ask message to the appropriate sibling Script Actor.
- Instance scripts **cannot** call alarm on-trigger scripts — the call direction is one-way.
### Audit correlation of an on-trigger run (`ParentExecutionId` tag-cascade)
An alarm fired by a **script- or inbound-API-initiated static attribute write**
is a genuine execution spawn, and the on-trigger run records the writing
execution as its parent in the central Audit Log (#23). The originating id rides
site-locally through three additive, nullable fields — no wire, proto or schema
change:
| Hop | Field |
|---|---|
| `ScriptRuntimeContext.SetAttribute` (this run's `ExecutionId`) / `RouteToSetAttributesRequest.ParentExecutionId` (the inbound request's) | → `SetStaticAttributeCommand.SourceExecutionId` |
| Instance Actor static-write path (`HandleSetStaticAttributeCore`) | → `AttributeValueChanged.SourceExecutionId` |
| Alarm Actor trigger evaluation → `SpawnAlarmExecution` | → `AlarmExecutionActor``ScriptRuntimeContext.ParentExecutionId` |
All four computed trigger types participate. `Expression` triggers evaluate a
whole attribute snapshot **off the dispatcher**, so the firing change is no
longer in scope when the boolean returns: the writer of the newest value folded
into the snapshot is captured *with* the snapshot and echoed back on the
evaluation result, so a change arriving mid-flight cannot mis-attribute the
raise.
The on-trigger run is correctly a **tree root** (`ParentExecutionId` NULL) when
the firing value has no originating execution:
- Values from the **Data Connection Layer** — external device data has no
spawning execution. This includes the confirmed value of a write to a
**data-sourced** attribute: that write is forwarded to the device and the echo
arrives on the subscription long after the writing execution ended, so only
**static** attribute writes cascade.
- Deploy-time seeding and central Test Run / tooling writes.
`AttributeValueChanged.SourceExecutionId` is deliberately **not** projected onto
the gRPC debug/stream wire shapes — the cascade is resolved inside the site node
that owns the Instance Actor, and central reads the linkage from audit rows.
---
## Native Alarm Actor