Actionable matters — implementation plan
Draft, 2026-09-15; not approved, nothing implemented. Implements the design spec for NOS-384. This plan goes to kwiss and to independent plan review before any product code. Substantial weight. No migration is generated in this lane (the db:generate slot is held by the dispatcher); no merge or deploy authorization is implied. Plan only: the five workstreams below are proposals for the configured seats, disjoint by file; no seat is spawned and no code is written from this document until kwiss approves it. Scope is the issue's; nothing here adds product scope, and the spec's completion gates (§ 1) apply: no build ships with identity, contributions or the review entry stubbed.
Ownership, seats and order
| Workstream | Seat (configured) | Owner scope (disjoint) |
|---|---|---|
| W1 Connector snapshot | Fable 5.1 high, Claude Code worker (Devin Fusion Fable high on the user's Devin route — recorded deviation) | packages/connector-clio/src/{matters,types}.ts, packages/connectors-core/src/matter-sync.ts, packages/clio-sync/src/matter-metadata.ts, apps/worker-clio/src/loops/webhook-consumer.ts |
| W2 Evidence-seam integration contract | Astra @write high (SQL/integration, non-frontend); RLS review Sol max | Provisional, settled by the sibling owner's reply (U2/U3): the integration test that exercises the sibling's evidence relation from this lane's transactions, and any glue the owner's contract requires on this side (candidate: packages/db/test/matter-work-evidence.integration.test.ts). Which package holds the relation itself is the owner's; these files are not settled until then. |
| W3 Web read layer | Fable 5.1 high | apps/web/lib/matters/{types,read,work-query,own-recency,viewer-identity,links}.ts and their tests |
| W4 Web UI | Fable 5.1 high | apps/web/components/matters/{matter-work-table,matter-filters,matter-row-actions,team-cell,contribution-cell}.tsx, apps/web/components/chat/live-chat.tsx (one optional prop), apps/web/app/(app)/matters/{page,loading}.tsx, apps/web/app/(app)/deals/[dealId]/overview/page.tsx, chats/new/page.tsx |
| W5 Tests for substantial Fable code | Sol or Grok medium (non-Claude, per seat table) | packages/e2e/tests/matters-work-table.spec.ts, pg pagination test under packages/db/test/, the ui-test smoke run (evidence outside the repo) |
| Reviews / gates / commit | Astra high + Grok xhigh deletions + fresh Fable high; Sol max review-rls for W2; gates Sol low; commit Astra low | Orchestrator arbitrates; workers never commit. |
Order: U1 and U5 first (read-only). W1 ∥ W3 (types and query builder first, so W4 codes against a real projection) → W2 as soon as the sibling's contract is agreed → W4 → W5. W3's viewer-identity.ts and own-recency.ts are written against the agreed U2/U3 contract, so W3 completes only after the sibling replies; there is no interim body that returns unmapped or absent unconditionally, and W4 is not signed off until the three completion gates pass end to end on the authorized local surface. Every worker records exact files touched and commands run; a worker exceeding its scope stops and reports.
0. Settle the unknowns first (read-only, no product code)
- U1 — one read-only
GET /api/v4/matters/{id}.json?fields=id,responsible_staff{id,name},originating_attorney{id,name}on a matter known to lack a staff assignment, through the existing connector credentials path in the authorized local runtime (no writes, no credential printed). Record the observed payload shape in the spec § 13. Blocking for W1's null conversion: no slot is writtennullfrom an omitted key until the observed semantics say omission is absence for that endpoint. - U2 / U3 — obtain from the sibling lane, by relay, the verified-identity record, the exact export and storage of the SQL-composable owner-scoped evidence relation (spec § 5 candidate contract), and the review entry path. Nothing ships with these unanswered; the lane does not invent an API and does not ship a stubbed state in their place. Once the sibling replies, the caller's recorded decisions define the final package seam; the roles are settled and are not put to kwiss again.
- U4 — the owner reads the live schema and rows to establish which
matter_link_statusvaluespipeline_runscarries forchip = 'sent', who writes them, and the honest send-time column; the card-sent provenance is admitted or excluded on that evidence, not on statuses nothing writes. - U5 — the orchestrator establishes the Clio region host and the matter web path from official documentation and connector data with read-only tools (no credential printed, no live write, no request to kwiss to look manually) and records both in the spec § 13. No link renders before both.
- Decisions recorded (orchestrator, 2026-09-15): J1 settled — default view is My matters only when identity is verified, otherwise All matters. J2 settled — substantive own contributions qualify;
ExpenseEntryexcluded by default; never a silent narrowing toTimeEntryalone. J3 settled — an automatic card→matter link is not reliable on its own; the owner evaluates actual provenance (U4). J4 — host source per U5. J5 settled — the roster lists people from current known assignment snapshots; a persistedpersonselection for someone no longer assigned stays applied and is shown as such.
1. W1 — assignment snapshot in the connector
types.ts: addresponsible_staff?: ClioMatterContactRef | nullandoriginating_attorney?: ClioMatterContactRef | nulltoClioMatter.matters.ts: extendMATTER_FIELDSwithresponsible_staff{id,name},originating_attorney{id,name};projectMatterMetadataprojectsclio_assignmentsper observed slot (spec § 4): a slot present as a ref → ref, present asnull→null, missing from the payload → unobserved. The key is written and pushed tometadata_requestedonly when all three slots were observed; otherwise the key is left out and the prior snapshot survives. The omission-is-absence rule for the matters endpoint is enabled only from U1's recorded outcome, never from the field list.connectors-core/src/matter-sync.ts: add"clio_assignments"toMatterMetadataKey.clio-sync/src/matter-metadata.ts: add it toOWNED_MATTER_METADATA_KEYS. No other writer change: unit replacement, explicit null and older-replay guard already apply.webhook-consumer.ts:135-137: add the two fields to the inline list; the delete stub at:103-108is unchanged and never observes people slots.- Tests (
packages/connector-clio/src/matters.test.ts,packages/clio-sync/test/matter-metadata.test.ts), behavioral only, no assertions on the field-list string: a payload with staff and originating set projects both refs; a payload with a slot present asnullclears that slot after the write (removal); a payload missing a slot leaves the stored snapshot unchanged after the write (partial response); a stale replay leaves a newer stored snapshot unchanged; the write leavescustom_field_valuesuntouched. Run:cd packages/connector-clio && bun run test;cd packages/clio-sync && bun run test.
2. W2 — evidence-seam integration contract (dependency on the sibling ledger lane)
- This workstream does not create a reader over
clio_activityor any other evidence source: personal evidence belongs to the sibling lane, which ships the relation and its grants in the package it chooses. W2 is the integration step on this side: put the spec § 5 candidate contract to the owner — an SQL-composable owner-scoped relation yielding(matter_id, latest_at, source)for the authenticated viewer only, organization and user from the GUCs, identity / connector / membership checks inside, per-matter maxima only — and, once the owner settles the exact export and storage, wire it intoread.tsas a join and write the integration test below.app_rolereceives no blanketclio_activitygrant under any outcome. - Integration test (file name provisional) on an isolated clone (
DATABASE_URL=<clone> bunx drizzle-kit migrate, thenDATABASE_URL=<clone> DB_APP_LOGIN_USER=<login role> bun run scripts/provision-roles.tsfrompackages/db), exercising the sibling's relation from this lane'swithRlsTransaction(db, organizationId, …, { userId }): directSELECTonclio_activityasapp_roledenied; two seeded viewers each see only their own per-matter maxima; a viewer without a verified Clio identity gets no Clio rows but still gets their own independently authorized mail / work-ledger rows, and a revoked mail or ledger authorization drops only that source; a wrongapp.organization_idgets no rows; expense rows and non-visiblerows do not move any maximum. Run:cd packages/db && DATABASE_URL=<clone> bun run test.
3. W3 — web read layer
types.ts: addMatterAssignmentRef,MatterAssignments,MatterWorkRow,MatterWorkQuery,OwnRecencyCoverage,MatterWorkPage { rows, total, page, pageSize, roster, coverage, sourcePosture }. Export throughapps/web/lib/matters/index.ts.work-query.ts:parseMatterWorkQuery(searchParams): MatterWorkQuery(validation and fallbacks, first-of-array for duplicates exceptperson; records whethersortwas explicit),mattersHref(query, patch)(resetspageon any non-page change),buildWorkPredicate(query, viewerClioUserId)andbuildWorkOrderBy(query)returning DrizzleSQLfragments overmatter_metadata #>> '{clio_assignments,<slot>,id}'; the order-by implements spec § 6's exact contract — relevance is added only whenqis non-empty andsortwas not explicit.viewer-identity.ts:resolveViewerClioIdentityper spec § 5, reading the U2 record; written once U2 is agreed. No interim body.own-recency.ts:readOwnRecencyEvidenceas the thin adapter of spec § 5: always joins the owner's relation (sources gate themselves inside it; identity and connector posture only feedcoverage), exposes the SQL fragment referencing the owner's relation, returnscoverage. No JS materialization, noVALUES.read.ts: addlistMatterWorkPage(organizationId, userId, query)andreadAssignmentRoster(tx, organizationId); keeplistMattersPage; projectassignmentsfrommatter_metadata.clio_assignmentswith a narrow runtime guard (no Zod: internal data).countMattersByTrackingStategains the full predicate. Persistedpersonids outside the roster stay in the predicate (J5).links.ts:clioMatterHref(host: string | null, verifiedPath: string | null, sourceId)returns the real destination when both are set, else null;netDocsWorkspaceHref(reference)returns a destination only forverified: true;findDocumentsHref(matterId);reviewMyWorkHref(matterId)built on the U3-agreed path. Never acceptsdisplay_number; no builder returns a permanently-null value by construction.- Tests (
read.test.tsharness style, newwork-query.test.ts,own-recency.test.ts,links.test.ts), asserting observable results on seeded rows, never SQL text or fragment shape: people OR / categories AND; role narrows the slot;unassignedrequires a present snapshot with both nulls and excludes unknown;view = "my"returns only matters whose working slot equals the verified id; withqand an explicitsort = recencythe returned order is exactlyown_latest_at DESC NULLS LAST, lower(name), id; the same query on the same snapshot returns the sametotaland page rows twice; a viewer's contribution column is identical with and without apersonfilter for another viewer; a row whose only change isupdated_at/last_synced_atkeeps its recency position; link tests on the produced URLs (real Clio destination when host + path are set, real ND destination when verified, no link otherwise, never reference-derived). Run:cd apps/web && bun run test -- lib/matters.
4. W4 — UI
matter-row-actions.tsx: moveRowActionsout ofmatter-table.tsxunchanged;matter-table.tsximports it (admin page behavior identical).matter-work-table.tsx(client component, no sort state): columns per spec § 7; sort headers are labelled, keyboard-reachableLinks built withmattersHrefinside eachth, witharia-sorton thethonly (spec § 7); the wrapper is<div role="region" aria-label="Matters" tabIndex={0} className="overflow-x-auto rounded-lg border border-border">.team-cell.tsxrenders the two working roles as wrapping text plus a Popover disclosure (pattern:netdocs-status-cell.tsx) listing all three roles with the originating caveat and the unknown/unassigned/stale states.contribution-cell.tsxrenders the date and a disclosure with source and coverage reason. A "Sync details" disclosure holds Last sync, tracking state andNetDocsStatusCellcontent.matter-filters.tsx: toolbar with search (MatterSearchInputextended to preserve other params), People combobox (multi,packages/uicombobox), Role/Client/Status/Practice selects (native-selectorselect), Unassigned toggle, Reset link; all emit URL state, wrap withflex-wrap, every control labelled.matters/page.tsx: parse the query, resolve identity, runlistMatterWorkPageand counts in parallel, render segmented views, source-disconnectedrole="status"warning, table, footer (existingPaginationFooteradapted tomattersHref), empties per view. Addloading.tsx.deals/[dealId]/overview/page.tsx: replaceEmptywith facts, Team, Documents and "Review my work on this matter" on the U3-agreed path (completion gate, not conditional rendering); keepClosedBanner.live-chat.tsx: optionalinitialComposerText?: stringprop (prefill only, never auto-send);chats/new/page.tsxmaps?intent=find_documentsto a fixed matter-scoped document search sentence and shows which document sources are admitted for the matter (spec § 7: source admission visible, no claim of a verified workspace).- Tokens and themes per
DESIGN.md; status pill reuses the existing classes.bunx prettier --writeon touched files only.
5. W5 — behavioral and privacy regressions
- UI smoke —
ui-testskill, mandatory. The smoke check runs through theui-testskill (Tier 1, localbrowser-useCLI) against the already-running authorized local server with synthetic accounts; no dev server is started by this lane. If the localbrowser-usecapability is absent on the box, the seat first obtains a supported local install by following theui-testskill and the official browser-use documentation (no asserted command here) rather than substituting another runner: a hand-driven Playwright session is not a replacement for the mandated check, and no dev server is started to work around it. Smoke covers the three views, one person filter, a sort header, the Team and contribution disclosures, 375 px, light and dark, and the second-user privacy check below, with screenshots kept as evidence outside the repo. packages/e2e/tests/matters-work-table.spec.ts(permanent regression guard, same server and accounts): views switch and keep filters; person OR and category AND reflected in URL and rows; a persistedpersonfor someone no longer assigned stays applied and labelled; Unassigned excludes "Not yet synced"; sort headers change order and reset page; with a search term and an explicit recency sort the visible order follows recency, not relevance; keyboard reaches region, headers, disclosures and menus; 375 px width wraps the toolbar and scrolls the table; light and dark; loading, error and each empty state; a second synthetic user with a person filter set to the first user sees assignments but no contribution data of the first user; rendered document links point at real destinations or are absent (no placeholder href);/connections/cliostill renders its tables and actions.- pg pagination test (isolated clone, synthetic matters with identical names and null recency, plus a search term with explicit sort): no row appears on two pages or on none across the full walk for each sort, and two walks over the same snapshot return identical pages and
total.
Gates and status
- Root
bun run lintandbun run typecheck; package tests for connector-clio, clio-sync, connectors-core, db (isolated clone), web;bun run eval:gate:cheapis not required (no retrieval, routing or memory change) unless theLiveChatprefill touches routing fixtures — decide at gate time and record. - Reviews to convergence: Astra high, Grok xhigh deletions, fresh Fable high; Sol max
review-rlson W2 and on any grant or RLS change the seam integration brings; the review prompt names the deletion audit (removed client-side sort, movedRowActions) and the completion gates (reviewers reject any unconditionalunmapped/absentbody or missing review entry). - Status:
bun docs/superpowers/status.ts set actionable-matters in-progresswhen implementation starts,implementedin the PR that lands it; Linear NOS-384 moves after the merge lands, never in a commit.
Acceptance — check only after attached evidence
- List shows matter, client, responsible lawyer, responsible staff and unknown/unassigned states from real snapshots; originating lawyer visible only under its own label.
- Person filters match by Clio User id; a same-name second user and an external contact never match; an observed removal in Clio clears the slot after the next poll or webhook and a partial payload never clears one (writer tests plus one observed sync).
- My matters, Recently worked on and All matters are documented in the page and behave distinctly; a sync-only change does not move any row in recency order.
- Viewer identity resolves from the verified record, "Your last contribution" shows genuine own contributions from the sibling seam, and "Review my work on this matter" reaches the live review entry — all three on the authorized local surface (completion gates).
-
app_rolehas noclio_activitytable grant; the evidence relation refuses cross-organization and cross-user reads; a person filter never changes the viewer's contribution column; no per-person totals exist on any surface. - Full-walk pagination is stable for every sort with ties and null recency; an explicit recency sort with a search term orders by recency alone.
- Clio and NetDocuments links are real destinations when the evidence is verified and absent otherwise, never reference-derived; Find documents opens the matter-scoped chat with a prefilled, unsent request and visible source admission.
-
/deals/[dealId]/overviewshows facts, team, documents and the review entry;/connections/cliounchanged in behavior. - UI evidence through the
ui-testskill plus e2e: keyboard, 375 px, light/dark, loading/error/empties, privacy check with two synthetic users, recorded from the existing authorized local server.
Verification and evidence boundary
Workers attach exact commands and outputs. Database evidence comes from an isolated clone provisioned with the real roles, every write rolled back; no shared dev database, no live preprod, no live Clio or NetDocuments writes. UI evidence comes from the existing authorized local server and synthetic accounts; no dev server is started by this lane. Documentation checks (bun docs/superpowers/status.ts sync, page parse) are not feature verification. Tooling for the Devin route is recorded in the lane ledger, not in this document.