diff --git a/ZB.MOM.WW.GalaxyRepository/src/ZB.MOM.WW.GalaxyRepository/GalaxyAlarmAttributeRow.cs b/ZB.MOM.WW.GalaxyRepository/src/ZB.MOM.WW.GalaxyRepository/GalaxyAlarmAttributeRow.cs
new file mode 100644
index 0000000..ae00764
--- /dev/null
+++ b/ZB.MOM.WW.GalaxyRepository/src/ZB.MOM.WW.GalaxyRepository/GalaxyAlarmAttributeRow.cs
@@ -0,0 +1,48 @@
+namespace ZB.MOM.WW.GalaxyRepository;
+
+///
+/// One alarm-bearing attribute discovered by GalaxyRepository.GetAlarmAttributesAsync:
+/// an attribute whose owning object configures an AlarmExtension primitive (the
+/// same is_alarm detection used by
+/// ).
+/// Used to build the subtag-fallback watch-list.
+///
+public sealed class GalaxyAlarmAttributeRow
+{
+ ///
+ /// Gets the alarm-bearing attribute reference (e.g. Tank01.Level.HiHi),
+ /// matching the full_tag_reference projection of
+ /// .
+ ///
+ public string FullTagReference { get; init; } = string.Empty;
+
+ ///
+ /// Gets the owning object reference (e.g. Tank01). This is the Galaxy
+ /// tag_name — the segment that precedes the first attribute dot in
+ /// .
+ ///
+ public string SourceObjectReference { get; init; } = string.Empty;
+
+ ///
+ /// Gets the owning object's Galaxy area (e.g. TestArea) — the alarm group.
+ ///
+ /// Resolved via gobject.area_gobject_id in AlarmAttributesSql. The
+ /// watch-list resolver composes the canonical Galaxy!{area}.{reference} from
+ /// this so the synthesized reference's group matches the native alarmmgr (wnwrap)
+ /// for reference parity. May be when the object has no
+ /// area; the resolver then falls back to the configured area.
+ ///
+ ///
+ public string Area { get; init; } = string.Empty;
+
+ ///
+ /// Gets the writable ack-comment attribute address.
+ ///
+ /// The Galaxy Repository schema does not expose an ack-comment subtag address
+ /// directly, so this is always here. The watch-list
+ /// resolver (a later task) composes the concrete address from configuration plus
+ /// / .
+ ///
+ ///
+ public string AckCommentSubtag { get; init; } = string.Empty;
+}
diff --git a/ZB.MOM.WW.GalaxyRepository/src/ZB.MOM.WW.GalaxyRepository/GalaxyRepository.cs b/ZB.MOM.WW.GalaxyRepository/src/ZB.MOM.WW.GalaxyRepository/GalaxyRepository.cs
index eb6224c..9a8d34b 100644
--- a/ZB.MOM.WW.GalaxyRepository/src/ZB.MOM.WW.GalaxyRepository/GalaxyRepository.cs
+++ b/ZB.MOM.WW.GalaxyRepository/src/ZB.MOM.WW.GalaxyRepository/GalaxyRepository.cs
@@ -114,6 +114,37 @@ public sealed class GalaxyRepository(GalaxyRepositoryOptions options) : IGalaxyR
return rows;
}
+ ///
+ /// Maps the SQL columns projected by AlarmAttributesSql onto a
+ /// .
+ ///
+ /// is the alarm-bearing attribute reference (e.g.
+ /// Tank01.Level.HiHi), matching the same full_tag_reference projection
+ /// of produces.
+ /// is left empty here; the
+ /// schema does not expose an ack-comment address and the watch-list resolver
+ /// composes it later.
+ ///
+ /// is the owning object's real Galaxy area (its alarm
+ /// group), resolved via gobject.area_gobject_id; the watch-list resolver
+ /// composes the canonical reference from it so the synthesized reference's group
+ /// matches what the native alarmmgr (wnwrap) emits.
+ /// Exposed internally so the derivation can be unit-tested without a database.
+ ///
+ /// The alarm-bearing attribute reference.
+ /// The owning object reference (tag name).
+ /// The owning object's Galaxy area (the alarm group).
+ internal static GalaxyAlarmAttributeRow MapAlarmRow(
+ string fullTagReference,
+ string sourceObjectReference,
+ string area) => new()
+ {
+ FullTagReference = fullTagReference,
+ SourceObjectReference = sourceObjectReference,
+ Area = area,
+ AckCommentSubtag = string.Empty,
+ };
+
// Area objects (category 13) are returned even when undeployed (deployed_package_id = 0):
// they are organizational/model nodes that group deployed objects, so excluding them
// orphans every area whose containing area is not itself deployed. All non-area objects
diff --git a/ZB.MOM.WW.GalaxyRepository/src/ZB.MOM.WW.GalaxyRepository/ZB.MOM.WW.GalaxyRepository.csproj b/ZB.MOM.WW.GalaxyRepository/src/ZB.MOM.WW.GalaxyRepository/ZB.MOM.WW.GalaxyRepository.csproj
index 1972545..e560bd9 100644
--- a/ZB.MOM.WW.GalaxyRepository/src/ZB.MOM.WW.GalaxyRepository/ZB.MOM.WW.GalaxyRepository.csproj
+++ b/ZB.MOM.WW.GalaxyRepository/src/ZB.MOM.WW.GalaxyRepository/ZB.MOM.WW.GalaxyRepository.csproj
@@ -27,4 +27,8 @@
+
+
+
+
diff --git a/ZB.MOM.WW.GalaxyRepository/tests/ZB.MOM.WW.GalaxyRepository.Tests/GalaxyAlarmAttributeMappingTests.cs b/ZB.MOM.WW.GalaxyRepository/tests/ZB.MOM.WW.GalaxyRepository.Tests/GalaxyAlarmAttributeMappingTests.cs
new file mode 100644
index 0000000..32b52d3
--- /dev/null
+++ b/ZB.MOM.WW.GalaxyRepository/tests/ZB.MOM.WW.GalaxyRepository.Tests/GalaxyAlarmAttributeMappingTests.cs
@@ -0,0 +1,68 @@
+using ZB.MOM.WW.GalaxyRepository;
+
+namespace ZB.MOM.WW.GalaxyRepository.Tests;
+
+///
+/// Pure mapper tests for . These assert the
+/// FullTagReference / SourceObjectReference derivation produced by
+/// AlarmAttributesSql without touching a database: the SQL projects
+/// tag_name as the source object and tag_name + '.' + attribute_name as
+/// the full reference, exactly as AttributesSql does.
+///
+public sealed class GalaxyAlarmAttributeMappingTests
+{
+ /// Verifies the mapper copies all projected columns onto the row.
+ [Fact]
+ public void MapAlarmRow_CopiesProjectedColumns()
+ {
+ GalaxyAlarmAttributeRow row = GalaxyRepository.MapAlarmRow(
+ fullTagReference: "Tank01.Level.HiHi",
+ sourceObjectReference: "Tank01",
+ area: "TestArea");
+
+ Assert.Equal("Tank01.Level.HiHi", row.FullTagReference);
+ Assert.Equal("Tank01", row.SourceObjectReference);
+ Assert.Equal("TestArea", row.Area);
+ }
+
+ ///
+ /// Verifies is always empty:
+ /// the schema does not expose an ack-comment address, so the watch-list resolver
+ /// composes it later from configuration.
+ ///
+ [Fact]
+ public void MapAlarmRow_LeavesAckCommentSubtagEmpty()
+ {
+ GalaxyAlarmAttributeRow row = GalaxyRepository.MapAlarmRow(
+ fullTagReference: "Tank01.Level.HiHi",
+ sourceObjectReference: "Tank01",
+ area: "TestArea");
+
+ Assert.Equal(string.Empty, row.AckCommentSubtag);
+ }
+
+ ///
+ /// Verifies the SourceObjectReference is the owning object (the SQL tag_name),
+ /// i.e. the segment that precedes the first attribute dot in the full reference, even
+ /// when the attribute itself is a multi-segment extension path.
+ ///
+ [Theory]
+ [InlineData("Tank01", "Level.HiHi", "Tank01.Level.HiHi")]
+ [InlineData("Pump_001", "Speed", "Pump_001.Speed")]
+ [InlineData("TestAlarm001", "Alarm001", "TestAlarm001.Alarm001")]
+ public void MapAlarmRow_SourceObjectIsSegmentBeforeFirstAttributeDot(
+ string tagName,
+ string attributeName,
+ string expectedFullReference)
+ {
+ // Mirror the AlarmAttributesSql projection: full_tag_reference = tag_name + '.' + attribute_name.
+ string fullTagReference = tagName + "." + attributeName;
+
+ GalaxyAlarmAttributeRow row = GalaxyRepository.MapAlarmRow(fullTagReference, tagName, area: "TestArea");
+
+ Assert.Equal(expectedFullReference, row.FullTagReference);
+ Assert.Equal(tagName, row.SourceObjectReference);
+ Assert.Equal("TestArea", row.Area);
+ Assert.Equal(row.FullTagReference, row.SourceObjectReference + "." + attributeName);
+ }
+}