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 indocs/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 → session cookie | LiveSessionEstablisher (InterlinedKit/Auth) | M7 | ☑ | ☑ |
POST /api/auth/logout |
Auth | Session | AuthService (InterlinedKit/Auth) | M0 | ☑ | ◐⁴ |
POST /api/auth/register |
Auth | Public | AuthService (InterlinedKit/Auth) | M0 | ☑ | ☐⁶ |
POST /api/auth/sync-token |
Auth | Public → Bearer token | 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 | Public | 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 | Session or Bearer | UserService¹ (+ EntitlementsService reads customerStatus) |
M0 | ☑ | ☑ |
POST /api/user/update |
User | Session | UserService¹ | M7 | ☑ | ☑ |
POST /api/user/avatar/upload |
User | Session | UserService¹ | M7 | ☑ | ☑ |
POST /api/user/avatar/from-url |
User | Session | UserService¹ | M7 | ☑ | ◐⁴ |
GET /api/user/identities |
User | Session | UserService¹ | M6 | ☑ | ☑ |
GET /api/user/organizations |
User | Session | UserService¹ ⁷ | M6 | ☑ | ☑ |
POST /api/user/change-email/request |
User | Session | UserService¹ | M7 | ☑ | ☑ |
POST /api/user/delete |
User | Session | UserService¹ | M7 | ☑ | ☑ |
GET /api/messages |
Messages | Session or Bearer | MessagesService | M1 | ☑ | ☑ |
POST /api/messages |
Messages | Session or Bearer | MessagesService | M2² | ☑ | ☑ |
GET /api/messages/[id] |
Messages | Session or Bearer | MessagesService | M1 | ☑ | ☑ |
PUT /api/messages/[id] |
Messages | Session or Bearer | MessagesService | M2 | ☑ | ☑ |
DELETE /api/messages/[id] |
Messages | Session or Bearer | MessagesService | M2 | ☑ | ☑ |
GET /api/messages/scheduled |
Messages | Session or Bearer | MessagesService | M6 | ☑ | ☑ |
GET /api/messages/[id]/replies |
Messages | Session | MessagesService | M1 | ☑ | ☑ |
POST /api/messages/[id]/dig |
Messages | Session | MessagesService | M2 | ☑ | ☑ |
DELETE /api/messages/[id]/dig |
Messages | Session | MessagesService | M2 | ☑ | ☑ |
POST /api/messages/images/upload |
Messages | Session or Bearer | MessagesService | M6 | ☑ | ☑ |
POST /api/messages/videos/upload |
Messages | Session or Bearer | MessagesService | M6 | ☑ | ☑ |
GET /api/lists |
Lists | Session or Bearer | ListsService | M3 | ☑ | ◐⁴ |
POST /api/lists |
Lists | Session or Bearer | ListsService | M3 | ☑ | ◐⁴ |
GET /api/lists/[id] |
Lists | Session or Bearer | ListsService | M3 | ☑ | ◐⁴⁹ |
PUT /api/lists/[id] |
Lists | Session or Bearer | ListsService | M3 | ☑ | ◐⁴⁹ |
DELETE /api/lists/[id] |
Lists | Session or Bearer | ListsService | M3 | ☑ | ☑ |
GET /api/lists/[id]/schema |
Lists | Session or Bearer | ListsService | M3 | ☑ | ☑ |
PUT /api/lists/[id]/schema |
Lists | Session or Bearer | ListsService | M3 | ☑ | ☑ |
POST /api/lists/[id]/refresh |
Lists | Session or Bearer | ListsService | M3 | ☑ | ☑ |
GET /api/lists/[id]/data |
Lists | Session or Bearer | ListsService | M3 | ☑ | ☑ |
POST /api/lists/[id]/data |
Lists | Session or Bearer | ListsService | M3 | ☑ | ☑ |
GET /api/lists/[id]/data/[rowId] |
Lists | Session or Bearer | ListsService | M3 | ☑ | ◐⁴⁹ |
PATCH /api/lists/[id]/data/[rowId] |
Lists | Session or Bearer | ListsService | M3 | ☑ | ☑ |
DELETE /api/lists/[id]/data/[rowId] |
Lists | Session or Bearer | ListsService | M3 | ☑ | ☑ |
GET /api/lists/[id]/watchers |
Lists | Session or Bearer | ListsService | M3 | ☑ | ◐⁴⁹ |
GET /api/lists/[id]/watchers/me |
Lists | Session or Bearer | ListsService | M3 | ☑ | ☑ |
GET /api/lists/[id]/watchers/users |
Lists | Session or Bearer | ListsService | M3 | ☑ | ☑ |
PUT /api/lists/[id]/watchers/[userId] |
Lists | Session or Bearer | ListsService | M3 | ☑ | ☑ |
DELETE /api/lists/[id]/watchers/[userId] |
Lists | Session or Bearer | ListsService | M3 | ☑ | ☑ |
GET /api/users/[username]/lists |
Lists (public) | None | ListsService | M1 | ☑ | ☑ |
GET /api/users/[username]/lists/[id] |
Lists (public) | None | ListsService | M1 | ☑ | ☑ |
GET /api/users/[username]/lists/[id]/data |
Lists (public) | None | ListsService | M1 | ☑ | ☑ |
GET /api/lists/connections |
List Connections | Session or Bearer | ListsService | M3 | ☑ | ☑ |
POST /api/lists/connections |
List Connections | Session or Bearer | ListsService | M3 | ☑ | ☑ |
DELETE /api/lists/connections/[id] |
List Connections | Session or Bearer | ListsService | M3 | ☑ | ☑ |
GET /api/documents/sync |
Documents & Sync | Session or Bearer | DocumentSyncEngine (InterlinedPersistence) | M4 | ☑ | ☑ |
POST /api/documents/sync |
Documents & Sync | Session or Bearer | DocumentSyncEngine (InterlinedPersistence) | M4 | ☑ | ☑ |
GET /api/documents |
Documents & Sync | Session | DocumentsService | M4 | ☑ | ☑ |
POST /api/documents |
Documents & Sync | Session | DocumentsService | M4 | ☑ | ☑ |
GET /api/documents/[id] |
Documents & Sync | Session | DocumentsService | M4 | ☑ | ◐⁴¹⁰ |
PATCH /api/documents/[id] |
Documents & Sync | Session | DocumentsService | M4 | ☑ | ☑ |
DELETE /api/documents/[id] |
Documents & Sync | Session | DocumentsService | M4 | ☑ | ☑ |
POST /api/documents/[id]/images/upload |
Documents & Sync | Session | DocumentsService | M4 | ☑ | ☑ |
GET /api/documents/folders |
Documents & Sync | Session | DocumentsService | M4 | ☑ | ☑ |
POST /api/documents/folders |
Documents & Sync | Session | DocumentsService | M4 | ☑ | ☑ |
GET /api/documents/folders/[id] |
Documents & Sync | Session | DocumentsService | M4 | ☑ | ◐⁴¹⁰ |
PATCH /api/documents/folders/[id] |
Documents & Sync | Session | DocumentsService | M4 | ☑ | ☑ |
DELETE /api/documents/folders/[id] |
Documents & Sync | Session | DocumentsService | M4 | ☑ | ☑ |
GET /api/documents/folders/[id]/documents |
Documents & Sync | Session | DocumentsService | M4 | ☑ | ☑ |
POST /api/follow/[userId] |
Follow | Session | SocialService | M5 | ☑ | ☑ |
DELETE /api/follow/[userId] |
Follow | Session | SocialService | M5 | ☑ | ☑ |
GET /api/follow/[userId]/status |
Follow | Session | SocialService | M5 | ☑ | ☑ |
GET /api/follow/[userId]/followers |
Follow | Session | SocialService | M5 | ☑ | ☑ |
GET /api/follow/[userId]/following |
Follow | Session | SocialService | M5 | ☑ | ☑ |
GET /api/follow/[userId]/counts |
Follow | Session | SocialService | M5 | ☑ | ☑ |
GET /api/follow/[userId]/mutual |
Follow | Session | SocialService | M5 | ☑ | ☑ |
POST /api/follow/[userId]/approve |
Follow | Session | SocialService | M5 | ☑ | ☑ |
POST /api/follow/[userId]/reject |
Follow | Session | SocialService | M5 | ☑ | ☑ |
POST /api/follow/[userId]/remove |
Follow | Session | SocialService | M5 | ☑ | ◐⁴¹¹ |
GET /api/follow/requests |
Follow | Session | SocialService | M5 | ☑ | ☑ |
GET /api/organizations |
Organizations | Session | OrgService | M6 | ☑ | ◐⁴¹³ |
POST /api/organizations |
Organizations | Session | OrgService | M6 | ☑ | ☑ |
GET /api/organizations/[id] |
Organizations | Session | OrgService | M6 | ☑ | ☑ |
PATCH /api/organizations/[id] |
Organizations | Session | OrgService | M6 | ☑ | ☑ |
GET /api/organizations/[id]/members |
Organizations | Session | OrgService | M6 | ☑ | ☑ |
POST /api/organizations/[id]/members |
Organizations | Session | OrgService | M6 | ☑ | ☑ |
PUT /api/organizations/[id]/members/[userId] |
Organizations | Session | OrgService | M6 | ☑ | ☑ |
DELETE /api/organizations/[id]/members/[userId] |
Organizations | Session | OrgService | M6 | ☑ | ☑ |
GET /api/organizations/[id]/users |
Organizations | Session | OrgService | M6 | ☑ | ◐⁴¹³ |
GET /api/exports/messages |
Exports | Session | ExportsService¹ | M7 | ☑ | ☑ |
GET /api/exports/lists |
Exports | Session | ExportsService¹ | M7 | ☑ | ☑ |
GET /api/exports/list-data-rows |
Exports | Session | ExportsService¹ | M7 | ☑ | ☑ |
GET /api/exports/follows |
Exports | Session | ExportsService¹ | M7 | ☑ | ☑ |
GET /api/notifications |
Notifications | Session | NotificationsService | M5 | ☑ | ☑ |
PATCH /api/notifications/[id]/read |
Notifications | Session | NotificationsService | M5 | ☑ | ☑ |
POST /api/notifications/mark-all-read |
Notifications | Session | NotificationsService | M5 | ☑ | ☑ |
GET /api/user/[username]/messages |
Public | None | MessagesService⁸ | M1 | ☑ | ☑ |
GET /api/auth/linkedin/status |
Public | None | AuthService (OAuth flows) | M6 | ☑ | ☐¹² |
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.
- 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(seePackages/InterlinedKit/Sources/InterlinedKit/Endpoints/UserEndpoint.swift) and the Exports group asInterlinedKit.Exports(ExportsEndpoint.swift); domain-sideUserService/ExportsServicewrappers are deferred to the milestone in which the consuming UI lands (M6/M7). POST /api/messagesships 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_thenEncodesAllSetFieldsalready 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.createPostcarriesscheduledAt+ 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.- Repost (
pushedMessageId), visibility, and tag filters are request/response fields on existing rows above, not separate endpoints — they carry no row of their own. - Partial test coverage (◐). The row's request builder, DTOs, and
APIClient.sendpath 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 inAPIClientTests/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. POST /api/auth/login(cookie-session credential exchange) was deferred through Waves 1–7 (NullSessionEstablisherstub). Resolved Wave 8.1 (2026-07-03):LiveSessionEstablisher+CredentialStore+KeychainCredentialStorenow implement the lazyPOST /api/auth/loginpath;AuthService.signInpersists credentials toKeychainCredentialStoreso the establisher can re-authenticate on the next.sessioncall.LiveSessionEstablisherTestscovers the full quartet (happy 200/204, no-credentials, server 401, server 500, transport failure). Row flipped to ☑/☑; this footnote is resolved.POST /api/auth/registerships asAuthService.registerand is exercised by the liveContractTestswhenINTERLINEDLIST_EMAIL/INTERLINEDLIST_PASSWORDare present, but has no stubbed unit-test cases yet (onlysignInhas dedicated unit tests inAuthServiceTests). Tested ☐ until at least happy + invalid + failure + empty/boundary unit tests are added (likely in the onboarding-feature wave).GET /api/user/organizationslives inInterlinedKit.User.organizations()(notOrganizations.*) because the live API path is/api/user/organizations, not/api/organizations. Planned-service column corrected fromOrgServicetoUserService¹in Wave 1 to match the actual implementation.- No public profile read endpoint exists on the live API. PLAN.md §1 (Profile row) and §6 M1 ("user profiles") imply a
GET /api/users/[username]route, but the 2026-06-21 kit-gap spike confirmed every reasonable variation (/api/users/[username],/api/user/[username],/api/users/[username]/{profile,public},/api/profile/[username],/api/u/[username],/api/public/users/[username],/api/users/[username]/{followers,following}) returns 404, while the username pattern is otherwise valid (/api/users/[username]/listsand/api/user/[username]/messagesreturn 200 for the same handle). No such row appears in this matrix because the endpoint is not in the live reference. Decision0002-public-profile-fallbackrecords the M1 fallback:SocialService.profile(username:)reduces to the embedded{ id, username, displayName, avatar }author object on the first message returned byGET /api/user/[username]/messages. When the upstream endpoint lands, add the row here and check it off against the direct implementation. - 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
ListsServiceand reachable from the running app but their consuming UX was held back to a polish slice this wave:GET /api/lists/[id]andPUT /api/lists/[id](the detail-rename / single-list-refresh paths — rename UX deferred),GET /api/lists/[id]/data/[rowId](single-row hydration —RowInspectorViewreads from the already-paginatedListRowsViewModel.rowsarray), andGET /api/lists/[id]/watchers(the watcher pagination envelope —WatchersViewconsumes/usersonly 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. - M4 detail-read rows reachable but not view-model-tested this wave. Same pattern as footnote 9, applied to Documents.
GET /api/documents/[id]andGET /api/documents/folders/[id]are wired throughDocumentsService.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 fromfolder(id:)). The Wave 1 footnote-4 backfill rule still applies. - 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 fromDELETE /api/follow/[userId], which unfollows someone I follow) is wired throughSocialService.removeFollower(userId:)and reachable from the running app, but no Wave 6.3 view model exercises it through a tested path: the Followers tab inSocialRosterRootViewdisplays 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 aSocialRosterRowViewModel.removeFolloweraction behind a context menu on the Followers tab) consumes it. The Wave 1 footnote-4 backfill rule still applies. - OAuth
authorizebuilders Implemented but not end-to-end consumable — by design (Wave 7). The five M6 OAuth rows (GET /api/auth/{github,mastodon,bluesky,linkedin}/authorizeandGET /api/auth/linkedin/status) gained Kit request builders this wave (Auth.authorize(provider:link:instance:),Auth.linkedinStatus(), theOAuthProviderenum, and theLinkedInStatusResponseDTO, with 13 builder tests), so their Implemented column flips ☐ → ☑. Their Tested column stays untested (☐¹²) — not ◐⁴ — because native completion of the flow is blocked upstream (spike 0002, decision 0006): the/authorizecallback is a web URL oninterlinedlist.com(no custom scheme / universal link), the flow is cookie-bound (not Bearer), and there is no bearer…/linkendpoint a native client can complete against. The app therefore does not send these requests; it opens the…/authorize?link=trueURL in the default browser for completion on the web. The four…/authorizerows are reached only indirectly —UserService.identityLinkURL(provider:instance:)resolves theAuth.authorizebuilder to aURLthatLinkedAccountsViewhands to SwiftUI@Environment(\.openURL)— so there is no app-side send to test against;GET /api/auth/linkedin/statusis implemented but currently unconsumed. The Tested column flips to ☑ only when an upstream native-completion contract lands (custom-scheme / universal-link callback, or a bearerPOST /api/auth/{provider}/link) and the app sends/completes the flow itself — tracked inNEXT-WORK.mdNW-5 andAPI-backend-prompts-to-build.mdask 2.6. - 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 throughUserService.organizations()(GET /api/user/organizations) instead, so theOrgServicelist-all path stays unconsumed; andGET /api/organizations/[id]/users(OrgService.users(of:)) — the member roster is rendered fromGET /api/organizations/[id]/members(OrgMembersViewModel), leaving the/usersprojection unconsumed. Both rows stay ◐⁴ until a polish slice consumes them through a tested view model. The Wave 1 footnote-4 backfill rule still applies.
- 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), andGET /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 decision0002-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.
-
2026-07-03 — Wave 8 update (M7 Ship: LiveSessionEstablisher, Exports E2E, Settings/Account E2E). Wave 8.1 landed
LiveSessionEstablisher(CredentialStoreprotocol +KeychainCredentialStoreproduction +InMemoryCredentialStoretests) — the realPOST /api/auth/logincookie-session fallback that was stubbed viaNullSessionEstablishersince Wave 1.AuthService.signInnow persists credentials to Keychain so the establisher can re-authenticate lazily;AppEnvironment.live()wired with a dedicated ephemeralURLSession(isolated cookie jar).LiveSessionEstablisherTestscovers the full quartet (6 new Kit tests; InterlinedKit suite 190 → 196). Wave 8.2 addedExportViewModelTests(8 tests) +StubExportsServiceto the App test suite, exercising all four export paths end-to-end through the view model. Wave 8.3 confirmedAccountViewModelTests(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.mdprobe 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(viaExportViewModel→ExportsServicingend-to-end withExportViewModelTests),POST /api/user/avatar/upload,POST /api/user/change-email/request,POST /api/user/delete(viaAccountViewModel→UserServicingend-to-end withAccountViewModelTests). Row flipped ☐ → ☑ (Implemented + Tested):POST /api/auth/login(LiveSessionEstablisher+LiveSessionEstablisherTestsfull 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).
InterlinedDomainM6 slice (OrgServiceover the Organizations endpoints withOrganization/OrgMember/OrgUser/OrgRole/OrgsPage/OrgMembersPage/OrgMappers;UserServiceidentities()/organizations()+LinkedIdentity/IdentityProviderand the newidentityLinkURL(provider:instance:);MessagesServiceM6 write surface —createPostwith media / scheduled / cross-post,scheduledPosts(),uploadImage/uploadVideoviaImagePrep, all subscriber-gated viaEntitlementsServicewith a liveentitlementsProviderbackstop) plus theInterlinedPersistenceSwiftDataOrgStore/SwiftDataLinkedIdentityStore. App-layer M6 UI: Organizations (OrganizationsRootView+OrganizationsListViewModel/OrganizationDetailViewModel/OrgMembersViewModel,.organizationsroute flipped), the M6 composer extensions + read-onlyScheduledPostsRootView(ScheduledPostsViewModel,.scheduledroute flipped), and the browser-handoffSettingsRootView→ 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;ImagePrepexercised),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}/authorizeandGET /api/auth/linkedin/statusgained Kit builders (+13 tests) but native completion is blocked upstream (decision 0006) — the app opens…/authorize?link=truein the browser (reached indirectly viaUserService.identityLinkURL), it does not send these requests, andlinkedin/statusis unconsumed. Two org-read rows stay ◐⁴ — held back (new footnote 13):GET /api/organizations(list-all variant — UI usesUserService.organizations()instead) andGET /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 viaComposerViewModel— footnote 2 resolved),GET /api/messages/scheduled(already ☑ from Wave 1; re-consumed read-only byScheduledPostsViewModel). Math: Implemented 92 → 97 of 98 (+5 OAuth builders; onlyPOST /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-☐¹², plusPOST /api/auth/login⁵ andPOST /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).
InterlinedDomainFollow / Notifications slice (FollowMappers,FollowRelationship+FollowAction,FollowRequest,MutualCounts,Notification,NotificationKind+NotificationTarget,NotificationMappers,SocialServicewrite surface,NotificationsService) shipped across commitscae57cc(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_Modulemarker rename +SwiftDataNotificationStore/SwiftDataFollowCountsStorepersistence tests + Follow action backend ask 2.3b). App-layer Social + Notifications UI shipped in commit9a66154(FollowButton+FollowButtonViewModel,SocialRosterRootView+SocialRosterViewModelfor Followers / Following / Requests tabs,FollowRequestRowViewModelshared between tray and roster,NotificationsRootView+NotificationsListViewModel+NotificationRowView+NotificationRowCopy,NotificationsPermissionCoordinator,NotificationsUnreadBadgeCoordinator+NotificationsEventBus,ProfileHeaderView+ProfileViewModel,NotificationsMenuCommands+SocialMenuCommands, sidebar.connectionsentry, Decision-0005App/Composition/AppDelegate.swiftfor dock badge + UN delegate, andApp/Composition/FollowRelationshipReader.swiftcomposition-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 viaSocialService.removeFollowerbut 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 viaFollowRelationshipReader→FollowButtonViewModel),GET /api/follow/[userId]/followers/GET /api/follow/[userId]/following/GET /api/follow/[userId]/counts(already ☑ from Wave 2; re-consumed bySocialRosterViewModel/ProfileViewModel),GET /api/notifications?scope=tray(already ☑ from Wave 1; re-consumed byNotificationsListViewModel.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).
InterlinedDomainDocuments slice (Document,FolderNode,DocumentSyncEvent,DocumentChange,DocumentMappers,ImagePrep,DocumentsService,DocumentSyncTransport) andInterlinedPersistence(DocumentRecord,FolderRecord,OutboxEntryRecord,SyncStateRecord,SwiftDataDocumentStore,DocumentSyncEngine) shipped in commitdaf1eef; App-layer Documents UI (DocumentsRootView,DocumentsSidebarView+FolderTreeViewModel,DocumentsListView+DocumentsListViewModel,DocumentEditorView+DocumentEditorViewModel,ConflictBannerView,SyncStatusView+SyncStatusViewModel,DocumentsMenuCommands,KitDocumentSyncTransportwiring) shipped in commitbabb6d2. 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.pullDeltaviaDocumentSyncEngine.syncNowviaSyncStatusViewModel.syncNow),POST /api/documents/sync(KitDocumentSyncTransport.pushChangeviaDocumentSyncEngine.syncNowoutbox 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;ImagePrepis 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]andGET /api/documents/folders/[id]are reachable viaDocumentsService.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).
InterlinedDomainLists write surface + schema DSL +InterlinedPersistenceSwiftData lists cache shipped in commit415c5c2; App-layer Lists UI (owned-lists root, schema editor, rows table, row inspector, watchers, connections graph) shipped in commits461e7df+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 viaListsServicebut not exercised by a tested view model this wave).GET /api/listsandPOST /api/listslikewise 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-policyrecorded;InterlinedDomain.MessagesServicegained the M2 write surface (create,reply,repost,update(messageId:…),delete(messageId:),dig(messageId:),undig(messageId:)) per commitc07ac8a; App-layer Composer / inline-reply / optimistic-dig / repost / edit / delete UI landed in the follow-on commit (InterlinedListTests44/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/messageswas 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/userwas already ☑ from Wave 2 and is additionally consumed this wave by the App-layerCurrentUserStorefor ownership gating — row state unchanged. The cross-post / scheduled / media request fields onPOST /api/messagesremain 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, andGET /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]/messagesrow annotated with footnote 8 to mark its role as the M1 profile fallback carrier per decision0002-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 arePOST /api/auth/login⁵, the four OAuthauthorizeendpoints, andGET /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/organizationsbelongs to theUsernamespace, notOrganizations. Footnotes 5 (login deferred) and 6 (register lacks dedicated stubbed unit tests) added.