diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Contracts/AbCipEquipmentTagParser.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Contracts/AbCipEquipmentTagParser.cs
index 80e1746a..2a799fcb 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Contracts/AbCipEquipmentTagParser.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Contracts/AbCipEquipmentTagParser.cs
@@ -49,9 +49,11 @@ public static class AbCipEquipmentTagParser
///
/// Resolve the 1-D array shape from an isArray / arrayLength pair (the foundation
- /// contract carrier). The tag is an ARRAY ⟺ isArray is truthy AND arrayLength
- /// is a number >= 1 (a 1-element array is valid). Returns
- /// (IsArray: false, ElementCount: 1) for a scalar.
+ /// contract carrier). The canonical rule: the tag is an ARRAY ⟺ isArray is truthy AND
+ /// arrayLength is a number >= 1. Any other combination (isArray absent/false,
+ /// or isArray:true with arrayLength missing/invalid/<1) is a SCALAR and returns
+ /// (IsArray: false, ElementCount: 1). This matches every other driver (Modbus, S7,
+ /// TwinCAT, AbLegacy) which also return scalar for that degenerate input.
///
private static (bool IsArray, int ElementCount) ReadArrayShape(JsonElement o)
{
@@ -62,8 +64,8 @@ public static class AbCipEquipmentTagParser
&& len.TryGetInt32(out var n)
&& n >= 1)
return (true, n);
- // isArray:true but arrayLength missing/invalid — treat as a 1-element array (count 1).
- return (true, 1);
+ // isArray:true but arrayLength missing/invalid/<1 — canonical rule: scalar (matches all other drivers).
+ return (false, 1);
}
private static TEnum ReadEnum(JsonElement o, string name, TEnum fallback) where TEnum : struct, Enum
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipArrayTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipArrayTests.cs
index 3ea4fb0e..f8047cd9 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipArrayTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipArrayTests.cs
@@ -201,6 +201,34 @@ public sealed class AbCipArrayTests
def.IsArray.ShouldBeFalse();
}
+ ///
+ /// Review finding I-2 — isArray:true with NO arrayLength is a DEGENERATE input
+ /// that must parse as SCALAR (IsArray false, ElementCount 1), matching every other driver
+ /// (Modbus, S7, TwinCAT, AbLegacy). The AdminUI validator blocks authoring this combination,
+ /// but the parser contract must still be consistent cross-driver.
+ ///
+ [Fact]
+ public void Parser_isArray_true_without_arrayLength_parses_as_scalar()
+ {
+ var json = """{"tagPath":"Recipe","dataType":"DInt","isArray":true}""";
+ AbCipEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
+ def!.IsArray.ShouldBeFalse();
+ def.ElementCount.ShouldBe(1);
+ }
+
+ ///
+ /// Review finding I-2 — isArray:true with an invalid (zero) arrayLength also
+ /// parses as SCALAR, consistent with the cross-driver rule.
+ ///
+ [Fact]
+ public void Parser_isArray_true_with_zero_arrayLength_parses_as_scalar()
+ {
+ var json = """{"tagPath":"Recipe","dataType":"DInt","isArray":true,"arrayLength":0}""";
+ AbCipEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
+ def!.IsArray.ShouldBeFalse();
+ def.ElementCount.ShouldBe(1);
+ }
+
///
/// Review finding I-1 — a 1-element array (isArray:true, arrayLength:1) is a valid
/// 1-element array, NOT a scalar: the parser sets