diff --git a/archreview/plans/R2-11-tagconfig-consolidation-plan.md.tasks.json b/archreview/plans/R2-11-tagconfig-consolidation-plan.md.tasks.json index b38dcc59..32a9a4cc 100644 --- a/archreview/plans/R2-11-tagconfig-consolidation-plan.md.tasks.json +++ b/archreview/plans/R2-11-tagconfig-consolidation-plan.md.tasks.json @@ -8,7 +8,7 @@ { "id": "T4", "subject": "DeviceConfigIntent (TryExtractHost/NormalizeHost) in Commons/Types (TDD)", "status": "completed", "blockedBy": [] }, { "id": "T5", "subject": "AddressSpaceComposer swap: single TagConfigIntent.Parse per tag, delete 4 Extract* statics (P-1 parse-once)", "status": "completed", "blockedBy": ["T1", "T3"] }, { "id": "T6", "subject": "Device-host re-home: composer TryExtractDeviceHost/NormalizeDeviceHost → DeviceConfigIntent; update DriverHostActor + DeploymentArtifact callers", "status": "completed", "blockedBy": ["T4", "T5"] }, - { "id": "T7", "subject": "DeploymentArtifact swap: delete 4 private mirrors, parse once per tag element via TagConfigIntent", "status": "pending", "blockedBy": ["T5"] }, + { "id": "T7", "subject": "DeploymentArtifact swap: delete 4 private mirrors, parse once per tag element via TagConfigIntent", "status": "completed", "blockedBy": ["T5"] }, { "id": "T8", "subject": "DraftValidator swap: Configuration→Commons ProjectReference; ExtractTagConfigFullName → TagConfigIntent.ExplicitFullName (null-on-absent preserved)", "status": "pending", "blockedBy": ["T2", "T3"] }, { "id": "T9", "subject": "EquipmentNodeWalker.ExtractFullName delegates to TagConfigIntent; repoint tree-wide canonical-parser doc cites", "status": "pending", "blockedBy": ["T2", "T3"] }, { "id": "T10", "subject": "Retire OpcUaServer.Tests ExtractTag*Tests (authority moved to Commons.Tests); verify zero 'MUST parse identically' hits", "status": "pending", "blockedBy": ["T3", "T5"] }, diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DeploymentArtifact.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DeploymentArtifact.cs index 202aea59..1c132341 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DeploymentArtifact.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DeploymentArtifact.cs @@ -458,8 +458,10 @@ public static class DeploymentArtifact // ordinary equipment tags now (GalaxyMxGateway is a standard Equipment-kind driver). if (!equipmentNamespaces.Contains(nsId)) continue; - var (isHistorized, historianTagname) = ExtractTagHistorize(tagConfig); - var (isArray, arrayLength) = ExtractTagArray(tagConfig); + // Parse the schemaless TagConfig blob ONCE per tag via the shared byte-parity authority + // (01/C-1) — the SAME TagConfigIntent.Parse the live-compose seam (AddressSpaceComposer) + // consumes, so the artifact-decode plan stays byte-equal with the composer's. + var intent = TagConfigIntent.Parse(tagConfig); result.Add(new EquipmentTagPlan( TagId: tagId!, EquipmentId: equipmentId!, @@ -467,13 +469,13 @@ public static class DeploymentArtifact FolderPath: folder ?? string.Empty, Name: name!, DataType: dataType ?? "BaseDataType", - FullName: ExtractTagFullName(tagConfig), + FullName: intent.FullName, Writable: writable, - Alarm: ExtractTagAlarm(tagConfig), - IsHistorized: isHistorized, - HistorianTagname: historianTagname, - IsArray: isArray, - ArrayLength: arrayLength)); + Alarm: intent.Alarm is { } a ? new EquipmentTagAlarmInfo(a.AlarmType, a.Severity, a.HistorizeToAveva) : null, + IsHistorized: intent.IsHistorized, + HistorianTagname: intent.HistorianTagname, + IsArray: intent.IsArray, + ArrayLength: intent.ArrayLength)); } result.Sort((a, b) => @@ -665,113 +667,6 @@ public static class DeploymentArtifact return result; } - /// - /// Extract the driver-side full reference from a tag's TagConfig JSON (top-level "FullName" - /// field). The artifact-decode mirror of AddressSpaceComposer.ExtractTagFullName / - /// EquipmentNodeWalker.ExtractFullName — replicated because Runtime does not reference - /// the Core driver assembly. Falls back to the raw blob when absent or non-JSON. - /// - private static string ExtractTagFullName(string? tagConfig) - { - if (string.IsNullOrWhiteSpace(tagConfig)) return tagConfig ?? string.Empty; - try - { - using var doc = JsonDocument.Parse(tagConfig); - if (doc.RootElement.ValueKind == JsonValueKind.Object - && doc.RootElement.TryGetProperty("FullName", out var fullName) - && fullName.ValueKind == JsonValueKind.String) - { - return fullName.GetString() ?? tagConfig; - } - } - catch (JsonException) { /* fall through to raw blob */ } - return tagConfig; - } - - /// Parses the optional alarm object from a tag's TagConfig JSON. Returns null - /// when absent, non-object, or non-JSON (the tag is then a plain variable). Never throws. The - /// live-edit side (AddressSpaceComposer.ExtractTagAlarm) MUST parse identically (byte-parity). - private static EquipmentTagAlarmInfo? ExtractTagAlarm(string? tagConfig) - { - if (string.IsNullOrWhiteSpace(tagConfig)) return null; - try - { - using var doc = JsonDocument.Parse(tagConfig); - if (doc.RootElement.ValueKind != JsonValueKind.Object) return null; - if (!doc.RootElement.TryGetProperty("alarm", out var a) || a.ValueKind != JsonValueKind.Object) return null; - var type = a.TryGetProperty("alarmType", out var tEl) && tEl.ValueKind == JsonValueKind.String - ? (tEl.GetString() ?? "AlarmCondition") : "AlarmCondition"; - var sev = a.TryGetProperty("severity", out var sEl) && sEl.ValueKind == JsonValueKind.Number - && sEl.TryGetInt32(out var sv) ? sv : 500; - // historizeToAveva (bool?, absent ⇒ null ⇒ historize): byte-parity with - // AddressSpaceComposer.ExtractTagAlarm — only an explicit false suppresses the durable AVEVA write. - bool? historize = a.TryGetProperty("historizeToAveva", out var hEl) - && hEl.ValueKind is JsonValueKind.True or JsonValueKind.False - ? hEl.GetBoolean() - : null; - return new EquipmentTagAlarmInfo(type, sev, historize); - } - catch (JsonException) { return null; } - } - - /// Parses the optional server-side HistoryRead intent from a tag's TagConfig JSON: - /// the isHistorized bool (absent / not a bool / non-object root / blank / malformed ⇒ - /// false) and the optional historianTagname string override (absent / not a string / - /// whitespace-or-empty ⇒ null, meaning the historian tagname defaults to the tag's FullName, - /// resolved later). The raw string value is used — not trimmed — matching ExtractTagFullName / - /// ExtractTagAlarm. Never throws. The live-edit composer side - /// (AddressSpaceComposer.ExtractTagHistorize) MUST parse identically (byte-parity). - private static (bool IsHistorized, string? HistorianTagname) ExtractTagHistorize(string? tagConfig) - { - if (string.IsNullOrWhiteSpace(tagConfig)) return (false, null); - try - { - using var doc = JsonDocument.Parse(tagConfig); - if (doc.RootElement.ValueKind != JsonValueKind.Object) return (false, null); - var isHistorized = doc.RootElement.TryGetProperty("isHistorized", out var hEl) - && (hEl.ValueKind == JsonValueKind.True || hEl.ValueKind == JsonValueKind.False) - && hEl.GetBoolean(); - string? tagname = null; - if (doc.RootElement.TryGetProperty("historianTagname", out var nEl) - && nEl.ValueKind == JsonValueKind.String) - { - var raw = nEl.GetString(); - if (!string.IsNullOrWhiteSpace(raw)) tagname = raw; - } - return (isHistorized, tagname); - } - catch (JsonException) { return (false, null); } - } - - /// Parses the optional array intent from a tag's TagConfig JSON: the isArray - /// bool (absent / not a bool / non-object root / blank / malformed ⇒ false) and the optional - /// arrayLength uint (honoured ONLY when isArray is true AND the prop is a JSON number - /// that fits uint; else null). Mirrors in structure + - /// null/blank/non-object/malformed-JSON tolerance. Never throws. The live-edit composer side - /// (AddressSpaceComposer.ExtractTagArray) MUST parse identically (byte-parity). - private static (bool IsArray, uint? ArrayLength) ExtractTagArray(string? tagConfig) - { - if (string.IsNullOrWhiteSpace(tagConfig)) return (false, null); - try - { - using var doc = JsonDocument.Parse(tagConfig); - if (doc.RootElement.ValueKind != JsonValueKind.Object) return (false, null); - var isArray = doc.RootElement.TryGetProperty("isArray", out var aEl) - && (aEl.ValueKind == JsonValueKind.True || aEl.ValueKind == JsonValueKind.False) - && aEl.GetBoolean(); - uint? arrayLength = null; - if (isArray - && doc.RootElement.TryGetProperty("arrayLength", out var lEl) - && lEl.ValueKind == JsonValueKind.Number - && lEl.TryGetUInt32(out var len)) - { - arrayLength = len; - } - return (isArray, arrayLength); - } - catch (JsonException) { return (false, null); } - } - private static IReadOnlyList ReadArray(JsonElement root, string propertyName, Func reader) where T : class {