diff --git a/.changeset/29061-formula-skill-date-subtraction-refused.md b/.changeset/29061-formula-skill-date-subtraction-refused.md new file mode 100644 index 00000000000..b4ac2d62863 --- /dev/null +++ b/.changeset/29061-formula-skill-date-subtraction-refused.md @@ -0,0 +1,12 @@ +--- +'@objectstack/skills': patch +--- + +docs(skills): objectstack-formula says the build refuses date subtraction (`end - start`), and the skills stop describing a stored formula that subtracts dates + +Clause-②: no +Metadata-protocol: none + +- **Before.** The "No date arithmetic" note in `objectstack-formula` said `end - start` "does not fault — it yields a `duration` stored as `{}`", which read as a shape that builds. `objectstack-ui` `rules/dashboards.md` and an analytics eval described a leave-days formula written as `end - start`, and a formula eval explained `record.close_date - today()` as "a duration stored as `{}`". +- **Now.** The note says the build refuses both `date + n` and `end - start`: a date or date-time mixed with a number, or minus another, reads `null`, and `today()` / `now()` minus a date field reads `{}`. It prescribes `daysBetween(a, b)` and keeps the Want / Write table. The leave-days formula is written `daysBetween(start, end)` in the dashboards rule and `daysBetween(record.start_date, record.end_date)` in the eval prompt, and the formula eval says the build refuses `record.close_date - today()`. +- The formula gate change itself ships in `@objectstack/formula` (`formula-date-subtraction-refused`). `SKILL.md` and `rules/dashboards.md` stay under their token ceilings (no ceiling moves). No schema, key, command or behaviour changes. diff --git a/skills/objectstack-formula/SKILL.md b/skills/objectstack-formula/SKILL.md index 66e00eb73e7..0050e9403b4 100644 --- a/skills/objectstack-formula/SKILL.md +++ b/skills/objectstack-formula/SKILL.md @@ -148,9 +148,9 @@ tests: every entry resolves at runtime, and this table documents them all. | `date(s)` / `datetime(s)` | timestamp | Parse an ISO date / date-time string to a timestamp | | `isoDate(t)` / `isoDatetime(t)` | string | Timestamp to UTC ISO text, `2026-10-08` / `2026-10-08T17:55:06.123Z`. Text in? `isoDate(date(s))` | -> **No date arithmetic.** A date mixed with a number faults and the build -> rejects it; `end - start` does not fault — it yields a `duration` stored as -> `{}`. +> **No date arithmetic.** The build refuses `date + n` and `end - start`: a +> date or date-time mixed with a number, or minus another, reads `null`, and +> `today()` / `now()` minus a date field reads `{}`. Use `daysBetween(a, b)`. | Want | Write — never `end - start`, `date + n`, `today() + 30` | |:---|:---| diff --git a/skills/objectstack-formula/evals/cel-predicates-formulas.json b/skills/objectstack-formula/evals/cel-predicates-formulas.json index 0b13a4a7b78..23b83138836 100644 --- a/skills/objectstack-formula/evals/cel-predicates-formulas.json +++ b/skills/objectstack-formula/evals/cel-predicates-formulas.json @@ -34,7 +34,7 @@ { "id": 4, "prompt": "Demo opportunities should close 45 days after the package is installed, and a formula should show how many days remain until close.", - "expected_output": "Seed value as a CEL envelope, `close_date: cel`daysFromNow(45)``, never a JS `Date` / `Date.now()` (two `os build` runs must be byte-identical). Days remaining is `daysBetween(today(), record.close_date)` — never `record.close_date - today()` (a duration stored as `{}`) and never date arithmetic on `today()` (it faults).", + "expected_output": "Seed value as a CEL envelope, `close_date: cel`daysFromNow(45)``, never a JS `Date` / `Date.now()` (two `os build` runs must be byte-identical). Days remaining is `daysBetween(today(), record.close_date)` — never `record.close_date - today()` (the build refuses it) and never date arithmetic on `today()` (it faults).", "files": [], "assertions": { "must_contain": ["cel`", "daysFromNow(", "daysBetween("], diff --git a/skills/objectstack-ui/evals/analytics-inline-vs-dataset.json b/skills/objectstack-ui/evals/analytics-inline-vs-dataset.json index 38be7f9b438..42ad503f3ce 100644 --- a/skills/objectstack-ui/evals/analytics-inline-vs-dataset.json +++ b/skills/objectstack-ui/evals/analytics-inline-vs-dataset.json @@ -55,7 +55,7 @@ }, { "id": 6, - "prompt": "leave_request shows days_requested, a formula (end_date - start_date, returnType 'number'), on its form; keep it. Add a KPI widget totalling days requested this quarter. Emit the metadata.", + "prompt": "leave_request shows days_requested, a formula (daysBetween(record.start_date, record.end_date), returnType 'number'), on its form; keep it. Add a KPI widget totalling days requested this quarter. Emit the metadata.", "expected_output": "Never sums the formula (virtual, whatever its returnType): adds a stored number a beforeInsert/beforeUpdate hook keeps in step and sums that, or titles the widget as the count it is.", "files": [], "assertions": { diff --git a/skills/objectstack-ui/rules/dashboards.md b/skills/objectstack-ui/rules/dashboards.md index ffbb79f6e2f..f90b7f6fef5 100644 --- a/skills/objectstack-ui/rules/dashboards.md +++ b/skills/objectstack-ui/rules/dashboards.md @@ -97,7 +97,7 @@ Standardized answers to the recurring ambiguous cases: or boolean field (`percent` not under `sum`; `min`/`max` also dates); a `formula` has no column: refused whatever its `returnType` says. Store the number (user-entered, a `summary` rollup, or hook-kept) or title the widget the `count` - it is. Leave days: `sum` over the `formula` `days_requested` (`end - start`) is + it is. Leave days: `sum` over the `formula` `days_requested` (`daysBetween(start, end)`) is refused (`date` fields: no `duration`) ⇒ a stored `number` `days_count` a `beforeInsert`/`beforeUpdate` hook writes, then `{ name: 'days_sum', aggregate: 'sum', field: 'days_count' }`. - **Breakdown by a multi-value field.** `multiselect`/`checkboxes`/`tags` and