"REW parameter versioning — immutable versioned formula parameters with 100% replay-equivalence on prior payslips"
§1 — Description (BCP-14 normative)
The REW service MUST ship parameter versioning at services/rew/src/params/ with immutable snapshots + version-pinned payslip computation + monthly replay-equivalence CI, 4 memory audit kinds.
- MUST validate
param_kindagainst closed enum per DEC-2161.
- MUST define tables at migration
0002: ``sql CREATE TABLE rew_param_versions ( version_id UUID PRIMARY KEY, tenant_id UUID NOT NULL, kind TEXT NOT NULL CHECK (kind IN ('tax_bracket','si_rate','overtime_multiplier','allowance_cap','bonus_formula')), value_jsonb JSONB NOT NULL, effective_from DATE NOT NULL, effective_to DATE, source_law_reference TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), created_by UUID NOT NULL ); CREATE INDEX param_kind_effective_idx ON rew_param_versions(tenant_id, kind, effective_from DESC); ALTER TABLE rew_param_versions ENABLE ROW LEVEL SECURITY; CREATE POLICY param_rls ON rew_param_versions USING (tenant_id = current_setting('auth.tenant_id')::uuid) WITH CHECK (tenant_id = current_setting('auth.tenant_id')::uuid); REVOKE UPDATE, DELETE ON rew_param_versions FROM cyberos_app;``
- MUST lookup at
loader.rs::get(tenant_id, kind, effective_at)per DEC-2162:
- Returns version effective at the date.
- Deterministic — same params → same result (critical for TASK-REW-005).
- MUST run monthly replay-equivalence test via TASK-MCP-007 per DEC-2163:
- For last 12 months of payslips, re-run compute with original version_id.
- Expect byte-identical output.
- Failure → sev-1 alert + CI block.
- MUST expose endpoints: ``
text POST /v1/rew/params (CFO; new version) GET /v1/rew/params/{kind}?at=... (lookup at date) POST /v1/rew/replay-test/trigger (CFO manual)``
- MUST emit 4 memory audit kinds per DEC-2164. PII per TASK-MEMORY-111: param values (mostly rates/brackets) ok in memory as they're public; member-specific compute hashes only.
- MUST thread trace_id from lookup → audit.
- MUST NOT mutate prior version per DEC-2162 (REVOKE UPDATE/DELETE).
- MUST NOT use
now()or random in compute path per DEC-2163.
§2 — Why this design
Why versioning (DEC-2160)? TASK-REW-005 payroll must reproduce historical payslips exactly; mutable params break replay.
Why replay test (DEC-2163)? Without automated check, drift creeps in unnoticed; monthly CI catches early.
Why immutable (DEC-2162)? Audit lineage requires unmutable history; corrections via new version with new effective_from.
§3 — API contract
Sample param lookup:
GET /v1/rew/params/tax_bracket?at=2026-06-01
{
"version_id": "uuid",
"kind": "tax_bracket",
"value_jsonb": [
{"min_vnd": 0, "max_vnd": 5000000, "rate": 0.05},
{"min_vnd": 5000000, "max_vnd": 10000000, "rate": 0.10}
],
"effective_from": "2025-01-01",
"source_law_reference": "Decree 152/2020 Art. 7"
}
§4 — Acceptance criteria
- param_kind enum cardinality 5. 2. Immutable rows (REVOKE). 3. Lookup by effective_at. 4. Replay test monthly via cron. 5. 100% match for prior periods. 6. Failure → sev-1 + CI block. 7. 4 memory audit kinds emitted. 8. PII: param values public; compute hashed. 9. RLS denies cross-tenant. 10. CFO-only write. 11. Trace_id preserved. 12. Append-only via REVOKE. 13. JSONB schema validated per kind. 14. Source law reference recommended. 15. Effective_to NULL = current. 16. Index on (kind, effective_from). 17. Lookup performance < 5ms. 18. Annual refresh runbook. 19. Replay test produces diff report on failure. 20. Deterministic (no now/random).
§5 — Verification
#[tokio::test]
async fn version_lookup_at_date() {
let ctx = TestContext::with_two_tax_bracket_versions("2024-01", "2025-01").await;
let v_2024 = ctx.get_param("tax_bracket", "2024-06-01").await;
let v_2025 = ctx.get_param("tax_bracket", "2025-06-01").await;
assert_ne!(v_2024.version_id, v_2025.version_id);
}
#[tokio::test]
async fn immutability_enforced() {
let ctx = TestContext::with_param_version().await;
let r = ctx.try_update_param(ctx.version_id).await;
assert!(r.is_err());
}
#[tokio::test]
async fn replay_equivalence_100pct() {
let ctx = TestContext::with_12_months_payslips().await;
let result = ctx.run_replay_test().await;
assert_eq!(result.match_pct, dec!(100.0));
}
// 5.4..5.10
§7 — Dependencies
Upstream: TASK-REW-001. Downstream: TASK-REW-005 (payroll compute uses versioned params). Cross-module: TASK-HR-005 (compares versioning approach), TASK-MCP-007 (replay cron), TASK-MEMORY-111 (audit).
§10 — Failure modes
| Failure | Detection | Outcome | Recovery |
|---|---|---|---|
| Replay test fail | sev-1 alert | CI block | investigate diff |
| Lookup at date with no version | error | 404 | seed gap |
| Two versions same date | UNIQUE on (kind, effective_from) | reject | use later date |
| JSONB schema invalid | validator | 400 | fix shape |
| Cross-tenant query | RLS | 0 rows | inherent |
| Non-CFO write | role check | 403 | request CFO |
| Decimal precision drift | rust_decimal | inherent | inherent |
| Cron skipped | catch-up | inherent | inherent |
| Test data corrupt | sev-1 | manual review | data fix |
| Param value range invalid | range check | 400 | fix |
§11 — Implementation notes
- §11.1 Replay test reruns TASK-REW-005 compute with member context at original period; compares to stored payslip blob hash.
- §11.2 Cron via TASK-MCP-007
kind: 'rew.replay_equivalence_test', monthly 1st at 02:00. - §11.3 Failure produces diff report uploaded to TASK-DOC-001 for CFO review.
- §11.4 memory audit body: tenant_id, kind, version_id; param values ok (not PII).
- §11.5 JSONB schema per kind documented in code constants + tested.
End of TASK-REW-002 spec.