From 191ad15ec2383868fa62ba8d93c9e2e639eccf4d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Jul 2026 16:28:58 +0000 Subject: [PATCH] =?UTF-8?q?fix(compliance):=20correct=20is=5Foverdue=5Ffor?= =?UTF-8?q?=5Freview=20date=20math=20=E2=80=94=20CEL=20has=20no=20Timestam?= =?UTF-8?q?p+int=20(#84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `is_overdue_for_review` was: daysFromNow(0) > record.last_assessed_at + record.review_frequency_days * 86400000 The formula engine is CEL, where dates are Timestamps and there is NO `Timestamp + int` operator — so `last_assessed_at + N` (and the whole RHS) evaluated to null for every control that HAS a last_assessed_at. The result: only never-assessed controls (the `last_assessed_at == null` branch) ever flagged as overdue; a control assessed long ago but past its review cadence never did. The `* 86400000` (a "days → epoch ms" conversion) was doubly wrong: CEL date arithmetic is day-granular via builtins, not millisecond epochs. Fix — use the CEL `addDays(date, n)` builtin and a Timestamp comparison: addDays(record.last_assessed_at, record.review_frequency_days) < today() i.e. overdue when the next-review date (last assessment + cadence days) is already in the past. Boot-verified against a fresh dev server (@objectstack 15.1.1, 0 errors) — all eight seeded controls now compute correctly: - GDPR Art.32 (assessed ~200d ago, 180d cadence) → overdue = true [was false] - HIPAA §164.308(a)(1) (~40d ago, 365d), CC6.1/CC7.1/CC8.1 (<90d), A.5.1 (100d/180d) → overdue = false - A.8.16 / 164.312(a)(1) (never assessed) → overdue = true The "Overdue for Review" list view (filter is_overdue_for_review = true) now surfaces stale-but-assessed controls, not just never-assessed ones. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01BZguyAaQbyUpwMZ2gMLaAP --- .../src/objects/compliance_control.object.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/compliance/src/objects/compliance_control.object.ts b/packages/compliance/src/objects/compliance_control.object.ts index 7205edc..25bd035 100644 --- a/packages/compliance/src/objects/compliance_control.object.ts +++ b/packages/compliance/src/objects/compliance_control.object.ts @@ -96,7 +96,15 @@ export const Control = ObjectSchema.create({ is_overdue_for_review: Field.formula({ label: 'Overdue for Review', group: 'ownership', - expression: F`record.last_assessed_at == null || record.review_frequency_days == null || daysFromNow(0) > record.last_assessed_at + record.review_frequency_days * 86400000`, + // Overdue if never assessed, or the next-review date (last assessment + + // review_frequency_days) is already in the past. The formula engine is CEL: + // dates are Timestamps and there is NO `Timestamp + int` operator, so day + // arithmetic must go through the `addDays(date, n)` builtin (Timestamp + // comparison with `<` is supported). The old `last_assessed_at + + // review_frequency_days * 86400000` silently evaluated to null for every + // assessed control (no `+` on Timestamp), so only never-assessed controls + // ever flagged — a stale-but-assessed control (e.g. GDPR Art.32) was missed. + expression: F`record.last_assessed_at == null || record.review_frequency_days == null || addDays(record.last_assessed_at, record.review_frequency_days) < today()`, }), notes: Field.markdown({ label: 'Notes', group: 'meta' }),