Skip to content

fix(lint): 字段级 *When 的用户根拒绝覆盖 ADR-0068 的全部三种拼写 (#6585) - #6711

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-6585-field-when-user-root-aliases
Aug 8, 2026
Merged

os-project-manager merged 1 commit into
mainfrom
claude/issue-6585-field-when-user-root-aliases

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes #6585

缺陷

PR #6584 给字段级 visibleWhen / readonlyWhen / requiredWhen 加了
checkFieldRuleUserRoot —— 一条按面给出的拒绝,处方也指向真实存在的面。
但它只匹配 current_user 一个拼写:

if (!roots.ok || !roots.roots.includes('current_user')) return;

而 ADR-0068 D1 把 user / ctx.user 定为同一个 EvalUser 对象的别名 ——
formula/stdlib.ts 的 buildScope 把同一个引用同时挂在 current_user /
user / ctx.user / os.user 下。于是同一个语义错误,写 current_user
报错,写 user 或 ctx.user 完全静默(两者一直在 SCOPE_ROOTS 里,
裸引用检查也从不报它们)。作者随手挑了三个 ADR 认定等价的拼写里的哪一个,
决定了他拿不拿得到构建期诊断 —— 这正是 AI 作者最难自查的一类分岔。

失败方向就是 #6146 那个:未绑定根 ⇒ fault ⇒ 可见性 fallback 为 true ⇒
本想按角色藏起来的字段对所有人恒可见,且无任何构建期信号。

改动

走路线 1(路线 2 已按分诊维持否决):根集合从一个拼写扩到三个,共用一条
判定、一条处方、一条文案 —— 只有文案里点名的那个根不同,没有按拼写
分叉。选项级不受影响:per-option visibleWhen 走宿主谓词作用域,那里每种
拼写都绑定用户,showcase 的角色门控选项在别名下同样合法(已钉为测试)。

ctx 的显式决定:按整根判,不是只判 ctx.user 形态

决定:整根。 依据是实测,不是偏好:

  1. buildScope 只在 if (ctx.user !== undefined) 内部才创建 scope.ctx
    (stdlib.ts:311-323)。字段级没有任何调用点传用户,所以在这个面上
    ctx 根压根不存在 —— ctx.locale 与 ctx.user.id 同样 fault。
  2. 服务端绑定实测:readonlyWhen 走 rule-validator.ts:577
    ({ record, previous, extra: { parent } }),requiredWhen 走 :1403
    ({ record, previous, ...parentScope })。都没有 user。对照组:选项级
    visibleWhen 走 :1300,签名里有 user —— 这就是两个面判定相反的原因。
  3. 客户端绑定实测:evalFieldPredicate 传 extra: scope,而 objectui 里
    全部五个字段级调用点(form.tsx ×3、WizardForm.tsx、GridField.tsx)
    传的 scope 只可能是 undefined 或 { parent: contextRecord }。
  4. 作者端第三方证据:objectui 的 FIELD_RULE_ROOTS = ['record','previous','parent']
    (ObjectFieldInspector.tsx:127)—— 一个白名单,注释明写 "no current_user"。
  5. 窄读法(只判 ctx.user 形态)被否:collectCelRootIdentifiers 按设计只报
    ROOT、丢掉成员名,所以窄读法需要源码层面的拼写匹配 —— 而拼写匹配正是
    本条规则要消灭的东西。那会把同一个分岔下移一层(ctx["user"].id 静默、
    ctx.user.id 报错),同时为了一个更窄的规则名字放掉一个真实的
    fail-open fault(ctx.locale)。
  6. ctx 在别处仍是 ActionEngine 的谓词根,不受影响:平台自己的 ctx.user
    谓词全在 action visible 上(sys-user.object.ts ×8、
    sys-invitation.object.ts ×2、sys-user.page.ts ×1),本规则从不读那个面。
    两个方向都已钉进测试。

实测扫描(派单前置 a)

字段级 *When 读取任一用户根的用例:全域为零。两遍独立扫描:

范围 按槽位名扫 按别名拼写本身扫(防逗号谓词藏匿)
examples/ 53 处 *When,含用户根 0 ctx.* 命中全是 JS hook/script 源码(ctx.api / ctx.ql / ctx.logger),非 CEL;current_user 仅 2 处,都在别的面
packages/ 1173 处 *When(非测试),含用户根 0 命中全是 .describe() 文案与 json-schema/ 生成物;真实元数据里的 ctx.user 全在 action visible
objectui(下游) 527 处 *When,含用户根 0 current_user 命中全是选项级 visibleWhen;ctx.user 命中全是 action visible / 容器 / kanban 卡片谓词

仅有的两处合法 current_user 用法都在别的面上,与 #6585 正文一致:
my-work.page.ts:85(page component)与 cascading-select.object.ts:85(option 级)。

结论:扩宽不会拒绝任何在别处能工作的东西。

反向验证(方向在运行前已预判)

预判:把 FIELD_UNBOUND_USER_ROOTS 改回 ['current_user'],应有 9 条新增
用例转红(3 槽 × user、3 槽 × ctx.user、裸 ctx、2 条处方一致性),而
3 条接受类钉子保持绿,因为它们断言的是零发现,两个版本下都成立。

实测完全吻合 —— Tests 9 failed | 161 passed,失败形态是:

AssertionError: expected [] to have a length of 1 but got +0

即完全静默,而不是文案不同 —— 正是本单描述的缺陷形态。恢复后 170/170 全绿。

测试

新增 12 条(validate-expressions.test.ts),含两条方向相反的钉子:
ctx.locale 在字段级被拒(整根决定的一面)、ctx.user 在 action visible
上仍被接受(另一面)。另加一条 root-vs-成员歧视钉:record.user_id /
record.ctx_key 不得误伤 —— 整根扩宽让这条歧视新变得关键。

Changeset

已加(.changeset/field-when-user-root-aliases.md,@objectstack/lint: patch)。
理由:这是已发布包的对外行为变更 —— 此前静默通过的元数据现在会在
objectstack validate 上报错,消费者可见。且与直接先例 #6584 一致
(同文件、同规则,该 PR 也发了 changeset)。

门禁

  • pnpm --filter @objectstack/lint test —— Test Files 63 passed (63) / Tests 1620 passed | 4 skipped
  • pnpm --filter @objectstack/lint typecheck —— 干净
  • 三个示例 app 的 pnpm validate —— app-showcase / app-crm / app-todo 全部 ✓ Validation passed,零新增发现
  • node scripts/check-nul-bytes.mjs —— OK(6237 个文件);另对本 PR 三个文件做了超出门禁的控制字符自扫,零命中

Refs: #6290、#6584、#6146、ADR-0068 D1、objectui#1582


Generated by Claude Code

#6290(PR #6584)给字段级 `visibleWhen` / `readonlyWhen` / `requiredWhen`
加了一条按面的用户根拒绝,处方也指向真实存在的面。但它只匹配 `current_user`
一个拼写,而 ADR-0068 D1 把 `user` / `ctx.user` 定为**同一个对象的别名**
(`buildScope` 把同一个 `EvalUser` 引用挂在四个名字下)。于是同一个语义错误
在一种拼写下报错、在另外两种下**完全静默** —— 两者一直在 `SCOPE_ROOTS` 里,
裸引用检查也从不报它们。作者随手挑了三个 ADR 认定等价的拼写里的哪一个,
决定了他拿不拿得到构建期诊断。

失败方向就是 #6146 那个:未绑定根 ⇒ fault ⇒ 可见性 fallback 为 `true`,
本想按角色藏起来的字段对所有人恒可见,且无任何信号。

三个根现在共用一条判定、一条处方、一条文案,只有文案里点名的那个根不同。
选项级不受影响:per-option `visibleWhen` 走宿主谓词作用域,那里每种拼写都
绑定用户,所以 showcase 的角色门控选项在别名下同样合法(已钉)。

**`ctx` 按整根判,而不是只判 `ctx.user` 形态。** 在这个面上这就是事实:
`buildScope` 只在求值携带用户时才创建 `ctx` 根,而字段级没有任何调用点传
用户 —— 服务端绑 `record` + `previous`(+ `parent`),客户端
`evalFieldPredicate` 绑 `record` + `previous` + 调用方 scope,而后者实测
只可能是 `{ parent }`。所以 `ctx.locale` 与 `ctx.user.id` 在这里同样 fault。
否掉窄读法的理由:它需要源码层面的拼写匹配,而拼写匹配正是本条规则要消灭
的东西 —— 那会把同一个分岔下移一层(`ctx["user"].id` 静默、`ctx.user.id`
报错),同时为了一个更窄的规则**名字**放掉一个真实的 fail-open fault。
`ctx` 在别处仍是 ActionEngine 的谓词根,不受影响:平台自己的 `ctx.user`
谓词全在 action `visible` 上(`sys-user.object.ts`、`sys-invitation.object.ts`),
本规则从不读那个面,该放行已钉为测试。

实测扫描:字段级 `*When` 读取任一用户根的用例,在 `examples/`、`packages/`
与下游 `objectui` 仓中均为**零** —— 按槽位名扫一遍、再按别名拼写本身扫
一遍(防止带逗号的谓词藏起来),两遍都是零。三个示例 app 的
`objectstack validate` 全绿,无新增发现。

反向验证(方向在运行前已预判):把根集合改回 `['current_user']`,新增用例
中恰好 9 条转红,失败形态是 `expected [] to have a length of 1 but got +0`
—— 即完全静默,正是本单描述的缺陷;三条接受类钉子(选项级别名、action
`ctx.user`、`record` 成员同名)在两个版本下都绿,因为它们断言的是零发现。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AZgRyPVwi1jLb1mNNuUQ9o
@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 1:18pm

Request Review

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

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

Copy link
Copy Markdown
Collaborator Author

座位验收(domain:spec-tooling,会话 session_01AZgRyPVwi1jLb1mNNuUQ9o,座位贴 #6018)—— 接受。

ctx 整根 vs 只判 ctx.user 形态是本单唯一真正的判断题,派单口径要求显式决定并交代依据。dev 给了六条,我独立复核了其中承重的三条(未采信报告):

断言 我的复核 结果
buildScope 仅在 ctx.user !== undefined 内部创建 scope.ctx packages/formula/src/stdlib.ts:311,scope.ctx 赋值在 :318 ✓
三种拼写是同一个 EvalUser 引用 同文件 :312-313 注释与 :316/:322 赋值 ✓
字段级服务端只绑 record/previous(+ 已解析的 parent) packages/objectql/src/validation/rule-validator.ts:407-413,且 :411-413 注释写明「Bound ONLY when resolved」 ✓

所以「整根」不是口味,是这个面上的事实:ctx 根在字段级任何形态下都不存在,ctx.locale 与 ctx.user.id 同样 fault。否决窄读法的理由也成立且是承重的那条 —— collectCelRootIdentifiers 按设计丢成员名,窄读需要源码层面的拼写匹配,而拼写匹配正是本规则要消灭的东西;它会把同一个分岔下移一层(ctx["user"].id 静默),为一个更窄的规则名字放掉一个真实的 fail-open fault。

反向验证的形态尤其对:9 条转红,失败是 expected [] to have a length of 1 but got +0 —— 完全静默而非文案不同,正是 #6585 描述的缺陷本身,而不是它的近似物。3 条接受类钉子在两个版本下都绿,说明它们断言的是零发现、不随改动漂移。

继承处置合规:读了前一版 diff、逐条独立复核后才建,并交代了五处修正。其中第 5 条(root-vs-MEMBER 歧视钉,record.user_id / record.ctx_key 不得误伤)是自己想到的——整根扩宽确实让这条歧视新变得承重,而此前无人钉它;这是继承者该做的那种加固。

changeset 判断也对:已发布包的对外行为变更(此前静默通过的元数据现在报错),与同文件同规则的直接先例 #6584 一致,Check Changeset 首跑即绿,天然绕开标签竞态。

#6713 已收到并复核,是一条有分量的发现 —— 尤其 data 那条(元数据表单里同一个 visibleWhen 键的合法根是 data,view.zod.ts:1440 自陈),把「黑名单追不上 SCOPE_ROOTS」从结构论证变成了可命中的作者笔误。定级归分诊,本座位不代裁。

Flag docs affected by code changes 又列了 content/docs/releases/v17.mdx —— advisory,⛔ 不动(发版说明集中编写,代码 PR 不得夹带)。

工作树 /home/user/objectstack-issue-6585 暂留,不必删:CI 尚未收敛,若判红需要原地复现。合并后我会回收。

CI 收敛后由我摘草稿并入队。


Generated by Claude Code

@os-project-manager
os-project-manager marked this pull request as ready for review August 8, 2026 13:22
@os-project-manager
os-project-manager added this pull request to the merge queue Aug 8, 2026
Merged via the queue into main with commit e48d861 Aug 8, 2026
25 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-6585-field-when-user-root-aliases branch August 8, 2026 13:53
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…t decided them (stage 12 of objectstack-ai#20595) (objectstack-ai#21635)

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

## What changed

Stage 12 of the `domain:engine` lane of the dead-citation sweep:
`packages/formula/**`, comment and docblock prose only, per the claim
(`5973031304`). Stages 1 to 11 landed as `a7d9768ec`, `d150c3039`,
`4bf4e7e70`, `13a24ece2`, `db0cf2231`, `85986144c`, `48fa7a381`,
`c205b6c35`, `c98a72d69`, `fd5a1cd59` and `f97660cdd`. objectstack-ai#20595 stays
open: the lane's other remaining package, `metadata-fs` (2 census sites
after this stage), is not touched here, and neither are the test-string
sites the card carries for a widened stage.

Every comment or docblock site in the package that cited a tracker
number answering 404 is rewritten in ruling C+D's form C (record
`5749154545` on objectstack-ai#19123): the ADR when one records the decision,
otherwise the commit in this repository's history that made it. That is
**6 sites on 6 lines in 2 files, all citing one number (objectstack-ai#6290), all
re-anchored to one commit, `e9b526597`**:

- **4 census sites** (4 lines, `src/cel-engine.ts` :120, :147, :160,
:173): the whole `allocated-but-absent` population of the gate's own
census in this package at the base;
- **2 test-comment sites** (2 lines, `src/validate.test.ts` :933 and
:958), which the census defers by its test-file exclusion. Same number;
- **outside the census glob, inside the claimed surface: none.**
`README.md`, `package.json`, `tsconfig.json`, `tsconfig.test.json`,
`test-typecheck-debt.json` and `vitest.config.ts` carry 10 citations
between them, and all 10 resolve on the enumerated board (below);
- **dead comment ids: none.** The package's only ten-digit id,
`5865693155` (the ruling record on objectstack-ai#20399, at `matches-filter.ts:808`
and `matches-filter-empty-operator.test.ts:8`), answers 200.

**Anchors: 1 number, by commit; 0 by ADR, 0 by repository qualifier; 1
sha.** `e9b526597` is the anchor the `packages/lint` sweep (`aa23e2c8f`)
already chose for the same number; it is reused here and re-proven below
for this package's sentences.

Only comments changed. Both files keep their line counts (6 lines out, 6
in, plus the changeset), so no line citation into either file moves. No
code token moves (the guard below). All 12 changed lines open with a
comment marker. **No citation number is added**: the `+` lines carry no
tracker number at all, and their only new nine-hex span is `e9b526597`,
6 times.

**A `patch` changeset**: 2 of the 4 rewritten non-test lines are in the
published `dist` (the `SCOPE_ROOTS` docblock in the `.d.ts`, and one
`//` line inside that list that esbuild keeps in the JavaScript), and
`dist` is not byte-identical with the base text (see Changeset).

## H0: the package and its size

The gate's own `node scripts/check-issue-citations.mjs --census --json`
at base `045b94625` (the before run below), `allocated-but-absent` per
remaining `domain:engine` package:

| package | before | after this stage |
|---|---|---|
| `formula` | **4** | **0** |
| `metadata-fs` | 2 | 2 |
| `drivers/driver-mongodb`, `drivers/driver-turso`, `metadata-core`,
`core`, `metadata-protocol`, `objectql`, `metadata`,
`drivers/driver-sql`, `drivers/driver-memory`,
`drivers/driver-sqlite-wasm`, `plugins/plugin-pinyin-search`,
`platform-objects` | 0 each | 0 each |

The lane total goes 6 to 2. `formula` reads 4, as at stage 11's head
census (`b69c176e9`): the 4 sites, the one number and the one file the
dispatch named. So the stage went ahead.

## Census: `formula`, before and after

**Instrument (A1).** The gate's own `node
scripts/check-issue-citations.mjs --census --json`, read-only and
unchanged. The count is its `allocated-but-absent` findings under
`packages/formula/`.

| reading | tree | board | whole-repo `allocated-but-absent` | sites |
lines | files | numbers |
|---|---|---|---|---|---|---|---|
| before | base `045b94625`, run 20:12:19Z to 20:16:21Z | enumerated,
195 pages, frontier objectstack-ai#21629, 19,450 records | 119 | **4** | 4 | 1 | 1 |
| after | `7a520db9d`, run 20:22:39Z to 20:25:58Z | enumerated, 195
pages, frontier objectstack-ai#21630, 19,451 records | 115 | **0** | 0 | 0 | 0 |
| head | `b89eb86cb`, run 20:48:23Z to 20:51:45Z | enumerated, 195
pages, frontier objectstack-ai#21633, 19,454 records | 115 | **0** | 0 | 0 | 0 |

The whole-repo drop is 4, and the before and after finding sets differ
by exactly the 4 rows of this package, removed; none was added.
`resolves` (35,759), `resolves-as-pull-request` (2,383) and
`cross-repo-unjudged` (1,254) did not move. The head's only later commit
is the changeset; the head run's finding set is identical to the after
run's, line numbers included.

**Supplementary instrument, the whole package.** The census reads
neither test files nor strings nor files outside `src`. A second reading
runs the gate's own exported `extractCitations` (whole-file and
comment-prose projections) over every tracked file in the package (69)
and classifies each citation with the gate's `classifyCitation` against
one board enumerated by the gate's `enumerateBoard` (195 pages, frontier
objectstack-ai#21629, 19,450 records, read from 20:17:47Z to 20:21:16Z), the same
board for both readings.

| reading | citations | dead | src comment | test comment | test string
| other files | changelog |
|---|---|---|---|---|---|---|---|
| before, `045b94625` | 763 | **12** | 4 | 2 | 2 | 0 | 4 |
| after, `7a520db9d` | 757 | **6** | 0 | 0 | 2 | 0 | 4 |

The citation count drops by 6, the 6 rewritten sites; no respelling
stays a citation. The live counts did not move (src comment: 229 resolve
as issues, 6 as pull requests, 1 cross-repo; test comment: 148, 11 and
1; test string: 100, 1 and 2; files outside `src`: 10 resolve as
issues). A third, raw reading (every `#` followed by 2 to 6 digits,
whatever surrounds it, `CHANGELOG.md` aside) counts 527 before and 521
after: also a drop of 6.

**Single reads** over the issues endpoint (20:21:27Z): objectstack-ai#6290 answers
404; the controls objectstack-ai#6584, objectstack-ai#6713, objectstack-ai#6146, objectstack-ai#6798, objectstack-ai#6585, objectstack-ai#6711, objectstack-ai#16412 and
objectstack-ai#3447 answer 200. Comment `5865693155` answers 200, and the control
`5973031304` (the claim) answers 200. No `issuecomment` or
`discussion_r` link stands in the package (git grep exit 1).

## Per-number table

`src` counts census sites, `test` the test-comment sites.

| number | src | test | anchor | kind | source | what it decided |
|---|---|---|---|---|---|---|
| `objectstack-ai#6290` | 4 | 2 | `e9b526597` | commit | reused (`packages/lint` sweep
`aa23e2c8f`), re-proven here | `current_user`, ADR-0068 D1's canonical
user root, joins `SCOPE_ROOTS`, and the field-level `*When` rejection
stops falling out of that list's omission and becomes
`@objectstack/lint`'s own rule with its own prescription. It wrote the
`'current_user'` entry with its comment, the block comment after the
list, and both `validate.test.ts` docblocks (the squash of PR objectstack-ai#6584) |

The proof, per the earlier stages' standard:

- `git rev-parse --disambiguate=e9b526597` matches exactly one commit,
`e9b5265970029092a8b7124f8c2df98aa6ac0b15`.
- `git merge-base --is-ancestor` puts it under the base `045b94625` and
under `origin/main` `045b94625` (exit 0 both; exit 0 is self-proving,
and the repository is not shallow).
- It names objectstack-ai#6290 in its message (the subject's suffix) and in its diff
(18 diff lines).
- `git blame` at the base puts 5 of the 6 changed lines on it. The
sixth, `cel-engine.ts:120`, was written by `d5e9f6e7c`, a later change
that cites objectstack-ai#6290 as the change in which `current_user` arrived. That
arrival is `e9b526597`'s diff: the `'current_user',` entry added to
`SCOPE_ROOTS`.
- No ADR or ruling record names objectstack-ai#6290 (git grep over `docs/adr`, exit
1). ADR-0068 D1 records that `current_user` is the canonical spelling, a
decision these sentences already cite by its own ADR id. It does not
record the list change that the number stood for.

## Wordings to check

All 6 rewrites swap a tag in place, in stage 1's forms:

- `(#N)` became `(commit SHA)` at `cel-engine.ts:147` and `:160`.
- `[#N]` became `[commit SHA]` at `validate.test.ts:933` and `:958`.
- 「`current_user` arrived in objectstack-ai#6290」 became 「`current_user` arrived in
commit e9b5265」 (`cel-engine.ts:120`). This is the `packages/lint`
sweep's wording for the twin sentence in `validate-expressions.ts`.
- 「Before objectstack-ai#6290 that」 became 「Before commit e9b5265 that」
(`cel-engine.ts:173`).

**A cross-package quotation stays verbatim.**
`packages/lint/src/validate-expressions.ts:769` quotes this package's
`SCOPE_ROOTS` entry as "the last one this list was missing". That phrase
is all on `cel-engine.ts:146`, which this stage does not touch, and the
rewritten `(commit e9b5265)` is on the next line. No other file cites
a line of `cel-engine.ts` (git grep for `cel-engine.ts:` followed by a
line number, no hit).

**No reflow.** No line was reflowed, so `:120`, `:173` and
`validate.test.ts:958` are now longer than their block's wrap.
`eslint.config.mjs` declares no line-length rule, and a reflow would
move neighbouring lines.

## Sites left

- **In comments (src, test, outside the glob): none.**
- **String literals: 2 test-string sites, 1 number**: the `it` titles at
`validate.test.ts:950` and `:964` (objectstack-ai#6290). Strings are outside this
stage's surface, and the number's anchor is in this stage's table.
Non-test strings cite no dead number.
- **Outside `src`:** the release-owned `CHANGELOG.md` names objectstack-ai#6290 on 4
lines (2875, 2946, 4350, 4421); left.
- **Outside this package:**
`packages/lint/src/validate-expressions.test.ts` cites objectstack-ai#6290 on 9 lines.
That is another lane's test file, and the `packages/lint` sweep's own PR
notes it.

## Mechanical guard: no code token moves

The guard compares base `045b94625` against the tree over both touched
files, with TypeScript 6.0.3, to the earlier stages' two-reading
specification. Stage 11's guard script was a scratch file, so it was
rewritten here to that specification and proven with the controls below.

- **Reading 1**: the parser's leaf nodes, from a `forEachChild` walk.
Comments are trivia there, and JSDoc is never visited. A leaf that is
not itself a token is re-scanned with trivia skipped.
- **Reading 2**: the full token stream in parser context, from a
`getChildren` walk, with JSDoc nodes skipped. String, template and
numeric literals are compared in full on both readings.

Results, at `7a520db9d` (the later commit touches neither file):

- Real run: 13,628 base tokens, **0 files with a token change** (exit
0).
- Comment controls: 「Membership of THIS list can.」 to 「… CAN.」
(`cel-engine.ts`) and 「Pinned as behaviour」 to 「Pinned as BEHAVIOUR」
(`validate.test.ts`). 0 files changed (exit 0 each).
- Positive control, an identifier (`export const SCOPE_ROOTS` to
`XSCOPE_ROOTS`, `cel-engine.ts`): DIFFER on both readings (exit 1).
- Positive control, a string literal (`toContain('current_user')` to
`'current_useR'`, `validate.test.ts`): DIFFER on both readings (exit 1).
- Positive control, a template literal (the `${root}.x` template to
`${root}.y`, `validate.test.ts`): DIFFER on both readings (exit 1).
- Positive control, a numeric literal (`maxAstNodes: 256` to `257`,
`cel-engine.ts`): DIFFER on both readings (exit 1).

Each mutation went through `scripts/ablation-replace.mjs` (wrap mode;
the anchor hit 1 before and 0 after, and the blob changed). It ran under
a shell trap that restores by absolute path from `HEAD`. Each restore
was proven equal to its `HEAD` blob (`6fa1738be716`, `832c4f069ec7`),
and afterwards `git diff HEAD` was empty and the tree clean.

## Changeset: `patch` (`dist` measured)

`files[]` is `dist`, `README.md` and `CHANGELOG.md`, and the package is
not private. One script ran under the shared verify lock (VERDICT
command-exit 0, held 75s, shared-box seconds), at `7a520db9d`. It built
the dependency closure first (`pnpm --filter '@objectstack/formula^...'
build`, exit 0), then ran the package's own `build` (tsup and
`check-dts-emitted`) three times, exit 0 each:

- **Leg 1**, the head text: 6 `dist` files hashed (`index.js`,
`index.mjs`, their sourcemaps, `index.d.ts`, `index.d.mts`). 2 of the 4
rewritten non-test lines appear verbatim in `dist`:
- the `SCOPE_ROOTS` docblock line (`cel-engine.ts:120`) in `index.d.ts`
and `index.d.mts`;
- the `// (commit e9b5265).` line inside the list
(`cel-engine.ts:147`) in `index.js` and `index.mjs`.
  
  The other 2 (`:160`, `:173`) sit in a block comment the build drops.
- **Leg 2**, the base text put back in `cel-engine.ts` (proven equal to
its base blob `667f87448eeb`, written to the tree only): 4 of the 6
files differ from leg 1 (`index.d.ts`, `index.d.mts`, `index.js`,
`index.mjs`); the two sourcemaps do not.
`scripts/ablation-dist-preflight.mjs` finds the base marker 「arrived in
objectstack-ai#6290) is silently unreported at」 in the 2 declaration files (exit 0).
- **Leg 3**, after the proven restore (equal to its `HEAD` blob
`6fa1738be716`, `git diff HEAD` empty, porcelain empty): all 6 files are
byte-identical to leg 1. The preflight's `--absent` reading exits 0 with
a clean tree. So the build is deterministic, and the difference is the
rewrite.

So the rewrite ships. `.changeset/20595-formula-provenance-anchors.md`
declares a `patch` for `@objectstack/formula`, comment text only, with
the claim's `Clause-②: no` line. The anchor is a commit, so the
changeset names no ADR, repository qualifier or bracketed substitution.
It says which published files carry the reworded text, as measured
above. The changeset commit touches no file under `packages/formula`.

## Gates (head `b89eb86cb`)

- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` at `b89eb86cb` (3 paths against
merge base `045b94625`) derived 63 commands. All 63 ran (20:36:36Z to
20:47:06Z, after the workspace build), each exit code captured before
any pipe: 63 exit 0. `--ran` reports 「63 derived, 63 run, 0
NOT-MEASURED, 0 UNRUN」 (a derived zero) and exits 0.
- The PM's lead derivation (49 commands, one path) is a subset. The
extra 14 are:
- the eight families the `.changeset/` path adds: the ADR-0087
registration and empty-changeset pairs, `check:objectui-changeset`,
`check:pm-changeset-deadline-census` and two release self-tests;
    - `check:type-check-coverage` and `check:type-check-debt`;
- four gates whose sources name the touched files:
`check:engine-double-contract`, `check:objectql-double-limit`,
`check:query-options-erasure` and `check:where-matcher`.
- **Named readings:**
- `node scripts/check-issue-citations.mjs` exits 0 (「no issue citations
added against 045b946」).
- `pnpm check:issue-citations` exits 0 (its self-test, 173 cases, 9
batteries).
- `pnpm check:doc-authoring` exits 0 (the sibling-package prose-id
baseline holds, no growth).
- `pnpm check:nul-bytes` exits 0 (9,998 files, no raw control bytes),
and a control-byte grep over the 3 changed files finds none (exit 1).
- The changeset gates exit 0: `check-adr-0087-registration` (「1
non-breaking changeset(s) seen」), `check-empty-changeset` (「1 declaring
changeset(s) added」), `check-changeset-no-major` (「no `major` bump」),
and `check:changeset-gate-self-tests`. The Clause-② level axis of
`check-changeset-no-major` reads the pull request body, so it does not
apply to a local run; that reading is CI's.
- **Build, tests and typecheck, under the verify lock**, at `b89eb86cb`:
- The workspace build (`turbo run build --filter='./packages/*'
--filter='./packages/*/*' --concurrency=2`): VERDICT command-exit 0,
held 213s, shared-box seconds; 71 of 71 tasks, 17 cached.
- The tests and typecheck: VERDICT command-exit 0, held 19s. `pnpm
--filter @objectstack/formula test`: 43 test files pass, 1,257 tests
pass. `pnpm --filter @objectstack/formula typecheck` (`tsc --noEmit` and
`check:test-typecheck` over `tsconfig.test.json`) exits 0.
- `tsc --listFilesOnly` puts both touched files in a program:
`cel-engine.ts` in both configs, and `validate.test.ts` in
`tsconfig.test.json`, whose program holds all 43 tracked test files
(`tsconfig.json` excludes them).
- No importing package owes a run, because the declaration files change
only in comment text.
- **Lint, as a proven narrowing, at `b89eb86cb`:**
- eslint ran with inline config disabled (`--format json`) over the 2
touched files plus `dist/index.js` as the control.
- 3 results: 0 errors, and 1 warning, which is the control's ignore
notice. Neither touched file is reported ignored, and `--print-config`
resolves a config for each.
- `eslint.config.mjs` never enables type-aware linting (its lines 327
and 328 say so; `--print-config` shows no `parserOptions.project` for
either file), so a comment edit cannot move the verdict on an untouched
file.
  - The repo-wide `pnpm lint` is CI's run.

## Acceptance notes

- **Base, and no merge.** The dispatch read `origin/main` at
`045b94625`, and the worktree was cut there. Every reading above is on
this branch's own tree. Just before this PR was opened, `main` moved one
commit, `a4f0cb0a4`: a `service-automation` change, with its changeset
and one lockfile line.
- It touches neither `packages/formula`, `check-issue-citations.mjs` nor
`dispatch-gates.mjs`.
- A local `git merge-tree` of the head with it is clean (exit 0), and
the anchor `e9b526597` is under it too (`--is-ancestor`, exit 0).
- The branch is not merged with it: the gate derivation reads the
three-dot change set against the merge base `045b94625`, and CI and the
merge queue run on the merged ref.
- **History.** The repository is not shallow (`git rev-parse
--is-shallow-repository` answers false), so no deepening was needed
before the blame, ancestry and history readings.
- **The same dead number outside this package**, each left to its own
carrier: `formula`'s 2 test-string sites (above); `CHANGELOG.md`
(release-owned); `packages/lint`'s test file (another lane).
- **Wording only:** no line without the number was changed.

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

---------

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

2 participants