Repository navigation
fix(app-shell): Studio CEL editors agree with the server verdict (builder null guard, stale RLS verdict) - #27677
Merged
Conversation
… 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>
Claude-Session: https://claude.ai/code/session_01Utp6CZVdZdKe4J1nCNL23t Co-authored-by: Claude <noreply@anthropic.com>
Contributor
Author
|
domain:ui#3 · os-bill · session_01Utp6CaJoinvGQSbHeDaWBe 席位核对ACCEPT · head
Generated by Claude Code |
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.
Requested by Bill · project thread
维护者速读
record.x != null && record.x > 40(与「数字不能为负」预设同形),服务端发布时接受;RLS 子句字段只在文本仍是服务端评判过的那段时才显示服务端报错,改了文本就不再挂着上一版草稿的旧错误。record.estimated_hours > 40,可空字段在 CEL 里没有>的 null 重载,构建器说「Valid CEL」,发布时却被拒并拖住整个包;RLS 字段在被客户端语法检查挡住保存时不会重读草稿,旧的服务端报错一直显示在新文本下面。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 toS != 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'").parseGuardedComparereads 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!= nullis 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.update→withGuardon the rows the edit replaced), following the builder's existing rule for the author's own quote spelling.CelPredicateField.tsx: the server's errors atverdictPathare 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-invalidand the destructive border follow the same set.useServerErrorsAtis no longer read here (the flow edge inspector still reads it).Why
#27513: the release walkthrough built "Reject the save when…
record.estimated_hoursgreater than40" 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 (findUnguardedNullableOperandsfrom@objectstack/lint, test-only) as clean and evaluated by@objectstack/formulaon 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.tsxfor #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.tsxnow provides the judged body its page provides (DraftVerdictBodyContext), asPermissionMatrixEditordoes.Readings (head 4d13c9a)
git grepover the changed components, 115 files): 115 passed / 1043 tests, on232f261aand again on the merged head (lock VERDICT command-exit 0).pnpm --filter @object-ui/app-shell run type-check: exit 0 on the merged head.dispatch-gates --commands: 34 commands, each:: exit 0on the merged head (includingnode scripts/check-sdui-manifest.mjsandpnpm --dir ui exec vitest run scripts/: 169 files / 4901 tests passed);dispatch-gates --ran: 34 derived, 34 run, 0 NOT-MEASURED.check:new-line-citations: exit 0, 0 new.compileRowturns 4 of the 7 null-guard cases red.Acceptance notes
validate-expressions.tspushes validation-rule findings (check()andcheckNullGuards, the Studio validation builder: "Reject the save when <optional number> greater than 40" says Valid CEL and saves, then the server refuses the rule at publish and holds back the whole package #27513 refusal) withwhereonly;authoring-rules.tssetspath: i.path ?? i.where;withPublishVerdictkeeps a non-positional path as written. So the draft read's_diagnosticsentry for a rule carries the prose locator (object 'X' · validation rule 'NAME'), nevervalidations.N.condition;protocol.runtime-authoring-gate.test.ts's finding(lint): the object save door gives no build verdict on validation conditions, field-rule slots (requiredWhen etc.), option visibleWhen or action predicates; os build refuses them, a metadata save stores them (#22019's sibling) #22032 block pinsissue.pathequal to thatwhere. Only a top-level flow edge's condition carries a key path (Studio: an expression field that says "Valid CEL" should read the draft's server verdict at its own path (RLS predicate, flow edge condition) #26858). No ui-only change can hold or show a server verdict at a rule's own path, so the seat returned Studio: an expression the engine refuses (RLS USING on a formula field, a null-unsafe validation predicate) keeps autosaving — the server verdict is shown on the field but never holds the save #27482 to triage to file the producer-plus-consumer seam.DataPillarregions ofStudioDesignSurface.tsx; this PR no longer touches that file, so no merge ordering applies.claude/project-thread-qx6k5bis the card thread's designated push branch (the recorded platform exception toclaude/issue-N-slug). The harness's after-PR assignee and review-request step is skipped by the dispatch order (the maintainer reviews after the seat accepts).Verification
Local subset per the dispatch: the touched-file test set,
@object-ui/app-shelltype-check, the derived gate list reconciled withdispatch-gates --ran, two ablations (one per card). Repo-wide lint and the full ui suite are CI's.Generated by Claude Code