Skip to content

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

Merged
hotlong merged 1 commit into
mainfrom
claude/issue-5693-runas-wording-two-way-true
Aug 7, 2026
Merged

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

Conversation

@hotlong

@hotlonghotlong 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 buildexplicit 支(唯一可达)explicit 支
os lintexplicit 支(唯一可达)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:1245ObjectStackDefinitionSchema.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/lintpatch: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

vercelBot commented Aug 6, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 6, 2026 4:05pm

Request Review

@hotlongClaude

Copy link
Copy Markdown
ContributorAuthor

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

@github-actions

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.

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

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@hotlong@claude