Skip to content

fix(platform-objects): Setup identity pages open on the tenant-wide list; a caller-scoped list view is never first - #21983

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-21972-caller-scoped-views-not-first
Oct 6, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-21972-caller-scoped-views-not-first

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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}, or recipient_id for 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.

  • Seven objects are reordered, and nothing else in them changes. In each, the unscoped "All" view moves to first place and the caller-scoped view follows it. The other views keep their relative order. No view is added, removed or edited.
  • Six Setup entries name their unscoped view with viewName, as nav_users already does. That is the key the spec already declares on an object navigation item, so there is no new key.
  • The Account app's Linked Accounts entry (nav_account_linked) now names mine. It was the one Account object entry that relied on the declared order. With mine no longer first, it would otherwise have opened all_links.
  • Two comments are corrected.
    • sys_user's me comment said RLS stops non-admins reading other users' rows. member_default admits the caller's organization's users through sys_user_org_members, and the comment now says so.
    • nav_users' comment now says me used to be first.
  • The generated translation bundles follow the new order. Eight files changed (four in each package), regenerated with node scripts/check-i18n-bundles.mjs --write. These are pure reorders of the _views keys: no translated text changed (+84 / −84).
  • New pins: packages/platform-objects/src/apps/caller-scoped-first-list-view.test.ts, with 29 cases (described below).
  • Two changesets, both patch: @objectstack/platform-objects and @objectstack/plugin-sharing. Each carries the Clause-②: no line.

⛔ No new key, no objectui change, no packages/spec edit, and no governed surface. In plugin-sharing, only the two declared files change (sys-record-share.object.ts and sharing-plugin.ts), plus their four regenerated bundles.

Implemented by the os-dev run of session session_017ErfyP2Rx7XWHJA27QjyUi on branch claude/issue-21972-caller-scoped-views-not-first.

object listViews at old first new order Setup entry · viewName Account entry · viewName
sys_user sys-user.object.ts:603 me all_users, me, unverified, two_factor, banned nav_users · all_users (already set by #21971) none routes to sys_user
sys_api_key sys-api-key.object.ts:121 mine all_keys, mine, active, revoked nav_api_keys · all_keys nav_account_api_keys · mine (unchanged)
sys_session sys-session.object.ts:113 mine all_sessions, mine, revoked nav_sessions · all_sessions nav_account_sessions · mine (unchanged)
sys_oauth_application sys-oauth-application.object.ts:210 mine all_apps, mine, active, disabled_apps nav_oauth_apps · all_apps nav_account_oauth_apps · mine (unchanged)
sys_account sys-account.object.ts:80 mine all_links, mine, by_provider nav_accounts · all_links nav_account_linked · mine (new)
sys_user_preference sys-user-preference.object.ts:36 mine all_preferences, mine, by_user nav_user_preferences · all_preferences none
sys_record_share plugin-sharing/src/objects/sys-record-share.object.ts:46 granted_to_me all_shares, granted_to_me, granted_by_me, by_object, manual_grants, rule_grants nav_record_shares (sharing-plugin.ts:591) · all_shares none

The dispatch's hypotheses, measured

  • H1 holds: each of the seven objects declares an unscoped list view. Each one's "All" view carries no filter, except all_sessions, whose only filter is revoked_at is_null. That view is the one now first (table above). Stop condition 2 does not fire.
  • H2 holds: the order changes which view opens, not which rows a caller may read. This was read from member_default's row-level security in packages/plugins/plugin-security/src/objects/default-permission-sets.ts. No real-door read was run.
    • sys_api_key, sys_session, sys_oauth_application and sys_user_preference carry _self policies with user_id == current_user.id, operation all (:921, :891, :955, :915). sys_account carries one for select (:897). Each is the same predicate as mine, so a member's "All" view returns exactly the rows mine returns.
    • sys_user carries sys_user_self (:871) and sys_user_org_members, id in current_user.org_user_ids (:885). So a member's "All Users" view returns their organization's users, and me did 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 through GET /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_default has no wildcard object grant and names no sys_record_share permission, so a member reads no rows through either view. The Setup entry also requires manage_platform_settings.
  • H3 holds: these are the readers of the declared order in this repository.
    • Account entries: nav_account_linked was the only object entry without a view. All six Account object entries now name mine.
    • Setup entries: after this change, no Setup entry in platform-objects names an object whose first view is caller-scoped. The pin's (a) cases enumerate all of them.
    • Code: 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.
    • Pages: sys-user.page.ts related lists name these objects with showViewAll: true and no view. How objectui's "View all" picks a view was not read (NOT MEASURED).
    • Dashboards: system.datasets.ts reads sys_user and sys_session by object, not by view.
    • Tests: none assert a view index. setup-users-nav-view.test.ts and i18n-resolver.object-list-views.test.ts read views by name.
    • objectui (out of scope): the views[0] fallback, the breadcrumb and the object switcher are covered by the reorder itself.
  • H4 holds: the reorder moved eight generated files. These are packages/platform-objects/src/apps/translations/{en,es-ES,ja-JP,zh-CN}.objects.generated.ts and packages/plugins/plugin-sharing/src/translations/{en,es-ES,ja-JP,zh-CN}.objects.generated.ts. pnpm check:i18n first read "platform-objects DRIFTED (4)" and "plugins/plugin-sharing DRIFTED (4)". After --write it 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 every type: 'object' entry of SETUP_NAV_CONTRIBUTIONS and ACCOUNT_APP (18 entries, 13 objects), looked up in this package's own exports.

  • (a), 11 cases: a named object's first declared list view carries no {current_user_id}. That is checked anywhere in the view, so the ${current_user_id} spelling is included.
  • (b), 12 cases: every entry whose object declares a caller-scoped view names a viewName. The object must declare that view under that same name, and on a Setup entry the named view must not be caller-scoped.
  • Population and non-vacuity, 4 cases:
    • both apps contribute object entries;
    • the six objects this card reordered in this package are judged by both (a) and (b);
    • the named objects this package cannot read are exactly sys_inbox_message, owned by service-messaging. (b) therefore requires its entry to name a view, and it names mine;
    • the named objects that declare only caller-scoped views are exactly sys_member. Only the Account app names it, with mine.
  • Parse, 2 cases: every Setup contribution parses through NavigationContributionSchema and the Account app through AppSchema, and each object entry keeps its viewName.
  • platform-objects: Setup → Users opens on the "My Profile" view — an admin sees one row (themselves, page size 1) instead of the user list #21960's setup-users-nav-view.test.ts stays green (7 of 7).

Reach of the pins.

  • Plugin-contributed entries are outside them. Setup entries contributed by plugins at runtime (nav_record_shares, nav_approval_requests, …) are not visible from platform-objects, which cannot import the plugins because they depend on it. plugin-sharing gets no test file here: the dispatch declares only its two files.
  • The sharing half was measured once instead. Each built plugin was booted with a fake manifest context, the way check-app-nav-i18n boots them, at a4d4688cfd. That read gives nav_record_shares → sys_record_share | first=all_shares (unscoped) | viewName=all_shares (unscoped).

Reverse verification (on committed f757947e6c, through scripts/ablation-replace.mjs)

The test imports its subjects by relative path from src/, so no dist/ sits between the mutation and the run. Directions were predicted before each run: one red case each.

  • (a): sys_session restored to its old order (mine, all_sessions, revoked), by one literal swap of the two adjacent view blocks.
    • Mutation landed: anchor 1 → 0, replacement 0 → 1, blob 4e46fda0773c → 884ccaa2b537.
    • Result: 1 failed | 28 passed (29). The failing case is (a) … › sys_session: "sys_session declares the caller-scoped list view "mine" first".
    • Restored: blob 4e46fda0773c equals HEAD, git diff HEAD is empty, and git status --porcelain is empty.
  • (b): viewName: 'all_sessions', deleted from nav_sessions.
    • Mutation landed: anchor 1 → 0, blob 17f1725c3cad → 2d7a16c6a894.
    • Result: 1 failed | 28 passed (29). The failing case is (b) … › setup nav_sessions → sys_session: "nav_sessions names no view".
    • Restored: blob 17f1725c3cad equals HEAD, git diff HEAD is empty, and git status --porcelain is empty.

Tests and gates, at a4d4688cfd

That commit is the merge of origin/main at dcf3eb494a, which brought only packages/spec test 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 typecheck and pnpm --filter @objectstack/plugin-sharing typecheck: both exit 0, with check:test-typecheck: OK. The new test file is in the tsconfig.test.json program, counted with --listFiles.
  • node scripts/pm/dispatch-gates.mjs --commands derived 66 commands, and all 66 exited 0. --ran reads "66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN". That zero is derived: every line carried its exit code.
    • 14 are new against the dispatch-time list: 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-debt and check:where-matcher.
    • check:type-check-debt ran 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".
  • Artifact-roster block: 55 families, all run. 52 exited 0.
    • check-closing-target-claim.mjs, check-partof-closing-keyword.mjs and check-single-claim-paths.mjs answer NOT WIRED without a PR context. CI runs them with one.
  • Symbol-anchor sweeps: check:adr-symbol-anchors, check:scripts-symbol-anchors, check:spec-docblock-symbol-anchors and check:adr-anchors all exited 0.
  • ESLint, narrowed to the 19 touched TypeScript files: 19 files, 0 errors, 0 warnings, none reported as ignored.
    • The population is read from eslint.config.mjs: its packages/**/*.{ts,tsx,mts,cts} blocks match all 19.
    • The count comes from --format json.
    • The config enables no 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

  • One more member of the family sits outside this card's lane.
    • The defect: Setup → Approvals → Requests (nav_approval_requests, packages/plugins/plugin-approvals/src/approvals-plugin.ts:147) names no view. sys_approval_request declares my_pending first, filtered on pending_approvers contains {current_user_id}, so an administrator sees only the requests pending on them.
    • How it was measured: by the same built-plugin read as above.
    • Why it is not here: it is in domain:services, and the dispatch allows nothing in that lane beyond the two plugin-sharing files. It is reported to the seat on the card.
  • Two named objects declare only caller-scoped list views: sys_member (mine) and sys_inbox_message (mine). Only Account entries name them, and those entries name mine. No unscoped view is added here; that is triage's decision. Both are pinned as exact sets, so a third object cannot join unnoticed.
  • The bare-object doors now open "All" for members too. RLS scopes the rows (H2). On sys_user, a member's object breadcrumb now opens their organization's user list rather than My Profile.
  • Sidebar highlight. Each entry that now names a view lights up only on that view, as nav_users, nav_notifications and the Account mine entries already do. This was not measured in a browser.

Generated by Claude Code

claude added 3 commits October 6, 2026 10:46
…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>
@github-actions github-actions Bot added the size/l label Oct 6, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Oct 6, 2026
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/platform-objects, @objectstack/plugin-sharing, touching 48 documentable anchor(s).

58 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 8a399b2b150dcae74aebbfe9f28e0d63f527f349.

⛔ 11 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 3 anchor(s) matched too much of the corpus to be a work list: sys_user (symbol, 37 pages), created_at (literal, 37 pages), sys_user (literal, 37 pages)
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 8a399b2b150dcae74aebbfe9f28e0d63f527f349 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from d2a1cb653fc0be6f0c1b9e8a046ccdc6cc122593 — the merge of head a4d4688cfddb24a16c8cc4b77657b91b3190cb85 into base 8a399b2b150dcae74aebbfe9f28e0d63f527f349, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 8a399b2b150dcae74aebbfe9f28e0d63f527f349 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

ACCEPT (seat review) — PR #21983 at head a4d4688cfd

domain:engine#1 · session_017ErfyP2Rx7XWHJA27QjyUi · read at 2026-10-06T12:04Z. The dev's report is os-dev-report on #21972.

  • The change. It follows triage's family rule (6012877503) on all seven objects.
    • The unscoped "All" list view is declared first in each object's listViews. The caller-scoped view follows it, unchanged.
    • No view is added, removed or edited.
    • The six Setup entries name their unscoped view. nav_account_linked names mine.
    • The seat read the sys_user and account.app.ts hunks.
  • Pins. caller-scoped-first-list-view.test.ts has 29 cases. Its population is derived from SETUP_NAV_CONTRIBUTIONS and ACCOUNT_APP.
  • Generated catalogs. Eight bundles were regenerated by check-i18n-bundles.mjs --write. They are pure _views key reorders.
  • CI on a4d4688cfd. 31 success, and the 3 skips are in the roster. Readings against main at db87a025df: NOT governed (0 of 21 paths), and git merge-tree is clean.
  • Clause-②: no, at patch. No contract review is owed.

The dev's open questions, answered by the seat:

  1. The eighth member, nav_approval_requests (plugin-approvals): A. Fixes #21972 stays, and the member is filed as finding(plugin-approvals): Setup → Approvals → Requests opens sys_approval_request's caller-scoped first view my_pending, so an administrator sees only requests pending on themselves (#21972's eighth family member) #21984 for domain:services. Triage ruled the seven objects, and the order allowed nothing else in that lane.
  2. sys_member and sys_inbox_message declare only caller-scoped views: A. No view is added. Both are Account-only. Every entry that names them names mine, and the exact-set pin makes a third such object loud. They are not among triage's seven, so its stop condition 2 does not apply to them.
  3. Stop condition 1 on sys_user: A, not a stop. A member's All Users view returns the organization's users, which me hid.

Noted, not filed:


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 6, 2026 12:05
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 6, 2026 12:05
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 6, 2026
Merged via the queue into main with commit 1c563af Oct 6, 2026
36 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21972-caller-scoped-views-not-first branch October 6, 2026 12:31
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants