diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/ScriptAnalysis/SandboxScriptHost.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/ScriptAnalysis/SandboxScriptHost.cs
index d07eca49..1b36df80 100644
--- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/ScriptAnalysis/SandboxScriptHost.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/ScriptAnalysis/SandboxScriptHost.cs
@@ -161,6 +161,18 @@ public class SandboxAlarmsAccessor
"Alarms.CurrentAsync() reads the site Instance Actor's live alarm conditions, " +
"which aren't available in the central Test Run sandbox — deploy to a site to " +
"exercise alarm-reading scripts.");
+
+ ///
+ /// Mirrors AlarmsAccessor.MesReportableAsync for compile parity; unsupported at
+ /// run time in the central Test Run sandbox.
+ ///
+ /// Token used to cancel the read (unused in the sandbox).
+ /// Never returns; always throws in the sandbox.
+ public Task> MesReportableAsync(CancellationToken cancellationToken = default)
+ => throw new ScriptSandboxException(
+ "Alarms.MesReportableAsync() reads the site Instance Actor's live alarm conditions, " +
+ "which aren't available in the central Test Run sandbox — deploy to a site to " +
+ "exercise alarm-reading scripts.");
}
///
diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Scripts/ScriptAlarm.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Scripts/ScriptAlarm.cs
index a637e652..7fa17c31 100644
--- a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Scripts/ScriptAlarm.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Scripts/ScriptAlarm.cs
@@ -55,6 +55,10 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Scripts;
/// True for a placeholder row standing in for a CONFIGURED native source binding that
/// currently holds no conditions. Scripts enumerating real alarms should filter these out.
///
+///
+/// True when the deployed configuration flags the alarm as MES-reportable (template
+/// checkbox or instance override). Always false for native conditions.
+///
public sealed record ScriptAlarm(
string Name,
string SourceReference,
@@ -76,4 +80,5 @@ public sealed record ScriptAlarm(
DateTimeOffset? AckTime,
string CurrentValue,
string LimitValue,
- bool IsConfiguredPlaceholder);
+ bool IsConfiguredPlaceholder,
+ bool IsMesReportable = false);
diff --git a/src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/ScriptCompileSurface.cs b/src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/ScriptCompileSurface.cs
index 128e0d88..60e11667 100644
--- a/src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/ScriptCompileSurface.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/ScriptCompileSurface.cs
@@ -154,6 +154,12 @@ public sealed class ScriptCompileSurface
/// The instance's current alarm conditions.
public Task> CurrentAsync(CancellationToken cancellationToken = default)
=> throw new NotSupportedException(CompileOnly);
+
+ /// Mirrors AlarmsAccessor.MesReportableAsync.
+ /// Token used to cancel the read.
+ /// The MES-reportable subset of the instance's current alarm conditions.
+ public Task> MesReportableAsync(CancellationToken cancellationToken = default)
+ => throw new NotSupportedException(CompileOnly);
}
/// Compile-only mirror of ScriptRuntimeContext.ExternalSystemHelper.
diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScopeAccessors.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScopeAccessors.cs
index 7af567ab..884527ea 100644
--- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScopeAccessors.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScopeAccessors.cs
@@ -398,6 +398,18 @@ public class AlarmsAccessor
/// The instance's current alarm conditions; empty when it holds none.
public Task> CurrentAsync(CancellationToken cancellationToken = default)
=> _ctx.GetAlarmsAsync(cancellationToken);
+
+ ///
+ /// restricted to alarms whose deployed configuration flags them
+ /// MES-reportable (the template checkbox, or an instance override). Only computed alarms
+ /// can carry the flag — the Instance Actor stamps it from the deployed config — so native
+ /// conditions and their placeholder rows are never included. Callers still filter on
+ /// Active to get only triggered alarms.
+ ///
+ /// Cancels the wait for the actor's reply.
+ /// The MES-reportable subset of the instance's current alarm conditions.
+ public Task> MesReportableAsync(CancellationToken cancellationToken = default)
+ => _ctx.GetMesReportableAlarmsAsync(cancellationToken);
}
internal static class ScopeAccessorFactory
diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptRuntimeContext.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptRuntimeContext.cs
index 1e2c5128..8934040c 100644
--- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptRuntimeContext.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptRuntimeContext.cs
@@ -1272,6 +1272,12 @@ public class ScriptRuntimeContext
return response.Alarms.Select(ToScriptAlarm).ToList();
}
+ /// restricted to rows flagged MES-reportable.
+ /// Cancels the wait for the actor's reply.
+ /// The MES-reportable subset of the instance's current alarm conditions.
+ internal async Task> GetMesReportableAlarmsAsync(CancellationToken cancellationToken = default)
+ => (await GetAlarmsAsync(cancellationToken)).Where(a => a.IsMesReportable).ToList();
+
///
/// Projects a retained onto the flat, script-facing
/// shape. Condition is the authority for the
@@ -1301,7 +1307,8 @@ public class ScriptRuntimeContext
AckTime: a.AckTime,
CurrentValue: a.CurrentValue,
LimitValue: a.LimitValue,
- IsConfiguredPlaceholder: a.IsConfiguredPlaceholder);
+ IsConfiguredPlaceholder: a.IsConfiguredPlaceholder,
+ IsMesReportable: a.IsMesReportable);
///
/// Site-local tracking-status API for cached operations.
diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/ScriptAnalysis/ScriptAnalysisServiceTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/ScriptAnalysis/ScriptAnalysisServiceTests.cs
index 74a34efc..b70cbafa 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/ScriptAnalysis/ScriptAnalysisServiceTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/ScriptAnalysis/ScriptAnalysisServiceTests.cs
@@ -819,7 +819,9 @@ public class ScriptAnalysisServiceTests
" AckDT = a.AckTime,\n" +
" AckComment = a.OperatorComment })\n" +
" .ToList();\n" +
- "return new { WasSuccessful = true, Alarms = rows };";
+ "var r = await Alarms.MesReportableAsync();\n" +
+ "var f = r.Where(a => a.IsMesReportable).ToList();\n" +
+ "return new { WasSuccessful = true, Alarms = rows, Flagged = f.Count };";
var resp = _svc.Diagnose(new DiagnoseRequest(code));
Assert.DoesNotContain(resp.Markers, m => m.Code.StartsWith("CS"));
diff --git a/tests/ZB.MOM.WW.ScadaBridge.ScriptAnalysis.Tests/RoslynScriptCompilerTests.cs b/tests/ZB.MOM.WW.ScadaBridge.ScriptAnalysis.Tests/RoslynScriptCompilerTests.cs
index ff4deb26..b3ad0b33 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.ScriptAnalysis.Tests/RoslynScriptCompilerTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.ScriptAnalysis.Tests/RoslynScriptCompilerTests.cs
@@ -56,6 +56,8 @@ public class RoslynScriptCompilerTests
var scope = Scope;
var currentAlarms = await Alarms.CurrentAsync();
var viaInstance = await Instance.Alarms.CurrentAsync();
+ var mesReportable = await Alarms.MesReportableAsync();
+ var flaggedAlarms = mesReportable.Where(a => a.IsMesReportable).ToList();
""";
var diagnostics = RoslynScriptCompiler.Compile(code, typeof(ScriptCompileSurface));
@@ -101,7 +103,12 @@ public class RoslynScriptCompilerTests
AckComment = a.OperatorComment,
}).ToList();
- return new { WasSuccessful = true, ErrorText = (string)null, Alarms = infos };
+ // The pre-filtered accessor + the configured flag are the supported shape for
+ // "which alarms does MES care about?" — both must bind at design time.
+ var r = await Alarms.MesReportableAsync();
+ var f = r.Where(a => a.IsMesReportable).ToList();
+
+ return new { WasSuccessful = true, ErrorText = (string)null, Alarms = infos, Flagged = f.Count };
""";
Assert.Empty(RoslynScriptCompiler.Compile(code, typeof(ScriptCompileSurface)));
diff --git a/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Scripts/AlarmsAccessorTests.cs b/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Scripts/AlarmsAccessorTests.cs
index eb35797c..6ee41acf 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Scripts/AlarmsAccessorTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Scripts/AlarmsAccessorTests.cs
@@ -187,6 +187,43 @@ public class AlarmsAccessorTests : TestKit, IDisposable
Assert.Equal("Z28061.HeartbeatTimeoutAlarm", real.Name);
}
+ [Fact]
+ public async Task CurrentAsync_ProjectsIsMesReportable()
+ {
+ // The deployed configuration's MES-reportable flag rides the snapshot row and must
+ // reach the script projection verbatim — a row without it reads false.
+ var probe = CreateTestProbe();
+ var ctx = MakeContext(probe.Ref);
+
+ var pending = ctx.Alarms.CurrentAsync();
+ RespondWith(probe,
+ NativeAcked() with { IsMesReportable = true },
+ new AlarmStateChanged("CvdReactor01", "HighTemp", AlarmState.Active, 700, ObservedAt));
+
+ var alarms = await pending;
+ Assert.Equal(2, alarms.Count);
+ Assert.True(Assert.Single(alarms, a => a.Name == "Z28061.HeartbeatTimeoutAlarm").IsMesReportable);
+ Assert.False(Assert.Single(alarms, a => a.Name == "HighTemp").IsMesReportable);
+ }
+
+ [Fact]
+ public async Task MesReportableAsync_FiltersToFlaggedRows()
+ {
+ // The convenience accessor is CurrentAsync restricted to the flagged subset, so a
+ // script does not have to repeat the filter (and cannot get it subtly wrong).
+ var probe = CreateTestProbe();
+ var ctx = MakeContext(probe.Ref);
+
+ var pending = ctx.Alarms.MesReportableAsync();
+ RespondWith(probe,
+ NativeAcked() with { IsMesReportable = true },
+ NativeAcked() with { AlarmName = "Z28061.UnflaggedAlarm", SourceReference = "Z28061.UnflaggedAlarm" });
+
+ var alarm = Assert.Single(await pending);
+ Assert.Equal("Z28061.HeartbeatTimeoutAlarm", alarm.Name);
+ Assert.True(alarm.IsMesReportable);
+ }
+
[Fact]
public async Task CurrentAsync_IsNotScopePrefixed_EveryScopeSeesTheSameList()
{