Repository navigation
fix(platform-objects): Setup identity pages open on the tenant-wide list; a caller-scoped list view is never first - #21983
Conversation
…p entries name their unscoped view On sys_user, sys_api_key, sys_session, sys_oauth_application, sys_account, sys_user_preference and sys_record_share the unscoped "All" view is now declared first. Each Setup entry for them names that view, and the Account app's Linked Accounts entry names `mine`. Adds the two enumeration pins. Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
…es for the new view order; changesets `node scripts/check-i18n-bundles.mjs --write`: the `_views` keys follow the declared order. Pure reorders, no translated text changed. Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
…ller-scoped-views-not-first
📓 Docs Drift CheckThis PR changes 2 package(s): 58 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 11 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin d2a1cb653fc0be6f0c1b9e8a046ccdc6cc122593 && git checkout d2a1cb653fc0be6f0c1b9e8a046ccdc6cc122593
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8a399b2b150dcae74aebbfe9f28e0d63f527f349 a4d4688cfddb24a16c8cc4b77657b91b3190cb85 && git checkout -B drift-repro 8a399b2b150dcae74aebbfe9f28e0d63f527f349 && git merge --no-ff a4d4688cfddb24a16c8cc4b77657b91b3190cb85
node scripts/docs-audit/affected-docs.mjs --json 8a399b2b150dcae74aebbfe9f28e0d63f527f349
|
ACCEPT (seat review) — PR #21983 at head
|
…wide list, and a merged-app pin closes the caller-scoped first-view family (objectstack-ai#21991) Fixes objectstack-ai#21984 Clause-②: no ## What changes An administrator who opens Setup → Approvals → Requests now lands on the "All" list of approval requests. Before this, `nav_approval_requests` named no view, and `sys_approval_request` declared the caller-scoped "My Pending" view first (`pending_approvers contains {current_user_id}`). The console opens an object's first declared list view when a route names none, so the administrator saw only the requests pending on themselves. This applies the family's rule from triage `6012877503`: a caller-scoped list view is never an object's first, and every entry that wants one names it. - **`sys_approval_request` declares `all_requests` first.** `my_pending`, `submitted_by_me` and `completed` follow it in their previous order. No view is added, removed or edited. A short comment at `listViews` states why the position is the contract. - **`nav_approval_requests` names `viewName: 'all_requests'`.** That is the key the spec already declares on an object navigation item, so there is no new key. - **The four generated translation bundles follow the new order.** They were regenerated with `node scripts/check-i18n-bundles.mjs --write`. The change is a pure reorder of `_views` keys (+12 / −12 over four files), and no translated text changed. - **Unit pin:** `nav-contribution.test.ts` gains one case for this entry. - **Merged-app pin (new):** `packages/qa/dogfood/test/platform-app-object-entry-views.test.ts`, with 56 cases. Triage `6015714713` requires it; it is described below. It closes the family, because the `platform-objects` pins cannot see plugin entries. - **One changeset:** `@objectstack/plugin-approvals` at `patch`, carrying `Clause-②: no`. ⛔ No new key, no `packages/spec` edit, no `platform-objects` edit, no objectui change, no new `check:*` gate, and no governed surface. ## Supporting edits outside the declared file surface (so the pin can exist) - **`packages/qa/dogfood/vitest.config.ts`:** anchored source aliases for `@objectstack/setup`, `@objectstack/account` and `@objectstack/plugin-sharing` in the `isolated` project. - The pin imports all three as values. - Without an alias, each would resolve through `dist/` and would have to be added to dogfood's `check:test-source-alias` row, which is shrink-only and set-equal. An alias is that gate's prescribed fix. - The other contributors are already aliased (`plugin-approvals`, `service-datasource`, `cloud-connection`) or already in that row (`plugin-security`, `plugin-audit`, `plugin-webhooks`, `service-messaging`, `mcp`, `objectql`, `platform-objects`). - **`packages/qa/dogfood/package.json`** gains `@objectstack/setup` and `@objectstack/account` as devDependencies, and **`pnpm-lock.yaml`** changes by 6 lines. - With these, `turbo ls --affected` reaches this pin when either app shell changes. - Without them, the pin would read two packages outside its dependency graph. ## The dispatch's hypotheses, measured At `origin/main` `1c563af40e`. PR objectstack-ai#21983 merged at 12:31Z, before this branch was cut. - **H1 holds.** `listViews` declared `my_pending` (`:62`), `submitted_by_me` (`:76`), `completed` (`:86`) and `all_requests` (`:99`). `nav_approval_requests` (`approvals-plugin.ts:147`) named no view. - **H2: no reader in this repository relies on `sys_approval_request`'s first view.** - **Account:** `nav_account_approvals` (`account.app.ts:114`–`:117`) routes to the `approvals:inbox` component. No Account entry names `sys_approval_request`; the merged pin enumerates all six Account object entries. - **Setup:** `nav_approval_requests` is the only entry naming the object. - **Code:** no code reads this object's first list-view key. `packages/cli/src/commands/lint.ts:168` (`firstListViewKey`) reads a first key only to place a label diagnostic. No view on this object sets `isDefault`. - **objectui at the `.objectui-sha` pin `0abd4f9f87`:** read, not edited. - `ApprovalsInboxPage.tsx` reads the approvals REST path (`services/approvalsApi`) and names `sys_approval_request` only for its declared actions (`:2494`, `:2541`). - `recordApprovalActions.ts:36` and `deadRecordReference.ts:66` name the object, not a view. - No objectui file names `my_pending`, `submitted_by_me` or `all_requests`. - The generic `views[0]` doors (the object breadcrumb and the object switcher) are fixed by the reorder itself. - **H3 holds: the reorder moved four generated files.** These are `plugin-approvals/src/translations/{en,es-ES,ja-JP,zh-CN}.objects.generated.ts`. `check-i18n-bundles` first read "plugins/plugin-approvals: 4 bundle(s) drifted", and after `--write` it exits 0. The `*.source-hashes.generated.ts` files did not move. - **H4 holds.** The merged pin is red on `main` for this entry only; see "Red on main" below. ## Stop conditions (from `6012877503`) - **Access difference: not met.** This was read from source; no real-door read was run. - The view filter is a presentation predicate added to the generic data query. Row-level security and sharing decide which rows a caller reads, whichever view is open. - `all_requests` was already a tab on this page, and the diff changes neither who can read nor which rows they get. - The Setup entry also sits behind `group_approvals`' `manage_platform_settings` gate. - **No unscoped view: not met.** `all_requests` carries no filter at all. ## The merged-app pin `platform-app-object-entry-views.test.ts` boots the composition the way `packages/cli/scripts/check-app-nav-i18n.mjs` does, which was read and not edited: - the same 11 contributors; - a fake context whose only real service is `manifest`; - each manifest handed to `ObjectQL.registerApp`; - the apps read back through `registry.getApp`, which applies the same `applyNavContributions` merge that `/api/v1/meta/app` serves. Its population is every `type: 'object'` entry of the merged apps, with nothing hand-listed: 26 Setup entries and 6 Account entries, naming 27 objects. - **(a), 25 cases:** every object an entry names declares a first list view without `{current_user_id}`. - **(b), 14 cases:** every entry whose object declares a caller-scoped view names a `viewName` that the object declares. On a Setup entry, that view must not be caller-scoped. - **Composition and non-vacuity, 17 cases:** - both apps are merged; - each contributor lands at least one id in each app it declares; - the population contains plugin entries (`nav_approval_requests`, `nav_record_shares`); - the eight objects this family reordered are judged by both rules; - no named object is unresolved; - the objects that declare only caller-scoped views are exactly `sys_inbox_message` and `sys_member`, and only Account entries name them, each with `mine`. - **Where an object's definition is read:** - first, the composition's own registry (`registry.getObject`); - otherwise, `@objectstack/platform-objects/identity`, which is the barrel `plugin-auth` registers its identity objects from (`authIdentityObjects`). `AuthPlugin` cannot boot without a secret, which is also why check-app-nav-i18n leaves it out. - At this head, 10 named objects come from the identity barrel and 17 from the composition. - **Reach:** - The roster mirrors check-app-nav-i18n's `CONTRIBUTORS` by hand, so a contributor added there and not here is not seen. Reading that script's text from this test would have needed a `CROSS_PACKAGE_TEST_INPUTS` declaration and a turbo.json edit, so that was not done. - `plugin-auth`'s `nav_sso_providers` is merged only when an external IdP is wired, so no boot here merges it. Its object `sys_sso_provider` declares no caller-scoped view. ## Red on main, green on the head - **Main (`1c563af40e`):** the exact base blobs of the two subject files were restored into the tree with `git restore --source`. The pin reads `plugin-approvals` through its source alias. - Pin: **2 failed | 54 passed (56)**. The failures are `(a) … › sys_approval_request` ("declares the caller-scoped list view "my_pending" first; it is opened by setup/nav_approval_requests") and `(b) … › setup/nav_approval_requests` ("names no viewName"). - Unit: **1 failed | 1 passed (2)**. - No other entry was red, because PR objectstack-ai#21983 had already landed. The restore was proven by blob equality with HEAD and an empty `git diff HEAD`. - **Head (`10ff7b0044`):** pin **56 passed (56)**; unit **2 passed (2)**. ## Ablation (on committed `079304d6ec`, through `scripts/ablation-replace.mjs`, restores proven by blob) The direction of each leg was predicted before it ran: one red pin case and one red unit case per leg. | leg | mutation | pin | unit | |:--|:--|:--|:--| | A | literal swap of the adjacent `all_requests` / `my_pending` blocks (blob `17a36501e7fd → 014a8acaa16d`) | 1 failed / 55: `(a) › sys_approval_request` | 1 failed: "declares all_requests first" | | B | drop `viewName: 'all_requests'` (blob `70ec3ecd12af → 9fb934b3be07`) | 1 failed / 55: `(b) › setup/nav_approval_requests` "names no viewName" | 1 failed: "names its view" | | C | `viewName: 'my_pending'` | 1 failed / 55: `(b)` "is an administrator's entry and names the caller-scoped view" | 1 failed | | D | `viewName: 'all_requestz'` | 1 failed / 55: `(b)` "names "all_requestz", which the object does not declare" | 1 failed | - No `dist/` sits between the mutation and the run: the pin reaches `@objectstack/plugin-approvals` through the source alias (`vitest.config.ts`), and the unit test imports it relatively. - `plugin-approvals` was rebuilt afterwards for its built readers. `ablation-dist-preflight` found the marker in both built files. - The composition guards (a contributor landing nothing, an unresolved object) were not ablated. ## Tests and gates (head `10ff7b0044`) - `pnpm --filter @objectstack/plugin-approvals test`: 61 files, 899 tests passed. - `typecheck` passed for both packages. - `plugin-approvals`: its main program excludes tests. `check:test-typecheck` compiles `nav-contribution.test.ts` through `tsconfig.test.json` (`--listFiles`: 1), and the debt ledger has no entry for it. - `dogfood`: its `tsc` compiles the pin (`--listFiles`: 1). - `pnpm check:app-nav-i18n` reports OK: 11 contributors, setup 55 and account 12 merged nav ids. - **Gates:** `node scripts/pm/dispatch-gates.mjs --commands` was re-derived on this change with no paths, giving 79 commands. That is the dispatch's 51 plus 28 from the changeset, manifest, lockfile and dogfood files. All 79 exit 0. - `--ran` reconciliation: "79 derived, 79 run, 0 NOT-MEASURED, 0 UNRUN", a derived zero. - `check:dual-build-cjs-loads` first exited 3 (PREREQUISITE NOT MET: 8 packages outside this closure had no `dist/`). Those were built (41 tasks, all turbo cache hits) and it re-ran to exit 0. - **Lint, narrowed:** eslint `--no-inline-config --format json` over the 9 touched lintable files reported 9 files, 0 ignored, 0 errors and 0 warnings. - The population is the repo's one `eslint.config.mjs`, and none of the 9 is ignored. - That config never enables type-aware linting (no `parserOptions.project`), so this diff cannot move a verdict on an untouched file. - The repo-wide `pnpm lint` is CI's. ## Acceptance notes (observed, not filed) - `packages/platform-objects/src/apps/account.app.ts:79` says the Account inbox entries "rely on pre-existing `*.mine` / `*.my_pending` listViews". The Approvals entry has opened the inbox component instead. This is comment drift in a file outside this card. - `docs/qa/platform-checklist/areas/platform-core.json:525` lists the Account destination as "Approvals (sys_approval_request/my_pending)", but that entry is the `approvals:inbox` component. This is checklist drift. - The pin's roster and check-app-nav-i18n's `CONTRIBUTORS` are two hand-kept copies of one composition, and nothing mechanical holds them equal. Implemented by the os-dev run of session `session_01WMQprn46CND82KmY8sZWBu` on branch `claude/issue-21984-approvals-requests-all-first`. --- _Generated by [Claude Code](https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #21972
Clause-②: no
What changes
An administrator who opens Setup → API Keys, Sessions, OAuth Applications, Identity Links, User Preferences or Record Shares now lands on the tenant-wide "All" list. Before this, each of those entries named no view. The console then opened the object's first declared list view, and on every one of them that view was filtered to the caller (
user_id = {current_user_id}, orrecipient_idfor record shares). The administrator saw only their own rows.This applies triage's rule for the family (6012877503): a caller-scoped list view is never an object's first, and every entry that wants one names it.
viewName, asnav_usersalready does. That is the key the spec already declares on an object navigation item, so there is no new key.nav_account_linked) now namesmine. It was the one Account object entry that relied on the declared order. Withmineno longer first, it would otherwise have openedall_links.sys_user'smecomment said RLS stops non-admins reading other users' rows.member_defaultadmits the caller's organization's users throughsys_user_org_members, and the comment now says so.nav_users' comment now saysmeused to be first.node scripts/check-i18n-bundles.mjs --write. These are pure reorders of the_viewskeys: no translated text changed (+84 / −84).packages/platform-objects/src/apps/caller-scoped-first-list-view.test.ts, with 29 cases (described below).patch:@objectstack/platform-objectsand@objectstack/plugin-sharing. Each carries theClause-②: noline.⛔ No new key, no objectui change, no
packages/specedit, and no governed surface. Inplugin-sharing, only the two declared files change (sys-record-share.object.tsandsharing-plugin.ts), plus their four regenerated bundles.Implemented by the os-dev run of session
session_017ErfyP2Rx7XWHJA27QjyUion branchclaude/issue-21972-caller-scoped-views-not-first.viewNameviewNamesys_usersys-user.object.ts:603meall_users,me,unverified,two_factor,bannednav_users·all_users(already set by #21971)sys_usersys_api_keysys-api-key.object.ts:121mineall_keys,mine,active,revokednav_api_keys·all_keysnav_account_api_keys·mine(unchanged)sys_sessionsys-session.object.ts:113mineall_sessions,mine,revokednav_sessions·all_sessionsnav_account_sessions·mine(unchanged)sys_oauth_applicationsys-oauth-application.object.ts:210mineall_apps,mine,active,disabled_appsnav_oauth_apps·all_appsnav_account_oauth_apps·mine(unchanged)sys_accountsys-account.object.ts:80mineall_links,mine,by_providernav_accounts·all_linksnav_account_linked·mine(new)sys_user_preferencesys-user-preference.object.ts:36mineall_preferences,mine,by_usernav_user_preferences·all_preferencessys_record_shareplugin-sharing/src/objects/sys-record-share.object.ts:46granted_to_meall_shares,granted_to_me,granted_by_me,by_object,manual_grants,rule_grantsnav_record_shares(sharing-plugin.ts:591) ·all_sharesThe dispatch's hypotheses, measured
all_sessions, whose only filter isrevoked_at is_null. That view is the one now first (table above). Stop condition 2 does not fire.member_default's row-level security inpackages/plugins/plugin-security/src/objects/default-permission-sets.ts. No real-door read was run.sys_api_key,sys_session,sys_oauth_applicationandsys_user_preferencecarry_selfpolicies withuser_id == current_user.id, operationall(:921,:891,:955,:915).sys_accountcarries one forselect(:897). Each is the same predicate asmine, so a member's "All" view returns exactly the rowsminereturns.sys_usercarriessys_user_self(:871) andsys_user_org_members,id in current_user.org_user_ids(:885). So a member's "All Users" view returns their organization's users, andmedid not. That is the declared staff-directory policy, and the header of the same file names it as intended. It is not an access defect: the member already had those rows through the existing "All Users" tab and throughGET /data/sys_user, and this diff changes neither. I read stop condition 1 as not met. The reasoning is stated here so the seat can disagree.sys_record_share:member_defaulthas no wildcard object grant and names nosys_record_sharepermission, so a member reads no rows through either view. The Setup entry also requiresmanage_platform_settings.nav_account_linkedwas the only object entry without a view. All six Account object entries now namemine.platform-objectsnames an object whose first view is caller-scoped. The pin's (a) cases enumerate all of them.packages/cli/src/commands/lint.ts:168(firstListViewKey) reads the first key only to place a label diagnostic when no list view has a label. Every view here has one.sys-user.page.tsrelated lists name these objects withshowViewAll: trueand no view. How objectui's "View all" picks a view was not read (NOT MEASURED).system.datasets.tsreadssys_userandsys_sessionby object, not by view.setup-users-nav-view.test.tsandi18n-resolver.object-list-views.test.tsread views by name.views[0]fallback, the breadcrumb and the object switcher are covered by the reorder itself.packages/platform-objects/src/apps/translations/{en,es-ES,ja-JP,zh-CN}.objects.generated.tsandpackages/plugins/plugin-sharing/src/translations/{en,es-ES,ja-JP,zh-CN}.objects.generated.ts.pnpm check:i18nfirst read "platform-objects DRIFTED (4)" and "plugins/plugin-sharing DRIFTED (4)". After--writeit exits 0. The other seven packages regenerated with no diff. No count or order pin moved.Pins
caller-scoped-first-list-view.test.ts, 29 cases. The population is derived from this package's navigation, not hand-listed. It is everytype: 'object'entry ofSETUP_NAV_CONTRIBUTIONSandACCOUNT_APP(18 entries, 13 objects), looked up in this package's own exports.{current_user_id}. That is checked anywhere in the view, so the${current_user_id}spelling is included.viewName. The object must declare that view under that same name, and on a Setup entry the named view must not be caller-scoped.sys_inbox_message, owned byservice-messaging. (b) therefore requires its entry to name a view, and it namesmine;sys_member. Only the Account app names it, withmine.NavigationContributionSchemaand the Account app throughAppSchema, and each object entry keeps itsviewName.setup-users-nav-view.test.tsstays green (7 of 7).Reach of the pins.
nav_record_shares,nav_approval_requests, …) are not visible fromplatform-objects, which cannot import the plugins because they depend on it.plugin-sharinggets no test file here: the dispatch declares only its two files.check-app-nav-i18nboots them, ata4d4688cfd. That read givesnav_record_shares → sys_record_share | first=all_shares (unscoped) | viewName=all_shares (unscoped).Reverse verification (on committed
f757947e6c, throughscripts/ablation-replace.mjs)The test imports its subjects by relative path from
src/, so nodist/sits between the mutation and the run. Directions were predicted before each run: one red case each.sys_sessionrestored to its old order (mine,all_sessions,revoked), by one literal swap of the two adjacent view blocks.4e46fda0773c→884ccaa2b537.(a) … › sys_session: "sys_session declares the caller-scoped list view "mine" first".4e46fda0773cequals HEAD,git diff HEADis empty, andgit status --porcelainis empty.viewName: 'all_sessions',deleted fromnav_sessions.17f1725c3cad→2d7a16c6a894.(b) … › setup nav_sessions → sys_session: "nav_sessions names no view".17f1725c3cadequals HEAD,git diff HEADis empty, andgit status --porcelainis empty.Tests and gates, at
a4d4688cfdThat commit is the merge of
origin/mainatdcf3eb494a, which brought onlypackages/spectest files. The whole workspace was built before it (turbo run build --concurrency=2, 72 tasks).pnpm --filter @objectstack/platform-objects exec vitest run --maxWorkers=2: Test Files 62 passed (62), Tests 996 passed (996).pnpm --filter @objectstack/plugin-sharing exec vitest run --maxWorkers=2: Test Files 40 passed (40), Tests 980 passed (980).pnpm --filter @objectstack/platform-objects typecheckandpnpm --filter @objectstack/plugin-sharing typecheck: both exit 0, withcheck:test-typecheck: OK. The new test file is in thetsconfig.test.jsonprogram, counted with--listFiles.node scripts/pm/dispatch-gates.mjs --commandsderived 66 commands, and all 66 exited 0.--ranreads "66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN". That zero is derived: every line carried its exit code.check-adr-0087-registration(base and self-test),check-empty-changeset(base and self-test),release-rehearsal-clone --self-test,release-pending-publish --self-test,check:engine-double-contract,check:objectql-double-limit,check:objectui-changeset,check:pm-changeset-deadline-census,check:query-options-erasure,check:type-check-coverage,check:type-check-debtandcheck:where-matcher.check:type-check-debtran twice. The first run was killed by my own batch timeout. The rerun exited 0 in 221 s: "1 ledger entr(ies) re-measured … none above its recorded number".check-closing-target-claim.mjs,check-partof-closing-keyword.mjsandcheck-single-claim-paths.mjsanswer NOT WIRED without a PR context. CI runs them with one.check:adr-symbol-anchors,check:scripts-symbol-anchors,check:spec-docblock-symbol-anchorsandcheck:adr-anchorsall exited 0.eslint.config.mjs: itspackages/**/*.{ts,tsx,mts,cts}blocks match all 19.--format json.parserOptions.project), so this diff cannot move a verdict on an untouched file. The repo-widepnpm lintis CI's.Acceptance notes
nav_approval_requests,packages/plugins/plugin-approvals/src/approvals-plugin.ts:147) names no view.sys_approval_requestdeclaresmy_pendingfirst, filtered onpending_approvers contains {current_user_id}, so an administrator sees only the requests pending on them.domain:services, and the dispatch allows nothing in that lane beyond the twoplugin-sharingfiles. It is reported to the seat on the card.sys_member(mine) andsys_inbox_message(mine). Only Account entries name them, and those entries namemine. No unscoped view is added here; that is triage's decision. Both are pinned as exact sets, so a third object cannot join unnoticed.sys_user, a member's object breadcrumb now opens their organization's user list rather than My Profile.nav_users,nav_notificationsand the Accountmineentries already do. This was not measured in a browser.Generated by Claude Code