docs: rewrite CLAUDE.md with architecture rules and current project state
Some checks failed
CI / Build · macos-14 · Debug (push) Has been cancelled
CI / Build · macos-15 · Debug (push) Has been cancelled
CI / Build · macos-14 · Release (push) Has been cancelled
CI / Build · macos-15 · Release (push) Has been cancelled

Establishes the core contract: RSS → backend cache → API → frontend.
Frontend must never fetch RSS. Updates project structure (all views now
built), pill→tag mapping, key decisions, and adds reporting requirement.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Robin Kutesa
2026-06-22 02:36:19 +03:00
parent 595f21fb6c
commit a476e426f3

188
CLAUDE.md
View File

@@ -1,53 +1,153 @@
# Jarvis — Claude Code Handoff
# Jarvis iOS — Claude Code
## What this is
Jarvis is an iOS app (SwiftUI + SwiftData) that connects to a self-hosted RSS
correlation platform. It displays news stories ranked by signal score, caches
full article content for offline reading, and receives live updates over WebSocket.
Jarvis is an iOS app (SwiftUI, SwiftData, iOS 17+) that displays ranked news stories
served from a self-hosted backend. The backend ingests RSS, clusters articles into
stories, ranks them by signal score, and serves cached results via REST + WebSocket.
## Architecture
- REST on launch → WebSocket for live updates
- Server computes signal scores — app only renders them
- Full offline support via SwiftData cache
- No auth — local network / self-hosted only
The app is a **cached news intelligence client**. It is not an RSS reader.
## Files produced so far
- `API/contract.md` — full REST + WebSocket API spec
- `Jarvis/Models/Models.swift` — all Codable structs + SwiftData models
- `Jarvis/Networking/APIClient.swift` — REST layer (actor-based)
- `Jarvis/Networking/WebSocketManager.swift` — WS connection + reconnect backoff
- `Jarvis/Store/StoryStore.swift` — central ObservableObject, drives all views
- `Jarvis/Store/ServerSettings.swift` — persists server host to UserDefaults
- `Jarvis/JarvisApp.swift` — app entry point, SwiftData container
- `Jarvis/Views/RootTabView.swift` — tab navigation
- `Jarvis/Views/Onboarding/OnboardingView.swift` — server setup screen
---
## Views still needed
- `SignalFeedView` — home screen, story list sorted by signal score
- `StoryDetailView` — cluster detail, timeline, consensus/conflict
- `ArticleReaderView` — full article, cached badge, more from cluster
- `FeedManagerView` — feed list, health states, add/delete
- `LatestView`, `SavedView`, `SearchView` — secondary tabs
## Architecture rule
```
RSS feeds → backend ingestion → cache/database → API → frontend
```
**The frontend must never fetch RSS feeds directly.**
**The frontend must never parse, deduplicate, or rank articles.**
**The frontend consumes cached backend data only.**
---
## Backend responsibilities (not the app's job)
- RSS fetching, validation, timeout handling
- Feed health checks and dead-feed detection
- Article normalization and deduplication
- Story clustering and ranking
- Freshness, interestingness, and signal scoring
- API response caching and last-known-good fallback
The app trusts these to be done. It does not replicate them.
---
## Frontend responsibilities
- Load cached stories immediately on open (SwiftData)
- Display stories with freshness indicators and source attribution
- Show connection/refresh state without blocking the UI
- Show stale-cache label when backend data is old
- Paginate via cursor (`nextCursor`)
- Never show an empty state when the cache has data
**Performance targets:**
- App opens with cached news: instant
- Home feed API response: < 1 second
- Refresh response: < 2 seconds
- Story open: < 500ms
- Background refresh does not block UI
If any of these fail, treat it as a bug.
---
## Frontend validation checklist
Before marking any feed/story feature complete, verify:
- [ ] Cached news loads immediately without waiting for RSS
- [ ] Refresh does not freeze the UI
- [ ] Empty state does not appear when cache has data
- [ ] Stale cache is labeled (age visible)
- [ ] Fresh stories rise above old ones
- [ ] Categories are balanced across the feed
- [ ] Source attribution is correct and links work
- [ ] Offline state shows last-known-good content
---
## Project structure
```
Jarvis/
Models/Models.swift — Codable structs + SwiftData models
Networking/APIClient.swift — REST layer (actor-based)
Networking/WebSocketManager.swift — WS + reconnect backoff
Store/StoryStore.swift — central ObservableObject, drives all views
Store/StoryStore+Supplement.swift — background section supplement fetches
Store/ServerSettings.swift — persists host to UserDefaults
Store/CacheMaintenance.swift — SwiftData eviction
Views/
Home/SignalFeedView.swift — main feed (All digest, pill pills, sub-sections)
Home/StoryRowView.swift — story card
Reader/ArticleReaderView.swift — full article, cached badge
Story/StoryDetailView.swift — cluster detail + timeline
Feeds/FeedManagerView.swift — feed list + health states
Feeds/AddFeedSheet.swift
Settings/SettingsView.swift
Settings/NotificationsView.swift
Shared/Theme.swift — StoryPill enum, NewsSection, design tokens
Shared/SignalStripe.swift
Shared/SourceChips.swift
Shared/CachedBadge.swift
Shared/ConnectionBanner.swift
Onboarding/OnboardingView.swift
JarvisApp.swift
API/contract.md — REST + WebSocket API spec
```
---
## Design tokens
- Primary accent: `KisaniOrange` = #FF5C00 (add to Assets.xcassets)
- Background: pure black #000000
- Surface: #111111, #1A1A1A
- All views: `.preferredColorScheme(.dark)`
## Key decisions already made
- Story = user-facing product object (never expose Cluster to UI)
- Pipeline: Article → Cluster → Story
- Signal score is server-computed, breakdown exposed as `scoreBreakdown`
- WebSocket pushes lightweight events (storyId + score only), app refetches full object via REST
- WS reconnect: exponential backoff 1s → 2s → 4s → max 60s
- Pagination: cursor-based via `nextCursor`
- Accent: `KisaniOrange` = `#FF5C00`
- Background: `#000000` (pure black)
- Surface: `#111111` / `#1A1A1A`
- All views: `.preferredColorScheme(.dark)` unless user overrides in settings
## To start in Claude Code
```bash
cd jarvis
# Create new Xcode project named Jarvis, iOS target
# Drag all .swift files into the project
# Add KisaniOrange (#FF5C00) to Assets.xcassets as a Color Set
# Set minimum deployment target to iOS 17 (SwiftData requirement)
```
---
## Key decisions
- `Story` is the user-facing object. Never expose `Cluster` to UI.
- Pipeline: `Article → Cluster → Story`
- Signal score is server-computed; `scoreBreakdown` is exposed for display.
- WebSocket pushes `{storyId, score}` only; app re-fetches the full object via REST.
- WS reconnect: exponential backoff 1s 2s 4s max 60s.
- Pagination: cursor-based via `nextCursor`.
- `StoryStore.setPill()` resets `lastSyncedAt` to bypass the 30s rate-limit on filter switch.
- `sectionSupplement` fetches populate sparse regional sections in the All digest independently.
---
## Pill → tag mapping
| Pill | Backend tags queried |
|---|---|
| All | (no filter, full ranked feed) |
| F1 | `formula-1` |
| Sport | `sports`, `esports` |
| Tech | `technology`, `artificial-intelligence`, `machine-learning`, `cloud-computing`, `homelab`, `cybersecurity`, `security`, `programming-and-software-development`, `privacy-and-data-protection`, `web-design-and-ui-ux`, `wordpress-and-web-development`, `robotics` |
| East Africa | `east-africa`, `uganda` (Uganda sub-section pinned first) |
| Southern Africa | `south-africa`, `namibia`, `botswana`, `zimbabwe`, `zambia`, `mozambique`, `southern-africa` |
| Canada | `canada` |
| US | `united-states` |
---
## Reporting requirement
At the end of any significant work session, generate a report covering:
1. **Executive summary** is the app usable, fast, serving cached news?
2. **Issues found** backend, frontend, cache, feed, ranking, data quality
3. **Fixes made** file, function, reason, result
4. **Measurements** before/after for response times, article counts, dead feeds
5. **Remaining risks** nothing hidden
6. **Next actions** highest-value improvements
7. **Status** one of: `NOT READY` / `PARTIALLY READY` / `READY FOR TESTING` / `READY FOR RELEASE`
Never claim READY FOR RELEASE without evidence from logs, API responses, or timing measurements.