Skip to content

Latest commit

 

History

History
91 lines (69 loc) · 8.33 KB

File metadata and controls

91 lines (69 loc) · 8.33 KB

Architecture

A focused tour of how Orbion is wired. Start here before reading code.

Layers

Views ──► AI generators ──► Scoring ──► Sync ──► iTunes clients ──► Apple
                              │            │
                              └─► Models (SwiftData)

Everything below the Views layer is plain Swift modules. SwiftUI surfaces read from SwiftData and trigger sync; nothing reaches outward from a model.

Foundations

  • Orbion/Models/ - One file per SwiftData @Model. TrackedApp, TrackedKeyword, KeywordRanking, KeywordPopularity, SerpSnapshot/SerpResult, Competitor + CompetitorKeywordOverlap, AppMetadataSnapshot, Review + ReviewTheme, WeeklyASOReport, AIGenerationLog, UserSettings singleton, KeywordResearchSnapshot, PendingMetadataChange (PROVE loop), plus the OrbionSchema registration.
  • Orbion/iTunes/ - Lookup, Search, Reviews-RSS clients. All actor-based, all global (no auth).
  • Orbion/ASC/ - Optional App Store Connect API integration. JWTSigner mints ES256 tokens from a Keychain-stored .p8; ASCClient is the async actor wrapper with token caching; ASCMetadataFetcher reads appInfoLocalizations + appStoreVersionLocalizations and merges them per-locale so the metadata sweep can populate subtitle and the keywords field - fields iTunes Lookup hides.
  • Orbion/Utilities/ - Hashing, DateMath, Country (50-market table), Keychain, ArrayChunked, KeywordCSVExport.
  • Orbion/Diff/MetadataDiffer.swift - content-hash + change detection for metadata snapshots.
  • Orbion/Scoring/ - CompetitorScorer, OpportunityScorer, KeywordResearchAnalyzer, KeywordGapAnalyzer, DemandIndexScorer, ASOScorer, OutcomeScorer (PROVE loop).

Sync orchestration

Sync/SyncCoordinator.swift sweeps in order: rank tracking → metadata snapshot → adoption detection → reviews → demand index → outcome scoring.

  • Auto-sync on app launch (≥2h cooldown) + after adding a new app.
  • Manual Sync Now (⌘R) and per-keyword "Check Now" available.
  • Sync/CompetitorDiscovery.swift auto-creates Competitor rows from top-50 SERP results, recomputes scores after every sweep.
  • Sync/NotificationManager.swift is a thin UNUserNotificationCenter wrapper. Events: rank crash, competitor change, 1-star reviews, outcome worked, outcome regressed, pending-rewrite nudge.
  • Sync/AdoptionDetector.swift matches PendingMetadataChange rows against new live metadata; auto-detects title/description adoptions; fires a 7-day nudge for subtitle/keywords (manual mark-as-shipped path).

AI layer (Foundation Models on-device)

  • AI/FoundationModelsClient.swift wraps the FoundationModels framework behind a single typed funnel: respond<T: Generable>(_:as:). Each public method is a one-liner over that funnel. The whole client is gated behind #if canImport(FoundationModels); the #else branch fulfills the same interface and always throws .unavailable so callers don't need to special-case missing-SDK builds.
  • AI/KeywordSuggester.swift - FM-first; falls back to heuristic.
  • AI/ReviewClusterer.swift - FM clustering, keyword-bucket fallback.
  • AI/ReviewReplyDrafter.swift - drafts a single reply per review.
  • AI/MetadataRewriter.swift - generates 3-5 candidate strings for title / subtitle / keywords / description.
  • AI/ReviewKeywordExtractor.swift - pulls user-vocabulary keywords from recent reviews.
  • AI/WeeklyPlanGenerator.swift - heuristic plan as deterministic source of truth, FM enrichment on top.
  • AI/ASORationaler.swift - FM rationale + structured "What to fix next" steps anchored to ASOScorer breakdown.
  • AI/ResearchInsighter.swift - Research view's "What I'd try instead" - positioning angle + long-tail variants.
  • AI/CompetitorIntel.swift - per-competitor FM summary on CompetitorDetailView.
  • AI/ReviewActionPicker.swift - picks one action per review on ReviewsTab.

UI

  • Views/RootView.swift - NavigationSplitView, sidebar with apps, dashboard / research / per-app drill-in.
  • Views/OnboardingView.swift - welcome → country picker → add first app → done.
  • Views/AddAppView.swift - paste URL/ID, country picker, look-up preview, duplicate detection.
  • Views/DashboardView.swift - greeting, stats row, Track record card (PROVE loop), today's movements (deterministic), sync card.
  • Views/ResearchView.swift - pre-launch keyword scouting: verdict, cross-market comparison, FM positioning angle + long-tail variants with one-click Track.
  • Views/SettingsView.swift - three tabs: General, AI, Data.
  • Views/AppDetailView.swift - segmented tab picker for the five tabs.
  • Views/Tabs/OverviewTab.swift - ASO Score circle + structured "What to fix next" steps, stats, quick actions, recent movements.
  • Views/Tabs/KeywordsTab.swift - sortable table with Demand column, 7d delta pill, Add row, gap suggestions chip strip, review-derived suggestions chip strip.
  • Views/KeywordDetailView.swift - Demand sources, SwiftUI Charts trend, top-10 SERP, Check-Now button.
  • Views/Tabs/CompetitorsTab.swift - sortable table, push navigation to CompetitorDetailView.
  • Views/CompetitorDetailView.swift - stats, shared keywords, recent metadata changes, ignore/restore.
  • Views/Tabs/ReviewsTab.swift - theme chips, reviews list with star ratings + country flags + Draft reply + ReviewActionPicker per-review action.
  • Views/Tabs/WeeklyPlanTab.swift - eligibility check, structured "Recommended test" card with Copy proposed text button + markdown rendering.
  • Views/MetadataRewriterSheet.swift - pick field + 1-5 target keywords → 3-5 FM candidates with char counts. Tracking badge on previously-copied candidates: shows adoption status, outcome verdict, best-keyword delta, and a Mark as shipped button for subtitle/keywords.

The PROVE loop, in detail

The wedge that makes Orbion uncategorizable next to ASO trackers:

  1. User generates candidates in MetadataRewriterSheet for a given field (title/subtitle/keywords/description) with 1-5 target keywords.
  2. User clicks Copy on a candidate → PendingMetadataChange row inserted with the candidate text, target keywords, and a JSON snapshot of current ranks (the baseline) for those keywords.
  3. On every sync's metadata sweep, AdoptionDetector compares the latest AppMetadataSnapshot against unadopted pending rows ≤14 days old. For title and description fields (which iTunes Lookup returns), substring match → adoptedAt = now. For subtitle/keywords (which Apple doesn't expose publicly), the user clicks Mark as shipped in the Rewriter sheet's tracking badge.
  4. OutcomeScorer runs after every rank sweep, computes per-target-keyword rank deltas at 7/14/28-day windows post-adoption, picks a verdict (worked / inconclusive / regressed) with a ±3-position threshold, persists as outcomeJSON.
  5. Dashboard's Track record card rolls all adopted rewrites into a wins ledger.
  6. Rewriter sheet's tracking badge on each candidate row surfaces prior-rewrite outcomes inline, so users see what worked before generating new candidates.

Known caveats

  • iTunes Lookup doesn't return subtitle or keywords field. Without App Store Connect connected, adoption auto-detection only works for title and description; subtitle/keywords rewrites need manual "Mark as shipped" - and a push notification fires once, 7 days after Copy, if the change is still pending. When ASC is connected, ASCMetadataFetcher populates these on the snapshot during the metadata sweep, and AdoptionDetector auto-detects them like any other field.
  • Outcome scoring has a 7-day floor post-adoption before any verdict appears. The Rewriter badge surfaces "outcome scoring kicks in at day 7" until then.
  • Outcome notifications fire exactly once per change (keyed by change UUID), tracked via PendingMetadataChange.notifiedVerdictRaw and nudgedAt.

Project setup

  • Orbion.entitlements - sandbox + network.client + user-selected.read-only.
  • Synchronized file groups (Xcode 16+) - new Swift files auto-discovered, no project.pbxproj surgery needed.
  • Bundle ID: jafforge.Orbion (overridable; see README "Local build overrides").

Local build overrides

Configs/Local.xcconfig (gitignored) can override DEVELOPMENT_TEAM and PRODUCT_BUNDLE_IDENTIFIER without touching project.pbxproj. See Configs/Local.xcconfig.example.