Enforce per-medium track cardinality in the upload service via MediumRules

Promote the Session/Mix single-track rule from a CMS-form convention to a
domain invariant: declare cardinality as data in MediumRules, enforce it in
UnifiedTrackService before the vault write (no orphan), return 409, and read
the same rule in the batch-form collapse.
This commit is contained in:
daniel-c-harvey
2026-06-13 14:12:01 -04:00
parent 6f42464294
commit b893ca84de
8 changed files with 261 additions and 9 deletions
+35
View File
@@ -0,0 +1,35 @@
namespace DeepDrftModels.Enums;
/// <summary>
/// The allowed track-count range for a <see cref="ReleaseMedium"/>, expressed as an inclusive
/// [Min, Max] band. <c>Max == int.MaxValue</c> denotes an unbounded (many-track) medium.
/// </summary>
public readonly record struct MediumCardinality(int Min, int Max)
{
/// <summary>True when <paramref name="trackCount"/> falls within the inclusive band.</summary>
public bool Allows(int trackCount) => trackCount >= Min && trackCount <= Max;
/// <summary>True when the medium permits exactly one track (Max capped at 1).</summary>
public bool IsSingleTrack => Max == 1;
}
/// <summary>
/// Single source of truth for per-medium structural rules. Today it declares track cardinality;
/// the same table is read by the upload service (to reject over-limit track-adds) and the CMS
/// batch forms (to decide whether to collapse the master list to one row), so the two cannot
/// drift. A future medium declares its cardinality here — and only here — and every consumer
/// honours it automatically. Pure declaration, no dependencies.
/// </summary>
public static class MediumRules
{
private static readonly IReadOnlyDictionary<ReleaseMedium, MediumCardinality> Cardinalities =
new Dictionary<ReleaseMedium, MediumCardinality>
{
[ReleaseMedium.Cut] = new(1, int.MaxValue),
[ReleaseMedium.Session] = new(1, 1),
[ReleaseMedium.Mix] = new(1, 1),
};
/// <summary>The declared track-count band for <paramref name="medium"/>.</summary>
public static MediumCardinality CardinalityOf(ReleaseMedium medium) => Cardinalities[medium];
}