diff --git a/AGENTS.md b/AGENTS.md index dd2dbce7..84b4604d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,9 +8,12 @@ | SwiftFormat | 0.60.1 | `.mise.toml` | | Swift PM | 6.2 | `Package.swift` (`swift-tools-version`) | -**Libraries** (**StuffCore**, **LifecycleKit**, **JournalKit**, **LogKit**, **LogViewerUI**, **PeriscopeCore**, **PeriscopeUI**, **PeriscopeTools**, **TestHostSupport**, **RegionKit**, **WhereCore**, **WhereUI**) are defined in the root [`Package.swift`](Package.swift) — local package for libraries, Tuist for apps and test bundles. - -Tuist manifests live at the repo root ([`Project.swift`](Project.swift), [`Tuist.swift`](Tuist.swift)). `Project.swift` references `Package.local(path: .relativeToRoot("."))` and declares the **Where** app, **StuffTestHost**, and unit-test targets that depend on package products. +Library targets live in the root [`Package.swift`](Package.swift) (one local +package); apps, app extensions, and test bundles are Tuist targets in +[`Project.swift`](Project.swift) (plus [`Tuist.swift`](Tuist.swift)), which +references the package via `Package.local(path: .relativeToRoot("."))`. The +two manifests are the authoritative target catalog — it is deliberately not +duplicated here. Run `./ide` (or `./ide -i` to also install dependencies) to regenerate the Xcode project, install external agent skills, and point Git at `.githooks/`. @@ -53,8 +56,10 @@ by `./sync-agents`. ## Targets -- **Package products** ([`Package.swift`](Package.swift)) — **StuffCore** ([`Shared/StuffCore/Sources/`](Shared/StuffCore/Sources/)), **LifecycleKit** ([`Shared/LifecycleKit/Sources/`](Shared/LifecycleKit/Sources/)), **JournalKit** ([`Shared/JournalKit/Sources/`](Shared/JournalKit/Sources/), the generic append-only crash-durable journal), **LogKit** ([`Shared/LogKit/Sources/`](Shared/LogKit/Sources/), the logging facade), **LogViewerUI** ([`Shared/LogViewerUI/Sources/`](Shared/LogViewerUI/Sources/), the generic SwiftUI log viewer), **PeriscopeCore** / **PeriscopeUI** / **PeriscopeTools** ([`Shared/Periscope/`](Shared/Periscope/), the typed hierarchical observability framework — core + SwiftData store, SwiftUI environment integration, and on-device exploration tooling), **SwiftDataInspector** ([`Shared/SwiftDataInspector/Sources/`](Shared/SwiftDataInspector/Sources/), the generic SwiftData browser), and **TestHostSupport** ([`Shared/TestHostSupport/Sources/`](Shared/TestHostSupport/Sources/), the shared UIKit hosting + run-loop helpers for hosted test bundles) under [`Shared/`](Shared/); the **Broadway** design-system libraries **BroadwayCore** / **BroadwayUI** under [`Shared/Broadway/`](Shared/Broadway/) (see [`Shared/Broadway/AGENTS.md`](Shared/Broadway/AGENTS.md)); **RegionKit** ([`Where/RegionKit/Sources/`](Where/RegionKit/Sources/), the geometry + GeoJSON + region-lookup engine WhereCore builds on) / **WhereCore** / **WhereUI** under [`Where/`](Where/). -- **Tuist targets** ([`Project.swift`](Project.swift)) — **Where** app ([`Where/Where/`](Where/Where/)) and its embedded app extensions **WhereWidgets** ([`Where/WhereWidgets/`](Where/WhereWidgets/), home/lock-screen widgets) and **WhereShareExtension** ([`Where/WhereShareExtension/`](Where/WhereShareExtension/), a Share-sheet action that saves shared content as evidence into the App Group store), **RegionViewer** ([`Where/RegionViewer/`](Where/RegionViewer/), a thin standalone **Mac Catalyst** host for the WhereUI region-map developer tool — the only target with a `.macCatalyst` destination), **BroadwayCatalog** ([`Shared/Broadway/BroadwayCatalog/`](Shared/Broadway/BroadwayCatalog/), the Broadway component showcase app), **StuffTestHost** ([`Shared/StuffTestHost/`](Shared/StuffTestHost/)), **WhereTests** (app tests, no host), and hosted **\*Tests** bundles (**StuffCoreTests**, **LifecycleKitTests**, **JournalKitTests**, **LogKitTests**, **LogViewerUITests**, **PeriscopeCoreTests**, **PeriscopeUITests**, **PeriscopeToolsTests**, **SwiftDataInspectorTests**, **RegionKitTests**, **WhereCoreTests**, **WhereUITests**, **BroadwayCoreTests**, **BroadwayUITests**, **BroadwayCatalogTests**) that depend on **StuffTestHost** + **TestHostSupport** + the relevant package product (most via the shared `unitTests` helper). +- For the current list of library products, apps, extensions, and test + bundles, read [`Package.swift`](Package.swift) and + [`Project.swift`](Project.swift); each module's own `README.md` / + `AGENTS.md` says what it is and how it may be used. - Add SPM library targets in `Package.swift` and wire apps/tests in `Project.swift` (see existing `unitTests` helper). A new module also ships a root `README.md` and `AGENTS.md` — see [Per-module docs](#per-module-docs). - **CI scheme**: CI runs the explicit shared **Stuff-iOS-Tests** scheme (all test bundles) rather than the autogenerated `Stuff-Workspace` scheme. New test bundles must be added to the `Stuff-iOS-Tests` scheme in `Project.swift` or CI won't run them. - **Never double-link a package product into a test bundle that already gets it through a dynamic-framework dependency.** Xcode's default SPM integration (how `Package.local` is wired here) embeds a product's code into *every* image that links it. **WhereUI** is a dynamic framework that statically embeds its own dependencies (WhereCore, BroadwayCore/BroadwayUI, LifecycleKit, LogViewerUI, SwiftDataInspector, …), so a `*Tests` bundle that depends on **WhereUI** *and* re-lists one of those in `extraPackageProducts` ends up with a **second copy** of it. When the shared **StuffTestHost** loads several `.xctest` bundles into one process, that leaves duplicate **type metadata** for the module, and any *type-keyed runtime lookup that crosses the WhereUI boundary* — SwiftUI `EnvironmentKey`s, `UITraitBridgedEnvironmentKey` bridging, the type-keyed `BTraits`/`BThemes`/`BStylesheets` containers — silently resolves against the wrong copy and returns the default (the writer stores under one copy's key *type*, the reader looks it up under another's). It only reproduces in the **full multi-bundle scheme** (not isolated `tuist test WhereUITests` runs) and is papered over by newer Xcode linkers, so it is brutal to diagnose (it cost ~2 hours once). **Depend on such products only transitively via `WhereUI`; keep them out of `extraPackageProducts`.** `WhereStylesheetTests.resolvesTraitAwareTokensFromTheBroadwayRoot` is the regression guard — it exercises a `\.stylesheet` (→ `\.bContext`) read across the boundary and fails if a duplicate copy returns. @@ -67,7 +72,9 @@ by `./sync-agents`. ## Directory layout -Shared code and the shared iOS test host live under **`Shared/`**. Feature apps and their modules (e.g. **Where**) live under a top-level folder per feature (e.g. **`Where/`**). The imported **Broadway** design-system modules live together under **`Shared/Broadway/`** (each module keeps its own `Sources/`, `Tests/`, `README.md`, and `AGENTS.md`). +Every module — shared ones under `Shared/`, feature ones under a top-level +folder per feature (e.g. `Where/`) — follows this skeleton, and a new module +must too: ``` Shared// @@ -123,6 +130,10 @@ the generated (gitignored) `CLAUDE.md` is produced next to it. behind `@_spi(Testing)` (with `#if DEBUG` when release must not ship them); tests inject small values (e.g. a retry-queue size of 20) instead of hardcoding production limits. +- **Test doubles conform to the production protocol.** Model a seam as a + protocol the real and fake both conform to (`LocationSource` / + `ScriptedLocationSource`) — never an enum switch inside a production type + that branches to fake behavior. - State machines with many branches (launch runners, lifecycle drives) benefit from **seeded fuzz/adversarial tests** that replay failures exactly. - **Non-obvious types get a brief doc comment** on the type — detectors, @@ -134,6 +145,11 @@ the generated (gitignored) `CLAUDE.md` is produced next to it. one field or that escapes a single function — tuples are fine as ad-hoc inline returns but should not appear in property types, collection element types, or public API. +- **Group large flat types into sub-structs and child types.** When a type + grows a long flat property list (e.g. a config with a cluster of watchdog + knobs) or a file accretes several behavioral areas, group related properties + into nested structs and split responsibilities into focused child types — + don't let one god-type keep growing. - Identifiers/keys are `Hashable` (ideally a typed enum) or `AnyHashable`, not raw `String`s — a typed token can't silently typo into a new, untracked id, and any `Hashable` converts to `AnyHashable` implicitly at the call site. @@ -193,6 +209,19 @@ the generated (gitignored) `CLAUDE.md` is produced next to it. - **`didSet` must skip work when the value is unchanged.** When the stored type is `Equatable`, guard `oldValue != newValue` before invalidation, logging, or other side effects — reassigning the same value should be a no-op. +- **Prefer synthesized `Codable`; keep persisted formats rename-safe.** + Anything persisted (journals, backups, stored preferences) must survive + Swift-side renames — synthesized coding of an enum with associated values + freezes the *case names* into the wire format, so renaming a case silently + breaks old data. A hand-written conformance needs a load-bearing reason, + documented on the conformance (see `LogJournalEntry`); simple structs of + primitives should just use the synthesized one (see `CalendarDay`). +- **Retain notification-observer tokens; every `start` has a `stop`.** A + block-based `addObserver(forName:)` observation stays alive in the center + whether or not you keep the token — dropping it makes the observation + unremovable and immortalizes everything the block captured. Store the token + (see `AmbientObserverTokens`) and give any `start…`-style observation API a + paired `stop()` that removes it. - **Testing-only APIs use `@_spi(Testing)`.** Hooks meant exclusively for unit tests or previews (direct store mutation, failure injection, queue introspection, etc.) are marked `@_spi(Testing)`; wrap in `#if DEBUG` when @@ -268,8 +297,20 @@ mark `in_progress`, implement, run local checks, commit, mark `completed`. - Pure-groundwork steps (no behavior change) still get their own commit; say so in the body. - Name the plan step each commit closes (the to-do title is fine). -- Don't push until the user asks — unless the plan says otherwise, or the - request clearly implies it (e.g. "open a PR", "ship it"). +- Pushing follows the [GitHub](#github) rules: a branch with an open PR gets + every commit pushed as it lands; otherwise don't push until the user asks — + unless the plan says otherwise, or the request clearly implies it + (e.g. "open a PR", "ship it"). + +## GitHub + +- Use the `gh` CLI for **all** GitHub interaction — PRs, issues, checks, + releases, review comments — not raw API calls or the web UI. +- Open PRs in **ready-for-review** mode, not draft. +- **Keep open PRs current.** When the current branch has an open PR, push each + local commit as it lands so the PR never goes stale. A branch without a PR + still waits for the user before pushing (see [Working on + plans](#working-on-plans)). ## Working on PR feedback @@ -280,6 +321,13 @@ editing and pushing, not on understanding. Once the user points you at feedback (names comments, says "address these", etc.), that's your go-ahead — do the work end to end without re-asking per comment. +- **When a commit resolves an issue a comment called out, reply to that + comment** saying so — name the commit and what changed — using the AI-agent + prefix from [Posting on the user's behalf](#posting-on-the-users-behalf). +- **Deferred feedback gets filed, never dropped.** If a comment is deliberately + not addressed, record it somewhere durable (the module's `TODOs.md` or the + review-tracking file) and reply linking where it's tracked. + ## Debugging build/test/CI failures **Check `git status` and recent history first — before analyzing the error.** A diff --git a/Shared/Periscope/PeriscopeCore/AGENTS.md b/Shared/Periscope/PeriscopeCore/AGENTS.md index 54fee80c..b8975aff 100644 --- a/Shared/Periscope/PeriscopeCore/AGENTS.md +++ b/Shared/Periscope/PeriscopeCore/AGENTS.md @@ -8,17 +8,6 @@ pipeline, ambient event sources, and the SwiftData store. See This file complements the root [`AGENTS.md`](../../../AGENTS.md), which owns the build system, formatting, and global conventions. Read that first. -## Layout - -`Sources/` groups by concern: `Events/` (the event vocabulary — levels, -tags, attachments), `Loggers/` (`Log` and the scope tree), -`Context/` (task-local and per-instance context derivation), `Spans/` -(timing and exits), `Pipeline/` (the `Periscope` system, records, sinks), -`Store/` (SwiftData persistence and queries), `Journal/` (the crash -journal's log-layer envelope and writer, over -[`JournalKit`](../../JournalKit)), and `Ambient/` (environmental event -sources). Tests stay flat, named 1:1 with their source files. - ## Scope & dependencies - **Foundation + os + SwiftData + Network + JournalKit only** (plus the diff --git a/Where/AGENTS.md b/Where/AGENTS.md index 303541b0..5d8a8a0b 100644 --- a/Where/AGENTS.md +++ b/Where/AGENTS.md @@ -14,40 +14,13 @@ system, formatting, and global conventions. Read that first. ## Modules -``` -Where/ - Where/ App target – SwiftUI entry point (WhereApp → RootView) - RegionKit/ SPM library – geometry, GeoJSON, Region model + lookup (WhereCore depends on it) - WhereCore/ SPM library – domain model, persistence, GPS, aggregation - WhereUI/ SPM library – SwiftUI views + view models (depends on WhereCore) - WhereWidgets/ Widget extension – reads published snapshots, renders WhereUI views - WhereShareExtension/ Share extension – saves shared content as Evidence into the App Group store - RegionViewer/ Mac Catalyst shell for the region-map developer tool -``` - -- **App target** `Where` is intentionally tiny: it wires `RootView` from - `WhereUI` into a `WindowGroup`. Add domain behavior to `WhereCore`, - presentation and view-model wiring to `WhereUI`. -- **`RegionKit`** is the lowest layer: the `Region` model, coordinate geometry - (`Coordinate`, `GeoPolygon`, `BoundingBox`, `LongitudeSpan`), GeoJSON - decoding, and coordinate-to-`Region` lookup (`RegionAttributor`). Pure Swift + - Foundation + LogKit; the bundled region polygons (`Resources/*.geojson`) and - the region-name catalog ship here. See [`RegionKit/AGENTS.md`](RegionKit/AGENTS.md). -- **`WhereCore`** is the domain layer: pure Swift + Foundation + SwiftData + - CoreLocation; it must **not** import SwiftUI or UIKit. It depends on - **`RegionKit`** and calls into it for region lookup. -- **`WhereUI`** is the SwiftUI layer: views plus `@Observable` view models — - the app-level `WhereModel`, the always-on `WhereSession` coordinator (no - presentation state), and its scope-tiered children (scene-scoped - `YearReportModel`, view-scoped `ResolveModel` / `BackupModel` / - `RemindersSettingsModel`). It is **not** the domain model — see - [Layering](#layering). -- **Developer tools** live behind a DEBUG-only floating overlay, not in Settings. - `Developer/DeveloperOverlay` is attached once at `RootView` (above every launch - phase and tab, reachable even logged out): a draggable, corner-snapping - `DeveloperOverlayButton` that expands into a Picture-in-Picture panel and grows - to full screen, hosting `Developer/DeveloperToolsView` (Logs / SwiftData - inspector / region map). All of it is compiled out of release. +Each module's own `AGENTS.md` / `README.md` is the authority on what it is. +The layering stack, bottom-up: **RegionKit** (geometry + region lookup) → +**WhereCore** (domain; never imports SwiftUI/UIKit) → **WhereUI** (SwiftUI +views + view models — *not* the domain model) → the thin hosts (**Where** app, +**WhereIntents**, **WhereWidgets**, **WhereShareExtension**, **RegionViewer**). +Each layer reaches only *down*. The app target stays intentionally tiny: add +domain behavior to WhereCore, presentation to WhereUI — not there. ## Layering @@ -90,27 +63,19 @@ Rules the code enforces and agents must preserve: - **Location comes through the `LocationSource` protocol** — production is `CoreLocationSource`; tests and previews use `ScriptedLocationSource`. Besides the passive `sampleStream`, it offers a best-effort one-shot - `requestCurrentLocation()` (re-exposed as `LocationIngestor.currentLocation()`) - used to stamp manual entries; it returns `nil` rather than throwing when no - fix is available. + `requestCurrentLocation()`, used to stamp manual entries; it returns `nil` + rather than throwing when no fix is available. - **Manual entries carry a `ManualEntryAudit`** (when made, an optional note, and a best-effort capture-time `CapturedLocation`). The view-model intents - (`YearReportModel.setManualDay` / `setManualDays` / `overrideDay`) assemble it - from a `note:` plus `currentLocation()`; `DayJournal`'s write methods take an - explicit `audit:` (no default) and persist it on `DayPresence` / - `SDManualDay`. An additive backfill can't downgrade an authoritative row's - regions, but the newer audit always wins. `DayRelabelView` shows it read-only. -- **`WhereServices.recentActivity`** is a standalone, on-demand - `RecentActivitySummarizer` that summarizes a selectable look-back window - (`RecentActivityWindow`: 24h / week / month / year-so-far) of locations on - device via Foundation Models (behind the `ActivitySummaryGenerating` seam). It - collapses consecutive same-region readings into transitions and caps them so a - long window's prompt still fits the model's context. It is distinct from - `WhereServices.summary` (the daily notification recap); model unavailability - surfaces as a typed reason, never a silent empty summary. The sheet - (`RecentActivitySummaryView`) streams the result in with a typewriter reveal - (`TypewriterText`) and shows `AppIconActivityIndicator` — a subtle cousin of - the launch splash's pulsing icon — while generating. + assemble it; `DayJournal`'s write methods take an explicit `audit:` (no + default) and persist it. An additive backfill can't downgrade an + authoritative row's regions, but the newer audit always wins. +- **`WhereServices.recentActivity`** (the on-demand Foundation Models + activity summarizer, behind the `ActivitySummaryGenerating` seam) is + distinct from `WhereServices.summary` (the daily notification recap). It + caps collapsed region transitions so a long window's prompt still fits the + model's context, and model unavailability surfaces as a typed reason — never + a silent empty summary. ## Localization @@ -120,17 +85,14 @@ views or thrown errors. - **WhereUI:** funnel every string through `Strings.swift` (keys in the module `Localizable.xcstrings`, `bundle: .module`). Counts use catalog plural variations; years use a grouping-free number style ("2026", not "2,026"). -- **WhereCore:** user-visible errors use static - `String(localized:bundle: .module)` keys in its own catalog. -- **RegionKit:** region names (`Region.localizedName`) use static - `String(localized:bundle: .module)` keys in RegionKit's own catalog. +- **WhereCore** (user-visible errors) and **RegionKit** (region names via + `Region.localizedName`) use static `String(localized:bundle: .module)` keys + in their own catalogs. +- **Extensions** (WhereWidgets, WhereShareExtension) keep their chrome in + their own catalogs and reuse WhereUI's public presentation helpers for + shared copy. - **DEBUG-only UI** still gets catalog entries — don't bypass localization because a surface is dev-only. -- **WhereWidgets:** gallery name/description live in the extension's own - catalog; in-widget copy reuses WhereUI `Strings`. -- **WhereShareExtension:** compose-sheet chrome lives in the extension's own - catalog (`ShareStrings`); evidence kind names reuse WhereUI's public - `EvidenceKind` presentation helpers. Add the key to the catalog first, then reference it — never ship English literals in SwiftUI `Text` or `errorDescription`. @@ -159,15 +121,11 @@ literals in SwiftUI `Text` or `errorDescription`. derives 365/366 rather than assuming a length). - **Core layout APIs throw on failure**; views surface `ContentUnavailableView` + log, never `!`. -- Layout tokens live in `WhereStylesheet` (a Broadway `BStylesheet`, read in - views via `@Environment(\.stylesheet)`; off the `View` tree — layout helpers, - tests — use `WhereStylesheet.default`). `RootView` seeds the Broadway context - with `.broadwayRoot(themes: WhereThemes.current)`, so tokens can derive from - live traits (e.g. bigger day-grid tap targets at accessibility Dynamic Type - sizes, a flatter card under Reduce Transparency). Shared date-range copy lives - in `DateRangeFormatting`; numbers and dates use `FormatStyle`, not string - interpolation. Expensive layout computes once into state, not per `body` - pass. Sharing uses `ShareLink` / `Transferable`. +- Appearance tokens live in `WhereStylesheet` — see + [`WhereUI/AGENTS.md`](WhereUI/AGENTS.md) for how to read and extend it. + Shared date-range copy lives in `DateRangeFormatting`; numbers and dates use + `FormatStyle`, not string interpolation. Expensive layout computes once into + state, not per `body` pass. Sharing uses `ShareLink` / `Transferable`. ## SwiftUI views & previews @@ -183,12 +141,21 @@ shell, the `WhereSession` coordinator for logged-in views). Cover the states that matter — empty, loaded, and distinct edge states — not just the happy path. -Animate transitions between distinct states in a way that fits the surface and -its content — don't hard-cut. A view that swaps on a `LoadState` (or shows an -in-flight status) should fade/move rather than snap (e.g. `.transition(.opacity)` -on each `switch` arm plus `.animation(_:value:)`, or `.animation(_:value:)` on a -form that reveals a saving row). See `RecentActivitySummaryView` and the -manual-entry forms. +- **Animate transitions between distinct states** in a way that fits the + surface and its content — don't hard-cut. A view that swaps on a `LoadState` + (or shows an in-flight status) should fade/move rather than snap (e.g. + `.transition(.opacity)` on each `switch` arm plus `.animation(_:value:)`). + Hidden means *out of the tree* (`if` + transition), not opacity zero. +- **Derive UI dimensions; don't repeat them.** A repeated dimension gets one + named home; real chrome is measured from the live UI via a preference key / + `onGeometryChange` (see `DeveloperTabBarInset`) rather than hardcoding its + expected size; controls scale with `@ScaledMetric`; prefer semantic font + styles over fixed point sizes. +- **Custom full-screen surfaces must work under VoiceOver.** A surface that + takes over the screen carries the `.isModal` accessibility trait and posts + `.screenChanged` when crossing the modal boundary (`.layoutChanged` for + lighter transitions). Non-modal floating chrome stays reachable behind (see + `DeveloperOverlay`). ## Adding things diff --git a/Where/WhereCore/AGENTS.md b/Where/WhereCore/AGENTS.md index fc91ad01..76b50ceb 100644 --- a/Where/WhereCore/AGENTS.md +++ b/Where/WhereCore/AGENTS.md @@ -16,15 +16,9 @@ internal shape. ## Scope & dependencies -- **Pure Swift + Foundation + SwiftData + CoreLocation + FoundationModels**, - plus [`RegionKit`](../RegionKit) (region lookup) and - [`LogKit`](../../Shared/LogKit); `ZIPFoundation` backs the backup archive. It +- Dependencies live in the root [`Package.swift`](../../Package.swift). It must **not** import SwiftUI or UIKit — if a behavior would still be correct without SwiftUI, it belongs here, not in `WhereUI`. -- Library target in [`Package.swift`](../../Package.swift) - (`Where/WhereCore/Sources`); depended on by `WhereUI` and the `WhereWidgets` - extension. User-visible error strings ship in its own - `Sources/Resources/Localizable.xcstrings` (`bundle: .module`). ## Shape & invariants @@ -78,6 +72,12 @@ internal shape. its cache on the same signal *and* is invalidated inline where a caller needs it provably fresh (see `WhereServices.reset()`), which is the deterministic half of that pair, not redundant with it. +- **Post-write reconciliation is defined once.** Every write and import routes + through `DayJournal.reconcileAfterDayChange()` (or its widget-less subset + `reconcileIssueState()` for dismiss/restore paths) — never copy the reconcile + fan-out into a new write path. Cross-collaborator hooks take a single closure + wired at the composition root (`BackupCoordinator.onImport`), not an injected + list of reconcilers. - **`LocationSource` abstracts GPS.** Production is `CoreLocationSource` (Visits + significant-change); tests/previews use `ScriptedLocationSource`. The one-shot `requestCurrentLocation()` returns `nil` (never throws) when no fix diff --git a/Where/WhereCore/Sources/BackupCoordinator.swift b/Where/WhereCore/Sources/Backup/BackupCoordinator.swift similarity index 100% rename from Where/WhereCore/Sources/BackupCoordinator.swift rename to Where/WhereCore/Sources/Backup/BackupCoordinator.swift diff --git a/Where/WhereCore/Sources/DismissedIssue.swift b/Where/WhereCore/Sources/DataResolution/DismissedIssue.swift similarity index 100% rename from Where/WhereCore/Sources/DismissedIssue.swift rename to Where/WhereCore/Sources/DataResolution/DismissedIssue.swift diff --git a/Where/WhereCore/Sources/Calendar+DayCount.swift b/Where/WhereCore/Sources/Days/Calendar+DayCount.swift similarity index 100% rename from Where/WhereCore/Sources/Calendar+DayCount.swift rename to Where/WhereCore/Sources/Days/Calendar+DayCount.swift diff --git a/Where/WhereCore/Sources/CalendarDay.swift b/Where/WhereCore/Sources/Days/CalendarDay.swift similarity index 100% rename from Where/WhereCore/Sources/CalendarDay.swift rename to Where/WhereCore/Sources/Days/CalendarDay.swift diff --git a/Where/WhereCore/Sources/Date+CalendarDays.swift b/Where/WhereCore/Sources/Days/Date+CalendarDays.swift similarity index 100% rename from Where/WhereCore/Sources/Date+CalendarDays.swift rename to Where/WhereCore/Sources/Days/Date+CalendarDays.swift diff --git a/Where/WhereCore/Sources/DayAggregator.swift b/Where/WhereCore/Sources/Days/DayAggregator.swift similarity index 100% rename from Where/WhereCore/Sources/DayAggregator.swift rename to Where/WhereCore/Sources/Days/DayAggregator.swift diff --git a/Where/WhereCore/Sources/DayPresence.swift b/Where/WhereCore/Sources/Days/DayPresence.swift similarity index 100% rename from Where/WhereCore/Sources/DayPresence.swift rename to Where/WhereCore/Sources/Days/DayPresence.swift diff --git a/Where/WhereCore/Sources/PresenceCalendar.swift b/Where/WhereCore/Sources/Days/PresenceCalendar.swift similarity index 100% rename from Where/WhereCore/Sources/PresenceCalendar.swift rename to Where/WhereCore/Sources/Days/PresenceCalendar.swift diff --git a/Where/WhereCore/Sources/Region+Ordering.swift b/Where/WhereCore/Sources/Days/Region+Ordering.swift similarity index 100% rename from Where/WhereCore/Sources/Region+Ordering.swift rename to Where/WhereCore/Sources/Days/Region+Ordering.swift diff --git a/Where/WhereCore/Sources/RegionDayLocations.swift b/Where/WhereCore/Sources/Days/RegionDayLocations.swift similarity index 100% rename from Where/WhereCore/Sources/RegionDayLocations.swift rename to Where/WhereCore/Sources/Days/RegionDayLocations.swift diff --git a/Where/WhereCore/Sources/EvidenceReader.swift b/Where/WhereCore/Sources/Evidence/EvidenceReader.swift similarity index 100% rename from Where/WhereCore/Sources/EvidenceReader.swift rename to Where/WhereCore/Sources/Evidence/EvidenceReader.swift diff --git a/Where/WhereCore/Sources/DayJournal.swift b/Where/WhereCore/Sources/Journal/DayJournal.swift similarity index 100% rename from Where/WhereCore/Sources/DayJournal.swift rename to Where/WhereCore/Sources/Journal/DayJournal.swift diff --git a/Where/WhereCore/Sources/ManualEntryAudit.swift b/Where/WhereCore/Sources/Journal/ManualEntryAudit.swift similarity index 100% rename from Where/WhereCore/Sources/ManualEntryAudit.swift rename to Where/WhereCore/Sources/Journal/ManualEntryAudit.swift diff --git a/Where/WhereCore/Sources/LocationIngestor.swift b/Where/WhereCore/Sources/Location/LocationIngestor.swift similarity index 100% rename from Where/WhereCore/Sources/LocationIngestor.swift rename to Where/WhereCore/Sources/Location/LocationIngestor.swift diff --git a/Where/WhereCore/Sources/LocationSample.swift b/Where/WhereCore/Sources/Location/LocationSample.swift similarity index 100% rename from Where/WhereCore/Sources/LocationSample.swift rename to Where/WhereCore/Sources/Location/LocationSample.swift diff --git a/Where/WhereCore/Sources/StoreChangeBroadcaster.swift b/Where/WhereCore/Sources/Persistence/StoreChangeBroadcaster.swift similarity index 100% rename from Where/WhereCore/Sources/StoreChangeBroadcaster.swift rename to Where/WhereCore/Sources/Persistence/StoreChangeBroadcaster.swift diff --git a/Where/WhereCore/Sources/InMemoryKeyValueStore.swift b/Where/WhereCore/Sources/Preferences/InMemoryKeyValueStore.swift similarity index 100% rename from Where/WhereCore/Sources/InMemoryKeyValueStore.swift rename to Where/WhereCore/Sources/Preferences/InMemoryKeyValueStore.swift diff --git a/Where/WhereCore/Sources/KeyValueStore.swift b/Where/WhereCore/Sources/Preferences/KeyValueStore.swift similarity index 100% rename from Where/WhereCore/Sources/KeyValueStore.swift rename to Where/WhereCore/Sources/Preferences/KeyValueStore.swift diff --git a/Where/WhereCore/Sources/WherePreferences.swift b/Where/WhereCore/Sources/Preferences/WherePreferences.swift similarity index 100% rename from Where/WhereCore/Sources/WherePreferences.swift rename to Where/WhereCore/Sources/Preferences/WherePreferences.swift diff --git a/Where/WhereCore/Sources/DailySummaryReconciler.swift b/Where/WhereCore/Sources/Reminders/DailySummaryReconciler.swift similarity index 100% rename from Where/WhereCore/Sources/DailySummaryReconciler.swift rename to Where/WhereCore/Sources/Reminders/DailySummaryReconciler.swift diff --git a/Where/WhereCore/Sources/DataIssueAlertReconciler.swift b/Where/WhereCore/Sources/Reminders/DataIssueAlertReconciler.swift similarity index 100% rename from Where/WhereCore/Sources/DataIssueAlertReconciler.swift rename to Where/WhereCore/Sources/Reminders/DataIssueAlertReconciler.swift diff --git a/Where/WhereCore/Sources/ReminderReconciler.swift b/Where/WhereCore/Sources/Reminders/ReminderReconciler.swift similarity index 100% rename from Where/WhereCore/Sources/ReminderReconciler.swift rename to Where/WhereCore/Sources/Reminders/ReminderReconciler.swift diff --git a/Where/WhereCore/Sources/MissingDays.swift b/Where/WhereCore/Sources/Reporting/MissingDays.swift similarity index 100% rename from Where/WhereCore/Sources/MissingDays.swift rename to Where/WhereCore/Sources/Reporting/MissingDays.swift diff --git a/Where/WhereCore/Sources/ReportReader.swift b/Where/WhereCore/Sources/Reporting/ReportReader.swift similarity index 100% rename from Where/WhereCore/Sources/ReportReader.swift rename to Where/WhereCore/Sources/Reporting/ReportReader.swift diff --git a/Where/WhereCore/Sources/YearReport+MissingDays.swift b/Where/WhereCore/Sources/Reporting/YearReport+MissingDays.swift similarity index 100% rename from Where/WhereCore/Sources/YearReport+MissingDays.swift rename to Where/WhereCore/Sources/Reporting/YearReport+MissingDays.swift diff --git a/Where/WhereCore/Sources/YearReport.swift b/Where/WhereCore/Sources/Reporting/YearReport.swift similarity index 100% rename from Where/WhereCore/Sources/YearReport.swift rename to Where/WhereCore/Sources/Reporting/YearReport.swift diff --git a/Where/WhereCore/Sources/WidgetSnapshotPublisher.swift b/Where/WhereCore/Sources/Widgets/WidgetSnapshotPublisher.swift similarity index 100% rename from Where/WhereCore/Sources/WidgetSnapshotPublisher.swift rename to Where/WhereCore/Sources/Widgets/WidgetSnapshotPublisher.swift diff --git a/Where/WhereIntents/AGENTS.md b/Where/WhereIntents/AGENTS.md index f42d234a..e0602534 100644 --- a/Where/WhereIntents/AGENTS.md +++ b/Where/WhereIntents/AGENTS.md @@ -11,12 +11,11 @@ layering, localization, and the WhereUI duplicate-metadata rule). ## Scope & dependencies -- **App Intents + SwiftUI + `WhereCore` + `WhereUI`** (plus `RegionKit`, - `LogKit`). Library target in [`Package.swift`](../../Package.swift) - (`Where/WhereIntents/Sources`), linked by the **Where** app. It depends on - **WhereUI** for its snippet cards — mirroring **WhereWidgets** — so it must - **not** link `BroadwayUI`/`BroadwayCore` directly (a second copy would split - Broadway's type-keyed metadata; see the root AGENTS "Targets" note). +- Dependencies live in the root [`Package.swift`](../../Package.swift). It + depends on **WhereUI** for its snippet cards — mirroring **WhereWidgets** — + so it must **not** link `BroadwayUI`/`BroadwayCore` directly (a second copy + would split Broadway's type-keyed metadata; see the root AGENTS "Targets" + note). - Intents stay **thin adapters**: they resolve `WhereServices.forIntents()` and delegate to its collaborators. Domain rules, persistence, and aggregation stay in `WhereCore`; presentation (the card bodies) stays in `WhereUI`. Don't diff --git a/Where/WhereUI/AGENTS.md b/Where/WhereUI/AGENTS.md index 5f0816ef..c34cc78b 100644 --- a/Where/WhereUI/AGENTS.md +++ b/Where/WhereUI/AGENTS.md @@ -12,17 +12,15 @@ extended. ## Scope & dependencies -- **SwiftUI + `WhereCore`** (and `RegionKit` transitively) plus the Broadway - design-system stack (`BroadwayCore` / `BroadwayUI`). It is the presentation - layer — no domain rules, persistence, or store I/O here (see - [Layering](../AGENTS.md#layering)). -- Library target in [`Package.swift`](../../Package.swift) - (`Where/WhereUI/Sources`); consumed by the **Where** app and the - **WhereWidgets** extension. `WhereWidgets` gets Broadway *through* WhereUI (a - dynamic framework) and must **not** link `BroadwayUI` itself — a second copy - would split Broadway's type-keyed environment metadata and the stylesheet - would stop resolving across the boundary. This is why `whereBroadwayRoot()` - lives here rather than being called as `broadwayRoot` at each site. +- Presentation layer only — no domain rules, persistence, or store I/O here + (see [Layering](../AGENTS.md#layering)). Dependencies live in the root + [`Package.swift`](../../Package.swift). +- Consumers (`WhereWidgets`, `WhereIntents`) get Broadway *through* WhereUI (a + dynamic framework) and must **not** link `BroadwayUI`/`BroadwayCore` + themselves — a second copy would split Broadway's type-keyed environment + metadata and the stylesheet would stop resolving across the boundary. This + is why `whereBroadwayRoot()` lives here rather than being called as + `broadwayRoot` at each site. ## Design system — `WhereStylesheet` @@ -35,14 +33,12 @@ not back inline in a view. ### Using tokens -- Read them in a view with `@Environment(\.stylesheet) private var stylesheet`, - then reach for the token: `stylesheet.spacing.medium`, - `stylesheet.calendar.dayMinHeight`, `stylesheet.palette.splash.background`. -- The active sheet is seeded at the app root by `whereBroadwayRoot()` (in - `RootView`) and — because it has no other Broadway root — by the - **WhereWidgets** extension. With no root present (isolated previews, code off - the `View` tree) resolution falls back to `WhereStylesheet.default`; use that - static directly in layout helpers and tests. +- Read tokens in a view with `@Environment(\.stylesheet) private var + stylesheet` (e.g. `stylesheet.spacing.medium`). The active sheet is seeded + by `whereBroadwayRoot()` at the app root and in each Broadway-root-less + consumer (WhereWidgets); with no root present (isolated previews, code off + the `View` tree) resolution falls back to `WhereStylesheet.default` — use + that static directly in layout helpers and tests. - **Resolve a variant once.** For a component with more than one look, vend a resolved sub-spec and read it into a single property rather than branching through the body: `RegionSummaryCard` reads `stylesheet.card[variant]` into a