feat(beheer): stamdata deletion protection — CI referential gate + editor expire/warn
CI / frontend (push) Successful in 2m27s
CI / backend (push) Successful in 2m3s
CI / storybook-a11y (push) Failing after 6m30s
CI / semgrep (push) Successful in 1m5s
CI / e2e (push) Successful in 3m22s
CI / api-client-drift (push) Successful in 2m10s

CI gate (authoritative): generalize the dangling-reference test in StamdataValidationTests
into a declared, extensible reference list ("every declared reference into a stamdata key
resolves against the currently-valid stamdata"), starting with Diploma.Opleiding →
professions.program. Removing/renaming a referenced program, or expiring it while current
data still references it, fails the PR build (ADR-0004). Editor (fast feedback): confirm
before delete (warns a referenced row fails CI) and, for temporal tables, a "Sluiten per
vandaag" action that closes validity (geldigTot) — steering to expire over hard delete.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
eho
2026-07-23 22:35:17 +02:00
co-authored by Claude Opus 4.8
parent 67802c68b4
commit fbc4bf51d0
6 changed files with 122 additions and 22 deletions
@@ -11,14 +11,31 @@ namespace BigRegister.Tests;
/// </summary>
public class StamdataValidationTests
{
[Fact]
public void Every_seeded_diploma_program_maps_to_a_known_profession()
/// Declared references INTO stamdata keys — the FK-like invariants the build gate enforces
/// (WP-48). Add an entry when a consumer starts depending on a stamdata key; the gate then
/// fails a delete/rename/expire that orphans it. Resolvers use the "valid today" views, so
/// expiring a row (geldigTot in the past) that current data still references also fails —
/// which steers the editor toward closing validity only once nothing current relies on it.
private sealed record StamdataRef(string Description, IEnumerable<string> Keys, Func<string, bool> Resolves);
private static readonly IReadOnlyList<StamdataRef> References = new[]
{
// The dangling-reference guard: a seed program with no entry in Professions would
// silently render "Onbekend" to the user. Fail the build instead.
foreach (var d in SeedData.Diplomas)
Assert.True(DiplomaRules.ProfessionFor(d) != "Onbekend",
$"Diploma program '{d.Opleiding}' has no profession in Stamdata.Professions.");
new StamdataRef(
"Diploma.Opleiding → professions.program (valid today)",
SeedData.Diplomas.Select(d => d.Opleiding),
key => Professions.ByProgram.ContainsKey(key)),
};
[Fact]
public void Every_declared_reference_into_stamdata_resolves()
{
// The dangling-reference guard (generalized): a referenced key with no (currently valid)
// stamdata row would silently break its consumer. Fail the build instead of prod.
foreach (var r in References)
foreach (var key in r.Keys)
Assert.True(r.Resolves(key),
$"Dangling stamdata reference [{r.Description}]: '{key}' no longer resolves — " +
"deleting or expiring the referenced row would break it.");
}
[Fact]