rename: Jarvis -> Jervis target, scheme, and folder throughout
The bundle-ID and display-name renames weren't enough — the actual Xcode target/product name was still "Jarvis", which drives CFBundleName, Xcode's Organizer archive list, the .xcodeproj filename, and the scheme name. Renamed all the way through: - project.yml: top-level name, target key, PRODUCT_NAME, source/info paths - Jarvis/ -> Jervis/ (source folder, git-tracked as renames, no content diffs on the moved files) - .gitignore, CI workflows, README, CONTRIBUTING: Jarvis.xcodeproj / scheme Jarvis -> Jervis.xcodeproj / scheme Jervis Verified: xcodebuild -scheme Jervis succeeds, produces Jervis.app, CFBundleName/CFBundleExecutable/CFBundleDisplayName all read "Jervis". CFBundleIdentifier intentionally stays com.kisani.jarvis (per prior decision — bundle ID isn't user-facing anywhere including Organizer). Swift type names (JarvisApp, JarvisWordmark, etc.) and the Gitea repo name/URL are unchanged — pure internal source identifiers, not user or developer-facing product identity. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,272 +0,0 @@
|
||||
// Models.swift
|
||||
// Jarvis — all Codable structs matching the API contract
|
||||
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
// MARK: - Signal Score
|
||||
|
||||
struct SignalScore: Codable, Hashable {
|
||||
let total: Int
|
||||
let sourceAuthority: Int
|
||||
let freshness: Int
|
||||
let localRelevance: Int
|
||||
let crossSourceConfirmation: Int
|
||||
let topicImportance: Int
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case total = "signalScore"
|
||||
case sourceAuthority, freshness, localRelevance
|
||||
case crossSourceConfirmation, topicImportance
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Source
|
||||
|
||||
struct StorySource: Codable, Hashable, Identifiable {
|
||||
let id: String
|
||||
let name: String
|
||||
let url: String
|
||||
let publishedAt: Date
|
||||
let isBreaking: Bool
|
||||
}
|
||||
|
||||
// MARK: - Timeline Entry
|
||||
|
||||
struct TimelineEntry: Codable, Hashable, Identifiable {
|
||||
let articleId: String
|
||||
let source: String
|
||||
let headline: String
|
||||
let publishedAt: Date
|
||||
let isBreaking: Bool
|
||||
|
||||
var id: String { articleId }
|
||||
}
|
||||
|
||||
// MARK: - Story Summary (list view)
|
||||
|
||||
struct StorySummary: Codable, Identifiable, Hashable {
|
||||
let id: String
|
||||
let headline: String
|
||||
let summary: String
|
||||
let topic: String
|
||||
/// Canonical category slugs from the backend. Optional so decoding tolerates
|
||||
/// the transition while the backend rolls out `tags[]`; falls back to `topic`.
|
||||
let tags: [String]?
|
||||
let signalScore: Int
|
||||
let scoreBreakdown: ScoreBreakdown
|
||||
let sourceCount: Int
|
||||
let sources: [StorySource]
|
||||
let consensus: String?
|
||||
let conflict: String?
|
||||
let updatedAt: Date
|
||||
let createdAt: Date
|
||||
/// When the cluster first appeared. `updatedAt` bumps on every article the
|
||||
/// backend attaches to the cluster (even a loosely-related one), so it can
|
||||
/// read "1 hr ago" on stories that are actually weeks old — `firstSeenAt`
|
||||
/// is the honest age. Optional so decoding tolerates older API responses.
|
||||
let firstSeenAt: Date?
|
||||
}
|
||||
|
||||
struct ScoreBreakdown: Codable, Hashable {
|
||||
let sourceAuthority: Int
|
||||
let freshness: Int
|
||||
let localRelevance: Int
|
||||
let crossSourceConfirmation: Int
|
||||
let topicImportance: Int
|
||||
}
|
||||
|
||||
// MARK: - Story Detail (full, with timeline)
|
||||
|
||||
struct StoryDetail: Codable, Identifiable {
|
||||
let id: String
|
||||
let headline: String
|
||||
let summary: String
|
||||
let topic: String
|
||||
let signalScore: Int
|
||||
let scoreBreakdown: ScoreBreakdown
|
||||
let sourceCount: Int
|
||||
let consensus: String?
|
||||
let conflict: String?
|
||||
let timeline: [TimelineEntry]
|
||||
let updatedAt: Date
|
||||
let createdAt: Date
|
||||
}
|
||||
|
||||
// MARK: - Article (full content, for offline cache)
|
||||
|
||||
struct Article: Codable, Identifiable {
|
||||
let id: String
|
||||
let storyId: String
|
||||
let source: String
|
||||
let sourceUrl: String
|
||||
let headline: String
|
||||
let body: String
|
||||
let imageUrl: String?
|
||||
let author: String?
|
||||
let publishedAt: Date
|
||||
}
|
||||
|
||||
// MARK: - Feed
|
||||
|
||||
struct Feed: Codable, Identifiable {
|
||||
let id: String
|
||||
let name: String
|
||||
let url: String
|
||||
let health: FeedHealth
|
||||
let pollIntervalSeconds: Int
|
||||
let failureCount: Int
|
||||
let lastFetchedAt: Date?
|
||||
let articleCountToday: Int
|
||||
}
|
||||
|
||||
enum FeedHealth: String, Codable {
|
||||
case active, failing, dead
|
||||
}
|
||||
|
||||
// MARK: - Server Health
|
||||
|
||||
struct ServerHealth: Codable {
|
||||
let status: String
|
||||
let version: String
|
||||
let storiesCount: Int
|
||||
let feedsCount: Int
|
||||
let uptime: Int
|
||||
}
|
||||
|
||||
// MARK: - Paginated Response
|
||||
|
||||
struct PaginatedStories: Codable {
|
||||
let data: [StorySummary]
|
||||
let nextCursor: String?
|
||||
let hasMore: Bool
|
||||
let total: Int
|
||||
let unreadCount: Int?
|
||||
let readSuppressedCount: Int?
|
||||
let lastReadSyncAt: Date?
|
||||
let activePill: String?
|
||||
}
|
||||
|
||||
// MARK: - WebSocket Events
|
||||
|
||||
enum WSEventType: String, Codable {
|
||||
case storyUpdated = "story.updated"
|
||||
case storyCreated = "story.created"
|
||||
case storyStale = "story.stale"
|
||||
case feedHealth = "feed.health"
|
||||
case ping, pong
|
||||
}
|
||||
|
||||
struct WSEvent: Codable {
|
||||
let type: WSEventType
|
||||
let storyId: String?
|
||||
let feedId: String?
|
||||
let signalScore: Int?
|
||||
let sourceCount: Int?
|
||||
let headline: String?
|
||||
let topic: String?
|
||||
let health: FeedHealth?
|
||||
let failureCount: Int?
|
||||
let updatedAt: Date?
|
||||
let createdAt: Date?
|
||||
}
|
||||
|
||||
// MARK: - SwiftData cached models
|
||||
|
||||
@Model
|
||||
final class CachedStory {
|
||||
@Attribute(.unique) var id: String
|
||||
var headline: String
|
||||
var summary: String
|
||||
var topic: String
|
||||
/// Canonical backend tags, so offline pill filtering stays accurate.
|
||||
/// Defaulted for lightweight migration of existing cached rows.
|
||||
var tags: [String] = []
|
||||
var signalScore: Int
|
||||
var sourceCount: Int
|
||||
var consensus: String?
|
||||
var conflict: String?
|
||||
var updatedAt: Date
|
||||
/// Original publish timestamp. Defaults to updatedAt for rows cached before
|
||||
/// this field was added — SwiftData handles the automatic migration.
|
||||
var createdAt: Date = Date()
|
||||
/// When the cluster first appeared — the honest age, unlike `updatedAt` which
|
||||
/// bumps whenever the backend attaches another article. Defaults to `updatedAt`
|
||||
/// for rows cached before this field was added.
|
||||
var firstSeenAt: Date = Date()
|
||||
var cachedAt: Date
|
||||
|
||||
init(from summary: StorySummary) {
|
||||
self.id = summary.id
|
||||
self.headline = summary.headline
|
||||
self.summary = summary.summary
|
||||
self.topic = summary.topic
|
||||
self.tags = summary.tags ?? []
|
||||
self.signalScore = summary.signalScore
|
||||
self.sourceCount = summary.sourceCount
|
||||
self.consensus = summary.consensus
|
||||
self.conflict = summary.conflict
|
||||
self.updatedAt = summary.updatedAt
|
||||
self.createdAt = summary.createdAt
|
||||
self.firstSeenAt = summary.firstSeenAt ?? summary.updatedAt
|
||||
self.cachedAt = Date()
|
||||
}
|
||||
}
|
||||
|
||||
@Model
|
||||
final class ReadStory {
|
||||
@Attribute(.unique) var id: String
|
||||
var readAt: Date
|
||||
|
||||
init(id: String) {
|
||||
self.id = id
|
||||
self.readAt = Date()
|
||||
}
|
||||
}
|
||||
|
||||
@Model
|
||||
final class SavedStory {
|
||||
@Attribute(.unique) var id: String
|
||||
var savedAt: Date
|
||||
|
||||
init(id: String) {
|
||||
self.id = id
|
||||
self.savedAt = Date()
|
||||
}
|
||||
}
|
||||
|
||||
@Model
|
||||
final class SeenStory {
|
||||
@Attribute(.unique) var id: String
|
||||
var seenAt: Date
|
||||
|
||||
init(id: String) {
|
||||
self.id = id
|
||||
self.seenAt = Date()
|
||||
}
|
||||
}
|
||||
|
||||
@Model
|
||||
final class CachedArticle {
|
||||
@Attribute(.unique) var id: String
|
||||
var storyId: String
|
||||
var source: String
|
||||
var headline: String
|
||||
var body: String
|
||||
var imageUrl: String?
|
||||
var author: String?
|
||||
var publishedAt: Date
|
||||
var cachedAt: Date
|
||||
|
||||
init(from article: Article) {
|
||||
self.id = article.id
|
||||
self.storyId = article.storyId
|
||||
self.source = article.source
|
||||
self.headline = article.headline
|
||||
self.body = article.body
|
||||
self.imageUrl = article.imageUrl
|
||||
self.author = article.author
|
||||
self.publishedAt = article.publishedAt
|
||||
self.cachedAt = Date()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user