fix(lint): report a config.timeRelative that is not a descriptor object (#5647) - #5758
Merged
Merged
Conversation
…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.
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
marked this pull request as ready for review
August 6, 2026 04:11
This was referenced Aug 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #5647
问题
flow start 节点的
config.timeRelative写成标量(timeRelative: 'daily'—— 把 sweep 的节奏和描述符两个概念揉在一起时最自然的错写)时,每一层都完全沉默。前提验证(先在 origin/main 上实测,再写规则)
立单方给的是探针级证据,本 PR 在 API 面和 CLI 面各复证一遍:
FlowSchema.safeParse/defineFlowconfig是开放槽(ADR-0018),外层没有任何 gate 看进去timeRelative仍是string—— schema 既没拒也没实体化掉,规则在两个 tier 都看得见validateFlowTriggerReadiness[]lintFlowPatterns[]os validate(真实 example app)所以前提成立,而且比 #5496 修掉的缺口还差一档。因果链:引擎的路由判据是
config.timeRelative != null && typeof … === 'object',标量从 time-relative 分支穿过;该 start 节点再无其他 trigger 键时,resolveTriggerBinding返回 undefined、activateFlowTrigger静默 return。连「描述符是对象但形状错」那一行 bind 期 warn 都没有 —— 因为 trigger 从来没被交到这个 flow。这正是 #3481 在triggerType上发现的同一个形状(「折叠成无 trigger,到处都看不见」),换了一个键。改动
packages/lint/src/validate-flow-trigger-readiness.ts新增判据 1e + 规则 idflow-time-relative-descriptor-unroutable(warning),在 authoring 期报出值、类型和后果,hint 把揉在一起的两个概念拆开:描述符说扫哪些记录({ object, dateField, 以及 withinDays / offsetDays 二选一 }),多久扫一次是同级键config.schedule。真实
os validate输出:独立判据,不是放宽 #5496
⛔ 没有动
isTimeRelative。它同时喂 §1b 的 unknown-object 与isAutoTriggered,放宽它会把另外两条已发布规则的覆盖面当作副作用改掉。两条 id 改为沿引擎自己的路由判据划分config.timeRelative的非空取值:typeof === 'object'(含数组和Date)→ 引擎会路由,TimeRelativeTriggerSchema给出裁决 →flow-time-relative-descriptor-invalid(timeRelative描述符跑不通时 authoring 期零诊断 —— 两条 flow lint 一条只看非空、一条只看对象名(#4966 建议 2) #5496)。这条路径有 bind 期 warn,规则只是把它提前。'daily'、7、true、函数)→ 引擎哪儿都不路由,schema 和 trigger 永远看不到 → 本条。这条路径没有任何运行时通道可供提前。这一点修正了派发单里「非普通对象(标量/数组等)」的措辞:实测数组与
Date的typeof都是'object',#5651 落地的规则已经报它们(用 schema 自己的话)。若按「非普通对象」实现,数组会被两条规则同时报。测试里有双向的互不越界断言。后果分句按 flow 计算,不统一断言
标量单独出现 = 绑不上任何东西、永不触发(issue 的情形,也是更坏的那个)。但同一个 start 节点若还声明了引擎认识的 trigger,flow 确实会绑上、会触发 —— 按那个 trigger 的条款,描述符被静默丢弃(每次触发一次、context 上没有记录,而不是每条匹配记录一次)。对这种 flow 说「永不触发」是假诊断,而假诊断比没有诊断更没价值。所以后果分句分两种,两种都有测试钉住。
验证
??别名读法 (#5017) #5046 那种「因为什么都没产出而绿」),已在同一用例内加了对照,现在也会红。唯一保持绿的是常量身份断言(…_UNROUTABLE !== …_INVALID),它本来就没有行为可失去。pnpm --filter @objectstack/lint test:60 files / 1397 tests 全绿;typecheck、build 绿;eslint 绿。pnpm --filter @objectstack/cli test→ 83 files / 825 tests 全绿。全仓扫描确认 cli / examples / triggers 里没有任何 fixture 用标量timeRelative,故无 fixture 需要三分诊处置。timeRelativesweep 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:在
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