Repository navigation
fix(formula,lint): a date-arithmetic verdict in a flow condition says the run fails, not that the value reads null - #30240
Conversation
… the run fails, not that it reads null Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6Ca5BRUDW37pk8jE1gW
📓 Docs Drift Check8 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 15 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin b1b1ac05dcf96ca7463634ef25b6ba6c082ede59 && git checkout b1b1ac05dcf96ca7463634ef25b6ba6c082ede59
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5fa9375ec8279a7ee9e779049c0e93661c1786a3 3eea4f73219259ea84ba8d4f441c027455380992 && git checkout -B drift-repro 5fa9375ec8279a7ee9e779049c0e93661c1786a3 && git merge --no-ff 3eea4f73219259ea84ba8d4f441c027455380992
node scripts/docs-audit/affected-docs.mjs --json 5fa9375ec8279a7ee9e779049c0e93661c1786a3 |
|
domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF 席位意见:ACCEPT。四个码各配一种句子,三类流程 × 三个站点都钉住了,record 范围的对照也在,消融做了。 席位核对Head-sha:
|
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 41962230207 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 分类: 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Requested by Bill · project thread
domain:server#4 · os-bill · session_01Utp6Ca5BRUDW37pk8jE1gW
维护者速读
close_date + 1 > 3、end_date - start_date > duration("1h")),判词不再说「字段静默得到 null」,而是如实说明:条件出错时运行在此失败、流程要做的事没做。记录触发流程(及扫描本对象的定时相对触发)上说「运行失败」;其它流程(API、定时、动作或父流程启动)上说「只要这个名字在运行时拿到的是日期,运行就失败」。四个新拒绝码承载这两种句子。evaluateCondition在每种流程上遇错都抛异常、运行失败,旧句子却告诉作者流程会继续往下走。date-arithmetic-run-fails/-run-may-fail、date-text-arithmetic-run-fails/-run-may-fail,按旧码date-arithmetic/date-text-arithmetic识别流程站点的使用方需改认;公式字段、校验规则等 record 范围站点原码原句不动。回滚即 revert,无数据迁移。packages/formula/src/validate.ts(dateFlowArm/dateFlowFault两个小函数和date-arith分支)、packages/formula/src/expression-refusal.ts(四个码)、packages/lint/src/validate-expressions.test.ts(三种流程 × 三个站点的钉子)。pnpm --filter @objectstack/lint exec vitest run src/validate-expressions.test.ts -t 30016,预期 2 条通过:记录触发、定时、API 三个流程的起始条件、连线条件、decision 分支都报 error 且文案说运行失败、不说 null;record 范围校验规则对照仍说 null。Fixes #30016
Clause-②: yes (widening) — four codes join @objectstack/formula's ExpressionRefusalCode (date-arithmetic-run-fails, date-arithmetic-run-may-fail, date-text-arithmetic-run-fails, date-text-arithmetic-run-may-fail) and a flow condition's date verdicts move to them from date-arithmetic / date-text-arithmetic (a consumer keyed to those codes on a flow site re-keys); grading unchanged (error at every site); record-scoped sites keep their code and sentence.
Metadata-protocol: none
What changes
Measured on
mainatdfafae81(after PR #30015, merged 2026-11-04T08:28Z). Thedate-arithbranch oftypeSoundnessIssueand thefaultarm ofdateTextIssueserved one sentence, "this faults at runtime, so the field silently evaluates to null", at every scope. A'flattened'site is a flow condition (lint's flow walk, the engine's registration advisory pass, MCPflow_condition), anddag-traversal.tsevaluateConditionthrows on a fault on every flow (ADR-0032 §1c): the run fails at the condition, nothing reads null.faultOutcome: 'run-fails'(record-triggered start / edge /decision; a time-relative sweep of the typed object),bareNamesUnprovenunsetdate-arithmetic/date-text-arithmetic, error, "silently evaluates to null"date-arithmetic-run-fails/date-text-arithmetic-run-fails, error: "this faults at runtime, and a flow condition that cannot be evaluated fails the run: the flow stops at this condition and the work the flow exists to do is not done (it does not read null)."bareNamesUnproven)date-arithmetic-run-may-fail/date-text-arithmetic-run-may-fail, error: "this faults whereverclose_dateholds the date (that text) when the condition runs, and a flow condition that cannot be evaluated fails the run: …"record-scoped site (formula field, validation rule, display predicate)date-arithmetic/date-text-arithmeticWhy two flow sentences, not one (measured). On a flow that is not record-triggered the bare name holds what the entrance hands the run (
engine.tscelScopedocblock: the inbound hook's request body fortrigger-api, an action's loaded orid-only record, a parent's record, or nothing). Through the built@objectstack/formulaon this head:celEngine.evaluate({ dialect: 'cel', source: 'close_date + 1 > 3' }, { extra: { close_date: 5 } })answers{ ok: true, value: true }; with'2026-01-01'it faults (cel-jsno such overloadfor a dyn string+ int). So "this faults at runtime" is not proven there, and a code names one message template (expression-refusal.ts, the contract), hence four codes, mirroringtype-mismatch-run-fails/type-mismatch-run-may-fail. The run-fails sentence carries no "the write that fired it still lands" clause:run-failsalso covers a time-relative sweep, which no write fires.Routing. No new mechanism:
dateFlowArm(scope, faultOutcome, bareNamesUnproven)reads the hint lint already passes (faultOutcomeOf,bareNamesUnprovenfrom #30025). The date verdicts read bare names only, sobareNamesUnprovendowngrades them to-run-may-fail; a bare name a flow variable holds (flowVariables) is still excluded from typing, as before. Grading stayserroreverywhere.@objectstack/lintchanges only the fault-outcome ledger's docblock.Tests
packages/formula/src/expression-refusal-codes.test.ts: aPINSentry for each new code (exact params and message); the flattened pins that used to sit underdate-arithmetic/date-text-arithmeticmove to-run-may-fail(their schema declares no outcome); the record-scoped pins are untouched.SWEEP_SCHEMASgains the flattened deal schema withfaultOutcome: 'run-fails'andSWEEP_SOURCESa flattened date subtraction, so the sweep reaches all four codes and holds their params to their messages.packages/formula/src/validate.test.ts: flattenedstart_date + 1 > 3underrun-fails→-run-fails; with no outcome and withbareNamesUnproven→-run-may-fail; neither says "evaluates to null";flowVariables: ['start_date']→ no verdict; control: a record-scopedfail-closedrule keepsdate-arithmeticand its null sentence.packages/formula/src/validate-date-text-arithmetic.test.ts: the flattened case now asserts-run-may-fail, plus-run-failsunder the outcome and-run-may-failunderbareNamesUnproven.packages/lint/src/validate-expressions.test.ts: three flows (record-triggered,schedule,type: 'api'), each with a start condition, an edge condition (date arithmetic) and adecisionbranch (date subtraction): every verdicterror, the record-triggered one says "this faults at runtime, and a flow condition …", the other two "this faults whereverclose_dateholds …", none "evaluates to null". Control: a record-scoped validation rule's date arithmetic keeps the null sentence.Suites (head f4501b7, under os-verify-lock, shared box)
Ablation (scripts/ablation-replace.mjs; restored: blob == HEAD, git diff HEAD empty)
Acceptance notes
fail-closedvalidation rule still says "silently evaluates to null". There the fault refuses the write (rule-validator.tscheckPredicate, the ledger'sfail-closedrow), so the sentence is not true either. The card keeps that sentence and the seat ruled it out of scope; reported as a finding, not fixed here.type-mismatch-run-failson a time-relative sweep says "the write that fired it still lands". Since PR fix(lint)!: a flow condition's text-vs-number comparison gets a true verdict on every flow, record.x read and decision branch #30015 therun-failsoutcome also covers a time-relative sweep of the typed object, which no write fires. Pre-existing, outside this card's date scope; the date sentence here avoids the clause. Reported as a finding.-run-may-failsentence states the condition under which the run fails, which is true at that grade.content/docs/data-modeling/formulas.mdxis outside the claimed file surface; its two date rows make no flow-specific null claim, so it is left as is.Verification
All at head
3eea4f73(git rev-parse --short HEAD), the commit this PR opens on (origin/mainmerged once).node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 58 derived families (re-derived after the merge oforigin/main, the same 58), each run on this head after a fullturbo run build(--filter='!@objectstack/docs', VERDICT command-exit 0), all exit 0.--ran: "58 derived famil(ies) accounted for — 58 run, 0 NOT-MEASURED". Among themcheck-adr-0087-registration --base origin/main(exit 0: no semantic entry is required for this move, so none is added),check-changeset-no-major,check-empty-changeset,check:docs-transcript-drift,check:doc-authoring,check:test-source-alias,check:cross-package-test-inputs,check:dts-closure,check:dual-build-cjs-loads,check:lean-entry-closure.pnpm lint(eslint . --no-inline-config): exit 0.!andClause-②has no(narrowing)arm.🤖 Generated with Claude Code
https://claude.ai/code/session_01Utp6Ca5BRUDW37pk8jE1gW