fix(transport): base-first template reference resolution on import (ScadaBridge#100 item 1)
TemplateLibrary.GetIdByNameAsync was first-row-wins, so a derived slot template could shadow a same-named base. Now: exact name, base first, derived-only name only when unique, otherwise unresolved (composition audit says ambiguous). Tracked-template indexes are base-first too. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
@@ -2402,6 +2402,29 @@ public sealed class BundleImporter : IBundleImporter
|
||||
internal static Dictionary<string, Template> ToTemplateLookup(IEnumerable<Template> rows) =>
|
||||
ToNameLookup(rows.OrderBy(t => t.IsDerived), t => t.Name);
|
||||
|
||||
/// <summary>The by-name reference rule of <c>TemplateLibrary.GetIdByNameAsync</c>
|
||||
/// (ScadaBridge#100 item 1): exact name; base template first; a derived-only name resolves
|
||||
/// only when unique; several derived and no base ⇒ omitted (ambiguous).</summary>
|
||||
internal static Dictionary<string, int> ResolveTemplateReferenceIds(
|
||||
IEnumerable<(int Id, string Name, bool IsDerived)> pairs)
|
||||
{
|
||||
var result = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
foreach (var group in pairs.GroupBy(p => p.Name, StringComparer.Ordinal))
|
||||
{
|
||||
var bases = group.Where(p => !p.IsDerived).ToList();
|
||||
if (bases.Count > 0)
|
||||
{
|
||||
result[group.Key] = bases.Min(p => p.Id);
|
||||
continue;
|
||||
}
|
||||
|
||||
var derived = group.ToList();
|
||||
if (derived.Count == 1)
|
||||
result[group.Key] = derived[0].Id;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task ApplyTemplatesAsync(
|
||||
IReadOnlyList<TemplateDto> dtos,
|
||||
Dictionary<(string, string), ImportResolution> map,
|
||||
@@ -3367,7 +3390,10 @@ public sealed class BundleImporter : IBundleImporter
|
||||
OwnerTemplateName = parent.Name,
|
||||
compDto.InstanceName,
|
||||
UnresolvedComposedTemplateName = compDto.ComposedTemplateName,
|
||||
Reason = "Composed template name not present in bundle or target.",
|
||||
Reason = (await library.GetAmbiguousNamesAsync(ct).ConfigureAwait(false))
|
||||
.Contains(compDto.ComposedTemplateName)
|
||||
? "Composed template name is held by several derived (composition-slot) templates in the target and by no base template; refusing to guess (ScadaBridge#100)."
|
||||
: "Composed template name not present in bundle or target.",
|
||||
},
|
||||
ct).ConfigureAwait(false);
|
||||
continue;
|
||||
@@ -3492,7 +3518,9 @@ public sealed class BundleImporter : IBundleImporter
|
||||
|
||||
public LocalTemplateIndex(IReadOnlyList<Template> tracked)
|
||||
{
|
||||
foreach (var template in tracked)
|
||||
// Base first (ScadaBridge#100 item 1): a tracked derived slot template must not
|
||||
// shadow a base template of the same name for a composed/base reference.
|
||||
foreach (var template in tracked.OrderBy(t => t.IsDerived))
|
||||
{
|
||||
_exact.TryAdd(template.Name, template);
|
||||
}
|
||||
@@ -3633,19 +3661,43 @@ public sealed class BundleImporter : IBundleImporter
|
||||
return _analysis;
|
||||
}
|
||||
|
||||
/// <summary>Target template name → id (first wins on a duplicate name).</summary>
|
||||
/// <summary>
|
||||
/// Target template name → id, used to resolve a bundle's by-name references (composed,
|
||||
/// base and instance template names) against templates this import did not stage.
|
||||
/// <para>
|
||||
/// ScadaBridge#100 item 1 — the rule, keyed by exact name: a <b>base</b> template wins
|
||||
/// (base names are unique through the filtered unique index, and the base is the row a
|
||||
/// by-name reference means — the same rule <see cref="ToTemplateLookup"/> applies to
|
||||
/// Overwrite targets); a name held only by derived (composition-slot) templates resolves
|
||||
/// only when exactly ONE derived template holds it; a name held by several derived
|
||||
/// templates and no base is <b>ambiguous</b> and is left out, so the caller records it
|
||||
/// unresolved rather than binding to an arbitrary slot owned by another composition.
|
||||
/// It used to be first-row-wins over the projection order, so a derived slot template
|
||||
/// with a lower id silently shadowed a base template of the same name.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public async Task<IReadOnlyDictionary<string, int>> GetIdByNameAsync(CancellationToken ct)
|
||||
{
|
||||
if (_idByName is null)
|
||||
{
|
||||
var pairs = await GetPairsAsync(ct).ConfigureAwait(false);
|
||||
_idByName = pairs
|
||||
.GroupBy(p => p.Name, StringComparer.Ordinal)
|
||||
.ToDictionary(g => g.Key, g => g.First().Id, StringComparer.Ordinal);
|
||||
_idByName = ResolveTemplateReferenceIds(pairs);
|
||||
}
|
||||
return _idByName;
|
||||
}
|
||||
|
||||
/// <summary>Target names held only by two or more derived templates (no base) —
|
||||
/// the references <see cref="GetIdByNameAsync"/> refuses to guess.</summary>
|
||||
public async Task<IReadOnlySet<string>> GetAmbiguousNamesAsync(CancellationToken ct)
|
||||
{
|
||||
var pairs = await GetPairsAsync(ct).ConfigureAwait(false);
|
||||
return pairs
|
||||
.GroupBy(p => p.Name, StringComparer.Ordinal)
|
||||
.Where(g => g.All(p => p.IsDerived) && g.Count() > 1)
|
||||
.Select(g => g.Key)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>Target template id → name.</summary>
|
||||
public async Task<IReadOnlyDictionary<int, string>> GetNameByIdAsync(CancellationToken ct)
|
||||
{
|
||||
@@ -3659,14 +3711,14 @@ public sealed class BundleImporter : IBundleImporter
|
||||
|
||||
// Id/name pairs from whichever projection is already in hand: the analysis
|
||||
// load when a caller has made it, else the lighter stub projection.
|
||||
private async Task<IReadOnlyList<(int Id, string Name)>> GetPairsAsync(CancellationToken ct)
|
||||
private async Task<IReadOnlyList<(int Id, string Name, bool IsDerived)>> GetPairsAsync(CancellationToken ct)
|
||||
{
|
||||
if (_analysis is not null)
|
||||
{
|
||||
return _analysis.Select(t => (t.Id, t.Name)).ToList();
|
||||
return _analysis.Select(t => (t.Id, t.Name, t.IsDerived)).ToList();
|
||||
}
|
||||
_stubs ??= await _repo.GetTemplateStubsAsync(ct).ConfigureAwait(false);
|
||||
return _stubs.Select(t => (t.Id, t.Name)).ToList();
|
||||
return _stubs.Select(t => (t.Id, t.Name, t.IsDerived)).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4844,6 +4896,9 @@ public sealed class BundleImporter : IBundleImporter
|
||||
await library.GetIdByNameAsync(ct).ConfigureAwait(false), StringComparer.Ordinal);
|
||||
foreach (var local in _dbContext.Templates.Local)
|
||||
{
|
||||
// A tracked derived slot template never displaces a base of the same name
|
||||
// (ScadaBridge#100 item 1 — instances are of base templates).
|
||||
if (local.IsDerived && templateIdByName.ContainsKey(local.Name)) continue;
|
||||
templateIdByName[local.Name] = local.Id;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user