Skip to content

Seven smaller things a real user hits — items 1 and 6: legal tabs and boards shown to requesters, an empty landing page for lawyers - #104

Draft
objectstack-fleet[bot] wants to merge 1 commit into
mainfrom
claude/issue-99-requester-navigation
Draft

objectstack-fleet[bot] wants to merge 1 commit into
mainfrom
claude/issue-99-requester-navigation

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #99. This PR covers items 1 and 6 of the card, as far as this app can take them. Items 2, 3, 4, 5 and 7 are not touched here, because open PRs hold their files, so #99 remains open for them. The view-switcher half of item 1 needs a platform capability that does not exist. It is reported below and not worked around.

What changed

One file: src/apps/clm.app.ts. No view, object, dashboard, flow or translation file changes. Navigation labels are keyed by item id, and no id changed.

  1. Item 6: each audience opens on its own desk. The top-level groups now run Legal Desk, Finance, Execution & Records, Administration, then My Contracts and Analytics. The order inside every group is unchanged and still follows §05's 导航 column.
  2. Item 1: a requester is no longer served the legal and finance boards. nav_legal_workbench now carries requiredPermissions: ['clm_legal.access'] and nav_finance_overview carries ['clm_finance.access']. The group gate stays clm_requester.access. Executive Overview gets no row gate, on purpose (see open questions).
  3. Comments. A new file-header section explains why the group order decides each audience's first screen. The analytics group's note on OR no longer says per-audience rows are out of reach.

Why this mechanism (measured on 17.7.0)

  • Landing page. The console opens an app on the first entry its findFirstRoute finds in the navigation the server served to that user. It walks in declaration order and skips action, url and component rows. homePageId was retired in spec 17.0.0, and nothing else chooses a landing page. Every position also binds clm_requester (src/security/bind-position-sets.ts), so My Contracts survived for everyone. Because it was listed first, it was every audience's first screen.
  • Row gates. filterAppForUserWithReason (@objectstack/rest 17.7.0) prunes each child on its own requiredPermissions using every(...). One capability per row therefore needs no OR.
  • Which audience lands where. §05 gives each of five audiences its own group with its 主视图, and marks 我的合同 as 所有人 (shared by all). So the four groups that each belong to one audience now come first, and the shared group follows. A requester-only user, and the two leadership rungs who hold only clm_requester, still open on 我发起的.

Audience matrix: what GET /api/v1/meta/app/clm serves

Before is 46e65f0 (main). After is cad612f (this branch). Same database, same six accounts.

Persona Landing before Landing after Analytics rows before Analytics rows after
Business Requester 1 (clm_requester set, no position) my_contracts my_contracts all 3 Executive Overview
Executive 1 (clm_executive) my_contracts my_contracts all 3 Executive Overview
Legal Counsel 1 (clm_legal_counsel) my_contracts legal_intake all 3 Legal Workbench, Executive Overview
Finance Controller 1 (clm_finance_controller) my_contracts all_payment_plans all 3 Executive Overview, Finance Overview
Records Manager 1 (clm_records_manager) my_contracts pending_execution all 3 Executive Overview
CLM Admin 1 (clm_admin) my_contracts clm_approval_rule all 3 Executive Overview

Positive control for the two new gates: Legal Counsel 1 is served Legal Workbench and Finance Controller 1 is served Finance Overview. Neither spelling hides its row from its own audience. validate also prints no "requiredPermissions references capability … registered nowhere" warning.

Browser evidence

Chromium 1194 at 1440×900, each persona signed in through the console's own login form, port 3499.

Boot. pnpm demo -- --port 3499 printed Plugins: 41 loaded, with no degraded capabilities and no no such table. The warnings were the known set: six scheduled flows unbound by deployment policy, analytics admitObjectRead, OAuth over plain HTTP, and the seeder's background budget. On the second boot there were also unresolved Business Requester 2/3 and Legal Counsel 2 names, which are accounts I did not create.

Before (46e65f0).

  • Business Requester 1 lands on /_console/apps/clm/clm_contract/view/my_contracts.
    • Sidebar: My Contracts, then Analytics with Legal Workbench, Executive Overview and Finance Overview.
    • View switcher: All Contracts · My Contracts · Awaiting Intake · My Reviews · "7 more".
  • Legal Counsel 1 lands on the same my_contracts URL, and the page reads "No matching records".

After (cad612f).

  • Legal Counsel 1 lands on /_console/apps/clm/clm_contract/view/legal_intake ("Awaiting Intake").
    • The footer reads "6 records", and the list request (filter on status equals submitted) answered 200, total=6.
    • The sidebar starts with Legal Desk.
  • Business Requester 1 still lands on my_contracts, whose owner_id request answered 200, total=43.
    • Sidebar: My Contracts, then Analytics with Executive Overview only.
    • The view switcher is unchanged: All Contracts · My Contracts · Awaiting Intake · My Reviews · "7 more". The overflow menu lists In Negotiation, Status Board, Expiry Calendar, Active Contracts, Awaiting Execution, Awaiting Archive and Contract Register. See the platform finding.
  • The other four personas:
    • Finance Controller 1 lands on all_payment_plans (total=300).
    • Records Manager 1 lands on pending_execution (total=6).
    • CLM Admin 1 lands on clm_approval_rule (total=6).
    • Executive 1 lands on my_contracts (total=0, see open questions).
  • Console, the same before and after:
    • one 401 GET /api/v1/auth/get-session, the session probe before sign-in;
    • two component-registry warnings (view:calendar and view:timeline bare-name fallback).
  • Direct URL. As Business Requester 1, /_console/apps/clm/dashboard/legal_workbench still renders the board. Its numbers are row-scoped (Awaiting Intake 3, the requester's own contracts). The row gate hides the entry. It is not a gate on the dashboard.

The dev agent keeps the screenshots locally and hands them to the seat. They are not committed: they are outside this card's file surface, and the write budget allows no evidence branch.

Gates (on cad612f)

pnpm verify chains the four gates with &&. It ran through the shared verify lock, which printed VERDICT command-exit 0.

validate:

  ✓ Validation passed (1090ms)

lint (the six suggestions are the existing approval-approvers-may-resolve-empty ones on contract_approval):

  6 suggestion(s) (1004ms)

typecheck: tsc --noEmit printed nothing and exited 0 (the && chain went on to the next gate).

lint:i18n-gate:

✓ i18n gate
  COVERAGE : 0 missing keys across 2 locale(s)

Platform findings (reported to the seat, not filed, no workaround)

P1. A list view and a dashboard carry no audience gate. So the view switcher shows every audience's views, and a dashboard hidden from the navigation still opens.

  • Symptom. Business Requester 1's clm_contract switcher lists all 11 list views, including the legal desk's, finance's and records'. As that requester, GET /api/v1/meta/view returns all 11 clm_contract view items. The Legal Workbench opens by URL even though its navigation row is not served.
  • Minimal repro.
    1. Give one object two listViews, mine and team_only, and point a navigation item gated requiredPermissions: ['x.access'] at team_only.
    2. Sign in as a user who lacks x.access.
    3. The navigation row is gone, but team_only is still a tab in the switcher.
    4. A type: 'dashboard' row behaves the same way: hidden from the navigation, but its URL opens.
  • What was read.
    • ListViewSchema has 50 top-level keys. The only access-shaped one is sharing (type personal or collaborative, plus lockedBy).
    • DashboardSchema has no requiredPermissions and no visible.
    • objectui's ViewTabBar gets every list-family view of the object from the adapter's listViews(), with no audience filter.
  • Expected capability. An audience gate on a list view and on a dashboard, enforced by the server the way a navigation item's requiredPermissions is.
  • Data exposure. None found. Every list and board is row-scoped, so a requester's legal tabs show only their own rows.
  • Version. @objectstack/spec, @objectstack/console and @objectstack/rest 17.7.0.

P2. requiredPermissions has no any-of form. Every layer checks every(...), so a row cannot be served to "legal or admin". Here that is why CLM Admin 1 is no longer served Legal Workbench and Finance Overview. Version 17.7.0. The app's analytics comment already records the AND semantics. I did not search upstream for an existing issue; that check is the seat's.

Acceptance notes

  • Group order changes for four audiences. Legal, finance, records and clm_admin now see their own group above My Contracts. A requester and the two leadership rungs are served exactly the tree they had before.
  • Wider than the card's wording. Item 6 names the lawyer. Finance, records and clm_admin had the same defect and the same cause, and the one reorder fixes all four. This is the same defect class in the same file, and nothing new needs verifying.
  • CLM Admin 1 loses two rows. Before, the admin was served all three boards. Now it is served Executive Overview only, because rows cannot be served on an OR of capabilities (P2).
  • Leadership still lands on an empty page. Executive 1 lands on an empty My Contracts. The leadership rungs hold no capability of their own and have no §05 group (open question 2).

Open questions (for the seat)

  1. Who reads Executive Overview? The leadership rungs' capabilities are exactly a requester's, so no requiredPermissions spelling can serve the board to leadership and not to requesters.
    • A: keep it served to every CLM user, with row-scoped numbers. This is today's behaviour and the default kept here.
    • B: give leadership a capability of its own, such as a set bound to clm_executive and clm_general_manager. That is a §04 change and the maintainer's call.
    • C: a client-side visible predicate on positions. It is still served, and it ties the navigation to position names.
    • Recommendation: A now, B if the maintainer wants a leadership desk. B would also answer question 2.
  2. Where should the two leadership rungs land? Their work arrives through Waiting on Me, which is a component row that findFirstRoute skips.
    • A: accept the empty 我发起的.
    • B: a leadership partition in §05.
    • Recommendation: decide together with question 1.

Generated by Claude Code

…ance boards

An app opens on the first navigation entry the server still serves to the
caller. Every position also holds clm_requester, so with 我的合同 listed
first every audience opened on 我发起的: right for a business requester,
empty for a lawyer. The four single-audience partitions now come first, then
我的合同 and 分析. A requester's served tree is unchanged.

Legal Workbench and Finance Overview now carry the gate of the §05 partition
they belong to (clm_legal.access / clm_finance.access). The server prunes a
child on its own requiredPermissions, so a requester is no longer served
those rows. Executive Overview stays ungated: the leadership rungs hold only
clm_requester, so no spelling separates them from a requester.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HihZ11bQSqjCgjzHbpv4M1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants