Skip to content

fix(lint): report a config.timeRelative descriptor the sweep will refuse, at authoring time (#5496) - #5651

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-5496-time-relative-descriptor-lint
Aug 5, 2026
Merged

fix(lint): report a config.timeRelative descriptor the sweep will refuse, at authoring time (#5496)#5651
os-zhuang merged 1 commit into
mainfrom
claude/issue-5496-time-relative-descriptor-lint

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #5496

按分诊裁决执行方案 1:在 packages/lint/src/validate-flow-trigger-readiness.ts 的 §1b 区域新增规则,对非空 config.timeRelativeTimeRelativeTriggerSchema.safeParse,失败时逐字转发 zod 的 issue 列表。判定权唯一留在 schema —— lint 里没有任何一行复写描述符的形状知识。

前提复核(先证实,再动手)

issue 正文的前提在当前 main(e6b1bb0,#5635 今日改过 lint-flow-patterns.ts 之后)仍然成立。实测:task 对象存在、status: 'active'runAs: 'system',即除描述符外一切合规:

validateFlowTriggerReadiness = []
lintFlowPatterns             = []

两条 lint 全沉默,原因与正文一致:lint-flow-patterns.ts:254startCfg.timeRelative != null 只看非空;§1b 只读 timeRelative.object 拿去比对 stack 里的对象名。TimeRelativeTriggerSchema 确实拒绝这个描述符,但它唯一运行的地方是 bind 期(TimeRelativeTriggerPlugin.start():warn 后 return)—— sweep 永不装,flow 自称已武装,作者唯一的反馈是服务器日志里的一行。对 AI 作者来说那行完全在反馈回路之外,它读的是 os validate

改法

新规则 flow-time-relative-descriptor-invalid(severity warning,与该文件现行一致):

  • 判据刻意与引擎的路由判据逐字一致(AutomationEngine 的 trigger 解析:config.timeRelative != null && typeof … === 'object'),所以规则只为「引擎真正交给 time-relative trigger 的那些 flow」发言,不多说也不少说。
  • 诊断把 zod 的 issue 列表按 path: message 拼接,与 bind 期 warn 的渲染方式完全一致(TimeRelativeTrigger.start() 用的就是这个格式),两条通道讲同一个故事、同一种方言。仅做空白折叠:finding 在 CLI 里是一行(• where: message),而 schema 的 guidance 分条带换行。
  • 因此诊断自带 schema 已经算好的东西:缺失的键名、错误的类型、未知键的「Did you mean」(fielddateField),以及 wrong-layer 提示(schedule 写在描述符里面时,告诉作者它是同级兄弟)。
  • 唯一被前移的是 schema 运行的时机;判定与措辞仍然全部属于 TimeRelativeTriggerSchema,所以描述符将来加键时规则自动跟上,不会和第二份副本漂移。没有引入任何消费端宽容(PD Add comprehensive test suite for Zod schema validation #12):不加 ?? 别名、不做 coercion。

packages/lint/src/index.ts 补了新规则 id 常量的 barrel 导出(该包只开 "." / "./runtime" 两个入口,不进 barrel 消费者就拿不到)。

三条完成判据(逐条实测,非推断)

① 坏描述符点名 config.timeRelative 且含 zod 键名 —— 把 showcase 的 Task Due Reminder 临时改成 issue 正文那个描述符,跑真的 os validate(已还原):

⚠ flow "showcase_task_due_reminder" › start node: has a config.timeRelative descriptor the
time-relative trigger REFUSES at bind time, so the sweep is never installed — the flow declares a
time-relative trigger and then never runs (the only trace is one warn in the server log).
dateField: Invalid input: expected string, received undefined; offsetDays: Invalid input: expected
array, received number; (root): Unrecognized key(s) on this flow start node's `config.timeRelative`
descriptor: `field`. … Did you mean `field` → `dateField`?
      rule: flow-time-relative-descriptor-invalid  at flows[0].nodes[0].config.timeRelative

三条 zod issue(dateField 缺失、offsetDays 需数组、field 未知键)全部到位。

② canonical 描述符零诊断(A/B 实测) —— 仓库里 10 个真实 time-relative 描述符逐个 safeParse,全部 spec-valid(showcase Task Due Remindercontent/docs/automation/flows.mdxcontent/docs/references/automation/time-relative-trigger.mdx 三个例子、各包测试 fixture)。三个 example app 的 os validate 输出改动前后逐行一致:

##### DIFF app-showcase (before vs after) → IDENTICAL — zero new diagnostics
##### DIFF app-crm      (before vs after) → IDENTICAL — zero new diagnostics
##### DIFF app-todo     (before vs after) → IDENTICAL — zero new diagnostics

「before」是真的 before:stash 掉规则、重建 @objectstack/lint dist、重跑同一条命令。三个 app 都 exit=0,showcase 输出里 task_due_reminder 一次都没出现。

③ 与 §1b unknown-object 不重复报同一件事 —— 对象名错 + 形状同时错时的实测输出:

⚠ … sweeps object 'contract', which this stack does not define — …
      rule: flow-trigger-unknown-object  at flows[0].nodes[0].config.timeRelative.object
⚠ … has a config.timeRelative descriptor the time-relative trigger REFUSES at bind time, …
      dateField: …; (root): Unrecognized key(s) … Did you mean `field` → `dateField`?
      rule: flow-time-relative-descriptor-invalid  at flows[0].nodes[0].config.timeRelative

两条 finding、两个 path、两件不同的事:只有 stack 知道对象名存不存在,只有 schema 知道形状。schema 没有 stack 知识,永远报不出对象名;这条规则不读 tr 的任何其他键,也永远不报形状之外的事。测试里对此双向设了断言(名字那条不提 dateField,形状那条不提 'contract')。

反向验证(方向是先预测再跑的)

预测:把新规则的 safeParse 分支停掉,新增的坏描述符用例应该报零诊断(而不是报错的诊断)—— 因为这正是 issue 的前提。实测方向一致:

× flags the descriptor from #5496 and names every key zod named
  AssertionError: expected [] to have a length of 1 but got +0
× reports a wrong object name and a wrong shape as two facts, not one twice
  expected [ { severity: 'warning', …(5) } ] to have a length of 2 but got 1
(7 failed)

值得记一笔:断言「沉默」的那几个用例(canonical 全绿、非对象标量不报)在没有规则时也通过 —— 它们本来就该那样,所以它们不是这条规则的红证据,真正的红证据是上面那 7 条。恢复后 31/31 全绿。

另设了一条防漂移 pin:测试从 schema 现场算出期望的 issue 文本再比对,所以 schema 措辞变了规则输出跟着变、测试照样绿,而手抄一份副本在规则里就不会。

验证

结果
pnpm --filter @objectstack/lint test 59 files / 1358 passed(新增 10)
pnpm --filter @objectstack/lint typecheck 绿
pnpm --filter @objectstack/lint build 绿
消费半径 pnpm --filter @objectstack/cli test 82 files / 812 passed
example app A/B ×3 逐行一致,零新增诊断
node scripts/check-nul-bytes.mjs OK(5539 files)
控制字节自扫(超出闸门盲区) 改动文件全清
eslint 改动文件 exit=0

severity 建议

维持 warning。跑完真实输出后的实感:后果确实接近 error(声明了 time-relative 触发、运行时永远不绑),但转发的 zod 文案里 strictObject 会带上 history 那句(「Until #4001 these were dropped silently — …」),在 error 的显示位上噪音偏大;而且该文件现行规则全是 warning,单独升一条会让 flow 家族的 severity 不成体系。若要升,建议与该文件其他 never-fire 类规则(flow-trigger-unknown-event)同批升,并单独一个 PR —— 已发布规则 id 的 severity 变更本身就该独立成 PR。

边界(都没碰)

changeset

.changeset/flow-time-relative-descriptor-lint.md —— "@objectstack/lint": patch

范围外发现(已另开,均未指派)

…use (#5496)

A flow start node declaring `config.timeRelative` got zero authoring-time
diagnostics when the descriptor could not parse. The two rules that look at
the slot each looked at something else: `lint-flow-patterns` decides
"time-relative flow" from `timeRelative != null` alone, never the shape, and
`validate-flow-trigger-readiness`'s existing check reads only
`timeRelative.object`, to compare it against the stack's objects.

`TimeRelativeTriggerSchema` does reject a bad descriptor, but the only place
it ran was BIND time, inside `TimeRelativeTriggerPlugin.start()`, which warns
and returns: the sweep is never installed, the flow reports itself armed, and
the author's sole feedback is one line in a server log — outside an AI
author's feedback loop entirely.

New rule `flow-time-relative-descriptor-invalid` (warning) runs that same
schema at authoring time and forwards its issue list verbatim, rendered
exactly as the bind-time warning renders it. No shape knowledge is
re-implemented and no consumer-side tolerance is added: the verdict and its
wording stay the schema's, so the rule tracks the descriptor's contract
instead of drifting from a second copy of it.

The new rule and the existing object-name check decide different facts and
cannot report the same one twice — only the stack knows whether an object name
exists, only the schema knows the shape. Its guard mirrors the engine's
routing predicate character for character, so the rule speaks for exactly the
flows the engine hands to the time-relative trigger.

Verified: every time-relative descriptor shipped in the repo parses, and
`os validate` output on all three example apps is identical before and after.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GX3sL71LFq8m2usg6VqTSE
@vercel

vercel Bot commented Aug 5, 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 5, 2026 10:04pm

Request Review

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

github-actions Bot commented Aug 5, 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.

@os-zhuang
os-zhuang marked this pull request as ready for review August 5, 2026 22:17
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 5, 2026
Merged via the queue into main with commit b2e1057 Aug 5, 2026
25 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-5496-time-relative-descriptor-lint branch August 5, 2026 22:37
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.

timeRelative 描述符跑不通时 authoring 期零诊断 —— 两条 flow lint 一条只看非空、一条只看对象名(#4966 建议 2)

1 participant