Gå til innhold

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 proposed til accepted — tidsversjoneringen er implementert (GyldigFra/GyldigTil, WorkflowPublisher, frosne regelkopier i workflow_rules, Sak.Søknadsdato). Typenavn og fillenker oppdatert etter ADR-015 (CaseSak, SøknadsDatoSø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.Version er 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_dato finnes i Mimir-spørringen (MimirCaseLookupAdapter.cs) men løftes ikke inn i MimirCaseSnapshot.
  • Regel-DefinitionJson lagres i rules-tabellen og overskrives in-place når en regel oppdateres (Rule.UpdateDefinition bumper Version). 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 regelens DefinitionJson fra den mutbare rules-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:

  1. 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.
  2. 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-DefinitionJson inne 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; WorkflowYamlDeserializer må bære regeldefinisjoner.
  • C. Versjons-pinning per node (R-VALUTA-NOK@v3) mot en append-only rules-tabell. Beholder regler som rader, men blander utkast og frosne historiske rader i samme tabell, gjør rules-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 periodes GyldigTil lukkes til samme dato).
  • Buggy regel/graf i gjeldende periode → ny rad med samme GyldigFra og høyere Version; 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
1
2
3
4
5
WHERE virkemiddel_id = @virkemiddel
  AND gyldig_fra <= @søknadsdato
  AND (gyldig_til IS NULL OR gyldig_til > @søknadsdato)
ORDER BY version DESC
LIMIT 1

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): WorkflowDefinitionRecord med GyldigFra/GyldigTil + Version (ren graf-YAML), og en eid samling workflow_rules — frosne JSONB-kopier av de refererte reglenes DefinitionJson, én rad per (workflow_id, rule_id). Immutabelt.

En WorkflowPublisher-tjeneste utfører publisering: les utkast-graf + de refererte regel- definisjonene , 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 i workflow_rules. En enkelt frossen regel kan inspiseres/diffes direkte i SQL; ingen JSON-i-YAML-nøsting, og WorkflowYamlDeserializer er uendret.
  • Negativt: regel-definisjoner dupliseres (frosset kopi i workflow_rules per snapshot + levende rad i rules). Bevisst: duplisering er prisen for selvinneholdte, frosne snapshots.
  • Negativt: eksisterende Sak-rader mangler Søknadsdato og må backfilles fra Mimir (eller falle tilbake til nyeste snapshot).
  • Nøytralt: Version beholdes, 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 — ny Søknadsdato.
  • src/Enova.Kai.Infrastructure/Mimir/MimirCaseLookupAdapter.cs, MimirCaseSnapshot — løft soknad_sendt_dato inn i snapshot.
  • src/Enova.Kai.Infrastructure/Persistence/WorkflowDefinitionRecord.cs + Configurations/WorkflowDefinitionConfigurationGyldigFra/GyldigTil, eid samling workflow_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 for IRegelRepository.
  • src/Enova.Kai.Infrastructure/Persistence/Seeding/WorkflowConfigurationSeeder.cs — produser daterte snapshots med frosne regelkopier via WorkflowPublisher; v17-baseline GyldigFra = -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 graferersStatus-parameteren i GetAsync forblir 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⚓︎

  • [ ] WorkflowDefinitionRecord har GyldigFra/GyldigTil; unik-indeks (virkemiddel_id, gyldig_fra, version).
  • [ ] Sak.Søknadsdato settes fra Mimir ved materialisering; MimirCaseSnapshot bærer datoen.
  • [ ] DbWorkflowRepository.GetAsync velger snapshot der søknadsdato ∈ [GyldigFra, GyldigTil), høyeste Version som tie-breaker (enhetstest med overlappende perioder + samme-periode-bump).
  • [ ] Søknadsdato == null faller 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 i rules-tabellen etter publisering endrer ikke snapshot-en (test).
  • [ ] Evaluerings-stien resolver regeldefinisjon fra snapshot-ens workflow_rules, ikke fra rules-tabellen: endre en regel i rules etter 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_evaluations lagrer valgt (workflow-id, Version, GyldigFra).

Revisit-triggere⚓︎

  • Hvis et virkemiddel trenger ulike grafer for samme periode avhengig av ERS-status: aktiver ersStatus i utvelgelsen (i dag reservert).
  • Hvis regelverksendringer blir hyppige nok til at frys-dupliseringen i workflow_rules blir et lagrings- eller vedlikeholdsproblem: revurder per-node versjons-pinning (alt. C) mot en append-only rules-tabell.
  • Hvis søknadsdato viser seg å være feil tidsanker for noen virkemidler (f.eks. vedtaks- eller registreringsdato gjelder): generaliser matche-datoen per virkemiddel.

Tilleggsinformasjon⚓︎

  • Utvider ADR-013; innfrir dens utsatte «regelkilde og versjonering».
  • Relaterte: ADR-011 (Postgres for regel-/resultatpersistens), ADR-012 (durabel evaluerings-inngang).
  • Implementasjonsdesign: docs/superpowers/specs/2026-06-24-tidsversjonering-workflow-design.md.