fix(stamdata): evaluate the profession validity window per call, not at type-load

Professions.ByProgram was a `static readonly` field filtered on
DateTime.Today, so it evaluated once when the type first loaded. Two
consequences, both real:

- A long-running process kept serving the answer it computed at startup. A
  mapping whose geldigVan fell after boot never appeared; one whose geldigTot
  passed never disappeared.
- Both branches of StamdataFile.ActiveOn were unreachable from this caller,
  which is why this table's validity window had no test at all. It is the
  cleanest single explanation for Stamdata's 71.7% branch coverage (BL-005).

Adds ByProgramOn(DateOnly) — the peildatum as an argument, matching
StamdataTable.RowsOn which already parameterizes it — and makes ByProgram a
property delegating to it with today's date. Call sites (DiplomaRules) are
unchanged and keep the same behaviour, now with a current date.

ProfessionsTests covers both ActiveOn branches plus the regression itself:
the same date must give the same answer, a different date a different one.

Found by the testability pass (TE-009).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
eho
2026-08-26 18:13:38 +02:00
co-authored by Claude Opus 5
parent 4b94f8edb5
commit 9440ce1345
2 changed files with 66 additions and 7 deletions
@@ -19,14 +19,26 @@ public static class Professions
/// <summary>Every mapping in the data-file, typed.</summary>
public static readonly IReadOnlyList<ProfessionMapping> Mappings = StamdataFile.Load<ProfessionMapping>("professions");
/// <summary>The mappings valid today, as a program→beroep lookup. Consumers that don't
/// yet reason about a peildatum (e.g. <c>DiplomaRules.ProfessionFor</c>) use this — it
/// preserves the pre-valid-time behaviour exactly while the file's rows are all current.</summary>
public static readonly IReadOnlyDictionary<string, string> ByProgram =
Mappings.Where(m => StamdataFile.ActiveOn(m.GeldigVan, m.GeldigTot, DateOnly.FromDateTime(DateTime.Today)))
/// <summary>The mappings valid on <paramref name="on"/>, as a program→beroep lookup.
///
/// Takes the peildatum as an argument rather than reading the clock. It used to be a
/// <c>static readonly</c> field filtered on <c>DateTime.Today</c>, which evaluated once at
/// type-load: a long-running process kept yesterday's answer across midnight, and a mapping
/// whose <c>geldigVan</c> fell after startup never appeared at all. It also made both
/// branches of <see cref="StamdataFile.ActiveOn"/> permanently unreachable from here, which
/// is why this table's validity window was never exercised by a test.</summary>
public static IReadOnlyDictionary<string, string> ByProgramOn(DateOnly on) =>
Mappings.Where(m => StamdataFile.ActiveOn(m.GeldigVan, m.GeldigTot, on))
.ToDictionary(m => m.Program, m => m.Beroep, StringComparer.OrdinalIgnoreCase);
/// <summary>Distinct professions, in declaration order — the list a user may declare
/// for a manual (unlisted) diploma.</summary>
/// <summary>The mappings valid today. Consumers that don't yet reason about a peildatum
/// (e.g. <c>DiplomaRules.ProfessionFor</c>) use this — same behaviour as before, but
/// evaluated per call so the date is current.</summary>
public static IReadOnlyDictionary<string, string> ByProgram => ByProgramOn(Today());
/// <summary>Distinct professions valid today, in declaration order — the list a user may
/// declare for a manual (unlisted) diploma.</summary>
public static IReadOnlyList<string> All() => ByProgram.Values.Distinct().ToList();
private static DateOnly Today() => DateOnly.FromDateTime(DateTime.Today);
}