ADR-013: Workflow-orkestrering på regelmotoren (utvidelse av ADR-010)⚓︎
Utvider ADR-010 med workflow-graf-orkestrering og typede facts-pipelines. Regel-definisjon, lasting og evaluering eies fortsatt av ADR-010.
Revidert 2026-07-13 (AB#21271): status oppdatert fra
proposedtilaccepted— arkitekturen beskrevet her er bygget, testdekket og i drift. Typenavn og fillenker er oppdatert etter det norske domenespråk-omdøpet (ADR-015):Rule→Regel,RuleKind→RegelType,RuleEvaluationResult→RegelEvalueringsResultat,CaseEvaluation→Regelresultat.
Kontekst og problemstilling⚓︎
ADR-010 ble akseptert 2026-05-21 med valget «MAF Workflows + YAML-deklarert regelmotor»: regler i
YAML-filer som embedded resources, en funksjon-peker per regel til en C#-metode (IRegelFunksjon
+ FunksjonRegistry), IDataRetriever for datahenting, og MAF Workflows som beslutningstaker.
Ingen av disse komponentene finnes i kodebasen. Det som ble bygget — først i regelmotor-spiken 2026-04-27, deretter i v2-restruktureringen og US #20185 — er en annen arkitektur:
- Regelmotor: Microsoft RulesEngine evaluerer strukturerte regler; en LLM evaluerer
skjønnsregler. Regeldefinisjoner lagres som JSON i Postgres (
Regel.DefinitionJson), ikke som YAML embedded resources. - Beslutningstaker: en egen, native
WorkflowGraphRunner— ikke MAF Workflows.
ADR-010 er dermed faktuelt feil om systemet den beskriver. «Presiseringer ved akseptering»-blokken
korrigerte beslutningstakeren til native runner, men resten av ADR-010 (regelmotor-mekanismen,
implementasjonsplanen, IRegelFunksjon/FunksjonRegistry/IDataRetriever) beskriver fortsatt
ikke-eksisterende kode. En agent eller utvikler som leser ADR-010 og stoler på den, vil bygge feil.
ADR-010s revisit-trigger («hvis antall virkemidler overstiger ~10 … vurder Microsoft RulesEngine på nytt») er teknisk utløst — det er 37 virkemidler — men poenget er irrelevant: RulesEngine var ikke et fremtidig alternativ å revurdere, det var allerede den implementerte løsningen da ADR-010 ble akseptert.
Denne ADR-en bringer beslutningsdokumentasjonen i samsvar med koden, og fastsetter de to forskjellene fra ADR-010 (regelmotor og beslutningstaker) som bevisste, besluttede valg.
Nøkkeldrivere⚓︎
- Beslutningsdokumentasjon må stemme med koden — en ADR som beskriver ikke-eksisterende komponenter er verre enn ingen ADR.
- Regler skal kunne opprettes og endres uten ny deploy. Regler som kompilerte C#-funksjoner (ADR-010s valg) krever rekompilering og deploy for hver regelendring og hver nye regel. Med 37 virkemidler × flere regler hver er det en uakseptabel driftsmodell.
- Regelkjøringer skal være sporbare og deterministiske: saksbehandler skal kunne se hvilke regler som kjørte, mot hvilke fakta, med hvilket utfall og hvilken begrunnelse.
- Regler skrives av utviklere (i samråd med fagansvarlige for faglige avklaringer) — ikke av fagansvarlige direkte. RulesEngines uttrykkssyntaks er akseptabel lesbarhet for utviklere.
- Strukturerte sjekker (terskelverdier, datoer, sammenligninger) skal evalueres deterministisk; skjønnsvurderinger skal kunne uttrykkes som LLM-prompts.
- Løsningen skal kjøre på .NET 10 og passe inn i v2-arkitekturen (Application/Domain/Infrastructure).
- Eksterne rammeverk for orkestrering vurderes som leverandøravhengighet; MAF Workflows er fortsatt prerelease.
Vurderte alternativer⚓︎
ADR-010 vurderte fire alternativer for regelmotoren (C# pipeline, Microsoft RulesEngine, NRules, MAF Workflows + YAML) — den analysen gjentas ikke her. Etter at RulesEngine-hybriden og den native runneren allerede er bygget og testdekket, er det reelle valget nå avgrenset:
- Behold og fastsett det som er bygget — RulesEngine-hybrid + native
WorkflowGraphRunner. - Riv ut og bygg ADR-010s løsning — MAF Workflows + YAML-funksjoner. Forkaster fungerende, testdekket kode; tar inn en prerelease-avhengighet; samtale-/agent-orientert rammeverk som ikke passer en saks-evalueringspipeline.
- Behold regelmotoren, bytt beslutningstaker til MAF Declarative Workflows — gir checkpointing og løkker, men forkaster den native runneren og dens YAML-format, og krever fortsatt en prerelease-avhengighet.
Beslutning⚓︎
Valgt alternativ: alternativ 1 — fastsett RulesEngine-hybriden og den native workflow-grafen som besluttet arkitektur.
Regelmotor: hybrid RulesEngine + LLM Judgment⚓︎
Regel er et domene-aggregat (Regel.cs) persistert i
Postgres. Hver regel har en Kind-diskriminator og en DefinitionJson:
RegelType.Structured—DefinitionJsoner en RulesEngineWorkflow(JSON). Evalueres deterministisk av RulesEngineEvaluator.RegelType.Judgment—DefinitionJsoner enJudgmentDefinition(Prompt,OutputType,RequiredFacts), derPrompter hele prompt-teksten inline (AB#21258). Evalueres av en LLM via JudgmentEvaluator (IChatClient), med typet, strukturert utdata.
KindDispatchingRuleEvaluator
implementerer IRegelEvaluator og dispatcher på Kind. Begge grener returnerer samme
RegelEvalueringsResultat (Verdict = Met/NotMet/NotApplicable/Error, Reason, ReasonDetail,
AiCallAuditRef).
Et strukturert regeluttrykk ser slik ut:
Beslutningstaker: native WorkflowGraphRunner⚓︎
WorkflowGraphRunner er
~140 linjer egen kode som eksekverer en graf av regel-noder. Kanttyper: Direct, Conditional,
FanOut, FanIn. Grafen lagres som YAML i Postgres (WorkflowDefinitionRecord.YamlContent,
deserialisert av WorkflowYamlDeserializer). HITL-pause skjer ved at en regel-node returnerer
RuleResult.WaitingForHuman = true. Orkestreringslaget er 100 % deterministisk C#; LLM finnes kun
inne i Judgment-regler.
Fakta-laget⚓︎
IFactsExtractorRegistry løser
opp riktig Facts-type og ekstraktor per virkemiddel. Facts er per-virkemiddel C#-records (f.eks.
EnergiYrkesbyggFacts) — ikke en felles katalog.
Konsekvenser⚓︎
- Positivt: regler opprettes og endres som data i Postgres (
Regel.DefinitionJson, nyVersion,StatusDraft→Active) — ingen deploy for nye eller endrede regler. - Positivt: strukturerte regler er deterministiske og enhetstestbare (
RulesEngineEvaluatorTests). - Positivt:
RegelresultatlagrerFactsSchemaVersion,FactsSnapshotHash(SHA-256) ogEngineVersion— full sporbarhet for hver kjøring. - Positivt: orkestreringsgrafen er ren deterministisk C#; ikke-determinisme er innkapslet i
Judgment-noder og isolert som egneRegelEvalueringsResultat. - Positivt: ingen ekstern rammeverksavhengighet for orkestrering — teamet eier hele runneren.
- Negativt: RulesEngine-uttrykk er ikke statisk typesjekket — feil i et uttrykk oppdages først ved
kjøring (
Verdict.Error). Se «Kjente begrensninger». - Negativt: den native runneren har ikke durable resume eller løkker. Se «Kjente begrensninger».
- Nøytralt:
Microsoft.RulesEngine6.0.0 er en ny avhengighet — men stabil og GA, i motsetning til MAF Workflows (prerelease).
Prompt-forvaltning: inline i regelen, ikke eget register (AB#21258, 2026-07-14)⚓︎
Judgment-prompten lå opprinnelig i et eget kode-register (InMemoryPromptTemplateRegistry), og
JudgmentDefinition pekte på den via (PromptTemplateId, PromptTemplateVersion). To auditer
anbefalte motsatt løsning: (a) inline prompten i regelen for én kilde, (b) behold registeret for
gjenbruk på tvers av virkemidler. Besluttet: inline — prompt-teksten bor i prompt-feltet i
regelens YAML og bæres uendret inn i JudgmentDefinition.Prompt (og dermed DefinitionJson).
Begrunnelse: (1) virkemiddelansvarlig ser hele regelen — inkludert prompten — på ett sted;
(2) feilklassen «regel peker på ikke-eksisterende prompt» blir strukturelt umulig — det finnes
ingen peker å bomme på, teksten er definisjonen (registeret gav bare en degradert Verdict.Error
ved oppslagsfeil, ikke en fastslått garanti); (3) prompten versjoneres nå sammen med regelen
(Regel.Version), ikke i et parallelt versjonsskjema. Kostnad: tre domene-nøytrale prompter
(gyldighet/kontrakt/MVA) dupliseres på tvers av de to virkemidlene — men per-virkemiddel-eierskap er
mer korrekt enn delt id-kobling, der en endring i det ene virkemiddelet ellers stille ville endret
det andre. Historikk over tidligere prompt-tekst eies av den DB-authored versjoneringen (AB#21018)
og append-only-historikken (AB#21260), ikke av et register.
Arkitektur (som bygget)⚓︎
flowchart TD
A[EvaluateCaseCommand] --> R[IRegelRepository.GetApplicableAsync<br/>virkemiddel × ErsStatus × Mode]
W[RunCaseWorkflowCommand] --> G[WorkflowGraphRunner<br/>graf av regel-noder]
R --> F[IFactsExtractorRegistry.ExtractAsync]
G --> F
F --> E[IRegelEvaluator = KindDispatchingRuleEvaluator]
E -- Structured --> RE[RulesEngineEvaluator<br/>RulesEngine 6.0.0]
E -- Judgment --> JE[JudgmentEvaluator<br/>IChatClient]
RE --> RES[RegelEvalueringsResultat]
JE --> RES
RES --> CE[Regelresultat<br/>persistert i Postgres]
Det finnes to orkestrerings-innganger som begge ender i samme IRegelEvaluator og Regelresultat:
| Inngang | Handler | Orkestrering | EngineVersion |
|---|---|---|---|
EvaluateCaseCommand |
EvaluateCaseHandler | Flat liste — alle anvendelige regler i rekkefølge | RulesEngine/6.0.0 |
RunCaseWorkflowCommand |
RunCaseWorkflowHandler | Graf via WorkflowGraphRunner |
WorkflowGraphRunner/1.0.0 |
RunCaseWorkflowJob (TickerQ, ADR-012) er den durable inngangen og
kaller graf-stien.
Berørte filer⚓︎
src/Enova.Kai.Domain/Rules/—Regel,RegelType,RegelStatus,Verdict,RegelEvalueringsResultat,Regelresultatsrc/Enova.Kai.Application/Rules/—IRegelEvaluator,IFactsExtractorRegistry,JudgmentDefinition,EvaluateCaseHandler,ActivateRule*, Facts-recordssrc/Enova.Kai.Infrastructure/Rules/Engine/—RulesEngineEvaluator,JudgmentEvaluator,KindDispatchingRuleEvaluatorsrc/Enova.Kai.Application/WorkflowEngine/—WorkflowGraphRunner,EdgeType,WorkflowGraphConfiguration,RunCaseWorkflowHandlersrc/Enova.Kai.Infrastructure/Persistence/—RuleRepository,RuleConfiguration,WorkflowDefinitionRecord,WorkflowConfigurationSeeder,Yaml/WorkflowYamlDeserializersrc/Enova.Kai.Infrastructure/Enova.Kai.Infrastructure.csproj—PackageReference RulesEngine
Kjente begrensninger⚓︎
Disse er reelle hull i den bygde løsningen. De er ikke blokkerende for første reelle bruk, men de må følges opp og er listet her slik at ingen tror de er løst.
- Regeluttrykk valideres ikke ved aktivering.
ActivateRuleValidator
sjekker kun
RuleIdogActivatedBy. Et RulesEngine-uttrykk som refererer et felt Facts-typen ikke har, girVerdict.Errorførst ved kjøring. → Oppfølging: tørrkjør uttrykket mot Facts-skjemaet iActivateRule-stien. - HITL-persistens er ufullstendig.
Regelresultathar ikkeStatusellerPausedAtRuleId.WorkflowGraphRunnerkan pause påWaitingForHuman, men en pauset kjøring lagres ikke som gjenopptakbar tilstand. → Oppfølging: ADR-010s presisering omRegelresultat.Status+resume-endepunkt (Task #20423). - Ingen durable resume.
WorkflowGraphRunner.ExecuteAsyncstarter alltid på__start__og tar ingen «gjenoppta fra node»-parameter. Etter en HITL-pause vil en gjenopptakelse re-eksekvere hele grafen — inkludertJudgment-noder, som da gir ny LLM-kostnad og ny ikke-determinisme. → Oppfølging: persistérRuleContext+ pausepunkt, og la runneren hoppe forbi noder som allerede har resultat. - Ingen løkke-kant. Runneren støtter ikke løkker (f.eks. «be om manglende fakta → re-ekstraher»).
Manglende fakta håndteres i stedet som en gate-node (
FactsAvailabilityExecutor). - Workflow-grafer er tomme. Den seedede grafen for Tunge nullutslipp har
nodes: []/edges: []; graf-stien short-circuiter. I praksis kjører evaluering i dag som flat regelliste viaEvaluateCaseHandler. Grafene må fylles inn per virkemiddel. - Produksjons-wiring ufullstendig.
RuleEngineStubsregistrererNotImplementedChatClientogPendingFactsExtractorRegistryi Postgres-modus — ekteIChatClient(Azure AI Foundry) og Facts-ekstraktor er ikke wiret ennå. - Judgment: strukturert utdata ikke håndhevet.
ChatResponseFormat.ForJsonSchemaer ikke satt; malformert LLM-svar degraderer tilVerdict.Error.RenderPromptfletter søker-kontrollert dokumenttekst inn i prompten — kjent prompt-injection-vektor (R026).
Verifisering⚓︎
- [x]
RulesEngineEvaluatorevaluererStructured-regler fraRegel.DefinitionJson(RulesEngineEvaluatorTestspasserer). - [x]
JudgmentEvaluatorevaluererJudgment-regler viaIChatClientmed typet utdata. - [x]
KindDispatchingRuleEvaluatordispatcher korrekt påRegelType. - [x]
WorkflowGraphRunnereksekverer Direct/Conditional/FanOut/FanIn og pauser påWaitingForHuman(WorkflowGraphRunnerTests). - [x]
RegelresultatlagrerFactsSchemaVersion,FactsSnapshotHashogEngineVersion. - [ ] Regeluttrykk valideres ved aktivering (begrensning 1).
- [ ] Pauset
Regelresultatlagres som gjenopptakbar tilstand medStatus/PausedAtRuleId(begrensning 2–3). - [ ] Workflow-graf fylt inn for minst ett virkemiddel (begrensning 5).
- [ ] Ekte
IChatClientog Facts-ekstraktor wiret i Postgres-modus (begrensning 6).
Revisit-triggere⚓︎
- Hvis durable resume over lange HITL-pauser blir et krav, og re-kjøring av
Judgment-noder ved gjenopptakelse blir et reelt kostnads- eller determinismeproblem: implementer checkpoint-skip iWorkflowGraphRunner, eller revurder MAF Declarative Workflows når den er GA. - Hvis workflow-grafer trenger løkker: legg til en løkke-kanttype, eller revurder MAF.
- Hvis regler skal forfattes av fagansvarlige direkte (ikke utviklere): RulesEngines uttrykkssyntaks er ikke egnet — revurder et mer fagvennlig regelformat.
Tilleggsinformasjon⚓︎
- Erstatter ADR-010. ADR-010 settes til
superseded. - Relaterte beslutninger: ADR-011 (Postgres for fakta og regelresultater), ADR-012 (TickerQ — durable inngang for graf-stien), ADR-006 (købasert analyse).
- Regelmotor-spiken som etablerte hybriden:
docs/superpowers/plans/2026-04-27-rules-engine-spike.md. - Regelkilde og versjonering (hvordan SharePoint-regler speiles til v2) er fortsatt utsatt til egen ADR — uendret fra ADR-010.