Skip to content

fix(formula,lint): current_user 收进 SCOPE_ROOTS,字段级拒绝改为按面的真规则 (#6290) - #6584

Merged
baozhoutao merged 2 commits into
mainfrom
claude/issue-6290-current-user-scope-roots
Aug 8, 2026
Merged

baozhoutao merged 2 commits into
mainfrom
claude/issue-6290-current-user-scope-roots

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes #6290

packages/formula 对同一个根说了两套话:introspectScope 把 current_user 当合法命名空间交给作者、checkRoleCatalog 四条正则全以它打头(两处都对 —— ADR-0068 D1 把它定为规范拼写,buildScope 也确实挂了它),只有 cel-engine.ts 的 SCOPE_ROOTS 不认。于是严格环境把被祝福的拼写读成裸字段引用,而它的两个别名(user、ctx)一路放行;拒绝时给出的还是通用裸字段处方 ——「Write record.current_user」,一个在平台任何一层都不绑定的形状。

按 PM 裁决落地:收下进 SCOPE_ROOTS,字段级判定改为按面的真规则,并补齐 option 级遍历。


前提复核表(动手前逐条实测)

# 前提 结论 证据
P1 origin/main 三处事实原样 ✅ 成立 cel-engine.ts:54-68 的 SCOPE_ROOTS 有 user 无 current_user;validate.ts:427 的 introspectScope 返回 ['record','previous','input','os','current_user','user','vars'];validate.ts:261-269 四条 ROLE_*_RE 全写 `(?:current_user
P2 ADR-0068 把 option 级 visibleWhen 的 current_user 定为合法面 ✅ 成立(经由 spec 与 ADR 两侧) ADR-0068 D1:「同一谓词在 formula / RLS / 客户端 visible gate 求值结果相同」,规范变量名 current_user;spec 侧写得更死 —— field.zod.ts:129-143 的 SelectOptionSchema.visibleWhen JSDoc:「options resolve through resolveCascadingOptions against the predicate scope (ADR-0068 / objectui#2284)… Per-option is the one *When surface where a current_user test actually resolves」
P3 裁决前提:收下后字段级拒绝仍可按「面」维持 ✅ 成立(见下节实测) 耦合确实存在,但可调和
P4 与在飞 #6146 零文件交集 ✅ 成立,且 #6146 已落地 #6146 已于 2026-08-07 15:32Z closed,PR #6315 = commit 8a88885cb,只动 packages/spec/src/{data/field.zod.ts,ui/view.zod.ts} + content/docs/** + changeset;本单动 packages/formula/src/cel-engine.ts 与 packages/lint/src/validate-expressions.ts,交集为空。推前已 merge origin/main(至 a36db28b7),复核 #6146 的口径原样保留

P3 —— 裁决前提的实测细节

耦合方向确认(裁决前提所担心的那半边,确实存在):字段级 visibleWhen 对 current_user 的拒绝,今天完全来自 validateExpression(…, {scope:'record'}) → firstUndeclaredReference → buildScopedEnv 注册 SCOPE_ROOTS。收下 current_user 后该路径不再报:

# origin/main(收下之前)
'admin' in current_user.positions
  validateExpression.ok= false ["bare reference `current_user` — … Write `record.current_user`."]
  firstUndeclaredReference= "current_user"

# 收下之后
'admin' in current_user.positions
  validateExpression.ok= true []
  firstUndeclaredReference= null

但两者可调和 —— 裁决成立,不作废。 判据是同包另一个 helper 的结构性独立:collectCelRootIdentifiers 走 buildEnv(unlistedVariablesAreDyn: true)读 AST,完全不读 SCOPE_ROOTS,收下前后同答案:

# 收下之前 / 之后,两次都是:
collectCelRootIdentifiers("'admin' in current_user.positions") = { ok: true, roots: ['current_user'] }

所以「按面区分」不是勉强绕开,而是本仓已有的既定机制:#3447 P2 的审批节点 approver 就用 collectCelRootIdentifiers 表达闭合根集,validate-visibility-predicates.ts:418 用逐面 VIEW_PAGE_EXTRA_ROOTS 表达同一件事,而 validate-expressions.ts:81 早已 import 了这个 helper。

结论:SCOPE_ROOTS 是「永不 fault」的基线(其自身 doc-comment 就是这么写的:「an unknown root is a missed catch, a missing root is a false positive that would break the build」),不是逐面契约;逐面闭合由面自己声明。收下 current_user 之后,字段级判定不但仍可维持,而且比原来更正:原来它是基线遗漏的副产物,所以只能借通用文案说话 —— 那正是错误处方的成因。

三个半边

1. SCOPE_ROOTS 收下 current_user(packages/formula/src/cel-engine.ts)

一词之改 + 成因注释。新增行为钉(不是列表相等断言,SCOPE_ROOTS 作为基线本就可以多于它对外宣告的):introspectScope 报出的每个根,都必须能在严格环境里解析。

2. 删错误修法提示 —— 判定搬到面上(packages/lint/src/validate-expressions.ts)

新增 checkFieldRuleUserRoot,只作用于字段级 visibleWhen / readonlyWhen / requiredWhen(三者共用一个求值器)。新处方原文:

`visibleWhen` reads `current_user`, but a field-level conditional rule binds only `record` (plus `previous`, and `parent` on a master-detail line item) — `current_user` is unbound here, so the predicate faults and falls back to VISIBLE, leaving the field the test was meant to hide showing for everyone (#6146). To gate the CHOICES of a select by user, move the predicate to the option's own `visibleWhen` (`options: [{ …, visibleWhen: … }]`) — per-option is the one `*When` surface that binds `current_user`. To hide the FIELD by role, declare field-level security on a permission set (`fields: { '< object >.< field >': { readable: false } }`), which the server enforces. To gate on record state, rewrite the predicate against `record`.

三条处方逐条落到实际存在的面上,而非文案改写:option 级 visibleWhen = SelectOptionSchema.visibleWhen(field.zod.ts:143);字段级安全 = PermissionSetSchema.fields(permission.zod.ts:455,FieldPermissionSchema.readable);record 改写 = #6146 收窄后的字段级口径。测试用一条否定断言 + 三条肯定断言钉住(「文案变了」不是要保的性质,「文案指向真的绑定的形状」才是)。

3. option 级遍历补齐(同文件)

f.options[].visibleWhen 首次被走查,按 option value 定位。与宿主字段同为 record scope —— 收下 current_user 之后,两个面的全部差别就落在 checkFieldRuleUserRoot 这一条上,而这正是它们该有的差别(求值器不同:evalFieldPredicate vs resolveCascadingOptions)。

读的是字面 f.options 而非 (f as AnyRec).options:后者会让 #5017 的 meta-guard 源码扫描看不见这个新面(该文件自己就写着这条纪律)。相应更新了 meta 表:f 的期望读集加入 options,新增 opt → SelectOptionSchema 行。

反向验证表(方向先写死,再实测)

预测在动手前落笔,与实测逐条比对。

回退 A —— 从 SCOPE_ROOTS 删掉 'current_user'(其余保留)

测试 预测 实测
formula: every root introspectScope advertises really resolves 红 红 ✅
formula: role-catalog verdict reachable through every user spelling 红 红 ✅
lint ①: still REJECTS a field-level visibleWhen 红 —— 不是因为拒绝消失,而是因为拒绝翻倍(通用裸引用错误回来了,和新规则并排) 红:expected [ { …(4) }, { …(4) } ] to have a length of 1 but got 2 ✅ 连红的原因都对上
lint ①b: prescribes surfaces that exist — never record.current_user 红,且 [0] 就是那条字面写着 Write \record.current_user`` 的通用文案 红:expected 'bare reference \current_user` — a for…' not to contain 'record.current_user'` ✅ 本 issue 的缺陷被原样复现
lint ②: ACCEPTS the showcase role-gated OPTION 红 —— 合法用法开始失败 红:expected [ { …(4) } ] to have a length of +0 but got 1 ✅
lint ③: option 遍历正向证据(5 条) 绿(不受影响) 绿 ✅

未列入预测表、同向连带转红的一条:rejects it on readonlyWhen / requiredWhen too —— 同一「翻倍」成因(4 条而非 2 条)。如实记录,非预测命中。

回退 B —— 删掉 option 级遍历循环(保留 SCOPE_ROOTS 改动)

测试 预测 实测
lint ③: option 遍历正向证据(5 条) 红 红(5 条全红,expected [] to have a length of 1) ✅
lint ②: ACCEPTS the showcase role-gated OPTION 绿 —— 且是空绿。toHaveLength(0) 通过是因为什么都没走,不是因为判定对(#5046 的空绿陷阱) 绿 ✅ 预测方向与成因一致
lint ①/①b 绿 绿 ✅
formula 两条钉 绿 绿 ✅

未列入预测表、额外转红的两条:#5017 meta-guard 的 every key read off 'opt' 与 the field receiver reads only declared keys —— 源码扫描独立于行为发现了「读没了」。属于额外收益,如实记录。

这就是 ② 必须与 ③ 并存的理由:② 单独看,在回退 B 下是空绿的;真正证明遍历跑起来的是 ③ 产出的 finding。

命令输出

$ pnpm --workspace-concurrency=2 --filter @objectstack/formula --filter @objectstack/lint test -- --maxWorkers=2
packages/formula test:  Test Files  18 passed (18)
packages/formula test:       Tests  424 passed (424)
packages/lint test:  Test Files  62 passed (62)
packages/lint test:       Tests  1596 passed (1596)

$ pnpm --workspace-concurrency=2 --filter @objectstack/formula --filter @objectstack/lint typecheck
packages/formula typecheck: Done
packages/lint typecheck: Done

$ node scripts/check-nul-bytes.mjs
check-nul-bytes: OK (scanned 6163 tracked text file(s); … no raw ASCII control bytes).

$ turbo run build --filter='./packages/*' --filter='./packages/*/*' --concurrency=2
 Tasks:    70 successful, 70 total

$ pnpm check:type-check-debt
check-type-check-coverage: OK — 62/77 workspace packages type-checked …
check-type-check-coverage --re-measure: OK — 34 ledger entr(ies) re-measured …, none above its recorded number.

真实 metadata sweep(半边 3 是激活一条检查,同 #5026,必须扫真栈):

$ (examples/app-showcase) pnpm validate   →  ✓ Validation passed (1267ms)   exit=0
$ (examples/app-crm)      pnpm validate   →  ✓ Validation passed (329ms)    exit=0
$ (examples/app-todo)     pnpm validate   →  ✓ Validation passed (263ms)    exit=0

零新增 finding —— 包括同时携带 record 级联选项与角色门控 current_user 选项的 showcase_cascading_select。

消费半径外扩(SCOPE_ROOTS 加宽会改动所有 record scope 面的判定,故不止本两包):

$ pnpm --filter @objectstack/cli test   →  Test Files  91 passed (91)   Tests  928 passed (928)
$ pnpm --filter @objectstack/mcp test   →  Test Files   9 passed (9)    Tests   97 passed (97)

说明

  • user / ctx.user 别名在字段级仍静默放行,本 PR 不动 —— 那是收下 current_user 之前就存在的洞(两者一直在 SCOPE_ROOTS 里),不是本次引入的回归,且 ctx 还是 ActionEngine 的根,爆炸半径另算。已按纪律另立 finding,不在本 PR 修。新文案指的是面而非拼写,所以不会把作者推去写 user.positions。
  • 未触 objectql,check:engine-double-contract 一类 objectql 面门禁不适用。
  • type-check-debt 台账只缩不抬:本次未抬高任何条目(@objectstack/lint TEST_DEBT 记 42、实测 19,该盈余为改动前既有,不属本单)。

Generated by Claude Code

claude added 2 commits August 8, 2026 05:47
`@objectstack/formula` 对同一个根说了两套话。`introspectScope` 把
`current_user` 作为合法命名空间交给作者,`checkRoleCatalog` 的四条
position 成员判定正则也全以它打头 —— 两处都对:ADR-0068 D1 把
`current_user` 定为**规范**拼写,`buildScope` 也确实把同一个 `EvalUser`
挂在它下面。只有 `cel-engine.ts` 的 `SCOPE_ROOTS` 不认,于是严格环境把
这个被祝福的拼写读成**裸字段引用**,而它的两个别名(`user`、`ctx`)
一路放行。

三处改动:

1. `SCOPE_ROOTS` 收下 `current_user`。该表是「永不 fault」的基线,不是
   逐面契约,现在它宣告的与本包别处宣告的一致。新增行为钉:
   `introspectScope` 报出的每个根都必须能在严格环境里解析。

2. 删掉错误修法提示。旧拒绝是基线遗漏的副产物,作者拿到的是**通用**
   裸字段诊断 ——「Write `record.current_user`」。这个形状在平台的任何
   一层都不绑定,照做的作者得到的东西比原来更糟,而且照样静默。字段级
   判定现在由 `@objectstack/lint` 里一条自己的规则给出,写明真实失败链
   (未绑定 ⇒ fault ⇒ 可见性 fallback 为 `true` ⇒ 本想藏起来的字段对所有
   人恒可见,#6146),并给出**真实存在**的处方:把谓词移到选项自己的
   `visibleWhen`、在权限集上声明字段级安全
   (`fields: { '<object>.<field>': { readable: false } }`)、或改写成
   `record` 谓词。覆盖共用同一求值器的 `visibleWhen` / `readonlyWhen` /
   `requiredWhen`。

3. option 级 `visibleWhen` 首次被校验。`validate-expressions.ts` 走完
   字段级条件规则就停了,于是 `SelectOption.visibleWhen` —— 一个客户端
   过滤、服务端强制的可授权 CEL 槽 —— 穿过 compile / validate / 运行期
   无人校验。裸字段引用、指向不存在字段、语法错误、误用 template 方言
   全部静默通过,选项只是从此不再出现。现在按 option value 定位逐条走查,
   与宿主字段同为 `record` scope。

两个面**故意**对 `current_user` 给出相反判定,因为求值器不同:字段级走
`evalFieldPredicate`(`record` + `previous` + `parent`,从不绑用户),
option 级走 `resolveCascadingOptions`,对宿主 predicate scope 求值,确实
绑定它(ADR-0068 / objectui#2284)。showcase 的角色门控选项
(`'admin' in current_user.positions`)此前从未撞上本规则,现在作为合法
用法被钉住。

Sweep:option 遍历生效后,三个示例应用(`app-showcase` / `app-crm` /
`app-todo`)的 `objectstack validate` 全部通过 —— 零新增 finding,包括
同时携带 record 级联与角色门控选项的那个 showcase 对象。

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

vercel Bot commented Aug 8, 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 8, 2026 5:54am

Request Review

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/formula, @objectstack/lint.

9 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/data-modeling/formulas.mdx (via @objectstack/formula)
  • content/docs/data-modeling/validation.mdx (via @objectstack/formula)
  • content/docs/permissions/authorization.mdx (via @objectstack/lint)
  • content/docs/plugins/packages.mdx (via @objectstack/formula)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/formula)
  • content/docs/releases/v15.mdx (via @objectstack/formula)
  • content/docs/releases/v16.mdx (via @objectstack/formula)
  • 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 8, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review August 8, 2026 06:07
@baozhoutao
baozhoutao added this pull request to the merge queue Aug 8, 2026
Merged via the queue into main with commit e9b5265 Aug 8, 2026
25 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-6290-current-user-scope-roots branch August 8, 2026 06:18
veigajoao pushed a commit to veigajoao/objectstack that referenced this pull request Sep 29, 2026
… to the commits that decided them (objectstack-ai#20612)

Part of objectstack-ai#20597
Clause-②: no

The `packages/lint` stage of the dead-citation sweep: the `domain:spec`
lane's only package (census `5884031174` on objectstack-ai#20556, claim `5885046469`).
Every comment or docblock line in 22 of the 23 claimed
`packages/lint/src/` files that cited a tracker number answering 404 now
cites, in ruling C+D's form C, the commit in this repository's history
that decided what the line describes, and says in its own words what was
decided. Comments only: 81 lines out, 81 in, across 22 files. No code
token, string literal, rule message, hint or rule id moves.

`authoring-rules.ts` (5 sites) is excluded and left at its base blob: PR
objectstack-ai#20593 (objectstack-ai#20553) edits it and was still open at the last read
(2026-09-29T07:34Z). So this PR says `Part of`: those 5 sites stay for a
follow-up once that PR lands, with their anchors already verified (see
Acceptance notes).

The census is the gate's own `node scripts/check-issue-citations.mjs
--census --json`, filtered to `packages/lint/`. Before: base
`7a1faf1a5d`, 2026-09-29T06:42:03Z to 06:45:24Z, board enumerated (185
pages, frontier objectstack-ai#20606). After: head `0c7b847f18`, 07:28:22Z to
07:31:52Z (185 pages, frontier objectstack-ai#20611).

## Measurement

| file (under `packages/lint/src/`) | dead before | after | numbers,
then anchor |
|---|---:|---:|---|
| `lint-flow-patterns.ts` | 9 | 0 | objectstack-ai#13681 ×9 to `8ed9c54b4` (objectstack-ai#14394
stays, 200) |
| `validate-hook-body-writes.ts` | 9 | 0 | objectstack-ai#8663 ×6 to `192213f66`;
objectstack-ai#13657 ×3 to `b003cf2e8` |
| `runtime-gate.ts` | 8 | 0 | objectstack-ai#10064 ×5 to `def0d3e63`; objectstack-ai#19370 ×2 to
`a227afa41` (objectstack-ai#19143 stays); objectstack-ai#9798 to `c7655d472` (objectstack-ai#9261 stays) |
| `validate-searchable-fields.ts` | 7 | 0 | objectstack-ai#8404 ×4 to `b849e6911`, the
`pre-objectstack-ai#8404` control at `:312` included; objectstack-ai#10001 ×3 to `f1b5ad39a` |
| `authoring-rules.ts` | 5 | **5** | excluded: PR objectstack-ai#20593 holds the file
|
| `validate-expressions.ts` | 5 | 0 | objectstack-ai#6290 ×5 to `e9b526597` (objectstack-ai#6584,
that commit's own PR, stays) |
| `validate-react-page-props.ts` | 5 | 0 | objectstack-ai#11284 ×4 to `5383fa670`;
objectstack-ai#8404 to `b849e6911` |
| `validate-sortable-fields.ts` | 5 | 0 | objectstack-ai#10001 ×4 to `f1b5ad39a`;
objectstack-ai#8404 to `b849e6911` |
| `validate-flow-node-writes.ts` | 4 | 0 | objectstack-ai#8663 ×4 to `192213f66` |
| `validate-page-field-bindings.ts` | 4 | 0 | objectstack-ai#6629 ×2 to `cd584d559`
(plus the slash-joined `:208`, see Deviations); objectstack-ai#8664 ×2 to `8798cd2a6`
|
| `validate-translation-references.ts` | 4 | 0 | objectstack-ai#14700 ×3 to
`de3c52beb` (objectstack-ai#14253 stays); objectstack-ai#6124 to `b3c1f3cd5` |
| `lint-liveness-properties.ts` | 3 | 0 | objectstack-ai#10262 ×3 to `2aca1bc4c` |
| `validate-action-body-writes.ts` | 3 | 0 | objectstack-ai#8663 ×3 to `192213f66` |
| `validate-security-posture.ts` | 3 | 0 | objectstack-ai#19370 ×3 to `a227afa41`
(objectstack-ai#8310 stays) |
| `flow-template-grammar.ts` | 2 | 0 | objectstack-ai#11060 ×2 to `815585513` |
| `data-model-rules.ts` | 1 | 0 | objectstack-ai#10064 to `def0d3e63` |
| `reference-integrity-suite.ts` | 1 | 0 | objectstack-ai#13653 to `36d287803` |
| `validate-component-types.ts` | 1 | 0 | objectstack-ai#12950 to `225e7690f` (objectstack-ai#12183
stays) |
| `validate-empty-combinators.ts` | 1 | 0 | objectstack-ai#6528 to `3510e4a25` (objectstack-ai#5659
stays) |
| `validate-list-view-field-refs.ts` | 1 | 0 | objectstack-ai#10001 to `f1b5ad39a` |
| `validate-readonly-action-writes.ts` | 1 | 0 | objectstack-ai#13653 to `36d287803` |
| `validate-readonly-flow-writes.ts` | 1 | 0 | objectstack-ai#13653 to `36d287803` |
| `validate-readonly-hook-writes.ts` | 1 | 0 | objectstack-ai#13653 to `36d287803` |
| **23 files** | **84** | **5** | 21 numbers; 19 removed from the 22
edited files, to 19 distinct shas |

Per-file counts at base equal the claim's (census at `f11b5f20a2`) in
all 23 files. A second instrument agrees site for site: every `#N` in
the 23 files, classified by the TypeScript parser as comment, string or
code, and each of 360 distinct numbers probed by REST `issues/N` without
following redirects. At base it found 1,323 sites (1,259 comment, 64
string, 0 code); 339 numbers answer 200 and 21 answer 404, the census's
21. Its dead comment sites are the census's 84 plus one slash-joined
`objectstack-ai#5775/objectstack-ai#6629` the grammar does not read, and it found one dead
**string**: `validate-react-page-props.ts:1198` (see Acceptance notes).
At head: 1,243 sites and 344 numbers, the same 339 answer 200, and 5
answer 404, all in `authoring-rules.ts` comments or that one string. Lit
controls objectstack-ai#16862, objectstack-ai#16847 and objectstack-ai#17698 answered 200, and dead controls
objectstack-ai#16714, objectstack-ai#16715 and objectstack-ai#16697 answered 404, at every checkpoint (5 at base,
5 at head).

## Why each anchor decides its line

Each sha resolves uniquely, is an ancestor of `origin/main` and of the
base, has one parent, and names the number it replaces in its own
message (15 of 19) or its own diff (17 of 19); every one does at least
one. Each was read for the rule its line states.

- **objectstack-ai#13681 to `8ed9c54b4`**: lands the per-iteration containment rule
PAIR (`flow-loop-body-uncontained`, `flow-try-catch-without-catch`) and
its measured minimal `catch`; its diff wrote all nine lines, and its
changeset records the measurements the lines cite. objectstack-ai#14394 (the rule
card, 200) stays beside it.
- **objectstack-ai#8663 to `192213f66`**: "three write rules ask anchor provenance
before exempting a system column"; its body names objectstack-ai#8663 and its diff
wrote the `[objectstack-ai#8663]` lines in all three rule files.
- **objectstack-ai#13657 to `b003cf2e8`**: the post-hook half of the declared-field
door, one envelope on every driver; its diff wrote the three lines.
- **objectstack-ai#10064 to `def0d3e63`**: name-keys collection-resident publish-gate
finding paths; its body reads "maintainer ruling 2026-08-20: Option A"
for objectstack-ai#10064.
- **objectstack-ai#19370 to `a227afa41`**: `security-role-word` crosses to the runtime
publish gate, whole, per ruling batch objectstack-ai#203 item 3 letter B; it maps
`position` / `app` and writes the past-tense crossing lines.
- **objectstack-ai#9798 to `c7655d472`**: the change that carried objectstack-ai#9798 to done (its
body names it), restoring the sys_comment unscoped multi-delete refusal
that could not fire through the wired engine, the
declared-but-unenforced fail-open the line lists beside objectstack-ai#9261 and
ADR-0110 D3.
- **objectstack-ai#8404 to `b849e6911`**: warns when `searchableFields` declares an
unprovisioned injected anchor, adding the optional provenance index the
lines describe; the SORT twin line names it as the SEARCH wiring.
- **objectstack-ai#10001 to `f1b5ad39a`**: a standalone ViewItem record's nested
`config.sort` / `config.searchableFields` reach the runtime publish
gate, the RECORD rung.
- **objectstack-ai#6290 to `e9b526597`**: `current_user` joins `SCOPE_ROOTS`, the
field-level rejection becomes its own rule, and option-level
`visibleWhen` is walked for the first time. `:770` quotes `SCOPE_ROOTS`'
docblock in `packages/formula`; the quote now stops at "the last one
this list was missing", verbatim, with the commit outside the quotation.
- **objectstack-ai#11284 to `5383fa670`**: the ListView react-tier vocabulary
converges on the metadata-tier spelling, deprecate-first; its changeset
reads "(objectstack-ai#11284, maintainer ruling 2026-08-23)".
- **objectstack-ai#6629 to `cd584d559`**: drops the retired `displayField` /
`searchFields` from the record_picker entry and adds
`component-field-specs-liveness.test.ts`.
- **objectstack-ai#8664 to `8798cd2a6`**: names what actually guards the
`unprovisionedAnchors` wiring; its diff wrote both lines.
- **objectstack-ai#14700 to `de3c52beb`**: descends into `conditional` `then` /
`otherwise` when building the `_validations` universe; its diff wrote
all three lines.
- **objectstack-ai#6124 to `b3c1f3cd5`**: the squash commit of objectstack-ai#6124 itself, leg 1 of
the `_views` key ruling (the CLI i18n extractor keyed by the runtime
view identity).
- **objectstack-ai#10262 to `2aca1bc4c`**: adds the package-internal test seam for
`getNested`'s array fan-out; its diff wrote all three lines.
- **objectstack-ai#11060 to `815585513`**: its body records "Maintainer ruling on
objectstack-ai#11060 (2026-08-23): option A", the CEL-mirrored six with no second
semantics, which the lines quote.
- **objectstack-ai#13653 to `36d287803`**: gates a hook body's `ctx.api` write to a
readonly field, and shares `buildReadonlyIndex` from the flow rule, the
export `:118` describes.
- **objectstack-ai#12950 to `225e7690f`**: created `validate-component-types.ts`, the
author-time rejection for unknown component types in spec-reserved
namespaces (stage 5's anchor for the same number).
- **objectstack-ai#6528 to `3510e4a25`**: the squash commit of objectstack-ai#6528 itself, one
implementation of the filter identity reduction (maintainer ruling
2026-08-06, option 1). The line read `PR objectstack-ai#6528`; it now names the
commit.

Rung: no ADR, `docs/NORTH-STAR.md` or `scripts/adr-anchors/` file
records any of these 19 decisions (the one lint anchor file,
`data-model-rules.ts`, pins ADR-0120, which none of these lines cites),
so the commit rung is the right one, as in objectstack-ai#20234's stages.

## Mechanical proof

- **Token guard** (scratch `tokcmp.mjs`: TypeScript 6.0.3 leaf tokens,
JSDoc kinds excluded, controls mutate the head text in memory only). The
merge base `c96beb2707` against the head, 22 files, 54,508 base tokens
(the 22 files are byte-identical at `7a1faf1a5d` and at the merge base):
  - Real run: 0 files with a token change (exit 0).
  - Comment-insertion control (`runtime-gate.ts`): 0 (exit 0).
- Code-insertion positive control (`validate-hook-body-writes.ts`):
DIFFER at token 216 (exit 1).
- String positive control (a parser-located `StringLiteral` in
`validate-react-page-props.ts`): DIFFER at token 5 (exit 1).
- **Line balance**: every file is +N/−N (81/81 across 22 files), every
changed line is comment-shaped, and every line count is equal at base
and head.
- **Tracker numbers**: added-not-removed is empty in every file, and no
`PR #N` stands on an added line. Net-removed: 80 sites (the census's 79
in these files plus the slash-joined one), 19 numbers. The numbers kept
on added lines all answer 200: objectstack-ai#5659, objectstack-ai#5775, objectstack-ai#8310, objectstack-ai#8340, objectstack-ai#9261, objectstack-ai#9313,
objectstack-ai#12183, objectstack-ai#13390, objectstack-ai#14253, objectstack-ai#14394, objectstack-ai#19143, and objectstack-ai#6584 (a pull request, the
anchor commit's own PR).
- **Shas**: 19 distinct on added lines, 0 on removed lines. `rev-parse
--disambiguate` answers 1 object for each; `merge-base --is-ancestor`
exits 0 against `origin/main` and against the base; each is
single-parent; the repository is not shallow; the control leg
`e9584681a4` exits 0.
- **Literal readers**: every string or regex literal in the repository
that carries one of the 21 numbers (85 literals) was matched against the
23 files' text: no reader of any rewritten line. The lint tests that
read these sources as text stay green below. For example,
`validate-expressions.test.ts` strips comments before it matches, and
`validate-security-posture.runtime-surface.test.ts` collects the
`stack.X` reads inside `validateSecurityRoleWord`, which no added line
carries.

## Tests and gates (at head `0c7b847f18`)

- `pnpm exec turbo run build --concurrency=2 --filter=./packages/*
--filter=./packages/*/*` under `os-verify-lock`: Tasks 71 successful, 71
total, VERDICT command-exit 0.
- `pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2` under
the lock: Test Files 115 passed (115), Tests 5363 passed (5363); then
`pnpm --filter @objectstack/lint typecheck`: exit 0,
`check:test-typecheck` OK (2 files / 6 errors / 2 pinned signatures
held). VERDICT command-exit 0. The same two runs passed with the same
counts on the pre-merge head `2ce32f6b48`.
- Lint, a proven narrowing: `eslint --no-inline-config --format json`
over the 22 touched `.ts` files gives 22 files, 0 errors, 0 warnings.
`isPathIgnored` is false for all 22, read through eslint's API.
`eslint.config.mjs:327-328` says type-aware linting is never enabled, so
a comment edit cannot move an untouched file's verdict. The repo-wide
`pnpm lint` is CI's.
- `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands`: 54 families derived, all run, every one exit 0. `--ran`
reads "54 derived, 54 run, 0 NOT-MEASURED, 0 UNRUN", a derived zero
(every line carries its exit code). Among them:
- `node scripts/check-issue-citations.mjs` (the live diff-scoped run)
judged 18 citations across 22 files: 17 answer as issues and 1 answers
as a pull request, the kept objectstack-ai#6584; `pnpm check:issue-citations`
(self-test, 114 cases in 8 batteries) passes.
- `pnpm check:doc-authoring`: the sibling prose-id baseline holds, 810
pinned sites across 230 files, no growth.
- `pnpm check:nul-bytes`: OK over 9,239 tracked text files; a
control-byte scan of the 23 changed files finds none.
- No generated page carries a lint docblock: no page under
`content/docs/references/` names any of the 19 numbers, and no generator
reads `packages/lint/src`, so nothing was regenerated.
- Changeset: `patch` for `@objectstack/lint`. `files[]` ships `dist`,
and the rewritten comments reach it: 12 of the 19 shas appear in the
built `dist` (for example `8ed9c54b4` and `def0d3e63` in `index.d.ts`,
`b849e6911` in `index.js` and `index.d.ts`); the positive control, the
unchanged sentence "the near-miss shape: a `try_catch` that declares no
`catch`" of an exported docblock, is in `index.d.ts`. Hence patch, not
`skip-changeset`.
- Merge probe: a no-driver `merge-tree` of the head onto `origin/main`
`0f6dcac5e9`, from a bare shared clone with no `merge.*` config, exits
0. The two commits `main` gained after the merge touch none of the 23
files.
- No ablation or reverse verification: the change is comment-only, so
there is no behaviour to invert.

## Hypotheses (measured first)

1. **Holds.** 84 dead sites, 21 numbers, 23 files at the tip
`7a1faf1a5d`, equal per file to the claim.
2. **Holds.** Only comment and docblock lines moved. The one dead number
inside a string (`validate-react-page-props.ts:1198`, a finding
`message`) stays byte-identical; no test or script reads a rewritten
line by literal.
3. **Holds, and conditions the card.** PR objectstack-ai#20593 was still open at
07:34Z, so `authoring-rules.ts` stays at its base blob. At that read,
the 9 open PRs' full file lists and the newest `Claim:` on all 11
`pm:dispatched` cards name none of the other 23 paths.
4. **Holds.** `validate-searchable-fields.ts:312` `pre-objectstack-ai#8404` is listed
dead before and is gone after.
5. **Holds, with nothing to regenerate.** No lint docblock projects into
a generated page; no release page is touched.

## Deviations

- Two changed lines beyond the census's sites.
`validate-page-field-bindings.ts:208` carried `objectstack-ai#5775/objectstack-ai#6629`, a
slash-joined dead number the citation grammar does not read; it now
reads "the same objectstack-ai#5775 residue class (commit cd584d5)", stage 5's
precedent for the slash-joined `objectstack-ai#9972`. `runtime-gate.ts:780` is the
other half of the rewritten `:779` sentence and held no number.
- `origin/main` was merged once (`0c7b847f18`, merging `c96beb2707`):
the first derivation read STALE TREE because
`scripts/sdui-manifest.record.json` changed on `main`. The merge was
clean, no driver-routed path and no lockfile change, and it touches none
of the 23 files; the build, tests and gates above ran after it.
- Commit trailers follow AGENTS.md's model-free pair (`Claude-Session`
plus `Co-authored-by: Claude`); the pre-push trailer check passed on
every push.

## Acceptance notes

**What stays for this card** (why it says `Part of`):
`authoring-rules.ts`, 5 sites, excluded while PR objectstack-ai#20593 holds it.
Anchors, verified the same way, for whoever takes it after that PR
lands: `:198` objectstack-ai#10064 to `def0d3e63`; `:1117` objectstack-ai#16659 to `ecdfc9411` (it
added `flow-schedule-organization-missing` to the registry); `:1687`
"(PR objectstack-ai#8546)" to `ba5e957ef`, that PR's own squash commit; `:1713` and
`:1733` objectstack-ai#19370 to `a227afa41`.

**Form D, not touched:** `validate-react-page-props.ts:1198` is the
`react-prop-deprecated` finding `message`, which ends "...is removed
after the deprecation window (objectstack-ai#11284)." An author sees it, so it takes
ruling D (no number), which is a string change and outside this
comment-only scope. `scripts/doc-authoring-prose-id.baseline.json` pins
it (`objectstack-ai#11284: 1` for this file). It needs a form-D carrier.

**Outside the census's surface**, which blanks strings and defers test
files (noted, not swept here):
- `packages/lint/src/*.test.ts` titles and comments still cite several
of these dead numbers (objectstack-ai#6290, objectstack-ai#8404, objectstack-ai#8663, objectstack-ai#10001, objectstack-ai#10064, objectstack-ai#10262,
objectstack-ai#13681, objectstack-ai#19370 and others).
- Hand-written docs pages cite them too:
`content/docs/automation/hook-bodies.mdx` (objectstack-ai#8663, objectstack-ai#13657),
`content/docs/automation/flows.mdx` (objectstack-ai#11060) and
`content/docs/deployment/validating-metadata.mdx` (objectstack-ai#19370).
- `packages/formula/src/cel-engine.ts` cites objectstack-ai#6290 four times, including
the docblock `validate-expressions.ts:770` quotes. It is in the census,
in another lane's package.

**Wording, each true of its commit.**
- `validate-expressions.ts:571` keeps objectstack-ai#6584 beside `e9b526597`: objectstack-ai#6584 is
that commit's own PR, so "arrived in commit e9b5265, and needed that
same change (objectstack-ai#6584) to be noticed" states the one act both old numbers
named.
- `runtime-gate.ts:362` names the objectstack-ai#9798 shape in words, as the fail-open
that commit c7655d4 ended, next to objectstack-ai#9261 and ADR-0110 D3.
- `lint-flow-patterns.ts:343` reads "The measured case commit 8ed9c54
records, exactly: one row with a null owner killed the sweep"; that
commit wrote the sentence, and `c02f70e13` later fixed the same shape in
the showcase flow.

---
_Generated by [Claude
Code](https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
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.

2 participants