diff --git a/docs/api-coverage.md b/docs/api-coverage.md
index 6933d2e..6e27b2b 100644
--- a/docs/api-coverage.md
+++ b/docs/api-coverage.md
@@ -1,6 +1,8 @@
# API Endpoint Coverage Matrix
-> **Re-baselined 2026-07-31 against the live `openapi.json` (~150 endpoints).** The **original 98 rows** below cover the 2026-06-11 API surface and keep their real ☑/◐/☐ implementation-and-test state unchanged. The live API has since grown to **~150 endpoints** across whole new feature areas the app has not yet implemented; those are captured in the new **[New endpoints (2026-07-31 re-baseline)](#new-endpoints-2026-07-31-re-baseline--not-yet-implemented)** section, each starting ☐/☐ and mapped to its gap ID (G1–G14) in **[`work-consolidation.md`](../work-consolidation.md)**. This file remains the home for the per-endpoint ☑/◐ **test** matrix; the maintenance rule below still governs when a new row may flip.
+> **Re-baselined 2026-07-31 against the live `openapi.json` (~150 endpoints).** The **original 98 rows** below cover the 2026-06-11 API surface and keep their real ☑/◐/☐ implementation-and-test state unchanged. The live API has since grown across whole new feature areas; those are captured in the **[New endpoints](#new-endpoints-2026-07-31-re-baseline--implementation-state-reconciled-2026-09-05)** section, mapped to their gap ID (G1–G14) in **[`work-consolidation.md`](../work-consolidation.md)**. **Reconciled 2026-09-05** against the shipped code: the matrix is **187 rows** (not the previously stated 151 — see footnote 14), and the new rows now carry their real ☑/◐/☐ state instead of the blanket ☐/☐ they were added with. This file remains the home for the per-endpoint ☑/◐ **test** matrix; the maintenance rule below still governs when a new row may flip.
+
+> **⚠️ The endpoint inventory below is stale as of 2026-09-05.** A fresh authenticated pull of `GET /api/openapi.json` reports **226 paths / 294 operations** — this matrix holds 191 rows, and the client builds 154 request builders. Roughly 100 live operations have no row here at all (whole product areas: AI, "Create from…"/materialize, app-settings & devices, notification preferences, sessions, tags, link metadata). The gap analysis lives in [`work-consolidation.md` §1d](../work-consolidation.md#1d-new-feature-areas-2026-09-05-re-measure); **six shipping client calls also use a verb the live server rejects** — see [§1c](../work-consolidation.md#1c-live-verb-defects--fix-first). Re-baselining this matrix against the 294-operation surface is queued work, not done.
**Audience:** engineering (maintainers and implementing agents).
@@ -116,92 +118,100 @@ This matrix exists so that full coverage of the [InterlinedList API](https://int
**Original-surface totals:** 98 endpoints — Auth 12 · User 8 · Messages 11 · Lists 21 (incl. 3 public) · List Connections 3 · Documents & Sync 14 · Follow 11 · Organizations 9 · Exports 4 · Notifications 3 · Public-only 2.
-## New endpoints (2026-07-31 re-baseline) — not yet implemented
+## New endpoints (2026-07-31 re-baseline) — implementation state reconciled 2026-09-05
-The 2026-07-31 authenticated live probe ([`work-consolidation.md`](../work-consolidation.md)) plus `GET /api/openapi.json` show the surface has grown to ~150 endpoints across new feature areas the app has never implemented. Every endpoint below is **absent** from the original 98-row matrix; each starts **Implemented ☐ / Tested ☐** and maps to the gap ID (G1–G14) in [`work-consolidation.md`](../work-consolidation.md). **Backend** column: ✅ = confirmed live & Bearer-reachable in the 2026-07-31 probe; ⚠️ = live but constrained; *per OpenAPI, unverified* = present in the spec / named in the gap plan but **not** individually hit in the read-only probe (writes were deliberately not exercised). Rows flip ◐→☑ only under the same maintenance rule (a tested App-layer view model drives them end-to-end).
+The 2026-07-31 authenticated live probe ([`work-consolidation.md`](../work-consolidation.md)) plus `GET /api/openapi.json` show the surface has grown well beyond the original 98-row matrix. Every endpoint below is **absent** from those 98 rows (verified: zero path overlap). Rows were added ☐/☐ on 2026-07-31 and **reconciled against the shipped code on 2026-09-05** (see footnote 14) — most of G1–G5, G11a, G12 and G14 shipped in the interim and had never been scored and maps to the gap ID (G1–G14) in [`work-consolidation.md`](../work-consolidation.md). **Backend** column: ✅ = confirmed live & Bearer-reachable in the 2026-07-31 probe; ⚠️ = live but constrained; *per OpenAPI, unverified* = present in the spec / named in the gap plan but **not** individually hit in the read-only probe (writes were deliberately not exercised). Rows flip ◐→☑ only under the same maintenance rule (a tested App-layer view model drives them end-to-end).
### Direct Messages (G1) — 11
| Endpoint (method + path) | Group | Backend | Gap | Purpose | Implemented | Tested |
| --- | --- | --- | --- | --- | --- | --- |
-| `GET /api/dm` | Direct Messages | ✅ | G1 | List DMs by folder (inbox/sent/deleted), cursor-paginated | ☐ | ☐ |
-| `POST /api/dm` | Direct Messages | ✅ | G1 | Send a DM to a mutual follower (≤8 image attachments) | ☐ | ☐ |
+| `GET /api/dm` | Direct Messages | ✅ | G1 | List DMs by folder (inbox/sent/deleted), cursor-paginated | ☑ | ☑ |
+| `POST /api/dm` | Direct Messages | ✅ | G1 | Send a DM to a mutual follower (≤8 image attachments) | ☑ | ☑ |
| `POST /api/dm/images/upload` | Direct Messages | ✅ | G1 | Upload an image for a DM | ☐ | ☐ |
-| `GET /api/dm/recipients` | Direct Messages | ✅ | G1 | List eligible DM recipients (mutual followers) | ☐ | ☐ |
-| `GET /api/dm/thread/{username}` | Direct Messages | ✅ | G1 | Fetch the conversation thread with a user | ☐ | ☐ |
-| `GET /api/dm/thread/{username}/updates` | Direct Messages | ✅ | G1 | Poll for new messages in a thread since a marker | ☐ | ☐ |
-| `GET /api/dm/unread-count` | Direct Messages | ✅ | G1 | Unread-DM count for the badge | ☐ | ☐ |
+| `GET /api/dm/recipients` | Direct Messages | ✅ | G1 | List eligible DM recipients (mutual followers) | ☑ | ☑ |
+| `GET /api/dm/thread/{username}` | Direct Messages | ✅ | G1 | Fetch the conversation thread with a user | ☑ | ☑ |
+| `GET /api/dm/thread/{username}/updates` | Direct Messages | ✅ | G1 | Poll for new messages in a thread since a marker | ☑ | ◐ |
+| `GET /api/dm/unread-count` | Direct Messages | ✅ | G1 | Unread-DM count for the badge | ☑ | ☑ |
| `GET /api/dm/{id}` | Direct Messages | ✅ | G1 | Fetch a single DM | ☐ | ☐ |
-| `POST /api/dm/{id}/read` | Direct Messages | ✅ | G1 | Mark a DM read | ☐ | ☐ |
-| `POST /api/dm/{id}/restore` | Direct Messages | ✅ | G1 | Restore a trashed DM (per-side) | ☐ | ☐ |
-| `POST /api/dm/{id}/trash` | Direct Messages | ✅ | G1 | Soft-delete a DM (per-side) | ☐ | ☐ |
+| `POST /api/dm/{id}/read` | Direct Messages | ✅ | G1 | Mark a DM read | ☑ | ◐ |
+| `POST /api/dm/{id}/restore` | Direct Messages | ✅ | G1 | Restore a trashed DM (per-side) | ☑ | ◐ |
+| `POST /api/dm/{id}/trash` | Direct Messages | ✅ | G1 | Soft-delete a DM (per-side) | ☑ | ◐ |
### Moderation (G2) — 10
| Endpoint (method + path) | Group | Backend | Gap | Purpose | Implemented | Tested |
| --- | --- | --- | --- | --- | --- | --- |
-| `GET /api/user/blocks` | Moderation | ✅ | G2 | List blocked users (paginated) | ☐ | ☐ |
-| `POST /api/user/blocks` | Moderation | per OpenAPI, unverified | G2 | Block a user | ☐ | ☐ |
-| `DELETE /api/user/blocks/{username}` | Moderation | per OpenAPI, unverified | G2 | Unblock a user | ☐ | ☐ |
+| `GET /api/user/blocks` | Moderation | ✅ | G2 | List blocked users (paginated) | ☑ | ☑ |
+| `POST /api/users/{username}/block` | Moderation | per OpenAPI, unverified | G2 | Block a user | ☑ | ◐ |
+| `DELETE /api/users/{username}/block` | Moderation | per OpenAPI, unverified | G2 | Unblock a user | ☑ | ◐ |
| `GET /api/user/blocks/{username}` | Moderation | per OpenAPI, unverified | G2 | Is-blocking status for a user | ☐ | ☐ |
-| `GET /api/user/mutes` | Moderation | ✅ | G2 | List muted users (paginated) | ☐ | ☐ |
-| `POST /api/user/mutes` | Moderation | per OpenAPI, unverified | G2 | Mute a user | ☐ | ☐ |
-| `DELETE /api/user/mutes/{username}` | Moderation | per OpenAPI, unverified | G2 | Unmute a user | ☐ | ☐ |
+| `GET /api/user/mutes` | Moderation | ✅ | G2 | List muted users (paginated) | ☑ | ☑ |
+| `POST /api/users/{username}/mute` | Moderation | per OpenAPI, unverified | G2 | Mute a user | ☑ | ◐ |
+| `DELETE /api/users/{username}/mute` | Moderation | per OpenAPI, unverified | G2 | Unmute a user | ☑ | ◐ |
| `GET /api/user/mutes/{username}` | Moderation | per OpenAPI, unverified | G2 | Is-muting status for a user | ☐ | ☐ |
-| `POST /api/reports/user` | Moderation | per OpenAPI, unverified | G2 | Report a user (reason + detail) | ☐ | ☐ |
-| `POST /api/reports/message` | Moderation | per OpenAPI, unverified | G2 | Report a message (reason + detail) | ☐ | ☐ |
+| `POST /api/users/{username}/report` | Moderation | per OpenAPI, unverified | G2 | Report a user (reason + detail) | ☑ | ☑ |
+| `POST /api/messages/{id}/report` | Moderation | per OpenAPI, unverified | G2 | Report a message (reason + detail) | ☑ | ◐ |
-### Share Links & Collaborators (G3) — 17
+### Share Links & Collaborators (G3) — 23
| Endpoint (method + path) | Group | Backend | Gap | Purpose | Implemented | Tested |
| --- | --- | --- | --- | --- | --- | --- |
-| `GET /api/lists/{id}/share-links` | Share Links & Collaborators | ✅ | G3 | List a list's tokenized share links | ☐ | ☐ |
-| `POST /api/lists/{id}/share-links` | Share Links & Collaborators | per OpenAPI, unverified | G3 | Create a share link (role + expiry, subscriber-gated) | ☐ | ☐ |
-| `DELETE /api/lists/{id}/share-links/{token}` | Share Links & Collaborators | per OpenAPI, unverified | G3 | Revoke a list share link | ☐ | ☐ |
-| `GET /api/lists/shared/{token}` | Share Links & Collaborators | per OpenAPI, unverified | G3 | Resolve a shared list by token (read-only viewer) | ☐ | ☐ |
-| `POST /api/lists/shared/{token}` | Share Links & Collaborators | per OpenAPI, unverified | G3 | Claim a shared list link | ☐ | ☐ |
+| `GET /api/lists/{id}/share-links` | Share Links & Collaborators | ✅ | G3 | List a list's tokenized share links | ☑ | ☑ |
+| `POST /api/lists/{id}/share-links` | Share Links & Collaborators | per OpenAPI, unverified | G3 | Create a share link (role + expiry, subscriber-gated) | ☑ | ☑ |
+| `DELETE /api/lists/{id}/share-links/{token}` | Share Links & Collaborators | per OpenAPI, unverified | G3 | Revoke a list share link | ☑ | ☑ |
+| `GET /api/lists/shared/{token}` | Share Links & Collaborators | per OpenAPI, unverified | G3 | Resolve a shared list by token (read-only viewer) | ☑ | ☑ |
+| `POST /api/lists/shared/{token}` | Share Links & Collaborators | per OpenAPI, unverified | G3 | Claim a shared list link | ☑ | ◐ |
| `GET /api/lists/shared/{token}/data` | Share Links & Collaborators | per OpenAPI, unverified | G3 | Read shared-list row data by token | ☐ | ☐ |
| `GET /api/lists/watching` | Share Links & Collaborators | ✅ | G3 | "Shared-with-me" lists the user is watching | ☐ | ☐ |
-| `GET /api/documents/{id}/share-links` | Share Links & Collaborators | ✅ | G3 | List a document's share links | ☐ | ☐ |
-| `POST /api/documents/{id}/share-links` | Share Links & Collaborators | per OpenAPI, unverified | G3 | Create a document share link (subscriber-gated) | ☐ | ☐ |
-| `DELETE /api/documents/{id}/share-links/{token}` | Share Links & Collaborators | per OpenAPI, unverified | G3 | Revoke a document share link | ☐ | ☐ |
-| `GET /api/documents/shared/{token}` | Share Links & Collaborators | per OpenAPI, unverified | G3 | Resolve a shared document by token | ☐ | ☐ |
-| `POST /api/documents/shared/{token}` | Share Links & Collaborators | per OpenAPI, unverified | G3 | Claim a shared document link | ☐ | ☐ |
-| `GET /api/documents/{id}/collaborators` | Share Links & Collaborators | ✅ | G3 | List per-person document collaborators (paginated) | ☐ | ☐ |
-| `POST /api/documents/{id}/collaborators` | Share Links & Collaborators | per OpenAPI, unverified | G3 | Add a document collaborator (by @handle + role) | ☐ | ☐ |
-| `GET /api/documents/{id}/collaborators/users` | Share Links & Collaborators | per OpenAPI, unverified | G3 | Search users for collaborator invite | ☐ | ☐ |
-| `PUT /api/documents/{id}/collaborators/{userId}` | Share Links & Collaborators | per OpenAPI, unverified | G3 | Set a collaborator's role | ☐ | ☐ |
-| `DELETE /api/documents/{id}/collaborators/{userId}` | Share Links & Collaborators | per OpenAPI, unverified | G3 | Remove a document collaborator | ☐ | ☐ |
-
-### List Folders (G6) — 4
+| `GET /api/documents/{id}/share-links` | Share Links & Collaborators | ✅ | G3 | List a document's share links | ☑ | ◐ |
+| `POST /api/documents/{id}/share-links` | Share Links & Collaborators | per OpenAPI, unverified | G3 | Create a document share link (subscriber-gated) | ☑ | ◐ |
+| `DELETE /api/documents/{id}/share-links/{token}` | Share Links & Collaborators | per OpenAPI, unverified | G3 | Revoke a document share link | ☑ | ◐ |
+| `GET /api/documents/shared/{token}` | Share Links & Collaborators | per OpenAPI, unverified | G3 | Resolve a shared document by token | ☑ | ◐ |
+| `POST /api/documents/shared/{token}` | Share Links & Collaborators | per OpenAPI, unverified | G3 | Claim a shared document link | ☑ | ◐ |
+| `GET /api/documents/{id}/collaborators` | Share Links & Collaborators | ✅ | G3 | List per-person document collaborators (paginated) | ☑ | ☑ |
+| `POST /api/documents/{id}/collaborators` | Share Links & Collaborators | per OpenAPI, unverified | G3 | Add a document collaborator (by @handle + role) | ☑ | ☑ |
+| `GET /api/documents/{id}/collaborators/users` | Share Links & Collaborators | per OpenAPI, unverified | G3 | Search users for collaborator invite | ☑ | ☑ |
+| `PUT /api/documents/{id}/collaborators/{userId}` | Share Links & Collaborators | per OpenAPI, unverified | G3 | Set a collaborator's role | ☑ | ◐ |
+| `DELETE /api/documents/{id}/collaborators/{userId}` | Share Links & Collaborators | per OpenAPI, unverified | G3 | Remove a document collaborator | ☑ | ◐ |
+| `GET /api/lists/{id}/invites` | Share Links & Collaborators | per OpenAPI, unverified | G3 | List a list's pending email invites | ☑ | ◐ |
+| `POST /api/lists/{id}/invites` | Share Links & Collaborators | per OpenAPI, unverified | G3 | Invite someone to a list by email | ☑ | ◐ |
+| `DELETE /api/lists/{id}/invites/{token}` | Share Links & Collaborators | per OpenAPI, unverified | G3 | Revoke a list invite | ☑ | ◐ |
+| `GET /api/documents/{id}/invites` | Share Links & Collaborators | per OpenAPI, unverified | G3 | List a document's pending email invites | ☑ | ☑ |
+| `POST /api/documents/{id}/invites` | Share Links & Collaborators | per OpenAPI, unverified | G3 | Invite someone to a document by email | ☑ | ☑ |
+| `DELETE /api/documents/{id}/invites/{token}` | Share Links & Collaborators | per OpenAPI, unverified | G3 | Revoke a document invite | ☑ | ◐ |
+
+### List Folders (G6) — 4 — ~~planned~~ **FEATURE REMOVED**
+
+> **Withdrawn 2026-09-05.** The List Folders feature (grouping *lists* into folders) was removed from the client in PR #19 (`1afb89d`); there is no `FoldersEndpoint.swift` and no domain service. These four rows are retained for inventory completeness — the routes still exist server-side — but they are **not targets for implementation** and are scored `—`, like the out-of-scope Stripe rows. Parent/child list nesting via `parentID` is a different feature and is unaffected.
| Endpoint (method + path) | Group | Backend | Gap | Purpose | Implemented | Tested |
| --- | --- | --- | --- | --- | --- | --- |
-| `GET /api/folders` | List Folders | ✅ | G6 | List hierarchical list-folders (flat array + `parentId`) | ☐ | ☐ |
-| `POST /api/folders` | List Folders | per OpenAPI, unverified | G6 | Create a list-folder (subscriber-gated) | ☐ | ☐ |
-| `PUT /api/folders/{id}` | List Folders | per OpenAPI, unverified | G6 | Rename / move a list-folder (cycle-safe) | ☐ | ☐ |
-| `DELETE /api/folders/{id}` | List Folders | per OpenAPI, unverified | G6 | Delete a list-folder (detaches lists to root) | ☐ | ☐ |
+| `GET /api/folders` | List Folders | ✅ | ~~G6~~ **REMOVED** | List hierarchical list-folders (flat array + `parentId`) | — | — |
+| `POST /api/folders` | List Folders | per OpenAPI, unverified | ~~G6~~ **REMOVED** | Create a list-folder (subscriber-gated) | — | — |
+| `PUT /api/folders/{id}` | List Folders | per OpenAPI, unverified | ~~G6~~ **REMOVED** | Rename / move a list-folder (cycle-safe) | — | — |
+| `DELETE /api/folders/{id}` | List Folders | per OpenAPI, unverified | ~~G6~~ **REMOVED** | Delete a list-folder (detaches lists to root) | — | — |
### Search (G5) — 3
| Endpoint (method + path) | Group | Backend | Gap | Purpose | Implemented | Tested |
| --- | --- | --- | --- | --- | --- | --- |
-| `GET /api/messages/search` | Search | ✅ | G5 | Server-side message search (`?q=`; POST → 405, search is GET) | ☐ | ☐ |
-| `GET /api/lists/search` | Search | ✅ | G5 | Server-side list search | ☐ | ☐ |
-| `GET /api/documents/search` | Search | ✅ | G5 | Server-side document search | ☐ | ☐ |
+| `GET /api/messages/search` | Search | ✅ | G5 | Server-side message search (`?q=`; POST → 405, search is GET) | ☑ | ☑ |
+| `GET /api/lists/search` | Search | ✅ | G5 | Server-side list search | ☑ | ☑ |
+| `GET /api/documents/search` | Search | ✅ | G5 | Server-side document search | ☑ | ☑ |
### GitHub (G4) — 8
| Endpoint (method + path) | Group | Backend | Gap | Purpose | Implemented | Tested |
| --- | --- | --- | --- | --- | --- | --- |
-| `GET /api/github/repos` | GitHub | ⚠️ | G4 | List linked-account repos (400 "not linked" until OAuth link) | ☐ | ☐ |
-| `GET /api/github/issues` | GitHub | per OpenAPI, unverified | G4 | List issues for a repo | ☐ | ☐ |
-| `POST /api/github/issues` | GitHub | per OpenAPI, unverified | G4 | Create an issue | ☐ | ☐ |
-| `PATCH /api/github/issues/{owner}/{repo}/{number}` | GitHub | per OpenAPI, unverified | G4 | Edit an issue (labels / assignees / state) | ☐ | ☐ |
-| `POST /api/github/issues/{owner}/{repo}/{number}/comments` | GitHub | per OpenAPI, unverified | G4 | Comment on an issue | ☐ | ☐ |
-| `GET /api/github/repos/{owner}/{repo}/assignees` | GitHub | per OpenAPI, unverified | G4 | List assignable users for a repo | ☐ | ☐ |
-| `GET /api/github/repos/{owner}/{repo}/labels` | GitHub | per OpenAPI, unverified | G4 | List labels for a repo | ☐ | ☐ |
-| `GET /api/github/repos/{owner}/{repo}/next-issue-number` | GitHub | per OpenAPI, unverified | G4 | Next issue number for a repo | ☐ | ☐ |
+| `GET /api/github/repos` | GitHub | ⚠️ | G4 | List linked-account repos (400 "not linked" until OAuth link) | ☑ | ☑ |
+| `GET /api/github/issues` | GitHub | per OpenAPI, unverified | G4 | List issues for a repo | ☑ | ☑ |
+| `POST /api/github/issues` | GitHub | per OpenAPI, unverified | G4 | Create an issue | ☑ | ☑ |
+| `PATCH /api/github/issues/{owner}/{repo}/{number}`¹⁵ | GitHub | ✅ live (`Allow: OPTIONS, PATCH`) | G4 | Edit an issue (labels / assignees / state) — **the client points at `/api/github/repos/{repo}/issues/{number}`, which 404s** | ☐ | ☐ |
+| `POST /api/github/issues/{owner}/{repo}/{number}/comments`¹⁵ | GitHub | ✅ live (`Allow: OPTIONS, POST`) | G4 | Comment on an issue — **the client points at `/api/github/repos/{repo}/issues/{number}/comments`, which 404s** | ☐ | ☐ |
+| `GET /api/github/repos/{owner}/{repo}/assignees` | GitHub | per OpenAPI, unverified | G4 | List assignable users for a repo | ☑ | ☑ |
+| `GET /api/github/repos/{owner}/{repo}/labels` | GitHub | per OpenAPI, unverified | G4 | List labels for a repo | ☑ | ☑ |
+| `GET /api/github/repos/{owner}/{repo}/next-issue-number` | GitHub | per OpenAPI, unverified | G4 | Next issue number for a repo | ☑ | ☑ |
### Push (G9) — 2
@@ -222,7 +232,7 @@ The 2026-07-31 authenticated live probe ([`work-consolidation.md`](../work-conso
| Endpoint (method + path) | Group | Backend | Gap | Purpose | Implemented | Tested |
| --- | --- | --- | --- | --- | --- | --- |
| `GET /api/linkedin/targets` | LinkedIn targets | ✅ | G11a | List LinkedIn posting targets (personal target present) | ☐ | ☐ |
-| `GET /api/linkedin/posting-targets` | LinkedIn targets | ✅ | G11a | Read enabled posting targets (`enabled:true`, `orgScopeMissing:true`) | ☐ | ☐ |
+| `GET /api/linkedin/posting-targets` | LinkedIn targets | ✅ | G11a | Read enabled posting targets (`enabled:true`, `orgScopeMissing:true`) | ☑ | ☑ |
| `PUT /api/linkedin/posting-targets` | LinkedIn targets | per OpenAPI, unverified | G11a | Set enabled posting targets | ☐ | ☐ |
| `POST /api/linkedin/sync-pages` | LinkedIn targets | per OpenAPI, unverified | G11a | Refresh available LinkedIn pages | ☐ | ☐ |
@@ -230,7 +240,7 @@ The 2026-07-31 authenticated live probe ([`work-consolidation.md`](../work-conso
| Endpoint (method + path) | Group | Backend | Gap | Purpose | Implemented | Tested |
| --- | --- | --- | --- | --- | --- | --- |
-| `GET /api/auth/twitter/authorize` | Twitter / X auth | per OpenAPI, unverified | G7 | Begin X/Twitter OAuth authorization | ☐ | ☐ |
+| `GET /api/auth/twitter/authorize` | Twitter / X auth | ✅ | G7 | Begin X/Twitter OAuth authorization — built by the shared `Auth.authorize(provider:)` builder (`OAuthProvider` includes `.twitter`), consumed by `UserService` + `LinkedAccountsViewModel` | ☑ | ☑ |
| `GET /api/auth/twitter/callback` | Twitter / X auth | per OpenAPI, unverified | G7 | X/Twitter OAuth callback | ☐ | ☐ |
| `GET /api/auth/twitter/status` | Twitter / X auth | ✅ | G7 | X/Twitter link status (`configured:true`) | ☐ | ☐ |
@@ -238,10 +248,10 @@ The 2026-07-31 authenticated live probe ([`work-consolidation.md`](../work-conso
| Endpoint (method + path) | Group | Backend | Gap | Purpose | Implemented | Tested |
| --- | --- | --- | --- | --- | --- | --- |
-| `GET /api/documents/templates` | Document templates & tree | ✅ | G12 | List server-side document templates (seeded + `_templates` folder) | ☐ | ☐ |
-| `POST /api/documents/templates/seed-defaults` | Document templates & tree | per OpenAPI, unverified | G12 | Seed the default template set | ☐ | ☐ |
+| `GET /api/documents/templates` | Document templates & tree | ✅ | G12 | List server-side document templates (seeded + `_templates` folder) | ☑ | ☑ |
+| `POST /api/documents/templates/seed-defaults` | Document templates & tree | per OpenAPI, unverified | G12 | Seed the default template set | ☑ | ◐ |
| `GET /api/documents/from-template` | Document templates & tree | per OpenAPI, unverified | G12 | Preview a new document from a template | ☐ | ☐ |
-| `POST /api/documents/from-template` | Document templates & tree | per OpenAPI, unverified | G12 | Create a document from a template | ☐ | ☐ |
+| `POST /api/documents/from-template` | Document templates & tree | per OpenAPI, unverified | G12 | Create a document from a template | ☑ | ☑ |
| `GET /api/documents/tree` | Document templates & tree | ✅ | G12 | One-call folders + documents sidebar payload | ☐ | ☐ |
| `POST /api/documents/folders/{id}/documents` | Document templates & tree | per OpenAPI, unverified | G12 | Create a document directly inside a folder | ☐ | ☐ |
@@ -256,7 +266,7 @@ The 2026-07-31 authenticated live probe ([`work-consolidation.md`](../work-conso
| Endpoint (method + path) | Group | Backend | Gap | Purpose | Implemented | Tested |
| --- | --- | --- | --- | --- | --- | --- |
-| `GET /api/limits` | Utility / limits | ✅ | G14 | Quota / media limits (drives composer validation + plan card) | ☐ | ☐ |
+| `GET /api/limits` | Utility / limits | ✅ | G14 | Quota / media limits (drives composer validation + plan card) | ☑ | ☑ |
| `GET /api/images/proxy` | Utility / limits | per OpenAPI, unverified | G14 | Image-proxy helper (rich previews / avatars) | ☐ | ☐ |
### Multi-account (G10) — 3
@@ -271,8 +281,8 @@ The 2026-07-31 authenticated live probe ([`work-consolidation.md`](../work-conso
| Endpoint (method + path) | Group | Backend | Gap | Purpose | Implemented | Tested |
| --- | --- | --- | --- | --- | --- | --- |
-| `GET /api/users/{username}` | Public profile | ✅ | D2 (fn 8) | Direct public-profile read (now live — replaces the decision-0002 fallback) | ☐ | ☐ |
-| `POST /api/auth/{provider}/link` | Auth (OAuth) | ✅ | fn 12 | Bearer native OAuth identity-link completion (endpoint live; native flow built on this branch) | ☐ | ☐ |
+| `GET /api/users/{username}` | Public profile | ✅ | D2 (fn 8) | Direct public-profile read (now live — replaces the decision-0002 fallback) | ☑ | ◐ |
+| `POST /api/auth/{provider}/link` | Auth (OAuth) | ✅ | fn 12 | Bearer native OAuth identity-link completion (endpoint live; native flow built on this branch) | ☑ | ◐ |
### Messages & auth drift additions (D1 / D3 / new methods on existing paths) — 4
@@ -285,9 +295,29 @@ New HTTP methods / paths on already-listed resource families, surfaced by the re
| `POST /api/lists/{id}/watchers` | Lists | per OpenAPI, unverified | — | Invite a watcher via POST (matrix has `PUT …/watchers/{userId}`) | ☐ | ☐ |
| `POST /api/auth/verify-email-change` | Auth | per OpenAPI, unverified | — | Confirm a pending email-change (pairs with existing `change-email/request`) | ☐ | ☐ |
-**New-endpoints subtotal:** 53 rows — Direct Messages 11 · Moderation 10 · Share Links & Collaborators 17 · List Folders 4 · Search 3 · GitHub 8 · Push 2 · Stripe/Billing 2 · LinkedIn targets 4 · Twitter/X auth 3 · Document templates & tree 6 · Document presence 2 · Utility/limits 2 · Multi-account 3 · Public profile & OAuth-link (D2 / fn 12) 2 · Messages & auth drift additions 4.
+### User lookup & provider status — 4
+
+> **Added by the 2026-09-05 live re-measure.** Implemented in `UserEndpoint` / `AuthEndpoint` and consumed by `UserService`, but never carried a matrix row in either audit pass.
+
+| Endpoint (method + path) | Group | Backend | Gap | Purpose | Implemented | Tested |
+| --- | --- | --- | --- | --- | --- | --- |
+| `GET /api/users/search` | User | ✅ | — | Search users by handle (collaborator / invite pickers) | ☑ | ◐ |
+| `GET /api/users/lookup` | User | ✅ | — | Resolve a single `@handle` to a user | ☑ | ◐ |
+| `GET /api/auth/bluesky/status` | Auth (OAuth) | ✅ | NW-4 | Whether Bluesky OAuth is configured on the server | ☑ | ◐ |
+| `GET /api/auth/mastodon/status` | Auth (OAuth) | ✅ | NW-4 | Whether Mastodon OAuth is configured for an instance | ☑ | ◐ |
+
+**New-endpoints subtotal:** **93 rows** — Direct Messages 11 · Moderation 10 · Share Links & Collaborators 23 · List Folders 4 (retired) · Search 3 · GitHub 8 · Push 2 · Stripe/Billing 2 · LinkedIn targets 4 · Twitter/X auth 3 · Document templates & tree 6 · Document presence 2 · Utility/limits 2 · Multi-account 3 · Public profile & OAuth-link (D2 / fn 12) 2 · Messages & auth drift additions 4 · User lookup & provider status 4. *(Recomputed from the rows themselves on 2026-09-05. The former "53" was an arithmetic slip — it is the running total through the GitHub section, i.e. the addition stopped six sections early. Share Links grew 17 → 23 when the six shipped `/invites` endpoints were added; see footnote 14.)*
-**Re-baseline grand total:** **98 original + 53 new = 151 endpoints** (~150 as reported in [`work-consolidation.md`](../work-consolidation.md)). Original 98 keep their real implementation/test state (98 implemented; 74 ☑ / 18 ◐ / 6 ☐ tested as of Wave 8); all 53 new rows start ☐ Implemented / ☐ Tested.
+**Re-baseline grand total:** **98 original + 93 new = 191 rows** (new section: 59 ☑ implemented · 31 Tested ☑ · 28 ◐ · 28 ☐ · 6 out of scope). *(Supersedes the previously stated "151 (~150)", which inherited the 53 slip above. `work-consolidation.md` and any note citing a "~151-endpoint API surface" carry the same stale figure.)*
+
+**Scored state as of the 2026-09-05 reconciliation:**
+
+| Band | Rows | Implemented | Tested |
+| --- | --- | --- | --- |
+| Original 98 (unchanged this pass) | 98 | 98 ☑ | 74 ☑ / 18 ◐ / 6 ☐ |
+| New rows, scoreable | 83 | 56 ☑ / 27 ☐ | 30 ☑ / 26 ◐ / 27 ☐ |
+| New rows, not targets (G6 removed 4, G8 out-of-scope 2) | 6 | — | — |
+| **Total** | **187** | **154 ☑ / 27 ☐ / 6 —** | **104 ☑ / 44 ◐ / 33 ☐ / 6 —** |
## Footnotes and assumptions
@@ -298,13 +328,23 @@ New HTTP methods / paths on already-listed resource families, surfaced by the re
5. `POST /api/auth/login` (cookie-session credential exchange) was deferred through Waves 1–7 (`NullSessionEstablisher` stub). **Resolved Wave 8.1 (2026-07-03):** `LiveSessionEstablisher` + `CredentialStore` + `KeychainCredentialStore` now implement the lazy `POST /api/auth/login` path; `AuthService.signIn` persists credentials to `KeychainCredentialStore` so the establisher can re-authenticate on the next `.session` call. `LiveSessionEstablisherTests` covers the full quartet (happy 200/204, no-credentials, server 401, server 500, transport failure). Row flipped to ☑/☑; this footnote is resolved.
6. `POST /api/auth/register` ships as `AuthService.register` and is exercised by the live `ContractTests` when `INTERLINEDLIST_EMAIL` / `INTERLINEDLIST_PASSWORD` are present, but has no stubbed unit-test cases yet (only `signIn` has dedicated unit tests in `AuthServiceTests`). Tested ☐ until at least happy + invalid + failure + empty/boundary unit tests are added (likely in the onboarding-feature wave).
7. `GET /api/user/organizations` lives in `InterlinedKit.User.organizations()` (not `Organizations.*`) because the live API path is `/api/user/organizations`, not `/api/organizations`. Planned-service column corrected from `OrgService` to `UserService¹` in Wave 1 to match the actual implementation.
-8. **~~No public profile read endpoint exists on the live API.~~ RESOLVED 2026-07-31 — the endpoint now exists.** *(Historical:* the 2026-06-21 kit-gap spike found every variation of `GET /api/users/[username]` returned 404, so `SocialService.profile(username:)` fell back — per decision [`0002-public-profile-fallback`](decisions/0002-public-profile-fallback.md) — to the embedded `{ id, username, displayName, avatar }` author object on the first message from `GET /api/user/[username]/messages`.*)* The 2026-07-31 live probe ([`work-consolidation.md`](../work-consolidation.md)) confirms `GET /api/users/{username}` now returns a **real public profile** (`/api/users/messenger` → 200). The direct-read row is added in the [New endpoints re-baseline](#new-endpoints-2026-07-31-re-baseline--not-yet-implemented) section under **Public profile & multi-account (migration D2)** at ☐/☐; migration **D2** ([`work-consolidation.md`](../work-consolidation.md)) tracks replacing the decision-0002 fallback with the direct call (keep the fallback only for pre-migration servers). When that row is implemented and view-model-tested it flips per the maintenance rule.
+8. **~~No public profile read endpoint exists on the live API.~~ RESOLVED 2026-07-31 — the endpoint now exists.** *(Historical:* the 2026-06-21 kit-gap spike found every variation of `GET /api/users/[username]` returned 404, so `SocialService.profile(username:)` fell back — per decision [`0002-public-profile-fallback`](decisions/0002-public-profile-fallback.md) — to the embedded `{ id, username, displayName, avatar }` author object on the first message from `GET /api/user/[username]/messages`.*)* The 2026-07-31 live probe ([`work-consolidation.md`](../work-consolidation.md)) confirms `GET /api/users/{username}` now returns a **real public profile** (`/api/users/messenger` → 200). The direct-read row is added in the [New endpoints re-baseline](#new-endpoints-2026-07-31-re-baseline--implementation-state-reconciled-2026-09-05) section under **Public profile & multi-account (migration D2)**, and was flipped to Implemented ☑ / Tested ◐ in the 2026-09-05 reconciliation (`User.publicProfile` → `SocialService`); migration **D2** ([`work-consolidation.md`](../work-consolidation.md)) tracks replacing the decision-0002 fallback with the direct call (keep the fallback only for pre-migration servers). When that row is implemented and view-model-tested it flips per the maintenance rule.
9. **M3 reachable but not exercised by a tested App-layer view model this wave.** Per Wave 1 footnote 4, a row only flips ◐⁴ → ☑ when an App-layer consumer drives it end-to-end under test. Four Lists rows are wired through `ListsService` and reachable from the running app but their consuming UX was held back to a polish slice this wave: `GET /api/lists/[id]` and `PUT /api/lists/[id]` (the detail-rename / single-list-refresh paths — rename UX deferred), `GET /api/lists/[id]/data/[rowId]` (single-row hydration — `RowInspectorView` reads from the already-paginated `ListRowsViewModel.rows` array), and `GET /api/lists/[id]/watchers` (the watcher pagination envelope — `WatchersView` consumes `/users` only this wave). These rows stay ◐⁴ until the next M3 polish wave consumes them through a tested view model. The Wave 1 footnote-4 backfill rule still applies.
10. **M4 detail-read rows reachable but not view-model-tested this wave.** Same pattern as footnote 9, applied to Documents. `GET /api/documents/[id]` and `GET /api/documents/folders/[id]` are wired through `DocumentsService.document(id:)` / `DocumentsService.folder(id:)` and reachable from the running app, but the Wave 5.3 App-layer view models (`DocumentsListViewModel`, `DocumentEditorViewModel`, `FolderTreeViewModel`) consume documents and folders from the **list** payload (`GET /api/documents`, `GET /api/documents/folders[/[id]/documents]`) and the **sync delta** payload rather than re-reading by id. The detail-read endpoints stay ◐⁴ until a polish slice consumes them through a tested view-model path (a likely candidate: a single-document deep-link / quick-look refresh, or a focused folder-rename inspector that re-hydrates from `folder(id:)`). The Wave 1 footnote-4 backfill rule still applies.
11. **M5 follower-removal reachable but not view-model-tested this wave.** Same pattern as footnotes 9 and 10, applied to Follow. `POST /api/follow/[userId]/remove` (the "remove a user from **my** followers" action — distinct from `DELETE /api/follow/[userId]`, which unfollows someone I follow) is wired through `SocialService.removeFollower(userId:)` and reachable from the running app, but no Wave 6.3 view model exercises it through a tested path: the Followers tab in `SocialRosterRootView` displays the roster and approves/rejects pending requests, but does not yet surface a "remove this follower" action against an already-accepted follower. The row stays ◐⁴ until a polish slice (most likely a `SocialRosterRowViewModel.removeFollower` action behind a context menu on the Followers tab) consumes it. The Wave 1 footnote-4 backfill rule still applies.
-12. **OAuth `authorize` builders Implemented (Wave 7).** The five M6 OAuth rows (`GET /api/auth/{github,mastodon,bluesky,linkedin}/authorize` and `GET /api/auth/linkedin/status`) gained Kit request builders in Wave 7 (`Auth.authorize(provider:link:instance:)`, `Auth.linkedinStatus()`, the `OAuthProvider` enum, and the `LinkedInStatusResponse` DTO, with 13 builder tests), so their **Implemented** column is ☑. **UPDATE 2026-07-31 — native OAuth identity linking is now BUILT on `feature/web-parity-batch-2026-07`, so the "blocked upstream" note is resolved** ([`work-consolidation.md`](../work-consolidation.md)): `Auth.linkIdentity` → `POST /api/auth/{provider}/link`, `UserService.linkIdentityNative`, a registered `interlinedlist://oauth/callback` custom scheme, and `ASWebAuthenticationSession` now let the app complete the flow natively rather than only handing `…/authorize?link=true` to the browser. The bearer `POST /api/auth/{provider}/link` completion endpoint that footnote 12 said "does not exist" is live and consumed; the new `POST /api/auth/{provider}/link` row is added in the [New endpoints re-baseline](#new-endpoints-2026-07-31-re-baseline--not-yet-implemented) section under **Public profile & multi-account**. The five original `authorize`/`status` rows keep their historical Tested ☐ state here (their per-endpoint completion tests are backfilled with the native-linking work); flips follow the maintenance rule once a view-model test drives them end-to-end.
+12. **OAuth `authorize` builders Implemented (Wave 7).** The five M6 OAuth rows (`GET /api/auth/{github,mastodon,bluesky,linkedin}/authorize` and `GET /api/auth/linkedin/status`) gained Kit request builders in Wave 7 (`Auth.authorize(provider:link:instance:)`, `Auth.linkedinStatus()`, the `OAuthProvider` enum, and the `LinkedInStatusResponse` DTO, with 13 builder tests), so their **Implemented** column is ☑. **UPDATE 2026-07-31 — native OAuth identity linking is now BUILT on `feature/web-parity-batch-2026-07`, so the "blocked upstream" note is resolved** ([`work-consolidation.md`](../work-consolidation.md)): `Auth.linkIdentity` → `POST /api/auth/{provider}/link`, `UserService.linkIdentityNative`, a registered `interlinedlist://oauth/callback` custom scheme, and `ASWebAuthenticationSession` now let the app complete the flow natively rather than only handing `…/authorize?link=true` to the browser. The bearer `POST /api/auth/{provider}/link` completion endpoint that footnote 12 said "does not exist" is live and consumed; the new `POST /api/auth/{provider}/link` row is added in the [New endpoints re-baseline](#new-endpoints-2026-07-31-re-baseline--implementation-state-reconciled-2026-09-05) section under **Public profile & multi-account**. The five original `authorize`/`status` rows keep their historical Tested ☐ state here (their per-endpoint completion tests are backfilled with the native-linking work); flips follow the maintenance rule once a view-model test drives them end-to-end.
13. **M6 organization-read rows reachable but not view-model-tested this wave.** Same pattern as footnotes 9, 10, and 11, applied to Organizations. Two OrgService read rows are wired and reachable but not driven by a tested App-layer view model this wave: `GET /api/organizations` (the *list-all-orgs* variant) — the Wave 7.3 Organizations UI lists the current user's orgs through `UserService.organizations()` (`GET /api/user/organizations`) instead, so the `OrgService` list-all path stays unconsumed; and `GET /api/organizations/[id]/users` (`OrgService.users(of:)`) — the member roster is rendered from `GET /api/organizations/[id]/members` (`OrgMembersViewModel`), leaving the `/users` projection unconsumed. Both rows stay ◐⁴ until a polish slice consumes them through a tested view model. The Wave 1 footnote-4 backfill rule still applies.
+14. **2026-09-05 reconciliation pass — how the new rows were scored, and what was wrong.** The 2026-07-31 rows were added ☐/☐ and never rescored, though G1–G5, G11a, G12 and G14 shipped in the following weeks. This pass walked every row against the code and applied the existing maintenance rule literally. **Implemented ☑** requires a Kit request builder **and** a DTO **and** a Domain service call path — so `GET /api/linkedin/targets` stays ☐ despite `LinkedIn.targets()` existing, because no service calls it. **Tested ☑** was given where the endpoint has its own decode/encode test *and* its group supplies failure and empty coverage; **◐** where only the group-wide "builders construct the expected method/path/auth" test reaches it. No box was checked speculatively.
+ Four defects were found and fixed beyond the flips:
+ - **Six Moderation paths were wrong.** The rows were transcribed from OpenAPI as `POST /api/user/blocks`, `DELETE /api/user/blocks/{username}`, `POST /api/reports/user`, `POST /api/reports/message` and the mute equivalents. The shipped client uses `POST|DELETE /api/users/{username}/block`, `…/mute`, `POST /api/users/{username}/report` and `POST /api/messages/{id}/report`, whose header comment records them as verified against the live `/help/api/moderation` docs and the 2026-07-31 probe. The rows were corrected to the live-verified paths; the two is-blocking / is-muting **status** reads (`GET /api/user/{blocks,mutes}/{username}`) have no client implementation and stay ☐.
+ - **Two GitHub paths were wrong.** The matrix documented `PATCH /api/github/issues/{owner}/{repo}/{number}` and its `/comments` sibling; the client sends `PATCH /api/github/repos/{repo}/issues/{number}` and `POST /api/github/repos/{repo}/issues/{number}/comments`. Rows corrected to what the client actually sends. Both routes remain **unconfirmed against the live API** (P1-H2 in `work-consolidation.md`) — Implemented ☑ records that the client path is built and tested, not that the route answers.
+ - **Six shipped endpoints were missing from the matrix entirely** — the list and document `/invites` families (`GET|POST /api/{lists,documents}/{id}/invites`, `DELETE …/invites/{token}`), all built in `SharingEndpoint.swift` and driven by `SharingService`. Added as G3 rows, taking that section 17 → 23.
+ - **The subtotal and grand total were arithmetic errors** — "53 new" is the running total through the sixth of sixteen sections, and "151 endpoints" inherited it. Recomputed from the rows: 89 new, 187 total.
+ Two rows are scored `—` as non-targets: **G6 List Folders** (feature removed from the client 2026-09-05, PR #19) joins **G8 Stripe** (out of scope 2026-07-31). The routes still exist server-side; the client will not implement them.
+
+15. **GitHub issue update / comment: the live routes are the FLAT ones, and the client points elsewhere (2026-09-05 live re-measure).** Both audit passes on 2026-09-05 corrected these rows to the *client's* nested path. The live API disagrees with the client: `GET /api/openapi.json` lists `PATCH /api/github/issues/{owner}/{repo}/{number}` and `POST /api/github/issues/{owner}/{repo}/{number}/comments`, and authenticated `OPTIONS` calls return `Allow: OPTIONS, PATCH` and `Allow: OPTIONS, POST` on those paths — i.e. exactly the three-segment flat form the 2026-07-31 re-baseline originally documented. The client's `GitHub.updateIssue` / `GitHub.comment` build `/api/github/repos/{repo}/issues/{number}[/comments]`, which 404s, so these rows are **Implemented ☐**: a builder exists, but not for this route. This also resolves the backend ask [P1-H2](../work-consolidation.md#p1-h2-github-issue-update-comment-routes) — no backend change is needed, only a client path fix ([`work-consolidation.md` §1c · V7](../work-consolidation.md#1c-live-verb-defects--fix-first)). `labels`, `assignees`, and `next-issue-number` are unaffected — their nested `/api/github/repos/{owner}/{repo}/…` routes answer `GET`.
+
## Cross-check against PLAN.md §1 (2026-06-11)
- Every API surface named in PLAN.md §1 maps to at least one row above. No PLAN.md endpoint is missing from the live reference.
@@ -314,7 +354,11 @@ New HTTP methods / paths on already-listed resource families, surfaced by the re
## Update history
-- **2026-07-31 — Re-baseline against live `openapi.json` (~150 endpoints).** The matrix was stale at 98 endpoints (2026-06-11 surface); the live API has grown to ~150 across new feature areas. Re-baselined per [`work-consolidation.md`](../work-consolidation.md): (1) the intro banner now states the ~150-endpoint re-baseline; (2) all **98 original rows keep their real ☑/◐/☐ implementation-and-test state unchanged** (98 implemented; 74 ☑ / 18 ◐ / 6 ☐ tested); (3) a new **"New endpoints (2026-07-31 re-baseline)"** section adds **53 rows** — all ☐ Implemented / ☐ Tested — grouped by feature area and mapped to gap IDs G1–G14: Direct Messages (G1) 11, Moderation (G2) 10, Share Links & Collaborators (G3) 17, List Folders (G6) 4, Search (G5) 3, GitHub (G4) 8, Push (G9) 2, Stripe/Billing (G8) 2, LinkedIn targets (G11a) 4, Twitter/X auth (G7) 3, Document templates & tree (G12) 6, Document presence (G13) 2, Utility/limits (G14) 2, Multi-account (G10) 3, Public profile & OAuth-link (D2 / fn 12) 2, and Messages & auth drift additions (D1/D3) 4. Each new row carries a **Backend** marker: ✅ confirmed live in the 2026-07-31 authenticated probe, ⚠️ live-but-constrained, or *per OpenAPI, unverified* (spec-listed / gap-planned but not individually hit read-only). **Footnote 8 RESOLVED** — `GET /api/users/{username}` public profile now exists (verified live, migration D2); the direct-read row is added and the decision-0002 fallback is slated for replacement. **Footnote 12 RESOLVED** — native OAuth identity linking (`POST /api/auth/{provider}/link` via `Auth.linkIdentity` + `ASWebAuthenticationSession` + `interlinedlist://oauth/callback`) is BUILT on `feature/web-parity-batch-2026-07`; the "blocked upstream" note no longer applies and the `…/link` row is added. **Grand total: 98 original + 53 new = 151 endpoints (~150).** No original row's ☑/◐/☐ mark was changed; new rows flip only under the existing maintenance rule (a tested App-layer view model drives them end-to-end).
+- **2026-09-05 (later) — reconciled the two parallel audit passes and folded in the live re-measure.** Two sessions audited this matrix the same evening; PR #22 merged the first. This entry merges the second pass on top and adds what a fresh live pull found. **From the second pass:** four implemented endpoints neither pass had a row for (`GET /api/users/search`, `GET /api/users/lookup`, `GET /api/auth/bluesky/status`, `GET /api/auth/mastodon/status`) are added as a new section, and `GET /api/auth/twitter/authorize` flips ☐/☐ → ☑/☑ — it is built by the shared `Auth.authorize(provider:)` builder (`OAuthProvider` includes `.twitter`) and consumed by `UserService` + `LinkedAccountsViewModel`. Where the two passes graded *Tested* differently on shared rows, PR #22's grades stand; they are internally consistent and no less defensible. **From the live re-measure:** the two GitHub issue rows are corrected again — to the **flat** routes the live spec and `OPTIONS` confirm, with Implemented back to ☐ because the client's builders point at a 404ing nested path (footnote 15); and a staleness banner now heads the file, because `GET /api/openapi.json` reports **294 operations** against this matrix's 191 rows. **Totals recomputed:** 93 new rows, 191 total.
+
+- **2026-09-05 — Reconciliation pass over the 2026-07-31 new-endpoint rows.** The re-baseline rows were added ☐/☐ and never rescored, so the matrix understated the app by a wide margin: **56 of the 83 scoreable new rows were already shipped**. Walked every row against the Kit builders, DTOs and Domain service call paths and applied the maintenance rule literally (footnote 14 records the exact ☑-vs-◐ rule used). **58 row edits:** 50 rows flipped ☐/☐ → Implemented ☑ with Tested ☑ or ◐ across Direct Messages (9 of 11), Moderation (8 of 10), Share Links & Collaborators (15 of 17 documented), Search (3 of 3), GitHub (8 of 8), LinkedIn posting-targets, Document templates (3 of 6), `GET /api/limits`, `GET /api/users/{username}` (D2) and `POST /api/auth/{provider}/link` (fn 12); **8 rows had their documented path corrected** to what the client actually sends (six Moderation, two GitHub — see footnote 14). **Six shipped `/invites` endpoints that were absent from the matrix were added** (G3: 17 → 23 rows). **`GET /api/dm/images/upload`, `GET /api/dm/{id}`, `GET /api/lists/watching`, `GET /api/lists/shared/{token}/data`, `GET /api/linkedin/targets`, `GET /api/documents/tree`, and the two block/mute status reads stay ☐** — verified absent from the client. **G6 List Folders rescored `—` (feature removed, PR #19 `1afb89d`)**, joining the out-of-scope G8 Stripe rows. **Totals recomputed from the rows, not carried forward: the previous "53 new / 151 total" was an arithmetic slip** (53 is the running total through the sixth of sixteen sections); the matrix is **89 new + 98 original = 187 rows**, of which **154 ☑ implemented / 27 ☐ / 6 —** and **104 ☑ / 44 ◐ / 33 ☐ / 6 — tested**. No original-98 row was touched. Footnote 14 added.
+
+- **2026-07-31 — Re-baseline against live `openapi.json` (~150 endpoints).** The matrix was stale at 98 endpoints (2026-06-11 surface); the live API has grown to ~150 across new feature areas. Re-baselined per [`work-consolidation.md`](../work-consolidation.md): (1) the intro banner now states the ~150-endpoint re-baseline; (2) all **98 original rows keep their real ☑/◐/☐ implementation-and-test state unchanged** (98 implemented; 74 ☑ / 18 ◐ / 6 ☐ tested); (3) a new **"New endpoints (2026-07-31 re-baseline)"** section adds **53 rows** — all ☐ Implemented / ☐ Tested — grouped by feature area and mapped to gap IDs G1–G14: Direct Messages (G1) 11, Moderation (G2) 10, Share Links & Collaborators (G3) 17, List Folders (G6) 4, Search (G5) 3, GitHub (G4) 8, Push (G9) 2, Stripe/Billing (G8) 2, LinkedIn targets (G11a) 4, Twitter/X auth (G7) 3, Document templates & tree (G12) 6, Document presence (G13) 2, Utility/limits (G14) 2, Multi-account (G10) 3, Public profile & OAuth-link (D2 / fn 12) 2, and Messages & auth drift additions (D1/D3) 4. Each new row carries a **Backend** marker: ✅ confirmed live in the 2026-07-31 authenticated probe, ⚠️ live-but-constrained, or *per OpenAPI, unverified* (spec-listed / gap-planned but not individually hit read-only). **Footnote 8 RESOLVED** — `GET /api/users/{username}` public profile now exists (verified live, migration D2); the direct-read row is added and the decision-0002 fallback is slated for replacement. **Footnote 12 RESOLVED** — native OAuth identity linking (`POST /api/auth/{provider}/link` via `Auth.linkIdentity` + `ASWebAuthenticationSession` + `interlinedlist://oauth/callback`) is BUILT on `feature/web-parity-batch-2026-07`; the "blocked upstream" note no longer applies and the `…/link` row is added. **Grand total: 98 original + 53 new = 151 endpoints (~150).** No original row's ☑/◐/☐ mark was changed; new rows flip only under the existing maintenance rule (a tested App-layer view model drives them end-to-end). *[Corrected 2026-09-05: the "53 new / 151 total" arithmetic in this entry was wrong — 53 is the running total through the sixth of sixteen sections. The 2026-07-31 pass actually added **83** rows for a **181**-row matrix; it is 187 today after six omitted `/invites` endpoints were added. This historical entry is left as written; see footnote 14 and the 2026-09-05 entry above.]*
- **2026-07-03 — Wave 8 update (M7 Ship: LiveSessionEstablisher, Exports E2E, Settings/Account E2E).** Wave 8.1 landed `LiveSessionEstablisher` (`CredentialStore` protocol + `KeychainCredentialStore` production + `InMemoryCredentialStore` tests) — the real `POST /api/auth/login` cookie-session fallback that was stubbed via `NullSessionEstablisher` since Wave 1. `AuthService.signIn` now persists credentials to Keychain so the establisher can re-authenticate lazily; `AppEnvironment.live()` wired with a dedicated ephemeral `URLSession` (isolated cookie jar). `LiveSessionEstablisherTests` covers the full quartet (6 new Kit tests; InterlinedKit suite 190 → 196). Wave 8.2 added `ExportViewModelTests` (8 tests) + `StubExportsService` to the App test suite, exercising all four export paths end-to-end through the view model. Wave 8.3 confirmed `AccountViewModelTests` (11 tests) already in the suite, covering avatar upload, email-change, account deletion, and sign-out quartet. Wave 8.0 (NW probe) confirmed all 6 NW-blocked items remain upstream-blocked; `NEXT-WORK.md` probe log appended. **Rows flipped ◐⁴ → ☑ this wave (7 total):** `GET /api/exports/messages`, `GET /api/exports/lists`, `GET /api/exports/list-data-rows`, `GET /api/exports/follows` (via `ExportViewModel` → `ExportsServicing` end-to-end with `ExportViewModelTests`), `POST /api/user/avatar/upload`, `POST /api/user/change-email/request`, `POST /api/user/delete` (via `AccountViewModel` → `UserServicing` end-to-end with `AccountViewModelTests`). **Row flipped ☐ → ☑ (Implemented + Tested): `POST /api/auth/login`** (`LiveSessionEstablisher` + `LiveSessionEstablisherTests` full quartet). **Math: Implemented 97 → 98 of 98 (all endpoints now implemented); Tested fully 66 → 74 of 98 (+8); Tested partial 25 → 18 of 98 (−7 from ◐⁴→☑, plus the ☐→☑ POST /api/auth/login removes 1 from untested not partial); Untested ☐ 7 → 6 of 98 (POST /api/auth/login now fully tested).** App test suite: 278 → 305 tests; InterlinedKit: 190 → 196; grand total across all targets: 976 → 1017. Footnote 5 resolved.
diff --git a/work-consolidation.md b/work-consolidation.md
index a6cd443..19bb9e2 100644
--- a/work-consolidation.md
+++ b/work-consolidation.md
@@ -2,8 +2,8 @@
**Single source of truth for remaining work, in execution order.** This file consolidates and replaces six older docs (see [Provenance](#provenance)): the parity gap lists, the backend-blocker index + paste-ready prompts, the Document Sync Agent plan, and the v1 release checklist.
-- **Consolidated:** 2026-08-15 · **Last synced to code:** 2026-09-05 · **Branch:** `dev` (⚠️ `main` is behind — catch it up before a release cut) · **Bundle:** `com.interlinedlist.macos` · **Team:** `BJA9558E4B`
-- **Structure:** [§1 Immediate work (do now)](#1-immediate-work--unblocked-do-now) → [§2 Blocked work (backend / spike-first)](#2-blocked-work--backend-gated-or-spike-first) → [§3 Final work (release & App Store)](#3-final-work--release--app-store)
+- **Consolidated:** 2026-08-15 · **Last synced to code:** 2026-09-05 · **Last re-measured against the live API:** 2026-09-05 (`GET /api/openapi.json` under a Bearer token from the `.env` test account: **226 paths / 294 operations**; the client builds 154) · **Branch:** `dev` (⚠️ `main` is behind — catch it up before a release cut) · **Bundle:** `com.interlinedlist.macos` · **Team:** `BJA9558E4B`
+- **Structure:** [§1 Immediate work (do now)](#1-immediate-work--unblocked-do-now) — including [§1c verb defects](#1c-live-verb-defects--fix-first) and [§1d the 2026-09-05 parity batch](#1d-new-feature-areas-2026-09-05-re-measure) → [§2 Blocked work (backend / spike-first)](#2-blocked-work--backend-gated-or-spike-first) → [§3 Final work (release & App Store)](#3-final-work--release--app-store)
- **Test baseline (all green, re-run 2026-09-05):** InterlinedKit **314** · InterlinedDomain **619** · InterlinedPersistence **135** · App target **622** (`** TEST SUCCEEDED **`). *(Kit/Domain dipped from 317/628 because PR #19 removed the List Folders endpoint + service tests along with the feature.)* Packages verified this session under plain `swift test`; the App target ran green under `xcodebuild test … CODE_SIGNING_ALLOWED=NO` (the signing override is still required on this machine). *(Prior 2026-08-16 baseline was Kit 286 / Domain 590 / App 566; growth is from G4 GitHub issues, sharing collaborators/invites/visibility, and timeline cross-post links.)*
- **Distribution model:** notarized **`.pkg`** (+ `.dmg`) is the **current** ship path (closed-source private repo, no `LICENSE`). Mac App Store is a **later** path on a separate branch. Billing is handled by the web app — the native app has **no** in-app-purchase surface; it only *reads* `customerStatus` to gate subscriber features.
@@ -27,7 +27,13 @@ Milestones **M0–M7** feature work is complete; post-milestone items NW-1…NW-
- **Sharing collaborators / invites / visibility** (PR #13, merged 2026-09-02) — extends the G3 sharing group: per-person document collaborators (search/add/set-role/remove), email invites for lists **and** documents, and a make-public visibility toggle. Full stack (Kit `SharingEndpoint`/`SharingDTO`, Domain `Sharing` models + `SharingService`, App `DocumentCollaborators*`/`Invites*`/`Visibility*` views + VMs) with Kit/Domain/App tests. Create paths are subscriber-gated.
- **Timeline cross-post destination links** (PR #14, merged 2026-09-02) — a message row links out to where it was cross-posted (Bluesky/Mastodon/X/LinkedIn external URLs) via the Domain `Message` cross-post projection + mappers.
-**Where we are now:** §1 (client-side parity) is **exhausted** — every unblocked gap is built and merged, the G14 `ImagePrep` tail included (verified 2026-09-05). The only open §1-adjacent thread is G4's two backend-blocked routes, which are not a client gap. The remaining levers are **§2** (backend-gated — nothing more buildable from the client until the backend moves; the one high-impact item is [P1-G](#p1-g-following-feed)) and **§3** (release engineering — the PKG/DMG ship path in [§3a](#3a-notarized-pkgdmg-release--the-current-ship-path), which is now the critical path to shipping). **Next phase: release engineering.**
+**Where we are now (re-measured against the live API 2026-09-05):** the "§1 is exhausted" conclusion **no longer holds**. A fresh authenticated pull of `GET /api/openapi.json` reports **226 paths / 294 operations** — the ~150-endpoint figure this doc was built on is a year-stale baseline. The macOS client builds **154** of them. Stripping the parts a native client should never call (admin 26, cron 7, webhooks 2, analytics-ingest, `test-db`, `openapi.json`, `oauth/client-metadata`, Stripe 2 — billing stays in the web app by owner decision) leaves roughly **100 live operations the app does not implement**, including whole product areas that shipped on the web after the re-baseline: **AI writing/generation, "Create from…" (materialize), Applications settings-and-devices sync, notification preferences, session revocation, tags, and link-metadata previews.**
+
+Worse, six calls the client ships today use a **verb the live server rejects** — message edit, preferences save, list-row edit, organization edit, document-folder rename, and follower removal are all broken against production right now ([§1c](#1c-live-verb-defects--fix-first)). Those are correctness bugs, not gaps.
+
+So the levers are, in priority order: **[§1c](#1c-live-verb-defects--fix-first)** (verb defects — fix first, they break shipped features), **[§1d](#1d-new-feature-areas-2026-09-05-re-measure)** (the new unblocked parity batch), **§2** (still genuinely backend-gated — [P1-G](#p1-g-following-feed) re-verified still broken 2026-09-05), and **§3** (release engineering, unchanged and still the ship path).
+
+*(The G1–G14 batch is indeed complete — the paragraph this replaced was accurate about the gaps known at the time, the G14 `ImagePrep` tail included. What changed is the size of the live surface it was measured against.)*
---
@@ -35,7 +41,9 @@ Milestones **M0–M7** feature work is complete; post-milestone items NW-1…NW-
Everything here is client-side and buildable today (the backend already exists or none is needed). Ordered by value.
-> **Status 2026-09-05 — §1 is DONE.** Every item below is built and merged to `dev`, including the **G14** `ImagePrep` size-ceiling tail (verified in code 2026-09-05 — see G14). The one remaining open thread is **G4**'s issue **update**/**comment** routes, which are backend-blocked ([P1-H2](#p1-h2-github-issue-update-comment-routes)) — not a client gap. No client-only parity work remains; **§3 release is the critical path**.
+> **⚠️ Superseded 2026-09-05 by the live re-measure — see [§1c](#1c-live-verb-defects--fix-first) and [§1d](#1d-new-feature-areas-2026-09-05-re-measure).** The statement below is true of the gaps *known at the time*; the live API has since grown well past this doc's baseline.
+>
+> **Status 2026-09-05 — the G1–G14 batch is DONE.** Every item below is built and merged to `dev`, including the **G14** `ImagePrep` size-ceiling tail (verified in code 2026-09-05 — see G14). The one remaining open thread is **G4**'s issue **update**/**comment** routes, which are backend-blocked ([P1-H2](#p1-h2-github-issue-update-comment-routes)) — not a client gap. No client-only parity work remains; **§3 release is the critical path**.
### 1a. Parity features buildable now
@@ -63,6 +71,7 @@ The `/api/github/*` routes are deployed (`GET /api/github/repos` → 400 "GitHub
> - **`PUT /api/messages/[id]` → HTTP 405 — backend ask, see [§2 · P2-I](#p2-i-message-edit-method).** Message **edit** is broken against the live API; the correct verb (likely `PATCH`) needs backend confirmation before a client change. Left as-is (client still sends `PUT`) pending that confirmation.
> - **`GET /api/user/identities` responded 200 under Bearer** even though the client declares it `auth: .session`. Read-only note; no change made (session remains the documented contract).
+
**G11a · LinkedIn posting target** — ✅ **Target-aware toggle shipped 2026-08-15.** `LinkedInService` is now wired into `AppEnvironment`; enabling the composer's LinkedIn cross-post toggle fetches `postingTargets()` and shows **which destination the post publishes to** ("Posting as …"), rolls the toggle back with a connect hint when the account has no LinkedIn target, and surfaces the org-scope-missing note — all mirroring the Bluesky/Mastodon readiness pattern and reusing the verified `crossPostToLinkedIn` request path. 6 composer tests. **Deferred (needs a verified wire shape):** a true multi-*target selector* and the `POST /api/linkedin/sync-pages` refresh both wait on a confirmed per-target request field; LinkedIn **org** pages are upstream-blocked (G11b).
**G14 · `/api/limits` composer validation** — ✅ **Message-length validation shipped 2026-08-15.** New Kit `Limits` endpoint + `LimitsDTO`; domain `ContentLimits` model + `ContentLimitsService` (fetch with `ContentLimits.default` fallback); wired into the composer as a live character counter + publish gate (over-limit disables Post, turns the counter/border red). 13 tests (4 Kit + 4 Domain + 5 App). **Tail also shipped — verified 2026-09-05.** The media *size* ceilings are server-driven end to end: `ImagePrep.Limits` + `prepare(_:limits:)` replace the hard-coded constants (which remain only as `Limits.default`), `ContentLimits.imagePrepLimits` projects the live values in, and both `MessagesService.uploadImage` (`MessagesService.swift:654`) and `DocumentsService.uploadImage` (`DocumentsService.swift:300`) pass them; `uploadVideo` (`MessagesService.swift:668`) uses the live `videoMaxBytes` with the static `maxVideoBytes` as fallback. `AppEnvironment` injects `ContentLimitsService` into both services. Covered by `ImagePrepTests` (custom-limits cases) and `MessagesServiceM6Tests` (live-limit override).
@@ -78,6 +87,93 @@ The `/api/github/*` routes are deployed (`GET /api/github/repos` → 400 "GitHub
---
+
+### 1c. Live-verb defects — fix first (found 2026-09-05)
+
+Six shipping calls send an HTTP verb the live server does not accept, so the feature behind each one **fails against production today**. Each was confirmed twice: the live `openapi.json` lists only the other verb, and an authenticated `OPTIONS` returns an `Allow` header without the client's verb. No backend work is needed — these are one-line client fixes plus a regression test.
+
+| # | Client call | Sends | Live `Allow` | Broken user-facing behavior |
+| --- | --- | --- | --- | --- |
+| V1 | `Messages.update` → `/api/messages/{id}` | `PUT` | `DELETE, GET, HEAD, OPTIONS, PATCH` | **Editing a message.** Closes [P2-I](#p2-i-message-edit-method) — the verb is `PATCH`, no backend ask needed. |
+| V2 | `User.update` → `/api/user/update` | `POST` | `OPTIONS, PATCH` | **Saving Settings ▸ Preferences.** The pane shipped 2026-08-16 against a `POST` the server no longer accepts. |
+| V3 | `Lists.updateRow` → `/api/lists/{id}/data/{rowId}` | `PATCH` | `DELETE, GET, HEAD, OPTIONS, PUT` | **Editing a list row** (the row inspector's save path). |
+| V4 | `Organizations.update` → `/api/organizations/{id}` | `PATCH` | `DELETE, GET, HEAD, OPTIONS, PUT` | **Editing an organization.** |
+| V5 | `Documents.updateFolder` → `/api/documents/folders/{id}` | `PATCH` | `DELETE, GET, HEAD, OPTIONS, PUT` | **Renaming / moving a document folder.** |
+| V6 | `Follow.remove` → `/api/follow/{userId}/remove` | `POST` | `DELETE, OPTIONS` | **Removing a follower.** |
+
+**V7 · GitHub issue update + comment routes — [P1-H2](#p1-h2-github-issue-update-comment-routes) is RESOLVED, and the client is pointed at the wrong paths.** The live spec and `OPTIONS` both confirm the **flat** routes exist: `PATCH /api/github/issues/{owner}/{repo}/{number}` (`Allow: OPTIONS, PATCH`) and `POST /api/github/issues/{owner}/{repo}/{number}/comments` (`Allow: OPTIONS, POST`) — exactly the shape the coverage matrix documented before the 2026-08-17 pass moved the client to nested `/api/github/repos/{repo}/issues/{n}` paths that 404. Point `GitHub.updateIssue` and `GitHub.comment` back at the flat routes and close/reopen, label/assignee editing, and commenting all start working. (`labels`, `assignees`, and `next-issue-number` are already correct — their nested `/api/github/repos/{owner}/{repo}/…` routes answer `GET`.)
+
+> **Verify-before-fix, as always:** `OPTIONS` proves which verb is accepted, not which body shape or response envelope the route returns. Exercise each corrected call against the `.env` test account before tightening any decoder — the `POST /api/messages` envelope drift found during the G7 pass is exactly the failure mode to expect.
+
+
+### 1d. New feature areas — the 2026-09-05 re-measure
+
+Product areas that exist on the live API and in the published help docs (`https://interlinedlist.com/help`) but have **no client implementation at all**. Ordered by user-visible value. Sizes are rough. Every one is client-side buildable: the backend already ships them.
+
+
+**G15 · AI writing & generation — HIGH, the biggest single parity gap. Size L.**
+Live and available to the test account: `GET /api/ai/status` returns `{"subscriber":true,"providers":["anthropic"],"defaultModels":{…},"quota":{"usedToday":0,"dailyLimit":50,"remaining":50}}`; the write pair is `POST /api/ai/suggest` (run a feature, return a validated **preview**) and `POST /api/ai/generate` (persist a confirmed artifact from that preview). Per `/help/ai` the surface is four features: **composer writing assistant** (rewrite, tighten, expand, fix grammar, convert-to-thread, suggest tags), **series planning** (a brief → a sequence of connected posts, or an article series), **AI list templates** (describe a structure → drafted schema + starter rows), and **AI documents** (from a topic, a list, an existing article, or a URL). Subscriber-gated, and the user brings their own OpenAI / Anthropic / Gemini key via the web Integrations page. The suggest→confirm→generate shape maps cleanly onto a preview sheet with a Confirm button. **The `feature` enum and both request/response bodies are unmodelled in the OpenAPI spec** (`{"feature": string}` is all it declares) — probe live or read the web app's network calls before building.
+
+
+**G16 · "Create from…" (materialize) — HIGH. Size M.**
+`POST /api/materialize` creates a **List, a Document, or both** from a source object. Per `/help/create-from` the sources are one or many messages, one or many lists, one or many list rows, and a whole document or a highlighted markdown selection; the preview lets the user set title/description/visibility, rename columns and change column types for the list target, and choose numbered/bulleted styling for the doc target. On macOS this is a `+ Create` menu on a row, a selection-bar action for multi-select, and a selection menu in the document editor. Request body is unmodelled in the spec (`{"source": string}`) — probe first.
+
+
+**G17 · Applications: synced settings + device registry — HIGH for a native client specifically. Size M.**
+`GET/PUT/DELETE /api/user/app-settings/{appKey}`, `GET /api/user/app-settings/{appKey}/bootstrap?deviceId=…`, `GET/POST /api/user/app-settings/{appKey}/devices`, `GET/PUT /api/user/app-settings/{appKey}/devices/{deviceId}/settings`, `PATCH/DELETE /api/user/app-settings/{appKey}/devices/{deviceId}`. Per `/help/app-settings` this is the platform's *own* mechanism for companion apps: **shared settings** follow the account to every machine, **per-machine settings** stay pinned to one computer, one machine is the "main workstation" whose config seeds a brand-new device on first sign-in, and devices can be renamed or deregistered. This is the sanctioned home for the macOS app's preferences **and** the Document Sync Agent's per-machine configuration, replacing purely local `UserDefaults` state. Pick and register an `appKey` with the backend owner before building.
+
+
+**G18 · Notification preferences — MEDIUM. Size S.**
+`GET /api/user/notification-preferences` returns a typed event catalogue — `{"events":[{"key":"dig","label":"Digs on your messages","description":"…","channels":{"push":true,"inApp":true}}, …]}` — and `PATCH` writes it. Server-driven labels and descriptions mean the pane renders itself from the payload; drop it into Settings beside Preferences. Also the per-event `channels.push` flags are the switchboard [G9 push](#2b-spike-first-native-gaps) will need.
+
+
+**G19 · Active sessions & token revocation — MEDIUM. Size S. (Closes the client half of [P3-D](#p3-d-sessions-revocation).)**
+`GET /api/user/sessions` is **Bearer-reachable** and returns `{"sessions":[{"id","deviceLabel","createdAt","lastUsedAt","isCurrent"}…]}`; `DELETE /api/user/sessions/{id}` revokes one. A Settings ▸ Security pane listing sessions with a Revoke action is a small, self-contained slice — and it is the honest complement to a never-expiring sync token.
+
+
+**G20 · Tags: trending + autocomplete — MEDIUM. Size S.**
+`GET /api/tags/trending` (verified: `{"tags":[{"tag","count","lastUsedAt"}…]}`) and `GET /api/tags/autocomplete` (prefix match on public messages). Feeds a composer tag-completion popover and a trending strip on the timeline. Pairs naturally with G15's "suggest tags" assistant.
+
+
+**G21 · Link metadata / previews — MEDIUM. Size S–M.**
+`GET /api/link-metadata?url=…`, plus `GET /api/messages/{id}/metadata` (read stored metadata, lightweight) and `POST /api/messages/{id}/metadata` (fetch and persist a message's link metadata). The app already **ships a "link previews" toggle** in Settings ▸ Preferences with nothing behind it, and [P3-F](#2c-backend-confirmation--polish-asks) documents the client rendering previews from a value set it guessed. Also `GET /api/images/proxy` for server-side image fetching.
+
+
+**G22 · Direct-message completeness — MEDIUM. Size S.**
+Three routes the DM feature shipped without: `GET /api/dm/conversations` (one row per conversation grouped by `pairKey`, newest first — verified live, `{"items":[],"nextCursor":null}` — this is the natural inbox list, versus today's folder-based `GET /api/dm`), `GET /api/dm/{id}` (single message), and `POST /api/dm/images/upload` (DM image attachments, which the DM composer advertises but cannot perform).
+
+
+**G23 · Lists: shared-with-me, contributors, watcher add — MEDIUM. Size M.**
+`GET /api/lists/watching` (verified live, returns real rows) is the **"shared with me" / watched-lists** surface the sidebar lacks. Also `GET /api/lists/{id}/contributors` (full ranked contributor list), `POST /api/lists/{id}/watchers` (add watchers — the client can only read and delete), `GET /api/lists/shared/{token}/data` (row data for a token-shared list, the read-only viewer's missing half), and the invite landing pair `GET`/`POST /api/lists/invite/{token}`.
+
+
+**G24 · Documents: sidebar tree, public docs, invites, presence — MEDIUM. Size M.**
+`GET /api/documents/tree` returns `{folders, rootDocuments}` in **one** call — today the sidebar assembles that from several. `GET /api/users/{username}/documents` is public documents by user (the profile page has no documents tab). `GET`/`POST /api/documents/invite/{token}` are the invite landing/claim pair matching the list ones. `POST /api/documents/folders/{id}/documents` creates a document directly in a folder. `POST`/`DELETE /api/documents/{id}/presence` is the live-cursor heartbeat — **defer**: it is a collaborative-editing feature with a polling cost, worth building only if multi-user editing is a goal.
+
+
+**G25 · Organization admin + LinkedIn org pages — MEDIUM. Size M. (This is [G11b](#2d-upstream-blocked--deferred-confirm-demand-before-building), no longer upstream-blocked.)**
+`PUT /api/organizations/{id}` and `DELETE /api/organizations/{id}` (the client can create and read but not rename or delete — and it sends `PATCH`, see [V4](#1c-live-verb-defects--fix-first)), plus the org LinkedIn set that was recorded as 404/not-deployed and now exists: `GET /api/organizations/{id}/linkedin/status`, `POST /api/organizations/{id}/linkedin/sync-pages`, `PUT /api/organizations/{id}/linkedin/assignments`, `DELETE /api/organizations/{id}/linkedin/credential`. Personal-scope `PUT /api/linkedin/posting-targets` and `POST /api/linkedin/sync-pages` also exist, which closes the "needs a verified per-target wire shape" note on [G11a](#g11a-linkedin-posting-target).
+
+
+**G26 · Identity management: unlink + verify — LOW–MEDIUM. Size S.**
+`DELETE /api/user/identities` (unlink a provider — Settings ▸ Linked Accounts can link but never unlink), `POST /api/user/identities/verify`, and `GET /api/auth/github/status` (the client has `bluesky`/`mastodon`/`linkedin`/`twitter` status but not GitHub's).
+
+
+**G27 · Small, self-contained gaps — LOW. Size XS each.**
+`DELETE /api/notifications/{id}` (delete a single notification; the client can only mark read), `POST /api/messages/{id}/reply-counts`, `GET /api/user/engagement` (aggregate dig/push engagement on your own messages — **note:** returned 401 under Bearer in the 2026-09-05 probe, so it may be session-only; confirm before building), and `PUT /api/documents/{id}` (a full-replace variant beside the `PATCH` the client already uses).
+
+
+**G28 · Dashboard / front-wall layouts + widgets — CONFIRM DEMAND before building. Size L.**
+`GET`/`PUT /api/user/dashboard-layout`, `GET`/`PUT /api/user/front-wall-layout`, and the widget feeds `GET /api/widgets/{markets,news,transit,transit/stops,bike-share}` plus `GET /api/weather` and `GET /api/location`. `/help/getting-started` puts the Dashboard second in prominence on the web, so this is real product surface — but it is a large, web-layout-shaped feature, and a native app may want its own arrangement rather than mirroring the web's saved layout. **Owner decision needed before any of it is built.**
+
+
+**G29 · Blog — CONFIRM DEMAND. Size M (read) / L (authoring).**
+`/help/blog` documents a Blog feature; the public API surface is only subscribe/confirm/unsubscribe (`/api/blog/*`), while authoring lives behind admin-only routes (`/api/admin/blog*`). A native reader is plausible; native authoring is admin-gated and probably out of scope. **Owner decision needed.**
+
+**Explicitly out of scope for the native client** (counted here so future re-measures stop re-flagging them): `/api/admin/**` (26 ops, admin console), `/api/cron/**` (7, scheduler), `/api/webhooks/**` (2, Stripe + Resend), `/api/stripe/**` (2 — billing is managed in the web app by owner decision, see [G8](#2d-upstream-blocked--deferred-confirm-demand-before-building)), `POST /api/analytics/ingest`, `GET /api/test-db`, `GET /api/openapi.json`, `GET /api/oauth/client-metadata`, and `/api/architecture-aggregates/**`.
+
+---
+
## 2. Blocked work — backend-gated or spike-first
Cannot be finished from the client alone. Each item carries a paste-ready prompt for the InterlinedList backend Claude Code session (base URL `https://interlinedlist.com`). **The single high-impact backend blocker is P1-G (Following feed);** everything else is a spike, a confirmation, or low-priority polish.
@@ -85,15 +181,15 @@ Cannot be finished from the client alone. Each item carries a paste-ready prompt
### 2a. High-impact blocker
-**P1-G · Following / home feed endpoint** — **HIGH.** Re-verified 2026-07-31: `GET /api/messages` ignores `feed`/`scope`/`following`/`filter` (every variant returns the same "all" feed) and `POST /api/user/update {viewingPreference}` → 405. The client's `TimelineScope.following` is fully UI-wired (All/Mine/Following picker) but `MessagesService.timeline` short-circuits `.following` to an empty "coming soon" page (`MessagesService.swift:364`). One client branch flips to consume this the moment it exists.
+**P1-G · Following / home feed endpoint** — **HIGH. Re-verified STILL BROKEN 2026-09-05:** an authenticated `GET /api/messages?limit=50` and `GET /api/messages?limit=50&scope=following` return the identical 50 messages from the identical author set, so the parameter is still ignored a month on. This remains the one high-impact backend blocker. *(Original 2026-07-31 finding:* `GET /api/messages` ignores `feed`/`scope`/`following`/`filter` (every variant returns the same "all" feed) and `POST /api/user/update {viewingPreference}` → 405. The client's `TimelineScope.following` is fully UI-wired (All/Mine/Following picker) but `MessagesService.timeline` short-circuits `.following` to an empty "coming soon" page (`MessagesService.swift:364`).* ) One client branch flips to consume this the moment it exists.
> **PROMPT:** You are working on the InterlinedList API (interlinedlist.com). Add a followed-accounts timeline feed. Preferred: extend `GET /api/messages` with `?scope=following` (or add `GET /api/feed/following`), returning only messages authored by accounts the caller follows, using the **same paginated envelope** as `GET /api/messages` (same `limit`/`offset`/`hasMore` shape). Bearer auth. Document it. Note: as of 2026-07-31 the live server silently ignores `scope`/`feed`/`following`/`filter` on `GET /api/messages` and `POST /api/user/update {viewingPreference}` → 405, so this needs a real implementation, not just docs. The macOS client already has the UI wired and flips one branch to consume it.
### 2b. Spike-first native gaps
-**G9 · Push notifications (APNs)** — route live (`POST /api/push/register` → 400 "token is required"). **Spike S2 first:** does a sandboxed, notarized, non-App-Store `.pkg` support APNs, and what provisioning is required? Also confirm the `unregister` verb (POST → 405, likely DELETE). Deep-link routing wants the backend `routePath` field ([P2-C](#p2-c-notification-routepath)). Then: register the device token on launch/sign-in, unregister on sign-out; real pushes augment (don't replace) tray polling. **Size M.**
+**G9 · Push notifications (APNs)** — routes confirmed in the 2026-09-05 live spec (`POST /api/push/register`, `DELETE /api/push/unregister` — the `unregister` verb question is settled: it is **DELETE**). Per-event push switches now exist too ([G18](#g18-notification-preferences)). **Spike S2 first:** does a sandboxed, notarized, non-App-Store `.pkg` support APNs, and what provisioning is required? Also confirm the `unregister` verb (POST → 405, likely DELETE). Deep-link routing wants the backend `routePath` field ([P2-C](#p2-c-notification-routepath)). Then: register the device token on launch/sign-in, unregister on sign-out; real pushes augment (don't replace) tray polling. **Size M.**
-**G10 · Multi-account switching** — **Spike S4 first:** `/api/auth/accounts` returns 401 under Bearer (session-cookie-only). Resolve the Bearer-vs-session constraint (drive a cookie session for these routes, or request a bearer variant upstream — see [P3-D](#p3-d-sessions-revocation)) before building the account switcher + per-account `KeychainCredentialStore` + cache reset on switch. **Size M.**
+**G10 · Multi-account switching** — **Spike S4 first:** `/api/auth/accounts` returns **401 under Bearer, re-verified 2026-09-05** (session-cookie-only; `/api/auth/switch` and `/api/auth/remove-account` are the same family). Resolve the Bearer-vs-session constraint (drive a cookie session for these routes, or request a bearer variant upstream — see [P3-D](#p3-d-sessions-revocation)) before building the account switcher + per-account `KeychainCredentialStore` + cache reset on switch. **Size M.**
### 2c. Backend confirmation & polish asks
@@ -121,7 +217,7 @@ Reconciliations and additive niceties. The client already works around each; the
> **PROMPT (optional doc-only):** Document `crossPostToTwitter: true` on `POST /api/messages` alongside `crossPostToBluesky` / `crossPostToLinkedIn`, and document the `crossPosts[]` result entry `{ platform: "twitter", status, externalUrl }`.
-**P2-I · Message edit verb drift — `PUT /api/messages/[id]` → HTTP 405.** **NEW, found 2026-08-17.** The client edits a message via `PUT /api/messages/[id]` (`Messages.update`), but the live route returns **405 Method Not Allowed** with an empty body, so **editing a message is broken end-to-end**. `POST` (create) and `DELETE` work; `GET` returns a flat `MessageDTO`. The correct edit verb needs confirmation before the client changes (likely `PATCH`, mirroring `/api/documents/[id]`). Until then the composer's edit path fails against production.
+**P2-I · Message edit verb drift — ✅ RESOLVED 2026-09-05, no backend ask needed.** The live spec and `OPTIONS /api/messages/{id}` both give `Allow: DELETE, GET, HEAD, OPTIONS, PATCH` — the edit verb is **`PATCH`**. This is now a client fix, tracked as [V1](#1c-live-verb-defects--fix-first). *(Original 2026-08-17 finding:* The client edits a message via `PUT /api/messages/[id]` (`Messages.update`), but the live route returns **405 Method Not Allowed** with an empty body, so **editing a message is broken end-to-end**. `POST` (create) and `DELETE` work; `GET` returns a flat `MessageDTO`. The correct edit verb needs confirmation before the client changes (likely `PATCH`, mirroring `/api/documents/[id]`). Until then the composer's edit path fails against production.*)
> **PROMPT:** `PUT /api/messages/[id]` returns HTTP 405. Confirm the supported method for editing a message (we expect `PATCH /api/messages/[id]` with the same body shape as `POST /api/messages` — `content`, `publiclyVisible`, `tags`, cross-post flags). Document the verb, the accepted body fields, and the response envelope (the create response now wraps the message under `data` with a top-level `crossPosts` array — confirm edit matches). If `PUT` is intended to keep working, restore it.
**P3-A · Document version / ETag** for sync conflict detection.
@@ -134,7 +230,7 @@ Reconciliations and additive niceties. The client already works around each; the
> **PROMPT:** Add to List objects with `githubSource`: `{ "lastRefreshedAt": "iso-8601 or null", "refreshStatus": "idle|pending|failed", "refreshError": "string or null" }`. Also accept `githubSource` on `POST /api/lists`: `{ "owner", "repo", "path", "ref" }`; if provided, trigger initial refresh and return `refreshStatus: "pending"`.
-**P3-D · Token revocation + `GET /api/user/sessions`.** Relates to [G10](#2b-spike-first-native-gaps) (accounts are session-cookie-only, 401 under Bearer); a Bearer-reachable sessions surface helps both.
+**P3-D · Token revocation + `GET /api/user/sessions`** — ✅ **client half unblocked 2026-09-05.** `GET /api/user/sessions` **is** Bearer-reachable and returns `{id, deviceLabel, createdAt, lastUsedAt, isCurrent}` rows; `DELETE /api/user/sessions/{id}` revokes. Build it as [G19](#g19-sessions). The remaining ask is only the accounts/switch family below. Relates to [G10](#2b-spike-first-native-gaps) (accounts are session-cookie-only, 401 under Bearer); a Bearer-reachable sessions surface helps both.
> **PROMPT:** Add optional `deviceLabel` to `POST /api/auth/sync-token`. Implement `GET /api/user/sessions` → `{ sessions: [{ id, deviceLabel, createdAt, lastUsedAt, isCurrent }] }` and `DELETE /api/user/sessions/[id]` → 204 (token immediately invalid; 400 `{"error":"cannot_revoke_current_session"}` on self-revoke).
**P3-E · `RateLimit-*` headers universally.** Currently only on `POST /api/messages` and `POST /api/documents/sync`; client already nil-guards absent headers.
@@ -146,15 +242,15 @@ Reconciliations and additive niceties. The client already works around each; the
**P3-G · List "save to my lists" clone-with-rows.** `ListDetailViewModel.saveToMyLists` copies title/description/schema only, **no rows** (documented degradation; no clone endpoint exists).
> **PROMPT:** Add `POST /api/lists/[id]/clone` (or a rows-copy option on save) that duplicates a public list's rows into a new owned list, so "save to my lists" carries the data, not just the schema.
-**P3-H · Message edit verb reconciliation.** Reference documents `PATCH /api/messages/[id]`; client sends `PUT` (both work live).
+**P3-H · Message edit verb reconciliation** — ⛔ **superseded / the premise was wrong.** "Both work live" is no longer true (and may never have been): `PUT` is rejected outright. Folded into [P2-I](#p2-i-message-edit-method) / [V1](#1c-live-verb-defects--fix-first).
> **PROMPT:** The API reference documents message edit as `PATCH /api/messages/[id]`, but the client sends `PUT` and it works. Confirm the canonical verb and reconcile the reference with live behavior. While you're here, confirm the canonical verb for `/api/messages/{id}/replies` (OpenAPI shows `POST`; the client uses `GET`).
-**P1-H · GitHub issue create/comment + labels/assignees shapes** *(docs; unblocks [§1 · G4](#g4-github-issue-integration) end-to-end).* Routes are deployed; shapes are undocumented and can only be exercised once a test identity links GitHub. *(2026-08-17: `create` route + `title`-required confirmed via unlinked probe; `next-issue-number` confirmed to exist.)*
+**P1-H · GitHub issue create/comment + labels/assignees shapes** *(docs; unblocks [§1 · G4](#g4-github-issue-integration) end-to-end).* **Partially unblocked 2026-09-05: the test account is now GitHub-linked** — `GET /api/github/repos` returns **200** instead of the old 400 "not linked", and `GET /api/github/orgs` (a route the client does not build) also answers 200. But the repo list comes back **empty**, so issue request/response envelopes still cannot be exercised end-to-end; a repo has to be configured on that account first. Routes are deployed; shapes remain undocumented. *(2026-08-17: `create` route + `title`-required confirmed via unlinked probe; `next-issue-number` confirmed to exist.)*
> **PROMPT:** The GitHub issue routes are deployed but undocumented for third-party clients. Define and document, with concrete request/response JSON: (1) issue **labels** and **assignees** as fields on GitHub-sourced list rows; (2) the endpoint to **create a GitHub issue** from a synced list; (3) the endpoint to **comment on an issue**; (4) `next-issue-number` if it exists. State the linked-identity precondition and the exact 400 error body when unlinked. Coordinate with P3-C.
-**P1-H2 · GitHub issue UPDATE + COMMENT route location** *(NEW 2026-08-17 — the specific blocker for G4 close/reopen/label/assignee editing).* Unlinked route-surface probing established the issue **list/create** routes are flat (`GET`/`POST /api/github/issues?repo={owner/repo}`; `OPTIONS`→`Allow: GET, HEAD, OPTIONS, POST`) and the client is now fixed to use them. But the **update** and **comment** routes could not be found: `PATCH`/`PUT /api/github/issues` → 405; every nested/flat single-issue candidate (`/api/github/repos/{repo}/issues/{n}`, `/api/github/issues/{n}`, `/api/github/issues/{n}/comments`, `/api/github/issues/comments`, `/api/github/comments`) → 404.
+**P1-H2 · GitHub issue UPDATE + COMMENT route location** — ✅ **RESOLVED 2026-09-05 by the live spec; no backend ask needed.** The routes are the **flat** ones the coverage matrix originally documented: `PATCH /api/github/issues/{owner}/{repo}/{number}` (`OPTIONS` → `Allow: OPTIONS, PATCH`) and `POST /api/github/issues/{owner}/{repo}/{number}/comments` (`Allow: OPTIONS, POST`). The 2026-08-17 probe searched nested and single-segment candidates but not the three-segment flat form. Client fix tracked as [V7](#1c-live-verb-defects--fix-first). *(Original 2026-08-17 finding:* Unlinked route-surface probing established the issue **list/create** routes are flat (`GET`/`POST /api/github/issues?repo={owner/repo}`; `OPTIONS`→`Allow: GET, HEAD, OPTIONS, POST`) and the client is now fixed to use them. But the **update** and **comment** routes could not be found: `PATCH`/`PUT /api/github/issues` → 405; every nested/flat single-issue candidate (`/api/github/repos/{repo}/issues/{n}`, `/api/github/issues/{n}`, `/api/github/issues/{n}/comments`, `/api/github/issues/comments`, `/api/github/comments`) → 404.*)
> **PROMPT (ready-to-paste):**
> You are working on the InterlinedList API backend (base URL `https://interlinedlist.com`). The macOS client integrates the GitHub issue routes under `/api/github/*`, but **two** operations point at routes that return 404/405 live, so issue **editing** and **commenting** are broken end-to-end. I mapped the route surface by probing with a Bearer token for an account whose GitHub identity is **not** linked — so every route that *exists* returns `400 {"error":"GitHub account not linked"}`, and every route that is *missing* returns the Next.js HTML 404. Please locate/confirm the two missing routes and document them.
>
@@ -173,8 +269,8 @@ Reconciliations and additive niceties. The client already works around each; the
### 2d. Upstream-blocked / deferred (confirm demand before building)
-- **G11b · LinkedIn org posting pages** — upstream-blocked on this tenant (`orgScopesEnabled:false`, `…/linkedin-page` → 404; not deployed). Personal targets (G11a) are unaffected.
-- **G13 · Document presence / live cursors** — highest complexity, lowest urgency; not probed. Confirm demand first.
+- ~~**G11b · LinkedIn org posting pages** — upstream-blocked on this tenant.~~ — ✅ **DEPLOYED as of the 2026-09-05 live spec; moved to [§1d · G25](#g25-org-admin).** `GET /api/organizations/{id}/linkedin/status`, `POST /api/organizations/{id}/linkedin/sync-pages`, `PUT /api/organizations/{id}/linkedin/assignments`, and `DELETE /api/organizations/{id}/linkedin/credential` all exist now, as do the personal-scope `PUT /api/linkedin/posting-targets` and `POST /api/linkedin/sync-pages`. Re-confirm `orgScopesEnabled` on the tenant before building.
+- **G13 · Document presence / live cursors** — routes confirmed live 2026-09-05 (`POST`/`DELETE /api/documents/{id}/presence`, a combined heartbeat+poll), so it is no longer *blocked* — just still the highest-complexity, lowest-urgency item. Tracked with the rest of the document gaps in [§1d · G24](#g24-documents-gaps). Confirm demand first.
---
@@ -256,6 +352,10 @@ Removing Sparkle breaks the PKG/DMG channel, so App Store work happens on a sepa
## Provenance
+> **Docs reconciliation 2026-09-05.** `docs/api-coverage.md` was walked row-by-row against the shipped code after its 2026-07-31 re-baseline rows were found never to have been rescored — **56 of 83 scoreable new rows were already shipped but still marked ☐/☐**. That pass also corrected eight endpoint paths the matrix had transcribed from OpenAPI rather than from the live-verified client (six Moderation, two GitHub), added six shipped `/invites` endpoints the matrix omitted entirely, rescored the removed G6 List Folders rows as non-targets, and recomputed the totals: the matrix is **187 rows**, not the "151 (~150)" previously carried here and in `docs/api-coverage.md`. **Any note in this file or elsewhere citing a "~151-endpoint API surface" is stale by that amount.** Details in footnote 14 of `docs/api-coverage.md`.
+
+
+
This file consolidates and replaces the following, now removed (recoverable via git history):
- `feature-gaps.md` — parity gap snapshot (2026-07-18 → refreshed 2026-08-15).
@@ -266,3 +366,8 @@ This file consolidates and replaces the following, now removed (recoverable via
- `v1-release-checklist.md` — the release/App Store checklist (folded into [§3](#3-final-work--release--app-store)).
Retained references: `App-Dmg-Pkg-Deployment.md` (deployment command detail), `README.md`, `docs/`.
+
+## Re-measure log
+
+- **2026-09-05 — live re-measure + parity sweep.** Authenticated against `https://interlinedlist.com` with the `.env` contract-test account (`POST /api/auth/sync-token` → Bearer), pulled `GET /api/openapi.json` (**226 paths / 294 operations**, OpenAPI 3.1, `InterlinedList API 0.1.0`) and diffed it against the 154 request builders in `Packages/InterlinedKit/Sources/InterlinedKit/Endpoints/*.swift`. Cross-read the published help docs at `/help` (18 topics) for the user-facing feature list. **Found:** (1) six shipping calls send a verb the server rejects — [§1c](#1c-live-verb-defects--fix-first), each confirmed by both the spec and an authenticated `OPTIONS` `Allow` header; (2) ~100 unimplemented live operations after excluding admin/cron/webhooks/Stripe/analytics, including whole new product areas — [§1d](#1d-new-feature-areas-2026-09-05-re-measure) G15–G29; (3) [P1-H2](#p1-h2-github-issue-update-comment-routes) and [P2-I](#p2-i-message-edit-method) are **resolved** — both were client-side path/verb errors, not backend gaps; (4) [P1-G](#p1-g-following-feed) re-verified **still broken**, and [G10](#2b-spike-first-native-gaps)'s `/api/auth/accounts` re-verified **still 401 under Bearer**; (5) the test account is now **GitHub-linked** (`/api/github/repos` → 200) but its repo list is empty, so G4 issue envelopes still cannot be exercised. All probes were read-only (`GET`/`OPTIONS`); no writes were made.
+