fix(template-engine): resolve TemplateEngine-011,013,014 — remove dead converter, duplicate-id-safe cycle detection, unified deletion logic; TemplateEngine-012 deferred

This commit is contained in:
Joseph Doherty
2026-05-16 22:32:30 -04:00
parent 9e2416b34c
commit adb5e75ec3
9 changed files with 274 additions and 98 deletions
@@ -8,19 +8,24 @@ namespace ScadaLink.TemplateEngine.Flattening;
/// <summary>
/// Produces a deterministic SHA-256 hash of a FlattenedConfiguration.
/// Same content always produces the same hash across runs by using
/// canonical JSON serialization with sorted keys.
/// Same content always produces the same hash across runs.
/// <para>
/// DETERMINISM CONTRACT: System.Text.Json serializes properties in CLR
/// declaration order, which it does NOT sort. Stable output therefore relies
/// on the private <c>Hashable*</c> records below declaring their properties
/// in alphabetical order. A contributor adding a property out of alphabetical
/// order would silently change every revision hash; the ordering is guarded
/// by <c>RevisionHashServiceTests.HashableRecords_PropertiesDeclaredAlphabetically</c>.
/// Collections are explicitly sorted by <c>CanonicalName</c> before hashing.
/// </para>
/// </summary>
public class RevisionHashService
{
private static readonly JsonSerializerOptions CanonicalJsonOptions = new()
{
// Sort properties alphabetically for determinism
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
WriteIndented = false,
// Ensure consistent ordering
Converters = { new SortedPropertiesConverterFactory() }
WriteIndented = false
};
/// <summary>
@@ -84,7 +89,9 @@ public class RevisionHashService
return $"sha256:{Convert.ToHexStringLower(hashBytes)}";
}
// Internal types for deterministic serialization (sorted property names via camelCase + alphabetical)
// Internal types for deterministic serialization. Properties MUST be declared
// in alphabetical order — System.Text.Json emits them in declaration order and
// does not sort. See the DETERMINISM CONTRACT note on the class summary.
private sealed record HashableConfiguration
{
public List<HashableAlarm> Alarms { get; init; } = [];
@@ -128,14 +135,3 @@ public class RevisionHashService
public string? TriggerType { get; init; }
}
}
/// <summary>
/// A JSON converter factory that ensures properties are serialized in alphabetical order
/// for deterministic output. Works with record types.
/// </summary>
internal class SortedPropertiesConverterFactory : JsonConverterFactory
{
public override bool CanConvert(Type typeToConvert) => false;
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) => null;
}