Skip to content

Latest commit

 

History

History
431 lines (362 loc) · 76.7 KB

File metadata and controls

431 lines (362 loc) · 76.7 KB

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 across whole new feature areas; those are captured in the New endpoints section, mapped to their gap ID (G1–G14) in 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.

⚠️ Re-measured 2026-09-14 — Auth is now verified; Implemented is NOT yet re-scored. A fresh authenticated pull of GET /api/openapi.json reports 232 paths / 303 operations (up from 226/294 on 2026-09-05), against this matrix's 191 rows. The client builds 200 request builders covering 194 spec operations; roughly 52 in-scope operations remain unimplemented (down from ~100). The gap analysis lives in work-consolidation.md §1f.

What this pass did change: the Auth column is now derived from the live spec's x-auth-type, not hand-written — 168 of 191 rows rewritten, replacing the ~50 that read "per OpenAPI, unverified". Bearer ✅ means a sync-token client can reach it; Session-only ⛔ means it rejects Bearer entirely. This is the difference between not built and not buildable, and it is now legible per row. Eleven rows also had a stale method or path corrected — six of them the §1c verb defects, where the matrix still documented the broken verb the client stopped sending in PR #24 (see footnote 16).

What this pass did NOT do: re-score Implemented / Tested. This file's own maintenance rule requires a Kit builder and a DTO and a Domain service call path before a row may flip ☑ — a semantic check per row, not a mechanical one. Flipping 191 rows mechanically would have produced confident-looking marks nobody verified, which is the failure mode footnote 14 was written about. That re-score is queued as its own pass. Footnote 16 records the evidence gathered for it.

Audience: engineering (maintainers and implementing agents).

This matrix exists so that full coverage of the InterlinedList API is verified, not assumed (PLAN.md §7). It maps every documented endpoint to the service planned to implement it (PLAN.md §3) and the milestone that ships it (PLAN.md §6), with check-off columns for implementation and tests.

Maintenance rule: the documentation engineer updates this matrix at the end of each wave, after the wave gate passes. A row's Implemented box is checked only when the endpoint's request builder, DTOs, and service call path are merged; Tested is checked only when BDD-named unit tests against APIClient stubs cover that endpoint (happy path, invalid input, API failure, empty/boundary — PLAN.md §7). No box is checked speculatively.

  • Source of truth for the endpoint inventory: https://interlinedlist.com/help/api (verified 2026-06-11), cross-checked against PLAN.md §1.
  • ☐ = not done, ☑ = done, ◐ = partial (builder + DTO + service path merged and at least one behavior test exists, but not all four of happy/invalid/failure/empty are present yet — see footnote 4). All rows start unchecked.
  • Auth column reproduces the API reference's annotation. Groups marked Session are subject to the M0 Bearer-vs-Session spike (docs/spikes/auth-bearer-vs-session.md, decision in docs/decisions/0001-auth-transport.md).
  • The three GET /api/users/[username]/lists* endpoints appear in the API reference under both Lists and Public; they are listed once here, under Lists, with no-auth noted.
Endpoint (method + path) Group Auth Planned service Milestone Implemented Tested
POST /api/auth/login Auth Public LiveSessionEstablisher (InterlinedKit/Auth) M7 ☑ ☑
POST /api/auth/logout Auth Public AuthService (InterlinedKit/Auth) M0 ☑ ◐⁴
POST /api/auth/register Auth Public AuthService (InterlinedKit/Auth) M0 ☑ ☐⁶
POST /api/auth/sync-token Auth Public AuthService (InterlinedKit/Auth) M0 ☑ ◐⁴
POST /api/auth/forgot-password Auth Public AuthService (InterlinedKit/Auth) M0 ☑ ◐⁴
POST /api/auth/reset-password Auth Public AuthService (InterlinedKit/Auth) M0 ☑ ◐⁴
POST /api/auth/send-verification-email Auth Session-only ⛔ AuthService (InterlinedKit/Auth) M0 ☑ ◐⁴
POST /api/auth/verify-email Auth Public AuthService (InterlinedKit/Auth) M0 ☑ ◐⁴
GET /api/auth/github/authorize Auth (OAuth) Public AuthService (OAuth flows) M6 ☑ ☐¹²
GET /api/auth/mastodon/authorize Auth (OAuth) Public AuthService (OAuth flows) M6 ☑ ☐¹²
GET /api/auth/bluesky/authorize Auth (OAuth) Public AuthService (OAuth flows) M6 ☑ ☐¹²
GET /api/auth/linkedin/authorize Auth (OAuth) Public AuthService (OAuth flows) M6 ☑ ☐¹²
GET /api/user User Bearer ✅ UserService¹ (+ EntitlementsService reads customerStatus) M0 ☑ ☑
PATCH /api/user/update User Bearer ✅ UserService¹ M7 ☑ ☑
POST /api/user/avatar/upload User Bearer ✅ UserService¹ M7 ☑ ☑
POST /api/user/avatar/from-url User Bearer ✅ UserService¹ M7 ☑ ◐⁴
GET /api/user/identities User Bearer ✅ UserService¹ M6 ☑ ☑
GET /api/user/organizations User Bearer ✅ UserService¹ ⁷ M6 ☑ ☑
POST /api/user/change-email/request User Bearer ✅ UserService¹ M7 ☑ ☑
POST /api/user/delete User Bearer ✅ UserService¹ M7 ☑ ☑
GET /api/messages Messages Public MessagesService M1 ☑ ☑
POST /api/messages Messages Bearer ✅ MessagesService M2² ☑ ☑
GET /api/messages/[id] Messages Public MessagesService M1 ☑ ☑
PATCH /api/messages/[id] Messages Bearer ✅ MessagesService M2 ☑ ☑
DELETE /api/messages/[id] Messages Bearer ✅ MessagesService M2 ☑ ☑
GET /api/messages/scheduled Messages Bearer ✅ MessagesService M6 ☑ ☑
GET /api/messages/[id]/replies Messages Public MessagesService M1 ☑ ☑
POST /api/messages/[id]/dig Messages Bearer ✅ MessagesService M2 ☑ ☑
DELETE /api/messages/[id]/dig Messages Bearer ✅ MessagesService M2 ☑ ☑
POST /api/messages/images/upload Messages Bearer ✅ MessagesService M6 ☑ ☑
POST /api/messages/videos/upload Messages Bearer ✅ MessagesService M6 ☑ ☑
GET /api/lists Lists Bearer ✅ ListsService M3 ☑ ◐⁴
POST /api/lists Lists Bearer ✅ ListsService M3 ☑ ◐⁴
GET /api/lists/[id] Lists Bearer ✅ ListsService M3 ☑ ◐⁴⁹
PUT /api/lists/[id] Lists Bearer ✅ ListsService M3 ☑ ◐⁴⁹
DELETE /api/lists/[id] Lists Bearer ✅ ListsService M3 ☑ ☑
GET /api/lists/[id]/schema Lists Bearer ✅ ListsService M3 ☑ ☑
PUT /api/lists/[id]/schema Lists Bearer ✅ ListsService M3 ☑ ☑
POST /api/lists/[id]/refresh Lists Bearer ✅ ListsService M3 ☑ ☑
GET /api/lists/[id]/data Lists Bearer ✅ ListsService M3 ☑ ☑
POST /api/lists/[id]/data Lists Bearer ✅ ListsService M3 ☑ ☑
GET /api/lists/[id]/data/[rowId] Lists Bearer ✅ ListsService M3 ☑ ◐⁴⁹
PUT /api/lists/[id]/data/[rowId] Lists Bearer ✅ ListsService M3 ☑ ☑
DELETE /api/lists/[id]/data/[rowId] Lists Bearer ✅ ListsService M3 ☑ ☑
GET /api/lists/[id]/watchers Lists Bearer ✅ ListsService M3 ☑ ◐⁴⁹
GET /api/lists/[id]/watchers/me Lists Bearer ✅ ListsService M3 ☑ ☑
GET /api/lists/[id]/watchers/users Lists Bearer ✅ ListsService M3 ☑ ☑
PUT /api/lists/[id]/watchers/[userId] Lists Bearer ✅ ListsService M3 ☑ ☑
DELETE /api/lists/[id]/watchers/[userId] Lists Bearer ✅ ListsService M3 ☑ ☑
GET /api/users/[username]/lists Lists (public) Public ListsService M1 ☑ ☑
GET /api/users/[username]/lists/[id] Lists (public) Public ListsService M1 ☑ ☑
GET /api/users/[username]/lists/[id]/data Lists (public) Public ListsService M1 ☑ ☑
GET /api/lists/connections List Connections Bearer ✅ ListsService M3 ☑ ☑
POST /api/lists/connections List Connections Bearer ✅ ListsService M3 ☑ ☑
DELETE /api/lists/connections/[id] List Connections Bearer ✅ ListsService M3 ☑ ☑
GET /api/documents/sync Documents & Sync Bearer ✅ DocumentSyncEngine (InterlinedPersistence) M4 ☑ ☑
POST /api/documents/sync Documents & Sync Bearer ✅ DocumentSyncEngine (InterlinedPersistence) M4 ☑ ☑
GET /api/documents Documents & Sync Bearer ✅ DocumentsService M4 ☑ ☑
POST /api/documents Documents & Sync Bearer ✅ DocumentsService M4 ☑ ☑
GET /api/documents/[id] Documents & Sync Public DocumentsService M4 ☑ ◐⁴¹⁰
PATCH /api/documents/[id] Documents & Sync Bearer ✅ DocumentsService M4 ☑ ☑
DELETE /api/documents/[id] Documents & Sync Bearer ✅ DocumentsService M4 ☑ ☑
POST /api/documents/[id]/images/upload Documents & Sync Bearer ✅ DocumentsService M4 ☑ ☑
GET /api/documents/folders Documents & Sync Bearer ✅ DocumentsService M4 ☑ ☑
POST /api/documents/folders Documents & Sync Bearer ✅ DocumentsService M4 ☑ ☑
GET /api/documents/folders/[id] Documents & Sync Bearer ✅ DocumentsService M4 ☑ ◐⁴¹⁰
PUT /api/documents/folders/[id] Documents & Sync Bearer ✅ DocumentsService M4 ☑ ☑
DELETE /api/documents/folders/[id] Documents & Sync Bearer ✅ DocumentsService M4 ☑ ☑
GET /api/documents/folders/[id]/documents Documents & Sync Bearer ✅ DocumentsService M4 ☑ ☑
POST /api/follow/[userId] Follow Bearer ✅ SocialService M5 ☑ ☑
DELETE /api/follow/[userId] Follow Bearer ✅ SocialService M5 ☑ ☑
GET /api/follow/[userId]/status Follow Bearer ✅ SocialService M5 ☑ ☑
GET /api/follow/[userId]/followers Follow Bearer ✅ SocialService M5 ☑ ☑
GET /api/follow/[userId]/following Follow Bearer ✅ SocialService M5 ☑ ☑
GET /api/follow/[userId]/counts Follow Bearer ✅ SocialService M5 ☑ ☑
GET /api/follow/[userId]/mutual Follow Bearer ✅ SocialService M5 ☑ ☑
POST /api/follow/[userId]/approve Follow Bearer ✅ SocialService M5 ☑ ☑
POST /api/follow/[userId]/reject Follow Bearer ✅ SocialService M5 ☑ ☑
DELETE /api/follow/[userId]/remove Follow Bearer ✅ SocialService M5 ☑ ◐⁴¹¹
GET /api/follow/requests Follow Bearer ✅ SocialService M5 ☑ ☑
GET /api/organizations Organizations Bearer ✅ OrgService M6 ☑ ◐⁴¹³
POST /api/organizations Organizations Bearer ✅ OrgService M6 ☑ ☑
GET /api/organizations/[id] Organizations Bearer ✅ OrgService M6 ☑ ☑
PUT /api/organizations/[id] Organizations Bearer ✅ OrgService M6 ☑ ☑
GET /api/organizations/[id]/members Organizations Bearer ✅ OrgService M6 ☑ ☑
POST /api/organizations/[id]/members Organizations Bearer ✅ OrgService M6 ☑ ☑
PUT /api/organizations/[id]/members/[userId] Organizations Bearer ✅ OrgService M6 ☑ ☑
DELETE /api/organizations/[id]/members/[userId] Organizations Bearer ✅ OrgService M6 ☑ ☑
GET /api/organizations/[id]/users Organizations Bearer ✅ OrgService M6 ☑ ◐⁴¹³
GET /api/exports/messages Exports Bearer ✅ ExportsService¹ M7 ☑ ☑
GET /api/exports/lists Exports Bearer ✅ ExportsService¹ M7 ☑ ☑
GET /api/exports/list-data-rows Exports Bearer ✅ ExportsService¹ M7 ☑ ☑
GET /api/exports/follows Exports Bearer ✅ ExportsService¹ M7 ☑ ☑
GET /api/notifications Notifications Bearer ✅ NotificationsService M5 ☑ ☑
PATCH /api/notifications/[id]/read Notifications Bearer ✅ NotificationsService M5 ☑ ☑
POST /api/notifications/mark-all-read Notifications Bearer ✅ NotificationsService M5 ☑ ☑
GET /api/user/[username]/messages Public Public MessagesService⁸ M1 ☑ ☑
GET /api/auth/linkedin/status Public Public AuthService (OAuth flows) M6 ☑ ☐¹²

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) — implementation state reconciled 2026-09-05

The 2026-07-31 authenticated live probe (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. Auth column (regenerated 2026-09-14 from the live spec's x-auth-type — see footnote 16): Bearer ✅ = a sync-token client can reach it; Session-only ⛔ = rejects Bearer entirely and is unbuildable natively; Public = no auth; Does not exist ⛔ = the matrix documented a method or path the live spec does not serve. The retired "per OpenAPI, unverified" marker is gone from every row — that uncertainty is what the regeneration resolved. 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 Bearer ✅ G1 List DMs by folder (inbox/sent/deleted), cursor-paginated ☑ ☑
POST /api/dm Direct Messages Bearer ✅ G1 Send a DM to a mutual follower (≤8 image attachments) ☑ ☑
POST /api/dm/images/upload Direct Messages Bearer ✅ G1 Upload an image for a DM ☐ ☐
GET /api/dm/recipients Direct Messages Bearer ✅ G1 List eligible DM recipients (mutual followers) ☑ ☑
GET /api/dm/thread/{username} Direct Messages Bearer ✅ G1 Fetch the conversation thread with a user ☑ ☑
GET /api/dm/thread/{username}/updates Direct Messages Bearer ✅ G1 Poll for new messages in a thread since a marker ☑ ◐
GET /api/dm/unread-count Direct Messages Bearer ✅ G1 Unread-DM count for the badge ☑ ☑
GET /api/dm/{id} Direct Messages Bearer ✅ G1 Fetch a single DM ☐ ☐
POST /api/dm/{id}/read Direct Messages Bearer ✅ G1 Mark a DM read ☑ ◐
POST /api/dm/{id}/restore Direct Messages Bearer ✅ G1 Restore a trashed DM (per-side) ☑ ◐
POST /api/dm/{id}/trash Direct Messages Bearer ✅ G1 Soft-delete a DM (per-side) ☑ ◐

Moderation (G2) — 10

Endpoint (method + path) Group Backend Gap Purpose Implemented Tested
GET /api/user/blocks Moderation Bearer ✅ G2 List blocked users (paginated) ☑ ☑
POST /api/users/{username}/block Moderation Bearer ✅ G2 Block a user ☑ ◐
DELETE /api/users/{username}/block Moderation Bearer ✅ G2 Unblock a user ☑ ◐
GET /api/users/{username}/block Moderation Bearer ✅ G2 Is-blocking status for a user ☐ ☐
GET /api/user/mutes Moderation Bearer ✅ G2 List muted users (paginated) ☑ ☑
POST /api/users/{username}/mute Moderation Bearer ✅ G2 Mute a user ☑ ◐
DELETE /api/users/{username}/mute Moderation Bearer ✅ G2 Unmute a user ☑ ◐
GET /api/users/{username}/mute Moderation Bearer ✅ G2 Is-muting status for a user ☐ ☐
POST /api/users/{username}/report Moderation Bearer ✅ G2 Report a user (reason + detail) ☑ ☑
POST /api/messages/{id}/report Moderation Bearer ✅ G2 Report a message (reason + detail) ☑ ◐

Share Links & Collaborators (G3) — 23

Endpoint (method + path) Group Backend Gap Purpose Implemented Tested
GET /api/lists/{id}/share-links Share Links & Collaborators Bearer ✅ G3 List a list's tokenized share links ☑ ☑
POST /api/lists/{id}/share-links Share Links & Collaborators Bearer ✅ G3 Create a share link (role + expiry, subscriber-gated) ☑ ☑
DELETE /api/lists/{id}/share-links/{token} Share Links & Collaborators Bearer ✅ G3 Revoke a list share link ☑ ☑
GET /api/lists/shared/{token} Share Links & Collaborators Public G3 Resolve a shared list by token (read-only viewer) ☑ ☑
POST /api/lists/shared/{token} Share Links & Collaborators Bearer ✅ G3 Claim a shared list link ☑ ◐
GET /api/lists/shared/{token}/data Share Links & Collaborators Public G3 Read shared-list row data by token ☐ ☐
GET /api/lists/watching Share Links & Collaborators Bearer ✅ G3 "Shared-with-me" lists the user is watching ☐ ☐
GET /api/documents/{id}/share-links Share Links & Collaborators Bearer ✅ G3 List a document's share links ☑ ◐
POST /api/documents/{id}/share-links Share Links & Collaborators Bearer ✅ G3 Create a document share link (subscriber-gated) ☑ ◐
DELETE /api/documents/{id}/share-links/{token} Share Links & Collaborators Bearer ✅ G3 Revoke a document share link ☑ ◐
GET /api/documents/shared/{token} Share Links & Collaborators Public G3 Resolve a shared document by token ☑ ◐
POST /api/documents/shared/{token} Share Links & Collaborators Bearer ✅ G3 Claim a shared document link ☑ ◐
GET /api/documents/{id}/collaborators Share Links & Collaborators Bearer ✅ G3 List per-person document collaborators (paginated) ☑ ☑
POST /api/documents/{id}/collaborators Share Links & Collaborators Bearer ✅ G3 Add a document collaborator (by @handle + role) ☑ ☑
GET /api/documents/{id}/collaborators/users Share Links & Collaborators Bearer ✅ G3 Search users for collaborator invite ☑ ☑
PUT /api/documents/{id}/collaborators/{userId} Share Links & Collaborators Bearer ✅ G3 Set a collaborator's role ☑ ◐
DELETE /api/documents/{id}/collaborators/{userId} Share Links & Collaborators Bearer ✅ G3 Remove a document collaborator ☑ ◐
GET /api/lists/{id}/invites Share Links & Collaborators Bearer ✅ G3 List a list's pending email invites ☑ ◐
POST /api/lists/{id}/invites Share Links & Collaborators Bearer ✅ G3 Invite someone to a list by email ☑ ◐
DELETE /api/lists/{id}/invites/{token} Share Links & Collaborators Bearer ✅ G3 Revoke a list invite ☑ ◐
GET /api/documents/{id}/invites Share Links & Collaborators Bearer ✅ G3 List a document's pending email invites ☑ ☑
POST /api/documents/{id}/invites Share Links & Collaborators Bearer ✅ G3 Invite someone to a document by email ☑ ☑
DELETE /api/documents/{id}/invites/{token} Share Links & Collaborators Bearer ✅ 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 Bearer ✅ G6 REMOVED List hierarchical list-folders (flat array + parentId) — —
POST /api/folders List Folders Bearer ✅ G6 REMOVED Create a list-folder (subscriber-gated) — —
PUT /api/folders/{id} List Folders Bearer ✅ G6 REMOVED Rename / move a list-folder (cycle-safe) — —
DELETE /api/folders/{id} List Folders Bearer ✅ 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 Bearer ✅ G5 Server-side message search (?q=; POST → 405, search is GET) ☑ ☑
GET /api/lists/search Search Bearer ✅ G5 Server-side list search ☑ ☑
GET /api/documents/search Search Bearer ✅ G5 Server-side document search ☑ ☑

GitHub (G4) — 8

Endpoint (method + path) Group Backend Gap Purpose Implemented Tested
GET /api/github/repos GitHub Bearer ✅ G4 List linked-account repos (400 "not linked" until OAuth link) ☑ ☑
GET /api/github/issues GitHub Bearer ✅ G4 List issues for a repo ☑ ☑
POST /api/github/issues GitHub Bearer ✅ G4 Create an issue ☑ ☑
PATCH /api/github/issues/{owner}/{repo}/{number}¹⁵ GitHub Bearer ✅ 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 Bearer ✅ 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 Bearer ✅ G4 List assignable users for a repo ☑ ☑
GET /api/github/repos/{owner}/{repo}/labels GitHub Bearer ✅ G4 List labels for a repo ☑ ☑
GET /api/github/repos/{owner}/{repo}/next-issue-number GitHub Bearer ✅ G4 Next issue number for a repo ☑ ☑

Push (G9) — 2

Endpoint (method + path) Group Backend Gap Purpose Implemented Tested
POST /api/push/register Push Bearer ✅ G9 Register an APNs device token (400 "token is required" when empty → route live) ☐ ☐
DELETE /api/push/unregister Push Bearer ✅ G9 Unregister a device token (POST → 405; verb likely DELETE — confirm) ☐ ☐

Stripe / Billing (G8) — 2

Endpoint (method + path) Group Backend Gap Purpose Implemented Tested
POST /api/stripe/create-checkout-session Stripe / Billing Session-only ⛔ G8 OUT OF SCOPE Billing managed in the online app (owner decision 2026-07-31); route also 404s live — —
POST /api/stripe/create-portal-session Stripe / Billing Session-only ⛔ G8 OUT OF SCOPE Billing managed in the online app; route also 404s live — —

LinkedIn targets (G11a) — 4

Endpoint (method + path) Group Backend Gap Purpose Implemented Tested
GET /api/linkedin/targets LinkedIn targets Bearer ✅ G11a List LinkedIn posting targets (personal target present) ☐ ☐
GET /api/linkedin/posting-targets LinkedIn targets Bearer ✅ G11a Read enabled posting targets (enabled:true, orgScopeMissing:true) ☑ ☑
PUT /api/linkedin/posting-targets LinkedIn targets Bearer ✅ G11a Set enabled posting targets ☐ ☐
POST /api/linkedin/sync-pages LinkedIn targets Bearer ✅ G11a Refresh available LinkedIn pages ☐ ☐

Twitter / X auth (G7) — 3

Endpoint (method + path) Group Backend Gap Purpose Implemented Tested
GET /api/auth/twitter/authorize Twitter / X auth Public 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 Public G7 X/Twitter OAuth callback ☐ ☐
GET /api/auth/twitter/status Twitter / X auth Public G7 X/Twitter link status (configured:true) ☐ ☐

Document templates & tree (G12) — 6

Endpoint (method + path) Group Backend Gap Purpose Implemented Tested
GET /api/documents/templates Document templates & tree Bearer ✅ G12 List server-side document templates (seeded + _templates folder) ☑ ☑
POST /api/documents/templates/seed-defaults Document templates & tree Bearer ✅ G12 Seed the default template set ☑ ◐
POST /api/documents/from-template Document templates & tree Bearer ✅ G12 Preview a new document from a template ☐ ☐
POST /api/documents/from-template Document templates & tree Bearer ✅ G12 Create a document from a template ☑ ☑
GET /api/documents/tree Document templates & tree Bearer ✅ G12 One-call folders + documents sidebar payload ☐ ☐
POST /api/documents/folders/{id}/documents Document templates & tree Bearer ✅ G12 Create a document directly inside a folder ☐ ☐

Document presence (G13) — 2

Endpoint (method + path) Group Backend Gap Purpose Implemented Tested
POST /api/documents/{id}/presence Document presence Bearer ✅ G13 Send a live co-editing presence heartbeat ☐ ☐
DELETE /api/documents/{id}/presence Document presence Bearer ✅ G13 Clear presence on leaving a document ☐ ☐

Utility / limits (G14) — 2

Endpoint (method + path) Group Backend Gap Purpose Implemented Tested
GET /api/limits Utility / limits Public G14 Quota / media limits (drives composer validation + plan card) ☑ ☑
GET /api/images/proxy Utility / limits Public G14 Image-proxy helper (rich previews / avatars) ☐ ☐

Multi-account (G10) — 3

Endpoint (method + path) Group Backend Gap Purpose Implemented Tested
GET /api/auth/accounts Multi-account Session-only ⛔ G10 List switchable accounts (401 under Bearer — session-cookie-only; spike S4) ☐ ☐
POST /api/auth/switch Multi-account Session-only ⛔ G10 Switch the active account (session-bound) ☐ ☐
POST /api/auth/remove-account Multi-account Session-only ⛔ G10 Remove a linked account ☐ ☐

Public profile & multi-account (migrations D2 / OAuth-link) — 2

Endpoint (method + path) Group Backend Gap Purpose Implemented Tested
GET /api/users/{username} Public profile Public D2 (fn 8) Direct public-profile read (now live — replaces the decision-0002 fallback) ☑ ◐
POST /api/auth/{provider}/link Auth (OAuth) Bearer ✅ 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

New HTTP methods / paths on already-listed resource families, surfaced by the re-baseline (see work-consolidation.md):

Endpoint (method + path) Group Backend Gap Purpose Implemented Tested
POST /api/messages/scheduled Messages Does not exist ⛔ D1 RESOLVED 2026-09-14 — speculative row. The live spec offers only GET /api/messages/scheduled; there is no POST. The app's scheduledAt-on-create is the mechanism. Not a target. — —
POST /api/messages/{id}/replies Messages Does not exist ⛔ D3 RESOLVED 2026-09-14 — speculative row. The live spec offers only GET /api/messages/{id}/replies. Replies are created via POST /api/messages with a parent reference; there is no verb to align. Not a target. — —
POST /api/lists/{id}/watchers Lists Bearer ✅ — Invite a watcher via POST (matrix has PUT …/watchers/{userId}) ☐ ☐
POST /api/auth/verify-email-change Auth Public — Confirm a pending email-change (pairs with existing change-email/request) ☐ ☐

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 Bearer ✅ — Search users by handle (collaborator / invite pickers) ☑ ◐
GET /api/users/lookup User Bearer ✅ — Resolve a single @handle to a user ☑ ◐
GET /api/auth/bluesky/status Auth (OAuth) Bearer ✅ NW-4 Whether Bluesky OAuth is configured on the server ☑ ◐
GET /api/auth/mastodon/status Auth (OAuth) Bearer ✅ 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 + 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

  1. UserService / ExportsService were not explicitly named in PLAN.md §3 (its service list ends with an ellipsis: "MessagesService, ListsService, DocumentsService, SocialService, OrgService, NotificationsService…"). Wave 1 confirmed the convention: the User endpoint group ships as InterlinedKit.User (see Packages/InterlinedKit/Sources/InterlinedKit/Endpoints/UserEndpoint.swift) and the Exports group as InterlinedKit.Exports (ExportsEndpoint.swift); domain-side UserService / ExportsService wrappers are deferred to the milestone in which the consuming UI lands (M6/M7).

  2. POST /api/messages ships in M2 for plain posting; its scheduled-post (scheduledAt) and cross-posting (mastodonProviderIds, crossPostToBluesky, crossPostToLinkedIn) request fields land in M6. The row is checked Implemented at M2; the M6 wave update must confirm the extended fields are covered before the row counts toward M6. Wave 1 note: MessagesEndpointTests.test_givenCrossPostAndScheduled_whenCreateBuilt_thenEncodesAllSetFields already exercises encoding for the M6 fields against the builder. Resolved Wave 7 (2026-06-25): the M6 extended fields are now consumed end-to-end — MessagesService.createPost carries scheduledAt + cross-post flags + uploaded-media references, driven by the M6 composer extensions (ComposerViewModel) through a tested App-layer path. The row was already ☑ from Wave 1's plain-posting builder coverage and stays ☑; this footnote now records that the M6 field carriage is closed rather than pending.

  3. Repost (pushedMessageId), visibility, and tag filters are request/response fields on existing rows above, not separate endpoints — they carry no row of their own.

  4. Partial test coverage (◐). The row's request builder, DTOs, and APIClient.send path are merged and at least one behavior test exists (typically builder-shape assertion plus one or two of happy/invalid/failure/empty), but the full happy + invalid + failure + empty/boundary quartet required by PLAN.md §7 is not yet present for that specific endpoint. APIClient-level failure decoding is exercised exhaustively in APIClientTests / APIErrorTests, so per-endpoint failure paths inherit correct error mapping; the gap is dedicated per-endpoint behavior tests. To be backfilled in the milestone in which the row's domain service lands, before the row counts toward that milestone's gate.

  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 — 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) 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 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) 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): 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 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 — no backend change is needed, only a client path fix (work-consolidation.md §1c · V7). labels, assignees, and next-issue-number are unaffected — their nested /api/github/repos/{owner}/{repo}/… routes answer GET.

  16. 2026-09-14 auth-verification pass — what was done, and what was deliberately left. GET /api/openapi.json was pulled under a Bearer sync-token from the .env test account (232 paths / 303 operations, OpenAPI 3.1.0) and every row's Auth cell was regenerated from that operation's x-auth-type. 176 of 191 rows matched a live operation; 168 cells changed.

    The x-auth-type distribution is the headline, and it overturns a standing conclusion: sync-token 207 · none 57 · session 32 · cron 7. The matrix and work-consolidation.md both previously recorded 45 session-only operations as a hard ceiling on native parity. It is now 32, and 28 of those are routes a native client should never call (24 /api/admin/*, 2 /api/architecture-aggregates/*, 2 /api/stripe/*). The genuinely limiting set is four: GET /api/auth/accounts, POST /api/auth/switch, POST /api/auth/remove-account (G10 / #60) and POST /api/auth/send-verification-email. Both dashboard-layout routes, both front-wall-layout routes, GET /api/user/engagement and all five widgets/* routes are now sync-token and were confirmed 200 under Bearer by live read-only probe — so the Dashboard (G28 / #61) is no longer backend-blocked. Controls probed the other way: /api/auth/accounts and send-verification-email both returned 401, confirming they really are session-only.

    Eleven rows carried a method or path the live spec does not serve. Six were the §1c verb defects — the matrix still documented POST /api/user/update, PUT /api/messages/[id], PATCH /api/lists/[id]/data/[rowId], PATCH /api/documents/folders/[id], POST /api/follow/[userId]/remove and PATCH /api/organizations/[id], i.e. the broken verbs the client stopped sending in PR #24. The matrix was behind the code, not ahead of it. Two block/mute rows had the wrong path shape (/api/user/blocks/{username} → /api/users/{username}/block), two Stripe rows had pre-rename paths, and GET /api/documents/from-template is a POST. Two further rows (POST /api/messages/scheduled, POST /api/messages/{id}/replies) were speculative inventions — the spec offers only GET on both paths — and are now scored —, not targets.

    Implemented / Tested were NOT re-scored, on purpose. Footnote 14's rule requires a Kit request builder and a DTO and a Domain service call path before a row may show ☑. That is a per-row semantic judgement; a script can see the builder but not the call path. Mechanically flipping 191 rows would have produced exactly the unverified-but-confident marks footnote 14 exists to prevent. Evidence gathered for that queued pass: the client builds 200 distinct (method, path) request builders, 194 matching live operations; of 259 in-scope operations about 52 are unimplemented. That count is ±5 — it under-counts parameterised builders (one /api/auth/{provider}/authorize builder covers five spec paths) and requests built inline rather than through Request( (sync-token, login).

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.
  • Present in the live reference but not explicitly named in PLAN.md §1: POST /api/auth/logout (implied by the auth feature), GET /api/auth/linkedin/status (supports the LinkedIn cross-post/OAuth feature, M6), and GET /api/user/[username]/messages (public user messages; nearest §1 feature is user profiles, M1 — and after the 2026-06-21 spike, this row carries the M1 profile fallback per decision 0002 and footnote 8).
  • PLAN.md §1 surface with no live endpoint: the Profile-row's natural backing call GET /api/users/[username] does not exist on the live API (2026-06-21 spike). Captured in footnote 8 and decision 0002-public-profile-fallback; no row added to the matrix above until the upstream endpoint ships.
  • PLAN.md §4's "Session-only" list (replies, digs, follow, organizations, notifications, document CRUD) matches the live annotations. The live reference additionally marks the User group's write endpoints and Exports as Session — the M0 spike should probe these groups too.

Update history

  • 2026-09-14 — auth column verified against the live spec; eleven stale rows corrected; the session-only ceiling collapses. Pulled GET /api/openapi.json under a Bearer sync-token (232 paths / 303 operations, up from 226/294) and regenerated every row's Auth cell from x-auth-type: 168 of 191 cells rewritten, retiring the ~50 that read "per OpenAPI, unverified". Bearer ✅ vs Session-only ⛔ now makes not built and not buildable distinguishable per row — the column the 2026-09-07 sweep asked for. Session-only operations fell 45 → 32, and 28 of the 32 are admin/architecture/Stripe routes a native client should never call, leaving four that genuinely constrain us; the Dashboard, front-wall and widget routes are all Bearer-reachable now and were probed 200 live, so G28/#61 is a product decision rather than a blocker. Eleven rows had a stale method or path corrected, six of them the §1c verb defects where this matrix still documented the broken verb the client stopped sending in PR #24 — the matrix was behind the code. Two speculative rows (POST /api/messages/scheduled, POST /api/messages/{id}/replies) were disproved by the spec and rescored —. Implemented/Tested were deliberately NOT re-scored — footnote 14's rule needs a per-row semantic check, and a mechanical flip would manufacture unverified marks; that pass is queued, with its evidence recorded in footnote 16. All probes read-only. See footnote 16.

  • 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: (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.

  • 2026-06-25 — Wave 7 update (M6 Subscriber + orgs consumed end-to-end; native OAuth blocked upstream). InterlinedDomain M6 slice (OrgService over the Organizations endpoints with Organization / OrgMember / OrgUser / OrgRole / OrgsPage / OrgMembersPage / OrgMappers; UserService identities() / organizations() + LinkedIdentity / IdentityProvider and the new identityLinkURL(provider:instance:); MessagesService M6 write surface — createPost with media / scheduled / cross-post, scheduledPosts(), uploadImage / uploadVideo via ImagePrep, all subscriber-gated via EntitlementsService with a live entitlementsProvider backstop) plus the InterlinedPersistence SwiftDataOrgStore / SwiftDataLinkedIdentityStore. App-layer M6 UI: Organizations (OrganizationsRootView + OrganizationsListViewModel / OrganizationDetailViewModel / OrgMembersViewModel, .organizations route flipped), the M6 composer extensions + read-only ScheduledPostsRootView (ScheduledPostsViewModel, .scheduled route flipped), and the browser-handoff SettingsRootView → Linked accounts pane (LinkedAccountsView / LinkedAccountsViewModel). Kit gained the additive OAuth builders (Auth.authorize(provider:link:instance:), Auth.linkedinStatus(), OAuthProvider, LinkedInStatusResponse) from the 7.0 spike (spike 0002). Per the Wave 1 footnote-4 rule, every M6-consumed row exercised by a tested App-layer view model this wave flips ◐⁴ → ☑. 11 rows flipped ◐⁴ → ☑: GET /api/user/organizations (UserService.organizations → OrganizationsListViewModel), POST /api/organizations (OrgService.create → OrganizationsListViewModel), GET /api/organizations/[id] (OrgService.organization(id:) → OrganizationDetailViewModel), PATCH /api/organizations/[id] (OrgService.update → OrganizationDetailViewModel), GET /api/organizations/[id]/members (OrgService.members(of:) → OrgMembersViewModel), POST /api/organizations/[id]/members (OrgService.addMember → OrgMembersViewModel), PUT /api/organizations/[id]/members/[userId] (OrgService.setMemberRole → OrgMembersViewModel), DELETE /api/organizations/[id]/members/[userId] (OrgService.removeMember → OrgMembersViewModel), GET /api/user/identities (UserService.identities → LinkedAccountsViewModel), POST /api/messages/images/upload (MessagesService.uploadImage → ComposerViewModel; ImagePrep exercised), POST /api/messages/videos/upload (MessagesService.uploadVideo → ComposerViewModel). Five OAuth rows flipped Implemented ☐ → ☑ but stay Tested ☐¹² — by design (new footnote 12): GET /api/auth/{github,mastodon,bluesky,linkedin}/authorize and GET /api/auth/linkedin/status gained Kit builders (+13 tests) but native completion is blocked upstream (decision 0006) — the app opens …/authorize?link=true in the browser (reached indirectly via UserService.identityLinkURL), it does not send these requests, and linkedin/status is unconsumed. Two org-read rows stay ◐⁴ — held back (new footnote 13): GET /api/organizations (list-all variant — UI uses UserService.organizations() instead) and GET /api/organizations/[id]/users (OrgService.users(of:) unconsumed — roster renders from /members). Re-consumed but unchanged (☑ already): POST /api/messages (already ☑ from Wave 1's M6-field builder coverage; its scheduled / cross-post / media fields are now consumed end-to-end via ComposerViewModel — footnote 2 resolved), GET /api/messages/scheduled (already ☑ from Wave 1; re-consumed read-only by ScheduledPostsViewModel). Math: Implemented 92 → 97 of 98 (+5 OAuth builders; only POST /api/auth/login⁵ remains unimplemented); Tested fully 55 → 66 of 98 (+11); Tested partial 36 → 25 of 98 (−11); Untested 7 of 98 (unchanged — the 5 OAuth rows are now Implemented-but-Tested-☐¹², plus POST /api/auth/login⁵ and POST /api/auth/register⁶). Footnotes 12 and 13 added; footnote 2 marked resolved. No other footnotes touched.

  • 2026-06-24 — Wave 6 update (M5 Social + Notifications consumed end-to-end). InterlinedDomain Follow / Notifications slice (FollowMappers, FollowRelationship + FollowAction, FollowRequest, MutualCounts, Notification, NotificationKind + NotificationTarget, NotificationMappers, SocialService write surface, NotificationsService) shipped across commits cae57cc (Wave 1 deviation 5 closure — Follow envelopes pinned to live API), da13846 (domain models + services + tests), 159f71a (namespace-alias workaround), 085b91f (Wave 6.1 closure: InterlinedDomain → InterlinedDomain_Module marker rename + SwiftDataNotificationStore / SwiftDataFollowCountsStore persistence tests + Follow action backend ask 2.3b). App-layer Social + Notifications UI shipped in commit 9a66154 (FollowButton + FollowButtonViewModel, SocialRosterRootView + SocialRosterViewModel for Followers / Following / Requests tabs, FollowRequestRowViewModel shared between tray and roster, NotificationsRootView + NotificationsListViewModel + NotificationRowView + NotificationRowCopy, NotificationsPermissionCoordinator, NotificationsUnreadBadgeCoordinator + NotificationsEventBus, ProfileHeaderView + ProfileViewModel, NotificationsMenuCommands + SocialMenuCommands, sidebar .connections entry, Decision-0005 App/Composition/AppDelegate.swift for dock badge + UN delegate, and App/Composition/FollowRelationshipReader.swift composition-root adapter). Per the Wave 1 deviation-6 rule, every M5-consumed row exercised by a tested App-layer view model this wave is now fully tested (☑) end-to-end (Kit builder → Domain service → App view-model). 8 rows flipped ◐⁴ → ☑: POST /api/follow/[userId] (SocialService.follow → FollowButtonViewModel.performFollow), DELETE /api/follow/[userId] (SocialService.unfollow → FollowButtonViewModel.performUnfollow), GET /api/follow/[userId]/mutual (SocialService.mutual → ProfileViewModel.loadProfile), POST /api/follow/[userId]/approve (SocialService.approve → SocialRosterViewModel.approve / FollowRequestRowViewModel.approve), POST /api/follow/[userId]/reject (SocialService.reject → SocialRosterViewModel.reject / FollowRequestRowViewModel.reject), GET /api/follow/requests (SocialService.requests → SocialRosterViewModel.loadRequests), PATCH /api/notifications/[id]/read (NotificationsService.markRead → NotificationsListViewModel.markRead), POST /api/notifications/mark-all-read (NotificationsService.markAllRead → NotificationsListViewModel.markAllRead). One Follow row stays ◐⁴ — held back as documented in new footnote 11: POST /api/follow/[userId]/remove (the "remove from my followers" action — distinct from unfollow) is reachable via SocialService.removeFollower but no Wave 6.3 view model surfaces it; flips when a polish slice consumes it. Re-consumed but unchanged (☑ already): GET /api/follow/[userId]/status (already ☑ from Wave 2; re-consumed via FollowRelationshipReader → FollowButtonViewModel), GET /api/follow/[userId]/followers / GET /api/follow/[userId]/following / GET /api/follow/[userId]/counts (already ☑ from Wave 2; re-consumed by SocialRosterViewModel / ProfileViewModel), GET /api/notifications?scope=tray (already ☑ from Wave 1; re-consumed by NotificationsListViewModel.load). Math: Implemented 92 of 98 (unchanged); Tested fully 47 → 55 of 98 (+8); Tested partial 44 → 36 of 98 (−8); Untested 7 of 98 (unchanged). Footnote 11 added. No other footnotes touched.

  • 2026-06-23 — Wave 5 update (M4 Documents consumed end-to-end). InterlinedDomain Documents slice (Document, FolderNode, DocumentSyncEvent, DocumentChange, DocumentMappers, ImagePrep, DocumentsService, DocumentSyncTransport) and InterlinedPersistence (DocumentRecord, FolderRecord, OutboxEntryRecord, SyncStateRecord, SwiftDataDocumentStore, DocumentSyncEngine) shipped in commit daf1eef; App-layer Documents UI (DocumentsRootView, DocumentsSidebarView + FolderTreeViewModel, DocumentsListView + DocumentsListViewModel, DocumentEditorView + DocumentEditorViewModel, ConflictBannerView, SyncStatusView + SyncStatusViewModel, DocumentsMenuCommands, KitDocumentSyncTransport wiring) shipped in commit babb6d2. Per the Wave 1 protocol in footnote 4, every M4-consumed Documents & Sync row exercised by a tested App-layer view model this wave is now fully tested (☑) end-to-end (Kit builder → Domain service → App view-model). 12 rows flipped ◐⁴ → ☑: GET /api/documents/sync (KitDocumentSyncTransport.pullDelta via DocumentSyncEngine.syncNow via SyncStatusViewModel.syncNow), POST /api/documents/sync (KitDocumentSyncTransport.pushChange via DocumentSyncEngine.syncNow outbox push), GET /api/documents (DocumentsService.documents(in:limit:offset:) when folder is nil → DocumentsListViewModel.reload), POST /api/documents (DocumentsService.create → DocumentsListViewModel.createDocument), PATCH /api/documents/[id] (DocumentsService.update → DocumentEditorViewModel.saveNow), DELETE /api/documents/[id] (DocumentsService.delete → DocumentsListViewModel.deleteDocument), POST /api/documents/[id]/images/upload (DocumentsService.uploadImage → DocumentEditorViewModel.uploadImage; ImagePrep is exercised in the upload path), GET /api/documents/folders (DocumentsService.folders → FolderTreeViewModel.initialLoad), POST /api/documents/folders (DocumentsService.createFolder → FolderTreeViewModel.createFolder), PATCH /api/documents/folders/[id] (DocumentsService.renameFolder → FolderTreeViewModel.renameFolder), DELETE /api/documents/folders/[id] (DocumentsService.deleteFolder → FolderTreeViewModel.deleteFolder), GET /api/documents/folders/[id]/documents (DocumentsService.documents(in:limit:offset:) when folderID != nil → DocumentsListViewModel.reload). Two Documents & Sync rows stay ◐⁴ — held back as documented in new footnote 10: GET /api/documents/[id] and GET /api/documents/folders/[id] are reachable via DocumentsService.document(id:) / folder(id:) but the Wave 5.3 view models open documents and folders from the list payload (and from the sync delta) rather than re-reading by id. Math: Implemented 92 of 98 (unchanged); Tested fully 35 → 47 of 98 (+12); Tested partial 56 → 44 of 98 (−12); Untested 7 of 98 (unchanged). Footnote 10 added. No other footnotes touched.

  • 2026-06-23 — Wave 4 update (M3 Lists consumed end-to-end). InterlinedDomain Lists write surface + schema DSL + InterlinedPersistence SwiftData lists cache shipped in commit 415c5c2; App-layer Lists UI (owned-lists root, schema editor, rows table, row inspector, watchers, connections graph) shipped in commits 461e7df + 155c955 (view models) + 099d8d9 (views, sidebar router, menu commands). Per the Wave 1 protocol in footnote 4, every M3-consumed row that was partial (◐⁴) and is exercised by a tested App-layer view model this wave is now fully tested (☑) end-to-end (Kit builder → Domain service → App view-model). 15 rows flipped ◐⁴ → ☑: DELETE /api/lists/[id], GET /api/lists/[id]/schema, PUT /api/lists/[id]/schema, POST /api/lists/[id]/refresh, GET /api/lists/[id]/data, POST /api/lists/[id]/data, PATCH /api/lists/[id]/data/[rowId], DELETE /api/lists/[id]/data/[rowId], GET /api/lists/[id]/watchers/me, GET /api/lists/[id]/watchers/users, PUT /api/lists/[id]/watchers/[userId], DELETE /api/lists/[id]/watchers/[userId], GET /api/lists/connections, POST /api/lists/connections, DELETE /api/lists/connections/[id]. Four Lists rows stay ◐⁴ — held back as documented in new footnote 9: GET /api/lists/[id], PUT /api/lists/[id], GET /api/lists/[id]/data/[rowId], GET /api/lists/[id]/watchers (reachable via ListsService but not exercised by a tested view model this wave). GET /api/lists and POST /api/lists likewise stay ◐⁴ for this wave — their App-layer consumers (OwnedListsViewModel.initialLoad / loadMore, NewListViewModel.submit + ListDetailViewModel.saveToMyLists) exercise the request path but the M3 polish slice will pin the full happy + invalid + failure + empty/boundary quartets at the view-model layer before they flip. The three public-Lists rows (GET /api/users/[username]/lists*) were already ☑ from Wave 2. Math: Implemented 92 of 98 (unchanged); Tested fully 20 → 35 of 98 (+15); Tested partial 71 → 56 of 98 (−15); Untested 7 of 98 (unchanged). Footnote 9 added. No other footnotes touched.

  • 2026-06-22 — Wave 3 update (M2 posting consumed end-to-end). Decision 0003-kit-import-policy recorded; InterlinedDomain.MessagesService gained the M2 write surface (create, reply, repost, update(messageId:…), delete(messageId:), dig(messageId:), undig(messageId:)) per commit c07ac8a; App-layer Composer / inline-reply / optimistic-dig / repost / edit / delete UI landed in the follow-on commit (InterlinedListTests 44/44 passing). Per the Wave 1 protocol in footnote 4, every M2-consumed row that was partial (◐⁴) after Wave 2 is now fully tested (☑) end-to-end (Kit builder → Domain service → App view-model). Four rows flipped ◐⁴ → ☑: PUT /api/messages/[id], DELETE /api/messages/[id], POST /api/messages/[id]/dig, DELETE /api/messages/[id]/dig. POST /api/messages was already ☑ from Wave 1 (cross-post-fields builder coverage) and is re-exercised this wave by all three App-layer entry points (create, reply, repost) — row state unchanged. GET /api/user was already ☑ from Wave 2 and is additionally consumed this wave by the App-layer CurrentUserStore for ownership gating — row state unchanged. The cross-post / scheduled / media request fields on POST /api/messages remain M6 per footnote 2. Implemented: 92 of 98 (unchanged). Tested: 20 of 98 fully (☑), 71 of 98 partial (◐⁴), 6 untested ☐ plus 1 untested-with-context ☐⁶. No new footnotes added.

  • 2026-06-21 — Wave 2 update (M1 read-only core consumed). Domain (InterlinedDomain) services + Persistence (InterlinedPersistence) SwiftData cache + App-layer Timeline / Lists / Profile UI landed for PLAN.md §6 M1. Per the Wave 1 protocol in footnote 4, every M1-consumed row was promoted from partial (◐⁴) to full (☑) at the domain-service layer. Ten rows flipped: GET /api/messages, GET /api/messages/[id]/replies, GET /api/users/[username]/lists, GET /api/users/[username]/lists/[id], GET /api/users/[username]/lists/[id]/data, GET /api/follow/[userId]/status, GET /api/follow/[userId]/followers, GET /api/follow/[userId]/following, GET /api/follow/[userId]/counts, and GET /api/user/[username]/messages. (GET /api/messages/[id] was already ☑ from Wave 1 and is not in the flip count.) Implemented: 92 of 98 (unchanged). Tested: 16 of 98 fully (☑), 75 of 98 partial (◐⁴), 6 untested ☐ plus 1 untested-with-context ☐⁶. No new footnotes added.

  • 2026-06-21 — Public profile gap recorded. 2026-06-21 kit-gap spike confirmed GET /api/users/[username] (and every reasonable variation) does not exist on the live API. Footnote 8 added; GET /api/user/[username]/messages row annotated with footnote 8 to mark its role as the M1 profile fallback carrier per decision 0002-public-profile-fallback. No row added to the matrix; no Implemented / Tested counts change.

  • 2026-06-18 — Wave 1 update. InterlinedKit endpoint groups (Auth additive, User, Messages, Lists, Documents & Sync, Follow, Organizations, Notifications, Exports) merged in commits 86eea76, a1e6d1c, 6ed194a. Implemented: 92 of 98 (the 6 unimplemented rows are POST /api/auth/login ⁵, the four OAuth authorize endpoints, and GET /api/auth/linkedin/status — all M6/M7). Tested: 6 of 98 fully (☑), 85 of 98 partial (◐⁴), 6 untested ☐ plus 1 untested-with-context ☐⁶. Footnote 1 resolved (planned-service column matches code). Footnote 7 added: GET /api/user/organizations belongs to the User namespace, not Organizations. Footnotes 5 (login deferred) and 6 (register lacks dedicated stubbed unit tests) added.