Skip to content

fix(lint): report a config.timeRelative that is not a descriptor object (#5647) - #5758

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-5647-scalar-timerelative-lint
Aug 6, 2026
Merged

fix(lint): report a config.timeRelative that is not a descriptor object (#5647)#5758
os-zhuang merged 1 commit into
mainfrom
claude/issue-5647-scalar-timerelative-lint

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #5647

问题

flow start 节点的 config.timeRelative 写成标量(timeRelative: 'daily' —— 把 sweep 的节奏描述符两个概念揉在一起时最自然的错写)时,每一层都完全沉默

前提验证(先在 origin/main 上实测,再写规则)

立单方给的是探针级证据,本 PR 在 API 面和 CLI 面各复证一遍:

现状(main)
FlowSchema.safeParse / defineFlow 接受标量。节点 config 是开放槽(ADR-0018),外层没有任何 gate 看进去
parsed tier timeRelative 仍是 string —— schema 既没拒也没实体化掉,规则在两个 tier 都看得见
validateFlowTriggerReadiness []
lintFlowPatterns []
os validate(真实 example app) 注入标量前后输出逐字节相同,exit 0,"Validation passed"

所以前提成立,而且比 #5496 修掉的缺口还差一档。因果链:引擎的路由判据是 config.timeRelative != null && typeof … === 'object',标量从 time-relative 分支穿过;该 start 节点再无其他 trigger 键时,resolveTriggerBinding 返回 undefined、activateFlowTrigger 静默 return。连「描述符是对象但形状错」那一行 bind 期 warn 都没有 —— 因为 trigger 从来没被交到这个 flow。这正是 #3481triggerType 上发现的同一个形状(「折叠成无 trigger,到处都看不见」),换了一个键。

改动

packages/lint/src/validate-flow-trigger-readiness.ts 新增判据 1e + 规则 id flow-time-relative-descriptor-unroutable(warning),在 authoring 期报出值、类型和后果,hint 把揉在一起的两个概念拆开:描述符说扫哪些记录({ object, dateField, 以及 withinDays / offsetDays 二选一 }),多久扫一次是同级键 config.schedule

真实 os validate 输出:

⚠ flow "task_reminder" › start node: has config.timeRelative = "daily" (a string),
which is not the descriptor OBJECT this slot takes — the engine routes a flow to the
time-relative sweep only when config.timeRelative is an object, so this one is never
routed there and the sweep is never installed. Nothing else on this start node declares
a trigger either, so the flow binds to NOTHING and never fires — with zero diagnostics
at any layer, not even the one bind-time warn a descriptor that IS an object gets when
the trigger refuses it.

独立判据,不是放宽 #5496

⛔ 没有动 isTimeRelative。它同时喂 §1b 的 unknown-object 与 isAutoTriggered,放宽它会把另外两条已发布规则的覆盖面当作副作用改掉。两条 id 改为沿引擎自己的路由判据划分 config.timeRelative 的非空取值:

这一点修正了派发单里「非普通对象(标量/数组等)」的措辞:实测数组与 Datetypeof 都是 'object',#5651 落地的规则已经报它们(用 schema 自己的话)。若按「非普通对象」实现,数组会被两条规则同时报。测试里有双向的互不越界断言。

后果分句按 flow 计算,不统一断言

标量单独出现 = 绑不上任何东西、永不触发(issue 的情形,也是更坏的那个)。但同一个 start 节点若还声明了引擎认识的 trigger,flow 确实会绑上、会触发 —— 按那个 trigger 的条款,描述符被静默丢弃(每次触发一次、context 上没有记录,而不是每条匹配记录一次)。对这种 flow 说「永不触发」是假诊断,而假诊断比没有诊断更没价值。所以后果分句分两种,两种都有测试钉住。

验证

  • 新用例先红后绿:反向验证(把 1e 判据关掉)→ 预判方向为直红,实测 10 条失败,含改写后的边界用例。其中「key 缺失/为 null 时沉默」原本是纯负向断言(判据删掉也会绿 —— fix(lint): 收敛 validate-expressions / validate-security-posture 的 spec 不声明键 ?? 别名读法 (#5017) #5046 那种「因为什么都没产出而绿」),已在同一用例内加了对照,现在也会红。唯一保持绿的是常量身份断言(…_UNROUTABLE !== …_INVALID),它本来就没有行为可失去。
  • pnpm --filter @objectstack/lint test:60 files / 1397 tests 全绿;typecheck、build 绿;eslint 绿。
  • fix(lint): every exported rule id constant is reachable from a published barrel (#5648) #5735 的 barrel 门禁首次真实命中(下方单列)。
  • 消费半径:pnpm --filter @objectstack/cli test83 files / 825 tests 全绿。全仓扫描确认 cli / examples / triggers 里没有任何 fixture 用标量 timeRelative,故无 fixture 需要三分诊处置。
  • examples 三 app A/B:app-todo 逐条 finding 与基线相同(只差耗时);crm、showcase 新规则零命中 —— 包括 showcase 真实的 timeRelative sweep flow([P2] Provide a declarative time-relative trigger (avoid fragile date-equality on record-change) #1874)保持沉默,这是最有力的不误报证据。
  • 正向对照:临时注入标量 → os validate 两种后果分句都正确渲染(其中一次还暴露了首字母小写的接缝,已修)→ 还原,工作树干净。
  • node scripts/check-nul-bytes.mjs 绿;另按控制字符纪律对改动文件做了越过门禁的自查(grep -naP),干净。

#5735 barrel 门禁的首次真实命中

派发单要求顺手验证这条刚落地的门禁。它按设计红了 —— 我加常量、加 barrel 导出,rule-id-barrel-exports.test.ts 立刻点名了常量名、它的值和 file:line:

FAIL  every declared rule id constant is exported from a barrel, with its own value
  "unreachable": [
    "FLOW_TIME_RELATIVE_DESCRIPTOR_UNROUTABLE ('flow-time-relative-descriptor-unroutable')
     — validate-flow-trigger-readiness.ts:95",
  ],

src/index.ts 补上那一行后转绿。评价:门禁按承诺工作,而且报错信息可直接执行(它连该改哪个文件的哪个 export 块都写了)。#5648 的判断成立 —— 这一行确实是靠「记得住」防不住的,七次同类遗漏之后第八次也没记住。

severity 建议(不自行升级)

按派发单保持 warning,与该文件 never-fire 族一致。但值得 PM 记一笔:本条与族里其他成员有一点不同 —— flow-trigger-unknown-object 带「对象可能来自其他已安装包」的跨包免责,是真 advisory;而标量永远不是描述符,没有任何合法解读,os validate 面上它更接近 error。升级会改已发布 severity,应单独一个 PR,不搭本次便车。

顺带记录(未做,不在本单范围)

方向 2(让标量 timeRelative 也算 isAutoTriggered,从而被 flow-draft-status-ambiguous 覆盖)按 PM 裁定未做。CLI 正向对照顺手拍到了这个缺口的实物:把注入 flow 改成 type: 'autolaunched' 后,它同时失去了 draft-status 告警(因为 isAutoTriggered 为 false)。测试里有一条用例把这个「刻意未改」的行为钉住,并配了对象形对照,免得日后有人误以为 draft 规则坏了。是否要单开,交 PM 定。


Generated by Claude Code

…ct (#5647)

A flow start node whose `config.timeRelative` held a scalar — `timeRelative:
'daily'`, from fusing the sweep's cadence with its descriptor — was accepted in
complete silence at every layer.

Verified against origin/main before writing the rule: `FlowSchema.safeParse` and
`defineFlow` ACCEPT the scalar (a node `config` is an open slot, ADR-0018, so no
outer gate looks inside), `validateFlowTriggerReadiness` and `lintFlowPatterns`
both returned `[]`, and `os validate` on a real example app printed
byte-identical output with and without it. The engine routes a flow to the
time-relative sweep only when `typeof config.timeRelative === 'object'`, so the
scalar falls through that branch; with no other trigger key on the node
`resolveTriggerBinding` returns undefined and `activateFlowTrigger` returns
silently. Not one diagnostic anywhere — not even the single bind-time warn an
object-but-unparseable descriptor gets, because the trigger is never handed the
flow at all. That is the same "folds to no trigger, invisible everywhere" shape
#3481 found for a non-string `triggerType`, one key over.

`flow-time-relative-descriptor-unroutable` (warning) reports it at authoring
time with the value, its type and the consequence, plus a hint that separates
the two fused concepts: the descriptor says WHICH records to sweep, while HOW
OFTEN is the sibling key `config.schedule`.

A separate criterion, not a widening of #5496's `flow-time-relative-descriptor-
invalid`. Widening was rejected because `isTimeRelative` also feeds
`isAutoTriggered`, so it would have moved two already-published rules' coverage
as a side effect of adding a third. The two ids instead partition the key's
non-null values along the engine's own routing predicate — a value the engine
routes gets the schema's verdict, a value it routes nowhere gets this one, and
never both. Arrays and `Date` are `typeof 'object'`, so they stay with the shape
rule, which already reports them off the schema's own words.

The consequence clause is computed per flow rather than asserted uniformly: a
start node that also declares a trigger the engine recognizes DOES bind and fire
— on that trigger's terms, with the descriptor silently dropped — and claiming
"never fires" about such a flow would be a false diagnostic.

Nothing is made tolerant: a scalar is still not a descriptor, and runtime
behaviour is unchanged.
@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 6, 2026 4:02am

Request Review

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint.

3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/automation/hook-bodies.mdx (via @objectstack/lint)
  • content/docs/permissions/authorization.mdx (via @objectstack/lint)
  • content/docs/releases/v17.mdx (via @objectstack/lint)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Aug 6, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 6, 2026 04:11
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 6, 2026
Merged via the queue into main with commit 06fc07a Aug 6, 2026
24 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-5647-scalar-timerelative-lint branch August 6, 2026 04:21
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.

标量 config.timeRelative(如 timeRelative: 'daily')= 引擎解析不出任何 trigger,flow 永不触发且全层零输出

2 participants