Repository navigation
feat(app-shell): the view inspector refuses an object-required field inside a predicate-gated form section (objectui#6900) - #10589
Conversation
…oth view inspector hosts Red-first pins for the author-time refusal ruled on the card (ruling 5749269225, letter c): an object-required field inside a form section gated on an identity predicate must be reported as a blocking inspector issue and must hold Save shut, on the scoped (ViewInspector) and the home (ViewDefaultInspector) path alike. Controls: a record-scoped gate, a not-required field, and the field moved out of the section draw nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KUxVUa7e39aNjhkKi1gsoy
…inside a predicate-gated form section
Lands the author-time refusal ruled on objectstack#13252 (option B) in
the channel ruling 5749269225 (letter c) names: the view inspector's
blocking issues, which already gate Save. validateMetadataDraft stays
advisory; the objectui#6980 pin is untouched.
- inspectors/requiredInGatedSection.ts: reads the form body's sections
(sections, else the legacy groups alias when sections is absent), each
section's visibleWhen (string or cel envelope) and its members (names
and { field } entries); reads the predicate's roots with
@objectstack/formula's collectCelRootIdentifiers, loaded lazily; fences
the step-1 rows 7-11 roots only (current_user, user, ctx, os, features,
app, page). data is read as the row spelling (row 6), since no runtime
tier binds it as the host adapter any more.
- ViewVariantInspector: a second blocking term for the form family, beside
the list-only CEL term, reported through the same effect; renders the
refusal naming the field, that it is required on the object, the
section and its predicate, and the three remedies.
- useObjectFields: ObjectFieldInfo carries an optional required flag, read
from the object document the hook already fetches.
- i18n: the refusal copy in the en and zh tables.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KUxVUa7e39aNjhkKi1gsoy
…uired-in-gated-section-refusal Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KUxVUa7e39aNjhkKi1gsoy
|
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
|
The editor titled every inspector-blocked Save "Fix the CEL syntax errors before saving.", and the inspector channel now also carries the required-field-in-gated-section refusal, so that title was false on it. Per ruling 5831744213 (clarifying 5749269225: "unchanged" binds the objectui#6980 pin's Save-gate asymmetry, not its tooltip wording): - i18n: perm.inspector.saveBlocked, en and zh. - ResourceEditPage: the inspector-blocked Save title reads it. PermissionMatrixEditor and ObjectHooksPanel keep perm.cel.saveBlocked. - The five pins asserting the old title flip their title string only: ResourceEditPage.celGate, .defaultGate, .schemaAdvisory, .serverRefusalGate and .requiredInGatedSection-6900. One new pin: the refusal holding Save shows the neutral title. - Changeset: one paragraph on the new Save title. Claude-Session: https://claude.ai/code/session_01KUxVUa7e39aNjhkKi1gsoy 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: Reviewed against the diff ① Derived judgmentsLanding site and behaviour (ruling
Premise (1) — the object's Premise (2) — census. Not re-run here. The positive-control fixture files exist at the head with the identity gate ( Ruling Existing pins edited — load-bearing, not weakened.
New pins — can they fail?
Asked but missing: none found. Disclosed and left alone (verified true at the head, not defects): ② Semver level
Changeset prose, sentence by sentence:
Code docblock prose added by the diff, checked where load-bearing: ③ Boundary flags
Implemented-by: VERDICT: PASS |
Fixes #6900
Clause-②: no
What this does
The metadata designer's view inspector now refuses a form view that places an OBJECT-required field inside a section whose
visibleWhenreads identity, feature, host or page state. The refusal is a blocking issue in the inspector's existingonBlockingIssuesChangechannel, so the editor's Save stays disabled while it is reported, on both hosts of the inspector. This is the landing site ruling 5749269225 (letter c) names.validateMetadataDraftstays advisory, and every asymmetry assertion of the objectui#6980 pinResourceEditPage.schemaAdvisory.test.tsxis byte-unchanged and green. Only its Save-title string moved, in the patch round below, on the maintainer's ruling5831744213. Nothing inobjectstack, nothing server-side.The inspector renders the refusal. It names the field and says the object requires it, names the section and quotes its predicate, and lists the three remedies: move the field out of the section, remove the predicate, or make the field not required on the object.
Files
inspectors/requiredInGatedSection.ts(new)inspectors/ViewVariantInspector.tsxpreviews/useObjectFields.tsObjectFieldInfogains an optionalrequired. It is set totrueonly when the object's field def saysrequired: true, read from the def the normalizer already reads. Other fields keep the old shape.i18n.tsengine.inspector.view.gatedRequired.*keys, in both tables (en, zh), in their own rows afterengine.inspector.view.noSchema. They stay clear of theengine.form.*rows objectui PR 10551 edits.Premise (1): can the inspector channel see the object's
requiredflag? Yes, on the path the inspector already uses.useObjectFields(binding.value, objectFieldsOverride). That callsMetadataClient.get('object', NAME), which unwraps the{ type, name, item }envelope and returns the object document itself.itemwith every field def whole. objectstackpackages/runtime/src/domains/meta.tsanswersGET /meta/object/NAMEthroughmaskObjectSchema.applyObjectSchemaMaskremoves an unreadable field whole and never removes a key inside a def.FieldSchema.requiredis a declared spec key (z.boolean().default(false)).readFields(previews/object-fields-io.ts) strips onlyRETIRED_FIELD_KEYS, which isindexed,referenceToandisSystem(pinned byretired-field-key-tombstones.test.ts).requiredpasses through. The object designer's own required toggle (ObjectFieldInspector) reads the samedef.requiredoff the same served document.Premise (2): the census, with a positive control. Taken at BASE
9cbe4db, before any edit.The instrument parses the files. It uses the TypeScript AST for
.ts/.tsx/.js/.mjs/.cjsandJSON.parsefor.json, overgit ls-files. It counts a section by SHAPE: an object literal carryingfieldsplusvisibleWhenorvisibleOn. It resolves the predicate through literals,cel(...)calls,{ source }envelopes, tagged templates,constbindings, parameter defaults and, for a parameter with no default, the call sites of its function. It reads the predicate's roots withcollectCelRootIdentifiers.4db37cd, shallow clone)visibleWhen: GATE, or agateparameter fedGATEat the call site), and it resolves to'sales_manager' in current_user.positions. Counts:tabbedFormSectionPredicate-62376,fieldtab-visiblewhen-62373,section-grouping-62361. That is 10 literals in 3 files, the same as the last reading. The fixtures are untouched.visibleWhen: section.visibleWheninObjectForm,TabbedForm,ModalFormand siblings) plus one zoddescribestring. None is authored metadata.*.form.ts,dataroot) and conversion-registry examples (recordroot).sections:, and all 28 of their predicate lines are field-level (visibleOn).Mechanism hypotheses: readings
!isFormFamily, and the formatting editor that feeds it mounts for list families only. The new term sits beside it:blockingIssues = celBlocking + (isFormFamily ? gatedRequiredIssues.length : 0). The CEL term is not repurposed.@objectstack/formula'scollectCelRootIdentifiers. That is the export@object-ui/core'srowPredicateCanonuses, and the one the server's closed-root sites use. It is loaded lazily and destructured in the callback, the same waycelAuthoring.tsloads its engine, so the parser stays off the eager graph. No regex is involved. Fenced roots:current_user,user,ctx,os(row 7),features(8),app(9),page(11). A source that does not parse draws nothing, because syntax is the CEL gates' job.data.*split (row 6 vs row 10): as row 6, so no fence. Row 10 needs a runtime tier that bindsdataas the host's data-source adapter, and on this tree none does.buildExpressionScopepublishes none (objectui#8166), andSchemaRendererdropped itsdata: dataSource(objectui#9308, option B). What remains is the row spelling, which@objectstack/lintalready teaches as "Rewritedata.KEYasrecord.FIELD". Fencing it would push an author to delete a predicate they only need to respell.appis not bound at any runtime tier either (objectui#8155). So at BASE a row-9 section predicate faults open to a VISIBLE section.appstays fenced because the ruling names host predicates. The refusal is still correct advice there, because such a predicate is inert.features.*on any form-view predicate is already refused at parse by@objectstack/spec(ruled 2026-08-27). Row 8 therefore gets this refusal on top of the server's 422.ResourceEditPagewiresonBlockingIssuesChangeinto both the scopedInspectorComponentand theDefaultInspectorComponentbranch.ViewInspectorforwards it, andViewDefaultInspectorspreads it. The pins cover both hosts.sections[], or the legacygroups[]alias whensectionsis ABSENT. This is the precedence of the spec's ownfoldFormGroupsIntoSections:sectionswins when present, an empty array included.visibleWhenas a bare CEL string or a{ dialect, source }envelope. A non-celdialect is not read.{ field }entries (the spec'sFormFieldSchemakey).simple,tabbed,wizard,split,drawer,modal) read the samesections[], so none is special-cased.{ group }section, whose predicate is inherited from the object'sfieldGroupsrather than written on the view. Reaching it would need a second catalog widening (fieldGroupsplus fieldgroup), which is not authorized here. See Acceptance notes.Pins, and the base-red proof
ResourceEditPage.requiredInGatedSection-6900.test.tsx, run once per host (describe.eachdefault / scoped):salary, and the refusal is on screen.record.status == 'sent'gate saves.useObjectFields, and the roots through the real parser.inspectors/ViewVariantInspector.requiredInGatedSection-6900.test.tsx, run on the scoped and home paths:inspectors/requiredInGatedSection.test.ts: every step-1 row, in both directions, and each spelling above.08c15a2(tests-only commit on9cbe4db), host pin copied byte-identical (cmp) from5a8db23Tests 4 failed | 10 passed (14): the 4 main pins fail (toBeDisabled()on the host pin,expected +0 to be 1on the inspector pin); the controls pass5a8db23, clean treeTests 39 passed (39)0461b3a(after mergingorigin/mainat6ea68e6), clean treeViewVariantInspector.celGate/.homeGateandResourceEditPage.defaultGate:Tests 49 passed (49)Gates (local, targeted)
pnpm exec vitest run packages/app-shell/src/views/metadata-admin/as--shard=1/4,2/4,3/45a8db23(the merge brought in no metadata-admin file)--shard=4/40461b3apnpm turbo run type-check --filter @object-ui/app-shell(includestsc -p tsconfig.test.json;--listFilesOnlylists all 3 new test files)5a8db23, then0461b3apnpm exec eslinton this branch's 7.ts/.tsxfiles (diffed against the merge parent6ea68e6)0461b3acheck:i18n-keys,check:i18n-drift("6 key(s) added"),check:i18n-dead-keys(no new key listed),check:i18n-designer-parity0461b3acheck:control-bytes,check:test-path-roots,check:vi-mock-specifiers,check:vi-mock-inherit,check:vi-mock-override-shape,check:new-line-citations(0 new)0461b3acheck:changeset-claims(report-only; 3 pending bodies namei18n.ts, and each is about keys this diff does not touch, so all still hold),check:pending-changeset-literals,node scripts/check-changeset-presence.mjs0461b3acheck:unreferenced-sources,check:phantom-deps,check:designer-field-key-parity,check:handler-key-reads,check:metadata-write-doors0461b3acheck:eager-closuredist; left to CIRepo-wide
pnpm lintand the fullpnpm testare CI's runs.Patch round (ruling
5831744213, head8fe6d58)The maintainer ruled B on this PR's open question (recorded on objectui#6900 as
5831744213): ruling5749269225's 「stays green and unchanged」 binds the objectui#6980 pin's Save-gate asymmetry, not its tooltip wording.ResourceEditPage's inspector-blocked Save title now reads a neutral key,perm.inspector.saveBlocked("Fix the issues shown in the inspector before saving." / "请先修复检查器中列出的问题再保存。").PermissionMatrixEditorandObjectHooksPanelkeepperm.cel.saveBlocked, which block on CEL only.ResourceEditPage.celGate,.defaultGate,.schemaAdvisory(objectui#6980),.serverRefusalGateand.requiredInGatedSection-6900, each with a one-line note naming the ruling. InschemaAdvisoryonly the finder regex and the exact-title assert changed (numstat 3/2);toBeEnabledon the advisory case,toBeDisabledon the CEL fault and the banner checks are byte-identical.ResourceEditPagestill describesinspectorBlockingas a CEL fault only.Acceptance notes (not fixed here, and nothing filed)
The Save button's tooltip still said "Fix the CEL syntax errors before saving." when this new kind holds Save.Resolved in the patch round below.ViewConfigPanel(a third host ofViewVariantInspector) passes noonBlockingIssuesChange, which is already true for CEL. ItsmapObjectFieldsalso buildsObjectFieldInfowithoutrequired. On that host the refusal therefore neither renders nor gates.ViewInspector.tsxstill carries the stale comment "The default (home) path below has no such prop to forward". Out of surface; the matching docblock inViewVariantInspectoris fixed.{ group }section (predicate inherited from the object'sfieldGroups) is not fenced. See H4.visibleWhenhas the same hazard.Changeset
.changeset/6900-required-in-gated-section-refusal.md,patchfor@object-ui/app-shell. It cites the card and ruling 5749269225.Written by the
os-devagent dispatched by thedomain:uiseat 5 PM, sessionhttps://claude.ai/code/session_01KUxVUa7e39aNjhkKi1gsoy, branchclaude/issue-6900-required-in-gated-section-refusal.Generated by Claude Code