architecture overview - fahadahmed/jamaal-app GitHub Wiki
Architecture overview
Status: draft, needs review. A map of how the pieces fit, pointing at the pages that hold the detail. It repeats no schema or rules — see the linked docs.
Pictures: the components, the data model, the engine tick, sync, widgets and the state machines are drawn in Diagrams; every screen is in the Screen gallery.
Shape of the system
Two Swift targets in one repo, plus one shared design package:
| Piece | What it is | Rules |
|---|---|---|
Jamaal |
The app: SwiftUI, multiplatform (iOS, iPadOS, macOS). Features (Today, Habits, Anchors, Night Planning, Settings), custom components, view models. | Owns presentation, navigation, notifications, StoreKit. |
JamaalCore |
A local Swift Package: the SwiftData models and the deterministic rules engine. | No UI imports. Pure logic, testable without a simulator. |
| ThreadsKit | A separate repo, added as a remote Swift Package (2.0.0 or later): the palette, category colours, space, elevation, motion and the bundled fonts with the type roles. | Tokens only; components live in Jamaal/Components/. See ThreadsKit usage. |
There is no .xcworkspace: JamaalCore is added as a local package dependency directly in Jamaal.xcodeproj.
How a decision flows
user action / clock / sync
│
view model (Jamaal)
│ builds a context (today's tasks, habits, Anchors, sessions, settings)
▼
rules engine (JamaalCore) ← deterministic: same state + inputs, same output
│ emits typed signals
▼
message templates (Jamaal) ← turns signals into the companion's words
▼
views
- The engine outputs typed signals, never strings. A separate template layer phrases them in the companion's voice, so the engine stays testable and language-free.
- Determinism is a hard rule: no machine learning and no heuristics that vary between runs. "Learning" (for example the normal-day suggestion) is a plain calculation the user accepts or ignores.
- The engine is organised as eight modules behind one protocol; see Rules engine.
Persistence and sync
- SwiftData with CloudKit sync from v1: 14 models, all attributes defaulted, relationships optional, no unique constraints. The full list, conventions and dedup keys are in the Schema overview.
- Because CloudKit can't enforce uniqueness and keeps the last write per record, the engine is written to be idempotent — rollover, Anchor generation and deferral all use keys that make repeating or racing them safe.
- Dates: day-level fields are floating calendar dates; times of day are real instants. The app's day rolls over at a user-set time. See The day boundary.
- Local-only state (per device, not synced): notification and appearance preferences, expanded/collapsed UI state, the engine's last-processed day.
- CloudKit's production schema is append-only, so the schema freeze matters before the first production deploy.
Time, sessions and notifications
- Focus sessions derive elapsed time from a start timestamp, never from memory, so a killed app loses nothing. See Task → Focus sessions.
- Notifications are local (
UNUserNotificationCenter): the evening planning prompt, an optional morning nudge, habit reminders and a light window-closing nudge. Sessions never notify. Trial reminders are few and quiet. iOS caps pending local notifications at 64, which the volume must stay under. - Live Activity / Dynamic Island for the running session is v1.1; it will be driven locally by the same session state, not by a push.
Platforms and layout
- Minimum OS: iOS 26 / macOS 26. Liquid Glass is a system feature from 26, so the design needs no fallback; ThreadsKit's lower floor (iOS 18 / macOS 15) is irrelevant to the app. iPhone Duo-specific APIs (iOS 27.1) are newer and are gated behind availability checks.
- Layout adapts by size class: system tab bar and toolbars on compact width, sidebar plus a detail pane on regular width, and iPhone Duo covered by the compact (outer display) and regular (inner display) layouts. Navigation must use the system components so this adaptation is automatic. See App flow → Adaptive layout.
Which platforms
v1 is iPhone, iPad and Mac, with widgets. A companion watchOS app is v1.1. visionOS is a possible later step (the iPad app can run there as a compatible app; a native target would follow the macOS pattern). tvOS is not planned (decided 8 Oct 2026): a television does not suit a personal planner you edit. JamaalCore is platform-neutral and UI-free (it builds for iOS, macOS, watchOS, visionOS and tvOS today), and platform differences stay in PlatformShims and AppNavigation.isRegular, so a later platform is an addition and not a rework. ThreadsKit builds for visionOS and tvOS but needs a small watchOS fix (two UIKit colour calls) before the watch app. The store location goes through one helper so a platform without persistent storage never forces a redesign.
Sync (#243). The store syncs through iCloud, the private database of iCloud.dev.fhdamd.Jamaal, when the app is signed with its App Group (which carries the iCloud entitlement); SyncMode.resolve decides, so tests, previews, unsigned builds, CI and in-memory stores stay local. The widget extension opens the same store read-only without CloudKit (only the app mirrors). Debug builds use CloudKit's Development environment, so they sync with each other but not with TestFlight or the App Store; the schema is deployed to Production before the first TestFlight build. Two devices that each seed the defaults on first launch make duplicate default rows; the engine's dedup (docs/schema/overview.md) merges them on the next tick. That tick runs at launch, when the app becomes active, and three seconds after changes from iCloud stop arriving (Debouncer, #251): on a Mac the window is already active when the first import lands, so without that the doubled Personal, Family and Work stayed until a relaunch. There is no sync status screen yet (the "Welcome back" flow and ST-06 are not built); -JamaalNoCloudKit turns sync off in debug builds.
Start-up runs once per process (#233). SwiftUI builds the App value more than once per launch (measured: two to six times on the simulator), so anything in JamaalApp.init ran that many times: the fonts were registered again (CoreText logs "GSFont: file already registered" and a related CoreFS cache line each time) and another model container was opened on the same store. AppBootstrap holds the work as static lets, so it happens once however often it is asked for: the font registration, the first model container, and the single notification delegate (before, each build made its own, so the delegate the system called back could be a different object from the one the view tree gave its route handler to, and a tapped notification could go nowhere). The widget provider registers its fonts once per process the same way. Messages that remain in the console and are not ours: cannot add handler to 0 from 0 - dropping (a system library; no code or dependency of ours contains the text) and fopen failed for data file: errno = 2 (the system's icon and font cache services, libCoreFSCache).
App icons. iPhone and iPad use the three 1024 px icons in AppIcon.appiconset (default, dark, tinted). The Mac uses AppIconMac.icon, an Icon Composer file built from the same art (a solid navy fill, and the J and the terracotta dot as one transparent layer), chosen for the macOS SDK only by ASSETCATALOG_COMPILER_APPICON_NAME[sdk=macosx*]. On macOS 26 an icon that isn't an Icon Composer file is shown inside a plain plate; this one gets the system's shape, shadow and glass. If the iOS art changes, redraw AppIconMac.icon/Assets/J.png from it (the J and dot on a transparent background) and keep the fill the same navy.
Widgets and the watch
Widgets are in v1 and a watch companion is v1.1 (ADR 0003, spec). A widget extension cannot open the app's private store, so the SwiftData store lives in an App Group container from the first sync-enabled build. Both the widgets and the watch read a pure, Codable WidgetSnapshot that JamaalCore builds from the engine's existing reads.
Testing
JamaalCore: Swift Testing exclusively (@Test,#expect,try #require); the natural home for parameterised tests such as Anchor generation across time windows and prayer-time scenarios.- App view models: Swift Testing in
JamaalTests. - Custom, interaction-heavy components (capacity slider, Night Planning wizard, focus chip): XCUITest in
JamaalUITests. - The Mac app (sidebar, menu shortcuts, the right-click menu): XCUITest on a real Mac in
JamaalMacUITests(schemeJamaalMac, a template to copy locally; seeCLAUDE.md). It drives the real desktop, so it is run by hand, not in an unattended session.
Business constraints that shape the build
- Free download, a 14-day app-managed trial, then a subscription; when it ends unsubscribed the app becomes read-only, not locked. See App flow → Trial and subscription.
- Sharing and referral are deferred to v1.1; categories are personal labels for a single user, not multi-account sharing.
Where the detail lives
Home · App flow · Schema overview · Rules engine · ADR 0001 · ADR 0002 · ADR 0003 · Widgets and the watch · Roadmap