fix(formula,lint): current_user 收进 SCOPE_ROOTS,字段级拒绝改为按面的真规则 (#6290) - #6584
Merged
Merged
Conversation
`@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
…rent-user-scope-roots
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 9 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
baozhoutao
marked this pull request as ready for review
August 8, 2026 06:07
This was referenced Aug 8, 2026
This was referenced Aug 8, 2026
Closed
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #6290
packages/formula对同一个根说了两套话:introspectScope把current_user当合法命名空间交给作者、checkRoleCatalog四条正则全以它打头(两处都对 —— ADR-0068 D1 把它定为规范拼写,buildScope也确实挂了它),只有cel-engine.ts的SCOPE_ROOTS不认。于是严格环境把被祝福的拼写读成裸字段引用,而它的两个别名(user、ctx)一路放行;拒绝时给出的还是通用裸字段处方 ——「Writerecord.current_user」,一个在平台任何一层都不绑定的形状。按 PM 裁决落地:收下进
SCOPE_ROOTS,字段级判定改为按面的真规则,并补齐 option 级遍历。前提复核表(动手前逐条实测)
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_uservisibleWhen的current_user定为合法面visiblegate 求值结果相同」,规范变量名current_user;spec 侧写得更死 ——field.zod.ts:129-143的SelectOptionSchema.visibleWhenJSDoc:「options resolve throughresolveCascadingOptionsagainst the predicate scope (ADR-0068 / objectui#2284)… Per-option is the one*Whensurface where acurrent_usertest actually resolves」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,交集为空。推前已 mergeorigin/main(至a36db28b7),复核 #6146 的口径原样保留P3 —— 裁决前提的实测细节
耦合方向确认(裁决前提所担心的那半边,确实存在):字段级
visibleWhen对current_user的拒绝,今天完全来自validateExpression(…, {scope:'record'})→firstUndeclaredReference→buildScopedEnv注册SCOPE_ROOTS。收下current_user后该路径不再报:但两者可调和 —— 裁决成立,不作废。 判据是同包另一个 helper 的结构性独立:
collectCelRootIdentifiers走buildEnv(unlistedVariablesAreDyn: true)读 AST,完全不读SCOPE_ROOTS,收下前后同答案:所以「按面区分」不是勉强绕开,而是本仓已有的既定机制:#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(三者共用一个求值器)。新处方原文:三条处方逐条落到实际存在的面上,而非文案改写:option 级
visibleWhen=SelectOptionSchema.visibleWhen(field.zod.ts:143);字段级安全 =PermissionSetSchema.fields(permission.zod.ts:455,FieldPermissionSchema.readable);record改写 = #6146 收窄后的字段级口径。测试用一条否定断言 + 三条肯定断言钉住(「文案变了」不是要保的性质,「文案指向真的绑定的形状」才是)。3. option 级遍历补齐(同文件)
f.options[].visibleWhen首次被走查,按 optionvalue定位。与宿主字段同为recordscope —— 收下current_user之后,两个面的全部差别就落在checkFieldRuleUserRoot这一条上,而这正是它们该有的差别(求值器不同:evalFieldPredicatevsresolveCascadingOptions)。读的是字面
f.options而非(f as AnyRec).options:后者会让 #5017 的 meta-guard 源码扫描看不见这个新面(该文件自己就写着这条纪律)。相应更新了 meta 表:f的期望读集加入options,新增opt→SelectOptionSchema行。反向验证表(方向先写死,再实测)
预测在动手前落笔,与实测逐条比对。
回退 A —— 从
SCOPE_ROOTS删掉'current_user'(其余保留)every root introspectScope advertises really resolvesrole-catalog verdict reachable through every user spellingstill REJECTS a field-level visibleWhenexpected [ { …(4) }, { …(4) } ] to have a length of 1 but got 2prescribes 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'`ACCEPTS the showcase role-gated OPTIONexpected [ { …(4) } ] to have a length of +0 but got 1未列入预测表、同向连带转红的一条:
rejects it on readonlyWhen / requiredWhen too—— 同一「翻倍」成因(4 条而非 2 条)。如实记录,非预测命中。回退 B —— 删掉 option 级遍历循环(保留
SCOPE_ROOTS改动)expected [] to have a length of 1)ACCEPTS the showcase role-gated OPTIONtoHaveLength(0)通过是因为什么都没走,不是因为判定对(#5046 的空绿陷阱)未列入预测表、额外转红的两条:#5017 meta-guard 的
every key read off 'opt'与the field receiver reads only declared keys—— 源码扫描独立于行为发现了「读没了」。属于额外收益,如实记录。这就是 ② 必须与 ③ 并存的理由:② 单独看,在回退 B 下是空绿的;真正证明遍历跑起来的是 ③ 产出的 finding。
命令输出
真实 metadata sweep(半边 3 是激活一条检查,同 #5026,必须扫真栈):
零新增 finding —— 包括同时携带 record 级联选项与角色门控
current_user选项的showcase_cascading_select。消费半径外扩(
SCOPE_ROOTS加宽会改动所有recordscope 面的判定,故不止本两包):说明
user/ctx.user别名在字段级仍静默放行,本 PR 不动 —— 那是收下current_user之前就存在的洞(两者一直在SCOPE_ROOTS里),不是本次引入的回归,且ctx还是 ActionEngine 的根,爆炸半径另算。已按纪律另立 finding,不在本 PR 修。新文案指的是面而非拼写,所以不会把作者推去写user.positions。check:engine-double-contract一类 objectql 面门禁不适用。@objectstack/lintTEST_DEBT 记 42、实测 19,该盈余为改动前既有,不属本单)。Generated by Claude Code