feat(scripts): self/child/parent attribute and script accessors

Phases 1+2 of the design at
docs/plans/2026-05-12-script-scope-access-design.md.

Adds ergonomic scope-aware accessors to compiled scripts. A script
on a composed TempSensor reads its own attribute via
Attributes["Temperature"]; reaches up to the parent via
Parent.Attributes["SpeedRPM"]; invokes a child script via
Children["TempSensor"].CallScript("Sample"). All resolve to the
existing flat Instance.GetAttribute / SetAttribute / CallScript
delegates by prepending the script's canonical path prefix.

Runtime types (SiteRuntime.Scripts.ScopeAccessors):
  AttributeAccessor   sync indexer + GetAsync / SetAsync
  CompositionAccessor Attributes + CallScript
  ChildrenAccessor    Children["name"] => CompositionAccessor

ScriptGlobals gains Scope, Attributes, Children, Parent properties.
Sync indexer blocks on the Instance Actor Ask; explicit GetAsync /
SetAsync are also available for callers that want to await.

Plumbing:
  - Commons.Types.Scripts.ScriptScope record (SelfPath / ParentPath).
  - ResolvedScript.Scope (defaults to ScriptScope.Root for back-compat).
  - FlatteningService emits new ScriptScope(prefix, "") for each
    composed script so a script defined on TempSensor composed under
    a parent gets SelfPath = "TempSensor".
  - ScriptActor reads the Scope from its ResolvedScript and forwards
    it through ScriptExecutionActor into ScriptGlobals on each call.

RevisionHashService not touched: the per-script canonical name
already encodes the composition path, so any structural change
already flips the hash.

10 new unit tests on the path arithmetic. Site/Template engine
suites stay green (129 + 199).

Editor surface (Phase 3: metadata fetch, Phase 4: completion +
SCADA006 / SCADA007 diagnostics) follows in the next commits.
This commit is contained in:
Joseph Doherty
2026-05-12 05:45:24 -04:00
parent 3ed05f0595
commit efba01d10a
8 changed files with 258 additions and 3 deletions

View File

@@ -126,4 +126,12 @@ public sealed record ResolvedScript
public TimeSpan? MinTimeBetweenRuns { get; init; }
public string Source { get; init; } = "Template";
/// <summary>
/// Where the script sits in the composition tree. Seeded into ScriptGlobals
/// so the script's <c>Attributes</c> / <c>Children</c> / <c>Parent</c>
/// accessors resolve canonical names with the right path-prefix.
/// </summary>
public ScadaLink.Commons.Types.Scripts.ScriptScope Scope { get; init; }
= ScadaLink.Commons.Types.Scripts.ScriptScope.Root;
}

View File

@@ -0,0 +1,17 @@
namespace ScadaLink.Commons.Types.Scripts;
/// <summary>
/// Where a compiled script sits in the composition tree. Computed at
/// flattening time and seeded into the script's globals at execution time
/// so <c>Attributes["X"]</c> / <c>Parent.X</c> can prepend the right
/// path-prefix when delegating to the existing flat
/// <c>Instance.GetAttribute</c> / <c>Instance.SetAttribute</c> /
/// <c>Instance.CallScript</c> APIs.
/// </summary>
public sealed record ScriptScope(string SelfPath, string? ParentPath)
{
/// <summary>Scope for a script directly on the root template (no compositions).</summary>
public static readonly ScriptScope Root = new("", null);
public bool HasParent => ParentPath != null;
}