|
| 1 | +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. |
| 2 | + |
| 3 | +import type { SemanticMigration } from '../../types.js'; |
| 4 | + |
| 5 | +export const entry: SemanticMigration = { |
| 6 | + id: 'formula-date-subtraction-refused', |
| 7 | + surface: |
| 8 | + 'data.Field.expression of a formula field (and any CEL predicate judged with the object\'s field types: ' |
| 9 | + + 'validation conditions, flow conditions) — a date or datetime field subtracted from another ' |
| 10 | + + '(record.end_date - record.start_date), or arithmetic on one with a duration ' |
| 11 | + + '(record.start_date - duration("24h")); and, as a formula value, an expression that computes a ' |
| 12 | + + 'duration (today() - record.start_date, now() - record.start_at)', |
| 13 | + replacement: |
| 14 | + 'daysBetween(a, b) for the span in whole days: daysBetween(record.start_date, record.end_date) for end ' |
| 15 | + + 'minus start, daysBetween(record.start_date, today()) for the days since a date. addDays(d, n) / ' |
| 16 | + + 'addMonths(d, n) / daysFromNow(n) to shift a date. A predicate may still compare a duration, ' |
| 17 | + + 'today() - record.start_date > duration("72h")', |
| 18 | + reason: |
| 19 | + 'The authoring type-soundness gate declared a date or datetime field a CEL Timestamp, so the ' |
| 20 | + + 'subtraction of two of them checked as a legal Duration and every door passed it: os validate, ' |
| 21 | + + 'os build, os lint and the object save door. The runtime never holds a Timestamp there: every read ' |
| 22 | + + 'door hands a date field out as its YYYY-MM-DD text and a datetime field as its ISO-8601 instant ' |
| 23 | + + 'text, so the evaluator faulted on the subtraction and the field read null for every record, with ' |
| 24 | + + 'a server WARN as the only trace; a timestamp minus a date field evaluated to a Duration, which a ' |
| 25 | + + 'field serialises as {}. The gate now re-checks what the Timestamp typing passes with the field ' |
| 26 | + + 'declared as that text, and refuses both shapes (date-text-arithmetic, and date-duration-value for a ' |
| 27 | + + 'formula). The (record.end_date - record.start_date) + 1 shape keeps its existing date-arithmetic ' |
| 28 | + + 'refusal and words. There is no mechanical rewrite: which span the author meant (the days between, ' |
| 29 | + + 'inclusive or not, or hours) is a judgement about the field, and a stored formula of the refused ' |
| 30 | + + 'shape read null or {} for every record, so no stored value depends on it.', |
| 31 | + acceptanceCriteria: |
| 32 | + 'os validate and os build report no date-text-arithmetic or date-duration-value finding; each ' |
| 33 | + + 'rewritten formula field reads a number on GET /api/v1/data/OBJECT/ID where it read null or {} ' |
| 34 | + + 'before; no stored sys_metadata object row reports either finding in its _diagnostics.', |
| 35 | +}; |
0 commit comments