refactor(transport): single-source template child equality — diff and overwrite-sync can no longer drift
New TemplateChildEquality is the one place "has this template child changed?" is decided, comparing the COMPLETE writable field set per entity (attributes now incl. ElementDataType + LockedInDerived; alarms incl. the on-trigger script by name + LockedInDerived; scripts incl. MinTimeBetweenRuns/ExecutionTimeoutSeconds + LockedInDerived; native sources per T5). ArtifactDiff.CompareTemplate and all four BundleImporter.SyncTemplate*Async predicates now route through it — closing the class of bug where Preview reported Identical for an artifact an Overwrite would mutate. The attribute sync passes a value-normalised DTO so List-value re-imports stay idempotent; the alarm on-trigger change now surfaces in both diff and sync. Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
@@ -64,16 +64,23 @@ public sealed class ArtifactDiff
|
||||
AddIfDifferent(changes, "FolderName", FolderNameOf(existing), incoming.FolderName);
|
||||
AddIfDifferent(changes, "BaseTemplateName", BaseTemplateNameOf(existing), incoming.BaseTemplateName);
|
||||
|
||||
// Bundles reference the alarm on-trigger script by name; resolve the
|
||||
// persisted FK back to a name over this template's own scripts so the
|
||||
// shared equality can compare it. See TemplateChildEquality.
|
||||
var scriptNameById = TemplateChildEquality.ScriptNameResolver(existing.Scripts);
|
||||
|
||||
// Children: compare each child collection by name. We track which names
|
||||
// were added, which were removed, and which existed on both sides but
|
||||
// diverged in body. We use coarse value equality / line counts for
|
||||
// scripts so the diff JSON stays under a few KB per item.
|
||||
// scripts so the diff JSON stays under a few KB per item. Change
|
||||
// detection is single-sourced through TemplateChildEquality so the diff
|
||||
// can never disagree with what an Overwrite sync writes (#05-T6).
|
||||
DiffChildren(
|
||||
existing.Attributes,
|
||||
incoming.Attributes,
|
||||
e => e.Name,
|
||||
i => i.Name,
|
||||
AttributesEqual,
|
||||
TemplateChildEquality.AttributesEqual,
|
||||
"Attributes",
|
||||
changes);
|
||||
|
||||
@@ -82,7 +89,7 @@ public sealed class ArtifactDiff
|
||||
incoming.Alarms,
|
||||
e => e.Name,
|
||||
i => i.Name,
|
||||
AlarmsEqual,
|
||||
(e, i) => TemplateChildEquality.AlarmsEqual(e, i, scriptNameById),
|
||||
"Alarms",
|
||||
changes);
|
||||
|
||||
@@ -106,7 +113,7 @@ public sealed class ArtifactDiff
|
||||
incoming.NativeAlarmSources,
|
||||
e => e.Name,
|
||||
i => i.Name,
|
||||
NativeAlarmSourcesEqual,
|
||||
TemplateChildEquality.NativeAlarmSourcesEqual,
|
||||
"NativeAlarmSources",
|
||||
changes);
|
||||
|
||||
@@ -641,7 +648,7 @@ public sealed class ArtifactDiff
|
||||
foreach (var (name, ex) in existingByName)
|
||||
{
|
||||
if (!incomingByName.TryGetValue(name, out var inc)) continue;
|
||||
if (!ScriptsEqual(ex, inc))
|
||||
if (!TemplateChildEquality.ScriptsEqual(ex, inc))
|
||||
{
|
||||
// A script row can diverge in Code and/or its trigger/param metadata.
|
||||
// The structured line diff covers the Code body; when only the
|
||||
@@ -652,37 +659,6 @@ public sealed class ArtifactDiff
|
||||
}
|
||||
}
|
||||
|
||||
private static bool AttributesEqual(TemplateAttribute e, TemplateAttributeDto i) =>
|
||||
e.Value == i.Value
|
||||
&& e.DataType == i.DataType
|
||||
&& e.IsLocked == i.IsLocked
|
||||
&& e.Description == i.Description
|
||||
&& e.DataSourceReference == i.DataSourceReference;
|
||||
|
||||
private static bool AlarmsEqual(TemplateAlarm e, TemplateAlarmDto i) =>
|
||||
e.Description == i.Description
|
||||
&& e.PriorityLevel == i.PriorityLevel
|
||||
&& e.TriggerType == i.TriggerType
|
||||
&& e.TriggerConfiguration == i.TriggerConfiguration
|
||||
&& e.IsLocked == i.IsLocked;
|
||||
|
||||
private static bool ScriptsEqual(TemplateScript e, TemplateScriptDto i) =>
|
||||
string.Equals(e.Code, i.Code, StringComparison.Ordinal)
|
||||
&& e.TriggerType == i.TriggerType
|
||||
&& e.TriggerConfiguration == i.TriggerConfiguration
|
||||
&& e.ParameterDefinitions == i.ParameterDefinitions
|
||||
&& e.ReturnDefinition == i.ReturnDefinition
|
||||
&& e.IsLocked == i.IsLocked;
|
||||
|
||||
private static bool NativeAlarmSourcesEqual(TemplateNativeAlarmSource e, TemplateNativeAlarmSourceDto i) =>
|
||||
e.Description == i.Description
|
||||
&& e.ConnectionName == i.ConnectionName
|
||||
&& e.SourceReference == i.SourceReference
|
||||
&& e.ConditionFilter == i.ConditionFilter
|
||||
&& e.IsLocked == i.IsLocked
|
||||
&& e.IsInherited == i.IsInherited
|
||||
&& e.LockedInDerived == i.LockedInDerived;
|
||||
|
||||
private static bool ExternalSystemMethodsEqual(ExternalSystemMethod e, ExternalSystemMethodDto i) =>
|
||||
e.HttpMethod == i.HttpMethod
|
||||
&& e.Path == i.Path
|
||||
|
||||
Reference in New Issue
Block a user