Skip to content

Latest work for the interface match.#1

Merged
Adron merged 16 commits into
mainfrom
dev
Jun 24, 2026
Merged

Latest work for the interface match.#1
Adron merged 16 commits into
mainfrom
dev

Conversation

@Adron

@Adron Adron commented Jun 22, 2026

Copy link
Copy Markdown
Member

No description provided.

Adron and others added 16 commits June 22, 2026 08:44
…FollowCounts domain model

Decision 0003 (docs/decisions/0003-kit-import-policy.md): App-layer files
must import InterlinedKit explicitly; InterlinedDomain re-exports nothing.
Resolves Wave 2 deviation #1 by introducing the missing FollowCounts
domain model and removing the kit import from the three Social views.

InterlinedDomain — M2 write surface on MessagesService:
- create(body:parentId:tags:visibility:pushedMessageId:)
- reply(to:body:tags:visibility:)
- repost(_:commentary:visibility:)
- update(messageId:body:tags:visibility:)
- delete(messageId:)
- dig(messageId:) / undig(messageId:)
All wrap the existing Wave 1 InterlinedKit.Messages builders. Cross-post /
scheduling / media fields stay deferred to M6 per the doc TODOs.

MessageStore gains a remove(id:) method with a no-op default so the
out-of-package SwiftDataMessageStore conformance compiles unchanged;
InMemoryMessageStore implements concrete eviction across by-id and
timeline indexes so delete(messageId:) leaves no tombstoned cache.

Tests: InterlinedDomain 76 → 99 (BDD-named quartet — happy/invalid/
failure/empty — for each new write method, plus the FollowCounts mapper
and the new InMemoryMessageStore.remove). InterlinedPersistence 13/13
unchanged. InterlinedKit 174/174 unchanged (no kit sources touched).
xcodebuild -scheme InterlinedList -destination 'platform=macOS' build
succeeds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- README.md with CI badges (main + dev), Swift/macOS/Xcode, repo-status
- CI workflow: app job now runs xcodebuild test + uploads unsigned .app
  artifact (14-day retention)
- Contract-tests workflow: separate from CI, manual + nightly schedule,
  gated on INTERLINEDLIST_EMAIL / INTERLINEDLIST_PASSWORD secrets
- Dependabot for GitHub Actions (no SPM ecosystem yet)
- PR template + bug / feature issue templates + contact links
- API-backend-prompts-to-build.md: P1/P2/P3 asks for the InterlinedList
  backend team, derived from spike + decisions (public profile,
  watcher roles, user lookup, cross-post envelope, rate limits, etc.)
- NEXT-WORK.md: NW-1 (watcher invite flow) parked pending API lookup

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
+ InterlinedPersistence SwiftData lists cache

InterlinedDomain
- Models: OwnedList, OwnedListsPage, ListSchema, SchemaField,
  SchemaFieldType {text,number,boolean,date,url,email}, ListConnection,
  ListWatcher, WatcherStatus, WatcherRole {owner,editor,viewer,.other},
  GitHubListSource, OwnedListMappers
- Schema/SchemaDSL.swift: parse/serialize with typed SchemaDSLError;
  property-style tests (round-trip, every type, whitespace,
  duplicate/missing, trailing comma)
- ListsService write surface: myLists / detail / create / update /
  delete / schema / updateSchema / refresh / row CRUD / watchers /
  connections — every write routed through requireListManagement()
  consulting EntitlementsService.canManageLists, throwing
  ListsError.subscriberRequired before any HTTP call (defensive
  gating; permissive default holds through M6)
- EntitlementsService gains canManageLists +
  init(customerStatus:canManageLists:) test seam
- ListsStore cache port

InterlinedPersistence
- @model records: ListRecord, ListsPageRecord, ListSchemaRecord,
  SchemaFieldRecord, ListRowRecord, ListConnectionRecord,
  ListWatcherRecord
- RowDataCodec: JSON blob codec for dynamic [String: ListCellValue]
  rows (keeps persistence kit-independent)
- SwiftDataListsStore actor: inMemory() + onDisk(at:) factories,
  cascading removeList (rows/schema/watchers/connections/page index)
- NullListsStore no-op for hostile boot conditions

Tests: +82 domain (181 passing), +17 persistence (30 passing); kit
unchanged at 174. xcodebuild build green. Decision 0003 holds.

21 Lists endpoint rows now consumed at the domain-service layer;
flip to ☑ pending App-layer consumption in Wave 4.3 per the Wave 1
deviation-6 rule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Views, tests, sidebar router, and menu commands closing out the M3 Lists
App-layer slice (view models + ListsEventBus + AppEnvironment wiring
landed in 461e7df / 155c955).

Views
- OwnedListsRootView — NavigationSplitView, sidebar disclosure tree
  honoring parentID, toolbar (New List, Refresh, Edit Schema, Share,
  Connections), context-menu delete with .confirmationDialog
- NewListSheetView — title/description/schema-DSL/parent/visibility +
  GitHub-source fields (surface-only pending API-backend-prompts 2.3
  write-side companion)
- SchemaEditorView — per-field form builder: name/type/nullable,
  .onMove reorder, validation, read-only for non-owners
- ListRowsView + RowInspectorView — typed cells per SchemaFieldType,
  cards/list toggle, optimistic CRUD with snapshot rollback
- WatchersView — role editor only (NW-1 infobox), optimistic role
  change and remove
- ListConnectionsView — SwiftUI Canvas + drag-to-move, drag-to-add-edge,
  tap-to-remove-edge. Deterministic radial v1 layout with TODO(M3.x)
  marker for force-directed upgrade. No AppKit.
- ListsSidebarRouter — ownership gating: signed-in → OwnedListsRootView,
  signed-out → M1 ListsBrowserView (preserved)

Menu + navigation
- ListMenuCommands — Lists menu, "New List" on ⇧⌘N (avoids ⌘N
  collision with ComposeCommands)
- MainWindowView — .lists routes through ListsSidebarRouter
- ListDetailView/VM — "Save to my lists" hook on public-list detail
  (metadata-only v1 pending API-backend-prompts 2.3a clone endpoint)

Tests (+62 → 106 total App tests passing)
- StubListsService test double
- OwnedListsViewModelTests (14), NewListViewModelTests (4),
  SchemaEditorViewModelTests (10), ListRowsViewModelTests (14),
  WatchersViewModelTests (9), ListConnectionsViewModelTests (11)

Gate
- xcodebuild test → 106/106 passing
- swift test (Kit/Domain/Persistence) → 174/181/30 unchanged
- grep "import InterlinedKit" App/Features App/Navigation
  App/MenuCommands → empty (decision 0003 holds)
- grep "import AppKit" App/ → empty (SwiftUI-only constraint holds)

Backend prompts updated (API-backend-prompts-to-build.md)
- P2 item 2.3a: public list clone endpoint (unblocks full "Save to my
  lists" beyond metadata-only)
- Companion to item 2.3: POST /api/lists needs gitHubSource block on
  create so the New List sheet's GitHub fields wire through

Known follow-ups in agent report
- Dynamic-column SwiftUI Table deferred (needs macOS 14.4 deployment)
- Connections graph radial v1; force-directed upgrade flagged
- In-place rename UX held back to polish slice

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…status

api-coverage.md
- 15 Lists rows ◐⁴ → ☑: DELETE /lists/[id], schema (GET/PUT), refresh,
  data (GET/POST), data/[rowId] (PATCH/DELETE), watchers/me,
  watchers/users, watchers/[userId] (PUT/DELETE), connections
  (GET/POST/DELETE)
- 4 rows held at ◐⁴ via new footnote 9: GET/PUT /lists/[id],
  GET /lists/[id]/data/[rowId], GET /lists/[id]/watchers — reachable
  via ListsService but not exercised by a tested view model this wave
- GET /lists and POST /lists held at ◐⁴ pending M3 polish-slice
  quartet pass at the view-model layer
- Math: Implemented 92/98 unchanged; Tested fully 20 → 35; Tested
  partial 71 → 56; Untested 7 unchanged
- 2026-06-23 Update history entry; no other footnotes touched

progress.md
- Full Wave 4 section appended (M3 Lists, no new decisions,
  4.1 + 4.3 summaries with commit refs, gate result, per-suite test
  table, coverage delta, 9-item deviations + follow-ups including
  the secrets-set / contract-tests-inert-until-dev→main note)

user/feature-status.md
- M2 and M3 flipped to "Shipped."
- Removed obsolete "Inline message actions arrive with M2" bullet
- Added 4 user-facing M3 limit bullets: watcher invites pending API
  lookup endpoint, deterministic radial graph layout, manual GitHub
  refresh, metadata-only Save-to-my-lists

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
+ Decision 0004 (Markdown library: Textual + macOS 15)

InterlinedDomain
- Models: Document, FolderNode, DocumentSyncEvent, DocumentChange,
  DocumentMappers
- Caching: DocumentStore port
- Imaging: ImagePrep — passthrough → 1200px downscale → lossless
  re-encode (HEIC then PNG) → JPEG quality ladder (0.9 → 0.5);
  pure CoreGraphics/ImageIO, no AppKit
- Services: DocumentsService (full CRUD + folders + image upload +
  sync passthroughs) + DocumentSyncTransport seam
- Tests: +63 (DocumentMappers 18, ImagePrep 8, DocumentsService 32,
  Fixtures expanded); 244 total passing

InterlinedPersistence
- Schema: DocumentRecord (with localEditedAt dirty bit), FolderRecord,
  OutboxEntryRecord, SyncStateRecord
- Mapping: DocumentRecordMapping, DocumentChangeCodec (JSON blob for
  outbox payloads)
- Stores: SwiftDataDocumentStore actor (in-memory + on-disk factories,
  folder-cascade delete, outbox FIFO)
- Sync: DocumentSyncEngine actor — pull delta → server-wins conflict
  resolution (local preserved as "<id>-localcopy-<UUID>") → outbox
  push → cursor advance. AsyncStream<DocumentSyncEvent> with ordered
  conflictResolved → deltaApplied → pushed events; bufferingNewest(64)
- Package.swift: added InterlinedKit dep so the engine can populate
  DocumentsError.syncFailed(underlying: APIError)
- Tests: +40 incl. 50-trial randomized soak in
  DocumentSyncEngineTests; 70 total passing

Decision 0004 (docs/decisions/)
- Markdown library: gonzalezreal/textual chosen as the App target's
  first third-party SPM dependency
- Disqualifications: MarkdownUI (maintenance mode), MarkdownView
  (transitive Highlightr import AppKit), Splash (stale), swift-markdown
  (parser only, no renderer)
- Required corollary: bump macOS deployment 14 → 15 (Textual platform
  floor). Sequoia is 2 years old at this point; aligns with SwiftUI-only
  preference recorded in memory
- Wave 5.3 owns the implementation: bump pbxproj + three Package.swift
  manifests + add Textual SPM dep + update README badges + feature-status

Gate
- xcodebuild test → 106/106 AppTests passing (TEST SUCCEEDED)
- swift test Kit/Domain/Persistence → 174/244/70
- App-target grep "import InterlinedKit" → only doc comments (3 false
  positives in App/Features/Social affirming decision 0003); zero real
- grep "import AppKit" in Packages/ → empty

14 Documents & Sync endpoint rows now consumed end-to-end via Domain;
ready for Wave 4-style flip in the docs gate after Wave 5.3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Infrastructure (Decision 0004)
- macOS deployment target 14 → 15 across all three Package.swift
  manifests, all 6 pbxproj build configurations, and Info.plist
  (LSMinimumSystemVersion)
- Added gonzalezreal/textual @ 0.5.0 (.upToNextMajor) as the App
  target's first third-party SPM dep; transitive
  swiftui-math + swift-concurrency-extras (all pure SwiftUI,
  no AppKit reach)

Composition root + lifecycle
- AppEnvironment exposes documentsService, documentSyncEngine,
  documentSyncEvents stream
- KitDocumentSyncTransport wraps APIClient for the engine
- InterlinedListApp on-launch .task fires syncNow() detached after
  currentUserStore.restore() (errors swallowed)

Documents feature (App/Features/Documents/)
- DocumentsRootView — three-column NavigationSplitView with toolbar
  (New Document, Sync Now, status indicator)
- DocumentsSidebarView + FolderTreeViewModel — disclosure tree,
  create/rename/delete folders with optimistic rollback
- DocumentsListView + DocumentsListViewModel — list per folder,
  CRUD + deltaApplied event handling, pagination heuristic
- DocumentEditorView + DocumentEditorViewModel — vanilla SwiftUI
  TextEditor for editing, Textual.StructuredText(markdown:) for
  preview, 1.5s debounced auto-save (injectable), drag-and-drop
  + .fileImporter image upload routed through DocumentsService →
  ImagePrep
- ConflictBannerView — banner surfaces conflictResolved events
  matching the open document; preserved-as copy reachable via
  "Open local copy" action
- SyncStatusView + SyncStatusViewModel — Idle/Syncing/LastSynced/
  Failed state machine, manual sync button

Menu commands
- DocumentsMenuCommands (⌥⌘N New Document, ⌥⌘S Sync Now) posts
  NotificationCenter events the root view subscribes to

Navigation
- MainWindowView .documents now routes to DocumentsRootView

Docs
- README.md: macOS badge 14+ → 15+; Building requirements bumped;
  M3/M4 milestone rows flipped to Shipped (the user previously
  bumped M3 in the table; this commit picks up M4)
- docs/user/feature-status.md: M4 → Shipped; three new "Limits"
  bullets (manual + on-app-open sync, image-resize behavior,
  conflict banner UX)

Gate
- xcodebuild test → 150/150 passing (106 → 150, +44)
- swift test Kit/Domain/Persistence → 174/244/70 unchanged
- xcodebuild build → SUCCEEDED on macOS 15 deployment target
- xcodebuild -resolvePackageDependencies → textual 0.5.0 resolved
- All 4 verification greps empty (AppKit, decision 0003, macOS 14
  in pbxproj, macOS 14 in Package.swift)

Tests (+44)
- FolderTreeViewModelTests (11), DocumentsListViewModelTests (12),
  DocumentEditorViewModelTests (10), SyncStatusViewModelTests (5),
  ConflictBannerViewModelTests (6)
- StubDocumentsService support double

14 Documents & Sync endpoint rows now consumed end-to-end via
App view models; ready for the Phase 5 docs gate.

Known follow-ups (agent report)
- Markdown toolbar inserts at end-of-buffer not cursor (TextSelection
  is macOS 26+; SwiftUI TextEditor on macOS 15 doesn't expose it)
- Drag-drop accepts Data only; URL.self branch is a v1.x add
- "Open local copy" silently fails if preserved doc is in a
  different folder (sync engine should emit folderId — backend ask
  candidate, not added yet)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feature-status, P3.7 backend ask

api-coverage.md
- 12 Documents & Sync rows ◐⁴ → ☑: GET/POST /documents/sync,
  GET /documents, POST/PATCH/DELETE /documents/[id], image upload,
  GET/POST /documents/folders, PATCH/DELETE /documents/folders/[id],
  GET /documents/folders/[id]/documents
- 2 rows held at ◐⁴ via new footnote 10: GET /documents/[id] and
  GET /documents/folders/[id] — reachable via DocumentsService but
  the Wave 5.3 view models open documents from the list payload
  rather than re-reading by id
- Math: Implemented 92/98 unchanged; Tested fully 35 → 47 (+12);
  Tested partial 56 → 44 (-12); Untested 7 unchanged
- 2026-06-23 Wave 5 Update history entry

progress.md
- Full Wave 5 section appended in Wave 4 voice: Decision 0004
  summary, 5.1 + 5.3 slice writeups with commit refs (daf1eef +
  babb6d2), gate result, per-suite test table (174/244/70/150 =
  638 total), coverage delta, 8 deviations + follow-ups

user/feature-status.md
- Verified Wave 5.3's M4 → Shipped flip
- Added one new Limits bullet for the cross-folder "Open local
  copy" caveat (links to the engineering follow-up)

API-backend-prompts-to-build.md
- Added P3.7: sync delta needs to include folderId on changed
  documents so the macOS conflict-banner "Open local copy" action
  can succeed across folder boundaries

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Live probe on 2026-06-24 against interlinedlist.com pinned the actual
shape of every follow-list endpoint. The Wave 1 kit had four typed-
wrong endpoints that worked only because synthetic tests fed the wrong
shape too — real API calls would have failed to decode.

InterlinedKit
- FollowUserDTO: rename avatarUrl → avatar (live key); add followId,
  createdAt, status (live API includes them on follower/following rows)
- FollowMutualCountsDTO replaces FollowMutualDTO — the endpoint returns
  { mutualFollowers, mutualFollowing } counts, NOT a list of users
- FollowRequestsResponse { requests: [...] } for /api/follow/requests
  (no pagination today; downgrade ask 2.1 to a P3 follow-up)
- Follow.followers / Follow.following → Request<Paginated<FollowUserDTO>>
  with paginationKey "followers" / "following"; added limit/offset/status
  query parameters
- Follow.mutual → Request<FollowMutualCountsDTO>
- Follow.requests → Request<FollowRequestsResponse> (drops the wrong
  Paginated<FollowRequestDTO> shape)

InterlinedDomain
- UserSummary.init(from: FollowUserDTO): consume new avatar field name
- UsersPage.init(from: Paginated<FollowUserDTO>): hasMore + nextOffset
  derived from pagination block
- SocialService.followers/following: pass limit/offset query params,
  consume via sendRaw + PaginatedDecoder (same pattern as MessagesService)
- API-backend-prompts-to-build.md 2.1: marked resolved on the headline,
  downgraded the pagination-of-requests follow-up to P3

Tests
- FollowEndpointTests rewritten for new envelopes: 11 → 12 cases incl.
  pagination decode, mutual counts, requests envelope, query params
- Fixtures.swift: followUserObject updated (avatar, followId, status);
  followersEnvelope/followingEnvelope helpers added; bare-array helper
  removed
- SocialServiceTests: followers/following tests use the wrapped fixtures

Gate (run 2026-06-24)
- swift test InterlinedKit         → 177/177 (174 → 177, +3)
- swift test InterlinedDomain      → 244/244 (unchanged)
- swift test InterlinedPersistence → 70/70 (unchanged)
- xcodebuild test                  → 150/150 AppTests (unchanged)

Wave 1 deviation 5: CLOSED. Phase 6 (M5 Social) Wave 6.1 can now code
against the correct types without follow-up patches.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
+ Follow action backend ask 2.3b

InterlinedDomain
- Renamed module-marker enum InterlinedDomain → InterlinedDomain_Module
  to resolve the collision with the newly-added domain Notification type
  and Foundation.Notification. Public-API change confined to a low-
  traffic version marker; only consumer was InterlinedPersistence (also
  updated)

InterlinedPersistence
- Replaced the DomainNotificationAlias.swift workaround with the cleaner
  namespace rename above (file deleted)
- NotificationRecordMapping.swift + SwiftDataNotificationStore.swift
  now use the disambiguated InterlinedDomain_Module marker / explicit
  InterlinedDomain.Notification form
- New tests: SwiftDataNotificationStoreTests (11 cases — tray
  round-trip, mark-read flag flip, mark-all-read badge zero, every
  NotificationTarget case round-trip, clear cascade) and
  SwiftDataFollowCountsStoreTests (9 cases — follow + mutual
  combination, per-user isolation, second-write-wins, remove, clear)
- Persistence count: 70 → 90 (+20)

Domain models / services / tests (largely landed in da13846, 159f71a
interleaved commits — closure covers the namespace + remaining tests):
- Models: FollowRequest, FollowRelationship + FollowAction,
  MutualCounts, Notification, NotificationKind (8 typed cases +
  .other), NotificationTarget, NotificationMappers
- SocialService write surface: follow / unfollow / approve / reject /
  removeFollower / mutual / requests
- NotificationsService: tray / markRead / markAllRead (drops client-
  side limit since the server uses notificationTrayLimit clamped 10-40)
- Domain count: 244 → 299 (+55)

API-backend-prompts-to-build.md
- Added P2 item 2.3b: POST /api/follow/[userId] response should carry
  the resulting relationship state so the macOS client can skip the
  follow-up GET /api/follow/[userId]/status round-trip

Gate
- swift test InterlinedKit         → 177/177 unchanged
- swift test InterlinedDomain      → 299/299 passing (+55)
- swift test InterlinedPersistence → 90/90 passing (+20)
- xcodebuild test                  → 150/150 AppTests unchanged
- grep "import AppKit" Packages/{Domain,Persistence} → empty
- grep "import InterlinedKit" App/Features/Navigation/MenuCommands → empty
- Decision 0003 holds

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Adron Adron merged commit 085b91f into main Jun 24, 2026
4 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant