docs(specs): documents hub split + auto-filed client folders

Design for unifying /documents and /documents/files under a single hub
with stacked Signing/Files sections, owner-grouped aggregation across
the relationship graph, and three system-managed entity-folder roots
(Clients/Companies/Yachts) with lazy per-entity subfolders. Documents
hub stays anchored on document_folders; files gain folder_id; signed
PDFs auto-deposit on Documenso completion. Includes 14+ edge-case
decisions, schema deltas, backfill plan, and implementation surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-10 22:50:31 +02:00
parent ef63e86fde
commit 286eb51f81

View File

@@ -0,0 +1,375 @@
# Documents Hub Split + Auto-Filed Client Folders
**Status:** Draft — awaiting final review
**Date:** 2026-05-10
**Builds on:** Wave 11.B `feat/documents-folders` (per-port nestable `document_folders` tree, soft-rescue delete, sibling-name uniqueness)
## Overview
Today the CRM has two parallel document surfaces that confuse reps:
1. `/[port]/documents` — Documenso signature workflows only (rows in `documents`). Hub tabs are signing-status (`in_progress` / `awaiting_them` / `awaiting_me` / `completed` / `expired`). Carries the new `document_folders` tree (Wave 11.B).
2. `/[port]/documents/files` — bare uploaded files only (rows in `files`). Has its **own** "folder" mechanism driven by `storagePath` prefix matching, completely disconnected from `document_folders`.
The signed PDF that Documenso produces lives in the `files` table (`documents.signed_file_id` points at it), but it has no folder home and no entity-driven grouping — reps can't find a client's signed contracts without going through the signing workflow row first.
This spec unifies both surfaces under a single hub with a stacked **Signing in progress / Files** layout, anchored by a per-port nestable folder tree that gains three system-managed roots (`Clients/`, `Companies/`, `Yachts/`). Each entity gets one auto-created subfolder on first need; signed PDFs from completed workflows auto-deposit into the owner's folder. The folder view is **owner-aggregated**: opening `Clients/Smith, John/` surfaces files attached to John, plus files of his linked companies and yachts, each rendered as a labelled subsection.
## Conceptual model
Three first-class concepts after this spec ships:
- **File** (`files` row) — a stored binary artifact (PDF/image/etc.) with one `folder_id` and entity FKs (`client_id` / `company_id` / `yacht_id`). The canonical "document" reps file and find. Produced by either direct upload or as the output of a completed signing workflow.
- **Signing workflow** (`documents` row) — the *process* of getting a PDF signed via Documenso. Lifecycle `draft``sent``partially_signed``completed`. Surfaces in the hub's Signing section while in-flight. On completion, produces a signed-PDF file; the workflow row itself becomes audit history accessed via a "view signing details" link on the resulting file. Stops appearing in user-facing folder views.
- **Folder** (`document_folders` row) — per-port nestable tree (existing). Extended to hold both files and in-flight workflows. Gains three system-managed roots and per-entity auto-subfolders.
`documents.folder_id` stays meaningful for in-flight workflows (rep can file by deal/project). Becomes irrelevant on completion — the rendering layer hides completed workflows from folder views entirely.
`files.folder_id` is **new** (not in current schema) — added by this spec.
## Scope boundaries
### In scope
- New `files.folder_id` column + index, FK to `document_folders.id`
- `document_folders` schema additions: `system_managed`, `entity_type`, `entity_id`, `archived_at`
- Three system roots (`Clients/`, `Companies/`, `Yachts/`) auto-created on port init
- Lazy per-entity subfolder creation on first auto-deposit or first manual upload
- Auto-deposit logic in `handleDocumentCompleted` (set `files.folder_id` + entity FKs on signed PDF)
- Owner-resolution chain (Owner-wins: `client_id ?? company_id ?? yacht_id` on workflow, falling back to interest)
- Owner-aggregation projection in the files & documents listing endpoints
- Symmetric relationship walking (Client ↔ Company ↔ Yacht via memberships and ownership)
- Hub UI rebuild: stacked Signing/Files sections, owner-grouped headers, system-folder 🔒 markers
- "View signing details" dialog on signed-PDF file rows
- System-folder protection: rename/move/delete blocked at API + UI
- Entity rename auto-syncs system folder name (transactional)
- Entity archive applies `(archived)` suffix; entity hard-delete demotes to user folder with `(deleted)` suffix
- Search box scope: current folder + descendants, results across both Signing and Files
- Hub root view (no folder selected): port-wide Signing + recent Files
- One-time backfill script: ensure system folders exist, set `files.folder_id` from entity FKs, copy entity FKs from completed workflows onto signed files
- Removal of `/[port]/documents/files` route (301 redirect to `/[port]/documents`)
- Removal of the legacy `storagePath`-prefix folder rendering
### Explicitly out of scope
- Permission/role changes beyond what `documents.view` and `documents.manage_folders` already gate
- Bulk file actions (multi-select move, multi-select download zip) — separate work
- Tagging or labels on files — separate work
- Trash / restore for hard-deleted files (current behavior preserved)
- Search across file *content* (full-text PDF search) — current behavior preserved (search is title/filename only)
- Per-port admin override for aggregation symmetry (rejected as needless setting at E11)
- Per-user feature flag rollout — hard cutover (E rollout decision)
- Native PDF preview rebuild — existing `FilePreviewDialog` reused
## Folder tree structure & governance
### System-managed roots and subfolders
Three reserved root folders are auto-created when a port is initialised:
```
Clients/
Companies/
Yachts/
```
Per-entity subfolders are created **lazily on first need** — when a workflow completes for that entity, when a rep manually uploads a file scoped to that entity, or when a rep clicks "Open folder" on the entity's detail page. Empty entities don't appear in the tree.
Subfolder naming:
- Default name = entity display name (client `firstName lastName` / company `name` / yacht `name`).
- Numeric collision suffix: `Smith, John (2)`, `Smith, John (3)`, etc. Suffix appended to the *new* (later-created) folder; existing folder names never change due to collision.
- Auto-rename on entity rename — runs in the same DB transaction as the entity update.
- Entity archive: `(archived)` suffix appended, folder shown muted in tree, auto-deposit blocked until restored.
- Entity hard-delete: `(deleted)` suffix appended, `system_managed` flipped to `false` (folder demoted to a regular user folder; rep can rename/move/delete normally).
### System-folder protection
When `system_managed = true`:
- Rename API rejects with `ConflictError("System folders can't be renamed")`.
- Move API rejects with `ConflictError("System folders can't be moved")`.
- Delete API rejects with `ConflictError("System folders can't be deleted")`.
- UI hides rename/move/delete actions in `FolderActionsMenu` for these rows.
- UI displays a 🔒 marker next to the folder name.
The three roots themselves (`Clients/` / `Companies/` / `Yachts/`) are also `system_managed = true` and protected identically.
### User folders
User-created folders sit alongside the three system roots and inside any other folder (subject to existing depth/cycle rules from Wave 11.B). Standard CRUD via `documents.manage_folders` permission. Examples reps will create: `Templates/`, `Compliance/`, `Marketing PDFs/`.
## Routing on workflow completion
`handleDocumentCompleted` (in `src/app/api/webhooks/documenso/route.ts`) currently:
1. Verifies the Documenso secret.
2. Downloads the fully signed PDF.
3. Creates a `files` row for the signed PDF.
4. Sets `documents.signed_file_id` to the new file id.
5. Updates `documents.status = 'completed'`.
This spec extends the handler with steps 3a, 3b, 3c — inserted between (3) and (4):
```
3a. resolveOwner(workflow):
candidates = [
workflow.client_id,
workflow.company_id,
workflow.yacht_id,
workflow.interest?.primary_client_id,
workflow.interest?.primary_company_id,
workflow.interest?.primary_yacht_id,
]
return first non-null candidate (with its entity_type) OR null
3b. if owner != null:
folder = ensureEntityFolder(port_id, owner.entity_type, owner.entity_id)
// INSERT … ON CONFLICT (port_id, entity_type, entity_id) DO NOTHING RETURNING id
// re-SELECT on conflict to get the existing folder's id
file.folder_id = folder.id
// copy entity FK to file row if not already set (so aggregation reads file FKs as source of truth)
file[`${owner.entity_type}_id`] ??= owner.entity_id
3c. if owner == null:
file.folder_id remains null
// file lives at root, surfaced in the root-view Files section
```
Owner resolution happens at **completion time**, not creation time — if the rep edited the workflow's owner mid-signing (rare), the signed PDF lands in the most recent owner's folder.
The workflow's own `folder_id` is not touched. After `status = 'completed'`, the rendering layer hides the workflow from folder views; only the resulting signed file is visible (with a "view signing details" link to the workflow + signers + events timeline).
## Owner-aggregation projection
The killer feature. When a rep opens an entity folder (`Clients/Smith, John/`), the listing query is **not** a simple `WHERE folder_id = …` — it's a projection that walks the relationship graph and groups results by owner-source.
### Aggregation graph
Aggregation is **symmetric** (E aggregation reach decision). Walking from any entity, surface files attached to:
- the entity itself (DIRECTLY ATTACHED)
- linked clients via `company_memberships`
- linked companies via `company_memberships` and via yacht ownership
- linked yachts via current ownership (`yachts.current_owner_type` + `current_owner_id`)
- + any second-degree links (e.g., `Clients/Smith` shows files of `Smith Marine LLC`'s yachts via the chain Smith → Smith Marine LLC → owned yachts)
Each result group is rendered with a labelled header: `DIRECTLY ATTACHED · 3`, `FROM COMPANY — SMITH MARINE LLC · 1`, `FROM YACHT — MV SERENITY · 2`, etc. Files lived where they were physically filed (e.g., `Yachts/MV Serenity/`); the aggregation only borrows them for display, with a `lives in <path>` caption per row.
### Source-of-truth: file FKs
Aggregation reads each file's own `client_id` / `company_id` / `yacht_id` (snapshotted at upload/creation time), **not** the linked entity's current relationships. This makes yacht ownership transfer a no-op for historical files: a file uploaded for John when he owned MV Serenity stays under John's view forever, even after the yacht is sold to Mary. Mary's view shows files uploaded after the transfer (which carry `client_id = Mary`). Both clients' folders coexist with their respective historical artifacts.
### Per-group pagination
Each owner-source group renders its top 20 rows by `created_at desc`. When a group has more, a `Show all (148)` link drills into a flat paginated list scoped to that source. Keeps page render bounded for large portfolios (200+ yacht leasing clients).
### Defense-in-depth port_id
Every join in the aggregation SQL filters `port_id = $port` — at the entity table, at the membership table, at the yacht table, at the file table. Project pattern (per CLAUDE.md "defense-in-depth port_id scope" / berth recommender precedent). Single-place port_id check at the entry point alone is rejected — it bit the recommender exactly once and we fixed it the same way.
## UI layout
### Layout A: stacked sections, owner-labelled groups inside each
Confirmed in mockup review.
```
┌─────────────────────────────────────────────────────────────────────┐
│ /port-nimara/documents → Clients / Smith, John 🔒 │
├──────────────┬──────────────────────────────────────────────────────┤
│ FOLDERS │ Clients Smith, John 🔒 [Upload] [+ Sign] │
│ │ │
│ 📁 Clients │ ⏳ SIGNING IN PROGRESS · 2 │
│ 📁 Smith…🔒│ FROM CLIENT │
│ 📁 … │ ▢ EOI · Berth A12 · sent 2d ago Awaiting them │
│ 📁 Companies│ FROM YACHT — MV SERENITY │
│ 📁 Yachts │ ▢ NDA · sent yesterday Awaiting them │
│ │ │
│ 📁 Templates│ 📎 FILES │
│ 📁 Complian.│ DIRECTLY ATTACHED · 3 │
│ │ ▢ Signed EOI · A11.pdf signed Apr 14 · view sig… │
│ + New folder│ ▢ Passport scan.pdf uploaded Mar 2 │
│ │ │
│ │ FROM COMPANY — SMITH MARINE LLC · 1 │
│ │ ▢ Articles of inc.pdf · lives in Companies/… │
│ │ │
│ │ FROM YACHT — MV SERENITY · 2 │
│ │ ▢ Signed NDA.pdf · lives in Yachts/… │
│ │ ▢ Survey report.pdf · lives in Yachts/… │
└──────────────┴──────────────────────────────────────────────────────┘
```
Layout primitives:
- **Left panel:** existing `FolderTree` extended for 🔒 markers and `system_managed`-aware action suppression (rename/move/delete hidden in `FolderActionsMenu`).
- **Main panel:** breadcrumb + actions row, then stacked Signing/Files sections. Each section has its in-section grouped headers.
- **Signing section:** hidden entirely when no in-flight workflows match the entity scope. When present, renders above Files.
- **Files section:** always present (may be empty with placeholder).
- **"View signing details" link:** appears on rows for signed-PDF files (those whose source can be traced via `documents.signed_file_id`). Click opens `<SigningDetailsDialog>` — modal showing signers, events, timeline, signed-at timestamps.
### Hub root view (no folder selected)
Default landing when rep clicks Documents in the sidebar:
- **Signing section:** all in-flight workflows port-wide (effectively today's `/[port]/documents` hub behavior, minus the signing-status sub-tabs which collapse).
- **Files section:** recently uploaded/modified files port-wide, paginated by `updated_at desc`.
The folder tree on the left is the primary navigation; root view is the "I just opened the hub, show me what's recent" landing.
### Old `/[port]/documents/files` route
Removed. Server-side 301 redirect to `/[port]/documents`. The `<Files…>` components and the legacy `storagePath`-prefix folder code are deleted.
### Hub-tab simplification
Today's signing-status tabs (`in_progress` / `eoi_queue` / `awaiting_them` / `awaiting_me` / `completed` / `expired`) collapse into one Signing section — the rep will filter by signer-status via in-section chips if needed, but the dominant navigation is folders, not signing-status. The `documentsHubTabs` enum + `tab` query param are removed; `hub-counts` API endpoint is reduced to "in-flight count" only (used for the Signing section's counter badge).
## Edge cases — decisions
| ID | Edge case | Decision |
|----|-----------|----------|
| E1 | Entity renamed | System folder name auto-syncs in the same transaction. |
| E2 | Two entities collide on folder name (e.g., both "Smith, John") | Append numeric suffix `(2)`, `(3)` to the **new** colliding folder. Existing folders never change. |
| E3 | Entity archived | Folder stays with `(archived)` suffix, muted style. Auto-deposit halts. |
| E4 | Entity hard-deleted | Folder gets `(deleted)` suffix, `system_managed` flips to `false` (rep can clean up). Files retain orphaned data. |
| E5 | Yacht ownership transferred | Files snapshot their entity FKs at upload time. Old client folder retains historical files; new client gets a new folder for files created after transfer. Both coexist. |
| E6 | Workflow's owner FK changes mid-signing | Resolve owner at completion time. Signed PDF lands in current owner's folder. |
| E7 | Rep moves a file out of a system folder | Allowed. `folder_id` changes; entity FK is unchanged so aggregation still surfaces it via FK. The "lives in …" caption updates. |
| E8 | Rep manually uploads into an entity folder | Auto-set the file's matching entity FK from the destination folder's `entity_type` + `entity_id`. Custom folders → no auto-mapping. |
| E9 | Workflow has no entity at all | Signed PDF lands at root with `folder_id = null`. Surfaces in root-view Files section only. |
| E10 | File/workflow attached to interest only, interest has no resolved owner | Same as E9 — root, null folder. Manual move or future backfill resolves later. |
| E11 | Aggregated view returns 1000+ files | Top 20 per owner-source group, `Show all (N)` drilldown into flat paginated list per source. |
| E12 | Hub root view (no folder selected) | Port-wide Signing + recent Files, both paginated. |
| E13 | Concurrent completions race for the same entity folder | `INSERT … ON CONFLICT DO NOTHING RETURNING id`, then re-`SELECT` if needed. Uses the new partial unique index `uniq_document_folders_entity`. |
| E14 | Cross-port aggregation leak | `port_id = $p` filter at every join in aggregation SQL. Defense-in-depth. |
| Lazy folder creation | When are system root + per-entity folders created? | Roots: on port init. Subfolders: lazy on first need (auto-deposit, manual upload, or "Open folder" button on entity page). |
| Aggregation reach | Symmetric or owner-down only? | Symmetric — walk relationships in both directions. `Clients/Smith/`, `Companies/Smith Marine LLC/`, `Yachts/MV Serenity/` all show the full graph from their vantage point. |
| Search scope | Where does the search box look? | Current folder + descendants. Empty/root selection → port-wide. Includes both Signing and Files results. |
| Rollout | Feature flag or hard cutover? | Hard cutover. Migration backfills data; new hub replaces old hub on merge. |
## Schema deltas
### `files` table
```sql
ALTER TABLE files
ADD COLUMN folder_id text REFERENCES document_folders(id) ON DELETE SET NULL;
CREATE INDEX idx_files_folder ON files(folder_id);
CREATE INDEX idx_files_port_folder ON files(port_id, folder_id);
```
### `document_folders` table
```sql
ALTER TABLE document_folders
ADD COLUMN system_managed boolean NOT NULL DEFAULT false,
ADD COLUMN entity_type text, -- null | 'root' | 'client' | 'company' | 'yacht'
ADD COLUMN entity_id text, -- null when entity_type is null or 'root'
ADD COLUMN archived_at timestamptz; -- mirrors entity archive state
-- Per-port uniqueness on (entity_type, entity_id) for entity subfolders.
-- Excludes 'root' folders (handled by name uniqueness already in place).
CREATE UNIQUE INDEX uniq_document_folders_entity
ON document_folders(port_id, entity_type, entity_id)
WHERE entity_id IS NOT NULL;
-- Enforce: system_managed=true requires either entity_type='root' OR (entity_type IN ('client','company','yacht') AND entity_id IS NOT NULL).
ALTER TABLE document_folders
ADD CONSTRAINT chk_system_folder_shape CHECK (
NOT system_managed OR
entity_type = 'root' OR
(entity_type IN ('client','company','yacht') AND entity_id IS NOT NULL)
);
```
### Backfill migration (one-time data migration script)
Runs as part of the deploy. Idempotent — safe to re-run.
1. For every port: ensure `Clients/`, `Companies/`, `Yachts/` exist with `system_managed=true`, `entity_type='root'`.
2. For every `(client | company | yacht)` entity that has at least one file or completed workflow attached: ensure its subfolder exists.
3. For every file with a non-null `client_id` / `company_id` / `yacht_id`: set `folder_id` to the matching subfolder via owner-resolution (Owner-wins).
4. For every completed workflow with `signed_file_id`: ensure the signed file's entity FKs are populated by copying from the workflow row (handles legacy completions where the signed file row was created without entity FKs).
5. Files with no entity FKs → `folder_id` left null.
Script: `pnpm tsx scripts/backfill-document-folders.ts`. Wraps in `pg_advisory_xact_lock(<port_id_hash>)` per port to serialize concurrent runs.
## Implementation surface (preview, full breakdown in the plan)
### Service layer
- `src/lib/services/document-folders.service.ts`
- `ensureEntityFolder(portId, entityType, entityId)` — INSERT-ON-CONFLICT + re-SELECT
- `ensureSystemRoots(portId)` — idempotent root creation
- `syncEntityFolderName(portId, entityType, entityId, newName)` — called from entity update services
- `applyEntityArchivedSuffix(portId, entityType, entityId)` / `applyEntityRestoredSuffix(...)` — toggle `(archived)` suffix
- `demoteSystemFolderOnEntityDelete(portId, entityType, entityId)` — flip `system_managed=false`, append `(deleted)` suffix
- `src/lib/services/files.service.ts`
- `listFilesInFolder(portId, folderId, opts)` — direct listing (folder_id match)
- `listFilesAggregatedByEntity(portId, entityType, entityId, opts)` — owner-grouped projection
- `applyEntityFkFromFolder(portId, folderId, fileInsert)` — used by upload endpoints (E8)
- `src/lib/services/documents.service.ts`
- `listInflightWorkflowsAggregatedByEntity(...)` — same projection for in-flight workflows
- `src/lib/services/clients.service.ts` / `companies.service.ts` / `yachts.service.ts`
- Add hooks to call `syncEntityFolderName` on rename, `applyEntityArchivedSuffix` on archive/restore, `demoteSystemFolderOnEntityDelete` on hard delete
### API routes
- `src/app/api/v1/files/route.ts` — accept `folderId` (direct) or `entityType + entityId` (aggregated) query params
- `src/app/api/v1/documents/route.ts` — same; collapse `tab` enum to a `signingState` filter (in-flight only by default)
- `src/app/api/v1/documents/hub-counts/route.ts` — reduce to in-flight count
- `src/app/api/v1/documents/[id]/signing-details/route.ts`**new** — returns workflow + signers + events for the dialog
- `src/app/api/webhooks/documenso/route.ts` (`handleDocumentCompleted`) — extend with owner-resolve + ensure-folder + set-FK steps
### UI components
- `src/components/documents/documents-hub.tsx` — major rebuild: stacked Signing/Files sections, owner-grouped headers, system-folder integration. Drop the signing-status tabs.
- `src/components/documents/folder-tree.tsx` — render 🔒 marker for `system_managed`; suppress rename/move/delete in `FolderActionsMenu` for system rows
- `src/components/documents/aggregated-section.tsx`**new** — renders a Signing or Files section grouped by owner-source with per-group pagination
- `src/components/documents/signing-details-dialog.tsx`**new** — modal for "view signing details"
- `src/app/(dashboard)/[portSlug]/documents/files/page.tsx`**deleted**, replaced by 301 redirect in `next.config.mjs`
- `src/components/files/folder-tree.tsx` and the legacy `storagePath`-prefix logic — **deleted**
### Stores / hooks
- `src/stores/file-browser-store.ts` — repurposed to drive the unified hub state (currentFolder, viewMode); the legacy storagePath-keyed currentFolder semantics are replaced with `document_folders.id` references
## Testing strategy
### Unit (vitest)
- `document-folders.service.test.ts`: extend with system-folder tests — `ensureEntityFolder` idempotency, `syncEntityFolderName` collision (numeric suffix), `applyEntityArchivedSuffix` round-trip, `demoteSystemFolderOnEntityDelete` flips `system_managed`.
- `files.service.aggregated.test.ts`: aggregation projection — symmetric walk, defense-in-depth port_id, per-group pagination, file-FK-as-source-of-truth (yacht transfer scenario).
- `documents-completion.handler.test.ts`: `handleDocumentCompleted` with each owner-resolution branch (client direct, company direct, yacht direct, via interest, no owner).
### Integration (vitest + real Postgres)
- `documents-hub-system-folders.integration.test.ts`: API-level — listing aggregated, system folder protection (rename/move/delete return 4xx), entity rename round-trips, archive/delete lifecycle.
- `backfill-document-folders.integration.test.ts`: backfill script idempotency, multi-port isolation, legacy file FK propagation from completed workflows.
### E2E (Playwright)
- `documents-hub-aggregated.smoke.spec.ts`: open client folder → see grouped Signing + Files → open signing-details dialog → close.
- `documents-hub-upload-into-entity-folder.smoke.spec.ts`: upload PDF into Clients/Smith/ → verify `client_id` auto-set → verify file appears in entity folder.
- `documents-hub-completion-auto-deposit.realapi.spec.ts`: round-trip Documenso completion → verify signed PDF lands in owner's entity folder. (Joins the existing realapi project.)
### Visual
- Regenerate baselines for `/[port]/documents` (root view) and `/[port]/documents` with a folder selected. Snapshot key: hub-root, hub-entity-folder.
## Risks and mitigations
| Risk | Mitigation |
|------|------------|
| Aggregation queries slow on large portfolios (5k+ files per client) | Per-group pagination caps render cost; supporting indexes on `files(port_id, client_id)`, `files(port_id, company_id)`, `files(port_id, yacht_id)` already exist; new `files(folder_id)` and `files(port_id, folder_id)` cover folder filtering |
| Backfill migration locks production for too long | Per-port advisory lock; backfill batched in chunks of 1000 file rows per transaction; safe to re-run if interrupted |
| System-folder protection bypass via direct DB write | Application-level enforcement; we accept that direct DB writes can bypass (no DB constraint enforces "you can't update system_managed=true rows"). Audit log entries on folder ops surface anomalies |
| Hard cutover means broken hub if backfill fails | Backfill is idempotent and runs *before* code rollout; if backfill fails, the new hub still renders (just with sparse folders); rollback = revert the migration + redeploy old hub binary |
| Rep confused by "view signing details" link disappearing for non-Documenso signed files (e.g., manually uploaded "already signed" PDFs via /upload-signed) | The link shows only when `signed_file_id` traces to a `documents` row; manually-uploaded "signed" PDFs that bypass the workflow won't have the link, which is correct — there's nothing to show |
## Open questions deferred to plan
- Whether to add a "Signing status" filter chip strip inside the Signing section (the deferred replacement for `awaiting_them`/`awaiting_me` tabs). Default: defer; add if rep feedback asks for it.
- Whether `Signing section in entity folders` should also surface workflows whose `interest_id` resolves to the entity (not just direct entity FK match). Default: yes, via the same Owner-wins resolution chain — codify in the projection helper.