Skip to content

fix(lint): flow-runas-unscoped 用一句两读皆真的措辞命名有效身份 (#5693) - #6060

Open
hotlong wants to merge 1 commit into
mainfrom
claude/issue-5693-runas-wording-two-way-true
Open

fix(lint): flow-runas-unscoped 用一句两读皆真的措辞命名有效身份 (#5693)#6060
hotlong wants to merge 1 commit into
mainfrom
claude/issue-5693-runas-wording-two-way-true

Conversation

@hotlong

@hotlong hotlong commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Fixes #5693

按分诊两轮裁定走方向 (3):一句两读皆真的措辞,无新输入、无死支。⛔ 方向 (1)(规则增读 pre-parse stack)未做;也没有需要升级到 needs_decision 的开放问题 —— 理由写在「改了什么」一节里(本仓在同一机制上已有裁定过的先例)。

前提复核(先证伪再实现)

issue 的核心断言成立,并且我实测出一处它没有的修正,这处修正让方向 (3) 更站得住:

作者未写 runAs 作者写了 runAs:'user'
os validate / os build explicit 支(唯一可达) explicit 支
os lint explicit 支(唯一可达) explicit 支
runtime publish gate(#4463) default 支 explicit 支
  • CLI 侧 issue 说对了,机制比它写的更强一层:FlowSchema.runAs.default('user'),registry 把规则接为 input: 'parsed';os lint 本身 Zod-parse(runAuthoringRules('lint', { normalized: config }),authoring-rules.ts:1035parsed 时回落 normalized),照理应该逃过 —— 但 defineStack / defineFlow定义时parse(stack.zod.ts:1245 ObjectStackDefinitionSchema.safeParse 后返回 result.data),所以连 os lint 拿到的也是默认值已物化的 stack。实测确认:两条命令对「没写 runAs」的作者打的都是 explicit 支。
  • issue 没有的一条:default 支并非全局死支。 runtime publish gate 判的是逐字原文 body(protocol.ts:7724 注释 "Keep the body verbatim",schema 检查后 request.item 不被 parsed 结果替换),所以那一面两支都可达。

也就是说,缺陷比 issue 描述的更整齐一点:同一个 flow 被两个 shipped 面告知两件不同的事,而在作者最先遇到的那个面上,拿到的恰是读起来像指控的那支。

改了什么

删掉 typeof flow.runAs === 'string' 的三元,换成一个常量 RUNAS_EFFECTIVE_IDENTITY:

runs under `runAs:'user'` (the default when none is declared)

括号是对取值的陈述,不是对作者的指控,所以对确实写了 runAs:'user' 的人同样为真。这不是新发明的写法,而是本仓在同一机制上已有的做法 —— flow-draft-status-ambiguous(validate-flow-trigger-readiness.ts:480)说 has status 'draft' (the default when none is authored),它的行内注释写的正是同一个原因:「authored or defaulted(defineFlow parses at definition time, so the two are the same here)」。这条先例也是不需要升级决策的理由:同一诊断措辞合同在本仓已被裁定过一次,方向 (3) 就是那次的结论,不存在两种读法导向不同架构的岔口。

'user' 写死而不是从 flow.runAs 插值,是刻意的:分支已排除 'system',enum 只有这两个值,而每一个能走到这句话的面上取值都已被 schema 认可(CLI 是 parsed;runtime gate 在 gate 之前已按 overlay schema safeParse)。插值会重新长出一条只有 off-spec 字面量才能喂到的支 —— 正是这次要删掉的那个形状。

只有措辞变了:命中的 flow、severity: 'error'wherehint、以及嵌套证据节点的 region 从句都不变。

实跑证据(照 issue 的实测方法)

examples/app-todooverdue_escalation,只改 runAs 一行(测完已还原,不在本 PR diff 里):

BEFORE — 作者未写 runAs

os validate: flow 'overdue_escalation' · runAs: schedule-triggered flow runs as `runAs:'user'`, but a
             schedule run has no trigger user — so its data node 'get_overdue_tasks' (get_record) …
os lint:   ✗ flow 'overdue_escalation' · runAs: schedule-triggered flow runs as `runAs:'user'`, but a
             schedule run has no trigger user — so its data node 'get_overdue_tasks' (get_record) …

BEFORE — 作者写了 runAs:'user':与上面逐字相同(这就是「区分不可达」的直接证据)。

AFTER — 两种输入 × 两条命令,四次输出完全相同:

flow 'overdue_escalation' · runAs: schedule-triggered flow runs under `runAs:'user'`
(the default when none is declared), but a schedule run has no trigger user — so its
data node 'get_overdue_tasks' (get_record) has no identity to scope to and will be
REFUSED at run time.

runtime publish gate 直调 runRuntimeAuthoringRules({ type: 'flow', item }),改前无 runAs 键走 default 支、有键走 explicit 支;改后两者同样收敛到上面这一句。

三个 example app 的 os validate 均通过(app-todo / app-crm / app-showcase),无新增发现。

测试:把「可达性」钉住,不再让死支只被裸字面量喂绿

新增 #5693 一组四个用例,并且两个形状分工不同、只有第二个有牙 —— 这点写在用例注释里,不含糊:

  • parsed 形状(FlowSchema.parse,production 输入):先断言 parsed.runAs === 'user' 对两种作者输入都成立,再断言消息逐字相同。它证明的是分支为什么无意义;分支若复活,这个用例照样绿 —— 这正是缺陷本身。
  • unparsed 形状(runtime gate 的输入):是唯一还能区分两种作者输入的形状,因此是防复活的那道闸。
  • 加一条「同一 flow,两个面说同一句话」的对照。
  • 另有一条把 registry 接线钉住:AUTHORING_RULESlintFlowPatternsinput 必须是 'parsed'

既有走 default 支的两个用例按新措辞更新(含 #5633 那条逐字断言的 top-level twin)。新 fixture 补齐了 FlowSchema 要求的 label(flow 与每个 node),否则它根本 parse 不过 —— parse 不过的 fixture 证明不了任何关于 parsed 面的事。

反向验证(方向先声明后跑)。 第一次把整条三元原样放回时,四个用例连同两个老用例一起红 —— 但那一次同时撤掉了括号,证不出想证的东西,如实记在这里。于是做了隔离版:只让「CLI 不可达的那一支」不同,可达支保留新措辞。预期 parsed 绿 / unparsed 红,实测正是如此:

隔离版(分支复活,可达支措辞不变):
  ✓ PARSED input: the two authoring choices are literally the same object here
  × UNPARSED input: an absent key and an explicit one still get the same sentence
  × says the same thing to both surfaces about the same flow
  × leaves the TOP-LEVEL twin without a region clause (no regression)
  × flags a schedule flow whose runAs is unset
  Tests  4 failed | 104 passed (108)

Changeset

带 changeset,@objectstack/lint patch:lint 是发布包,这句话是每个 os validate / os build / os lint 使用者都会看到的诊断正文,措辞变更用户可见 —— 但没有 API/行为/severity 变化,所以是 patch 而非 minor。

顺带改了 #5633(PR #5695)那份尚未发布的 changeset 里的样例输出:它逐字引用了旧句子,与我这份同处一个发布窗口,不改的话编译出来的 release notes 会自相矛盾地给出一句从未 ship 过的措辞。只动样例和一句「byte-identical」的措辞,不动它的任何结论。

验证

  • pnpm --filter @objectstack/lint testTest Files 61 passed (61) / Tests 1457 passed (1457)
  • pnpm --filter @objectstack/lint typecheck → 干净
  • eslint packages/lint/src/lint-flow-patterns{,.test}.ts → 无输出
  • pnpm check:nul-bytesOK (scanned 5780 tracked text file(s) … no raw ASCII control bytes);另按逐字节自扫改动文件,无控制字节
  • check:release-notes / check:adr-anchors / check:doc-authoring / check:engine-double-contract / check:error-code-casing 全绿

范围

严格限于申报文件面:packages/lint/src/lint-flow-patterns.ts、其测试文件、.changeset/⚠️ 同文件家族的 #5700(loop fixture 的 itemVar)本轮未触碰 —— 本 PR 新增的 loop 类 fixture 一律用 canonical 的 iteratorVariable


🤖 Generated with Claude Code

https://claude.ai/code/session_01BDmDsu2575gDxeMCxXhDE3

规则原先按 `typeof flow.runAs === 'string'` 分支消息:作者写了就打
`runAs:'user'`,没写就打 `the default runAs:'user'`。这个区分是真的、也有
用,但规则观察不到它 —— 作者拿到哪一支取决于**面**,而不是取决于他们的文件:

- CLI 恒为 explicit 支。`FlowSchema.runAs` 带 `.default('user')`,registry 把
  该规则接为 `input: 'parsed'`;`os lint` 虽不 Zod-parse,但 `defineStack` /
  `defineFlow` 在**定义时**就 parse,所以连它拿到的也是默认值已物化的 stack。
- runtime publish gate(#4463)判的是逐字原文 body,两支都可达。

于是同一个 flow 被两个 shipped 面告知两件不同的事,而在作者最先遇到的那个面上,
拿到的恰是读起来像指控的那支:对一个没写过 runAs 的人说「runs as `runAs:'user'`」。

改法:删掉分支,换成一句对两种作者输入都为真的措辞 —— `runs under
\`runAs:'user'\` (the default when none is declared)`。括号是对**取值**的陈述,
不是对作者的指控,所以对确实写了 `runAs:'user'` 的人同样为真。这是本仓已有的
写法而非新发明:`flow-draft-status-ambiguous` 在同一机制下说
`has status 'draft' (the default when none is authored)`。

只有措辞变了:命中的 flow、`severity: 'error'`、`where`、`hint`、嵌套证据节点的
region 从句都不变。

测试同批把可达性钉住:parsed 形状(`FlowSchema.parse`)证明两种作者输入到规则
手上已是同一个对象;unparsed 形状(runtime gate 的输入)是唯一还能区分二者的
输入,因而是防止分支复活的那道闸。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BDmDsu2575gDxeMCxXhDE3
@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:05pm

Request Review

@github-actions github-actions Bot added the size/m label Aug 6, 2026

hotlong commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

CI 收敛记录(2026-08-06,GitHub Actions 自 ~15:14Z 起波状故障)。

门禁族:

  • TypeScript Type Check → success(job 92678285003,run 31118550776 attempt 2,16:30:57→16:37:06Z;34 步逐步 success,含 Build workspace packages / Type check workspace packages / Type check example apps)。
  • ESLint → 四次 attempt 均 cancelled,从未进入 Set up job 之后,因此既无 success 也无 failure 结论:attempt 1 92673942569(16:20:12Z 随整 run 取消)、attempt 2 92678284928(排队 16 分钟无 runner)、attempt 3 92681605560、attempt 4 92685589931(17:11:45Z 起排队)。

平台签名验签(逐个):

  • main 自身(sha 9e3709a4,即本 PR base)同一 ESLint job 92670395902Set up job 步骤结论 = failure,15:48:32→15:52:36Z —— 该门禁在 main 上同样起不来,与本 PR 改动无关。
  • 本 PR 其余红全部止步于 Prepare all required actions,日志逐字为 Failed to resolve action download info. Error: Service Unavailable:job 92675133177(Dogfood Regression Gate 1/3)、92675133221(2/3)、92675133227(Test Core 2/3)。

ESLint job 的内容已在本地按其步骤单跑,全绿:

npx eslint packages/lint/src/lint-flow-patterns.ts packages/lint/src/lint-flow-patterns.test.ts
  → 无输出

pnpm check:nul-bytes                → OK (scanned 5780 tracked text file(s) … no raw ASCII control bytes)
pnpm check:release-notes            → OK
pnpm check:adr-anchors              → OK (34 anchored file(s))
pnpm check:doc-authoring            → 362 files clean
pnpm check:engine-double-contract   → OK (73 pinned, 133 DEBT, 2 exempt)
pnpm check:error-code-casing        → OK (3060 scanned file(s))

ESLint 一旦拿到 runner 即为真结论;在此之前本 PR 不作绿判,记录在此供复核。


Generated by Claude Code


Generated by Claude Code

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

Labels

Projects

None yet

2 participants