Skip to content

feat(app-shell): the view inspector refuses an object-required field inside a predicate-gated form section (objectui#6900) - #10589

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-6900-required-in-gated-section-refusal
Sep 25, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-6900-required-in-gated-section-refusal

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

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 visibleWhen reads identity, feature, host or page state. The refusal is a blocking issue in the inspector's existing onBlockingIssuesChange channel, 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. validateMetadataDraft stays advisory, and every asymmetry assertion of the objectui#6980 pin ResourceEditPage.schemaAdvisory.test.tsx is byte-unchanged and green. Only its Save-title string moved, in the patch round below, on the maintainer's ruling 5831744213. Nothing in objectstack, 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.

⚠️ The SPLIT ruling comment 5486949502 answers HTTP 404 today; a control read of 5749269225 in the same act answered 200. I worked from the quotes that survive in ruling 5749269225 ("rows 7–11 only — identity / feature / host / page-state predicates") and in 5534977209.

Files

file change
inspectors/requiredInGatedSection.ts (new) Reads the form body's sections, each section's predicate and its members. Reads the predicate's roots with the parser, then returns one issue per (section, object-required member) pair when a fenced root is present. Also exports the hook the inspector calls.
inspectors/ViewVariantInspector.tsx Adds a second blocking term for the form family, beside the list-only CEL term (not in place of it), reported through the same effect. Renders the refusal. Refreshes the prop docblock, which said the home path had no channel.
previews/useObjectFields.ts ⭐ The one surface widening: ObjectFieldInfo gains an optional required. It is set to true only when the object's field def says required: true, read from the def the normalizer already reads. Other fields keep the old shape.
i18n.ts Six new engine.inspector.view.gatedRequired.* keys, in both tables (en, zh), in their own rows after engine.inspector.view.noSchema. They stay clear of the engine.form.* rows objectui PR 10551 edits.
3 test files, 1 changeset See below.

Premise (1): can the inspector channel see the object's required flag? Yes, on the path the inspector already uses.

  • The inspector already loads the bound object's catalog through useObjectFields(binding.value, objectFieldsOverride). That calls MetadataClient.get('object', NAME), which unwraps the { type, name, item } envelope and returns the object document itself.
  • The server serves that item with every field def whole. objectstack packages/runtime/src/domains/meta.ts answers GET /meta/object/NAME through maskObjectSchema. applyObjectSchemaMask removes an unreadable field whole and never removes a key inside a def. FieldSchema.required is a declared spec key (z.boolean().default(false)).
  • readFields (previews/object-fields-io.ts) strips only RETIRED_FIELD_KEYS, which is indexed, referenceTo and isSystem (pinned by retired-field-key-tombstones.test.ts). required passes through. The object designer's own required toggle (ObjectFieldInspector) reads the same def.required off the same served document.
  • One boundary: a required field that FLS makes unreadable to the author is missing from the catalog, so no refusal fires for it. The failure goes the quiet way (no refusal), never the false way.

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/.cjs and JSON.parse for .json, over git ls-files. It counts a section by SHAPE: an object literal carrying fields plus visibleWhen or visibleOn. It resolves the predicate through literals, cel(...) calls, { source } envelopes, tagged templates, const bindings, parameter defaults and, for a parameter with no default, the call sites of its function. It reads the predicate's roots with collectCelRootIdentifiers.

population files scanned section-shaped literals gated on a fenced root in authored (non-test) form views
objectui 6105 78 22 (all in test files) 0
objectstack 7678 66 3 (all in test files) 0
hotcrm (4db37cd, shallow clone) 532 0 0 0
  • Positive control, hit: all three objectui#6237 fixture files appear. Their predicate is bound to a variable (visibleWhen: GATE, or a gate parameter fed GATE at the call site), and it resolves to 'sales_manager' in current_user.positions. Counts: tabbedFormSectionPredicate-6237 6, fieldtab-visiblewhen-6237 3, section-grouping-6236 1. That is 10 literals in 3 files, the same as the last reading. The fixtures are untouched.
  • objectui's 14 non-test hits are renderer pass-through code (visibleWhen: section.visibleWhen in ObjectForm, TabbedForm, ModalForm and siblings) plus one zod describe string. None is authored metadata.
  • objectstack's non-test hits are metadata-editing forms (*.form.ts, data root) and conversion-registry examples (record root).
  • hotcrm's population is not empty: 10 view files declare sections:, and all 28 of their predicate lines are field-level (visibleOn).
  • ⇒ 0 authored production form views place an object-required field in a predicate-gated section. As the card says, the fence lands anyway, because it guards future generated forms.

Mechanism hypotheses: readings

  • H1, confirmed. A form variant reported nothing: the CEL term is gated !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.
  • H2, confirmed, with the parser. The roots are read by @objectstack/formula's collectCelRootIdentifiers. That is the export @object-ui/core's rowPredicateCanon uses, and the one the server's closed-root sites use. It is loaded lazily and destructured in the callback, the same way celAuthoring.ts loads 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.
    • How I read the data.* split (row 6 vs row 10): as row 6, so no fence. Row 10 needs a runtime tier that binds data as the host's data-source adapter, and on this tree none does. buildExpressionScope publishes none (objectui#8166), and SchemaRenderer dropped its data: dataSource (objectui#9308, option B). What remains is the row spelling, which @objectstack/lint already teaches as "Rewrite data.KEY as record.FIELD". Fencing it would push an author to delete a predicate they only need to respell.
    • ⚠️ A fact the step-1 table predates: app is not bound at any runtime tier either (objectui#8155). So at BASE a row-9 section predicate faults open to a VISIBLE section. app stays fenced because the ruling names host predicates. The refusal is still correct advice there, because such a predicate is inert.
    • Also: 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.
  • H3, confirmed. ResourceEditPage wires onBlockingIssuesChange into both the scoped InspectorComponent and the DefaultInspectorComponent branch. ViewInspector forwards it, and ViewDefaultInspector spreads it. The pins cover both hosts.
  • H4, the spellings covered:
    • sections[], or the legacy groups[] alias when sections is ABSENT. This is the precedence of the spec's own foldFormGroupsIntoSections: sections wins when present, an empty array included.
    • visibleWhen as a bare CEL string or a { dialect, source } envelope. A non-cel dialect is not read.
    • Members as name strings or { field } entries (the spec's FormFieldSchema key).
    • All six layout arms (simple, tabbed, wizard, split, drawer, modal) read the same sections[], so none is special-cased.
    • Not covered: a { group } section, whose predicate is inherited from the object's fieldGroups rather than written on the view. Reaching it would need a second catalog widening (fieldGroups plus field group), which is not authorized here. See Acceptance notes.
  • H5, done. The keys are in both locale tables the file carries, in their own rows.

Pins, and the base-red proof

  • ResourceEditPage.requiredInGatedSection-6900.test.tsx, run once per host (describe.each default / scoped):
    • Main pin: Save stays disabled after an edit on an identity-gated section holding the object-required salary, and the refusal is on screen.
    • Controls: the field not required on the object saves; a record.status == 'sent' gate saves.
    • Only the canvas is stubbed. The object catalog comes through the real useObjectFields, and the roots through the real parser.
  • inspectors/ViewVariantInspector.requiredInGatedSection-6900.test.tsx, run on the scoped and home paths:
    • One blocking issue, with the copy asserted: the field, "is required on object", the section, the predicate source and the three remedies.
    • Controls: a record gate, a not-required field and the field moved out of the section each draw 0.
  • inspectors/requiredInGatedSection.test.ts: every step-1 row, in both directions, and each spelling above.
leg tree result exit
BASE 08c15a2 (tests-only commit on 9cbe4db), host pin copied byte-identical (cmp) from 5a8db23 Tests 4 failed | 10 passed (14): the 4 main pins fail (toBeDisabled() on the host pin, expected +0 to be 1 on the inspector pin); the controls pass 1
AFTER 5a8db23, clean tree Tests 39 passed (39) 0
FINAL 0461b3a (after merging origin/main at 6ea68e6), clean tree new pins, the objectui#6980 pin, ViewVariantInspector.celGate / .homeGate and ResourceEditPage.defaultGate: Tests 49 passed (49) 0

Gates (local, targeted)

gate tree exit
pnpm exec vitest run packages/app-shell/src/views/metadata-admin/ as --shard=1/4, 2/4, 3/4 5a8db23 (the merge brought in no metadata-admin file) 0, 0, 0 (76 files each: 893 passed + 1 skipped, 802, 1029)
the same, --shard=4/4 0461b3a 0 (76 files, 738 tests)
pnpm turbo run type-check --filter @object-ui/app-shell (includes tsc -p tsconfig.test.json; --listFilesOnly lists all 3 new test files) 5a8db23, then 0461b3a 0, 0
pnpm exec eslint on this branch's 7 .ts/.tsx files (diffed against the merge parent 6ea68e6) 0461b3a 0 (0 errors; 4 warnings, all pre-existing, the same 4 on BASE)
check:i18n-keys, check:i18n-drift ("6 key(s) added"), check:i18n-dead-keys (no new key listed), check:i18n-designer-parity 0461b3a 0 each
check: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) 0461b3a 0 each
check:changeset-claims (report-only; 3 pending bodies name i18n.ts, and each is about keys this diff does not touch, so all still hold), check:pending-changeset-literals, node scripts/check-changeset-presence.mjs 0461b3a 0 each
check:unreferenced-sources, check:phantom-deps, check:designer-field-key-parity, check:handler-key-reads, check:metadata-write-doors 0461b3a 0 each
check:eager-closure n/a NOT MEASURED: needs a console build dist; left to CI

Repo-wide pnpm lint and the full pnpm test are CI's runs.

Patch round (ruling 5831744213, head 8fe6d58)

The maintainer ruled B on this PR's open question (recorded on objectui#6900 as 5831744213): ruling 5749269225'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." / "请先修复检查器中列出的问题再保存。"). PermissionMatrixEditor and ObjectHooksPanel keep perm.cel.saveBlocked, which block on CEL only.
  • Flipped title strings: ResourceEditPage.celGate, .defaultGate, .schemaAdvisory (objectui#6980), .serverRefusalGate and .requiredInGatedSection-6900, each with a one-line note naming the ruling. In schemaAdvisory only the finder regex and the exact-title assert changed (numstat 3/2); toBeEnabled on the advisory case, toBeDisabled on the CEL fault and the banner checks are byte-identical.
  • One new pin, on both hosts: the Save title held by this refusal is the neutral inspector copy.
  • Still stale, left alone: the JSX comment above the Save button in ResourceEditPage still describes inspectorBlocking as 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.
  • The runtime ViewConfigPanel (a third host of ViewVariantInspector) passes no onBlockingIssuesChange, which is already true for CEL. Its mapObjectFields also builds ObjectFieldInfo without required. On that host the refusal therefore neither renders nor gates.
  • ViewInspector.tsx still carries the stale comment "The default (home) path below has no such prop to forward". Out of surface; the matching docblock in ViewVariantInspector is fixed.
  • A { group } section (predicate inherited from the object's fieldGroups) is not fenced. See H4.
  • Not in this card's scope: an object-required field hidden by its own FIELD-level visibleWhen has the same hazard.

Changeset

.changeset/6900-required-in-gated-section-refusal.md, patch for @object-ui/app-shell. It cites the card and ruling 5749269225.

Written by the os-dev agent dispatched by the domain:ui seat 5 PM, session https://claude.ai/code/session_01KUxVUa7e39aNjhkKi1gsoy, branch claude/issue-6900-required-in-gated-section-refusal.


Generated by Claude Code

…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
@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 3 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/6310-designer-formula-key-retired.md

  • names packages/app-shell/src/views/metadata-admin/i18n.ts → packages/app-shell/src/views/metadata-admin/i18n.ts — edited by this change

    Not touched: designer.field.formula ('Formula (CEL)') in packages/app-shell/src/views/metadata-admin/i18n.ts, a different and live key belonging to metadata-admin's ObjectFieldInspector — the surface that still authors formula expressions.

.changeset/7125-dashboard-empty-state-keys-retired.md

  • names packages/app-shell/src/views/metadata-admin/i18n.ts → packages/app-shell/src/views/metadata-admin/i18n.ts — edited by this change

    Not touched: table.noRows ('No rows to display') and engine.form.noRows (packages/app-shell/src/views/metadata-admin/i18n.ts, read at widgets.tsx) — two different, same-named keys in different namespaces. Nor the comments in WidgetEmptyState.tsx, DatasetWidget.tsx, ObjectDataTable.tsx and PivotTable.tsx that record WHY three widgets with three strings became one shared empty state; the packs' own comment keeps that rationale and now names the retirement instead of a row that is gone.

.changeset/8632-malformed-picklist-option-loud.md

  • names views/metadata-admin/i18n.ts → packages/app-shell/src/views/metadata-admin/i18n.ts — edited by this change

    Two new strings land in the designer's own en / zh tables — the metadata-admin console owns its strings in views/metadata-admin/i18n.ts and is deliberately outside the ten locale packs (packages/i18n/README.md, "Scope — the engine.* carve-out").

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Compared the checked-out tree with d3df3c071 (merge-base with origin/main): 12 file(s) changed outside .changeset/, read against 1474 pending declaration(s) that publish a body (2057 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3048.6 KB 3104.5 KB
Main entry chunk (gzip) 147.8 KB 350 KB
Entry file index-BCdf-Mpj.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.68KB 6.20KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 545.30KB 130.42KB
core (index.js) 9.22KB 3.71KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 223.91KB 62.28KB
fields (index.js) 258.75KB 65.60KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 51.40KB 14.61KB
plugin-charts (index.js) 74.94KB 20.89KB
plugin-chatbot (index.js) 198.36KB 47.20KB
plugin-dashboard (index.js) 133.50KB 35.37KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 232.59KB 61.50KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 147.92KB 37.83KB
plugin-gantt (index.js) 169.62KB 41.91KB
plugin-grid (index.js) 215.37KB 58.92KB
plugin-kanban (index.js) 48.26KB 15.04KB
plugin-list (index.js) 114.43KB 28.26KB
plugin-map (index.js) 22.42KB 7.38KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.64KB 8.94KB
plugin-tree (index.js) 10.52KB 3.69KB
plugin-view (index.js) 87.31KB 21.78KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 114.58KB 37.60KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.78KB 2.56KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

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>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3049.5 KB 3104.5 KB
Main entry chunk (gzip) 147.9 KB 350 KB
Entry file index-B4DnkMIP.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.68KB 6.20KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 546.37KB 130.65KB
core (index.js) 9.22KB 3.71KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 223.91KB 62.28KB
fields (index.js) 259.41KB 65.82KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 51.40KB 14.61KB
plugin-charts (index.js) 74.94KB 20.89KB
plugin-chatbot (index.js) 198.36KB 47.20KB
plugin-dashboard (index.js) 133.50KB 35.37KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 232.96KB 61.65KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 147.92KB 37.83KB
plugin-gantt (index.js) 169.62KB 41.91KB
plugin-grid (index.js) 215.37KB 58.92KB
plugin-kanban (index.js) 48.26KB 15.04KB
plugin-list (index.js) 114.43KB 28.26KB
plugin-map (index.js) 22.42KB 7.38KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.67KB 8.95KB
plugin-tree (index.js) 10.52KB 3.69KB
plugin-view (index.js) 87.31KB 21.78KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 116.21KB 38.14KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.78KB 2.56KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 8fe6d58647225ec949bd556326a9d45d4ea607c2

Reviewed against the diff 6ea68e63e0794d1d393f06a6a712b0dc8b4f253a..8fe6d58647225ec949bd556326a9d45d4ea607c2 (merge-base of refs/review/main and refs/review/pr-10589), 13 files, +939/−18, four commits (08c15a2 tests, 5a8db23 feat, 0461b3a merge of main, 8fe6d58 title patch). Binding texts: card objectui#6900 body; ruling 5749269225 (letter c); maintainer ruling 5831744213 (letter B). The SPLIT comment 5486949502 answers HTTP 404 today (control read of 5749269225: 200), so rows 7–11 are taken from ruling 5749269225's own quote and the step-1 table on the card (5482060308, read only for the row definitions, not as evidence). Spec symbols read from the installed @objectstack/spec@17.4.0 and @objectstack/formula@17.4.0; the shared checkout /home/user/objectui carries no node_modules, so the same pnpm store entry was read, read-only, at /home/user/objectui-issue-10528/node_modules/.pnpm/@objectstack+spec@17.4.0_ai@7.0.65_zod@4.4.3_/. The os-dev-report comment was not used as evidence; every claim below is checked against the diff.

① Derived judgments

Landing site and behaviour (ruling 5749269225, letter c).

  1. RIGHT — the refusal is a blocking issue in the view inspector's existing onBlockingIssuesChange channel. inspectors/ViewVariantInspector.tsx:293-297 adds gatedRequiredIssues as a second term beside the list-only CEL term (celBlocking, gated !isFormFamily at :283-284) and reports the sum through the pre-existing effect at :303-305. The channel already gates Save: ResourceEditPage.tsx:2197 (the Save button's disabled term includes a positive inspectorBlocking), :1794, :1823. Ruling text: 「the refusal lands in the scoped inspector's blocking-issues channel … the channel already licensed to gate Save」 — met.
  2. RIGHT — both hosts carry it. Scoped: ResourceEditPage.tsx:2795 → ViewInspector.tsx:46 forwards onBlockingIssuesChange to ViewVariantInspector. Home: ResourceEditPage.tsx:2814 → ViewDefaultInspector spreads {...props} (ViewInspector.tsx:61); MetadataDefaultInspectorProps.onBlockingIssuesChange exists at default-inspector-registry.ts:55. Not stated by the ruling as required, but the card's hazard is the same on either path; pinned on both.
  3. RIGHT — validateMetadataDraft stays advisory. The diff mentions it only in two docblocks (requiredInGatedSection.ts:21, host test :21); no code path touches it. Ruling: 「validateMetadataDraft stays advisory」 — met.
  4. RIGHT — refusal wording names both facts and the three remedies (card: 「must name both facts … AND … and teach the remedy (move the field, or drop the predicate, or make the field not-required at the object)」). i18n.ts:509-514 (en) / :2494-2499 (zh): the issue string says the field 「is required on object "{object}", but it sits in section "{section}", which is shown only when {predicate}」; remedyMove, remedyDrop, remedyUnrequire are the three remedies. Rendered at ViewVariantInspector.tsx:444-472 with tFormat (i18n.ts:4739-4748 substitutes {name} tokens).
  5. RIGHT, with one read-against-the-tree — the fence covers rows 7, 8, 9, 11 and reads the data spelling as row 6. requiredInGatedSection.ts:77-85 fences roots current_user, user, ctx, os (row 7 and its ADR-0068 aliases, which the spec's own section visibleWhen docblock names), features (8), app (9), page (11). Row 10 (data.* bound as the host adapter) is not fenced; the module note (:36-42) argues no tier binds data as an adapter on this tree, and that holds at the head: providers/ExpressionProvider.tsx:54,116 (no data root, objectui#8166) and packages/react/src/SchemaRenderer.tsx:954-969 (data: dataSource removed, objectui#9308). The parser cannot tell row 10 from row 6 by source, and fencing data would refuse the metadata-form row alias the lint already teaches to respell. Judged an acceptable narrowing whose premise is verified, not a missing ask; recorded so a later tree that re-binds data as an adapter knows the fence does not cover it.
  6. RIGHT — rows 1–6 and 12 draw no issue. inspectors/requiredInGatedSection.test.ts:73-84 pins record, has(record.*), previous, a constant, a bare field, data as row alias, and parent to [], plus an unparsable source. Ruling: 「Rows 1–6 stay served by the build-time lint teaching — ⛔ no fence for them」 — met.
  7. RIGHT — roots are read by the real parser. requiredInGatedSection.ts:224-233 lazily imports @objectstack/formula and destructures collectCelRootIdentifiers (exported, dist/index.js:321,2318; return shape { ok, roots } matches the local CollectRoots type at :220). @objectstack/formula ^17.0.0 is already an app-shell dependency (package.json:84). An unparsable or blank source draws nothing (:235-243, :129-130), leaving syntax to the CEL gates.
  8. RIGHT — the section spellings match the spec. sections wins over groups when present, an empty array included (readFormSections :148) — identical to foldFormGroupsIntoSections in spec 17.4.0 (dist/identity/index.js:1626-1631). visibleWhen as a bare string or a { dialect, source } envelope (:120-131). Members as name strings or { field } entries (:134-143; FormFieldBaseSchema.field, spec dist/index.js:17165). Every layout arm reads sections[]: the spec's form view declares sections/groups only (:17696, :17704) and refuses a steps key with 「A wizard's steps are its sections」 (:17606). A { group } section is not fenced; the spec refuses visibleWhen beside group (derivedKeys at :17518), so such a section carries no predicate on the view and the card's letter (「a form section carrying a visibleWhen predicate」) is not left open; the object-side fieldGroups predicate is disclosed in the PR body as out of scope.
  9. OBSERVATION, not a defect — the fence fires for both keys of FORM_FAMILY (view-variant-model.ts:24: form and detail), the designer's pre-existing definition of a form-family body. The ctx and os roots are fenced whole (any member), which is broader than the prose's ctx.user / os.user but consistent with the step-1 criterion (nothing under ctx or os is restatable as requiredWhen).

Premise (1) — the object's required flag reaching the inspector. RIGHT, carried on the existing path. previews/useObjectFields.ts:95 adds ...(e.def.required === true ? { required: true } : {}) to the record already mapped from readFields(obj.fields) off client.get('object', name) (:81-88), the same call ViewVariantInspector.tsx:250 already makes. readFields strips only the tombstones whose metadataAdminFieldsReadDoor site is true (packages/types/src/internal/retired-field-keys.ts: indexed, referenceTo, isSystem; formula and sortOrder are false at that site); required is not a tombstone and passes. FieldSchema.required is a declared spec key (dist/index.js:1661, z.boolean().default(false)). No new fetch, no new client method, no new prop — the ruling's 「⛔ do not build a new data path」 is honoured. The widening is one optional member on the internal ObjectFieldInfo (:26-38), which is not on packages/app-shell/src/index.ts.

Premise (2) — census. Not re-run here. The positive-control fixture files exist at the head with the identity gate (packages/plugin-form/src/__tests__/tabbedFormSectionPredicate-6237.test.tsx, packages/components/src/renderers/form/__tests__/fieldtab-visiblewhen-6237.test.tsx, …/section-grouping-6236.test.tsx), which is the control the ruling's last reading names (10 literals in 3 files, 0 authored production views). The fixtures are untouched by the diff.

Ruling 5831744213 (letter B) — the Save title. RIGHT. ResourceEditPage.tsx:2205 now reads perm.inspector.saveBlocked for the inspector-blocked state; the key sits in both locale tables (i18n.ts:1299, :3312). PermissionMatrixEditor.tsx:949 and studio-design/ObjectHooksPanel.tsx:284 keep perm.cel.saveBlocked, as the ruling records. The five title flips are exactly the files the ruling names.

Existing pins edited — load-bearing, not weakened.

  • ResourceEditPage.schemaAdvisory.test.tsx (objectui#6980): numstat 3/2. Only the finder regex (:180), the exact-title assert (:228) and one comment line moved. The asymmetry assertions are byte-identical between base and head — toBeEnabled at :212/:220 (advisory Zod issue leaves Save enabled), toBeDisabled at :226 (CEL fault blocks), and both metadata-validation-banner / SERVER_ONLY_KEY checks (:185-186, :228) — verified by diffing the assertion lines with line numbers stripped. Ruling 5831744213: 「The objectui#6980 pin's ASSERTIONS about the asymmetry are ⛔ unchanged」 — met; 「stays green」 — CI green at head.
  • ResourceEditPage.celGate.test.tsx, .defaultGate.test.tsx: finder regex plus the exact-title assert flipped to the neutral copy (the old string no longer renders on this path, so the old assert would be red). The toBeDisabled on the CEL fault is untouched.
  • ResourceEditPage.serverRefusalGate.test.tsx: finder regex only (2/1).
    None of the four loosens a toBeDisabled/toBeEnabled or drops an assertion; each still asserts one exact title, now the ruled one.

New pins — can they fail?

  • inspectors/requiredInGatedSection.test.ts: the eight fenced rows assert the exact issue object (roots, field, label, section, predicate); removing a root from GATED_SECTION_ROOTS turns its row red. The seven unfenced rows and the unparsable source assert []; adding record/previous/parent/data to the fence turns them red. Spellings: bare string, { field } entry, dedupe per section, groups fallback, sections: [] precedence, title fallback order, and predicatesToRead filtering.
  • inspectors/ViewVariantInspector.requiredInGatedSection-6900.test.tsx, on scoped and home: the lit leg waits for the last onBlockingIssuesChange call to be 1 and asserts the copy (field, 「is required on object」, section, predicate, three remedies). On the base tree ObjectFieldInfo.required is absent, so the catalog holds no required field and the count stays 0 — the leg is red there. The three negative legs assert the last call is 0 (an uncalled spy yields undefined, so the assert cannot pass vacuously) and the alert is absent.
  • ResourceEditPage.requiredInGatedSection-6900.test.tsx, on default and scoped: the lit leg dirties the draft and asserts Save disabled with the alert on screen; the not-required control asserts Save enabled after the same dirtying, which proves the disabled state in the lit leg is the refusal and nothing else. A second leg pins the neutral title. Negatives read after settle() (awaits the parser import and one macrotask; the verdict chain is microtask-bound: client.get → setState → effect → dynamic import → setParsed), so the green is observed after the verdict lands. Only the canvas is stubbed; the catalog goes through the real useObjectFields and the roots through the real parser.

Asked but missing: none found.
Done but not asked: none. The ViewVariantInspector prop docblock refresh (:114-127) corrects a sentence that was false at the base (the home path does carry the channel) and is within the file surface.

Disclosed and left alone (verified true at the head, not defects): ViewConfigPanel.tsx passes no onBlockingIssuesChange and mapObjectFields (:96-108) builds ObjectFieldInfo without required, so the runtime third host neither renders nor gates the refusal (already true for CEL). ResourceEditPage.tsx:2182-2183 JSX comment still describes inspectorBlocking as 「a CEL predicate that does not parse」. ViewInspector.tsx:44-45 still says the home path 「has no such prop to forward」. Both comments are stale prose, not behaviour.

② Semver level

.changeset/6900-required-in-gated-section-refusal.md: '@object-ui/app-shell': patch. The diff changes published source under packages/app-shell/src/ (a fixed-group package), so a bump is owed and an empty frontmatter would be wrong; patch declares one. AGENTS.md 版本号策略 forbids major and routes even breaking changes to minor; it sets no floor between patch and minor for a feature, and the CI 「Changeset Bump Policy」 (scripts/check-changeset-no-major.mjs) passed. patch is therefore admissible under the repo's rules; conventional practice would more often put a new refusal at minor, but no rule here binds that. This is not a docs-only change, so the empty-frontmatter rule does not apply. No content/docs/** or README prose is added by the diff.

Changeset prose, sentence by sentence:

  1. 「The metadata designer's view inspector now refuses a form view that puts an object-required field inside a section gated on identity, feature, host or page state」 — TRUE (GATED_SECTION_ROOTS; blocking channel; both hosts).
  2. 「When a form section's visibleWhen reads current_user (or its aliases user, ctx.user, os.user), features, app or page, and the section holds a field the bound object declares required, the inspector reports one blocking issue per such field through its existing blocking-issues channel」 — TRUE; understated in one respect: the code fences the whole ctx and os roots, not only their .user member.
  3. 「The editor's Save stays disabled while any is reported, on both the selected-variant inspector and the no-selection view panel」 — TRUE (ResourceEditPage.tsx:2197; both host wirings; pinned on both).
  4. 「The inspector shows the refusal: it names the field and says it is required on the object, names the section and its predicate, and lists the three fixes」 — TRUE (i18n.ts:509-514; ViewVariantInspector.tsx:444-472).
  5. 「the editor's disabled Save button reads "Fix the issues shown in the inspector before saving." whenever an inspector issue holds it, a CEL fault included, instead of "Fix the CEL syntax errors before saving."」 — TRUE (ResourceEditPage.tsx:2205; celGate/defaultGate pins).
  6. 「The permission-matrix and object-hooks Save buttons keep the CEL wording」 — TRUE (PermissionMatrixEditor.tsx:949, ObjectHooksPanel.tsx:284).
  7. 「The server never evaluates a form section's predicate, so without this the view saves and the form cannot be completed whenever the section is hidden」 — TRUE per the spec's own section visibleWhen docblock (「nothing server-side evaluates a form-view section visibleWhen」); a premise the diff relies on, not something it changes.
  8. 「A section gated only on record, previous, parent, data or a bare field draws no issue; those predicate shapes stay with the build-time lint's requiredWhen teaching」 — TRUE (pinned rows 1–6, 12; a constant also draws nothing, not listed, not false).
  9. 「The live schema check of the draft stays advisory and does not gate Save」 — TRUE (validateMetadataDraft untouched; Client-side Zod validation errors do not gate Save, while inspector-reported errors do — one class of author-time error is advisory, the other blocking #6980 assertions identical).
  10. 「Nothing changes on the server」 — TRUE (app-shell only).

Code docblock prose added by the diff, checked where load-bearing: useObjectFields.ts:30-37 (「the served object document already holds the flag … not a second data path」; 「Hosts that pass an override catalog and leave it out get no such refusal」) — TRUE. ViewVariantInspector.tsx:122-127 (「Both hosts carry it … ViewDefaultInspector (home) spreads MetadataDefaultInspectorProps, whose contract has declared the same member」) — TRUE that the member exists (default-inspector-registry.ts:55); the 「since objectui#4527 phase 2」 provenance is not verified here and is not load-bearing. requiredInGatedSection.ts:36-42 (data not bound as an adapter on this tree) — TRUE at the head, see ①.5.

③ Boundary flags

  • PR body flags answered. (a) SPLIT comment 5486949502: 404 confirmed today, control 200; the PR worked from the surviving quotes, and so did this review. (b) The Save-tooltip fork the PR raised: ruled B on 5831744213 and executed in 8fe6d58; the Client-side Zod validation errors do not gate Save, while inspector-reported errors do — one class of author-time error is advisory, the other blocking #6980 asymmetry assertions are byte-identical. (c) check:eager-closure 「NOT MEASURED」 locally: the console eager-closure budget is CI's 「Bundle Analysis」 job (performance-budget.yml runs scripts/check-eager-closure-budget.mjs), success at the head. (d) Premise (2) census: control fixtures verified present; the zero itself is the PR's measurement, not re-taken here. (e) H1–H5: each verified against the diff and spec 17.4.0 as recorded in ①.
  • CI at the head 8fe6d58: first poll (11:59Z) 42 runs — 26 success, 3 skipped, 13 in progress. Second poll (12:11Z) 43 runs — 40 success, 3 skipped (Test (coverage), Test (coverage shard …/4), dependabot, all path-filter skips), 0 failure, 0 in progress. The run that appeared between polls is the Test rollup, success.
  • Draft: yes (draft: true), as a dispatched dev PR is expected to be.
  • Clause-②: no holds. No published export moves: packages/app-shell/src/index.ts is not in the diff; MetadataInspectorProps and MetadataDefaultInspectorProps are untouched; the widened ObjectFieldInfo is internal to views/metadata-admin/previews and gains only an optional member. No schema or validator accept set moves: nothing in @objectstack/* or packages/objectql changes and the server stays the sole authority. What narrows is the designer's own Save gate by one ruled term, on a channel already licensed to block — the card's deliverable, which the ruling anticipated the claiming seat would re-judge from the diff.
  • Main moved under the PR's files: no. main advanced 20 commits since the merge-base 6ea68e6; git diff --stat 6ea68e6 refs/review/main -- (the 13 PR paths) is empty. The PR is mergeable: true, state behind.

Implemented-by: claude/issue-6900-required-in-gated-section-refusal
Reviewed-by: session_01KUxVUa7e39aNjhkKi1gsoy

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 25, 2026 12:20
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 25, 2026
Merged via the queue into main with commit 402a266 Sep 25, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-6900-required-in-gated-section-refusal branch September 25, 2026 12:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Form authoring: refuse an object-required field placed inside a predicate-gated section at save time (ruled option B of objectstack#13252)

2 participants