Skip to content

fix(app-shell,plugin-list,i18n): list empty states match the page they sit on (objectui#11687) - #11727

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-11687-empty-state-copy-matches-page
Oct 6, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-11687-empty-state-copy-matches-page

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #11687

Clause-②: yes

Implemented by the os-dev agent dispatched from seat domain:ui#1, session https://claude.ai/code/session_01DrKzdPdyLLBW3qpZ4vtk7z, under the claim on the card (comment 6017757987).

What was wrong

The two empty-state defects measured on the console (objectui#11672, item 15):

  1. Invitations and OAuth Applications said "These records are created by the authentication provider … not added by hand here." beside an "Invite User" / "Register OAuth Application" button.
  2. Webhooks' Active view, zero rows, said "No records match your current filters or search." with no filter or search applied.

Mechanism, measured against the PM's hypotheses

  • H1 holds. The better-auth arm of resolveManagedByEmptyState returned the identity copy for every object except sys_user / sys_team, without asking anything about the page. The engine-owned arm already yields when the create affordance is open.
  • H2 holds, and it chose the predicate. Both buttons are object actions placed at list_toolbar. They are not the CRUD create affordance: sys_invitation and sys_oauth_application leave create at the bucket default, which is off. Their gates reach the client already lowered into visible (requiresFeature and requiresMembershipReach), so whether the button is on the page depends on the deployment and the caller. A predicate on resolveEffectiveCrudAffordances alone would have left both pages broken while the tests passed. Only create_oauth_application declares mode: 'create'. invite_user declares no mode, so a mode-based predicate would also have missed Invitations.
  • H3 holds and is kept. The view's own filter still counts as a query, for objectui#4155's reason: a list emptied by its view filter must not get the first-run copy.
  • H4 is partly falsified: the API Keys page offers no create action. sys_api_key declares no list_toolbar action and keeps create at the bucket default. Keys are minted through POST /api/v1/keys. This holds at the dogfood's framework commit ce577ec4 and at objectstack aa09db58. The sweep's sentence put three pages under two button names. API Keys is therefore pinned as the page whose identity copy stays.

The change

  • @object-ui/app-shell: ObjectView computes pageOffersCreate. It is true when the page's New button shows (objectCanCreate && createVisible) or when the toolbar draws a list_toolbar action. The toolbar half is the new listToolbarDrawsAction in utils/managedByEmptyState.ts. It asks the same three questions action:bar and then action:button ask: actionRendersAt(action, 'list_toolbar'), the useCapabilityGate closure, and the visible gate. The gate goes through hasDeclaredPredicate and is evaluated fail-closed with no row bound. resolveManagedByEmptyState takes the boolean as a fifth argument. In the better-auth arm, a page that offers a create action falls back to the list's own empty-state copy. sys_team keeps its copy, which already names its Create Team button, and stays ahead of the check. The package entry exports neither function, so the signature change does not reach the published surface.

  • @object-ui/plugin-list: ListView now chooses its empty-state message three ways:

    • a user query (search box, user-filter chips or filter panel) keeps list.noMatchesMessage;
    • the view's own filter alone gets the new list.viewFilterNoMatchesMessage, "No records match this view’s filter.", under the existing "No matching records" title;
    • neither gets the first-run copy.

    The key is also in LIST_DEFAULT_TRANSLATIONS.

  • @object-ui/i18n: list.viewFilterNoMatchesMessage is added to all ten packs.

  • Changeset .changeset/11687-empty-state-copy-matches-page.md: minor for i18n and plugin-list, whose published surface grows, and patch for app-shell.

Pins: the four measured pages

packages/app-shell/src/views/ObjectView.emptyStateMatchesPage-11687.test.tsx mounts the real ObjectView and the real ListView inside a real i18next instance booted in en. Each object is in its served shape, with gates already lowered:

  • Invitations (Pending view, multi-org, org admin): Invite User is drawn. The empty state has no identity copy and says "No records match this view’s filter."
  • Invitations, single-org (control): Invite User is hidden by its own gate, so the identity copy stays.
  • API Keys (all_keys): the page offers no create action, so the identity copy stays.
  • OAuth Applications (all_apps): Register OAuth Application is drawn, and the list shows the first-run copy.
  • Webhooks, Active view: "No matching records" with "No records match this view’s filter.", and no "your current filters or search".

Also added: unit cases in managedByEmptyState.test.ts (the resolver and listToolbarDrawsAction) and three cases in ListView.emptyStateFilteredCopy.test.tsx (view filter only; a typed search; a user filter over a filtered view).

Reverse verification

These were one-off runs; each direction was predicted before running. Each leg changed the committed fix through ablation-replace.mjs: the anchor went from 1 hit to 0 and the blob changed. The leg then ran the suites and restored the file, proven by blob == HEAD and an empty git diff HEAD. Tests resolve @object-ui/* to src through the root vitest alias, so no rebuild was needed.

  • A. The pageOffersCreate check removed: 4 failed, 29 passed. The two resolver unit cases, Invitations and OAuth failed. Both controls and Webhooks stayed green.
  • B. The two-way hasActiveQuery message choice restored: 3 failed, 30 passed. The ListView view-filter case, Invitations and Webhooks failed.
  • C. listToolbarDrawsAction no longer reads visible: 3 failed, 23 passed across two suites. The two gate unit cases and the single-org control failed.

Local verification at 0ef8b17

  • Build: the dependency closure, pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' --filter '@object-ui/plugin-list^...' --filter '@object-ui/i18n^...' build, exited 0 (29 of 47 workspace projects).
  • type-check for @object-ui/i18n, @object-ui/plugin-list and @object-ui/app-shell: all exit 0. Each script ran as tsc --noEmit && tsc -p tsconfig.test.json, and the test project includes src/**/*.test.ts(x).
  • test, i18n: 80 files, 1304 passed and 13 skipped. plugin-list: 113 files, 1224 passed. app-shell: 1032 files passed and 1 skipped, 10181 tests passed and 9 skipped.
  • check:control-bytes, check:new-line-citations, check:changeset-claims, check:pending-changeset-literals, check:i18n-keys, check:i18n-drift, check:i18n-dead-keys (report only; the new key is not a candidate), check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:test-path-roots, check:unreferenced-sources, and the changeset presence, no-major, fixed and overwrite scripts: all exit 0.
  • Not measured: check:eager-locale-catalogues. It printed PREREQUISITE NOT MET because it needs a built console bundle. That run belongs to CI.
  • ESLint was narrowed to the 16 changed .ts/.tsx files (the --format json count) and found 0 errors. The 366 warnings are pre-existing rules on ObjectView.tsx / ListView.tsx; none is on an added line. eslint.config.js sets no parserOptions.project / projectService, so linting is not type-aware and this diff cannot change the verdict on any untouched file. The repo-wide pnpm lint belongs to CI.

Acceptance notes (observations; none filed)

  • The identity copy still outranks a user query. ListView reads schema.emptyState before its own choice, and the objectui#4155 pin "the author's own emptyState copy still wins over both" holds that order. On a better-auth list whose page offers no create action, such as API Keys or single-org Invitations, a search that matches nothing therefore shows "No identity records" rather than "No matching records". This comes from reading the source; it was not measured in the running app. Carrier: none.
  • Default-on feature flags do not fail open when absent. The spec's featureGatePredicate doc says a default-on flag fails open when it is absent ("e.g. config not yet fetched"). Measured here: features.organization != false evaluated against features: {} throws in objectui's CEL evaluator, and the fail-closed visible hides the action. The server may always report every flag, so this was not measured in the running app. Seam: spec:featureGatePredicate → renderer:action-button. Carrier: none.
  • API Keys copy is inaccurate. The identity copy says API Keys rows arrive "through sign-in, provisioning, and security flows", but users mint keys themselves on the Connect an Agent page. This is copy polish outside this card.
  • The Teams copy names a button that can be hidden. sys_team's copy names "Create Team" even where that action is hidden: its gate is the organization feature plus owner/admin reach. This predates the PR and is unchanged.
  • Deep-link filters count as the view's filter. ObjectView folds URL filters (?filter= deep links) into the view's filter, so an empty deep-linked list now reads the view-filter message.
  • No docs change. No page under content/docs, and none of the three package READMEs, describes how the list chooses its empty state.

Generated by Claude Code

claude added 4 commits October 6, 2026 14:10
…jectui#11687)

The better-auth empty state yields to the list's own copy when the page
offers a way to add a row (its New button, or a list_toolbar action the
toolbar draws). The list's empty state tells the view's own filter apart
from a user filter or search, with a new list.viewFilterNoMatchesMessage
key in every locale pack.

Claude-Session: https://claude.ai/code/session_01DrKzdPdyLLBW3qpZ4vtk7z
Co-authored-by: Claude <noreply@anthropic.com>
…re flag (objectui#11687)

Claude-Session: https://claude.ai/code/session_01DrKzdPdyLLBW3qpZ4vtk7z
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DrKzdPdyLLBW3qpZ4vtk7z
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 331 chunks) 3324.1 KB 3330.4 KB
Main entry chunk (gzip) 153.7 KB 350 KB
Entry file index-D_IwcAAf.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) 17.82KB 6.58KB
app-shell (runtime-config.js) 22.52KB 7.86KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
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.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
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.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
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.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 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) 574.74KB 137.95KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 233.72KB 64.83KB
fields (index.js) 262.75KB 66.62KB
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.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 35.66KB 9.49KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 41.18KB 11.71KB
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.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
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.04KB 3.92KB
plugin-calendar (index.js) 53.39KB 15.52KB
plugin-charts (index.js) 84.26KB 23.05KB
plugin-chatbot (index.js) 198.81KB 47.14KB
plugin-dashboard (index.js) 143.75KB 38.87KB
plugin-designer (index.js) 231.46KB 48.87KB
plugin-detail (index.js) 247.25KB 65.05KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.09KB 45.89KB
plugin-gantt (index.js) 179.16KB 45.06KB
plugin-grid (index.js) 238.48KB 65.51KB
plugin-kanban (index.js) 52.17KB 16.37KB
plugin-list (index.js) 116.85KB 29.12KB
plugin-map (index.js) 25.60KB 8.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 38.90KB 11.74KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 90.23KB 22.73KB
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.81KB 3.58KB
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) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
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) 1.13KB 0.65KB
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) 5.78KB 2.70KB
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 (node-slots.js) 7.18KB 2.34KB
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) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.25KB
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: 0ef8b17e360914ea054d431f0eede8efd73a58ba
Local-runs: none

① Derived judgments

Inputs, read once at 2026-10-06T15:48Z with no polling: card objectui#11687 (body and its three comments: the claim, the os-dev-report, the in-seat ACCEPT); PR objectui#11727 (body, the 17-file list, the net diff against main at the head); the 42 check-runs on the head. For the surface judgment, the package.json exports maps and entry files of the three packages were read at the head, and the action:bar / action:button renderers and the @object-ui/react gate hooks on origin/main, read-only. Nothing was built, run or re-run.

Every accept-set and public-surface change the diff implies, each judged:

  1. @object-ui/i18n: one key, list.viewFilterNoMatchesMessage, added to all ten packs (ar, de, en, es, fr, ja, ko, pt, ru, zh). The . entry exports en and TranslationKeys = typeof en (the pack is as const), and the ./locales and ./locales/* subpaths export every pack, so the published type and value surface grows by one property. Right: the card's second Done-when asks for the view's own wording, and a new key is the only non-breaking carrier for it.

  2. @object-ui/plugin-list: LIST_DEFAULT_TRANSLATIONS (exported from src/index.tsx, typed as a string-to-string Record) gains the same key with the byte-identical en string, typographic apostrophe included, so the objectui#4401 mirror holds. ListView's empty-state message is now three-way: a user query (searchTerm, appliedUserFilterConditions, appliedFilters.conditions) keeps list.noMatchesMessage; the view's filter alone gives list.viewFilterNoMatchesMessage under the unchanged list.noMatches title; neither gives the first-run pair. schema.emptyState still outranks all three, and hasActiveQuery still picks the title. The ListView props surface is unchanged. Right: "your filters" is now said only on a user query, a view emptied by its own filter never gets first-run copy (objectui#4155 holds), and the restored initialSearchTerm / initialFilters count as the user's, which they are.
    Residue, named rather than failed: ObjectView folds filter[...] URL deep links into the same filter before ListView sees it (the combined line of the view's effective filter), so an empty deep-linked list now reads "this view's filter" where it read "your current filters" before. Neither Done-when sentence names a deep link, first-run copy is never shown for one, and the dev carried it as a behaviour note. Accepted as such.

  3. @object-ui/app-shell: resolveManagedByEmptyState gains a fifth parameter pageOffersCreate = false, and the module gains listToolbarDrawsAction. Neither reaches the published surface: exports is . and ./styles.css, and src/index.ts re-exports nothing from utils/managedByEmptyState (its utils/index.js re-exports are resolveHostAppSegment, appRouteSegment, filterActiveApps, the five override predicates, HOME_LAUNCHER_PATH, resolveDeclaredHomePath). ObjectView is exported, and ConsoleObjectViewProps is unchanged. Right: internal signature, behaviour fix, no surface growth.

  4. The predicate "the page offers a create action" is (objectCanCreate && createVisible) || listToolbarDrawsAction(objectDef.actions, mayInvoke, predicateScope), with mayInvoke from useCapabilityGate() and predicateScope from usePredicateScope(). Read against the renderers on main: action:bar filters mayInvoke(a.requiredPermissions) then actionRendersAt(a, schema.location); each member is an action:button that hides on hasDeclaredVisibilityGate(schema.visible) && !isVisible, where hasDeclaredVisibilityGate is core's hasDeclaredPredicate re-exported under another name and isVisible is useCondition(toPredicateInput(visible), record, { throwOnError: true }), which is new ExpressionEvaluator({ ...predicateScope, ...context }).evaluateCondition(..., { throwOnError: true }) caught to false. listToolbarDrawsAction asks the same three questions with the same helpers, no row bound, and fails closed in the same catch. Right: the copy follows the drawn button by construction. The two hooks are called at ObjectViewInner's top level, before and outside renderListView; pageOffersCreate is a primitive and is in renderListView's dependency list; objectDef.actions is the same array the toolbar maps ((objectDef.actions || []).map(...) then actionRendersAt(a, 'list_toolbar')), and the bar is mounted only when some action declares list_toolbar, which is the placement actionRendersAt requires (the unplaced-action unit case pins that).

  5. The better-auth arm yields (return undefined) when pageOffersCreate, after the sys_team arm, which keeps its copy. Right on the card's first Done-when for every generic-copy object and for sys_user, whose "not created here" sentence a drawn Invite User or Create User contradicts. sys_team is a deliberate reading of that sentence's intent over its letter: its copy is managed-by copy on a page that offers Create Team, but the copy names that button and so cannot contradict it, and the changeset discloses the exception in a sentence of its own. Judged right under the card's stated rationale ("the empty-state copy ignores the page's own create action"). Pre-existing residue the dev noted and left unchanged: the Teams copy names Create Team where the gate hides it.

  6. Pins. The card asks for "the four measured pages"; the PR mounts the real ObjectView and the real ListView in a real i18next en instance for Invitations (multi-org, drawn; single-org, hidden control), API Keys, OAuth Applications and Webhooks Active: five cases, four pages, each asserting the pack's strings and the button's presence or absence together, so copy and button are measured in one mount. API Keys is pinned as the page that offers no create action (H4 partly falsified: sys_api_key declares no list_toolbar action and leaves create at the bucket default). That is the dev's reading of objectstack's platform objects, adopted by the seat; it is not re-derived here, and it is consistent with the first Done-when, which binds only a page that offers a create action. Right.

  7. Three ablations are reported from committed state (A: no pageOffersCreate check, 4 red; B: the two-way message choice restored, 3 red; C: visible not read, 3 red), each red at its own pins and green at its controls. Consistent with what the diff changes; not re-run here.

Check-runs on the head at the read: 42. 29 completed / success; 3 completed / skipped (dependabot and the two coverage jobs); 10 in_progress: Type Check, Test (shard 1/8) through Test (shard 8/8), Spec Main Shape Gate. No run is red. Converged green for this diff's families: Lint (which covers the dev's narrowed ESLint run), Build & E2E, Test (dist pins), Bundle Analysis (the job that runs scripts/check-eager-locale-catalogues.mjs, the one gate the dev could not measure locally), Changeset Declaration, Changeset Bump Policy, Changeset Fixed Group Check, Changeset Overwrite Report, Changeset Claim Re-read, Line Citation Gate, Control Byte Scan, Inert vi.mock Specifier Check, README Export Check, Governed Surface Queue Guard. Unconverged reading: the type-check family and the unit-test family (the i18n parity suites, the three touched suites, the full app-shell and plugin-list suites) were still running, as was Spec Main Shape Gate. The dev reports the same type-check and test families green locally at this head; that is a report, not a gate verdict, and the enqueue leg owns their convergence.

② Semver level

Changeset .changeset/11687-empty-state-copy-matches-page.md: @object-ui/app-shell patch, @object-ui/plugin-list minor, @object-ui/i18n minor. That matches what the diff publishes per ①: the two packages whose exported surface grows (a TranslationKeys property; a LIST_DEFAULT_TRANSLATIONS entry and a new message path) take minor; app-shell publishes a behaviour fix behind an unchanged entry and takes patch. No major (objectui's fixed-group rule), no skip-changeset, no empty frontmatter. The body states both user-visible changes, the Teams exception, the single-organization hidden case and the no-filter first-run case, names the new key, and carries no model identifier.

Clause-②: line: the PR body carries Clause-②: yes at line start, with no arm. The declaration admits at most one arm, so bare yes is well-formed; the floor it sets, at least minor for the packages whose surface grows, is met, and the two packages that grow are exactly the two declared minor. Right.

③ Boundary flags

Dev open_questions (one):

  • Which predicate means "the page offers a create action". A (shipped): the toolbar draws a list_toolbar action, judged by placement, the capability gate and visible. B: only actions declaring mode: 'create', which needs eight objectstack producer changes and still misses invite_user, which declares no mode. C: the CRUD create affordance alone, which misses all three measured pages. The seat took A without escalation. Concur: A is the only option that satisfies the card's Done-when today without cross-repo work, and it cannot disagree with a drawn button because it asks the button's own questions. The code comment marks its reading of objectstack's better-auth toolbar actions as dated and not re-derived, which is the honest form. Answered.

Dev deviations, each:

  • Lock queue-timeouts, re-queued twice: process, not contract; the third run measured. Answered.
  • check:eager-locale-catalogues not measured locally: CI's Bundle Analysis runs that script on this head and is completed / success. Answered by the gate.
  • ESLint narrowed to the 16 changed files: CI Lint is completed / success on this head. Answered by the gate.
  • Corrected test assumption (an absent default-on flag does not fail open in objectui's evaluator): the same fact as finding (b) below. Answered.
  • Throwaway probe test deleted before commit: the 17-file list carries no such file. Answered.
  • Attribution, the model-free trailer pair per objectui AGENTS.md, which states the harness reminder yields to it: the rule the dev cites exists in that file; commit messages are outside this review's inputs and were not re-read. Answered by rule.
  • No docs change: none of the three package READMEs and no page under content/docs names list.noMatchesMessage or list.firstRunMessage, so there is no documented surface to extend; the changeset carries the consumer-facing sentence. Answered.

Dev out_of_scope_findings, each:

  • (a) The managed-by copy, passed as schema.emptyState, outranks a user query on a better-auth list with no create action (API Keys, single-org Invitations): pre-existing order, held by the objectui#4155 pin, outside both Done-when sentences, source-read only. Seat: acceptance note. Concur.
  • (b) featureGatePredicate's doc says an absent default-on flag fails open; objectui's evaluator throws on features.organization != false against an empty features and the fail-closed visible hides. For this PR the copy and the button fail the same way, so the card is not breached. Seat: not filed under the reach rule, routed to the round report for triage. Escalated; concur with the carrier.
  • API Keys identity copy says keys arrive "through sign-in, provisioning, and security flows" while users mint them on Connect an Agent: copy polish outside the first Done-when, since that page offers no create action. Seat: acceptance note. Concur, with one note for the seat: the card's own sweep measured that page at a public door, so a card for the wording would clear the reach rule if the seat chooses to file one. Not a gate on this PR.
  • sys_team copy names Create Team where the gate hides it: pre-existing, unchanged. Acceptance note. Concur.
  • Deep-link filters count as the view's filter: ① item 2. Acceptance note. Concur.

Nothing escalates to the maintainer. The unconverged check families named in ① are for the enqueue leg to see green before landing; they are not a contract question.

Implemented-by: claude/issue-11687-empty-state-copy-matches-page
Reviewed-by: session_01DrKzdPdyLLBW3qpZ4vtk7z

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

needs:contract-review removed: same-shape PASS on record for the current head

domain:ui seat 1 · session_01DrKzdPdyLLBW3qpZ4vtk7z · 2026-10-06T15:51Z.

  • Record: comment 6020008778 on this PR. It has a ## Contract review heading, Served-tier: CONTRACT_REVIEW_TIER, Head-sha: 0ef8b17e360914ea054d431f0eede8efd73a58ba, Local-runs: none, sections ①②③, the independence pair (Implemented-by: the dev's branch, Reviewed-by: the seat session that adopts it) and VERDICT: PASS.
  • Head: the PR's head is still 0ef8b17e360914ea054d431f0eede8efd73a58ba at this read, the head the record judged.
  • Carrier: needs:contract-review is removed from this PR in the same stroke as this comment; the PR is its only carrier.
  • Still owed before enqueue: every check on this head completes green. The record read 10 of 42 runs in_progress.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 6, 2026 15:53
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 6, 2026 15:53
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 6, 2026
Merged via the queue into main with commit 834c559 Oct 6, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-11687-empty-state-copy-matches-page branch October 6, 2026 16:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants