fix(types,components,plugin-form,console,core): a faulted or blank visibleWhen refuses the submit, naming the field and the rule; a blank field rule is refused at authoring; blank gates are diagnosed (objectui#8069) - #11233
objectstack-fleet[bot] merged 11 commits into
Conversation
…nk-gate silencers are diagnosed (objectui#8069) ADR-0137 D2 needs a submit path to know which field rule could not be evaluated. resolveFieldRuleState now returns `faults` beside the three verdicts, filled from the same evaluation (the onFault passback), so a refusal never re-evaluates. The verdicts are unchanged (D3). A blank predicate stays out of the report: objectui's form wire admits it, so a refusal on it would be a trap; it keeps its [blank] warning. ADR-0137 D4: evaluateCelCondition's blank guard and hasDeclaredPredicate's blank fold now report through evalFieldPredicate's [blank] channel. Both verdicts (objectui#3850 / #3960) are unchanged, throwOnError included. Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec Co-authored-by: Claude <noreply@anthropic.com>
…e submit, naming the field and the rule (objectui#8069) ADR-0137 D2 as ruled (Q1 = B, one judge per rule): the form renderer, the console's FormPage and the wizard's cross-step gate refuse a submit whose field visibleWhen could not be evaluated, with the new form.visibleWhenFaulted message naming the field and the rule. Each reads the fault from the resolveFieldRuleState call that already drew the verdict. requiredWhen / readonlyWhen are unchanged on the client: the server refuses their faults and form.tsx shows that inline. A blank visibleWhen stays "no gate" with its [blank] warning, and a visibleWhen reading previous on a create form is refused (the accepted residual). Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec Co-authored-by: Claude <noreply@anthropic.com>
…lyWhen faults are the server's (objectui#8069) Two sentences said a broken field predicate never blocks a submit; that is no longer true for visibleWhen (ADR-0137 D2 as ruled). Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec Co-authored-by: Claude <noreply@anthropic.com>
…i18n provider; the objectui#6958 boundary pin states D2 (objectui#8069) The form renderer and the wizard translate through createSafeTranslation tables; both now carry form.visibleWhenFaulted (and the wizard the list joiner) byte-identical to the en pack. The #6958 BOUNDARY pin asserted a broken visibleWhen writes the value as it stood; under ADR-0137 D2 the write is refused, so it now pins both halves of "a broken predicate never silently nulls a stored column": nothing cleared, nothing written. Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec Co-authored-by: Claude <noreply@anthropic.com>
…sis (objectui#8069) ADR-0137 D4 (Q2 = B as ruled): a blank gate folded to "no gate" is diagnosed, never silent. The verdicts these two pins hold (objectui#3862 enabled, objectui#3850 / #3960 kept) are unchanged; the "and silent" half now reads "and reported once per blank spelling, through core's [blank] channel, with no fault report". Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec Co-authored-by: Claude <noreply@anthropic.com>
…onfig Callback parameters over mock.calls are annotated (TS7006), and the console pin reads the last toast by index (its lib has no Array#at). Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec Co-authored-by: Claude <noreply@anthropic.com>
|
changeset-claim-re-read
|
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Inputs: card #8069 (body and all 23 comments, the ruling Check-runs on the head: 43 runs, all completed at 2026-09-30T16:26Z — 40 success, 3 skipped ( ① Derived judgmentsCore — The three paths (RIGHT).
Q2 — the two gate silencers (RIGHT). Silencer 1: The The residual pin (RIGHT, with one residual broader than the ruled one — finding 2). Restated pins (each is what the ruling implies, none a weakening). Docs (RIGHT, one sentence contested). Ablation legs (accepted on the report; not re-run — read-only). A (fault report records nothing) 13 red by name across all six pin files with every control, blank, Overlap with #11208 (disjoint, verified). #11208 merged at 2026-09-30T13:43Z (merge ② Semver levelChangeset Clause-②: the PR body reads ③ Boundary flagsUnder-listed surface (all within the ruling's scope). Open question — a stored blank Residual broader than the ruled one (finding 2, non-blocking; the seat names it on the card). The ruling accepted "a The skill sentence (confirmed false at this head). Two more blank silencers (filing class: ONE successor card under ADR-0137 D4, not an acceptance note). At the head, PR #11208 overlap. Verified disjoint in ① above; the PR is Findings, numbered:
Implemented-by: VERDICT: FAIL Generated by Claude Code |
… refused — at parse by the form wire (ADR-0137 D1), at submit when stored (D2) (objectui#8069) Contract review 5915380177 (finding 1), as the seat applied it: ADR-0137 refuses "blank means no rule" for the field-rule triad. FormFieldSchema's visibleWhen / readonlyWhen / requiredWhen now refuse a predicate that is blank after trimming, with the spec's EVALUATED_EXPRESSION_SOURCE_REQUIRED sentence, as a refinement OVER ExpressionWireSchema: the same two option schemas by reference, so the one wire shape of objectui#7530 holds and only the blank value is taken out. Gate keys keep the plain wire. resolveFieldRuleState now reports a blank rule as a fault ([blank]), so a stored blank visibleWhen is refused at submit on form.tsx, FormPage and the wizard gate. The objectui#7530 pin is re-pinned to one wire SHAPE plus the triad's blank refusal; the edit-wizard previous residual (finding 2) is named; changeset and metadata-diagnostics.md follow. Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec Co-authored-by: Claude <noreply@anthropic.com>
…nused binding (objectui#8069) Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…onInputSchema, not a restated rule (objectui#8069) The form field-rule triad's check now hands the predicate TEXT to the spec's own evaluated-slot schema (through the objectui#8317 import boundary) and refuses with its verdict and sentence, so the rule and the message are the spec's by reference. The import-boundary census lists the new crossing; the objectui#11073 census rebuilds its OPEN twin from the union's own def so a union-level check stays on both sides and only the arms are measured. Two view-level controls pin "no gate, no refusal". Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…e thank-you screen replaces the form (objectui#8069) Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Round 2, after the FAIL record Check-runs on the head: 43 runs, every one completed between 2026-09-30T18:09:48Z and 18:24:11Z, all on ① Derived judgmentsD1 on objectui's own form wire — ONE wire SHAPE, a blank VALUE taken out; not a second wire type (RIGHT; the fork-stop was right not to fire). D2 on all three submit paths, a stored blank included (RIGHT). The rest of the round-1 record stands at this head. The Finding 2 of round 1 (the edit-wizard The two instrument corrections — honest fixes, not gates bent. (a) Ablation legs D and E (accepted on the report; not re-run — read-only). D (the triad's blank refusal made unreachable): 12 red, exactly the twelve blank cases, every accept, identity, junk and gate control green. E (a stored blank taken out of the report): 7 red, exactly the seven stored-blank cases. Each with anchor count, blob before and after, and a hash-equal restore; the first D attempt was refused by the tool as an anchor miss before anything ran. Every relayed body sentence and the title, checked at this head — TRUE. The ten replacements are in the body as the dev wrote them; each was checked against the head files: the ② Semver levelChangeset:
③ Boundary flagsThe seat's option-B decision and its fork-stop — recorded in the dev report and the PR body, not on the card. The card's thread ends with the dev's follow-up report; the decision under which Card the seat must file, A — the governed skill sentence. Card the seat must file, B — the ADR-0137 D4 successor, now with THREE silent blank-gate paths. D4 says a blank or faulting GATE predicate is "diagnosed, never a silent Landing condition, not a finding. Round-1 items closed this round. Finding 1 (the blank at submit): resolved as option B, judged RIGHT in ① — a stored blank is refused at submit (D2) and a new one at authoring (D1), a blank gate stays diagnosed-not-refused (D4), and ADR-0137's Alternatives are now honoured for the triad. Finding 2: accepted and named. The under-listed claim surface (five items) still needs appending to the claim by the seat; No numbered findings: none of the items above blocks this head. Implemented-by: VERDICT: PASS Generated by Claude Code |
…age and two console pins name reference PR #11233 has landed, so the remaining tail is in scope: the form-field zod comment, FormPage's metadata comment, plugin-form.mdx's field-slot row and the shared-field-resolver pin now spell the target reference. deriveRelatedLists' docblock drops the reference_to it has not read since objectui#6837 half 2. Claude-Session: https://claude.ai/code/session_01TdiauJaVCHuj45EzZGUxHh Co-authored-by: Claude <noreply@anthropic.com>
Fixes #8069
Clause-②: yes (narrowing)
A field
visibleWhenthat cannot be evaluated now refuses the submit, naming the field and the rule, on the three submit paths the ruling names: the record form renderer (form.tsx, so everyObjectFormlayout), the console'sFormPage(/forms/:nameand/f/:slug), and the wizard's cross-step gate at final submit.requiredWhen/readonlyWhenare unchanged on the client; the server's ADR-0137 D2 refusal of them lands beside the input. Both blank-gate silencers are diagnosed, with no verdict changed.Ruling: card comment 5910115531 (batch #256 item 4) — Q1 = B, one judge per rule; Q2 = B as seat 1 answered it in 5908497249. Dispatch claim: 5912071659.
Narrowing
visibleWhenfaulted (typo in a column, syntax error, unbound root) or was stored BLANK drew the field (fail-open) and submitted as if the rule had said "show"; and objectui'sFormFieldSchemaaccepted a blankvisibleWhen/readonlyWhen/requiredWhenat parse.form.visibleWhenFaultednaming the field(s) and the rule — a stored BLANKvisibleWhenincluded ("A blank predicate takes this path too, wherever one is already stored"). The field is still drawn (D3 unchanged). Nothing is written.@object-ui/types(minor):FormFieldSchema's field-rule triad refuses a predicate whose text is blank after trimming, with the spec's own sentence (EVALUATED_EXPRESSION_SOURCE_REQUIRED), as the spec already does onFieldSchema. The accepted SHAPE is unchanged (see theExpressionWireSchemaparagraph); gate keys keep the plain wire, and a blank gate stays "no gate" plus a one-time diagnostic.requiredWhen/readonlyWhenfaults — a stored blank one included — are refused by objectql's D2 and stay the server's. ADR-0137 refuses "blank means no rule" for the triad (Alternatives), so a blank is refused at the first place that can see it: authoring (D1, objectui's own wire handled in the same round, as the ruling's execution text asks) or submit (D2). Applied after contract review 5915380177, finding 1.visibleWhenreadingpreviouscannot be evaluated on a CREATE form, so such a form is refused on every submit (ruled); and the wizard's cross-step gate binds nopreviousin either mode, so the same rule is refused at an EDIT wizard's final submit too (accepted by the seat under the ruling's refusal of option D;persistedRecordis deliberately not bound).@object-ui/core(minor):resolveFieldRuleStatereturns a newfaultsmember beside its three verdicts, and its typeFieldRuleFaultsis exported from the package root.@object-ui/i18n(minor): the new keyform.visibleWhenFaultedin all ten packs, so the exportedTranslationKeystype gains a member.What changed
Core — the report a submit path reads.
resolveFieldRuleStatefillsfaultsfrom theonFaultpassback of the veryevalFieldPredicatecalls that draw the verdicts, so no refusal evaluates a predicate a second time. It reports all three rule kinds; which ones a path refuses on is the path's ruling. Left out, by construction: a rule the verdict did not need (short-circuited, never ran). A stored BLANK rule is IN it, with its[blank]reason — ADR-0137 D2's third state, not a spelling of "no rule". The stale "open question" prose infieldRules.tsnow points at ADR-0137 D2 / D3 / D5. Edited regions stay clear of PR #11208's two hunks in that file (its import line and theevalFieldPredicateevaluate call);git merge-treeof this branch with #11208's head947b8d38bis clean.The three submit paths, each reading
faults.visibleWhenoff the call it already makes:form.tsx: theconditionallyHiddenFieldNamesmemo (every declared field'svisibleWhen, the6a449fc49clear-on-hide input) now also returns the faulted names.handleSubmitrefuses BEFORE react-hook-form validation (nothing the user types clears a broken rule), on the in-form banner and the outcome toast, and marks the tabs holding the fields. Every declared field is judged, not only drawn ones: a faultedvisibleWhennever clears on hide, so its value reaches the payload whatever hides it. Asection-dividerrow'svisibleWhen(its section's layout gate) is excluded.FormPage:findMissingRequiredbecamefindSubmitRefusals, oneresolveRowStatepass returning both the faulted rows and the missing ones; the fault is refused first. Hidden sections stay skipped, as the required check already skipped them: on this page visibility decides only what is drawn, so a row a hidden section keeps off screen cannot be shown by its own broken rule.WizardForm:missingRequiredByStepbecamegateFinalSubmit, the same pass returning both; a fault lands the wizard on the first step holding such a field.Q2 — the two blank-gate silencers, diagnosed (ADR-0137 D4), verdicts unchanged:
ExpressionEvaluator.evaluateCelConditionansweredtruefor a blank CEL source before anything could say so. It now reports throughevalFieldPredicate's[blank]channel (the caller'sonFaultwhen given, else the deduped built-in warning) and still answerstruein every mode,throwOnErrorincluded.hasDeclaredPredicatefolded blank text to "no gate". It still does (objectui#3850 / 「空谓词」的第四种拼法:{ dialect: 'cel', source: ' ' }(source 只有空白)仍被判成已声明门 → disabled 侧仍永久置灰、执行入口仍拒执行(#3850 裁决枚举未覆盖) #3960), and now reports once through the same channel. Its locator is fixed (the question is key-neutral), so the dedupe is per blank spelling across the app — stated in its docblock.Two existing pins asserted the silence and now pin the diagnosis instead, verdicts untouched:
SchemaRenderer.disabledGateFaultDiagnostic.test.tsx(objectui#3862 empty shapes) andActionParamDialog.test.tsx(blank paramvisible).ExpressionWireSchema— D1 on objectui's own form wire, and no second wire type.FormFieldSchema'svisibleWhen/readonlyWhen/requiredWhenareExpressionWireSchema.superRefine(...)(module-privateFieldRulePredicateWireSchemainform.zod.ts): the check hands the predicate's TEXT (the string, or an envelope'ssource) tostripImportedDefaults(EvaluatedExpressionInputSchema)from@objectstack/spec/sharedand refuses with that schema's verdict and sentence, so neither the blank rule nor its message is restated. Only the text is judged, never the envelope, whose specdialectenum is narrower than the wire. The refined union's arms AREExpressionWireSchema.options, by reference, and its parse output is the authored value unchanged — so the accepted SHAPE is the one wire type objectui#7530 ruled, and only a blank VALUE is taken out. That closes the submit-refusal trap: a blank cannot be authored, and one already stored is refused at submit.BaseSchema'svisible/hidden/disabled, a field's view-levelvisibleOnand an option'svisibleWhenkeep the plain const. Two test instruments followed:imported-defaults-8317.test.tslists the new spec crossing, andterminal-unknown-key-refusal-11073.test.tsrebuilds its OPEN twin from the union's own def (itsz.union(options)dropped the union's check and read the triad's blank refusal as an accept-set move).Docs:
content/docs/guide/metadata-diagnostics.mdandcontent/docs/plugins/plugin-form.mdxsaid a broken predicate never blocks a submit; both now describe the refusal, andmetadata-diagnostics.mdadds that a blank field rule is refused (at authoring by the form schema, at submit when stored) while a blank gate stays "no gate".Pins
packages/core/src/evaluator/__tests__/fieldRuleFaults-8069.test.ts— the report per rule kind, the create-formpreviousresidual with its edit-form control, short-circuited rules, a stored blank rule reported as[blank]for each rule kind with an absent-rule control, and both silencers with their controls (fresh module graph per case, since the unit project runsisolate: false).packages/components/src/renderers/form/__tests__/visibleWhen-fault-refuses-submit-8069.test.tsx— the refusal, its ordering before validation, the healthy control, a stored blankvisibleWhenrefused, a blank view-levelvisibleOnNOT refused (the gate control), the divider exclusion, the residual and its edit control, and the end-to-end server pin: a faultedrequiredWhen/readonlyWhenis let through by the client and the server's D2 envelope (VALIDATION_FAILED,code: 'rule_violation',constraint.reason: 'unevaluable', transcribed from objectql at the spec 17.5.0 tag0f6dcac5e9) lands inside that field'sdata-fieldwrapper.apps/console/src/components/FormPage.visibleWhenFault-8069.test.tsxandpackages/plugin-form/src/wizardVisibleWhenFault-8069.test.tsx— the same cases on the page (read off the stubbed write) and on the gate (a skipped step, so only the gate can see the field).visiblewhen-clear-on-hide-6958.test.tsxBOUNDARY pin restated for D2: a broken predicate clears nothing AND writes nothing — the verbatim constraint "a broken predicate must NEVER silently null a stored column" now holds in both halves.packages/types/src/__tests__/base-schema-predicate-envelope-7530.test.ts— re-pinned to what objectui#7530 protects, ONE wire shape:BaseSchema's gates and the form's gate legs still carryExpressionWireSchemaitself; the triad carries a refined clone whose arms areExpressionWireSchema.optionsby reference; each triad key refuses'', whitespace,{ source: '' }and a whitespace CEL envelope at the key withEVALUATED_EXPRESSION_SOURCE_REQUIRED, accepts a non-blank string, a CEL envelope and a dialect-less envelope with the value unchanged, and still refuses junk; the control: a blank gate onBaseSchema,visibleOnand an option'svisibleWhenstill parses. The comment cites ADR-0137 D1 and this ruling.Verification
All heavy runs went through
os-verify-lock.sh(slotissue-8069); verdicts read from itsVERDICTline and from the pass counts, never a bare exit code. Base0389650f3.Rework round (contract review 5915380177: finding 1 as option B, finding 2 accepted), final head
277476aaf:277476aaf:apps/console/src/components/36 files, 437 passed; the twelve pin files (the four new ones,fieldRules.test.ts, the The Console submits the value of a field its ownvisibleWhenhas hidden, and clears nothing — sovisibleWhenon a populated lookup is a dead end, not a hint #6958 file,wizardPredicateScope,ActionParamDialog,SchemaRenderer.disabledGateFaultDiagnostic, and the types pinsbase-schema-predicate-envelope-7530,imported-defaults-8317,terminal-unknown-key-refusal-11073) 12 files, 469 passed;@object-ui/consoletype-check EXIT 0.90d406183(the one later commit changes only the console pin file, re-run above):turbo run build --filter='@object-ui/console^...' --concurrency=2EXIT 0;type-checkEXIT 0 for@object-ui/types,@object-ui/core,@object-ui/i18n,@object-ui/components,@object-ui/plugin-form,@object-ui/console, 0error TS;packages/types/289 files, 6915 passed;packages/core/191 files, 3768 passed, 27 skipped;packages/components/src/renderers/form/69 files, 475 passed, 17 skipped;packages/plugin-form/in three chunks, 50 + 47 + 57 files, 310 + 514 + 953 passed, 1 skipped; rootscripts/__tests__/177 files, 5371 passed.eslinton the 12 code files this round changed: 0 errors, per-file warning counts unchanged (0 on the new and changed pins). Light gates at90d406183, all EXIT 0: the list below pluscheck:spec-symbolsandcheck:phantom-deps;check-governed-queue-guard --testover 34 paths NOT GOVERNED.277476aaf, same discipline as below: baseline 5 files, 112 passed. D — drop the triad's blank refusal (thesuperRefine's issue branch made unreachable): 12 failed / 50 passed, exactly the twelve blank cases (three keys, four spellings); every accept, shape-identity, junk and gate control green; blob53a98c5btoc748e2baand back. E — take a stored blank out of the fault report again: 7 failed / 43 passed, exactly the seven stored-blank cases (core four, form, FormPage, wizard); blob4d2bb278to01b3a2c2and back. The first D attempt, at90d406183, was an anchor miss the tool refused (exit 3, nothing ran): its anchor was the pre-superRefinespelling.First round, at
ff6d7775b:type-check(each afterturbo run build --filter='@object-ui/console^...' --concurrency=2, EXIT 0):@object-ui/core,@object-ui/i18n,@object-ui/components,@object-ui/plugin-form,@object-ui/console— all EXIT 0, 0error TS. Each package's script also compiles its tests (tsconfig.test.json, and the console's own config), which is where the first round found twelve TS7006 and one TS2550 in the new pins, all fixed.pnpm exec vitest run packages/components/src/renderers/form/: 69 files, 474 passed, 17 skipped.fieldRules.test.ts, the The Console submits the value of a field its ownvisibleWhenhas hidden, and clears nothing — sovisibleWhenon a populated lookup is a dead end, not a hint #6958 file,ActionParamDialog.test.tsx,SchemaRenderer.disabledGateFaultDiagnostic.test.tsx): 8 files, 189 passed.eslinton the 27 changed code files: 0 errors; every file's warning count equals its count at base (new files 0) — population is the changed-file list, counts from--format json, and type-aware linting is not enabled ineslint.config.js(noproject/projectService), so the diff cannot move a verdict on an untouched file.check-changeset-presence,check-control-bytes,check:new-line-citations(0 new),check:i18n-keys,check:i18n-drift,check:i18n-dead-keys,check-changeset-claims,check-changeset-no-major,check:test-path-roots,check-vi-mock-specifiers,check-unreferenced-sources,markdown-test-inputs.mjs --audit. A control-byte scan of every changed file finds none.check-readme-exports: NOT MEASURED in full — it exits 1 on two packages outside this diff that the build closure does not include (@object-ui/cli,@object-ui/plugin-ai, "run pnpm build first"); over the 35 built packages it judged 545 self-imports real, 0 wrong-path, 0 fabricated.Earlier heads (the later commits only touched test typing, two pin files re-run above, and one
(field as any).labelbecomingfield.label):packages/core/191 files, 3767 passed, 27 skipped ·packages/i18n/74 files, 1216 passed ·apps/console/src/components/36 files, 436 passed (all at5800ae995).packages/plugin-form/in three chunks: 50 + 47 + 57 files, 310 + 514 + 952 passed, 1 skipped ·packages/react/104 files ·packages/components/src/renderers/action/29 files, 475 passed · 18 consumer files ofhasDeclaredPredicate/resolveFieldRuleStatein app-shell, plugin-detail, plugin-grid and components · rootscripts/__tests__/177 files, 5371 passed (all at2dc06d4e2). The react and consumer runs each had ONE red, both a pin asserting a blank gate stays SILENT — the behaviour Q2 = B reverses; both pins now assert the diagnosis and are green in the final run.console.warnnear a blank value, found by grep across other packages: 22 files, 773 passed (atcfd6676a9).Ablations (at
cfd6676a9, each leg throughablation-replace.mjs: the anchor hit exactly once, the write proven on disk by anchor count and blob hash, the restore proven by blob hash equal toHEADand an emptygit diff HEAD; the package is aliased tosrcin the vitest config, so nodistleg applies):visibleWhenrefusal at its one source (the fault report records nothing): 13 failed / 91 passed, red by name on every refusal and report pin across all six files (form, The Console submits the value of a field its ownvisibleWhenhas hidden, and clears nothing — sovisibleWhenon a populated lookup is a dead end, not a hint #6958 boundary, wizard, FormPage, core); every CONTROL, blank,requiredWhen/readonlyWhenand edit-form case stayed green. Blob2e74e4cctodc5beddaband back to2e74e4cc.hasDeclaredPredicatewithout its report): 4 failed / 20 passed, exactly the four silencer-2 diagnosis cases; its controls stayed green. Blob748c4f17to0fb13924and back.evaluateCelConditionguard: the first attempt was a no-op the tool refused (its replacement contained the anchor, so the anchor count did not drop; exit 1 before anything ran). Re-run with a different anchor: 2 failed / 22 passed, exactly the two silencer-1 diagnosis cases; thethrowOnError-no-throw case and the control stayed green. Blobbcf875eeto24a1178dand back.Acceptance notes
ObjectFormand its drawer, modal, split and tabbed layouts, each wizard step, andMasterDetailForm's headerObjectFormall render atype: 'form'node, so they submit throughform.tsxand inherit the refusal; a wizard step whose own field faulted therefore refuses its Next as well.RecordFormPagedelegates toObjectForm. Not judged:ScreenView(flow screens — not a record write),GridField(grid-column rules are presentation-only in the spec) andplugin-kanban'srequiredWhenPrompt(requiredWhenonly, which the server refuses).FormPageshows a server D2 refusal as page-level text, not inline — it has noextractFieldErrorspath. The server's message still names the field and the rule. No change here.visibleOn, and a form view's ownvisibleWhen, whichsectionFieldsroutes there) and SECTION predicates are layout gates, not field rules; they stay fail-open and warned.examples/: a fieldvisibleWhenreadingparentoutside a grid, and one reading a column the create form does not render or seed, are unevaluable there and so refused.visibleWhenreadingpreviousis refused on a CREATE form (ruled), and at the final submit of an EDIT wizard, whose cross-step gate binds nopreviousin either mode (accepted by the seat under the ruling's refusal of option D;persistedRecordis deliberately not bound).evaluateCondition's legacy path returnstruefor a bare''/ whitespace string, andevalRowPredicatereturns its fallback for a bare blank string, both without a diagnostic. A third silent path, also a gate and also untouched: a blank VIEW-level field predicate is dropped by section normalization (sectionFields'attachVisibility) before any evaluator sees it, on both form chains — "no gate", never refused, but not diagnosed either.skills/objectui/guides/schema-expressions.mdsays field-rule evaluation "never blocks submit". It is not edited here, so this PR stays off the governed surface; it needs its own PR.Overlap
PR #11208 (objectui#4421) edits
fieldRules.ts(its import block and theevalFieldPredicateevaluate call) andRecordFormPage.tsx's evaluator memo. This PR touches neither hunk and does not touchRecordFormPage.tsx; the textual merge is clean. PR #11208 landed first (9cebfca5a); the merge queue's merge group runs this PR's CI overmain, which carries it.Session:
https://claude.ai/code/session_011p7ikEivgXefNDaE5S5UecGenerated by Claude Code