Repository navigation
Commit 0e4747a
fix(formula): an advisory validation rule's date verdict says the rule is skipped and the write proceeds (#30585)
<!-- claude-projects-attribution: {"github_login":"os-bill"} -->
_Requested by **Bill** · [project
thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wg6xDm8LQaqEyuhcYBN6LvNk)_
domain:server#5 · os-bill · session_01Utp6CYVV6fcAQrNTDQRvPb
## 维护者速读
- **改了什么**:advisory 校验规则(`severity: 'warning'` /
`'info'`)里对日期做加减时,报错不再说「字段静默得到 null」,改说「规则被跳过、写入照常进行、只留一行日志」,配新码
`date-arithmetic-fail-open` /
`date-text-arithmetic-fail-open`,文案与同位置的类型不匹配判词一字不差。
- **为什么改**:advisory 规则出错时运行时跳过该规则并放行写入,那里没有字段,也没有东西读到 null,旧句说的是不会发生的事。
- **风险与回滚**:分级不变(仍是 error),发布门不收也不放,只换文案和码。按旧码 `date-arithmetic` /
`date-text-arithmetic` 识别 advisory 规则的使用方需改认新码。公式字段原码原句不动。回滚即
revert,无数据迁移。
- **先看**:`packages/formula/src/validate.ts` 的 `dateArm` 与
`dateArmFault`(`fail-open`
分支),`packages/formula/src/expression-refusal.ts` 的两个新码。
- **两分钟验证**:`pnpm --filter @objectstack/lint exec vitest run
src/validate-expressions.test.ts -t 30494`,预期 1 条通过:warning / info
规则的日期判词说规则被跳过、写入照常,不含 null;公式字段对照仍说 null。
- **需要你做的**:无,席位落地。
Fixes #30494
Clause-②: yes (widening) — two codes join `@objectstack/formula`'s
exported `ExpressionRefusalCode` / `EXPRESSION_REFUSAL_CODES`:
`date-arithmetic-fail-open` and `date-text-arithmetic-fail-open`, each
with the params of the code it splits from. An advisory rule's date
verdicts move to them from `date-arithmetic` / `date-text-arithmetic` (a
consumer keyed to the old codes on an advisory rule re-keys). The grade
is unchanged; what is refused, warned or passed is unchanged.
Metadata-protocol: none — what an author can write is unchanged, and `os
validate` reports no expression refusal code: its findings are
`@objectstack/lint`'s `ExprIssue` (`where`, `message`, `source`,
`severity`, `path`), re-measured at
`packages/lint/src/validate-expressions.ts:157`, which carries no
`code`. Only the message text changes there.
Skills-docs: none — `git grep -c "silently evaluates to null" -- skills`
answers no hit (exit 1); control: `git grep -c "evaluates to" -- skills`
hits `skills/objectstack-automation/SKILL.md`,
`skills/objectstack-data/SKILL.md` and
`skills/objectstack-data/rules/validation.md`, none of them about a date
verdict on an advisory rule.
## What changes
Measured on `main` with PR #30492 merged (`67d5d5375`), and again after
`origin/main` was merged at `a4c3e738`.
| site | before | now |
|:--|:--|:--|
| advisory (`warning` / `info`) validation rule (`faultOutcome:
'fail-open'`): date arithmetic | `date-arithmetic` /
`date-text-arithmetic`, error, "this faults at runtime, so the field
silently evaluates to null." | `date-arithmetic-fail-open` /
`date-text-arithmetic-fail-open`, error: "this faults at runtime, and
here the rule is advisory, so a rule that cannot be evaluated is
skipped, not refused: the write proceeds, the rule is never applied (its
advisory never fires), and the only trace is a log line saying it could
not be evaluated." |
| formula field, display predicate (no outcome) | `date-arithmetic` /
`date-text-arithmetic` | unchanged, code and sentence |
| fail-closed site, flow condition | the `-fail-closed` / `-run-fails` /
`-run-may-fail` codes | unchanged |
- **One outcome, one sentence.** `dateArmFault` closes the `fail-open`
arm on `TYPE_MISMATCH_FAULT_ARMS['fail-open'].clause` itself, not a
copy, so the date verdict and the type-mismatch verdict on the same rule
cannot drift apart.
- **One code per template**, as #30250 did for fail-closed:
`expression-refusal.ts` is the contract that a code names one message
template.
- **No new signal.** `@objectstack/lint`'s `faultOutcomeOf` ledger
already answers `fail-open` for a `warning` / `info` rule
(`validate-expressions.ts:1358`), and MCP `validate_expression` already
passes it for `site: 'validation'` with `severity: 'warning' | 'info'`
(`mcp-http-tools.ts:666`). Neither package changes.
## Tests
- `packages/formula/src/validate.test.ts`:
- new: on a `fail-open` `record` site, `record.start_date + 1 > 3` and
`record.end_date - record.start_date > duration("24h")` get the
`-fail-open` codes and the fail-open sentence, and not "evaluates to
null". Control: the same two predicates with no outcome keep
`date-arithmetic` / `date-text-arithmetic` and the null sentence;
- changed: the #30250 test's third control asserted that an advisory
rule keeps `date-arithmetic` and the null sentence, which is this card's
defect. That row is dropped; its formula-field controls stay.
- `packages/formula/src/expression-refusal-codes.test.ts`: a `PINS`
entry for each new code (exact params and message), and `SWEEP_SCHEMAS`
gains the `fail-open` date schema, so the sweep reaches both codes and
holds their params to their messages.
- `packages/lint/src/validate-expressions.test.ts`, new: a `warning`
rule and an `info` rule with date arithmetic say the rule is skipped and
the write proceeds (not null, not refused). Control: a formula field's
`record.close_date + 1` keeps the null sentence.
- `packages/mcp/src/mcp-validate-expression.test.ts`: unchanged,
verbatim. Measured once with a probe file that was deleted afterwards
(not committed): `validate_expression` at `site: 'validation'` answers
`date-arithmetic-fail-open` / `date-text-arithmetic-fail-open` for
`severity: 'warning'` and `'info'`, `-fail-closed` for `'error'`, and
`date-arithmetic` at `site: 'formula'`; each message equals
`validateStackExpressions`' on the same rule.
<details><summary>Suites and red-first</summary>
```
red first (pins only, on PR #30492's head, commit 89eebee):
formula × on an advisory rule says the rule is skipped … (#30494) · × has a pin for every code Tests 2 failed | 181 passed (183)
lint × says an advisory validation rule's date arithmetic skips the rule … (#30494) Tests 1 failed
merged head dfc91e9 (origin/main a4c3e73 merged once), under os-verify-lock, shared box:
formula test Test Files 64 passed (64) Tests 1652 passed (1652)
lint test Test Files 195 passed (195) Tests 8093 passed (8093)
mcp test Test Files 56 passed (56) Tests 678 passed (678)
typecheck (formula, lint, mcp): exit 0
objectql src/publish-type-mismatch-fail-closed.test.ts Tests 12 passed (12)
```
</details>
<details><summary>Ablation (scripts/ablation-replace.mjs; restored: blob
== HEAD, git diff HEAD empty, dist marker absent after rebuild, tree
clean)</summary>
```
A1 formula: the fail-open date arm off
anchor faultOutcome === 'fail-closed' || faultOutcome === 'fail-open' ? faultOutcome : null
replace faultOutcome === 'fail-closed' || String(faultOutcome) === 'ablated-30494-a1' ? faultOutcome : null
anchor x1 -> x0, replacement x0 -> x1, blob 8931039 -> 19c5c422d2f2
rebuilt (tsup JS emitted; the DTS step exited 1 on the mutated return type), ablation-dist-preflight:
marker present in dist/index.js and dist/index.mjs
formula × on an advisory rule … (#30494) · × date-arithmetic-fail-open · × date-text-arithmetic-fail-open · × the SWEEP
Tests 4 failed | 181 passed (185)
lint × says an advisory validation rule's date arithmetic skips the rule … (#30494)
Tests 1 failed | 473 passed (474)
restore: blob 8931039 == HEAD, git diff HEAD empty; rebuild exit 0; preflight --absent: marker absent from all 6 built files, tree clean
```
</details>
## Acceptance notes
- **`packages/lint/src/validate-expressions.ts:1338-1341` doc comment**
names the date verdicts' `-run-fails`, `-run-may-fail` and
`-fail-closed` arms and not the new `-fail-open` one. It says nothing
false (it never says an advisory rule keeps the null sentence), and
`packages/lint/src/**` source is outside this card's file surface, so it
is left as is.
- **The #30250 changeset**
(`.changeset/30250-fail-closed-date-and-sweep-run-fails-sentences.md`)
says an advisory rule keeps `date-arithmetic` / `date-text-arithmetic`.
Both changesets ship in the same release; this PR's changeset says it
replaces that note, and the other file is not edited here.
- **`content/docs/data-modeling/formulas.mdx` date-arithmetic row** says
"this always faulted to `null`". That describes a formula's runtime, and
this PR changes no runtime, so it is left as is (as #30250 left it).
- **Merge.** `origin/main` was merged once at `a4c3e738`. The branch had
carried PR #30492's head as a merge until it landed as a squash, so git
reported conflicts in the five files both sides touch. They were
resolved by applying this branch's own diff (`ebda9aca..34b93ec`) to
`origin/main`'s copies: it applied cleanly, and the changed lines of the
resolved delta are identical to that diff's.
## Verification
All at head `dfc91e9d` (`git rev-parse --short HEAD`), the commit this
PR opens on (`origin/main` merged once, at `a4c3e738`).
- Full build after the merge: `pnpm install --frozen-lockfile && turbo
run build --filter='!@objectstack/docs' --concurrency=2`, 119/119 tasks,
`VERDICT command-exit 0`.
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`: change set of 6 paths vs merge base
`a4c3e738b`, 58 derived families, each run on this head, all **exit 0**
(each exit code recorded before any pipe).
`check-plugin-teardown-shape.mjs --self-test` ran after fetching its
pinned fixture commit `621a4876` into the shallow clone ("48 cases
pass"). `--ran`: "58 derived famil(ies) accounted for — 58 run, 0
NOT-MEASURED". Among them `check-adr-0087-registration --base
origin/main`, `check-changeset-no-major`, `check-empty-changeset`,
`check:doc-authoring`, `check:docs-transcript-drift`,
`check:test-source-alias`, `check:cross-package-test-inputs`,
`check:dts-closure`, `check:dual-build-cjs-loads`,
`check:lean-entry-closure`, `check:nul-bytes`,
`check:closing-keyword-parity`.
- `pnpm lint` (`eslint . --no-inline-config`): exit 0.
- Full dogfood: not run. The title has no `!` and `Clause-②` has no
`(narrowing)` arm.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01Utp6CbJFWadws6f9r4B8Ke
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 6b84fef commit 0e4747a
6 files changed
Lines changed: 165 additions & 19 deletions
File tree
- .changeset
- packages
- formula/src
- lint/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
313 | 313 | | |
314 | 314 | | |
315 | 315 | | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
316 | 336 | | |
317 | 337 | | |
318 | 338 | | |
| |||
377 | 397 | | |
378 | 398 | | |
379 | 399 | | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
380 | 418 | | |
381 | 419 | | |
382 | 420 | | |
| |||
752 | 790 | | |
753 | 791 | | |
754 | 792 | | |
| 793 | + | |
| 794 | + | |
| 795 | + | |
| 796 | + | |
| 797 | + | |
| 798 | + | |
| 799 | + | |
| 800 | + | |
| 801 | + | |
755 | 802 | | |
756 | 803 | | |
757 | 804 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
167 | 167 | | |
168 | 168 | | |
169 | 169 | | |
170 | | - | |
| 170 | + | |
| 171 | + | |
171 | 172 | | |
172 | 173 | | |
173 | 174 | | |
| |||
176 | 177 | | |
177 | 178 | | |
178 | 179 | | |
179 | | - | |
| 180 | + | |
| 181 | + | |
180 | 182 | | |
181 | 183 | | |
182 | 184 | | |
| |||
204 | 206 | | |
205 | 207 | | |
206 | 208 | | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
207 | 217 | | |
208 | 218 | | |
209 | 219 | | |
| |||
317 | 327 | | |
318 | 328 | | |
319 | 329 | | |
| 330 | + | |
| 331 | + | |
320 | 332 | | |
321 | 333 | | |
322 | 334 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1139 | 1139 | | |
1140 | 1140 | | |
1141 | 1141 | | |
1142 | | - | |
1143 | | - | |
1144 | | - | |
1145 | | - | |
1146 | | - | |
1147 | | - | |
| 1142 | + | |
| 1143 | + | |
| 1144 | + | |
| 1145 | + | |
| 1146 | + | |
1148 | 1147 | | |
1149 | | - | |
| 1148 | + | |
| 1149 | + | |
| 1150 | + | |
| 1151 | + | |
| 1152 | + | |
| 1153 | + | |
| 1154 | + | |
| 1155 | + | |
| 1156 | + | |
| 1157 | + | |
| 1158 | + | |
| 1159 | + | |
| 1160 | + | |
| 1161 | + | |
| 1162 | + | |
| 1163 | + | |
| 1164 | + | |
| 1165 | + | |
| 1166 | + | |
| 1167 | + | |
| 1168 | + | |
| 1169 | + | |
| 1170 | + | |
| 1171 | + | |
| 1172 | + | |
| 1173 | + | |
| 1174 | + | |
| 1175 | + | |
| 1176 | + | |
| 1177 | + | |
| 1178 | + | |
| 1179 | + | |
| 1180 | + | |
1150 | 1181 | | |
1151 | 1182 | | |
1152 | 1183 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
440 | 440 | | |
441 | 441 | | |
442 | 442 | | |
443 | | - | |
| 443 | + | |
444 | 444 | | |
445 | | - | |
| 445 | + | |
| 446 | + | |
446 | 447 | | |
447 | 448 | | |
448 | 449 | | |
| |||
611 | 612 | | |
612 | 613 | | |
613 | 614 | | |
614 | | - | |
615 | | - | |
| 615 | + | |
| 616 | + | |
| 617 | + | |
| 618 | + | |
616 | 619 | | |
617 | | - | |
| 620 | + | |
618 | 621 | | |
619 | 622 | | |
620 | 623 | | |
621 | 624 | | |
622 | 625 | | |
623 | | - | |
| 626 | + | |
| 627 | + | |
| 628 | + | |
624 | 629 | | |
625 | 630 | | |
626 | 631 | | |
627 | 632 | | |
628 | | - | |
629 | | - | |
| 633 | + | |
| 634 | + | |
| 635 | + | |
| 636 | + | |
630 | 637 | | |
631 | 638 | | |
632 | 639 | | |
| 640 | + | |
633 | 641 | | |
634 | 642 | | |
635 | 643 | | |
| |||
645 | 653 | | |
646 | 654 | | |
647 | 655 | | |
648 | | - | |
649 | | - | |
| 656 | + | |
| 657 | + | |
| 658 | + | |
650 | 659 | | |
651 | 660 | | |
652 | 661 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1382 | 1382 | | |
1383 | 1383 | | |
1384 | 1384 | | |
| 1385 | + | |
| 1386 | + | |
| 1387 | + | |
| 1388 | + | |
| 1389 | + | |
| 1390 | + | |
| 1391 | + | |
| 1392 | + | |
| 1393 | + | |
| 1394 | + | |
| 1395 | + | |
| 1396 | + | |
| 1397 | + | |
| 1398 | + | |
| 1399 | + | |
| 1400 | + | |
| 1401 | + | |
| 1402 | + | |
| 1403 | + | |
| 1404 | + | |
| 1405 | + | |
| 1406 | + | |
| 1407 | + | |
| 1408 | + | |
| 1409 | + | |
| 1410 | + | |
| 1411 | + | |
| 1412 | + | |
| 1413 | + | |
| 1414 | + | |
| 1415 | + | |
| 1416 | + | |
| 1417 | + | |
1385 | 1418 | | |
1386 | 1419 | | |
1387 | 1420 | | |
| |||
0 commit comments