Repository navigation
fix(ui): judge requiredWhen as the engine's transition gate on the record Edit form - #30216
Merged
objectstack-fleet[bot] merged 3 commits intoNov 5, 2026
Merged
objectstack-fleet[bot] merged 3 commits into
objectstack-fleet[bot] merged 3 commits into
Conversation
…cord Edit form Claude-Session: https://claude.ai/code/session_01QSeKYnE9YNkSZen7sijLA3 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QSeKYnE9YNkSZen7sijLA3 Co-authored-by: Claude <noreply@anthropic.com>
…Clause-② widening Claude-Session: https://claude.ai/code/session_01QSeKYnE9YNkSZen7sijLA3 Co-authored-by: Claude <noreply@anthropic.com>
Contributor
Author
|
domain:ui#2 · os-justin · session_01QSeKYnE9YNkSZen7sijLA3 维护者速读
席位意见:ACCEPT。 席位核对
|
objectstack-fleet
Bot
deleted the
claude/issue-30184-requiredwhen-transition-gate
branch
November 5, 2026 02:53
8 tasks done
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.
domain:ui#2 · os-justin · session_01QSeKYnE9YNkSZen7sijLA3
Fixes #30184
Clause-②: yes (widening)
Metadata-protocol: none
This widens the public surface of
@object-ui/core: the new exportrequiredWhenRestsOnStoredRecordand the newrequiredWhenAtRestfield in the return type ofresolveFieldRuleState.维护者速读
requiredWhen(转移闸门):已存记录本来就处在闸门内且缺值时,不再挡住无关编辑;创建表单与「把记录移入闸门」的编辑照旧拦截。PATCH返回 200,但 Console 编辑一律报「Signed Copy is required」,存量记录在 Console 里改不了。ui/packages/core/src/evaluator/fieldRules.ts(resolveFieldRuleState的requiredByRule/requiredWhenAtRest段)、ui/packages/components/src/renderers/form/form.tsx(渲染路径传入name,与动作提示的!ruleState.requiredWhenAtRest)、form-required-when-submit.test.tsx新增的 describe。pnpm --dir ui exec vitest run packages/components/src/renderers/form/__tests__/form-required-when-submit.test.tsx,预期 9 个用例全绿,其中「saves an unrelated edit to a record already in the gate」通过。What changes
The engine's rule, read on
origin/main31fe289f: objectqlevaluateValidationRules(packages/objectql/src/validation/rule-validator.ts, the "ADR-0113 non-regression: reject iff the MERGED state violates AND the PRE state complied" block) refuses an update only when the predicate holds over the merged record with the field missing AND the stored row did not already violate it (thepreViolatedcheck). A predicate that cannot be evaluated over the stored row counts it as compliant. An insert complies vacuously.Field.requiredWhenin@objectstack/specstates the same contract: "the write is refused only when the merged record violates the requirement AND the pre-write record complied".The client side is re-bound to that rule in the one place every form verdict comes from:
@object-ui/coreresolveFieldRuleStatetakesstatics.name. Withprevious(the stored row of an UPDATE) andname, a stored row that already violatesrequiredWhenresolvesrequired: falseand the newrequiredWhenAtRest: true. A caller that passes nonamekeeps the plain verdict. The stored-row check is exported alone asrequiredWhenRestsOnStoredRecord. "Missing" isisMissingForRequired, the same answer the client's required check gives.@object-ui/componentsform renderer passesnameat the render path (asterisk,aria-requiredand the submit validator all read that one verdict) and at the stale-error cleanup that readsrequired. The update-action heads-up (the "Required to run" note, objectstack#28382) is suppressed whenrequiredWhenAtRestis true: the engine judges an action's write against the same stored row, so the action is not refused for that field either.Chosen: mirror, not defer. Everything the rule reads is in the form: the Edit form's
previousValuesis the record fromfindOnewith no$select, seeded withnullfor every declared field, the same assembly the live evaluation already uses. Where the form's stored row could differ from the server's (a column the caller cannot read), the client answers "at rest" and lets the write through, and the server's field-attributed refusal still lands at the field through the existingextractFieldErrorspath. The client is never stricter than the engine here, except for the existing empty-array case (the client counts[]as missing, the engine does not), which this PR does not change.Create forms (
previousValuesabsent) are unchanged.Tests
form-required-when-submit.test.tsx(the realformrenderer): (a) an already-activated record with no signed copy saves an unrelated edit; (b) moving a record intoactivatedwithout the copy is refused in the form; clearing the copy of a record that held it is refused; (c) a Create form still enforces.form-required-for-action-28382.test.tsx: a stored row already inside the gate without the value shows neither the asterisk nor the action note, and Save writes.corefieldRules.test.ts: the six verdict cases, including nonamekeeps the plain verdict, staticrequired: trueis never relaxed, and a fault over the stored row counts it as compliant. The existing "all three compose"toEqualgainsrequiredWhenAtRest: false.Acceptance notes
nameyet (same defect class, outside this claim's file surface, so not edited):FormPage(ui/apps/console/src/components/FormPage.tsx,resolveRowState) edits through/forms/:name?recordId=with the stored record asprevious; it gets the fix by addingname: field.nameto its statics.plugin-formWizardFormgateFinalSubmitpasses nopreviousin either mode, deliberately ("persistedRecordis deliberately NOT bound here", objectui#8069's ruling). An Edit wizard's steps render through the fixed form renderer, but its cross-step final gate still refuses a stored row already in the gate. Fixing it means usingpersistedRecordfor the stored-row check, which that note forbids today, so it needs the seat's word.plugin-kanbancollectRequiredWhenPromptFieldspasses the pre-drop record aspreviousand noname, so a drop still prompts for a field the engine would let rest.fieldRules.tsstill says an unevaluablerequiredWhen"is skipped on BOTH ends". That has been stale since ADR-0137 D2 (the engine refuses). Not changed here.Verification
Head:
0e6a6f475(every reading below was taken on this head; base31fe289f).c03f4ecacafter it changes only the changeset'sClause-②:line and adds one sentence; the changeset gates were rerun on it (check-adr-0087-registration,check-changeset-no-major,check-empty-changeset,check:changeset-fixed,check:nul-bytes, all exit 0).Targeted tests (core evaluator + every ui test naming requiredWhen / resolveFieldRuleState / aria-required)
Ablation: transition check removed (anchor statics.name !== undefined replaced by false), restored to HEAD
Action-note half alone (the
!ruleState.requiredWhenAtRestgate deleted inform.tsx): the action-note case goes red (1 failed, 13 passed), restored, blob == HEAD.A first ablation attempt was a no-op: its replacement contained the anchor, so
ablation-replacerefused it and ran nothing.Typecheck
Both scripts run
tsc -p tsconfig.test.json, so the new tests are compiled.Derived gates (dispatch-gates --commands, 35 + check:changeset-fixed)
dispatch-gates --ran: "35 derived famil(ies) accounted for — 35 run, 0 NOT-MEASURED (a DERIVED zero — all 35 recorded an exit code and none of them is 3)".pnpm --dir ui exec vitest run scripts/exited 1 on the full run (3 failed of 4987): two cases ofdate-only-zone-pins-10183.test.ts(a forked plugin-calendar child) and one 15 s timeout injs-comment-mask-param-types-9324.test.ts. Neither reads a file this PR changes. Rerun alone on the same head under the lock:Test Files 2 passed (2),Tests 39 passed (39), VERDICT command-exit 0. Read as shared-box load; CI's run is the record.pnpm check:type-check-debtfirst returned exit 3 (PREREQUISITE NOT MET, lock queue timeout, nothing measured); rerun under the lock: "0 raw tsc error(s) total, none above its recorded number", exit 0. The table records the rerun.Generated by Claude Code