fix(template-engine): resolve TemplateEngine-015,016 — cascade-rename nested derived templates, correct composed-script ParentPath

This commit is contained in:
Joseph Doherty
2026-05-17 03:18:41 -04:00
parent 0135a6b2a6
commit d6221419c6
5 changed files with 177 additions and 14 deletions

View File

@@ -705,12 +705,28 @@ public class TemplateService
{
var newDerivedName = $"{owner.Name}.{newInstanceName}";
var allTemplates = await _repository.GetAllTemplatesAsync(cancellationToken);
if (allTemplates.Any(t => t.Id != derived.Id && t.Name == newDerivedName))
return Result<TemplateComposition>.Failure(
$"Cannot rename derived template to '{newDerivedName}': a template with that name already exists.");
derived.Name = newDerivedName;
await _repository.UpdateTemplateAsync(derived, cancellationToken);
// The cascade of derived templates created by AddComposition follows a
// dotted path (Pump.TempSensor and the nested Pump.TempSensor.Probe1).
// Renaming the slot must rename every derived template in that cascade
// so the dotted-path naming invariant holds — pre-check every new name
// the cascade will introduce before any row mutates.
var renames = new List<(Template Template, string NewName)>();
await CollectCascadeRenamesAsync(derived, newDerivedName, renames, cancellationToken);
var renamedIds = renames.Select(r => r.Template.Id).ToHashSet();
foreach (var (_, newName) in renames)
{
if (allTemplates.Any(t => !renamedIds.Contains(t.Id) && t.Name == newName))
return Result<TemplateComposition>.Failure(
$"Cannot rename derived template to '{newName}': a template with that name already exists.");
}
foreach (var (template, newName) in renames)
{
template.Name = newName;
await _repository.UpdateTemplateAsync(template, cancellationToken);
}
}
composition.InstanceName = newInstanceName;
@@ -747,6 +763,30 @@ public class TemplateService
return Result<bool>.Success(true);
}
/// <summary>
/// Recursively collects the (template, new name) pairs for a renamed derived
/// template and every cascaded inner derived template beneath it. Each inner
/// derived's new name is re-derived from its renamed parent and the slot's
/// instance name (mirroring the cascade <see cref="CreateCascadedCompositionAsync"/>
/// builds and the recursion in <see cref="CascadeDeleteDerivedAsync"/>).
/// </summary>
private async Task CollectCascadeRenamesAsync(
Template derived,
string newName,
List<(Template Template, string NewName)> renames,
CancellationToken cancellationToken)
{
renames.Add((derived, newName));
foreach (var child in derived.Compositions.ToList())
{
var childDerived = await _repository.GetTemplateByIdAsync(child.ComposedTemplateId, cancellationToken);
if (childDerived != null && childDerived.IsDerived && childDerived.OwnerCompositionId == child.Id)
await CollectCascadeRenamesAsync(
childDerived, $"{newName}.{child.InstanceName}", renames, cancellationToken);
}
}
/// <summary>
/// Recursively deletes a derived template along with the cascade of inner
/// derived templates the compose flow created. Each composition row on the