Repository navigation
fix(lint): 字段级 *When 的用户根拒绝覆盖 ADR-0068 的全部三种拼写 (#6585) - #6711
Conversation
#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
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
|
座位验收(
所以「整根」不是口味,是这个面上的事实: 反向验证的形态尤其对:9 条转红,失败是 继承处置合规:读了前一版 diff、逐条独立复核后才建,并交代了五处修正。其中第 5 条(root-vs-MEMBER 歧视钉, changeset 判断也对:已发布包的对外行为变更(此前静默通过的元数据现在报错),与同文件同规则的直接先例 #6584 一致, #6713 已收到并复核,是一条有分量的发现 —— 尤其
工作树 CI 收敛后由我摘草稿并入队。 Generated by Claude Code |
…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>
Fixes #6585
缺陷
PR #6584 给字段级
visibleWhen/readonlyWhen/requiredWhen加了checkFieldRuleUserRoot—— 一条按面给出的拒绝,处方也指向真实存在的面。但它只匹配
current_user一个拼写:而 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形态决定:整根。 依据是实测,不是偏好:
buildScope只在if (ctx.user !== undefined)内部才创建scope.ctx(
stdlib.ts:311-323)。字段级没有任何调用点传用户,所以在这个面上ctx根压根不存在 ——ctx.locale与ctx.user.id同样 fault。readonlyWhen走rule-validator.ts:577(
{ record, previous, extra: { parent } }),requiredWhen走:1403(
{ record, previous, ...parentScope })。都没有 user。对照组:选项级visibleWhen走:1300,签名里有user—— 这就是两个面判定相反的原因。evalFieldPredicate传extra: scope,而 objectui 里全部五个字段级调用点(
form.tsx×3、WizardForm.tsx、GridField.tsx)传的
scope只可能是undefined或{ parent: contextRecord }。FIELD_RULE_ROOTS = ['record','previous','parent'](
ObjectFieldInspector.tsx:127)—— 一个白名单,注释明写 "nocurrent_user"。ctx.user形态)被否:collectCelRootIdentifiers按设计只报ROOT、丢掉成员名,所以窄读法需要源码层面的拼写匹配 —— 而拼写匹配正是
本条规则要消灭的东西。那会把同一个分岔下移一层(
ctx["user"].id静默、ctx.user.id报错),同时为了一个更窄的规则名字放掉一个真实的fail-open fault(
ctx.locale)。ctx在别处仍是 ActionEngine 的谓词根,不受影响:平台自己的ctx.user谓词全在 action
visible上(sys-user.object.ts×8、sys-invitation.object.ts×2、sys-user.page.ts×1),本规则从不读那个面。两个方向都已钉进测试。
实测扫描(派单前置 a)
字段级
*When读取任一用户根的用例:全域为零。两遍独立扫描:examples/*When,含用户根 0ctx.*命中全是 JS hook/script 源码(ctx.api/ctx.ql/ctx.logger),非 CEL;current_user仅 2 处,都在别的面packages/*When(非测试),含用户根 0.describe()文案与json-schema/生成物;真实元数据里的ctx.user全在 actionvisibleobjectui(下游)*When,含用户根 0current_user命中全是选项级visibleWhen;ctx.user命中全是 actionvisible/ 容器 / 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,失败形态是:即完全静默,而不是文案不同 —— 正是本单描述的缺陷形态。恢复后 170/170 全绿。
测试
新增 12 条(
validate-expressions.test.ts),含两条方向相反的钉子:ctx.locale在字段级被拒(整根决定的一面)、ctx.user在 actionvisible上仍被接受(另一面)。另加一条 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 skippedpnpm --filter @objectstack/lint typecheck—— 干净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