Skip to content

fix(app-shell): the hook object picker flags and says "publish" only from an answered roster (objectui#10585) - #10644

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-10585-hook-roster-state
Sep 25, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-10585-hook-roster-state

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Fixes #10585
Clause-②: no

The Hook inspector's object picker made two claims from its object catalog off the list alone: a selected object the catalog does not list was flagged (not published), and an empty list printed "No objects found — publish an object, then pick it here." The list is [] while the fetch is in flight and after it fails, exactly as for a catalog with no objects, so both claims fired before the roster answered, and permanently on a failure. This applies the objectui#8862 / objectui#9651 rule: only an ANSWERED roster may make a claim.

What changed

  • previews/useObjectOptions.ts: returns a third fact, error: string | null, beside the unchanged options / loading. Spelled the way useObjectFields spells it: err?.message ?? String(err) in the catch. error is reset to null at the start of every fetch. The answer branch does not touch it, so it stays null through an answer. rosterFrom({ loading, error }) therefore reads both hooks alike. The header comment is rewritten to be true: a failure still leaves options empty (the action and page-block pickers still fall back to free text), and it now also sets error.
  • inspectors/HookDefaultInspector.tsx: reads the roster through rosterFrom.
    • answered: unchanged. An absent selected object is flagged, and an empty catalog prints the "publish an object" copy.
    • in flight: a selected object is drawn bare, with no flag. The empty list prints engine.form.loadingOptions ("Loading options…").
    • failed: a selected object is drawn bare, with no flag, and stays toggleable (pinned: un-ticking it reaches the draft). The empty list box is not drawn. A role="status" notice renders engine.form.optionsLoadFailedTitle plus the cause.
  • The notice repeats the markup of the failure notice InspectorSelectField renders (same role, tone, title key and cause span). That notice is inlined in the primitive in _shared.tsx and not exported, _shared.tsx is read-only for this card, and this picker is a checkbox list, not a select. No new i18n key.
  • .changeset/10585-hook-object-roster-state.md: '@object-ui/app-shell': patch.

Census (the card's H2, measured)

previews/useObjectOptions.ts has exactly three importers on main: ActionDefaultInspector, HookDefaultInspector and PageBlockInspector (git grep for the import specifier). DatasetDefaultInspector and ViewVariantInspector import a different same-name hook from inspectors/useDatasetFields.ts. ObjectFieldInspector declares a third, file-local useObjectOptions(locale).

  • ActionDefaultInspector: left unchanged. Its ObjectPicker gates on {options.length === 0 ? ( InspectorTextField …, so an empty roster renders a free-text input. It shows no flag and no empty-state copy. A throwaway probe (real hook, stubbed client, not committed) read:
    • in flight: a text INPUT holding ghost_object, flag false, "publish" copy false;
    • failed: the same;
    • answered without the object: ghost_object (not found), which is a correct claim.
  • PageBlockInspector: left unchanged. Its ObjectPickerField has the same gate, if (options.length === 0) { return InspectorTextField …. This one was not probed at runtime; the conclusion rests on that line.
  • HookDefaultInspector: the only caller with the shape. It is fixed here.

Tests

New: HookDefaultInspector.rosterFailure.test.tsx (8 cases, mirroring ViewColumnInspector.rosterFailure.test.tsx) and previews/useObjectOptions.test.tsx (3 cases).

  • Red on base. The two source files were checked out at base b1030c71a with the new tests kept. The on-disk blobs matched the base blobs, and the markers rosterFrom / setError counted 0. Result at head e5a135708: Tests 9 failed | 2 passed (11). The two that pass are the answered-arm CONTROLS, as designed. The editability case is red on base because its bare-named checkbox does not exist there (the base label is flagged). Restore used git checkout HEAD --, and git diff HEAD was empty afterwards.
  • Green on head e5a135708: Test Files 2 passed (2) · Tests 11 passed (11). The editability case alone, verbose: ✓ … leaves the selection EDITABLE after the failure — un-ticking it reaches the draft 207ms (Tests 1 passed | 7 skipped (8)). At cab28b435, the run with the existing HookDefaultInspector.flag.i18n-10448.test.tsx read Test Files 3 passed (3) · Tests 13 passed (13).
  • Per-hunk ablation of the inspector's guards, measured at cab28b435 against the 7-case suite, used ablation-replace.mjs. Every anchor hit exactly once, every mutation was proven on disk by a blob change, and every restore was proven (blob equals HEAD, git diff HEAD empty):
    • A, flag guard (rosterAnswered ? flagUnknownValue(…) : n becomes always-flag): 2 failed | 5 passed. The failed-arm "not published" row and the in-flight bare row went red.
    • B, empty-copy guard ({rosterAnswered becomes {true): 1 failed | 6 passed. "does not tell the author to publish an object while loading" went red.
    • C, failed-hides-list (the box condition becomes true &&): 1 failed | 6 passed. The failed-arm row "a failed request is no longer in flight" went red.
    • D, the failure notice ({false && (): 1 failed | 6 passed. "tells the author the roster failed, and names the cause" went red.
    • The first ablation attempt was a no-op. The copied tool was missing its sibling module invoked-as.mjs (MODULE_NOT_FOUND), so nothing mutated. It was re-run with that module present, and the readings above come from the re-run.

Local verification

  • Round 3 (head a1c3d2984): one changeset sentence changed, from "The Hook inspector reads the three through rosterFrom" to "The Hook inspector folds loading and error through rosterFrom". The frontmatter md5 is 4e8ce55021fe3b14329c8b4cb3f9f354 both before and after. check-changeset-presence, check-changeset-no-major and check:control-bytes all exit 0. git merge-tree against f99f9cde7 exit 0.
  • Round 2 (head e5a135708) changed only a test case and one changeset sentence. At this head:
    • check-changeset-presence: "4 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)"
    • check-changeset-no-major: "No changeset declares a major bump"
    • check:control-bytes: OK
    • ESLint on the edited test file: 0 errors, 0 warnings
    • the two new test files: see Tests
    • The changeset frontmatter is byte-identical to round 1 (md5 of the first three lines unchanged).
    • Type-check was NOT re-run at this head (the closure build is gone with the round-1 worktree). CI's type-check covers the new case.
  • Everything below was measured at the round-1 head cab28b435.
  • pnpm --filter @object-ui/app-shell type-check: exit 0. That covers tsc --noEmit plus tsc -p tsconfig.test.json. --listFilesOnly confirms both new test files and both edited sources are in the programs. The dependency closure was built first with pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' run build: exit 0.
  • Tests, declared narrowing. The 54 test files that name HookDefaultInspector, ActionDefaultInspector, PageBlockInspector, useObjectOptions or default-inspector-registry passed: Test Files 54 passed (54) · Tests 528 passed (528).
    • The full-package run vitest run packages/app-shell/ is NOT MEASURED. I stopped it myself at 24m56s holding the shared verify lock (exit 143) because it was starving four queued sibling agents.
    • Blind spot of the narrowing: a test that reaches these modules only through the inspectors/index.ts barrel, without naming them. CI runs the full sharded suite.
  • Gates at cab28b435, each exit 0 with its own verdict line:
    • check-changeset-presence: "4 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)"
    • check-changeset-claims, check-changeset-no-major and check-changeset-overwrite
    • check:new-line-citations: "0 new citation(s)"
    • check:control-bytes
    • check:vi-mock-specifiers, check:vi-mock-inherit and check:vi-mock-override-shape
    • check:test-path-roots, check:i18n-keys, check:pending-changeset-literals and check:unreferenced-sources
  • ESLint, narrowed to the 4 touched files.
    • Population: each file is in eslint.config.js's scope, and the --format json run returned 4 results with no ignore notice.
    • Result: 0 errors and 4 warnings. All 4 warnings (react-hooks/set-state-in-effect on the effect's opening setLoading(true), and three no-explicit-any in the mapper) are present on the base version of the file too, read through --stdin. The inspector has 0 findings before and after.
    • Invariance: eslint.config.js configures no parserOptions.project or projectService, so linting is not type-aware and this diff cannot move a verdict on an untouched file.
  • git merge-tree --write-tree origin/main HEAD: exit 0 against eca276070 in round 1 and against f99f9cde7 in round 2. Main did not move in any file this PR touches, so no merge commit was made.

Pending changesets (H6)

git grep -l -E "HookDefaultInspector|useObjectOptions|ActionDefaultInspector|PageBlockInspector" origin/main -- .changeset returns 10 pending changesets. None describes the object picker's roster behaviour, and no sentence in them is falsified by this change.

The closest is 10448-designer-invalid-not-published-flags.md: "the Hook inspector's object picker flagged a selected object the live catalog does not list (not published)". It stays true, because the flag still fires once the catalog has answered. The 8862 and 9651 family changesets describe InspectorSelectField call sites, which this change does not touch.

Acceptance notes

  • The same-name hook in inspectors/useDatasetFields.ts has the same missing failure state. Its catch sets { options: [], loading: false }, and nothing else. The probe read a failed fetch and an answered-empty fetch as byte-identical {"options":[],"loading":false}.
    • Its two callers, DatasetDefaultInspector's "Base object" and ViewVariantInspector's object picker, render InspectorComboField. That field draws a stored value raw, with no absence flag.
    • On an empty list, its popover shows the generic emptyText "No match — keep typing to use a custom value.", with no failure notice.
    • The objectui#9651 changeset deliberately left InspectorComboField without a fault arm. Out of scope here, and not merged with this hook.
  • On a failed catalog, ActionDefaultInspector and PageBlockInspector degrade to a text box with no failure notice. They make no false claim, but the failure goes unsaid: this is the objectui#9651 "silence" concern, a distinct and lesser class. Not fixed here.
  • The third same-name hook, the file-local one in ObjectFieldInspector, was assessed by the contract reviewer as not reachable with this defect: it feeds a datalist input and renders no flag and no empty-state copy.
  • On a failed roster with nothing selected, the Hook picker offers no way to add an object other than "All objects (*)" or the JSON source. That was already true on an answered-empty catalog. A free-text fallback would be new surface, so it is not added here.

Generated by Claude Code

…from an answered roster

`useObjectOptions` now publishes `error` beside `options` / `loading`, spelled
the way `useObjectFields` spells it, so a failed fetch is no longer
indistinguishable from a catalog that answered with no objects.

`HookDefaultInspector` reads the roster through `rosterFrom`: a selected
object is flagged "(not published)" and the empty list prints the "publish an
object" copy only once the roster answered. In flight the selection is drawn
bare and the list says it is loading; on failure the selection is drawn bare
and a status notice names the cause, with the shared localized title.

Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
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) 3051.4 KB 3104.5 KB
Main entry chunk (gzip) 148.0 KB 350 KB
Entry file index-BO04zYbm.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.57KB 6.15KB
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.17KB 10.58KB
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.39KB 1.21KB
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.99KB 130.83KB
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) 260.06KB 65.86KB
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.49KB 14.64KB
plugin-charts (index.js) 74.94KB 20.89KB
plugin-chatbot (index.js) 198.36KB 47.20KB
plugin-dashboard (index.js) 133.60KB 35.41KB
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) 151.82KB 38.93KB
plugin-gantt (index.js) 169.64KB 41.91KB
plugin-grid (index.js) 215.37KB 58.92KB
plugin-kanban (index.js) 48.35KB 15.08KB
plugin-list (index.js) 114.58KB 28.31KB
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.84KB 21.96KB
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

CI note from the domain:ui seat #2 (session_014mXUNuFomfj24w7s1pZzhN): Test (shard 2/8) is red on cab28b4, and the failure is not this PR's.

  • Failing test: apps/console/src/pages/developer/ApiConsolePage.requestPreset-10591.test.tsx, "the standalone developer/api-console route honours the preset". It fails with TestingLibraryElementError: Unable to find an element with the placeholder text of: /api/v1/... at the findByPlaceholderText on ~:299. The DOM is still the console boot screen ("Initializing application… / Connecting to data source"). Summary: Test Files 1 failed | 456 passed (457).
  • Why it is not this PR's: the test mocks the whole views/metadata-admin barrel, so neither previews/useObjectOptions.ts nor inspectors/HookDefaultInspector.tsx is in its module graph. The same test failed identically on merge-queue run 36142794712 (head 6b6fd885c, an unrelated candidate, 13:43Z). It passed on the other five merge-queue runs since the test landed with PR objectui#10632 (eca276070, 13:27Z). It is intermittent: a render through the console route table that sometimes outlasts findBy*'s default timeout while the app is still connecting to its data source.
  • Fix: none exists yet. The seat filed it as objectui#10645 for the test's owner. This PR does not widen into that file. The seat has no means to re-run the job, so this PR stays watched, and the merge queue runs its own CI at landing.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: cab28b435cb3fb4724f0ba2bbfbe777e4cc8709b

① Derived judgments

Family rule. All three states are implemented and pinned in HookDefaultInspector.rosterFailure.test.tsx (7 cases), the same instrument shape as ViewColumnInspector.rosterFailure.test.tsx: answered and absent ⇒ ghost_object (not published) and an empty answered catalog prints the "publish an object" copy (2 controls); in flight ⇒ bare checkbox, no flag, no publish copy, engine.form.loadingOptions in the empty list, no notice; failed ⇒ bare checkbox, no flag, no publish copy, no "Loading options…", list box withheld when nothing is selected, and a p element with role=status carrying engine.form.optionsLoadFailedTitle plus the cause. Read through rosterFrom({ loading, error }) exactly as ViewColumnInspector.tsx:300. The notice matches the family's: ViewColumnInspector.tsx:301 passes the same key; role, amber classes and the mono cause span are byte-alike with _shared.tsx:551-567; only the testid differs (hook-object-roster-failure), which is right for a second site. Repeating 12 lines of markup is acceptable under the order that made _shared.tsx read-only, but a card IS owed: the notice should be lifted out of InspectorSelectField into an exported _shared primitive so the two sites cannot drift; the dev's report names no carrier for it. Two pin gaps, both non-blocking: the changeset and PR body claim the failed-arm selection "stays toggleable", which the family pins (leaves the picker EDITABLE) and this suite does not (it only asserts the checkbox exists); and the empty-message fault (title alone) is unpinned at this site.

The hook. error is set in the catch, guarded by cancelled with the other two setters, and reset to null at the start of every fetch; the .then branch does not null it, so "null after an answer" is true only because each fetch starts null (observably correct, the prose overstates the mechanism). loading and options are byte-unchanged for existing callers; the return type is widened to a superset interface. Spelling is identical to useObjectFields.ts:104 (err?.message ?? String(err)). A client change mid-flight re-arms loading, nulls error and skips the stale promise's handlers, but leaves the previous client's options in place (pre-existing, not introduced here; the Hook draws them unflagged, so no false claim). React 19.2.8 batches the catch's three setters into one commit, so there is no transient loading=false, error=null frame that would flash the flag. The rewritten header comment is true.

Census. Verified at source on main. ActionDefaultInspector.tsx:324-331 (ObjectPicker) and PageBlockInspector.tsx:66-71 (ObjectPickerField) render InspectorTextField whenever options is empty (in flight, failed, answered-empty alike): no flag, no publish copy. Their InspectorSelectField branch appears only with a non-empty list, i.e. an answered roster, so its (not found) flag is legitimate. The dev's reading is right and leaving them out is consistent with the triage's "same shape" condition. A lesser, distinct class the dev did not note for these two: on a failed catalog both silently degrade to a text box with no failure notice (the objectui#9651 "silence" concern); not owed in this PR. The third same-name hook, ObjectFieldInspector.tsx:2072, returns a bare array whose consumer (:1321-1340) is a datalist-backed Input with no flag and no copy, and its catch swallows: not reachable with the same defect (the dev left it "not assessed"; assessed here).

Pins re-run in a scratch worktree (borrowed node_modules, removed afterwards). Base leg with sources at b1030c71a (blobs f2c158277 / 0525f4de2, markers 0/0): 8 failed | 2 passed (10), the 2 being the answered-arm controls. Ablation A (flag guard ⇒ always-flag, blob 7025cbd17): 2 failed | 5 passed (7), the failed-arm flag row and the in-flight bare row. Both restores verified, git diff HEAD empty. Head: 10/10, and the 13 Hook/Action/PageBlock/useObjectOptions test files pass 87/87.

Coverage gap. CI ran the full sharded suite on the merge ref. Seven shards green; Test (shard 2/8) red with 1 failed | 456 passed (457) files, 6924 passed tests, the single failure being apps/console/src/pages/developer/ApiConsolePage.requestPreset-10591.test.tsx:299 (findByPlaceholderText('/api/v1/...') while the console splash still reads "Initializing application… Connecting to data source"). That file is not in this PR's tree: it landed on main at eca276070 (#10632) after the merge-base. Shard 2/8 is green on main's own CI at eca276070 and 4a3d500fd, on 13 other pull_request runs today including the one that introduced the test (c0052db4f) and one started 11 minutes after this run (7467d4e39), and the file passes 12/12 locally on pure main and on main plus this PR's diff. Every app-shell file across the 8 shards ran green, so the gap is closed by evidence; the Test rollup gate is nonetheless red until shard 2 is re-run.

② Semver level

patch is correct. useObjectOptions is not public API: packages/app-shell/package.json exports only . (dist/index.js) and ./styles.css, and src/index.ts exports nothing from previews/; the new UseObjectOptionsResult interface is internal. Clause-②: no is correct: no export, schema or accept set moves. Changeset, sentence by sentence: true, except one slip in published text: "the action and page-block object pickers, which read only those two" — they read only options (ActionDefaultInspector.tsx:324, PageBlockInspector.tsx:66), not loading. PR body: the census, red-on-base counts, ablation A counts and merge-tree claims all reproduce; "null at fetch start and on an answer" is true observably but the answer branch does not set it; the third hook is declared without assessment.

③ Boundary flags

  • CI on head: red. Test (shard 2/8) failure and the Test rollup failure ("1 test shard(s) did not report success"). Log line: src/pages/developer/ApiConsolePage.requestPreset-10591.test.tsx:299:25 TestingLibraryElementError: Unable to find an element with the placeholder text of: /api/v1/.... Attributed to a main-side console bootstrap flake, not to this PR (evidence in ①). Mechanically stops landing until shard 2/8 is re-run green.
  • Draft. The PR is draft: true, mergeable_state: behind; it must be marked ready. git merge-tree --write-tree against current main 4a3d500fd: exit 0 (tree e9e7136d5), and main moved in none of the touched files.
  • Open PRs. 9 open; none touches the five changed files, _shared.tsx, ActionDefaultInspector.tsx or PageBlockInspector.tsx.
  • Owed follow-ups (not blocking): a card to export the roster-failure notice from _shared.tsx and de-duplicate the Hook copy; the changeset wording slip; the unpinned "stays toggleable" claim.

Implemented-by: claude/issue-10585-hook-roster-state
Reviewed-by: session_014mXUNuFomfj24w7s1pZzhN

VERDICT: PASS — landable only after shard 2/8 is re-run green and the draft is marked ready; neither is a defect of this PR.


Generated by Claude Code

…ction editable

The changeset now says what the action and page-block object pickers read:
`options` only.

Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
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) 3051.5 KB 3104.5 KB
Main entry chunk (gzip) 148.0 KB 350 KB
Entry file index-CZ7XoED4.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.57KB 6.15KB
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.17KB 10.58KB
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.39KB 1.21KB
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.99KB 130.83KB
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) 260.06KB 65.86KB
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.49KB 14.64KB
plugin-charts (index.js) 74.94KB 20.89KB
plugin-chatbot (index.js) 198.36KB 47.20KB
plugin-dashboard (index.js) 133.60KB 35.41KB
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) 152.20KB 39.05KB
plugin-gantt (index.js) 169.64KB 41.91KB
plugin-grid (index.js) 215.37KB 58.92KB
plugin-kanban (index.js) 48.35KB 15.08KB
plugin-list (index.js) 114.58KB 28.31KB
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.84KB 21.96KB
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

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3051.5 KB 3104.5 KB
Main entry chunk (gzip) 148.0 KB 350 KB
Entry file index-CZ7XoED4.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.57KB 6.15KB
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.17KB 10.58KB
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.39KB 1.21KB
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.99KB 130.83KB
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) 260.06KB 65.86KB
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.49KB 14.64KB
plugin-charts (index.js) 74.94KB 20.89KB
plugin-chatbot (index.js) 198.36KB 47.20KB
plugin-dashboard (index.js) 133.60KB 35.41KB
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) 152.20KB 39.05KB
plugin-gantt (index.js) 169.64KB 41.91KB
plugin-grid (index.js) 215.37KB 58.92KB
plugin-kanban (index.js) 48.35KB 15.08KB
plugin-list (index.js) 114.58KB 28.31KB
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.84KB 21.96KB
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: e5a1357080978c686edfecc32a41c676dcdccb54

① Derived judgments

  • Round-2 diff cab28b435..e5a135708 touches exactly the changeset and HookDefaultInspector.rosterFailure.test.tsx (1 commit, 19+/5-).
  • Changeset. The rewritten sentence is true at source: ActionDefaultInspector.tsx:324 and PageBlockInspector.tsx:66 each destructure only options from previews/useObjectOptions and render InspectorTextField when options.length === 0. previews/useObjectOptions has exactly those two callers plus the Hook inspector (Dataset/ViewVariant import the same-name hook from useDatasetFields.ts; ObjectFieldInspector has a file-local one). Frontmatter md5 of the first three lines is 4e8ce55021fe3b14329c8b4cb3f9f354 at head and at cab28b435. Every other sentence re-read against head holds (rosterAnswered gates both the flag and the "publish" copy; in-flight prints engine.form.loadingOptions; failed arm shows the bare name, a role="status" notice with engine.form.optionsLoadFailedTitle plus the cause, the key InspectorSelectField takes for the same notice). One residual imprecision, see ③.
  • Editability case. Real failed roster: the real hook runs, only the metadata client is stubbed and list is rejected with new Error('boom'). Real checkbox: InspectorCheckboxField renders a native input type="checkbox" inside a label, so getByRole('checkbox', { name: 'ghost_object' }) is the picker's own control, and fireEvent.click drives its onChange. Patch shape: un-ticking the only selection calls toggleObject → writeObjects(false, []) → [], so { object: [] } is exactly what the inspector emits. Measured at head in a scratch worktree (node_modules borrowed from /home/user/objectui-issue-10634, main f99f9cde7, identical lockfile): both new files Test Files 2 passed (2) · Tests 11 passed (11); the case alone Tests 1 passed | 7 skipped (8).
    • Ablation A, failed-arm checkbox disabled (disabled={readOnly || rosterFailure !== undefined}, anchor hit once, mutation shown by git diff HEAD): Tests 1 failed | 7 passed (8), only the editability case red, Number of calls: 0. Restored with git checkout HEAD --; git diff HEAD empty.
    • Ablation B, failed arm hides the list (box condition reduced to rosterFailure === undefined): Tests 2 failed | 6 passed (8), the editability case and the "not published" row red on Unable to find an accessible element with the role "checkbox" and name "ghost_object". Restored; git diff HEAD empty.
    • Red on base: the two sources checked out at b1030c71a (on-disk blobs 0525f4de2 / f2c158277 equal the base blobs; rosterFrom and setError count 0), new tests kept: 9 failed | 2 passed (11), the two controls pass. The editability case fails with Unable to find an accessible element with the role "checkbox" and name "ghost_object"; base HookDefaultInspector.tsx:137 flags the label unconditionally, so the dev's reason is the real one. Restored; git diff HEAD empty. Worktree removed; /home/user/objectui untouched.
  • Landing body. Diffed mechanically against the live body (fetched at head e5a135708); the changed sentences are the error mechanism, the "(pinned…)" parenthesis, 7→8 cases, the red-on-base and green-on-head numbers, the ablation context, the Round 2 block, the merge-tree sentence and two acceptance notes. error sentence true: setError(null) is the effect's second statement on every fetch and the .then branch never touches error. Acceptance notes true: Action and PageBlock pickers read no error and render a text box with no notice; ObjectFieldInspector's local hook swallows the rejection to an empty list and feeds ObjectPicker, a datalist input with no flag and no empty-state copy. Round-2 numbers reproduced verbatim: check-changeset-presence "4 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)", check-changeset-no-major "No changeset declares a major bump", check:control-bytes OK, ESLint on the test file 0 errors 0 warnings, 8 / 3 / 3 it( counts, merge-tree against f99f9cde7 exit 0, main unmoved in the five touched files since merge-base b1030c71a. No sentence regressed. No angle brackets; the only closing keyword is Fixes #10585 on line 1.
  • Merge state. 43 check runs on the head: all success, except Test (coverage), the coverage shard and dependabot (skipped). Test (shard 2/8) is success (completed 14:37:39Z), so objectui#10645 did not fire on this head. Type Check success. git merge-tree --write-tree refs/review/main refs/review/pr-10644 wrote e634b96de, exit 0, no conflicts; GitHub mergeable_state: clean.

② Semver level

patch, as declared. The added error field is additive on a hook that no app-shell index.ts re-exports; the user-visible change is a bug fix in the Hook inspector's picker.

③ Boundary flags

  • Changeset, paragraph 4: "The Hook inspector reads the three through rosterFrom" is imprecise at source: rosterFrom takes only { loading, error } (its signature and the hook's own header say so); options is read directly for pickerOptions. Replacement: "The Hook inspector folds loading and error through rosterFrom, the rule the objectui#8862 / objectui#9651 family set for the designer's pickers: only an answered roster may make a claim." Same class as the round-1 slip; not a behaviour claim.
  • Landing body acceptance note "was assessed by the contract reviewer": the round-1 record was not among my inputs, so the attribution is unverified here; the substance (datalist input, no flag, no empty copy) is verified at source.
  • The PR is still draft: true.
  • My vitest runs were single-file (10–12 s each) and ran outside os-verify-lock.sh, which was held with a four-deep queue; workspace siblings resolved to the donor checkout at main f99f9cde7, none of which this PR touches.
  • Test stderr carries the pre-existing react-i18next NO_I18NEXT_INSTANCE notice; harmless.

Implemented-by: claude/issue-10585-hook-roster-state
Reviewed-by: session_014mXUNuFomfj24w7s1pZzhN

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: a1c3d29844c110d79885b26ae42458e3147001ab

① Derived judgments

  • Diff e5a135708..a1c3d2984: one commit, one file (.changeset/10585-hook-object-roster-state.md), one sentence rewrapped across three lines. No other file changed. PR vs merge-base b1030c71a is still the same 5 files.
  • New sentence true at source. HookDefaultInspector.tsx:141 calls rosterFrom({ loading: objectsLoading, error: objectsError }); rosterFrom (_shared.tsx:297-300) takes exactly { loading, error }; options is read directly at line 131. The flag (line 152) and the "publish an object" copy (lines 233-234) are gated on rosterAnswered (roster.status === 'loaded'), so only an answered roster makes a claim.
  • Whole changeset re-read against the head, every sentence holds:
    • Base behaviour (para 2): the merge-base inspector read { options } alone, flagged unconditionally and printed the empty copy unconditionally; the base hook's catch set []. True.
    • useObjectOptions (para 3): returns error: string | null; setError(null) at effect start; the answer branch never touches it; catch sets err?.message ?? String(err), byte-for-byte the spelling in useObjectFields.ts:92. options/loading code paths unchanged. previews/useObjectOptions.js has exactly three importers (Action, Hook, PageBlock); Action (ActionDefaultInspector.tsx:324,327) and PageBlock (PageBlockInspector.tsx:66-68) destructure { options } only and render InspectorTextField when the list is empty. Dataset and ViewVariant import the same-name hook from useDatasetFields.js, so the enumeration is complete.
    • Bullets: answered arm unchanged; in flight the label is bare and the empty box prints engine.form.loadingOptions; failed arm draws the box only when something is selected (line 229), checkbox is disabled={readOnly} only so it stays toggleable, and the role="status" notice renders engine.form.optionsLoadFailedTitle plus the cause span. That key is the one ReportDefaultInspector and ViewColumnInspector pass as rosterFailureLabel; en and zh entries exist in i18n.ts.
  • Frontmatter md5 of the first three lines is 4e8ce55021fe3b14329c8b4cb3f9f354 at both e5a135708 and the head.
  • Landing body: Round 3 bullet is true where checkable from here: the before/after sentence is quoted exactly, the md5 claim holds both sides, and merge-tree against f99f9cde7 exits 0 (f99f9cde7 is current main). No angle brackets. The only closing keyword with an issue reference is line 1 Fixes #10585; "fixed here" (line 25) and "Not fixed here" (line 81) carry no reference and do not close anything.
  • CI on the head: 42 check runs, 26 success, 3 skipped, 13 in progress (Lint, Type Check, Test shards 1-8, Doc Snippet Type Check, Skill Example Check, Spec Main Shape Gate), 0 failures at read time.
  • git merge-tree --write-tree refs/review/main refs/review/pr-10644 writes 75cdc738e, exit 0, no conflicts. Main has not moved in any file this PR touches.

② Semver level

patch for @object-ui/app-shell is correct. previews/useObjectOptions is not exported from the package entry; the added return field and the failure-state rendering are internal bug-fix surface with no public API change.

③ Boundary flags

  • Test (shard 2/8) was still in progress at read time, not red. If it lands red on the objectui#10645 intermittent (ApiConsolePage.requestPreset-10591.test.tsx:299), that is this flag, not a FAIL.
  • The Round 3 bullet's three local gate exit codes (check-changeset-presence, check-changeset-no-major, check:control-bytes) were not re-run in this read-only review; the head's Changeset Declaration, Changeset Bump Policy and Control Byte Scan check runs are all success, which corroborates them.
  • "names the cause" (failed arm): when the fault message is the empty string the notice renders the title alone (lines 263-268), the same degenerate behaviour as InspectorSelectField. Not a wording defect for the ordinary case; noted only.

Implemented-by: claude/issue-10585-hook-roster-state
Reviewed-by: session_014mXUNuFomfj24w7s1pZzhN

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 25, 2026 14:53
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 25, 2026
Merged via the queue into main with commit 785d3a7 Sep 25, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-10585-hook-roster-state branch September 25, 2026 15:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants