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
- Clio's official guide names three matter people fields, all Users of the firm, each with a list filter:
responsible_attorney,originating_attorney,responsible_staff; clients and related people are Contacts (docs.developers.clio.com, Guides › Clio Manage › Matters › People on a Matter). Regional UI labels differ (Attorney / Lawyer / Solicitor); the API field names do not. - The connector requests only
responsible_attorney{id,name}(packages/connector-clio/src/matters.ts:46,60), maps it toresponsible_attorney_source_id/name(:111-119), andprojectMatterMetadatawrites only present keys withmetadata_requested(:150-182); the writer replaces requested keys as units and never writes an omission as null (packages/clio-sync/src/matter-metadata.ts:17-44,77-95). The webhook consumer re-fetches with its own inline field list (apps/worker-clio/src/loops/webhook-consumer.ts:135-137). replaceMatterPartiesmerges people intocanonical_entityby normalized name (packages/clio-sync/src/matter-events.ts:233-267,ON CONFLICT (organization_id, kind, canonical_name_normalized)): it cannot serve identity and is left untouched.clio_activitystoresuser_source_idas a Clio id,date,type,observation_state(packages/db/src/schema/clio_activity.ts:56-57,70,82-86);app_roledeliberately has no grant (packages/db/scripts/provision-roles.ts:833-839). A GUC-boundSECURITY DEFINERreader created fromprovision-roles.tsis an existing pattern (working_profile_answers_for_admin,:517-574).- Web reads:
listMattersPageorders byupdated_at DESC, id DESCin SQL and attaches NetDocuments status after pagination (apps/web/lib/matters/read.ts:409-429);getMatterreturns the full row plus parties (:106-160)./deals/[dealId]/overviewrenders anEmptyplaceholder under the closed banner (apps/web/app/(app)/deals/[dealId]/overview/page.tsx:46-49); the layout already guards matter existence (layout.tsx:38-39). The matter-scoped new-chat page passesmatterIdtoLiveChat(chats/new/page.tsx:42-47);LiveChathas no composer prefill prop (grep, 2026-09-15). - NetDocuments:
MatterNetDocsStatus.workspaceReferenceis typed{ present: boolean; verified: false }(packages/agent-runtime/src/tools/matter-netdocs-status.ts:27); no verified workspace destination exists anywhere today. Matter-scoped search exists only as thematter_idmode ofnetdocs_searchon the product toolbelt and MCP (cross-source spec § G). - Viewer identity: the Clio credential bundle's
provider_metadatacarries onlyapi_hostandtoken_type(packages/connector-clio/src/auth.ts:189-193) and issync_role-only ciphertext (apps/connectors-api/src/lib/connectors.ts:886-936); no Better Auth user ↔ Clio user mapping exists. The sibling ledger proposes a personal Clio identity connection verified throughGET /api/v4/users/who_am_i(its § 3.2); it is not implemented. - Mail: the local corpus is received mail (
mail_classifications.mail_sender,mail_received_at);pipeline_runscarriesuser_id,matter_id,matter_link_statusand achipwhosesentvalue means a card reply sent through North (packages/db/src/schema/pipeline.ts:135,168-190,mail.ts:47-59). No mailbox Sent Items evidence is stored locally; the ledger lane reads Sent Items live. - Owner-scoped RLS through
app.user_idalready exists (packages/db/src/rls.ts:13-44;matter_accesspolicyschema/matter.ts:276-277).
3. Definitions
| Term | Meaning in this design |
|---|---|
| Assignment | Clio responsible_attorney or responsible_staff on the matter, identified by Clio User id. Displayed as "Responsible lawyer" and "Responsible staff". |
| Originating lawyer | Clio originating_attorney. Displayed in the team disclosure under its own label, filterable by explicit role choice, excluded from My matters and from Unassigned. |
| Participation | Any own contribution evidence (§ 5). Never implies assignment; assignment never implies participation. |
| Unassigned | Observed 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 contribution | The 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 scope | Matters visible to the firm through RLS and tracking_state; no per-user Clio permission model is claimed. |
4. A. Assignment snapshot on the matter
- Fields.
MATTER_FIELDSand the webhook consumer's inline list addresponsible_staff{id,name}andoriginating_attorney{id,name}.MATTER_SEARCH_FIELDS(picker, index) is unchanged.ClioMattergains the two optional refs (packages/connector-clio/src/types.ts:50pattern). - Storage. One typed owned key
clio_assignmentsinmatter_metadata, added toMatterMetadataKey(packages/connectors-core/src/matter-sync.ts:15-20) andOWNED_MATTER_METADATA_KEYS. Shape:{ responsible_attorney: {id,name}|null, responsible_staff: {id,name}|null, originating_attorney: {id,name}|null }; ids are Clio User ids as strings, names are display hints only. No new table, no migration, no duplicate assignment schema: the requested-field writer already gives unit replacement, explicit-null removal and older-replay protection. - Observation, removal and unknown. Requesting a field does not prove that its omission in the response means removal; only Clio's actual read semantics do (§ 13, U1, verified read-only before any null conversion is coded). The projection therefore works per slot from what was observed: a slot present as a ref is an assignment, a slot present as
nullis an observed absence and is writtennull(a removal in Clio clears the slot on the next poll or webhook), and a slot missing from the response is unobserved.clio_assignmentsentersmetadata_requestedonly when all three slots were observed in that response; a partial or unobserved response leaves the key out of the requested set, so the writer's unit replacement keeps the prior snapshot intact and a matter never moves from assigned to Unassigned because a payload was thin. If U1 shows Clio omits requested empty associations, the omission-with-request case becomes an observed absence for that endpoint only, recorded in § 13 with the observed payload, never assumed.responsible_attorney_source_id/nameonMatterEventstay formatter-info. - Roster and entity graph. The person filter's option list is derived at read time from distinct Clio User ids across the organization's current snapshots, one entry per id with its last-seen name (one grouped JSONB query, ≤ hundreds of rows); people without North accounts appear because the identity is the Clio id. No Users endpoint call, no new scope. A persisted
personin the URL that is no longer in the roster (the person lost every assignment) stays applied and is shown in the toolbar as a selected chip labelled "no current assignments" (decision J5). The snapshot writes nocanonical_entity,matter_entityormatter_partyrow: assignments never enter the entity graph, so no duplicate person node can arise from them.
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.
| Source | Qualifying rule | Provenance to establish (owner) |
|---|---|---|
| Own Clio contributions | clio_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-linked | Substantive 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 episodes | Active, 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 changes | Admitted 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 activity | Never 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
}
- Predicate. Categories AND; people OR. Without
role, a person matches if either working slot's id equals theirs;rolenarrows to that slot, andoriginating_attorneymust be chosen explicitly.unassigned=clio_assignmentspresent AND both working ids null.qkeepsbuildTermClausesover number, name and client.clientmatchesprimary_client_id;practiceAreamatchespractice_areaexactly;statusmatches lifecycle status.view = "my"adds "either working slot = viewer's verified Clio id" and is refused (rendered as the unmapped state, no query) when identity is unmapped.view = "recent"adds "has own evidence". - Ordering, in SQL, before pagination — exact contract. Let
own_latest_atbe one value per matter:max(latest_at)over the joined evidence relation (§ 5),NULLwhen no own evidence exists.sort = "recency"orders by exactlyown_latest_at DESC NULLS LAST, lower(name) ASC, id ASC(dir = "asc"flips only the first term, nulls still last);name/client/statusorder by that column indir, thenlower(name) ASC, id ASC. Search relevance is never an ordering term when the user has chosen a sort: with a non-emptyqand an explicitsort,qfilters and the predicate above orders, nothing hidden precedes it. Relevance ordering applies only whenqis non-empty andsortis absent from the URL. Defaults whensortis absent andqempty:all/my→ name asc;recent→ recency desc. For one database snapshot the same URL yields the sametotaland the same rows on every page, in the same order. The page-local client sort is removed from the work table. - Pagination.
count(*)and rows share the predicate and transaction; page clamps to the last page;attachNetDocsStatusand the assignment projection run after pagination as today. Tab badges (countMattersByTrackingState) take the full filter, not onlyq. - URL contract.
/matters?view=&tab=&q=&person=&person=&role=&client=&status=&practice=&unassigned=1&sort=&dir=&page=; all server-validated with fallbacks; any filter, view or sort change resetspage; one URL builder (mattersHref) serves tabs, filters, sort headers and pagination. - Projection.
MatterWorkRow extends MatterSummarywithpracticeArea,assignments: MatterAssignments | null(null = unknown),ownLastContribution: { at: string; source } | null,documents: { netDocs, clioHref: string | null, findDocumentsHref: string }, plus page-levelcoverage,rosterandsourcePosture(Clio connector authorized or not) for the toolbar and disclosures.
7. D. The /matters page
- Columns. Matter (name primary, reference secondary, link to
/deals/[id]/overview) · Client · Status · Team · Your last contribution · Documents · row menu. Team shows "Responsible lawyer: Name" and "Responsible staff: Name" as text (names wrap; no avatar-only rendering), "Unassigned" for observed absence, "Not yet synced" for unknown; a keyboard- and touch-reachable disclosure lists all three roles with labels, including "Originating lawyer (not a working assignment)". Your last contribution shows a date and its source label in the disclosure ("From your Clio time" / "From mail you sent" / "From your work review"), or "—" with the coverage reason ("Connect your Clio identity to see your own work", "Clio disconnected", "Work review not available"). - Diagnostics leave the reading path. Last sync, tracking state and the NetDocuments status details move into a per-row "Sync details" disclosure (reusing
NetDocsStatusCell's popover content). A page-level source-disconnected warning (role="status") appears when the organization Clio connector is not authorized: assignments shown are the last observed, Clio time no longer contributes to recency (mail and work-review evidence keep contributing under their own authorization), and My matters still requires a verified Clio identity, with a link to/connections/cliofor admins. - Views and toolbar. Segmented views My matters / Recently worked on / All matters; the tracking tabs remain. Default is My matters only when identity is verified, otherwise All matters (decision J1, settled). Toolbar: search, People (multi-select combobox from the roster, OR; a persisted selection outside the roster stays applied per § 4), Role (enabled when ≥ 1 person), Client, Status, Practice area, Unassigned toggle, Reset. It wraps on narrow widths; every control is labelled; filters are links or form submissions that produce URL state.
- Documents. Real, validated provider destinations are built whenever the evidence supplies them; a link is never permanently false and never permanently withheld by design. "Open in Clio" renders once the region host and the matter web path are established from official or connector data (U5, settled by the orchestrator with read-only tools, not by asking the user to look); until then it is not rendered. "Open NetDocuments workspace" renders for a verified destination — when a source (ND workspace lookup or the connector) yields
workspaceReference.verified = truewith a destination, the link is built from it; while the reference is only a presence marker (verified: false, the state today) no link is derived from a matter number or reference string. "Find documents" is always offered for eligible Clio matters as the search fallback: it opens the matter-scoped chat (/deals/[id]/chats/new?intent=find_documents), where the additiveintentparameter pre-fills the composer with a matter-scoped document search request through a new optionalLiveChatinitialComposerTextprop; nothing is auto-sent. The fallback is acceptable only with its source admission visible: the chat surface states which document sources are admitted for this matter (NetDocuments, OneDrive, matter documents) and the entry is labelled "Find documents", never "Open workspace" — it does not claim a workspace was verified. When no feeder is admitted the agent's ownnetdocs_searchgating answers honestly. - States.
loading.tsxskeleton with the same column count; error boundary with retry; empties: unmapped My matters (explanation, switch to All), Recently worked on with no evidence (what counts as evidence, no productivity language), filter with no match, tab empties as today. Table is wrapped in a focusableregionwith an accessible name and horizontal overflow; sort headers are labelled, keyboard-reachable URL links (Linkbuilt withmattersHref) inside theth, andaria-sortis set on thethonly, never on the link; light and dark through semantic tokens only (DESIGN.md:bg-card,text-muted-foreground,border-border,focus-visible:ring-ring, radius pyramid).
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
- Personal signals are computed only inside
withRlsTransaction(db, organizationId, …, { userId })from the render session, never from a filter or route parameter; selecting people changes the predicate on assignments only. A behavioral test asserts it: with two seeded viewers, viewer A's page withperson = Bshows B's assignments and A's own contributions, never B's. - No organization-wide activity leaves the database:
app_rolenever gains aclio_activitytable grant; the evidence relation yields per-matter maxima for the caller only; no totals, hours, counts or per-person aggregates exist on any surface, cache or summary. - Revocation: identity connection revoked or Clio connector deauthorized → identity resolves
unmapped/ coveragesource_disconnectedon the next render; the page is dynamic (session-bound), uses no data cache, and detail and list share the same read functions. - Assignments are firm data (visible to every member, as in Clio's matter list); the originating label never widens My matters.
11. H. Schema and access contract implications
- No migration from this lane: the snapshot lives in the existing JSONB column. Whether the evidence relation ships through
provision-roles.tsor a sibling migration is the owner's call under the seam contract (§ 5); thedb:generateslot is held by the dispatcher and this lane never takes it. - Deferred, requires the slot: an expression index on
(organization_id, (matter_metadata #>> '{clio_assignments,responsible_attorney,id}'))and its staff twin if the person filter measurably exceeds budget at ~5,000 matters. Not planned for v1. - Grants and RLS review (
review-rls, Sol max) is mandatory on the integration of the evidence relation, whichever package ships it.
12. Rejected alternatives
| Alternative | Why rejected |
|---|---|
Rows in matter_entity / matter_party for assignments | replaceMatterParties merges by normalized name; two lawyers with one name collapse; a party is a Contact, not a User. |
New matter_assignment table | Needs 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_activity | Exposes 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 Users | Name identity is forbidden; needs a new scope; same-name users produce false My matters. |
| Admin-asserted mapping table | Asserted, 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 recency | Synchronization 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 field | The 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 sort | A hidden precedence makes "recency" mean something else whenever a search term is present. |
13. Unknown seams, named
- 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 presentnullkey 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 intonullbefore this is recorded (§ 4). - 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).
resolveViewerClioIdentityand the evidence relation both read it; the lane's identity code is written against the agreed record, not before it. - 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 readswork_episodeorclio_activitydirectly. - U4 — Sent-mail provenance. Which
matter_link_statusvaluespipeline_runsactually carries forchip = '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. - U5 — Clio web destination (orchestrator, read-only tools). The regional host: the connector's
api_hostinprovider_metadataissync_role-only ciphertext, but the orchestrator can establish the host from connector data or the sameCLIO_API_HOSTdefault 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 criterion | Design | Proof (plan) |
|---|---|---|
| Identify matter, client, responsible lawyer and verified team, including missing/unknown | § 4, § 7 Team column and disclosure, § 3 unknown vs unassigned | connector 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 removal | writer 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, § 10 | RLS/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 LAST | pg 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 U5 | link 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, § 9 | overview 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 States | ui-test skill smoke (browser-use) plus e2e spec in packages/e2e; existing authorized local server, synthetic accounts |
15. Sources
- Clio Developers — Guides › Clio Manage › Matters (People on a Matter; list filters
responsible_attorney_id,originating_attorney_id,responsible_staff_id; Related Contacts are Contacts):https://docs.developers.clio.com/guides/clio-manage/matters/, read 2026-09-15. - Clio Developers — FAQ, "Which API field is the Responsible Staff / Responsible Attorney":
https://docs.developers.clio.com/faq/. - Repository files cited inline with line numbers as of
268f19bb; Linear NOS-384 description and acceptance criteria fetched 2026-09-15.