ADR-014: Tidsversjonering av workflows og regler mot søknadsdato⚓︎
Utvider ADR-013 med en tidsakse på workflow-grafen. Innfrir det ADR-013 utsatte: «Regelkilde og versjonering … er fortsatt utsatt til egen ADR». Regel-evaluering, graf-runner og fakta-laget eies fortsatt av ADR-013.
Revidert 2026-07-13 (AB#21271): status oppdatert fra
proposedtilaccepted— tidsversjoneringen er implementert (GyldigFra/GyldigTil,WorkflowPublisher, frosne regelkopier iworkflow_rules,Sak.Søknadsdato). Typenavn og fillenker oppdatert etter ADR-015 (Case→Sak,SøknadsDato→Søknadsdato).
Kontekst og problemstilling⚓︎
Et virkemiddel får endrede eller nye regler i løpet av levetiden. Regelverket som gjaldt da en søknad ble sendt inn, er det regelverket saken skal vurderes mot — ikke nødvendigvis det som gjelder i dag. I dag finnes ingen slik kobling:
- DbWorkflowRepository.GetAsync
velger alltid nyeste graf:
OrderByDescending(w => w.Version).FirstOrDefault(). Alle saker på et virkemiddel kjører samme graf, uansett når søknaden kom inn. WorkflowDefinitionRecord.Versioner et teknisk revisjonsnummer som bumpes ved enhver graf-endring (v7–v17 i WorkflowConfigurationSeeder), inkludert bugfikser. Det finnes ingen gyldighetsdato.Sak(Sak.cs) hadde ingen søknadsdato lokalt.soknad_sendt_datofinnes i Mimir-spørringen (MimirCaseLookupAdapter.cs) men løftes ikke inn iMimirCaseSnapshot.- Regel-
DefinitionJsonlagres irules-tabellen og overskrives in-place når en regel oppdateres (Rule.UpdateDefinitionbumperVersion). En datert graf som refererer en regel via ID ville derfor plukket opp den nye regel-definisjonen — frysingen ville lekke.
Konsekvensen er at en regelverksendring i dag enten må (a) holdes tilbake til alle åpne saker er ferdigbehandlet, eller (b) feilaktig anvendes på gamle søknader. Ingen av delene er akseptabelt for en saksbehandlingspipeline der utfallet må kunne forsvares mot regelverket på søknadstidspunktet.
Nøkkeldrivere⚓︎
- En sak skal vurderes mot regelverket som gjaldt på søknadsdatoen, ikke dagens.
- En allerede publisert, datert workflow skal være immutabel — re-kjøring av en gammel sak skal
gi samme graf og regelsett som første kjøring (reproduserbarhet og sporbarhet, jf. ADR-013s
FactsSnapshotHash/EngineVersion-linje). - En buggy regel/graf i en allerede gjeldende periode må kunne rettes uten å flytte gyldighetsdatoene — en feilrettelse er ikke en regelverksendring.
- Det skal komme et GUI for å vedlikeholde workflows/regler. Datamodellen må støtte en hyggelig redigeringsflate (normalisert, gjenbrukbare regler, «hvor brukes denne regelen») uten å ofre immutabiliteten i kjøre-laget.
- Utvelgelsen skal være ett enkelt oppslag ved evaluering — ingen flerlags tidsoppløsning som kan motsi seg selv.
- Endringen skal passe inn i v2-arkitekturen.
WorkflowGraphRunner-orkestreringen og selve regel-evaluatorene (RulesEngine/Judgment/Hybrid, ADR-013) er uendret. Men regel-oppslaget i evaluerings-stien må endres: i dag laster EvaluateRuleHandler regelensDefinitionJsonfra den mutbarerules-tabellen på ID. Med mindre dette oppslaget resolver fra snapshot-ens frosne regelsett, fryser vi bare graf-topologien — regel-definisjonene lekker, og frys-garantien er vakuøs.
Vurderte alternativer⚓︎
Hvor lever tidsaksen:
- Kun på workflow-grafen. Grafen er et komplett, datert øyeblikksbilde som «pinner» regelsettet sitt. Én tidsakse, ett oppslag. Krever at regel-definisjonene fryses sammen med grafen.
- På både graf og regler uavhengig. Hver regel har egen gyldig-fra/til som resolves separat per dato. Mer fleksibelt, men to tidsakser som kan gli fra hverandre, og flerlags utvelgelse som er vanskelig å resonnere om og reprodusere.
Hvordan grafen pinner regelversjonene (gitt valg 1):
- A. Frys regel-definisjonene i en egen, normalisert snapshot-tabell (
workflow_rules) som hører til snapshot-en. Graf-YAML-en forblir ren (refererer regel-ID); de frosne reglene ligger som JSONB- rader keyed på snapshot. Selvinneholdt og immutabelt, men hvert format i sitt naturlige hjem. - B. Embed regel-
DefinitionJsoninne i graf-YAML-en (yaml_content). Også selvinneholdt, men nøster JSON inne i YAML i én text-kolonne — to serialiseringsformater i samme blob. Vanskelig å inspisere/diffe en enkelt regel;WorkflowYamlDeserializermå bære regeldefinisjoner. - C. Versjons-pinning per node (
R-VALUTA-NOK@v3) mot en append-onlyrules-tabell. Beholder regler som rader, men blander utkast og frosne historiske rader i samme tabell, gjørrules-PK til(id, version), og visker ut authoring-vs-publisert-skillet.
Forholdet til GUI — redigering vs. publisering:
- Et normalisert authoring-lag (mutbart, gjenbrukbare regler) er best for redigering; et frosset, datert snapshot er best for kjøring. Disse er ikke i konflikt hvis frysingen skjer ved publisering, ikke ved redigering (klassisk «rediger utkast → publiser frosset snapshot»).
Beslutning⚓︎
Valgt: tidsakse kun på workflow-grafen (alt. 1), regelsettet pinnes ved å fryse regel-definisjonene
i en egen normalisert snapshot-tabell (alt. A — workflow_rules), med et todelt
authoring-/publisert-lag.
1. Gyldighetsperiode + versjon som tie-breaker⚓︎
WorkflowDefinitionRecord får en gyldighetsperiode i tillegg til dagens Version:
| Felt | Type | Semantikk |
|---|---|---|
GyldigFra |
timestamptz |
Periodestart, inklusiv. |
GyldigTil |
timestamptz? |
Periodeslutt, eksklusiv. null = åpen framover. |
Version |
int |
Tie-breaker innen samme GyldigFra. Høyeste vinner. |
Intervallet er halvåpent: [GyldigFra, GyldigTil). De to behovene kollapser til én mekanisme:
- Regelverksendring → ny rad med ny
GyldigFra(forrige periodesGyldigTillukkes til samme dato). - Buggy regel/graf i gjeldende periode → ny rad med samme
GyldigFraog høyereVersion; tie-breakeren velger den.
2. Søknadsdato som matche-dato⚓︎
soknad_sendt_dato løftes fra Mimir-spørringen inn i MimirCaseSnapshot, videre til en ny
Sak.Søknadsdato, og persisteres ved materialisering. Den er datoen utvelgelsen matcher mot
perioden. Fordi den er frosset på Sak, er re-kjøring deterministisk.
3. Utvelgelse⚓︎
DbWorkflowRepository.GetAsync endres fra «alltid nyeste» til datert utvelgelse:
| Text Only | |
|---|---|
Fallback ved Søknadsdato == null (sak uten dato fra Mimir): velg snapshot med høyeste
GyldigFra, så høyeste Version (≈ dagens oppførsel), og logg at fallback ble brukt.
4. Frysing ved publisering (authoring vs. publisert)⚓︎
To lag:
- Authoring-lag (det framtidige GUI-et redigerer):
rules-tabellen som i dag — normalisert, én rad per regel, gjenbrukbar, mutbar. Ingen datoer. Graf-YAML-en refererer regel-ID. - Publisert lag (det runneren leser):
WorkflowDefinitionRecordmedGyldigFra/GyldigTil+Version(ren graf-YAML), og en eid samlingworkflow_rules— frosne JSONB-kopier av de refererte reglenesDefinitionJson, én rad per(workflow_id, rule_id). Immutabelt.
En WorkflowPublisher-tjeneste utfører publisering: les utkast-graf + de refererte regel-
definisjonene nå, skriv graf-YAML-en og de frosne regelkopiene til en ny datert snapshot, lukk
forrige periodes GyldigTil, sett Version = forrige + 1. Etter publisering rører videre redigering
i rules-tabellen ikke allerede publiserte snapshots. Både seederen og det framtidige GUI-et kaller
WorkflowPublisher.
5. Frys-oppslag i evaluerings-stien (kritisk)⚓︎
Frysing er først reell når evalueringen leser de frosne definisjonene. I dag løser
RuleEngineExecutor → EvaluateRuleCommand → EvaluateRuleHandler regelen via
IRegelRepository.GetAsync(ruleId) mot den mutbare rules-tabellen. Derfor: når en datert snapshot er
valgt, bygger RunCaseWorkflowHandler et frosset regelsett (RuleId → Rule) fra snapshot-ens
workflow_rules, og evaluerings-stien resolver fra dette settet i stedet for rules-tabellen.
Konkret seedes RuleExecutorFactory med det frosne settet, EvaluateRuleCommand bærer den frosne
regelen, og EvaluateRuleHandler slutter å slå opp i rules. Selve evaluatorene
(RulesEngine/Judgment/Hybrid) og orkestreringsgrafen er uendret — kun kilden til regeldefinisjonen
endres fra mutbar tabell til frosset snapshot.
6. Audit⚓︎
Valgt (workflow-id, Version, GyldigFra) registreres på case_evaluations, slik at hver kjøring
spores til eksakt snapshot.
Konsekvenser⚓︎
- Positivt: en sak vurderes mot regelverket på søknadsdatoen; regelverksendringer kan publiseres uten å vente på at åpne saker ferdigbehandles.
- Positivt: daterte snapshots er immutable og selvinneholdte — re-kjøring er reproduserbar, og graf og regel kan ikke gli fra hverandre i tid.
- Positivt: buggy regel i gjeldende periode rettes med en versjonsbump uten å røre datoene.
- Positivt: authoring-laget forblir normalisert og GUI-vennlig; immutabiliteten ligger i publiseringssteget, ikke i redigeringen.
- Positivt: hvert serialiseringsformat i sitt naturlige hjem — ren graf-YAML i
yaml_content, regler som JSONB iworkflow_rules. En enkelt frossen regel kan inspiseres/diffes direkte i SQL; ingen JSON-i-YAML-nøsting, ogWorkflowYamlDeserializerer uendret. - Negativt: regel-definisjoner dupliseres (frosset kopi i
workflow_rulesper snapshot + levende rad irules). Bevisst: duplisering er prisen for selvinneholdte, frosne snapshots. - Negativt: eksisterende
Sak-rader manglerSøknadsdatoog må backfilles fra Mimir (eller falle tilbake til nyeste snapshot). - Nøytralt:
Versionbeholdes, men endrer rolle fra «global revisjon» til «tie-breaker innen periode». Unik-indeksen utvides til(virkemiddel_id, gyldig_fra, version).
Arkitektur⚓︎
flowchart TD
subgraph Authoring [Authoring-lag · mutbart · GUI senere]
RU[rules-tabell<br/>normalisert, gjenbrukbar]
DR[workflow-utkast]
end
RU -- publiser --> P[WorkflowPublisher<br/>frys regler til workflow_rules<br/>lukk forrige GyldigTil]
DR -- publiser --> P
P --> S[(WorkflowDefinitionRecord<br/>ren graf-YAML · GyldigFra/GyldigTil + Version<br/>+ workflow_rules JSONB · immutabel)]
C[Sak.Søknadsdato<br/>fra Mimir] --> SEL
S --> SEL{DbWorkflowRepository<br/>søknadsdato ∈ periode<br/>høyeste Version}
SEL -- frosset regelsett fra workflow_rules --> RUN[WorkflowGraphRunner + EvaluateRuleHandler<br/>evaluatorer uendret · regel-kilde = snapshot]
RUN --> CE[case_evaluations<br/>+ valgt workflow-id/Version/GyldigFra]
Berørte filer⚓︎
src/Enova.Kai.Domain/Cases/Sak.cs— nySøknadsdato.src/Enova.Kai.Infrastructure/Mimir/MimirCaseLookupAdapter.cs,MimirCaseSnapshot— løftsoknad_sendt_datoinn i snapshot.src/Enova.Kai.Infrastructure/Persistence/WorkflowDefinitionRecord.cs+Configurations/WorkflowDefinitionConfiguration—GyldigFra/GyldigTil, eid samlingworkflow_rules, indeks(virkemiddel_id, gyldig_fra, version).- Ny
WorkflowRuleRecord+workflow_rules-tabell (frosne JSONB-regelkopier per snapshot). src/Enova.Kai.Infrastructure/Persistence/Repositories/DbWorkflowRepository.cs— datert utvelgelse- null-fallback; last frosne regler fra
workflow_rules. src/Enova.Kai.Application/WorkflowEngine/evaluerings-sti —RuleExecutorFactory,RuleEngineExecutor,EvaluateRuleCommand,EvaluateRuleHandler,RunCaseWorkflowHandler: resolve frosset regelsett i stedet forIRegelRepository.src/Enova.Kai.Infrastructure/Persistence/Seeding/WorkflowConfigurationSeeder.cs— produser daterte snapshots med frosne regelkopier viaWorkflowPublisher; v17-baselineGyldigFra = -infinity.- Ny
WorkflowPublisher(Application) — publiseringssemantikk + frysing av regler. case_evaluations-mapping — audit-felter for valgt snapshot.- Ny EF-migrasjon — kolonner på
cases+workflows, indeks, backfill.
Ikke-mål⚓︎
- GUI (redigering, publiser-knapp, «hvor brukes denne regelen») — egen, senere spec som bygger på
WorkflowPublisher+ authoring-laget definert her. - Per-regel uavhengig tidsakse (alt. 2) — eksplisitt forkastet; én tidsakse på grafen.
- ERS-status-spesifikke grafer —
ersStatus-parameteren iGetAsyncforblir reservert/ubrukt. - Å ta seederen ut av oppstart. Publisering bør på sikt bli en eksplisitt, idempotent operasjon
i stedet for en startup-bivirkning (no-op-gaten på
CurrentVersion), men det er en egen oppfølgings-task — ikke en del av denne ADR-en.
Verifisering⚓︎
- [ ]
WorkflowDefinitionRecordharGyldigFra/GyldigTil; unik-indeks(virkemiddel_id, gyldig_fra, version). - [ ]
Sak.Søknadsdatosettes fra Mimir ved materialisering;MimirCaseSnapshotbærer datoen. - [ ]
DbWorkflowRepository.GetAsyncvelger snapshot dersøknadsdato ∈ [GyldigFra, GyldigTil), høyesteVersionsom tie-breaker (enhetstest med overlappende perioder + samme-periode-bump). - [ ]
Søknadsdato == nullfaller tilbake til nyeste snapshot og logger det (enhetstest). - [ ] Publisert snapshot er selvinneholdt: frosne regelkopier i
workflow_rules; graf-YAML forblir ren (ingen embeddet JSON); endring irules-tabellen etter publisering endrer ikke snapshot-en (test). - [ ] Evaluerings-stien resolver regeldefinisjon fra snapshot-ens
workflow_rules, ikke frarules-tabellen: endre en regel irulesetter publisering, kjør en gammel sak på nytt → samme utfall som før endringen (frys-test gjennom hele kjøre-stien). - [ ] v17-baseline backfilles
GyldigFra = -infinity,GyldigTil = null— eksisterende saker treffer fortsatt. - [ ]
case_evaluationslagrer valgt(workflow-id, Version, GyldigFra).
Revisit-triggere⚓︎
- Hvis et virkemiddel trenger ulike grafer for samme periode avhengig av ERS-status: aktiver
ersStatusi utvelgelsen (i dag reservert). - Hvis regelverksendringer blir hyppige nok til at frys-dupliseringen i
workflow_rulesblir et lagrings- eller vedlikeholdsproblem: revurder per-node versjons-pinning (alt. C) mot en append-onlyrules-tabell. - Hvis søknadsdato viser seg å være feil tidsanker for noen virkemidler (f.eks. vedtaks- eller registreringsdato gjelder): generaliser matche-datoen per virkemiddel.