All docs · North OS2026-09-15

Actionable matters: Clio team, person filters, personal recency and document access — design spec

Status: draft for plan review; nothing implemented · Author: writer seat, lane actionable-matters (Devin, Fusion Fable 5.1 high — the Claude writer route; recorded deviation from the Claude Code launch line) · Date: 2026-09-15 · Repo: north-os, branch feat/actionable-matters, base includes #431 and #432 · Linear: NOS-384 · Related: Continuous Clio source ingestion (§ B metadata writer, § C clio_activity, § G matter-scoped search, § H matter view); the private work ledger design in the sibling lane feat/private-work-ledger (uncommitted draft 2026-09-15-private-work-ledger-design.html, read only) · Plan: implementation plan.

1. Intent and boundary

/matters becomes the page a lawyer opens to work: which matters are mine, who is on each, what did I last do on it, and where its documents live. Today the table sorts one loaded page by updatedAt and labels it "Last sync" (apps/web/components/matters/matter-table.tsx:70,131-137,166); that is synchronization, not work. This lane owns matters-list presentation, assignments, filtering, navigation and the reuse of the existing detail route. It does not own personal work evidence: the sibling ledger lane (feat/private-work-ledger) owns every personal evidence source, its storage and its privacy model, and this design consumes one owner-scoped evidence seam from it, named in § 5 and § 13, never re-implemented here.

Completion gates (orchestrator decision, 2026-09-15). The feature is complete only when, end to end on the authorized local surface, (a) the viewer's Clio identity resolves from a provider-verified record, (b) "Your last contribution" is fed by genuine qualifying own contributions from the sibling seam, and (c) "Review my work on this matter" reaches the sibling's review entry. unmapped, source_disconnected and absent are runtime account and error states that exist once the real integrations exist; they never justify a function whose body returns them unconditionally, a stub seam, or a "v1 foundation" that ships the list without personal evidence and defers the rest to a follow-up. Such a build is rejected at review.

Superseding user decision (2026-09-15): an assignment is exactly the explicit Clio responsible_attorney or responsible_staff on the matter. originating_attorney is shown and filterable under its own label and is not a working assignment. Clio access groups, subscribers, external contacts (clients, opposing counsel, co-counsel) and people who merely logged time are never assignments.

2. Evidence read for this design

3. Definitions

TermMeaning in this design
AssignmentClio responsible_attorney or responsible_staff on the matter, identified by Clio User id. Displayed as "Responsible lawyer" and "Responsible staff".
Originating lawyerClio originating_attorney. Displayed in the team disclosure under its own label, filterable by explicit role choice, excluded from My matters and from Unassigned.
ParticipationAny own contribution evidence (§ 5). Never implies assignment; assignment never implies participation.
UnassignedObserved absence: the assignment snapshot exists and both working roles are null. A matter with no snapshot yet is unknown, shown as "Not yet synced", and excluded from Unassigned.
Your last contributionThe latest qualifying own evidence date across admitted sources for the authenticated viewer only. Unknown is rendered as "—" and sorts last; it is never "inactive".
Accessible scopeMatters visible to the firm through RLS and tracking_state; no per-user Clio permission model is claimed.

4. A. Assignment snapshot on the matter

5. B. Viewer identity and owner-scoped evidence seams

Viewer identity. resolveViewerClioIdentity(organizationId, userId) in apps/web/lib/matters/viewer-identity.ts returns { state: "verified", clioUserId } or { state: "unmapped", reason: "no_identity_connection" | "identity_revoked" }. The only admitted source is a provider-verified identity (the sibling's personal Clio identity connection, who_am_i data.id); name or email matching is rejected (§ 12). The function reads the sibling's verified identity record (U2); unmapped is what it returns when that record has no row for the viewer or the row is revoked — a runtime account state, rendered as § 7 describes. A body that returns unmapped without reading the record is not an implementation of this section (§ 1 completion gates).

Required own-contribution sources. Personal evidence is owned by the sibling ledger lane end to end; this design names what must qualify and consumes one seam. Sources are never narrowed silently: dropping any required source below needs a recorded orchestrator decision in § 13.

SourceQualifying ruleProvenance to establish (owner)
Own Clio contributionsclio_activity.observation_state = 'visible', user_source_id = verified viewer Clio id, matter set; recency = max(date). Substantive types qualify (TimeEntry today; any further contribution type the sibling ingests is admitted by the same rule); ExpenseEntry is excluded by default as non-substantive (decision J2). Never "TimeEntry only" as a hidden narrowing of the feature.Which activity types the corpus actually carries (spec 2026-09-14 § C). Per-matter maxima only; no notes, quantities, ids or per-person aggregates cross the seam.
Own sent mail, matter-linkedSubstantive mail the viewer sent and that is reliably linked to a matter; received mail never qualifies. Two candidate provenances: card replies sent through North (pipeline_runs.user_id = viewer, chip = 'sent') whose matter link the owner has established as reliable, and mailbox Sent Items read by the ledger lane. An automatic card→matter link is not treated as reliable on its own.The owner evaluates the actual provenance the tables carry — which matter_link_status values are written today and by whom, which timestamp is the send time (U4) — instead of gating on status values that nothing writes. Sent Items provenance is the ledger design's.
Own ledger episodesActive, non-dismissed work_episode rows of the owner with a matter; recency = the episode's work date or observed end.Sibling-owned storage and privacy model.
NetDocuments changesAdmitted only where the provider supplies a verified actor (the viewer's own ND identity on the event); otherwise not a source. No local actor evidence exists today (§ 2).Owner establishes whether an actor-bearing event feed exists before admitting it.
Received mail, sync, ingestion, metadata changes, other people's activityNever evidence.

The evidence seam (candidate, not settled). This lane does not preselect a SECURITY DEFINER reader of its own over clio_activity, nor any other reader of a source it does not own; the earlier draft that did so is withdrawn. The candidate contract put to the sibling owner is an SQL-composable, owner-scoped relation — a function or view callable as app_role that yields (matter_id, latest_at, source) for the authenticated viewer only — with the organization and user taken from the app.organization_id / app.user_id GUCs set by withRlsTransaction(db, organizationId, …, { userId }), never from a caller argument, and with the access checks applied inside the relation per source (Clio rows require the viewer's verified Clio identity, an authorized connector and live membership; the viewer's own mail and work-ledger rows require only their own authorization and revocation state), so a wrong caller gets no rows and a missing Clio identity or connector excludes Clio evidence alone, never the independently authorized sources. Being SQL-composable lets listMatterWorkPage join it and order in the same statement (§ 6) instead of materializing every organization matter's evidence in JS and re-injecting it as a VALUES relation, which the earlier draft proposed and which is rejected. The owner settles the exact export (function or view name, package, whether provisioning or a migration ships it) and storage; app_role receives no blanket clio_activity table grant under any outcome (§ 12). Until the owner replies, the seam name in this document and in the plan is provisional and marked so.

Composition. readOwnRecencyEvidence in apps/web/lib/matters/own-recency.ts becomes a thin adapter: it always joins the seam (each source gates itself inside the relation; identity and connector posture only inform coverage), exposes the SQL fragment that references it, and returns the coverage record naming each source's state (clio: verified|unmapped|source_disconnected, mail: available|absent, work_ledger: available|absent). No time window is applied anywhere.

6. C. Read layer: views, filters, ordering, pagination

listMattersPage is replaced on /matters by listMatterWorkPage(organizationId, userId, query: MatterWorkQuery) in read.ts; the existing function stays for the sidebar fallback and tests.

export interface MatterWorkQuery {
  view: "my" | "recent" | "all"
  tab: "active" | "archived" | "disabled"
  q: string
  people: string[]                 // Clio User ids, OR within
  role: "responsible_attorney" | "responsible_staff" | "originating_attorney" | null
  client: string | null            // matter.primary_client_id
  status: "open" | "pending" | "closed" | null
  practiceArea: string | null
  unassigned: boolean
  sort: "name" | "client" | "status" | "recency"
  dir: "asc" | "desc"
  page: number
}

7. D. The /matters page

8. E. Detail: reuse /deals/[dealId]/overview

The placeholder is replaced, not the route. The page keeps getMatter and the closed banner and adds: facts (number, name, client, status, practice area, jurisdiction, opened/closed dates), Team (three labelled roles, unknown state), Documents (same rules as § 7), and "Review my work on this matter" linking to the sibling's authenticated review entry (provisional path /tasks/chats/new?review=work&matter_id=<matter.id>, exact contract from the owner, U3). The link is a completion gate (§ 1): the lane does not ship the detail surface with the entry missing or pointing at a route that does not exist. It shows the viewer's own last contribution with source and coverage, never a team activity list. No new detail route.

9. F. /connections/clio preserved

The admin page keeps the existing MatterTable (per-tracking-state instances, Last sync, NetDocuments column, kebab actions). The work table is a new component; the shared row actions are extracted to one file so archive / disable / re-track behave identically on both pages. No change to the connector controls, conflict list or sync widgets.

10. G. Privacy and revocation invariants

11. H. Schema and access contract implications

12. Rejected alternatives

AlternativeWhy rejected
Rows in matter_entity / matter_party for assignmentsreplaceMatterParties merges by normalized name; two lawyers with one name collapse; a party is a Contact, not a User.
New matter_assignment tableNeeds a migration during a held slot and duplicates the metadata writer's removal and replay semantics for three slots.
Grant app_role SELECT on clio_activityExposes the firm's time entries to the whole web role; the spec 2026-09-14 § C boundary would fall.
Viewer mapping by email or display name against Clio UsersName identity is forbidden; needs a new scope; same-name users produce false My matters.
Admin-asserted mapping tableAsserted, not verified; needs a migration; a verified provider identity is on the sibling's path.
updated_at, last_synced_at, clio_updated_at or received mail as recencySynchronization and inbound mail are not work.
Recent-window filter ("last 30 days")Arbitrary; hides older own work and turns unknown into inactive.
Workspace link from display_number or the reference fieldThe reference is unverified; a fabricated folder URL is worse than a search.
New detail route/deals/[dealId]/overview already guards access and is linked from the table.
Lane-owned SECURITY DEFINER reader over clio_activity (earlier draft)Personal evidence and its readers belong to the sibling ledger lane; preselecting a reader here fixes the contract before the owner agreed it.
Evidence materialized in JS and joined as a VALUES relation (earlier draft)Pulls every organization matter's evidence into the web process per render; the seam must compose in SQL.
Shipping with viewer-identity.ts returning unmapped unconditionally, ledger "absent", review link deferred (earlier draft's "v1")A hardcoded state is a stub, not a feature; the three integrations are completion gates (§ 1).
Relevance-first ordering on top of an explicitly chosen sortA hidden precedence makes "recency" mean something else whenever a search term is present.

13. Unknown seams, named

  1. U1 — Clio null-field read semantics (blocking for null conversion). Whether a requested empty responsible_staff{id,name} / originating_attorney{id,name} comes back as a present null key or is omitted. Verified by a read-only GET on one matter known to lack the assignment, through the existing connector credentials path in the authorized local runtime; the observed payload shape is recorded here. No code converts an omission into null before this is recorded (§ 4).
  2. U2 — Verified viewer identity record. The sibling's personal Clio identity connection and where it stores the verified Clio user id (table, columns, revocation marker). resolveViewerClioIdentity and the evidence relation both read it; the lane's identity code is written against the agreed record, not before it.
  3. U3 — Evidence seam and review entry (owner: sibling ledger lane). The exact export and storage of the SQL-composable owner-scoped evidence relation of § 5, and the review entry path (provisional /tasks/chats/new?review=work&matter_id=). Requested from the sibling; no reply yet. Once the sibling replies, the caller's recorded decisions — completion gates, no lane-owned reader, SQL-composable relation, no blanket grant, required sources — define the final package seam; the roles are settled and are not put to kwiss again. This design never reads work_episode or clio_activity directly.
  4. U4 — Sent-mail provenance. Which matter_link_status values pipeline_runs actually carries for chip = 'sent' rows, who writes them, and which column is the honest send time; the owner reads the live schema and rows rather than assuming values.
  5. U5 — Clio web destination (orchestrator, read-only tools). The regional host: the connector's api_host in provider_metadata is sync_role-only ciphertext, but the orchestrator can establish the host from connector data or the same CLIO_API_HOST default the connector uses (single-region deployment, decision J4) without printing any credential. The matter web path: established from Clio's official documentation or from a connector-side read of a matter's web URL if the API exposes one; the orchestrator does this with read-only tools, no live write, and does not ask the user to look manually. No link renders before both are recorded here.

14. Acceptance traceability

Issue acceptance criterionDesignProof (plan)
Identify matter, client, responsible lawyer and verified team, including missing/unknown§ 4, § 7 Team column and disclosure, § 3 unknown vs unassignedconnector projection tests (observed ref / observed null / unobserved slot); work-table render test for null/unknown; ui-test smoke
Person selection returns correctly assigned matters by stable identity; no same-name or external false matches; removals reflected§ 4 Clio User ids, § 6 predicate, § 4 removalwriter test: observed null clears the slot, partial response keeps the prior snapshot, older replay ignored; predicate tests on seeded rows; e2e person filter
My matters and Recently worked on have distinct meanings; sync never changes recency§ 3, § 5, § 6 (no updated_at in recency)pg ordering test: a sync-only updated_at change moves no row in recency order
Personal signals owner-scoped across queries, filters, summaries, caches, detail§ 5 seam, § 10RLS/grant tests on the evidence relation (owner's suite plus this lane's integration test); two-viewer privacy test (§ 10); e2e second-user check
Ordering/filtering consistent across pagination, ties, missing recency§ 6 exact sort contract with id tiebreak, NULLS LASTpg full-walk pagination test on synthetic data for every sort, including q + explicit sort
Validated external links reach the matter; unverified fall back honestly§ 7 Documents, § 13 U5link tests on the rendered URLs: a verified host + path yields the real Clio destination, a verified ND reference yields its destination, an unverified one renders no link and never a reference-derived URL; review entry resolves to a live route
Reuse existing detail surface; keep /connections/clio usable§ 8, § 9overview render test; admin page unchanged snapshot; ui-test smoke
Real UI verified: keyboard, narrow screen, light/dark, empty/loading/error, behavioral and privacy regressions§ 7 Statesui-test skill smoke (browser-use) plus e2e spec in packages/e2e; existing authorized local server, synthetic accounts

15. Sources