Skip to content

fix(app-shell): Studio CEL editors agree with the server verdict (builder null guard, stale RLS verdict) - #27677

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-qx6k5b
Oct 29, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-qx6k5b

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Bill · project thread

维护者速读

  • 改了什么:Studio 的无代码条件构建器对对象字段做「大于/小于/至少/至多」比较时,现在写出 record.x != null && record.x > 40(与「数字不能为负」预设同形),服务端发布时接受;RLS 子句字段只在文本仍是服务端评判过的那段时才显示服务端报错,改了文本就不再挂着上一版草稿的旧错误。
  • 为什么改:之前构建器写 record.estimated_hours > 40,可空字段在 CEL 里没有 > 的 null 重载,构建器说「Valid CEL」,发布时却被拒并拖住整个包;RLS 字段在被客户端语法检查挡住保存时不会重读草稿,旧的服务端报错一直显示在新文本下面。
  • 风险与回滚:只改 app-shell 两个源文件(ConditionBuilder.tsx 的比较编译路径、CelPredicateField.tsx 的显示条件);已存的条件打开时一个字节都不改写,未加守卫的旧比较保持原样直到作者编辑那一行。回滚即 revert 本 PR。
  • 先看:ConditionBuilder.tsx 的 compileRow 守卫分支与 update 里的 withGuard;parseGuardedCompare;CelPredicateField.tsx 里 serverErrors 改读 held。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/app-shell/src/views/metadata-admin/inspectors/ConditionBuilder.nullGuard-27513.test.tsx packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.serverVerdictHold-27482.test.tsx,预期 13 条全绿。
  • 需要你做的:无,席位落地。

Fixes #27513
Fixes #27452

Clause-②: no
Metadata-protocol: none

What changes

  • ConditionBuilder.tsx (the comparison compile path only): a row whose operator is an ordering comparison (> < >= <=) and whose subject names a catalog field compiles to S != null && S op V, a compound row, so it is parenthesised beside other rows. That is the shape the "Number can't be negative" preset already writes and the fix the server's own refusal prescribes ("Guard it with '!= null'"). parseGuardedCompare reads exactly that text back as the one row it compiles from, as a whole expression and as a parenthesised term. The guard is decided by the row, not by the field's nullability: the builder restates no publish rule, and != null is well typed on a field that can never be null too. Equality rows, value-less rows and rows on a context subject (record.id, user.*) compile exactly as before.
  • A stored condition is never rewritten on open. A comparison saved without the guard parses as a row with no guard and re-emits its own bytes; the guard is applied to a row when the author builds or edits that row (update → withGuard on the rows the edit replaced), following the builder's existing rule for the author's own quote spelling.
  • CelPredicateField.tsx: the server's errors at verdictPath are now shown on the same terms on which they already held the save since fix(app-shell): a server error at an RLS clause's path holds the permission-set autosave #27612 (useServerHoldAt): only while the text on screen is the value the judged body holds at that path. "Valid CEL", aria-invalid and the destructive border follow the same set. useServerErrorsAt is no longer read here (the flow edge inspector still reads it).

Why

#27513: the release walkthrough built "Reject the save when… record.estimated_hours greater than 40" on an optional Number field. The builder said "Valid CEL" and the rule saved, and publish refused it (@objectstack/lint's #4763 null-guard gate: an ordering operator on a nullable declared field with no dominating != null), holding every other draft in the package. Triage asked the builder to emit the preset's null-safe guard.

#27452: an RLS clause showed the previous draft's server error under new text. The verdict comes from the last draft read; an edit the lint holds never saves, so nothing re-reads, and the field read the verdict by path alone. Triage asked the field to clear the server verdict on edit and show the new one on save. #27612 had already keyed the save hold on the judged text, not the display; this keys the display the same way, so one verdict decides both.

Tests

New ConditionBuilder.nullGuard-27513.test.tsx (7 cases) on the Validations tab's mount (scope="record", the server host's subjects): the card's flow (Add condition → field → greater than → 40) emits the guarded text; that text is judged by the server's gate (findUnguardedNullableOperands from @objectstack/lint, test-only) as clean and evaluated by @objectstack/formula on a null row without a fault, against an instrument control in which the old emission is refused by the same gate and faults in the same engine; the guarded text reopens as one row; the OR join parenthesises and round-trips; a stored unguarded comparison keeps its bytes until that row is edited; equality needs no guard; a non-catalog subject is not guarded.

Two new cases in PermissionMatrixEditor.serverVerdictHold-27482.test.tsx for #27452, through the real permission-set editor and a fake door: a stored refused USING shows the server message; editing it to a lint-held text removes the message while the hold and the client error stay, with no PUT and no draft re-read; typing the judged text back shows it again. serverVerdictAtPath-26858.test.tsx now provides the judged body its page provides (DraftVerdictBodyContext), as PermissionMatrixEditor does.

Readings (head 4d13c9a)

Acceptance notes

Verification

Local subset per the dispatch: the touched-file test set, @object-ui/app-shell type-check, the derived gate list reconciled with dispatch-gates --ran, two ablations (one per card). Repo-wide lint and the full ui suite are CI's.


Generated by Claude Code

… it judged

The RLS clause editor kept showing the previous draft's server error under
newly edited text: a save the client lint holds never re-reads the draft, so
the served verdict on the stored text stayed, and the field read it by path
alone. The field now shows the server's errors on the same terms the save
hold already used (useServerHoldAt): only while the text on screen is the
value the served body holds at that path. Editing the text away hides it;
the next save's re-read brings the new one.

Claude-Session: https://claude.ai/code/session_01Utp6CZVdZdKe4J1nCNL23t
Co-authored-by: Claude <noreply@anthropic.com>
…ith != null

A no-code row "optional number greater than 40" compiled to
`record.estimated_hours > 40`; CEL has no `>` overload for null, so the
server refused the validation rule at publish and held the package back while
the builder said "Valid CEL". An ordering row (> < >= <=) whose subject names
a catalog field now compiles to `S != null && S op V`, the shape the
"can't be negative" preset writes, and reads back as that one row. The
builder decides nothing about nullability: the guard is well typed on every
field. A stored comparison without the guard keeps its spelling until the
author edits that row, so nothing is rewritten on open.

Claude-Session: https://claude.ai/code/session_01Utp6CZVdZdKe4J1nCNL23t
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#3 · os-bill · session_01Utp6CaJoinvGQSbHeDaWBe

席位核对

ACCEPT · head 4d13c9acc70b1510b6e813da300fcafa1b38f3f9 · 2026-10-29T17:32Z


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 29, 2026 17:34
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 29, 2026 17:34
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 29, 2026
Merged via the queue into main with commit 15b099e Oct 29, 2026
56 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-qx6k5b branch October 29, 2026 18:27
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 tooling

Projects

None yet

1 participant