summaryrefslogtreecommitdiffstats
path: root/AGENTS.md
diff options
context:
space:
mode:
authorivar <i@oiee.no>2026-05-07 01:24:28 +0200
committerivar <i@oiee.no>2026-05-07 01:24:28 +0200
commit6eb17a18e901e2d7faa219d7e5a79083a5891dc9 (patch)
tree3d0796e1e567864dfdf7c675f7e8a5a40fb51a95 /AGENTS.md
parent4fb690150b77afced6453e6bdb14cc4cf00d5305 (diff)
downloadsolverv-6eb17a18e901e2d7faa219d7e5a79083a5891dc9.tar.xz
solverv-6eb17a18e901e2d7faa219d7e5a79083a5891dc9.zip
RefactorsHEADmaster
Diffstat (limited to 'AGENTS.md')
-rw-r--r--AGENTS.md151
1 files changed, 151 insertions, 0 deletions
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..da711d2
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,151 @@
+# Solverv — Agent Reference
+
+Norwegian solstice and equinox countdown app for iOS, with a WidgetKit extension.
+Built with SwiftUI, SwiftData, CoreLocation, and WidgetKit.
+
+## Repository layout
+
+```
+Solverv/ Main app target
+ SolvervApp.swift @main entry point, LocationManager
+ ContentView.swift Primary UI (struct is ContentView; file header says InfoScreenView)
+ Item.swift SwiftData @Model stub (Xcode template remnant, not used by UI)
+ Assets.xcassets/
+
+Shared/ Code compiled into both the app and widget targets
+ Models/
+ SolsticeData.swift Singleton; hardcoded events 2025–2030; query API
+ SolsticeEvent.swift Identifiable+Codable struct; daysUntil(), localDateTime()
+ Season.swift Enum: spring/summer/autumn/winter; color, assetName, fromDate()
+ Utilities/
+ SunTimes.swift NOAA solar algorithm (zenith 90.833°)
+ AppGroupManager.swift App Group bridge (location + sun times)
+
+Solsnu.Widget/ WidgetKit extension
+ Solsnu_Widget.swift Provider, SolvervEntry, Solsnu_Widget config, SolvervDef view-model
+ Views/
+ SmallWidgetView.swift
+ MediumWidgetView.swift
+
+SolverVTests/ XCTest suite
+ Models/SolsticeDataTests.swift
+ Utilities/SunTimesTests.swift, AppGroupManagerTests.swift
+
+Solverv.xcodeproj/ Xcode project; no SPM Package.swift, no Podfile
+```
+
+## Key types and contracts
+
+### `SolsticeData` (Shared/Models/)
+Singleton (`SolsticeData.shared`). Stores hardcoded UTC events from 2025 through 2030,
+sorted ascending. All query methods take an optional `now: Date` (defaults to `Date()`).
+
+- `nextEvent(from:) -> SolsticeEvent?` — first event with `date > now`
+- `upcomingEvents(count:from:) -> [SolsticeEvent]` — next N events
+- `progressToNextEvent(from:) -> (elapsed: Int, total: Int)?` — day counts between
+ the previous and next event; used for progress bars
+
+**Coverage ends at 2030.** Agents adding future events must follow the existing pattern:
+`SolsticeEvent(name: "<Norwegian name> <year>", date: SolsticeData.dateFromUTC(...), season: .<season>)`.
+
+### `SolsticeEvent` (Shared/Models/)
+`Identifiable, Codable`. UUID is generated at init (not stable across serialization rounds).
+
+- `daysUntil(from:) -> Int` — calendar-day difference, floored at 0
+- `localDateTime() -> Date` — **known bug**: adds UTC offset as raw seconds into
+ DateComponents rather than using a proper timezone-aware conversion. Do not expand
+ callers of this method without fixing it first.
+
+### `Season` (Shared/Models/)
+`fromDate(_:)` uses meteorological (month-based) seasons, not astronomical ones.
+`assetName` returns `"Season\(displayName)"` — maps to named color sets in Assets.xcassets.
+
+### `SunTimes` (Shared/Utilities/)
+Pure value type. Implements the NOAA solar position algorithm (zenith = 90.833° to
+account for atmospheric refraction + solar disk radius). Returns `nil` for polar
+night/midnight-sun conditions (cosH out of [-1, 1] range). No caching; callers
+instantiate per-date.
+
+```swift
+let st = SunTimes(latitude: lat, longitude: lon, date: date)
+let sunrise = st.sunrise() // Date? in device local time
+let sunset = st.sunset() // Date? in device local time
+```
+
+### `AppGroupManager` (Shared/Utilities/)
+Singleton (`AppGroupManager.shared`). App Group ID: `group.com.ivarlovlie.solverv`.
+Bridges data between the app and widget via `UserDefaults(suiteName:)`.
+
+Two stored payloads:
+- `"userLocation"` → `UserLocation` (lat, lon, ISO8601 timestamp, isDefaultLocation)
+- `"sunTimes"` → `SunTimes` (date string, ISO8601 sunrise/sunset strings, timestamp)
+
+The widget treats location as stale after 24 hours. Sun times are cached separately
+but the widget recomputes them from the stored location rather than reading the cached
+sun times struct (the `"sunTimes"` key is written by the app but not read by the widget).
+
+### `SolvervDef` (Solsnu.Widget/Solsnu_Widget.swift)
+View-model struct used by widget views. Wraps `SolsticeData` and `SunTimes` queries.
+`init(utcString:)` force-unwraps the date parse — acknowledged in a comment, only used
+in preview code. Do not use this initializer in production paths.
+
+### `LocationManager` (Solverv/SolvervApp.swift)
+`CLLocationManagerDelegate`, ObservableObject. Requests `whenInUse` authorization,
+calls `startUpdatingLocation()`, saves to `AppGroupManager` on first fix, then stops.
+Does not handle permission denial or restricted state beyond silent no-op.
+
+## Data flow
+
+```
+[CLLocationManager] --location--> [LocationManager]
+ |
+ AppGroupManager.saveLocation()
+ AppGroupManager.saveSunTimes()
+ |
+ UserDefaults (App Group)
+ |
+ +------------------+------------------+
+ | |
+ [ContentView] [Widget Provider]
+ reads location, recomputes reads location, recomputes
+ SunTimes locally on loadData() SunTimes in getTimeline()
+ (every 60s via Timer) (refreshes at next midnight)
+```
+
+## Build and test
+
+This is a pure Xcode project. There is no `xcodebuild` wrapper script.
+
+Run unit tests from Xcode (Product → Test) or via:
+```
+xcodebuild test \
+ -project Solverv.xcodeproj \
+ -scheme Solverv \
+ -destination 'platform=iOS Simulator,name=iPhone 16'
+```
+
+Tests import the main target with `@testable import Solverv`. The widget target is not
+separately testable via the current test scheme.
+
+## Conventions
+
+- Norwegian event names in `SolsticeData`: Vårjevndøgn, Sommersolverv, Høstjevndøgn, Vintersolverv.
+- All astronomical times in `SolsticeData` are UTC; `SunTimes` output is device local time.
+- `Season.fromDate(_:)` is calendar-month-based; it does not agree with the astronomical
+ season encoded in `SolsticeEvent.season`. These are intentionally different.
+- Widget timeline policy: `.after(nextMidnight)` — one entry per day.
+- `SolvervDef` is the single source of truth for widget view logic; views must not
+ call `SolsticeData` or `SunTimes` directly.
+
+## Known issues / landmines
+
+1. `SolsticeEvent.localDateTime()` applies UTC offset as raw seconds — produces wrong
+ dates in non-UTC zones with non-whole-hour offsets (e.g., India, Nepal, some AU zones).
+2. `AppGroupManager.SunTimes` (the cached struct) is written by the app but the widget
+ ignores it and recomputes from the cached location. The two caches can diverge.
+3. `SolvervDef.init(utcString:)` force-unwraps — preview-only; never call from production.
+4. `Item` (SwiftData model) is wired into `SolvervApp.sharedModelContainer` but unused.
+ Removing it requires a schema migration.
+5. `LocationManager` does not handle `.denied` or `.restricted` authorization — the app
+ shows `—` for sun times without explanation.
+6. `SolsticeData` coverage ends at Vintersolverv 2030. After that, all queries return nil.