Skip to content

fix(formula,lint): a date-arithmetic verdict in a flow condition says the run fails, not that the value reads null - #30240

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-2w74w7
Nov 5, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-2w74w7

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Bill · project thread
domain:server#4 · os-bill · session_01Utp6Ca5BRUDW37pk8jE1gW

维护者速读

  • 改了什么:流程条件(起始条件、连线条件、decision 分支)里对日期字段做加减(如 close_date + 1 > 3、end_date - start_date > duration("1h")),判词不再说「字段静默得到 null」,而是如实说明:条件出错时运行在此失败、流程要做的事没做。记录触发流程(及扫描本对象的定时相对触发)上说「运行失败」;其它流程(API、定时、动作或父流程启动)上说「只要这个名字在运行时拿到的是日期,运行就失败」。四个新拒绝码承载这两种句子。
  • 为什么改:fix(lint)!: a type mismatch in a record-triggered flow condition or an advisory validation rule is served a verdict that says what really happens #29953 / fix(lint)!: a flow condition's text-vs-number comparison gets a true verdict on every flow, record.x read and decision branch #30015 已让文本、布尔比较的判词说真话,日期判词被落下。引擎的 evaluateCondition 在每种流程上遇错都抛异常、运行失败,旧句子却告诉作者流程会继续往下走。
  • 风险与回滚:分级不变(日期判词在每个位置本来就是 error),发布门不收也不放。流程条件上的日期判词改用新码 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 main at dfafae81 (after PR #30015, merged 2026-11-04T08:28Z). The date-arith branch of typeSoundnessIssue and the fault arm of dateTextIssue served 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, MCP flow_condition), and dag-traversal.ts evaluateCondition throws on a fault on every flow (ADR-0032 §1c): the run fails at the condition, nothing reads null.

site before now
flow condition, faultOutcome: 'run-fails' (record-triggered start / edge / decision; a time-relative sweep of the typed object), bareNamesUnproven unset date-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)."
any other flow condition (no outcome declared, or bareNamesUnproven) the same date-arithmetic-run-may-fail / date-text-arithmetic-run-may-fail, error: "this faults wherever close_date holds 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-arithmetic unchanged, code and sentence

Why 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.ts celScope docblock: the inbound hook's request body for trigger-api, an action's loaded or id-only record, a parent's record, or nothing). Through the built @objectstack/formula on 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-js no such overload for 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, mirroring type-mismatch-run-fails / type-mismatch-run-may-fail. The run-fails sentence carries no "the write that fired it still lands" clause: run-fails also covers a time-relative sweep, which no write fires.

Routing. No new mechanism: dateFlowArm(scope, faultOutcome, bareNamesUnproven) reads the hint lint already passes (faultOutcomeOf, bareNamesUnproven from #30025). The date verdicts read bare names only, so bareNamesUnproven downgrades them to -run-may-fail; a bare name a flow variable holds (flowVariables) is still excluded from typing, as before. Grading stays error everywhere. @objectstack/lint changes only the fault-outcome ledger's docblock.

Tests

  • packages/formula/src/expression-refusal-codes.test.ts: a PINS entry for each new code (exact params and message); the flattened pins that used to sit under date-arithmetic / date-text-arithmetic move to -run-may-fail (their schema declares no outcome); the record-scoped pins are untouched. SWEEP_SCHEMAS gains the flattened deal schema with faultOutcome: 'run-fails' and SWEEP_SOURCES a flattened date subtraction, so the sweep reaches all four codes and holds their params to their messages.
  • packages/formula/src/validate.test.ts: flattened start_date + 1 > 3 under run-fails → -run-fails; with no outcome and with bareNamesUnproven → -run-may-fail; neither says "evaluates to null"; flowVariables: ['start_date'] → no verdict; control: a record-scoped fail-closed rule keeps date-arithmetic and its null sentence.
  • packages/formula/src/validate-date-text-arithmetic.test.ts: the flattened case now asserts -run-may-fail, plus -run-fails under the outcome and -run-may-fail under bareNamesUnproven.
  • 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 a decision branch (date subtraction): every verdict error, the record-triggered one says "this faults at runtime, and a flow condition …", the other two "this faults wherever close_date holds …", 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)
formula test   Test Files 64 passed (64)    Tests 1644 passed (1644)
lint test      Test Files 193 passed (193)  Tests 7996 passed (7996)
mcp test       Test Files 56 passed (56)    Tests 675 passed (675)
typecheck (tsc --noEmit + check:test-typecheck): formula 0, lint 0
Ablation (scripts/ablation-replace.mjs; restored: blob == HEAD, git diff HEAD empty)
A1  formula: dateFlowArm always answers null (`if (scope !== 'flattened' || true /* ablated-30016-a1 */) return null;`)
    — the old "silently evaluates to null" sentence and code come back on every flow site
    × in a flow condition says the run fails, and keeps the null sentence on a record-scoped rule (#30016)
    × judges a flattened (flow) condition the same way, under the flow codes (#30016)
    × date-arithmetic-run-fails · × date-arithmetic-run-may-fail · × date-text-arithmetic-run-fails · × date-text-arithmetic-run-may-fail (PINS)
    × the SWEEP (code set unreached)
    Tests 7 failed | 197 passed (204)

Acceptance notes

  • A record-scoped fail-closed validation rule still says "silently evaluates to null". There the fault refuses the write (rule-validator.ts checkPredicate, the ledger's fail-closed row), 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-fails on 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 the run-fails outcome 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.
  • The date verdicts stay errors on flows that are not record-triggered, where the type-mismatch verdict is a warning because what the run binds is not proven. Grading was ruled unchanged; the -run-may-fail sentence states the condition under which the run fails, which is true at that grade.
  • content/docs/data-modeling/formulas.mdx is 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/main merged once).

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 58 derived families (re-derived after the merge of origin/main, the same 58), each run on this head after a full turbo 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 them check-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.
  • Full dogfood: not run. The title has no ! and Clause-② has no (narrowing) arm.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Utp6Ca5BRUDW37pk8jE1gW

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Nov 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

8 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. ⚠️ 1 changed file(s) yielded no anchor (packages/lint/src/validate-expressions.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/lint/src/validate-expressions.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 103 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 15 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 5fa9375ec8279a7ee9e779049c0e93661c1786a3 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from b1b1ac05dcf96ca7463634ef25b6ba6c082ede59 — the merge of head 3eea4f73219259ea84ba8d4f441c027455380992 into base 5fa9375ec8279a7ee9e779049c0e93661c1786a3, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

objectstack-fleet Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF

席位意见:ACCEPT。四个码各配一种句子,三类流程 × 三个站点都钉住了,record 范围的对照也在,消融做了。

席位核对

Head-sha: 3eea4f73219259ea84ba8d4f441c027455380992 · read 2026-11-05T03:30Z

Item Reading
Shape Draft → main, author objectstack-fleet[bot] (relay pr_create); line 1 attribution, Fixes #30016, Clause-② / Metadata-protocol at line start. 8 files, +265/−32.
Premise measured The engine's evaluateCondition throws on a faulting condition on every flow kind, so the old "silently null" sentence on a flow site was false. The dev's report 6648499418 and the diff agree.
Gates (check-runs on this head) 54 success, 9 skipped, 0 failure, 0 pending. Dev's local --ran: 58/58 families run, 0 NOT MEASURED, after a full build; pnpm lint exit 0. Ablation A1 (dateFlowArm → null): 7 tests fail, restored blob equals HEAD.
Clause-② yes (widening): four codes join the exported ExpressionRefusalCode, and a flow site's date verdicts move to them; the grading (error at every site) is unchanged, so no accept set changes. Precedent PR #30015 (one code per message template). Declared in claim 6647401295, the changeset and the PR body. Seat's own review of record at the default tier: PASS.
Out of scope Two sentences outside this card's date verdicts are not true at their site. They are filed as one folded finding card, #30250; not fixed here.
Landing Not governed (check-governed-merges --pr 30240: 0 of 8 paths), 297 lines ≤ 3000. Seat lands: pr_ready + automerge_enable.

@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 41962230207 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

分类:failure —— 按下面的日志分诊。

失败的 job(日志抽取,best effort):

  • Test UI (16/16) — 失败步骤: Run the ui tests (shard 16/16)

    × a read-only package: overlay types can be customized, never 可写; the create-only and closed types are read-only 128ms
    × a read-only package: the view list header reads 可定制, the page list header 只读 18ms
    × typeStateWord: the mapping in every context 5ms
    FAIL   dom  packages/app-shell/src/views/metadata-admin/DirectoryPage.stateVocabulary-29705.test.tsx > All metadata tiles: one state word, in the package context (objectstack#29705) > a read-only pack
      ↳ 失败原因: AssertionError: expected '可定制' to be '只读' // Object.is equality
    FAIL   dom  packages/app-shell/src/views/metadata-admin/DirectoryPage.stateVocabulary-29705.test.tsx > the list header draws the tile word (objectstack#29705) > a read-only package: the view list head
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    FAIL   dom  packages/app-shell/src/views/metadata-admin/DirectoryPage.stateVocabulary-29705.test.tsx > the state vocabulary census (objectstack#29705) > typeStateWord: the mapping in every context
      ↳ 失败原因: AssertionError: expected [ Array(4) ] to deeply equal [ 'customizable', 'readOnly', …(2) ]
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 6 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

expression gate: a date-arithmetic verdict in a flow condition still says the value "silently evaluates to null", though the run fails

1 participant