Migration to OpenFisca-Inspired Single Source of Truth Architecture
Problem Statement
The current PAA architecture uses 4 separate artifacts to encode a single Belgian social benefit:
- Gherkin Feature (
features/benefits/*.feature) - Human-readable specification
- Rules (
src/rules/*Rules.ts) - json-rules-engine implementation
- Machine (
src/workflows/*Machine.ts) - XState state machine (optional)
- Legal Metadata (
src/domain/legalMetadata.ts) - Legal references
This creates several issues:
1. Synchronization Overhead
- Version compliance system (
checkVersionCompliance) needed to ensure feature version matches rules version
- When law changes, must update 3-4 files manually or via AI pipeline
add-new-law pipeline generates 3 separate files that can drift
2. Duplication
- Eligibility conditions written in Gherkin (
Étant donné que...) AND in json-rules-engine
- Amounts appear in both feature files and rules files
- Legal references scattered across files
3. Complexity
- Developers must understand Gherkin, json-rules-engine, AND XState
- AI pipeline (
pipelineOrchestrator.ts) has complex multi-step generation
- Testing requires both Cucumber (for features) and Jest (for rules)
4. Cognitive Load
- To understand RIS eligibility, must read:
ris.feature + risRules.ts + possibly risMachine.ts
- No single place to see complete benefit logic
Research: Industry Standards for Rules-as-Code
OpenFisca (Recommended)
- Used by: France, New Zealand, Barcelona, Australia
- Recognized by: OECD as principal Rules-as-Code implementation
- Architecture: Single Python Variable class = specification + implementation
- Parameters: YAML files for amounts/thresholds (separate from logic)
- URL: https://openfisca.org/
# OpenFisca example - SINGLE SOURCE OF TRUTH
class revenu_integration_sociale(Variable):
value_type = float
entity = Person
label = "Revenu d'Intégration Sociale"
definition_period = MONTH
reference = "https://www.ejustice.just.fgov.be/..."
def formula(person, period, parameters):
age = person('age', period)
eligible = (age >= 18) * (residency >= 5)
return eligible * parameters(period).ris.amount
Catala
- Innovation: Literate programming - law text and code interleaved
- Strength: Mirrors legal text structure, built-in exception handling
- Used for: French family benefits, US Tax Code
- Found bugs: In official French government implementation
- URL: https://catala-lang.org/
Blawx
- Type: Visual drag-and-drop interface
- Target: Non-programmers encoding laws
- Developed by: Canadian government
- URL: https://dev.blawx.com/
L4/Legalese
Proposed Solution: OpenFisca-Inspired TypeScript Architecture
New Structure (2 artifacts instead of 4)
src/
benefits/
ris.benefit.ts # Single source: logic + docs + legal refs
agr.benefit.ts
index.ts
parameters/
benefits/
ris.yaml # Amounts only (easy to update without code change)
agr.yaml
engine/
types.ts # Variable, Person, Period types
defineVariable.ts # OpenFisca-like Variable factory
calculate.ts # Runtime calculation engine
generateDocs.ts # Auto-generate markdown from Variables
generateTests.ts # Auto-generate test cases
workflows/ # KEEP only for multi-step procedures
conversionMachine.ts # Legal text simplification still needs state
Example: RIS as Single Source of Truth
// src/benefits/ris.benefit.ts
export const RIS = defineVariable({
name: 'revenu_integration_sociale',
type: 'money',
entity: 'person',
period: 'month',
// Replaces Gherkin feature header
label: "Revenu d'Intégration Sociale",
description: `
Le RIS est une aide financière accordée par le CPAS aux personnes
qui ne disposent pas de ressources suffisantes pour mener une vie
conforme à la dignité humaine.
`,
// Replaces scattered legal references
reference: {
law: 'Loi du 26 mai 2002 concernant le droit à l\'intégration sociale',
url: 'https://www.ejustice.just.fgov.be/eli/loi/2002/05/26/2002022559/justel',
articles: ['Art. 3', 'Art. 14'],
authority: 'SPF Sécurité Sociale',
},
// Replaces Gherkin scenarios + json-rules-engine conditions
formula: (person, period, params) => {
// Eligibility conditions (readable, type-safe)
const age = person.get('age', period);
const residencyYears = person.get('residency_years', period);
const income = person.get('income', period);
const category = person.get('family_category', period);
const isAdult = age >= 18;
const hasResidency = residencyYears >= params.ris.min_residency_years;
const belowThreshold = income < params.ris.income_threshold[category];
if (!isAdult || !hasResidency || !belowThreshold) {
return { eligible: false, amount: 0, reason: determineReason(...) };
}
return {
eligible: true,
amount: params.ris.amounts[category],
reason: 'Toutes les conditions sont remplies',
};
},
});
Parameters in YAML (amounts separated from logic)
# parameters/benefits/ris.yaml
ris:
metadata:
version: "2025.1.0"
effective_date: "2024-09-01"
source: "Arrêté royal du 12 décembre 2023"
min_residency_years: 5
income_threshold:
single: 1214.13
cohabitant: 809.42
family: 1640.83
amounts:
single: 1214.13
cohabitant: 809.42
family: 1640.83
What Gets Removed
| Current Artifact |
Status |
Replacement |
features/*.feature |
❌ Remove |
label, description, reference in Variable |
src/rules/*Rules.ts |
❌ Remove |
formula function in Variable |
json-rules-engine dependency |
❌ Remove |
Native TypeScript |
| Cucumber/Gherkin dependencies |
❌ Remove |
Jest tests generated from Variables |
| Version sync checking |
❌ Remove |
Single file = always in sync |
src/workflows/*Machine.ts |
⚠️ Keep selectively |
Only for multi-step procedures |
What Gets Kept
| Component |
Reason |
| XState machines |
Still needed for complex multi-step procedures (e.g., legal text conversion) |
| TypeORM/PostgreSQL |
Data persistence unchanged |
| Fastify API |
HTTP layer unchanged |
| Redis caching |
Performance layer unchanged |
Benefits of Migration
1. Single Source of Truth
- One file per benefit contains ALL information
- No version sync issues
- No drift between specification and implementation
2. Simplified Pipeline
Current: Legal Text → [Claude] → Feature + Rules + Machine (3 files)
Proposed: Legal Text → [Claude] → Variable + Parameters (2 files)
3. Better Developer Experience
- Understand benefit by reading ONE file
- Type-safe formulas (TypeScript > json-rules-engine JSON)
- IDE autocomplete for all conditions
4. Easier Maintenance
- Amount changes: edit YAML only, no code changes
- Logic changes: edit formula only
- Legal reference updates: one place
5. Auto-Generated Documentation
- Generate markdown docs from Variable metadata
- Generate OpenAPI specs from Variable definitions
- Generate test cases from formula conditions
6. Reduced Dependencies
- Remove:
@cucumber/cucumber, json-rules-engine
- Keep:
xstate (for complex workflows only)
Migration Plan
Phase 1: Create Engine Infrastructure
Phase 2: Migrate Existing Benefits
Phase 3: Update Pipeline
Phase 4: Add Documentation Generation
Phase 5: Cleanup
Risks and Mitigations
| Risk |
Mitigation |
| Loss of Gherkin readability for legal experts |
Generate markdown docs with same structure |
| Formula complexity for non-developers |
Clear comments, generated documentation |
| Migration breaks existing tests |
Run both systems in parallel during migration |
| XState machines still needed |
Keep for genuinely stateful procedures |
Decision Required
-
Full OpenFisca adoption: Use actual OpenFisca framework (Python)
- Pro: Proven, large community, built-in features
- Con: Requires Python, different tech stack
-
OpenFisca-inspired TypeScript (Recommended): Copy patterns, keep TypeScript
- Pro: Keeps existing tech stack, type safety, simpler
- Con: Must build some infrastructure ourselves
-
Catala adoption: Use Catala DSL
- Pro: Most readable, literate programming
- Con: Steep learning curve, custom tooling, smaller community
References
Labels
architecture, refactoring, tech-debt, rules-as-code, future-migration
Priority
Medium - Current system works but creates maintenance overhead
Effort Estimate
Large - Estimated 2-3 weeks of focused work for full migration
Migration to OpenFisca-Inspired Single Source of Truth Architecture
Problem Statement
The current PAA architecture uses 4 separate artifacts to encode a single Belgian social benefit:
features/benefits/*.feature) - Human-readable specificationsrc/rules/*Rules.ts) - json-rules-engine implementationsrc/workflows/*Machine.ts) - XState state machine (optional)src/domain/legalMetadata.ts) - Legal referencesThis creates several issues:
1. Synchronization Overhead
checkVersionCompliance) needed to ensure feature version matches rules versionadd-new-lawpipeline generates 3 separate files that can drift2. Duplication
Étant donné que...) AND in json-rules-engine3. Complexity
pipelineOrchestrator.ts) has complex multi-step generation4. Cognitive Load
ris.feature+risRules.ts+ possiblyrisMachine.tsResearch: Industry Standards for Rules-as-Code
OpenFisca (Recommended)
Catala
Blawx
L4/Legalese
Proposed Solution: OpenFisca-Inspired TypeScript Architecture
New Structure (2 artifacts instead of 4)
Example: RIS as Single Source of Truth
Parameters in YAML (amounts separated from logic)
What Gets Removed
features/*.featurelabel,description,referencein Variablesrc/rules/*Rules.tsformulafunction in Variablejson-rules-enginedependencysrc/workflows/*Machine.tsWhat Gets Kept
Benefits of Migration
1. Single Source of Truth
2. Simplified Pipeline
3. Better Developer Experience
4. Easier Maintenance
5. Auto-Generated Documentation
6. Reduced Dependencies
@cucumber/cucumber,json-rules-enginexstate(for complex workflows only)Migration Plan
Phase 1: Create Engine Infrastructure
defineVariable()helper functioncalculate()runtime engineVariable,Person,Period,ParametersPhase 2: Migrate Existing Benefits
Phase 3: Update Pipeline
add-new-lawto generate Variable formatpipelineOrchestrator.tsPhase 4: Add Documentation Generation
generateDocs.ts(Variable → Markdown)generateTests.ts(Variable → Jest tests)Phase 5: Cleanup
features/directorysrc/rules/*Rules.tsfilesRisks and Mitigations
Decision Required
Full OpenFisca adoption: Use actual OpenFisca framework (Python)
OpenFisca-inspired TypeScript (Recommended): Copy patterns, keep TypeScript
Catala adoption: Use Catala DSL
References
Labels
architecture,refactoring,tech-debt,rules-as-code,future-migrationPriority
Medium - Current system works but creates maintenance overhead
Effort Estimate
Large - Estimated 2-3 weeks of focused work for full migration