diff --git a/AGENTS.md b/AGENTS.md index 04f64f562..e121a8a0d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -53,8 +53,8 @@ generating; plain `./ide` fails fast pointing at it. The executables in the repo root are the dev scripts — `ide`, `test`, `swiftformat`, `sync-agents`, `profile`, `icons`, `flaky`, `simulator`, -`worktree`, `xcstrings`, `attribution`, `codex-watchdog` — and each takes -`--help`. Reach for one rather than +`worktree`, `xcstrings`, `attribution`, `codex-watchdog`, `tla-check` — and each +takes `--help`. Reach for one rather than hand-rolling its job: `test` is the only way tests should be run (see [Running tests](#running-tests)), and `icons`, `attribution`, and `simulator` in particular own state that is easy to corrupt by hand — `./simulator` owns a per-checkout device (see the @@ -507,8 +507,11 @@ flag is needed there. ## Running tests -**Use [`./test`](test)** — the only way to run tests. Never hand-roll `tuist -test` or `xcodebuild`. It runs the host-side backup-upgrader regression before +**Use [`./test`](test)** — the only way to run the iOS bundles. Never hand-roll +`tuist test` or `xcodebuild` for them. The one exception is the native-macOS +**Ledger-macOS-Tests** scheme, which `./test` does not know how to run at all; +the [`running-tests`](.agents/skills/running-tests/SKILL.md) skill carries its +invocation. Closing that gap is filed in [`TODOs.md`](TODOs.md). It runs the host-side backup-upgrader regression before selecting an iOS bundle, so tool-only changes remain covered by the same entry point. **Validate in proportion to risk:** run `./swiftformat --lint` when the changed files are in its scope, and run the @@ -612,6 +615,11 @@ external agent skills, which are gitignored and so absent from a bare checkout. - **Tuist** — `tuist test`, `tuist build`, and `./ide` (which generates the Xcode project) - iOS Simulator, and running the **Where** app +- **Anything needing a Swift toolchain** — the VM ships none, so `swift run + bumper` (the architecture lint) and `./xcstrings` (a `#!/usr/bin/swift` + script) both fail here even though neither needs Xcode. Diagnostic signature + for the latter: ``mise ERROR "./xcstrings" couldn't exec process: No such file + or directory``. - Anything else needing Xcode These are limits of the **VM**, not of cloud agents generally: a remote-control diff --git a/Ledger/LedgerCore/AGENTS.md b/Ledger/LedgerCore/AGENTS.md index 2971ca276..a2892496c 100644 --- a/Ledger/LedgerCore/AGENTS.md +++ b/Ledger/LedgerCore/AGENTS.md @@ -90,8 +90,12 @@ build system, formatting, and global conventions. Read that first. ## Testing -Swift Testing in [`Tests/`](Tests), hostless on macOS (`tuist test -LedgerCoreTests -- -destination 'platform=macOS'`). Shared fixtures live in +Swift Testing in [`Tests/`](Tests), hostless on macOS. Run it through the +scheme, not the bundle — `tuist test Ledger-macOS-Tests --no-selective-testing +-- -destination 'platform=macOS'`, as the +[`running-tests`](../../.agents/skills/running-tests/SKILL.md) skill spells out. +This is the repo's one sanctioned `tuist test`: `./test` covers only the iOS +bundles and cannot run this one. Shared fixtures live in [`LedgerCoreTestSupport.swift`](Tests/LedgerCoreTestSupport.swift). The network, token-source, and Keychain seams use the module's `@_spi(Testing)` DEBUG doubles (`ScriptedDashboardProvider`, `StubTokenSource`, `InMemoryKeychainStore`); the diff --git a/Ledger/TODOs.md b/Ledger/TODOs.md new file mode 100644 index 000000000..bd9ab6b84 --- /dev/null +++ b/Ledger/TODOs.md @@ -0,0 +1,18 @@ +# Ledger todos + +The backlog for the Ledger menu bar app and `LedgerCore`. Ledger is native +macOS and sits outside the Where module graph, so the Bumper Bowling +architecture lint does not cover it (`BumperBowling.swift` includes only Where +paths) — the conventions below are enforced by review rather than by lint. + +The item format and the placement rule live in the root +[`TODOs.md`](../TODOs.md); raw notes go in [`INBOX.md`](../INBOX.md), not here. + +# Open issues + +## P2s (Nice to have) +- fix(LedgerCore) [quick-win]: `LedgerServices` resolves its calendar from the device (`LedgerServices.swift:157`, `calendar: Calendar = .current` on the `@_spi(Testing)` init) and uses it for the today/this-week spend deltas (`:303-307`), so on a non-Gregorian system calendar the window boundaries `SpendHistory` differences against move — the same defect class Where forbids outright, and the parameter default also violates the repo's "avoid parameter defaults on Core APIs" rule, since the composition root already knows the value. Inject an explicit Gregorian calendar with the current time zone from the app, and pass it in tests rather than relying on the default. Lower severity than Where's equivalent: this shifts a spend window rather than corrupting stored day identity, and no value is persisted against it. (audit 2026-08-09) +- test(LedgerCore) [quick-win]: Three implementation files have no namesake test — `LedgerLog.swift`, `LedgerSettings.swift`, and `SpendSnapshot.swift`. Each is exercised indirectly through `LedgerServicesTests`, so this is 1:1-convention debt rather than untested behavior; close it as those files change rather than in one pass. The rest of the module is genuinely well covered (13 test files over 16 sources, including the API, Keychain, token-source, and history seams). (audit 2026-08-09) +- test(Ledger) [needs-design]: The `Ledger` app target ships no test bundle, so the eight sources in the SwiftUI/AppKit shell — `MenuBarLabel`, `SpendView`, `SettingsView`, `LedgerSession`, `CurrencyFormat`, `WindowVisibilityReader` — are compile-only in CI (`Ledger-macOS-Tests` builds the app but runs only `LedgerCoreTests`, `Project.swift:703-707`). This matches how the Where extension targets are treated and is documented in [`Ledger/AGENTS.md`](Ledger/AGENTS.md), so it is a deliberate gap rather than an oversight; the decision worth making is whether `CurrencyFormat` and the menu-bar label's formatting deserve a hostless bundle of their own, since they are pure value transforms that a test could pin cheaply. (audit 2026-08-09) + +# Completed issues diff --git a/MODULE_AUDIT.md b/MODULE_AUDIT.md index f15b4704c..c76c197a8 100644 --- a/MODULE_AUDIT.md +++ b/MODULE_AUDIT.md @@ -1,10 +1,10 @@ # Swift Module Audit Report -Read-only review of all **14 SPM library targets**, **6 Tuist app/extension targets**, and the repo-owned **Bumper Bowling** architecture rules (~359 source / ~198 test Swift files across shipped targets, plus 2 unwired prototype sources). No code was changed. +Read-only review of all **20 SPM library targets**, **7 Tuist app/extension targets**, **25 test bundles**, and the repo-owned **Bumper Bowling** architecture rules (623 source / 337 test / 40 image-snapshot Swift files across shipped targets, plus 2 unwired prototype sources). No code was changed. -**Date:** July 26, 2026 -**Method:** Read-only verification of every open July 19 finding against current source; file-count refresh; new-surface review of the week's landings (Periscope migration #94, Settings drill-in #111, developer HUD #115, navigation restructure #119, log-viewer tooling #107, String Catalog symbols #124, Gregorian calendars `fe99dde`, previews `52f0136`, Bumper Bowling #127, catalog serialization #135). -**Prior audit:** July 19, 2026 (~308 source / ~189 test). +**Date:** August 9, 2026 +**Method:** Read-only verification of every open finding in all 13 `TODOs.md` files against current source, module-by-module, with each citation re-derived; file-count refresh; new-surface review of two weeks of landings; and a pass over the tree against the repo's own written rules (per-module docs, agent-file sync, CI scheme membership, the WhereUI double-linking rule, backlog format). +**Prior audit:** July 26, 2026 (~359 source / ~198 test). **This is a two-week diff, not a one-week one:** the August 2 pass was opened as PR #171 and closed unmerged, so nothing it found reached `main` and its work is re-derived here. > **This report carries no actionable items.** Every finding it describes is filed > in a `TODOs.md`; the root [`TODOs.md`](TODOs.md) owns the item format and says @@ -17,100 +17,79 @@ Read-only review of all **14 SPM library targets**, **6 Tuist app/extension targ ## Executive summary -What this pass turned up, before the findings were filed: - -| Severity | Count | -|----------|------:| -| Critical | 0 | -| High | 5 | -| Medium | 44 | -| Low | 48 | -| **Total** | **97** | - -| Category | Count | -|----------|------:| -| bug | 21 | -| test | 27 | -| convention | 24 | -| performance | 7 | -| duplication | 3 | -| localization | 4 | -| docs | 8 | -| design | 3 | - -**Overall:** A heavy week of landings. `LogKit` and `LogViewerUI` are **gone** — Periscope replaced them (#94) — so the target count drops to 14 SPM libraries while WhereUI grew 84 → 113 sources and WhereCore 70 → 87. Several long-standing findings closed for real: the Where app's `README.md`, the `.undetermined` launch-reason state machine, `SharedItemLoader` warning logs, `#Preview` coverage across WhereUI/WhereWidgets, and the String Catalog symbol migration (a typo'd key is now a compile error). The three **high** findings carried from July 19 are all still open — daily-summary staleness, the WhereUI tracking-toggle race, and the LifecycleKit terminal-phase race — and two new **high** ones landed: `CalendarDay.displayDate` resolves day labels through `Calendar.current`, and the brand-new `where.gregorian_calendar` Bumper rule that exists to catch exactly that is blind to the form the drift actually takes. +**The tree nearly doubled in two weeks: 359 → 623 sources, 198 → 337 tests, 14 → 20 SPM libraries.** Three areas are new since the last merged audit — **Ledger** (a native-macOS menu-bar app for Cursor spend, with its own CI job), **Flyover** (the developer screen browser), and **LifecycleKitUI** — and `SwiftDataInspector` became **Inspector**. WhereUI alone went 113 → 224 sources and WhereCore 87 → 118. + +What the verification pass found, in order of how much it should change your reading of the backlog: + +- **Five WhereUI items closed for real**, four of them from the broken-snapshots cluster that has sat open since PR #101. Two were fixed as filed, one was fixed by redesign, and one is *obsolete* rather than fixed — PR #200 deleted the code it described. The cluster is down from eight sub-items to three. +- **Three items turned out to describe code that doesn't work the way they claim.** The span-record-modeling P0 says `spanID`/`spanExit` are "bolted onto every `LogRecord`"; they're computed downcasts, and only `bypassesFloors` is stored. That changes what the item is asking for, so it's recorded as a dated correction inside the item rather than edited away. +- **Nothing in Periscope, Broadway, SnapshotKit, SnapshotKitTesting, CreditKit, JournalKit, LifecycleKit, or StuffCore shipped at all.** Their 24 open items are all still open. Periscope's three durability gaps remain the oldest work in the repo. +- **Two tracked debts grew while nobody was looking.** The PeriscopeTools hosting-smoke-test count went from 18 across 9 files to **20 across 10** (PR #152 added a file while the conversion was backlogged), and the WhereCore namesake-test gap went from 28 of 87 files to **59 of 118**. +- **Ledger passed its first audit** with three modest P2s and no defects of substance — notable for a brand-new network-facing app outside Bumper's scope. +- **Every measured number in the previous audit was stale**, and several in module docs were too. Those are corrected or dated with tripwires; the ones this pass cannot re-measure say so rather than being restated. --- -## Top 10 highest-impact findings +## Top findings Pointers only — each one's evidence and suggested fix live in the linked file. -| # | Sev | Module | Issue | Filed in | -|---|-----|--------|-------|----------| -| 1 | **high** | Bumper Bowling | `where.gregorian_calendar` matches only an explicit `Calendar.current` base, so the rule is green while production sites drift | [`TODOs.md`](TODOs.md) P0 | -| 2 | **high** | WhereUI | `CalendarDay.displayDate` resolves through `Calendar.current`, so day labels on a non-Gregorian device render a date ~543 years off | [`Where/TODOs.md`](Where/TODOs.md) P1 | -| 3 | **high** | WhereCore | `DailySummaryReconciler.reconcile()` is absent from the post-day-change fan-out — the notification body stays stale until a foreground re-`configure` | [`Where/TODOs.md`](Where/TODOs.md) P0 | -| 4 | **high** | WhereUI | Tracking toggle race — `trackingEnabled`'s setter spawns unserialized `Task`s | [`Where/TODOs.md`](Where/TODOs.md) P1 | -| 5 | **high** | LifecycleKit | Cancel during the *last* step's `minVisible` hold isn't observed, so a superseded drive can set `phase = .ready` | [`Shared/LifecycleKit/TODOs.md`](Shared/LifecycleKit/TODOs.md) P0 | -| 6 | **medium** | WhereCore | `setPrimaryRegions(_:)` commits atomically but skips `reconcileAfterDayDataChange()` | [`Where/TODOs.md`](Where/TODOs.md) P1 | -| 7 | **medium** | WhereCore | `setTrackedRegion(false)` hard-deletes the row; the shipped picker now reaches it, so past-year re-attribution risk is live | [`Where/TODOs.md`](Where/TODOs.md) P1 | -| 8 | **medium** | PeriscopeCore | Orphan sweep treats an undecodable `SpanBegan` as an orphan-close candidate, silently overriding `survivesRelaunch` | [`Shared/Periscope/TODOs.md`](Shared/Periscope/TODOs.md) P1 | -| 9 | **medium** | WhereUI | Load-state UI duplicated across four views; `PresenceTimelineList` renders the *empty* state while the year is still loading | [`Where/TODOs.md`](Where/TODOs.md) P1 | -| 10 | **medium** | BroadwayCatalog | The showcase app never seeds a Broadway root, and its test bundle is an empty `struct` wired into the CI scheme | [`Shared/Broadway/TODOs.md`](Shared/Broadway/TODOs.md) P1 | +| # | Module | Issue | Filed in | +|---|--------|-------|----------| +| 1 | Bumper Bowling | `where.gregorian_calendar` matches only an explicit `Calendar` base, so it reports none of the 12 implicit `.current` sites — and its own mutation test only feeds it the explicit form, which is why it has survived three audits | [`TODOs.md`](TODOs.md) P0 | +| 2 | WhereCore | `DailySummaryReconciler.reconcile()` is absent from the post-day-change fan-out — the daily notification body stays stale until a foreground re-`configure` | [`Where/TODOs.md`](Where/TODOs.md) P0 | +| 3 | WhereUI | `CalendarDay.displayDate` resolves through `Calendar.current`; four production sites remain, and every day label flows through them | [`Where/TODOs.md`](Where/TODOs.md) P1 | +| 4 | PeriscopeCore | Records emitted before the store attaches reach neither the store nor the journal — the durable log has a hole at every launch | [`Shared/Periscope/TODOs.md`](Shared/Periscope/TODOs.md) P0 | +| 5 | PeriscopeCore | `survivesRelaunch` is honored by the sweep but nothing re-seeds surviving spans, so `end(for:)` warns "without a matching begin" in the new process | [`Shared/Periscope/TODOs.md`](Shared/Periscope/TODOs.md) P0 | +| 6 | WhereUI | Notification authorization is requested unprompted during launch, and all three preferences default to `true` on a fresh install | [`Where/TODOs.md`](Where/TODOs.md) P1 | +| 7 | WhereCore | Untracking a region hard-deletes the row, so re-aggregating a past year re-attributes its GPS days to `.other`; both shipped pickers reach it | [`Where/TODOs.md`](Where/TODOs.md) P1 | +| 8 | Bumper Bowling | `duplicate_ownership` and `declared_dependency_cycle` have no mutation test, so neither has been shown to fail on a violating tree | [`TODOs.md`](TODOs.md) P1 | +| 9 | SnapshotKit | A case's content is built once and re-hosted for every configuration, while the type's doc comment tells authors each access is independent | [`Shared/SnapshotKit/TODOs.md`](Shared/SnapshotKit/TODOs.md) P1 | +| 10 | PeriscopeTools | 20 tests across 10 files assert only "the hosted view reached a window", and the image bundle that should replace them still holds one file | [`Shared/Periscope/TODOs.md`](Shared/Periscope/TODOs.md) P2 | --- ## Cross-cutting themes -### The Gregorian rule has a blind spot, and the catalog says otherwise - -`.bumper/RULES.md` states the tree "intentionally contains three violations" of `where.gregorian_calendar`, left visible so the live lint demonstrates enforcement. It doesn't: the rule filters `MemberAccessExprSyntax` on `base == "Calendar"`, which matches the spelled-out `Calendar.current` but **not** the implicit-member form (`calendar: Calendar = .current`, `startOfDay(in: .current)`) — and after `fe99dde` the implicit form is the *only* one left. CI runs `bumper lint` as a hard `severity: .error` gate and is green, which confirms it: the rule reports nothing while seven production sites drift. The same paragraph's claim about preview-coverage violations is also stale (`52f0136` closed those). A rule that reads as enforced but enforces nothing is worse than a documented convention, because it stops anyone from looking. - -### Reconciliation: same two holes, one now user-reachable - -`reconcileAfterDayDataChange()` still fans out to issue state and widgets only. **Daily summary** remains outside it, and **`setPrimaryRegions(_:)`** still commits without calling it. Related and newly urgent: untracking a region hard-deletes its row, and the shipped onboarding picker plus the Settings region editor both route into that path, so the past-year re-attribution risk the `SwiftDataStore` TODO describes is now something a user can trigger. - -### Presentation-layer calendar drift outlived the fix - -`fe99dde` moved the view call sites onto explicit Gregorian calendars, but the drift relocated into shared helpers: `CalendarDay.displayDate` hardcodes `.current`, and `DateRangeFormatting.abbreviated` / `PresenceTimeline.stints` *default* to it — with `PresenceTimelineList` not passing the report's calendar. Because these are the helpers every day label flows through, one line reaches the relabel, logged-days, resolution, and region drill-in screens. +The synthesis across items that no single item shows. -### The navigation restructure moved the duplication, not the shape +### A lint rule and its test can fail together -Locations / Your Year / Settings replaced the old four-tab shell, so the `PrimaryView` / `SecondaryView` / `CalendarView` load-state triplicate is gone — but the same `YearReportModel.loadState` gate is now copy-pasted across `LocationsView`, `ElsewhereView`, `ResolutionView`, and `CalendarContentView`, and `PresenceTimelineList` skipped the gate entirely (it shows "no stays" during load). A `ReportLoadGate` would now save four sites rather than three. +The `where.gregorian_calendar` blind spot has now survived three audits, and this pass found why: the rule filters on an explicit `Calendar` base, and its mutation test only ever feeds it an explicit `Calendar.current`. The test passes for precisely the reason the rule fails, so the pair is self-consistent and green while 12 sites drift. The same shape appears in the two untested graph assertions (finding 8) — an assertion nobody has watched fail is indistinguishable from one that passes everything. This is the strongest argument in the backlog for the repo's own rule that a lint rule and its mutation test land together. -### Periscope's durability gaps are the oldest open work in the repo +### Re-recording a reference does not fix what it pins -Three items — `survivesRelaunch` resume mechanics, journaling the pre-store-attach window, and multi-process journal coordination — remain P0/P1 in `Shared/Periscope/TODOs.md` and are all confirmed unimplemented. The store-side half of relaunch policy landed (the sweep leaves surviving spans open), but nothing re-seeds `Periscope.openSpans`, so `end(for:)` in the new process still warns "without a matching begin". +PR #196 re-recorded much of the WhereUI suite for intrinsic-height captures. That genuinely closed one broken-snapshot item — the blank VoiceOver calendar captures went from 66 KB of white to 3.2 MB of content — but for three others it re-recorded the defect at a new size, so the reference still faithfully pins a truncated day grid, an overflowing picker, and a misplaced badge. An image suite makes rendering visible; it does not make it correct, and a re-record in the changelog is not evidence a rendering bug closed. The remaining sub-items now carry that warning explicitly. -### PeriscopeTools grew fast; its live models rebuild from scratch +### The reconciliation fan-out has the same two holes it had in July -+9 sources / +9 tests this week (span tree, hierarchy, span history, density, Broadway stylesheet). The incremental **fetch** landed (`LogQuery.afterSequence`), but `SpanTreeModel.load` / `LogHierarchyModel.load` still rebuild the whole forest from all accumulated events on every `changes()` ping, `LogInspectorModel` re-queries full subtrees, and the new drill-ins re-read row density from `.standard` rather than the injectable `defaults` the viewer threads through. +`reconcileAfterDayDataChange()` still reaches issue state and widgets only. **Daily summary** remains outside it and **`setPrimaryRegions(_:)`** still commits without calling it — unchanged across two weeks in which PR #160 rewrote much of the surrounding recording stack and PR #209 rewrote every fetch beneath it. What did change is the diagnosis: the single-sample and bulk ingest paths were closed in August, and `reset()` does reconcile summary, so the gap is now specifically the *local* write paths rather than a general absence. `Where/Specifications/PostWriteReconcile` documents the canonical ordering and deliberately excludes summary, which means the spec cannot be used as evidence the hole is closed. -### Extension/app targets still defer tests to libraries +### Growth outran the tests, and the docs recorded the old ratio -WhereWidgets (7/0), WhereShareExtension (5/0), and RegionViewer (1/0) ship no test bundle by design; BroadwayCatalog ships an empty one. `ShareEvidenceModel.buildPendingEvidence()` and `WhereWidgetProvider`'s midnight timeline policy remain the two gaps that are worth closing regardless of the pattern. +WhereUI went 113 → 224 sources against 36 → 90 tests; WhereCore 87 → 118 against 58 → 74. The namesake-test debt therefore grew in absolute terms (28 → 59 files in WhereCore) even though real coverage was added. Three screens have no image coverage, and two of them have had none since PR #111 — prior audits missed them because they checked what was *new*, not what was uncovered. The lesson for this report: an inventory that only diffs the week can't see standing debt. -### Localization architecture is now compiler-enforced +### Documentation drifts fastest where it quotes a number -The String Catalog symbol migration (#124) is complete and the hand-maintained key facades are gone, so a removed key breaks the build. Remaining slips are individual, not architectural: a raw `String(localized: "region.other")` in RegionKit, a hardcoded caption in `IntentSnippets`, the parallel `share.form.*` / `evidence.form.*` namespaces, and four auto-extracted literals — three in Where's catalogs, one in LifecycleKit's. +Nine measured claims in the previous audit were stale, and four more were in module docs: two reference counts in `SnapshotKitTesting/AGENTS.md`, a coverage claim in `RegionKit/README.md` that named tests which don't exist, a geometry layout in `RegionViewer/README.md` describing the pipeline's build-time input as if it were the shipped bundle, and a `tuist test` command in `LedgerCore/AGENTS.md` naming the bundle instead of the scheme. **A false claim in an `AGENTS.md` is the worst case** — the RegionKit one told the next agent that GeoJSON decoding was tested, which is an argument against writing the missing tests. All are corrected in this pass; the two that can only be re-measured on macOS are dated with tripwires instead. -### Infrastructure that is genuinely done +### New code is landing clean; the debt is old -Periscope replaced LogKit/LogViewerUI outright; `.undetermined` replaced the cold-launch guess with a state that can't lie; `#Preview` coverage is complete across WhereUI/WhereWidgets; `@_spi(Testing)` is the norm for test seams (the only `…ForTesting` API is itself behind it); the SwiftData browser shipped into Settings → Developer; String Catalogs are serialized the way Xcode writes them and linted (`./xcstrings`); and `./simulator` now resolves destinations by UDID for `profile` / `flaky` / CI. +Across two weeks and ~264 new source files, the verification pass found few defects in new code: one honest gap in `WidgetSnapshotStore.read()`, an accessibility gap in the evidence discovery panels, three uncovered screens, and an untested Spotlight indexer. Ledger arrived with a typed error path, a single `LoadState`, no secrets in its JSON, and near-1:1 tests. Four candidate findings were investigated and rejected as false alarms — including one that looked like a shipped bug (`\.isCapturingSnapshot` set to `true` in WhereUI) until it turned out to be inside `#if DEBUG`. The backlog's weight is in items filed in July and earlier, not in what shipped this fortnight. --- ## Per-module notes -What each module was checked for and found clean, plus the trade-offs this pass -accepted as deliberate. Open work is in the linked `TODOs.md`. +What each module was checked for and found clean, plus the trade-offs this pass accepted as deliberate. Open work is in the linked `TODOs.md`. ### Bumper Bowling — architecture lint -The repo-owned rule set (`BumperBowling.swift`, `.bumper/Sources`, catalog in `.bumper/RULES.md`) covers **Where production sources only**: layer boundaries and forbidden imports, graph integrity, production store opening, checked-concurrency escape hatches, composition ownership (`WhereServices`, live `LocationSource`), the Gregorian calendar, the `store.perform` transaction boundary, `AppShortcutsProvider` ownership, the logging facade and logging-type placement, and `#Preview` coverage. Mutation tests live in `.bumper/Tests`; CI runs `config`, `test`, and `lint --timings` with every rule at `severity: .error`. +Covers **Where production sources only** (`BumperBowling.swift:15-23`): layer boundaries and forbidden imports, graph integrity, production store opening, checked-concurrency escape hatches, composition ownership, the Gregorian calendar, the `store.perform` boundary, `AppShortcutsProvider` ownership, the logging facade and logging-type placement, and `#Preview` coverage. CI runs `config`, `test`, and `lint --timings` as hard gates with every rule at `severity: .error`. -**Verified OK:** rule IDs and scopes in `RULES.md` match `WhereProjectRules.swift`; the component graph matches `BumperBowling.swift` / `WhereArchitecture.swift`; Broadway is forbidden on WhereIntents/WhereWidgets via `forbidden_import`, matching `Project.swift`. +**Verified OK:** all ten `where.*` rules in `WhereProjectRules.swift` appear in `.bumper/RULES.md` and each has a mutation test; `component_boundary` and `forbidden_import` are mutation-tested; the stale "three intentional calendar violations" claim is gone from the catalog. + +**Not covered, by design:** everything under `Shared/`, all of `Ledger/`, test bundles, and `Where/Specifications/`. Three of the four modules added since July — Flyover, Inspector, LifecycleKitUI, and both Ledger targets — therefore landed with zero architecture lint. Worth knowing when reading a green lint as a whole-repo signal. **Files:** 4 rule/test sources · RULES.md ✓ · Open: [`TODOs.md`](TODOs.md) @@ -118,63 +97,81 @@ The repo-owned rule set (`BumperBowling.swift`, `.bumper/Sources`, catalog in `. ### WhereCore -**Verified OK:** backup import → full fan-out via `onImport`; summary format args (guarded by `summaryBodyContainsNoFormatPlaceholders`); `BackupError` localization; drain-only ingest skipping the full reminder reconcile; no raw-string/`os.Logger` logging left; no PII in `.public` events; `RecentActivitySummarizer`'s typed unavailability and segment cap. +**Verified OK:** generation-scoped fetches cover all seven scoped entity types and preserve legacy `nil == .initial` rows, with rotation and mixed-generation reads tested; the crash-safe outbox journals whole snapshots and recovers a torn tail; automatic-recording consent is installation-local with no cross-device authority timeline; an unreadable backup asset now fails the import instead of committing metadata-only evidence; `DeviceRecordingController`'s import-recovery `catch` logs a typed error, revokes authorization, publishes `.unavailable`, and flags reconciliation — the sanctioned "log + honest state" path, not a swallowed error. -**Files:** 87 source / 58 test · README ✓ · AGENTS ✓ · Open: [`Where/TODOs.md`](Where/TODOs.md) +**Files:** 118 source / 74 test · README ✓ · AGENTS ✓ · Open: [`Where/TODOs.md`](Where/TODOs.md) --- ### WhereUI -**Verified OK:** no closure `Binding(get:set:)` anywhere in the module (`SaveErrorAlertState`, `AddEvidenceModel`, `AppIconModel` expose computed `get`/`set`); every load-state `switch` enumerates its cases; `MainTabs` drives `YearReportModel.activate()` / `deactivate()` off `scenePhase`; every previewable `View`/`Widget` ships an in-file `#Preview`; `README.md` and `AGENTS.md` are current on the three-tab shape. +**Verified OK:** no `Calendar.current` outside four helper defaults and eight DEBUG fixtures (`calendar.timeZone = .current` on an explicit Gregorian calendar is the correct pattern and is not drift); the only production write of `\.isCapturingSnapshot` is inside `#if DEBUG`; the capture-flag reads that remain are the documented stand-in carve-outs; every new screen this window except three ships image coverage; the automatic-recording binding is serialized behind a monotonic intent sequence and the Devices UI renders remote status read-only. -**Files:** 113 source / 36 test · README ✓ · AGENTS ✓ · Open: [`Where/TODOs.md`](Where/TODOs.md) +**Files:** 224 source / 90 test / 37 image-snapshot · README ✓ · AGENTS ✓ · Open: [`Where/TODOs.md`](Where/TODOs.md) --- -### LifecycleKit +### PeriscopeCore, PeriscopeUI, PeriscopeTools + +**Verified OK:** the Broadway dependency genuinely stops at PeriscopeTools — no `BroadwayCore`/`BroadwayUI` import in Core or UI, and `Package.swift:94-99` lists it on Tools alone; no test touches `Periscope.shared`; span-pair integrity holds across floors, redaction, and drop pressure; the relaunch sweep leaves surviving spans open and is tested. A concurrency-focused review of the new ambient and build-attribution code found no defects and rejected four candidates. -**Verified OK:** cancel-and-drain no longer waits out the full `minVisible` window; duplicate step-ID `precondition`; localized `LifecycleFailureView`; background *and* `.undetermined` promotion container tests. The `.undetermined` state machine (#109) holds up: `completedStepIDs` records only steps that ran to completion, so a promotion re-drive skips finished work while still running newly-applicable steps, and promotion/teardown both funnel through the same cancel-and-drain. +**Accepted:** the journal append sits outside the pipeline lock deliberately — the sequence in each entry makes recovery order-safe. -**Files:** 9 source / 11 test · README ✓ · AGENTS ✓ · Open: [`Shared/LifecycleKit/TODOs.md`](Shared/LifecycleKit/TODOs.md) +**Files:** PeriscopeCore 37/33 · PeriscopeUI 1/2 · PeriscopeTools 27/27 (+1 image) · README ✓ · AGENTS ✓ · Open: [`Shared/Periscope/TODOs.md`](Shared/Periscope/TODOs.md) --- -### SwiftDataInspector +### Ledger, LedgerCore — first audit -**Verified OK:** pagination + lazy rendering with regression tests; relationship resolution for materialized models. +Native macOS, outside the Where graph and outside Bumper's scope, in its own `Ledger-macOS-Tests` CI job. -**Accepted:** `try?` on fetches yields empty rows/counts (documented DEBUG degradation); a bare `default:` in `defaultFormat` over `Any` (open-type dispatch). +**Verified OK:** one `LoadState` enum rather than parallel loading/error/value fields; transport, HTTP, and decode failures become a typed `DashboardError` mapped to a `LoadError` and logged, with 401 distinguished as an expired session; a failed refresh keeps the last snapshot and surfaces staleness rather than blanking it; only the newest refresh may mutate state, via a request-generation guard that also protects recorded history; `state.vscdb` is opened `SQLITE_OPEN_READONLY`; no secrets in its JSON (pasted token in the Keychain, auto-token in Cursor's own store); no token value reaches a log string; `SecureField` for the token draft, cleared after save; Swift Testing throughout with 13 test files over 16 sources; docs correctly credit PeriscopeCore rather than the deleted LogKit. -**Files:** 13 source / 1 test · README ✓ · AGENTS ✓ · Open: [`Shared/SwiftDataInspector/TODOs.md`](Shared/SwiftDataInspector/TODOs.md) +**Files:** LedgerCore 16/14 · Ledger 8/0 · README ✓ · AGENTS ✓ (leaf modules; the **group** folder is missing both — filed) · Open: [`Ledger/TODOs.md`](Ledger/TODOs.md) --- -### PeriscopeCore, PeriscopeUI, PeriscopeTools +### Flyover — first audit + +**Verified OK:** no production `try?` or empty catch; the `default:` uses are dictionary subscript defaults, not enum switches; `README.md`/`AGENTS.md` match the code on the lazy catalog, the Broadway root, and the canvas cap; PR #166's snapshot stabilization left no `withKnownIssue` behind — it uses a settle floor. + +**Accepted:** `FlyoverScreenContent.swift:47` sets `\.isCapturingSnapshot` for overview frames, so descendants render their inert capture stand-ins. Deliberate for a screen browser whose whole job is deterministic miniatures, and the only such production write in the repo. -**Verified OK:** Broadway does not leak below PeriscopeTools; no test touches `Periscope.shared`; `@_spi(Testing)` used for injection hooks; span-pair floors, rollback-on-failed-save, and the seeded lifecycle fuzz all still in place. PeriscopeUI is a thin DEBUG bridge with nothing outstanding. +**Files:** 50 source / 12 test / 1 image · README ✓ · AGENTS ✓ · Open: [`Shared/Flyover/TODOs.md`](Shared/Flyover/TODOs.md) -**Files:** PeriscopeCore 35/31 · PeriscopeUI 1/2 · PeriscopeTools 24/22 · README ✓ · AGENTS ✓ · Open: [`Shared/Periscope/TODOs.md`](Shared/Periscope/TODOs.md) +--- + +### Inspector + +**Verified OK:** `AGENTS.md` invariants (filesystem protection, the pagination model, `Sendable` snapshots) match the code; the browsing test monolith was split by concern in July. + +**Accepted:** the dark SwiftData capture stays quarantined under `withKnownIssue(isIntermittent:)` — one of only two quarantines in the repo, both tracked. + +**Files:** 23 source / 14 test / 1 image · README ✓ · AGENTS ✓ · Open: [`Shared/Inspector/TODOs.md`](Shared/Inspector/TODOs.md) --- -### JournalKit, StuffCore, TestHostSupport, StuffTestHost +### SnapshotKit & SnapshotKitTesting + +**Verified OK:** the framework halves stay split as documented (shippable matrix vs test-only pipeline); each image bundle lists only `SnapshotKitTesting` in `extraPackageProducts`; the reporting channels no longer fabricate rows into `--review`/`--timings`; the non-converging `fullContent` measurement now fails rather than blessing an arbitrary height. -**JournalKit:** strong fuzz/truncation coverage. **Files:** 2/3 · README ✓ · AGENTS ✓ · Open: [`Shared/JournalKit/TODOs.md`](Shared/JournalKit/TODOs.md) +**Files:** SnapshotKit 8/3 · SnapshotKitTesting 14/11 · README ✓ · AGENTS ✓ · Open: [`Shared/SnapshotKit/TODOs.md`](Shared/SnapshotKit/TODOs.md), [`Shared/SnapshotKitTesting/TODOs.md`](Shared/SnapshotKitTesting/TODOs.md) -**StuffCore:** intentional scaffold. **Files:** 1/1 · README ✓ · AGENTS ✓ · Open: [`Shared/StuffCore/TODOs.md`](Shared/StuffCore/TODOs.md) +--- -**TestHostSupport:** dependency-free UIKit helpers; no dedicated bundle by design (exercised via hosted bundles), nothing open. **Files:** 1/0 · README ✓ · AGENTS ✓ +### LifecycleKit & LifecycleKitUI -**StuffTestHost:** the WhereCore-always-embedded trade-off is documented and verified load-bearing in `Project.swift:256`; the smoke test lives in `LifecycleKitTests`. **Files:** 2/0 · README ✓ · AGENTS ✓ · Open: [`TODOs.md`](TODOs.md) (both items reach the root Tuist manifest) +**Verified OK:** the typed `LaunchPlan` still makes a mis-ordered launch a compile error; the terminal-phase race is closed by the typed-engine rewrite (the engine holds nothing, and a superseded walk publishes nothing); `completedStepIDs` keeps a promotion re-drive from re-running finished work; the suites poll predicates rather than sleeping. + +**Files:** LifecycleKit 8/10 · LifecycleKitUI 5/3 · README ✓ · AGENTS ✓ · Open: [`Shared/LifecycleKit/TODOs.md`](Shared/LifecycleKit/TODOs.md) (LifecycleKitUI's items live in LifecycleKit's file by design) --- -### BroadwayCore, BroadwayUI, BroadwayCatalog +### Broadway (BroadwayCore, BroadwayUI, BroadwayCatalog) -**Verified OK:** stylesheet/trait/cycle behavior well tested; trait registration pairs with teardown. +**Verified OK:** stylesheet, trait, and cycle behavior are well tested; trait registration pairs with teardown. -**Accepted:** a bare `default:` mapping unknown `UIContentSizeCategory` to `.large` (`BTraits+Values.swift:125`, a deliberate fallback); hardcoded English in the catalog app (internal showcase). +**Accepted:** a bare `default:` mapping an unknown `UIContentSizeCategory` to `.large`; hardcoded English in the internal showcase app. **Files:** BroadwayCore 17/10 · BroadwayUI 6/4 · BroadwayCatalog 2/1 · README ✓ · AGENTS ✓ · Open: [`Shared/Broadway/TODOs.md`](Shared/Broadway/TODOs.md) @@ -182,47 +179,43 @@ The repo-owned rule set (`BumperBowling.swift`, `.bumper/Sources`, catalog in `. ### RegionKit & RegionViewer -**Verified OK:** the per-region catalog drives `RegionStyle`, the pickers, and the App Intents `RegionEntity` with no `Region` enum left to extend. RegionViewer ships no test bundle by design. +**Verified OK:** the per-region catalog drives `RegionStyle`, the pickers, and the App Intents `RegionEntity` with no `Region` enum to extend; `buildSourceOutlines()` decodes the 54 bundled per-region files, so RegionViewer's Source mode shows what ships. -**Files:** RegionKit 13/8 · RegionViewer 1/0 · README ✓ · AGENTS ✓ · Open: [`Where/TODOs.md`](Where/TODOs.md) +**Files:** RegionKit 15/10 · RegionViewer 1/0 · README ✓ · AGENTS ✓ · Open: [`Where/TODOs.md`](Where/TODOs.md) --- -### WhereIntents +### WhereIntents, WhereWidgets, WhereShareExtension, Where app -**Verified OK:** reader/writer seams well tested (`WhereIntentReaderTests`, `WhereIntentWriterTests`); `IntentServices` handoff still covered by `IntentServicesTests` (install/park/cancel/replace) with no self-creating fallback; no Broadway double-link. +**Verified OK:** the reader/writer seams are well covered; the `IntentServices` handoff has no self-creating fallback; `@unknown default:` on widget-family switches; the post-midnight stale snapshot is documented as intentional degradation in the provider and both docs; `WhereTests` pins `.undetermined` as the launch reason under the UIScene lifecycle. -**Accepted:** the per-intent `perform()` glue is untested because `@Dependency` traps outside the perform flow — the open item is to extract a seam or say so in `README.md`. +**Accepted:** the per-intent `perform()` glue is untested because `@Dependency` traps outside the perform flow — now explained in `WhereIntents/AGENTS.md`, though the README hasn't caught up (filed). Neither extension ships a test bundle, by design. -**Files:** 17/9 · README ✓ · AGENTS ✓ · Open: [`Where/TODOs.md`](Where/TODOs.md) +**Files:** WhereIntents 17/10 · WhereWidgets 7/0 · WhereShareExtension 5/0 · Where 6/2 · README ✓ · AGENTS ✓ · Open: [`Where/TODOs.md`](Where/TODOs.md) --- -### WhereWidgets & WhereShareExtension - -**Verified OK:** `SharedItemLoader` logs load failures at `warning`; widget gallery strings localized; `@unknown default:` on widget-family switches; no Broadway double-link in either target. The post-midnight stale snapshot is explicitly documented as intentional degradation in the provider, `README.md`, and `AGENTS.md`. - -**Accepted:** neither target ships a test bundle (documented). +### CreditKit, JournalKit, StuffCore, TestHostSupport, StuffTestHost -**Files:** WhereWidgets 7/0 · WhereShareExtension 5/0 · README ✓ · AGENTS ✓ · Open: [`Where/TODOs.md`](Where/TODOs.md) +**CreditKit:** `./attribution --check` passes and the derivation is honest — it credits the TLA+ tooling and all four external agent skills while correctly excluding repo-owned skills and tooling-only packages. **Files:** 2/3 · Open: [`Shared/CreditKit/TODOs.md`](Shared/CreditKit/TODOs.md) ---- +**JournalKit:** strong fuzz and truncation coverage; payload-agnostic, with no log semantics leaking in. **Files:** 2/3 · Open: [`Shared/JournalKit/TODOs.md`](Shared/JournalKit/TODOs.md) -### Where app +**StuffCore:** intentional scaffold. **Files:** 1/1 · Open: [`Shared/StuffCore/TODOs.md`](Shared/StuffCore/TODOs.md) -**Verified OK:** `Where/Where/README.md` now exists and matches the three-file shell; `WhereTests` pins `.undetermined` as the launch reason under the UIScene lifecycle; delegate wiring smoke test; no Broadway double-link. Nothing open. +**TestHostSupport:** dependency-free UIKit helpers, no bundle by design. **Files:** 1/0 · nothing open -**Files:** 3/1 · README ✓ · AGENTS ✓ +**StuffTestHost:** the WhereCore embed is gone; each `.xctest` carries its own resource bundles and `PACKAGE_RESOURCE_BUNDLE_PATH` covers the beta-4 dedup. The scene configuration name is now owned solely by the Tuist manifest. **Files:** 2/0 · nothing open --- ## Limitations -- Static analysis only — no `tuist test`, `bumper lint`, or simulator runs in this pass (the Cloud agent runs Linux; the full suite requires macOS CI). CI status on `main` was read via `gh` and is green, which is what lets the "the Gregorian rule finds nothing" conclusion stand. -- Some findings (the LifecycleKit terminal-phase race, the tracking toggle, outbox relaunch loss) need runtime confirmation. -- Severity counts are approximate — several low-severity 1:1 test gaps are folded into module summaries rather than filed individually. -- DEBUG-only surfaces (PeriscopeTools, SwiftDataInspector) are held to a lighter standard for `try?` degradation, per their module docs. -- `Shared/Periscope/Prototypes/JournalBenchmark` (2 sources) is wired into no target and is excluded from the counts below. +- **Static analysis only.** The cloud agent runs Linux, which has **no Swift toolchain at all** — so no `tuist test`, no simulator, and also no `swift run bumper lint` and no `./xcstrings --lint`, both of which are CI gates. `./swiftformat --lint` (0/1007 files) and `./attribution --check` (up to date) were run and pass. CI on `main` is green at `46a84015`, which is what lets the "the Gregorian rule finds nothing" conclusion stand. +- **No snapshot pixels were inspected.** Where this pass judged a reference re-recorded, it read the **Git LFS pointer's `size` field**, not the image. That is strong evidence for the blank-capture item (66 KB → 3.2 MB cannot be solid white) and no evidence at all about whether a re-recorded ax5 reference still shows a layout defect. A macOS `./test --review` would settle those. +- **Runtime-dependent items are unconfirmed by design**: the launch-time notification prompt, Flyover's log routing, multi-process journal coordination, the CloudKit import-readiness race, Spotlight indexing, and Ledger's live API and Keychain paths. Each says so in its own entry. +- **No severity or category counts.** Earlier revisions of this report carried them; they could not be reconciled against the backlog and are omitted deliberately rather than estimated. +- `Shared/Periscope/Prototypes/JournalBenchmark` (2 sources) is wired into no target and is excluded from every count here. --- @@ -230,52 +223,59 @@ The repo-owned rule set (`BumperBowling.swift`, `.bumper/Sources`, catalog in `. ### SPM library targets -| Module | Path | Source | Test | README | AGENTS | -|--------|------|-------:|-----:|:------:|:------:| -| StuffCore | `Shared/StuffCore/` | 1 | 1 | ✓ | ✓ | -| LifecycleKit | `Shared/LifecycleKit/` | 9 | 11 | ✓ | ✓ | -| JournalKit | `Shared/JournalKit/` | 2 | 3 | ✓ | ✓ | -| PeriscopeCore | `Shared/Periscope/PeriscopeCore/` | 35 | 31 | ✓ | ✓ | -| PeriscopeUI | `Shared/Periscope/PeriscopeUI/` | 1 | 2 | ✓ | ✓ | -| PeriscopeTools | `Shared/Periscope/PeriscopeTools/` | 24 | 22 | ✓ | ✓ | -| SwiftDataInspector | `Shared/SwiftDataInspector/` | 13 | 1 | ✓ | ✓ | -| TestHostSupport | `Shared/TestHostSupport/` | 1 | 0 | ✓ | ✓ | -| BroadwayCore | `Shared/Broadway/BroadwayCore/` | 17 | 10 | ✓ | ✓ | -| BroadwayUI | `Shared/Broadway/BroadwayUI/` | 6 | 4 | ✓ | ✓ | -| RegionKit | `Where/RegionKit/` | 13 | 8 | ✓ | ✓ | -| WhereCore | `Where/WhereCore/` | 87 | 58 | ✓ | ✓ | -| WhereUI | `Where/WhereUI/` | 113 | 36 | ✓ | ✓ | -| WhereIntents | `Where/WhereIntents/` | 17 | 9 | ✓ | ✓ | +| Module | Path | Source | Test | Image | README | AGENTS | +|--------|------|-------:|-----:|------:|:------:|:------:| +| StuffCore | `Shared/StuffCore/` | 1 | 1 | — | ✓ | ✓ | +| CreditKit | `Shared/CreditKit/` | 2 | 3 | — | ✓ | ✓ | +| JournalKit | `Shared/JournalKit/` | 2 | 3 | — | ✓ | ✓ | +| LifecycleKit | `Shared/LifecycleKit/` | 8 | 10 | — | ✓ | ✓ | +| LifecycleKitUI | `Shared/LifecycleKitUI/` | 5 | 3 | — | ✓ | ✓ | +| SnapshotKit | `Shared/SnapshotKit/` | 8 | 3 | — | ✓ | ✓ | +| SnapshotKitTesting | `Shared/SnapshotKitTesting/` | 14 | 11 | — | ✓ | ✓ | +| Inspector | `Shared/Inspector/` | 23 | 14 | 1 | ✓ | ✓ | +| Flyover | `Shared/Flyover/` | 50 | 12 | 1 | ✓ | ✓ | +| TestHostSupport | `Shared/TestHostSupport/` | 1 | 0 | — | ✓ | ✓ | +| BroadwayCore | `Shared/Broadway/BroadwayCore/` | 17 | 10 | — | ✓ | ✓ | +| BroadwayUI | `Shared/Broadway/BroadwayUI/` | 6 | 4 | — | ✓ | ✓ | +| PeriscopeCore | `Shared/Periscope/PeriscopeCore/` | 37 | 33 | — | ✓ | ✓ | +| PeriscopeUI | `Shared/Periscope/PeriscopeUI/` | 1 | 2 | — | ✓ | ✓ | +| PeriscopeTools | `Shared/Periscope/PeriscopeTools/` | 27 | 27 | 1 | ✓ | ✓ | +| RegionKit | `Where/RegionKit/` | 15 | 10 | — | ✓ | ✓ | +| WhereCore | `Where/WhereCore/` | 118 | 74 | — | ✓ | ✓ | +| WhereUI | `Where/WhereUI/` | 224 | 90 | 37 | ✓ | ✓ | +| WhereIntents | `Where/WhereIntents/` | 17 | 10 | — | ✓ | ✓ | +| LedgerCore | `Ledger/LedgerCore/` | 16 | 14 | — | ✓ | ✓ | ### Tuist app / extension targets | Target | Path | Source | Test | README | AGENTS | |--------|------|-------:|-----:|:------:|:------:| -| Where | `Where/Where/` | 3 | 1 | ✓ | ✓ | +| Where | `Where/Where/` | 6 | 2 | ✓ | ✓ | | WhereWidgets | `Where/WhereWidgets/` | 7 | 0 | ✓ | ✓ | | WhereShareExtension | `Where/WhereShareExtension/` | 5 | 0 | ✓ | ✓ | | RegionViewer | `Where/RegionViewer/` | 1 | 0 | ✓ | ✓ | +| Ledger | `Ledger/Ledger/` | 8 | 0 | ✓ | ✓ | | StuffTestHost | `Shared/StuffTestHost/` | 2 | 0 | ✓ | ✓ | | BroadwayCatalog | `Shared/Broadway/BroadwayCatalog/` | 2 | 1 | ✓ | ✓ | -**Totals:** ~359 source · ~198 test Swift files across shipped targets (plus 4 Bumper rule/test sources and 2 unwired prototype sources). +**Totals:** 623 source · 337 test · 40 image-snapshot Swift files across shipped targets (plus 4 Bumper rule/test sources and 2 unwired prototype sources). **361** LFS-backed reference images. **25** test bundles: 21 unit (`Stuff-iOS-Tests`, plus `Ledger-macOS-Tests`) and 4 image (`StuffSnapshotTests`) — every one is a member of a CI scheme. + +**Group-folder docs:** `Shared/Broadway/` and `Shared/Periscope/` carry the required group-level pair. `Where/` has `AGENTS.md` but **no `README.md`**, and `Ledger/` has **neither** — both filed in the root [`TODOs.md`](TODOs.md). --- -## Changes since July 19, 2026 audit - -| Area | July 19 state | July 26 state | -|------|---------------|---------------| -| Target count | 16 SPM + 6 Tuist | **14 SPM** + 6 Tuist — LogKit and LogViewerUI deleted, replaced by Periscope (#94) | -| File count | ~308 source / ~189 test | ~359 source / ~198 test (WhereUI 84 → 113, WhereCore 70 → 87, PeriscopeTools 15 → 24, RegionKit 9 → 13) | -| Architecture lint | — | Bumper Bowling (#127): Where component graph + 10 source-level rules, hard-gated in CI — with a Gregorian blind spot and a stale catalog | -| Navigation | Primary / Elsewhere / Resolve / Settings | Locations / Your Year / Settings (#119); Elsewhere is a card, Resolve a toolbar action, data screens under Settings | -| Settings | Flat list | iOS-style drill-in screens with search (#111) | -| Developer surfaces | LogViewerUI + overlay | Liquid Glass HUD (#115), Periscope viewer with hierarchy / span tree / span history / density (#107), in-app SwiftData browser | -| Launch reason | `applicationState` guess (cold launch read as headless) | `LifecycleReason.undetermined` + promotion, with `completedStepIDs` preventing re-runs (#109) | -| Localization | Hand-maintained key facades | Generated String Catalog symbols; a removed key is a compile error (#124); catalogs serialized as Xcode writes them and linted (#135) | -| Calendars | `Calendar.current` in view call sites | Fixed at the call sites (`fe99dde`) — but relocated into `CalendarDay.displayDate` and two helper defaults | -| Preview coverage | Gaps across WhereUI | Complete; enforced by `where.preview_coverage` (`52f0136`) | -| Simulator handling | Name-based destinations | `./simulator` resolves a UDID and boots it; `profile` / `flaky` / CI all go through it (#130) | -| Device installs | Xcode UI | `./Where/install` (#110, #112) | -| Backlog | Findings split between this file and two `TODOs.md` | One backlog across eight `TODOs.md`; this report is derived and carries no items | +## Changes since July 26, 2026 audit + +| Area | July 26 state | August 9 state | +|------|---------------|----------------| +| Target count | 14 SPM + 6 Tuist | **20 SPM + 7 Tuist** — Ledger/LedgerCore (#103), Flyover (#156), LifecycleKitUI, plus CreditKit, SnapshotKit and SnapshotKitTesting now counted | +| File count | ~359 source / ~198 test | **623 / 337** (WhereUI 113 → 224, WhereCore 87 → 118, PeriscopeTools 24 → 27) | +| Platforms | iOS only | iOS **and native macOS** — Ledger is the first macOS app, with its own `test-macos` CI job and a `Ledger-macOS-Tests` scheme, because no single xcodebuild destination builds both | +| Renames | `SwiftDataInspector` | **`Inspector`** (#158), now also a DEBUG boot runtime the app can launch instead of its regular composition root | +| Image suites | 1 bundle, 232 references | **4 bundles** (WhereUI, Flyover, Inspector, PeriscopeTools), **361** references, one shared `StuffSnapshotTests` scheme; scrolling content now captures at intrinsic height (#196) | +| Formal specs | — | **9 TLA+ specifications** under `Where/Specifications/`, run locally via `./tla-check` (opt-in, not CI) — launch lifecycle, scope exclusivity, log routing, post-write reconcile, remote device removal, store-perform serialization, and more | +| Multi-device | Single install | Installation-local recording consent, advisory check-ins, removal tombstones, a Devices settings screen, and a removal-recovery gate (#160) | +| Persistence | Unscoped fetches | Every scoped fetch is generation-aware, composed before materialization (#209) | +| Agent tooling | 2 skills | **10 skills** (6 repo-owned, 4 external and pinned), a PR template, and `codex-watchdog` + `worktree` for Codex-managed checkouts | +| Backlog | 8 `TODOs.md` | **13** — `Ledger/` and `Shared/Flyover/` opened by this pass | +| Dev scripts | 10 | **13** (`tla-check` added; the root `AGENTS.md` list had omitted it) | diff --git a/Shared/Broadway/TODOs.md b/Shared/Broadway/TODOs.md index b0db9b748..375838b72 100644 --- a/Shared/Broadway/TODOs.md +++ b/Shared/Broadway/TODOs.md @@ -10,15 +10,15 @@ here. # Open issues ## P1s (Should do) -- test(BroadwayCatalog) [quick-win]: Host `BroadwayCatalogTests` in `StuffTestHost` like every other hosted bundle. Today it is a hand-rolled target hosted by the BroadwayCatalog app itself (`Project.swift:545` — deps `[BroadwayCatalog, TestHostSupport]`, no `StuffTestHost`), a deviation from the convention that hosted tests run in the shared host. Rewire it through the `unitTests` helper (keeping the `BroadwayCatalog` code dependency) and confirm `tuist test BroadwayCatalogTests` stays green. (pr#149 review 2026-07-28) -- fix(BroadwayCatalog) [quick-win]: `BroadwayApp.swift:6` never seeds `.broadwayRoot()`, so the showcase renders with no `BContext` and every `@Environment(\.bContext)` read falls back to defaults — the one app whose job is to show Broadway is the one not using it. (audit 2026-07-26) -- test(BroadwayCatalog) [quick-win]: `Tests/BroadwayCatalogTests.swift:4` is an empty `struct BroadwayCatalogTests {}` wired into the `Stuff-iOS-Tests` scheme, so CI runs it and it asserts nothing. Replace it with a launch smoke test. (audit 2026-07-26) -- fix(BroadwayUI) [needs-design]: A nested `BRootViewController` registers duplicate trait observers (`BRootViewController.swift:92`, documented in a source `TODO`). Latent today — Where reaches Broadway only through `whereBroadwayRoot()` / `BRootView`, neither of which nests — but it fires the moment something does. (audit 2026-07-26) +- test(BroadwayCatalog) [quick-win]: Host `BroadwayCatalogTests` in `StuffTestHost` like every other hosted bundle. Today it is a hand-rolled target hosted by the BroadwayCatalog app itself (`Project.swift:650-661` — deps `[BroadwayCatalog, TestHostSupport]`, no `StuffTestHost`), a deviation from the convention that hosted tests run in the shared host. Rewire it through the `unitTests` helper (keeping the `BroadwayCatalog` code dependency) and confirm `tuist test BroadwayCatalogTests` stays green. (pr#149 review 2026-07-28) +- fix(BroadwayCatalog) [quick-win]: `BroadwayApp.swift:6-7` never seeds `.broadwayRoot()`, so the showcase renders with no `BContext` and every `@Environment(\.bContext)` read falls back to defaults — the one app whose job is to show Broadway is the one not using it. (audit 2026-07-26) +- test(BroadwayCatalog) [quick-win]: `Tests/BroadwayCatalogTests.swift:4` is an empty `struct BroadwayCatalogTests {}` wired into the `Stuff-iOS-Tests` scheme (`Project.swift:762`), so CI runs it and it asserts nothing. Replace it with a launch smoke test. (audit 2026-07-26) +- fix(BroadwayUI) [needs-design]: A nested `BRootViewController` registers duplicate trait observers (source `TODO` at `BRootViewController.swift:92-93`; the observer is still created unconditionally at `:95-103`). Latent today — Where reaches Broadway only through `whereBroadwayRoot()` / `BRootView`, neither of which nests — but it fires the moment something does. (audit 2026-07-26) ## P2s (Nice to have) -- convention(BroadwayCore) [quick-win]: Guard the `didSet` work in `BContext.swift:34` and `BRootViewController.swift:37` on an unchanged `Equatable` value, so reassigning the same context isn't a full invalidation. (audit 2026-07-26) +- convention(BroadwayCore) [quick-win]: Guard the `didSet` work in `BContext.swift:34-36`, `:45-47`, `:52-54` and `BRootViewController.swift:37-41` on an unchanged `Equatable` value, so reassigning the same context isn't a full invalidation. (audit 2026-07-26) - perf(BroadwayCore) [needs-design]: Evict the stylesheet cache under memory pressure (`BStylesheets.swift:90`, documented in a source `TODO`); it currently only grows. (audit 2026-07-26) - test(BroadwayCore) [quick-win]: Add the missing 1:1 tests for `UIViewControllerTraitObserver`, `EquatableIgnored`, and `BTraitOverrides+SwiftUI`. (audit 2026-07-26) -- docs(BroadwayCatalog) [quick-win]: `README.md` promises a "living catalog" of components that the placeholder `ContentView` doesn't provide. Build the gallery or narrow the README to what ships. (audit 2026-07-26) +- docs(BroadwayCatalog) [quick-win]: `README.md:3-4` promises a "living catalog" of components that the placeholder `ContentView` (`ContentView.swift:6-14`, an icon and a title) doesn't provide. Build the gallery or narrow the README to what ships. (audit 2026-07-26) # Completed issues diff --git a/Shared/CreditKit/TODOs.md b/Shared/CreditKit/TODOs.md index 76b79aaae..ca4285ad7 100644 --- a/Shared/CreditKit/TODOs.md +++ b/Shared/CreditKit/TODOs.md @@ -7,6 +7,6 @@ here. # Open issues ## P2s (Nice to have) -- fix [quick-win]: `github_slug` accepts anything after the host, so a malformed pin becomes a malformed API path instead of a clear error. It captures `.+?` (`Tools/generate-attribution.rb:91`) and the result is interpolated straight into `repos/#{slug}/license?ref=#{ref}` (`:82`), so a `location` of `https://github.com/foo/bar?x=y` asks for `repos/foo/bar?x=y/license?ref=…` and fails with whatever `gh` makes of that. Not a security issue: both inputs are repo-controlled (`Package.resolved`, `.agents/external-skills.json`) and `Open3.capture3` passes argv with no shell, so nothing is injectable. Constrain the capture to `[\w.-]+/[\w.-]+` so a bad pin fails as a bad pin. (pr#140 review) +- fix [quick-win]: `github_slug` accepts anything after the host, so a malformed pin becomes a malformed API path instead of a clear error. It captures `.+?` (`Tools/generate-attribution.rb:96-97`) and the result is interpolated straight into `repos/#{slug}/license?ref=#{ref}` (`:88`), so a `location` of `https://github.com/foo/bar?x=y` asks for `repos/foo/bar?x=y/license?ref=…` and fails with whatever `gh` makes of that. Not a security issue: both inputs are repo-controlled (`Package.resolved`, `.agents/external-skills.json`) and `Open3.capture3` passes argv with no shell, so nothing is injectable. Constrain the capture to `[\w.-]+/[\w.-]+` so a bad pin fails as a bad pin. (pr#140 review) # Completed issues diff --git a/Shared/Flyover/TODOs.md b/Shared/Flyover/TODOs.md new file mode 100644 index 000000000..67ac59fe8 --- /dev/null +++ b/Shared/Flyover/TODOs.md @@ -0,0 +1,19 @@ +# Flyover todos + +The backlog for Flyover, the app-agnostic developer screen browser. + +The item format and the placement rule live in the root +[`TODOs.md`](../../TODOs.md); raw notes go in [`INBOX.md`](../../INBOX.md), not +here. + +Flyover's known cross-module issue — its unactivated sibling demo world still +reaching the active scope's durable diagnostic store through the process-global +`WhereLog` facade — is filed in [`Where/TODOs.md`](../../Where/TODOs.md), +because the fix is Where's to make. + +# Open issues + +## P2s (Nice to have) +- test [needs-design]: The engine is well covered but the interactive surfaces are not. Ten test files pin what the module computes — the catalog, layout, model, canvas render and zoom plans, the serial content-load coordinator, and the stylesheet — while the UI it drives is verified only by the single `canvasAndList` image case (4 references). Untested: the focused inspector, the viewport and appearance menus, and the overview↔focus transition edge cases. Acceptable for a DEBUG-only tool, and deliberately not a hosting-smoke-test gap (the repo's convention is that an image bundle owns "does this screen render"), so the shape of the fix is more `SnapshotProviding` cases in [`SnapshotTests/`](SnapshotTests) rather than new unit tests — decide which surfaces are worth pinning before adding them wholesale. (audit 2026-08-09) + +# Completed issues diff --git a/Shared/Inspector/TODOs.md b/Shared/Inspector/TODOs.md index e67685cde..a576ddaa8 100644 --- a/Shared/Inspector/TODOs.md +++ b/Shared/Inspector/TODOs.md @@ -10,14 +10,23 @@ The item format and placement rule live in the root - fix [needs-design]: The second image capture in this bundle's process can render a search-field placeholder at a different width. The dark `inspectorSurfaces.SwiftData_iPhone_dark` assertion remains quarantined with - `withKnownIssue`; the likely fix is a measured capture-pipeline warm-up in - SnapshotKitTesting, not re-recording one bistable state. (agent 2026-07-28) + `withKnownIssue(..., isIntermittent: true)` + (`SnapshotTests/InspectorSnapshotTests.swift:61-73`); the likely fix is a + measured capture-pipeline warm-up in SnapshotKitTesting, not re-recording one + bistable state. It is one of only two `withKnownIssue` quarantines in the repo + (the other guards the Elsewhere inflection bug in WhereUI). + (agent 2026-07-28; re-verified 2026-08-09) - test [quick-win]: Cover the bare-`PersistentIdentifier` relationship branch - in `SwiftDataReflection.swift`; current relationship tests materialize the - model and exercise the other branch. (audit 2026-07-26) + in `SwiftDataReflection.swift:132-137` (`classify` when the relationship value + is a bare identifier); `InspectorSwiftDataRelationshipTests` materializes + `TestParent`/`TestChild` and only ever exercises the other branch, and + `SwiftDataReflectionTests` covers attribute reads and fetch helpers. + (audit 2026-07-26; re-verified 2026-08-09) - test [quick-win]: Add image cases for the paged row table, filesystem root, - defaults editor, and relationship drill-in. The entity list and developer - menu are covered. (pr#101 review) + defaults editor, and relationship drill-in. The bundle still has exactly one + case, `inspectorSurfaces`, producing four references — Root light/dark + (`SnapshotTests/InspectorSnapshotTests.swift:118-125`) and SwiftData light + plus the quarantined dark (`:36-73`). (pr#101 review; re-verified 2026-08-09) ## Completed issues diff --git a/Shared/JournalKit/TODOs.md b/Shared/JournalKit/TODOs.md index 266692a99..338f02b3d 100644 --- a/Shared/JournalKit/TODOs.md +++ b/Shared/JournalKit/TODOs.md @@ -7,7 +7,7 @@ here. # Open issues ## P2s (Nice to have) -- test [quick-win]: The concurrent-append test discards append errors with `try?` (`JournalTests.swift:186`), so it would pass with fewer entries than it asserts were written. Surface the error instead. (audit 2026-07-26) -- test [quick-win]: `.full` sync durability is exercised by a single append; widen it to something that would actually catch a regression. (audit 2026-07-26) +- test [quick-win]: The concurrent-append test discards append errors with `try?` (`JournalTests.swift:186`), so it would pass with fewer entries than it asserts were written. Surface the error instead. (audit 2026-07-26; re-verified 2026-08-09) +- test [quick-win]: `.full` sync durability is exercised by a single append (`JournalTests.swift:15`, inside `appendsRoundTripInOrder` — there is no dedicated `F_FULLFSYNC` regression); widen it to something that would actually catch a regression. (audit 2026-07-26; re-verified 2026-08-09) # Completed issues diff --git a/Shared/LifecycleKit/TODOs.md b/Shared/LifecycleKit/TODOs.md index 935dbf59a..9be9e0b68 100644 --- a/Shared/LifecycleKit/TODOs.md +++ b/Shared/LifecycleKit/TODOs.md @@ -7,7 +7,7 @@ here. # Open issues ## P2s (Nice to have) -- test [quick-win]: Add a test that duplicate node IDs trap. `LaunchPlan.append` `precondition`s on a duplicate (`LaunchPlan.swift:113`), and `LifecycleContainer` — now in LifecycleKitUI — does the same for duplicate gate-view registrations (`LifecycleKitUI/Sources/LifecycleContainer.swift:101`), but nothing exercises either. (audit 2026-07-26) +- test [quick-win]: Add a test that duplicate node IDs trap. `LaunchPlan.append` `precondition`s on a duplicate (`LaunchPlan.swift:137-140`), and `LifecycleContainer` — now in LifecycleKitUI — does the same for duplicate gate-view registrations (`LifecycleKitUI/Sources/LifecycleContainer.swift:101-104`), but nothing exercises either. (audit 2026-07-26) # Completed issues diff --git a/Shared/Periscope/TODOs.md b/Shared/Periscope/TODOs.md index fa1a88efe..3fcedb78e 100644 --- a/Shared/Periscope/TODOs.md +++ b/Shared/Periscope/TODOs.md @@ -11,29 +11,29 @@ here. # Open issues ## P0s (Must do) -- design(PeriscopeCore) [needs-design]: Span record modeling — `spanID`/`spanExit` bolted onto every `LogRecord` (and `bypassesFloors` as a one-off flag) feels wrong; consider `enum { case span(Span), case event(Event) }` or a dedicated span record type. Plan/build loop. (agent) +- design(PeriscopeCore) [needs-design]: Span record modeling — consider `enum { case span(Span), case event(Event) }` or a dedicated span record type instead of discriminating spans by downcast. Plan/build loop. **Correction (2026-08-09):** as filed, this said `spanID`/`spanExit` were "bolted onto every `LogRecord`". They aren't. Both are *computed* accessors that downcast the record's `event` (`LogSpan.swift:210`, `:216`, plus `spanRelaunchPolicy` at `:224`), so `LogRecord` stores exactly one span-related field — `bypassesFloors` (`LogRecord.swift:56`) — and the denormalized columns live on the persistence and journal shapes on purpose, so the sweep and queries read an indexed value instead of decoding payloads (`PeriscopeSchema.swift:50`, `:53`; `StoredLogEvent.swift:51`, `:54`; `LogJournalEntry.swift:94-95`). That reframes the item: the case discrimination already exists as optional downcasts, so the real questions are whether to make it a typed enum and whether `bypassesFloors` belongs on the record at all — not whether to unpick stored span fields. (agent) - design(PeriscopeCore) [needs-design]: Decompose `Periscope` (the type and its flat `State` — group watchdog/inspect/ambient/live-observer state into sub-structs) and `PeriscopeStore` into children per behavioral area. Plan/build loop. (agent) - design(PeriscopeCore) [needs-design]: `ScopeID` derivation — hash-derived vs a concatenated, human-readable path that preserves the input for debugging. Plan/build loop. (agent) - design(PeriscopeCore) [needs-design]: `LogContextProviding` parent hierarchy — instance logs need a way to nest under a container's context (e.g. a controller inside another controller). Plan/build loop. (agent) -- feat(PeriscopeCore) [needs-design]: Implement `SpanRelaunchPolicy.survivesRelaunch` resume mechanics. The policy is recorded on `SpanBegan` payloads *and* persisted as the `SDLogEvent.spanRelaunchPolicy` column, and the relaunch sweep honors it (surviving spans are left open, not orphan-closed), but nothing re-seeds them: `end(for:)` in the new process warns "without a matching begin" (policy: `SpanExit.swift:83–91`; warn: `LogSpan.swift:577`). Needs an async bootstrap step at store/system startup that queries unmatched surviving `SpanBegan` events and re-opens them in `Periscope.openSpans` — plus wall-clock durations for resumed spans (`ContinuousClock` instants don't survive reboot; `SpanEnded.duration` is already optional for this) and accepting that signpost intervals can't resume. (audit 2026-07-26) -- feat(PeriscopeCore) [needs-design]: Never drop the pre-store-attach window — journal from process start. `PeriscopeStore.make` is `async` (`:84`), so events emitted between process launch and `add(sink:)` (`Periscope.swift:192`) — early launch steps, ambient start-up snapshots — reach neither the store nor today's journal (journaling only begins once an on-disk store is attached); they survive only in the in-memory recent buffer and OSLog, and are lost from the durable record. We must never drop or lose events. Fix: write to a **short-lived journal from app start, before the store is registered, reusing the JournalKit infra**; when the store attaches, ingest that bootstrap journal (dedupe by event ID like the crash-journal ingest) and delete it. Composes with — but is stronger than — a recent-buffer replay into a late-added sink (that only covers what's still buffered, not a slow/large pre-attach burst). Related: the "No eager store handle" P2 below. (pr#94 review) +- feat(PeriscopeCore) [needs-design]: Implement `SpanRelaunchPolicy.survivesRelaunch` resume mechanics. The policy is recorded on `SpanBegan` payloads *and* persisted as the `SDLogEvent.spanRelaunchPolicy` column, and the relaunch sweep honors it (surviving spans are left open, not orphan-closed), but nothing re-seeds them: `end(for:)` in the new process warns "without a matching begin" (policy: `SpanExit.swift:83-89`; warn: `LogSpan.swift:600`; the store-side half and its guard: `PeriscopeStore.swift:215-219`, `:244-245`, `PeriscopeStoreTests.swift:477-493`). Needs an async bootstrap step at store/system startup that queries unmatched surviving `SpanBegan` events and re-opens them in `Periscope.openSpans` — plus wall-clock durations for resumed spans (`ContinuousClock` instants don't survive reboot; `SpanEnded.duration` is already optional for this) and accepting that signpost intervals can't resume. (audit 2026-07-26) +- feat(PeriscopeCore) [needs-design]: Never drop the pre-store-attach window — journal from process start. `PeriscopeStore.make` is `async` (`PeriscopeStore.swift:118`), and the journal installs only when a store sink is added (`Periscope.swift:223`, install at `:232-236` / `:286-291`), so events emitted between process launch and that call — early launch steps, ambient start-up snapshots — reach neither the store nor today's journal (journaling only begins once an on-disk store is attached); they survive only in the in-memory recent buffer and OSLog, and are lost from the durable record. We must never drop or lose events. Fix: write to a **short-lived journal from app start, before the store is registered, reusing the JournalKit infra**; when the store attaches, ingest that bootstrap journal (dedupe by event ID like the crash-journal ingest) and delete it. Composes with — but is stronger than — a recent-buffer replay into a late-added sink (that only covers what's still buffered, not a slow/large pre-attach burst). Related: the "No eager store handle" P2 below. (pr#94 review) ## P1s (Should do) - feat(PeriscopeCore) [needs-design]: Journal attachments via external storage. Instead of inlining blobs ≤64KB and omitting larger ones (`LogJournalEntry.swift:101`), write attachment bytes as files beside the journal segments (the entry referencing them by filename), clean them up with segment rotation and journal removal, and re-attach them at ingest. Removes the size cliff entirely — screenshots and payloads survive crashes too. (pr#86 review) -- feat(PeriscopeCore) [needs-design]: Multi-process store + journal coordination. Today only app processes ingest journals (extensions journal but never ingest, so an extension launch can't delete the live app's journal) — but the reverse hole remains: an app launching while an extension session is live would ingest and delete that *live* journal out from under its open descriptor (`PeriscopeStoreJournalIngest.swift:12`), silently ending its recoverability. Needs a claim mechanism (e.g. a claim file the writer holds, or skip-directories-with-live-claims) designed alongside App Group store sharing — which the store doesn't support yet either (exclusive sequence counters, SwiftData container coordination). (agent) -- fix(PeriscopeTools) [needs-design]: Hierarchy subtree counts tally by `primaryScope` only (`LogHierarchyModel.swift:89`), but the drill-in (`LogInspectorModel.swift:59` with `LogQuery.scope = .subtree`) matches events linking *any* scope in the subtree — so a scope's badge count and its drilled-in list length can disagree (compounded by the count being uncapped while the drill-in caps at `limit: 500`). Count by "any linked scope in the subtree" to match the query, or document the primary-only semantics. The asymmetry is pinned by a test, so this is a decision to revisit rather than a slip. (pr#107 review) +- feat(PeriscopeCore) [needs-design]: Multi-process store + journal coordination. Today only app processes ingest journals (extensions journal but never ingest, so an extension launch can't delete the live app's journal) — but the reverse hole remains: an app launching while an extension session is live would ingest and delete that *live* journal out from under its open descriptor (documented at `PeriscopeStoreJournalIngest.swift:12-18`, `:24-28`; the unconditional directory delete is at `:42-44` and `:71`), silently ending its recoverability. Needs a claim mechanism (e.g. a claim file the writer holds, or skip-directories-with-live-claims) designed alongside App Group store sharing — which the store doesn't support yet either (exclusive sequence counters, SwiftData container coordination). (agent) +- fix(PeriscopeTools) [needs-design]: Hierarchy subtree counts tally by `primaryScope` only (`LogHierarchyModel.swift:92-93`, pinned by `LogHierarchyModelTests.countsOnlyThePrimaryScope` at `LogHierarchyModelTests.swift:68-101`), but the drill-in (`LogInspectorModel.swift:59` with `LogQuery.scope = .subtree`) matches events linking *any* scope in the subtree — so a scope's badge count and its drilled-in list length can disagree (compounded by the count being uncapped while the drill-in caps at `limit: 500`). Count by "any linked scope in the subtree" to match the query, or document the primary-only semantics. The asymmetry is pinned by a test, so this is a decision to revisit rather than a slip. (pr#107 review) ## P2s (Nice to have) - design(PeriscopeTools) [needs-design]: `SpanTreeModel` models open spans with `effectiveEnd = .distantFuture`, so every span that begins later — even an independent, concurrent one — nests under any still-open span, collapsing the later tree into one deep chain. Confirm the intended containment semantics for open spans and pin them with a test, or nest more conservatively. (pr#107 review) - test(PeriscopeTools) [quick-win]: Pin whatever semantics that lands with a case covering two overlapping *open* spans; nothing covers it today, so either outcome regresses silently. (audit 2026-07-26) -- refactor(PeriscopeTools) [quick-win]: `ScopeEventsView`, `LogInspectorView`, `SpanTreeView`, and `SpanHistoryView` seed density from `.load(from: .standard)` directly, bypassing the injectable `defaults` the viewer threads through — so those surfaces can't be pointed at an ephemeral test suite and always touch the shared standard domain. It also means a drill-in *overrides* the density the viewer already seeded rather than inheriting it. Thread `defaults` through, or read the density from the environment. (pr#107 review; was nested under the `SpanTreeRow` density no-op, closed 2026-07-28) -- perf(PeriscopeTools) [needs-design]: The incremental *fetch* is bounded per commit, but `SpanTreeModel.load` / `LogHierarchyModel.load` still rebuild the whole tree/forest from all accumulated events on every `changes()` ping — O(total spans) per commit for a long-lived viewer over a busy store. Rebuild incrementally or throttle rebuilds. (pr#107 review) - - perf(PeriscopeTools) [needs-design]: `LogInspectorModel` didn't get even the bounded fetch — it re-runs its full subtree query on every `changes()` ping, so an open inspect sheet over a busy store re-reads everything per commit. Give it the same `afterSequence` cursor, or debounce it. (audit 2026-07-26) +- refactor(PeriscopeTools) [quick-win]: `ScopeEventsView` (`:38`), `LogInspectorView` (`LogInspectable.swift:89`), `SpanTreeView` (`:25`), and `SpanHistoryView` (`:32`) seed density from `.load(from: .standard)` directly, bypassing the injectable `defaults` only `PeriscopeViewer` threads through (`PeriscopeViewer.swift:17`, `:58-63`) — so those surfaces can't be pointed at an ephemeral test suite and always touch the shared standard domain. It also means a drill-in *overrides* the density the viewer already seeded rather than inheriting it. Thread `defaults` through, or read the density from the environment. (pr#107 review; was nested under the `SpanTreeRow` density no-op, closed 2026-07-28) +- perf(PeriscopeTools) [needs-design]: The incremental *fetch* is bounded per commit, but `SpanTreeModel.load` (`SpanTreeModel.swift:153`) / `LogHierarchyModel.load` (`LogHierarchyModel.swift:75`) still rebuild the whole tree/forest from all accumulated events on every `changes()` ping — O(total spans) per commit for a long-lived viewer over a busy store. Rebuild incrementally or throttle rebuilds. (pr#107 review) + - perf(PeriscopeTools) [needs-design]: `LogInspectorModel` didn't get even the bounded fetch — it re-runs its full subtree query on every `changes()` ping with no `afterSequence` cursor (`LogInspectorModel.swift:42-48`, `:51-64`), so an open inspect sheet over a busy store re-reads everything per commit. Give it the same cursor, or debounce it. (audit 2026-07-26) - refactor(PeriscopeCore) [needs-design]: Reconsider the `callAsFunction` scope-derivation API. `log(SomeLog.self)` / `log(for: id)` derivation reads as an opaque function call at declaration sites; a named form (`log.scope(SomeLog.self)` / `log.subcatalog(for: id)` / `log.child(_:)`) would read clearer. Constraint: the one-expression derive-and-emit (`log(PhotoLogs.self) { … }`) exists *because* `callAsFunction` lets Swift resolve the type arg + trailing closure as one application — a named method splits it, so the emit ergonomics need a paired design (a method that also takes the trailing closure) before renaming. Affects every derivation call site + all Periscope consumers. (pr#94 review) - feat(PeriscopeCore) [quick-win]: Add non-closure emit overloads alongside the `{}` form. Today emit is only `log { .event }` / `log(attachments:) { .event }`; the closure is nice for multi-line payload builds but heavy for a bare event. Add a value form — either `log.emit(.event)` (named, no overload ambiguity) or a `log(.event)` value overload — keeping `{}` for multi-line. Additive; pairs with the derivation-naming item above. (pr#94 review) - feat(PeriscopeTools) [needs-design]: Inspect-by-object is scope-granular, not instance-granular. `.logInspectable(_:)` keys the badge/inspector to a `Log`'s *scope*, so tagging a list row (Where tags `EvidenceRow` with `WhereLog.evidence`, `LocationStatusRow` with `WhereLog.session`) surfaces the whole scope's recent events, not that one row's. Events already carry `externalID` for object correlation, but the inspector can't filter by it — a per-instance child scope (blocked on the `LogContextProviding` parent-hierarchy P0) or an `externalID`-scoped inspect entry would make true row-/object-level inspection work. (pr#94 review) - design(PeriscopeCore) [needs-design]: No eager store handle — `PeriscopeStore.make` being `async` forces an "optional store, observe until it lands" dance on consumers. Where exposes an `Optional` on `WhereModel` that stays `nil` until the bootstrap `Task` completes, and `RootView` has to watch the transition (`.onChange` of the store identity) to wire the viewer/inspector/alerter. A synchronous pending-store handle (usable immediately, resolves in the background) or an `await`-readiness accessor would remove the optional-and-observe boilerplate every app repeats. (agent) -- test(PeriscopeTools) [needs-design]: broken-snapshots — replace the hosting smoke tests with image snapshots. Eighteen tests across nine files assert nothing but "the hosted view reached a window": `#expect(await waitUntil { host.view.window != nil })` in `LogEventListTests.swift:30`, `:41`, `LogHierarchyViewHostingTests.swift:23`, `:34`, `PeriscopeViewerHostingTests.swift:29`, `:42`, `ScopeEventsViewHostingTests.swift:25`, `:38`, `SpanHistoryViewHostingTests.swift:23`, `:34`, `SpanTreeViewHostingTests.swift:26`, `:37`, and the `try waitFor { host.view.window != nil }` spelling in `LogInspectableHostingTests.swift:25`, `:38`, `:50`, `LogTraceViewHostingTests.swift:23`, `OpenSpansViewHostingTests.swift:27`, `:38`. The predicate restates what `show`/`showHosted` already guarantee, so each test proves only that construction didn't crash — never what rendered, which is the part the elaborate seeding sets up (`LogHierarchyView`'s outline, the comfortable density `PeriscopeViewerHostingTests` injects, the "No Events" state `ScopeEventsViewHostingTests` documents at `:29`). The repo convention is now that an image bundle, not a hosting smoke test, owns "does this screen render" (see [`Where/WhereUI/AGENTS.md`](../../Where/WhereUI/AGENTS.md#testing) and the WhereUI suite that replaced its own smoke tests). Convert them to image snapshots over the same seeded stores, keeping any assertion that isn't the window check and deleting the files left empty. **The plumbing is already in place**: [`SnapshotTests/`](PeriscopeTools/SnapshotTests) exists and `PeriscopeViewerSnapshotTests` is the worked example — add a file per view beside it, and it compiles into the module's own `PeriscopeToolsSnapshotTests` bundle (one image bundle per module, gathered into the shared `StuffSnapshotTests` scheme — root [`AGENTS.md`](../../AGENTS.md#targets)) while recording references here. The remaining work is per-view authoring, not wiring: each view needs a deterministic fixture (a frozen store, as `PeriscopeViewerSnapshotTests` does) and ideally a `SnapshotProviding` conformance in its own source file — which needs a `SnapshotKit` dependency on PeriscopeTools, since the module has no `#Preview`s at all today. `OpenSpansView` is the one view with a genuine determinism problem: its `TimelineView(.periodic(from: .now, by: 1))` ticking ages (`OpenSpansView.swift:19`) need the `\.isCapturingSnapshot` treatment. (Note the two `window != nil` checks in `Shared/LifecycleKit/Tests/` are *not* in scope: they assert the hosting helper's own lifecycle contract, which is the one place the check is the point. Inspector carries the same hosting-smoke debt — see [`Shared/Inspector/TODOs.md`](../Inspector/TODOs.md).) (pr#101 review) +- test(PeriscopeTools) [needs-design]: broken-snapshots — replace the hosting smoke tests with image snapshots. **Twenty** tests across **ten** files assert nothing but "the hosted view reached a window" (re-counted 2026-08-09; filed as eighteen across nine, and it has *grown* rather than shrunk — PR #152 added a tenth file): `#expect(await waitUntil { host.view.window != nil })` in `LogEventListTests.swift:30`, `:41`, `LogHierarchyViewHostingTests.swift:23`, `:34`, `PeriscopeViewerHostingTests.swift:29`, `:42`, `ScopeEventsViewHostingTests.swift:25`, `:38`, `SpanHistoryViewHostingTests.swift:23`, `:34`, `SpanTreeViewHostingTests.swift:26`, `:37`, `LogEventDetailViewHostingTests.swift:30`, `:43`, and the `try waitFor { host.view.window != nil }` spelling in `LogInspectableHostingTests.swift:25`, `:38`, `:50`, `LogTraceViewHostingTests.swift:23`, `OpenSpansViewHostingTests.swift:27`, `:38`. The predicate restates what `show`/`showHosted` already guarantee, so each test proves only that construction didn't crash — never what rendered, which is the part the elaborate seeding sets up (`LogHierarchyView`'s outline, the comfortable density `PeriscopeViewerHostingTests` injects, the "No Events" state `ScopeEventsViewHostingTests` documents at `:29`). The repo convention is now that an image bundle, not a hosting smoke test, owns "does this screen render" (see [`Where/WhereUI/AGENTS.md`](../../Where/WhereUI/AGENTS.md#testing) and the WhereUI suite that replaced its own smoke tests). Convert them to image snapshots over the same seeded stores, keeping any assertion that isn't the window check and deleting the files left empty. **The plumbing is already in place**: [`SnapshotTests/`](PeriscopeTools/SnapshotTests) exists and `PeriscopeViewerSnapshotTests` is *still* the only file in it (2026-08-09), so none of the conversion has happened; the bundle and its `SnapshotKitTesting` link are wired at `Project.swift:619-623` — add a file per view beside it, and it compiles into the module's own `PeriscopeToolsSnapshotTests` bundle (one image bundle per module, gathered into the shared `StuffSnapshotTests` scheme — root [`AGENTS.md`](../../AGENTS.md#targets)) while recording references here. The remaining work is per-view authoring, not wiring: each view needs a deterministic fixture (a frozen store, as `PeriscopeViewerSnapshotTests` does) and ideally a `SnapshotProviding` conformance in its own source file — which needs a `SnapshotKit` dependency on PeriscopeTools, since the module has no `#Preview`s at all today. `OpenSpansView` is the one view with a genuine determinism problem: its `TimelineView(.periodic(from: .now, by: 1))` ticking ages (`OpenSpansView.swift:19`) need the `\.isCapturingSnapshot` treatment. (Note the two `window != nil` checks in `Shared/LifecycleKit/Tests/` are *not* in scope: they assert the hosting helper's own lifecycle contract, which is the one place the check is the point. Inspector carries the same hosting-smoke debt — see [`Shared/Inspector/TODOs.md`](../Inspector/TODOs.md).) (pr#101 review) # Completed issues diff --git a/Shared/SnapshotKit/TODOs.md b/Shared/SnapshotKit/TODOs.md index c637137cc..78c2129b3 100644 --- a/Shared/SnapshotKit/TODOs.md +++ b/Shared/SnapshotKit/TODOs.md @@ -9,6 +9,6 @@ # Open issues ## P1s (Should do) -- fix: A case's content and captured models are instantiated once and shared across every configuration — `SnapshotCase.init` (`Sources/SnapshotCase.swift`) evaluates `content()` once into an `AnyView`, and the runner re-hosts that same value (and re-runs the same `onReadyToSnapshot` closure) for each of up to 10+ configurations (`SnapshotKitTesting`'s `AssertSnapshots.swift`). `@State` re-initializes per hosting, but reference-type models captured in the builder (`PreviewSupport.loadedYearReportModel()` and every provider like it) are shared: a `.task` side effect or a pre-capture hook mutation persists into all later configurations of the case — deterministic but surprising (variant N's reference bakes in variant 1's mutations), and nothing in the `SnapshotCase`/hook docs says content is built once per case rather than per configuration. Fix: store the content closure and rebuild per configuration (isolating state), or document the one-instance-per-case contract loudly on `SnapshotCase` and the hook. (From the July 2026 snapshot-testing PR review.) +- fix: A case's content and captured models are instantiated once and shared across every configuration. `SnapshotCase.content` is a lazy `AnyView` accessor (`Sources/SnapshotCase.swift:49-51`), but the runner reads it **once** per case (`SnapshotKitTesting/Sources/AssertSnapshots.swift:44-45`) and re-hosts that same value — and re-runs the same `onReadyToSnapshot` closure — for each of up to 10+ configurations (`:104-105`). `@State` re-initializes per hosting, but reference-type models captured in the builder (`PreviewSupport.loadedYearReportModel()` and every provider like it) are shared: a `.task` side effect or a pre-capture hook mutation persists into all later configurations of the case — deterministic but surprising (variant N's reference bakes in variant 1's mutations). **The docs now make it worse rather than merely silent:** `SnapshotCase.swift:47-48` states "Each access creates an independent view value for its configuration", which is true of the accessor and false of how the runner uses it, so a test author reading the type is actively told the isolation exists. Fix: rebuild per configuration by accessing `content` inside the configuration loop (isolating state), or correct that sentence and document the one-instance-per-case contract loudly on `SnapshotCase` and the hook. (From the July 2026 snapshot-testing PR review; re-verified and doc contradiction found 2026-08-09) # Completed issues diff --git a/Shared/SnapshotKitTesting/AGENTS.md b/Shared/SnapshotKitTesting/AGENTS.md index 96ed7b2e6..3300d0cc7 100644 --- a/Shared/SnapshotKitTesting/AGENTS.md +++ b/Shared/SnapshotKitTesting/AGENTS.md @@ -120,8 +120,11 @@ Complements the root [`AGENTS.md`](../../AGENTS.md) — read that first. fixed-point passes fails the assertion and skips comparison/recording; never bless the last arbitrary height. Guard: `LargeViewCaptureTests.rejectsNonConvergingBoundedScrollMeasurement`. -- **A settle phase costs its floor, not its passes.** Measured over all 260 - references with `SNAPSHOT_TIMING=1`: 192 captures sit at 0.25-0.35s, the +- **A settle phase costs its floor, not its passes.** Measured 2026-07-28 with + `SNAPSHOT_TIMING=1` over the **260** references of the time — the suite holds + **361** as of 2026-08-09, so re-measure before acting on the split below; + the *conclusion* (the floor dominates) is what to rely on, not the seconds. + Then: 192 captures sat at 0.25-0.35s, the `minDuration` floor plus a pass or two, and the floor accounts for ~70s of the ~84s of settle time. The render passes themselves are ~14s across the whole suite. So making passes cheaper is worth ~11% and removing floors is @@ -148,8 +151,9 @@ Complements the root [`AGENTS.md`](../../AGENTS.md) — read that first. - **Quiescence can't replace the pixel digest.** `SNAPSHOT_SETTLE` selects `pixel` (default), `quiescence` (a `beforeWaiting` run-loop observer plus a recursive `needsLayout`/`needsDisplay`/`animationKeys` walk), or `both`, which - runs them together and reports disagreements. Run in `both` mode over all 260 - references: 226 settle phases, 134 with some disagreement, and **8 where + runs them together and reports disagreements. Run in `both` mode (2026-07-28) + over the 260 references of the time — 361 today, so the counts below are that + run's, not current: 226 settle phases, 134 with some disagreement, and **8 where quiescence declared settled *earlier* than the digest** — every one a `Loaded_*` case whose content arrives late. That is the one dangerous direction (it would capture a frame no reference recorded), and it is what diff --git a/Shared/SnapshotKitTesting/TODOs.md b/Shared/SnapshotKitTesting/TODOs.md index bb7bfa396..cb094bb0a 100644 --- a/Shared/SnapshotKitTesting/TODOs.md +++ b/Shared/SnapshotKitTesting/TODOs.md @@ -9,22 +9,22 @@ # Open issues ## P1s (Should do) -- fix: Accessibility-parse failures kill the whole host process instead of failing one test — all four `parseAccessibility()` catch arms in `Sources/AccessibilitySnapshotViewController.swift` use `preconditionFailure`, but at least `containedViewExceedsMaximumSize` and `containedViewHasZeroSize` are reachable from ordinary test-author declarations (a large `.fullContent` frame with `snapshotType: .accessibility`, or a view that measures to zero) — user-level failures per the repo's "distinguish user failures from programmer errors" rule. Because all tests share one `StuffTestHost` process, one oversized accessibility case crashes the entire suite run on CI, with the diagnosis buried in a crash report. Fix: convert the declarable cases to a recorded `Issue` (returning a failure the caller skips), keeping `preconditionFailure` only for the genuinely impossible arms. (From the July 2026 snapshot-testing PR review.) +- fix: Accessibility-parse failures kill the whole host process instead of failing one test — all four `parseAccessibility()` catch arms use `preconditionFailure` (`Sources/AccessibilitySnapshotViewController.swift:58`, `:63`, `:67`, `:71`; a fifth at `:13` is outside the parse), but at least `containedViewExceedsMaximumSize` and `containedViewHasZeroSize` are reachable from ordinary test-author declarations (a large `.fullContent` frame with `snapshotType: .accessibility`, or a view that measures to zero) — user-level failures per the repo's "distinguish user failures from programmer errors" rule. Because all tests share one `StuffTestHost` process, one oversized accessibility case crashes the entire suite run on CI, with the diagnosis buried in a crash report. Fix: convert the declarable cases to a recorded `Issue` (returning a failure the caller skips), keeping `preconditionFailure` only for the genuinely impossible arms. (From the July 2026 snapshot-testing PR review.) - test: Close the missing regression coverage for load-bearing pipeline behaviors. (From the July 2026 snapshot-testing PR review.) - - Nonzero safe-area preset: `ConcurrentCaptureTests` now pixel-probes the swizzle's nonzero branch (20pt override through `renderSnapshotImage`), but the `iPhoneNotched` preset still has no coverage flowing through `assertSnapshots`' config mapping — no test or reference image exercises `SnapshotConfiguration.device.safeAreaInsets` end to end. - - `fullContent` lazy-container iteration: the fixed-point measurement loop (`Sources/SnapshotImageRendering.swift`, `resolveContentSize`) exists for lazy stacks that under-report until rows materialize, but `LargeViewCaptureTests` uses only non-lazy content (converges on pass 1) — neither the multi-iteration path nor the 10-iteration cap has coverage, so a toolchain change to `LazyVStack` estimation would regress "year cut off mid-October" undetected except via the app-side `calendar.FullYear` reference. + - Nonzero safe-area preset: `ConcurrentCaptureTests` now pixel-probes the swizzle's nonzero branch (20pt override through `renderSnapshotImage`, `ConcurrentCaptureTests.swift:97`), but the `iPhoneNotched` preset still has no coverage flowing through `assertSnapshots`' config mapping — no test or reference image exercises `SnapshotConfiguration.device.safeAreaInsets` end to end. + - `fullContent` lazy-container iteration: **partially closed.** The 10-iteration cap now has coverage — `LargeViewCaptureTests.rejectsNonConvergingBoundedScrollMeasurement` (`LargeViewCaptureTests.swift:174-205`) exercises the non-converging path. Still uncovered: the *multi-iteration convergence* path in `resolveContentSize` (`Sources/SnapshotImageRendering.swift`) for lazy stacks that under-report until rows materialize, since the remaining cases use non-lazy content that converges on pass 1 — so a toolchain change to `LazyVStack` estimation would regress "year cut off mid-October" undetected except via the app-side `calendar.FullYear` reference. - Env parsing is untestable as written: `simulatorMatchesSnapshotExpectations`, `environmentRecordMode`, and `environmentDiffTool` (`Sources/AssertSnapshots.swift`) read `ProcessInfo` directly and are `private` — zero tests, no seam to inject an environment dictionary. The record-mode-typo path ("must not quietly assert") deserves a test. - - Tile seams: `LargeViewCaptureTests` probes at unit-y 0.1/0.9 of 800pt and 3000pt views, so a stitching error localized at the 2000pt seam (or a view exactly 2000pt tall — the single-tile/threshold edge) would pass. A probe pair straddling y = 2000 would pin it. + - Tile seams: `LargeViewCaptureTests` probes at unit-y 0.1/0.9 of 800pt and 3000pt views (`:22-47`), so a stitching error localized at the 2000pt seam (or a view exactly 2000pt tall — the single-tile/threshold edge) would pass. A probe pair straddling y = 2000 would pin it. ## P1s (Should do) -- perf [needs-design]: The settle floor is the single largest remaining cost in the suite, and only about a third of it looks addressable. Measured over all 260 references with `SNAPSHOT_TIMING=1`: 192 captures are floor-bound (0.25-0.35s), the `minDuration` floor accounts for ~70s of the ~84s of settle time — roughly 54% of total capture time — and every render pass in the suite combined is ~14s. So making passes cheaper is worth little and the floor is worth everything, but introspection cannot shorten a floor (see the rejected experiments in [`AGENTS.md`](AGENTS.md)): the only route is a deterministic completion signal awaited from `onReadyToSnapshot`, as `root.LoggedIn` already does with `await launcher.run()`. +- perf [needs-design]: The settle floor is the single largest remaining cost in the suite, and only about a third of it looks addressable. **The numbers below are stale — re-measure before acting on them.** They were taken with `SNAPSHOT_TIMING=1` over the **260** references of the time; the suite holds **361** today (+39%, re-counted 2026-08-09), and Flyover added a `.settledAtLeast(1.5)` case worth 2 more configurations (`Shared/Flyover/SnapshotTests/FlyoverSnapshotTests.swift:20`) that the split below doesn't include. As measured then: 192 captures are floor-bound (0.25-0.35s), the `minDuration` floor accounts for ~70s of the ~84s of settle time — roughly 54% of total capture time — and every render pass in the suite combined is ~14s. So making passes cheaper is worth little and the floor is worth everything, but introspection cannot shorten a floor (see the rejected experiments in [`AGENTS.md`](AGENTS.md)): the only route is a deterministic completion signal awaited from `onReadyToSnapshot`, as `root.LoggedIn` already does with `await launcher.run()`. **Addressable (~22s):** the 22 configurations on `.settledAtLeast` — `YearView` and `LocationsView` (10 configurations each at 1.0s) and `RootView` (2 at 1.5s). The `root.LoggedIn` seam is already specced in [`Where/TODOs.md`](../../Where/TODOs.md); the other two need an equivalent "the report finished loading" signal on `YearReportModel`. **Probably not addressable (~48s):** the default 0.25s floor on the ~162 remaining screen captures. An earlier version of this item proposed dropping it for component-level cases that host no navigation or tab-bar chrome — **that group is empty.** Every `.component*` case already declares `settle: .immediate`, so the captures still paying the default floor are all screen-level, where the floor is waiting for exactly the iOS 26 glass toolbar/tab-bar material adaptation it was added for. Shortening it there needs either a per-case seam for each of ~40 cases or a signal for chrome adaptation that UIKit does not publish. Worth re-checking if one appears. (agent) ## P2s (Nice to have) - perf [needs-design]: A byte-equality fast path around `assertSnapshot` was measured and **declined** — recorded so it isn't re-proposed without new numbers. 49 of 52 captures are byte-identical to their references, so the hit rate is there, but the comparison is only ~7% of a capture (mean 35ms) once the drain stall is gone, capping the win at ~6% of the suite. Paying for it means letting `snapshotReferenceURL`'s replication of swift-snapshot-testing's private layout gate the pass/fail verdict: a wrong path there currently degrades to a `referenceMissing` diff line (harmless), but on the verdict path it would skip a real comparison and read as a pass. Revisit if the comparison's share grows or the library exposes its reference URL. (agent) -- fix: Cancellation mid-settle proceeds to capture and assert — `settleContent` now reports `.cancelled` (`Sources/SnapshotRenderingSupport.swift`) but `reportIfUnsettled` deliberately stays quiet on it, so a cancelled test (e.g. a future time-limit trait) still captures half-settled content and records a spurious image mismatch on top of the cancellation. Propagate the outcome out of `renderSnapshotImage` so `assertSnapshots` can skip the comparison entirely, keeping cancelled tests clean. (From the July 2026 snapshot-testing PR review.) -- fix: The duplicate-identifier guard only protects the provider overload of `assertSnapshots` (`Sources/AssertSnapshots.swift`) — the inline `assertSnapshots(of:named:configurations:)` overload accepts a `configurations` array containing duplicates and silently compares the second against the first's recording. Run the same guard over `[SnapshotCase(name:configurations:)]` there. (From the July 2026 snapshot-testing PR review.) +- fix: Cancellation mid-settle proceeds to capture and assert — `settleContent` now reports `.cancelled` but `reportIfUnsettled` deliberately stays quiet on it (`Sources/SnapshotRenderingSupport.swift:66-69`), so a cancelled test (e.g. a future time-limit trait) still captures half-settled content and records a spurious image mismatch on top of the cancellation. Propagate the outcome out of `renderSnapshotImage` so `assertSnapshots` can skip the comparison entirely, keeping cancelled tests clean. (From the July 2026 snapshot-testing PR review.) +- fix: The duplicate-identifier guard only protects the provider overload of `assertSnapshots` (`Sources/AssertSnapshots.swift:30-41`) — the inline `assertSnapshots(of:named:configurations:)` overload (`:63-75`) accepts a `configurations` array containing duplicates and silently compares the second against the first's recording. Run the same guard over `[SnapshotCase(name:configurations:)]` there. (From the July 2026 snapshot-testing PR review.) # Completed issues - fix: The reporting tests wrote **fabricated rows into the reports** — `./test --review` listed a reference that does not exist, at the top of the list, and `--timings` counted captures that never happened. `./test` recovers both channels by grepping `SNAPSHOT_DIFF` / `SNAPSHOT_TIMING` out of the run logs (and counts timing lines as images for the progress line), while `SnapshotDiffReporting.report(...)` and `SnapshotCaptureTiming.emit()` each encoded *and* printed in one function — so the tests pinning those wire formats emitted real lines. The diff fixture sorted first, because its numbers were borrowed from the genuine `swiftDataInspector` regression (max delta 203, 7430 pixels, 0.235%), which made the one row most demanding investigation the one that wasn't real; `SnapshotCaptureTimingTests` contributed five invented captures, so `./test --only SnapshotKitTestingTests --timings` reported "5 captures, 0.1s total, 0.024s per image" for a run that captured nothing, and `--everything --timings` blended those into the aggregate the suite's perf decisions are read off. The `SNAPSHOT_DIFF` env gate never helped: it is checked by the *pipeline*, not inside `report`. (Resolved: each channel is split so printing is the pipeline's alone and the payload is separately askable — `SnapshotDiffReporting.line(describing:…)`, `SnapshotCaptureTiming.line()`, and `SnapshotSettleReporting.line(…)` return the JSON without emitting, and all four test sites call those. `SnapshotSettleReporting` got the same treatment for symmetry, though nothing aggregates that channel yet. Verified both ways: a unit-only run now reports no timing lines and no differing captures, while a real snapshot run still produces the full phase breakdown and diff table. The rule is now an invariant in [`AGENTS.md`](AGENTS.md).) diff --git a/Shared/StuffCore/TODOs.md b/Shared/StuffCore/TODOs.md index 0d695963e..cc223d7fb 100644 --- a/Shared/StuffCore/TODOs.md +++ b/Shared/StuffCore/TODOs.md @@ -9,6 +9,6 @@ here. # Open issues ## P2s (Nice to have) -- test [quick-win]: The `version` test is tautological. Replace it once there is real API to assert against. (audit 2026-07-26) +- test [quick-win]: The `version` test is tautological — `#expect(StuffCore.version == 1)` (`Tests/StuffCoreTests.swift:5-7`). Replace it once there is real API to assert against. (audit 2026-07-26; re-verified 2026-08-09) # Completed issues diff --git a/TODOs.md b/TODOs.md index 447489ddb..47f6965d9 100644 --- a/TODOs.md +++ b/TODOs.md @@ -16,7 +16,10 @@ One item per bullet: ``` - **``** — a conventional-commit type: `feat`, `fix`, `refactor`, `perf`, - `test`, `docs`, `design`. + `test`, `docs`, `design`. Two repo additions are also in use and allowed: + `convention` (the code disagrees with a rule this repo has written down) and + `localization`. Documented here rather than renamed away, because both were + already load-bearing in filed items when this list was audited (2026-08-09). - **`()`** — the module the work lands in (`WhereUI`, `PeriscopeTools`, `Bumper`, …). One `TODOs.md` usually covers several modules, so the scope says which; omit it in a file that covers exactly one. @@ -92,13 +95,16 @@ inbox rather than here. - feat: Update the deployment target to iOS 27 — this lets us use `HistoryObserver` for CloudKit/SwiftData instead of the notification. Spans every target's minimum OS (`Package.swift`, `Project.swift`), so it sits here rather than in `Where/TODOs.md`. (human) ## P0s (Must do) -- fix(Bumper) [quick-win]: `where.gregorian_calendar` matches only an explicit `Calendar` base, so it enforces nothing. It filters `MemberAccessExprSyntax` on `base?.trimmedDescription == "Calendar"` (`.bumper/Sources/WhereProjectRules.swift:121`), which catches a spelled-out `Calendar.current` but not the implicit-member form (`calendar: Calendar = .current`, `startOfDay(in: .current)`) — and after the Gregorian call-site pass (`fe99dde`) the implicit form is the only one left in the tree. CI hard-gates `bumper lint` at `severity: .error` and is green, which confirms it: the rule reports nothing while production sites drift. Also match a no-base `MemberAccessExprSyntax` whose contextual type is `Calendar`, or add a lexical `.current` check scoped to calendar parameters and arguments. A rule that reads as enforced but enforces nothing is worse than a documented convention, because it stops anyone from looking. Pairs with the `CalendarDay.displayDate` P1 in [`Where/TODOs.md`](Where/TODOs.md). (audit 2026-07-26) +- fix(Bumper) [quick-win]: `where.gregorian_calendar` matches only an explicit `Calendar` base, so it enforces nothing. It filters `MemberAccessExprSyntax` on `base?.trimmedDescription == "Calendar"` (`.bumper/Sources/WhereProjectRules.swift:122-124`, rule at `:116-135`, `severity: .error` at `:118`), which catches a spelled-out `Calendar.current` but not the implicit-member form (`calendar: Calendar = .current`, `startOfDay(in: .current)`) — and after the Gregorian call-site pass (`fe99dde`) the implicit form is the only one left in the tree: **12 sites today**, four of them shipped production paths and eight in DEBUG snapshot/preview fixtures (enumerated in the `CalendarDay.displayDate` P1 in [`Where/TODOs.md`](Where/TODOs.md)). CI hard-gates `bumper lint` (`.github/workflows/ci.yml:67-68`) and is green, which confirms the rule reports none of them. **Why it has survived three audits:** the rule's own mutation test only ever feeds it a spelled-out `Calendar.current` (`.bumper/Tests/WhereProjectRulesTests.swift:164-171`), so the test passes for the same reason the rule fails — fix both together, and add an implicit-member case to the test first. Also match a no-base `MemberAccessExprSyntax` whose contextual type is `Calendar`, or add a lexical `.current` check scoped to calendar parameters and arguments. A rule that reads as enforced but enforces nothing is worse than a documented convention, because it stops anyone from looking. (audit 2026-07-26; re-verified 2026-08-09) ## P1s (Should do) +- test(Bumper) [quick-win]: Two of the four architecture-graph assertions have no mutation test. `.bumper/Tests/` covers `component_boundary` (`WhereArchitectureTests.swift:28-46`) and `forbidden_import` (`:49-67`, `:70-91`), and each of the ten source-level `where.*` rules has one in `WhereProjectRulesTests.swift` — but nothing exercises `duplicate_ownership` or `declared_dependency_cycle`, so neither has been shown to fail on a tree that violates it. That is a gap against this repo's own discipline, which requires the rule, its catalog entry, and its mutation test to land together (root [`AGENTS.md`](AGENTS.md#architecture-lint)). Note `.bumper/RULES.md:40-41` is *not* wrong here — its "the mutation tests prove…" sentence is scoped to imports, which are genuinely covered — so this is missing coverage, not a false claim. Add a mutation per rule: assign one source path to two components, and declare a cycle between two Where layers. An untested assertion is indistinguishable from one that silently passes everything, which is exactly how `where.gregorian_calendar` came to enforce nothing. (audit 2026-08-09) +- docs [quick-win]: Two feature-group folders are missing the doc pair the root [`AGENTS.md`](AGENTS.md#per-module-docs) requires of a module group spanning several targets. `Where/` has an `AGENTS.md` but **no `README.md`** — so the app with 11 modules and by far the most surface has no human-facing entry point at its root, while `Shared/Broadway/` and `Shared/Periscope/` both carry the pair. `Ledger/` has **neither**, though it groups the app target and `LedgerCore` (each of which has its own complete pair). Write the group-level `README.md` for `Where/` and both files for `Ledger/`, covering only what the group shares — the module graph and the invariants no single module owns — per the group rule, and without restating what the leaf docs already say. (audit 2026-08-09) ## P2s (Nice to have) -- refactor(Scripts) [needs-design]: The root dev scripts want an overhaul — there are ten now, and three of them duplicate the same xcodebuild plumbing. `./test`, [`profile`](profile), and [`flaky`](flaky) each resolve a destination, invoke `xcodebuild`, and parse an `.xcresult` with their own inline Python. Deliberately **not** consolidated when `./test` landed: the three genuinely want different things from a run (`profile` avoids formatters for timing fidelity and passes `-showBuildTimingSummary`; `flaky` needs `-test-iterations` and per-test re-runs; only `./test` wants a progress filter), so folding them into `./test` would bend a front door into a library, and extracting a shared helper introduces a sourced-library pattern none of the scripts use today. The duplication that actually caused harm was the *documented* invocation drifting between the docs, CI, and agent sessions, and `./test` now owns that; `profile` and `flaky` are report-only tools nobody copies commands from. Worth revisiting as part of a broader pass over the scripts rather than on its own, at which point the shared pieces are: destination resolution (already funnelled through [`simulator`](simulator)), the `xcresulttool get test-results tests` walk, and the `==>` / `error:` output conventions. (human 2026-07-28) -- refactor(Scripts) [needs-design]: Evaluate `tuist xcodebuild test-without-building` as a way to retire `./test`'s affected-bundle parser. `affected_bundles` ([`test`](test):189-352) is ~165 lines of Python that regex-parses `Project.swift` to work out which bundles a diff touches, and it is the most fragile thing in the script: it infers declaration boundaries from indent level (its own comment explains why the two obvious alternatives silently under-select, and `verify_parse` exits non-zero rather than degrade to "no bundle covers these changes"). It is also **local convenience only** — CI runs `--all` / `--snapshots`, so nothing in the pipeline depends on it. Tuist 4.200.5 ships `tuist xcodebuild test-without-building`, advertised as adding selective testing to an otherwise plain xcodebuild invocation, which is the only known way to get both that and the raw output `./test` needs. **Verify the output first:** if it pipes through xcbeautify like `tuist test` does, it is a non-starter for the two reasons in `./test`'s header comment, and the parser stays. Also confirm what it does with an empty hash cache on a fresh checkout, since that is the case CI is in. (agent 2026-07-28) +- feat(Scripts) [needs-design]: Teach [`test`](test) the native-macOS tier, or stop stating that it is the only entry point. The root [`AGENTS.md`](AGENTS.md#running-tests) says "**Use `./test`** — the only way to run tests. Never hand-roll `tuist test` or `xcodebuild`", but `./test` contains no reference to Ledger or a macOS destination anywhere in its 869 lines, so `LedgerCoreTests` — a real bundle with its own CI job — simply cannot be run through it. The [`running-tests`](.agents/skills/running-tests/SKILL.md) skill already documents the exception with the raw command (`SKILL.md:114-116`, `tuist test Ledger-macOS-Tests -- -destination 'platform=macOS'`), and `LedgerCore/AGENTS.md` gives its own variant, so the truth lives in two places while the always-applied root rule contradicts both. An agent that reads only the root file — which is the one guaranteed to be loaded — concludes Ledger's tests go through `./test`, and nothing tells it otherwise until the command fails. Either add a macOS tier to `./test` (it already resolves destinations through [`simulator`](simulator) for iOS, and a macOS run needs no device at all, so this is the smaller change than it looks) or make the root rule name the carve-out explicitly. **The doc half is done** — root `AGENTS.md` now points at the skill for the macOS bundle — so what remains is deciding whether the script should absorb the tier. (audit 2026-08-09) +- refactor(Scripts) [needs-design]: The root dev scripts want an overhaul — there are **13** now (re-counted 2026-08-09; `ten` when this was filed, with `tla-check` and `codex-watchdog` added since), and three of them duplicate the same xcodebuild plumbing. `./test`, [`profile`](profile), and [`flaky`](flaky) each resolve a destination, invoke `xcodebuild`, and parse an `.xcresult` with their own inline Python. Deliberately **not** consolidated when `./test` landed: the three genuinely want different things from a run (`profile` avoids formatters for timing fidelity and passes `-showBuildTimingSummary`; `flaky` needs `-test-iterations` and per-test re-runs; only `./test` wants a progress filter), so folding them into `./test` would bend a front door into a library, and extracting a shared helper introduces a sourced-library pattern none of the scripts use today. The duplication that actually caused harm was the *documented* invocation drifting between the docs, CI, and agent sessions, and `./test` now owns that; `profile` and `flaky` are report-only tools nobody copies commands from. Worth revisiting as part of a broader pass over the scripts rather than on its own, at which point the shared pieces are: destination resolution (already funnelled through [`simulator`](simulator)), the `xcresulttool get test-results tests` walk, and the `==>` / `error:` output conventions. (human 2026-07-28) +- refactor(Scripts) [needs-design]: Evaluate `tuist xcodebuild test-without-building` as a way to retire `./test`'s affected-bundle parser. `affected_bundles` ([`test`](test):196-359, in an 869-line script) is ~164 lines of Python that regex-parses `Project.swift` to work out which bundles a diff touches, and it is the most fragile thing in the script: it infers declaration boundaries from indent level (its own comment explains why the two obvious alternatives silently under-select, and `verify_parse` exits non-zero rather than degrade to "no bundle covers these changes"). It is also **local convenience only** — CI runs `--all` / `--snapshots`, so nothing in the pipeline depends on it. Tuist 4.200.5 ships `tuist xcodebuild test-without-building`, advertised as adding selective testing to an otherwise plain xcodebuild invocation, which is the only known way to get both that and the raw output `./test` needs. **Verify the output first:** if it pipes through xcbeautify like `tuist test` does, it is a non-starter for the two reasons in `./test`'s header comment, and the parser stays. Also confirm what it does with an empty hash cache on a fresh checkout, since that is the case CI is in. (agent 2026-07-28) - refactor [needs-design]: Vendor the local package through Tuist instead of Xcode's SPM integration, so package products become real Tuist targets. Today [`Project.swift`](Project.swift) uses `Package.local(path: .relativeToRoot("."))`, which emits an `XCLocalSwiftPackageReference` and hands the whole package to **Xcode's** SPM integration: every product links statically into each consumer, Tuist never sees the targets, and `PackageSettings` is inert. The alternative — the arrangement Tuist actually intends, and which other projects using it don't hit these duplication problems with — declares the local package as a dependency of a `Tuist/Package.swift` and consumes products with `.external(name:)`, so Tuist generates the targets and their product types and settings become ours to set. What it would buy: `PackageSettings` (per-product `.framework`/`.staticFramework`, per-target build settings), `Config(generationOptions: .options(enforceExplicitDependencies: true))` to catch the transitive-import looseness the test bundles lean on, resource bundles that stop being copied into every consumer (the full GeoJSON set is currently embedded per bundle), and retirement of the double-linking rule as a discipline. **Prototyped — blocked on a repo-layout prerequisite, not on the mechanism.** (spike 2026-07-26) - The blocker: Tuist cannot vendor a local package whose directory *is* the project directory. `tuist generate` dies with `Fatal error: Duplicate values for key: '/Users/kve/Development/Stuff4'`. Confirmed this is specifically the root collision rather than something else about this repo: pointing `Tuist/Package.swift` at a throwaway probe package elsewhere vendored fine and advanced to graph construction (failing only with `` `LifecycleKit` is not a valid configured external dependency ``, the correct next error). Projects that use this arrangement successfully avoid the collision purely by layout — the package at the repo root with the Tuist manifests in a subdirectory — where Stuff has both at the root. - Only one escape route exists. Moving the *package* into a subdirectory is not possible: SwiftPM rejects target paths outside the package root (`target 'Outside' in package 'pkg' is outside the package root`, verified with a minimal repro), and every target here points at `Where/…` / `Shared/…`. So the **Tuist manifests** would have to move into a subdirectory, rewriting every source glob in `Project.swift` plus `./ide`, `profile`, `.github/workflows/ci.yml`, and the docs. The vendored mode also adds a `tuist install` step before generate. diff --git a/Where/RegionKit/README.md b/Where/RegionKit/README.md index 8f4ad4538..c1039b8f5 100644 --- a/Where/RegionKit/README.md +++ b/Where/RegionKit/README.md @@ -158,6 +158,12 @@ Everything downstream (`RegionStyle`, region pickers, the App Intents Swift Testing in [`Tests/`](Tests), hosted in `StuffTestHost` (so `Bundle.module` resolves the GeoJSON at runtime). Attribution, geometry -(point-in-polygon, bounding box, longitude span), GeoJSON decoding, and the -geometry catalog are covered here; internal types (`GeoJSON`, `GeoPolygon`, -`RegionPolygons`) are reached via `@testable import RegionKit`. +(point-in-polygon, bounding box, longitude span), and the geometry catalog are +covered here; internal types (`GeoJSON`, `GeoPolygon`, `RegionPolygons`) are +reached via `@testable import RegionKit`. + +**GeoJSON *decoding* is not covered** — there is no `GeoJSONTests.swift`, so the +unsupported-geometry throw and the malformed-coordinate drop are unexercised, and +`RegionCatalog`'s degrade-to-empty-catalog path is asserted only at the log-event +level. Filed in [`Where/TODOs.md`](../TODOs.md); this paragraph goes away when it +closes. diff --git a/Where/RegionViewer/README.md b/Where/RegionViewer/README.md index 101116460..0f8d69a5f 100644 --- a/Where/RegionViewer/README.md +++ b/Where/RegionViewer/README.md @@ -14,9 +14,13 @@ The same screen as the in-app **developer overlay → Region map** entry: - **Attribution** — the simplified polygons `RegionAttributor` actually loads and uses to attribute coordinates today (California, New York, and the simplified Canada / EU outlines; exterior rings only). - - **Source** — every feature decoded straight from the bundled GeoJSON files - (all US-state features in `us-states.geojson`, plus Canada and the EU) at - full authored fidelity. + - **Source** — every feature decoded straight from the bundled per-region + GeoJSON files at full authored fidelity. `RegionGeometryCatalog`'s + `buildSourceOutlines()` walks `RegionCatalog.shared.entries` and decodes each + region's own file under `RegionKit/Sources/Resources/regions/`, so this mode + shows exactly what ships. (The monolithic `us-states.geojson` under + `RegionKit/Tools/source/` is a build-time input to the extraction tooling — + never bundled, never read at runtime.) - `MapPolygon` overlays tinted by `RegionStyle` for known regions and a stable per-title color for unmapped source features. - A camera framed to the shown geometry, and a filterable legend that narrows diff --git a/Where/TODOs.md b/Where/TODOs.md index 332686eaa..cc7265dc5 100644 --- a/Where/TODOs.md +++ b/Where/TODOs.md @@ -17,33 +17,34 @@ The item format and the placement rule live in the root - fix(WhereCore): Nothing gets recorded on a day with no movement — presumably because background updates ride on GPS. Any way to guarantee a daily boot outside of GPS? (human) ## P0s (Must do) -- fix(WhereCore) [needs-design]: `DailySummaryReconciler.reconcile()` is absent from the post-day-change fan-out. Local GPS and `DayJournal` writes still fan out to issue state and widgets only, so the daily notification body stays stale until a foreground re-`configure`; backup and remote imports now use the composition root's full `DerivedDataReconciler`. Add it to the local fan-out or document the foreground-only policy. Verified canonical fan-out ordering in [`Specifications/PostWriteReconcile`](Specifications/PostWriteReconcile/README.md); summary reconciliation remains out of model scope until routed. (audit 2026-07-26; PR #160 narrowed scope) - - test(WhereCore) [quick-win]: Mutate data and assert the summary notification body updates without a re-`configure`. (audit 2026-07-26) +- fix(WhereCore) [needs-design]: `DailySummaryReconciler.reconcile()` is absent from the post-day-change fan-out. `DayJournal.reconcileAfterDayDataChange()` (`DayJournal.swift:70-74`) fans out to issue state and widgets only, and the live-GPS hot path (`WhereServices.swift:197-209`) does the same, so the daily notification body stays stale until a foreground re-`configure`; backup, remote-import, and reset paths do reconcile summary through the composition root's `DerivedDataReconciler` (`WhereServices.swift:13-21`, `:266-268`, and `reset()` at `:407`). Add it to the local fan-out or document the foreground-only policy. The canonical ordering in [`Specifications/PostWriteReconcile`](Specifications/PostWriteReconcile/README.md:36) deliberately excludes summary until it is routed, so the spec is not the authority for closing this. (audit 2026-07-26; PR #160 narrowed scope; re-verified 2026-08-09) + - test(WhereCore) [quick-win]: Mutate data and assert the summary notification body updates without a re-`configure`. The nearest existing guard covers the *remote-import* derived-data fan-out (`WhereServicesTests.swift:541-560`), not a local day mutation, so it would not catch this. (audit 2026-07-26) - perf(WhereCore) [needs-design]: Performance pass — how often is the app booting? Can we only do it on changes of, say, 1 km or more? (human) ## P1s (Should do) - fix(WhereCore) [needs-design]: Scope initial CloudKit-import readiness to Where's expected store/container. `CloudKitImportReadiness.start()` observes `NSPersistentCloudKitContainer.eventChangedNotification` with `object: nil`, and `eventChanged(_:)` accepts any successful completed import (`WhereCore/Sources/Persistence/CloudKitImportReadiness.swift:19-49`), while discovery starts that observer before `WhereLaunch.prepareStore()` creates the intended store (`WhereUI/Sources/Launch/WhereLaunch.swift:326-333`). An unrelated CloudKit-backed store in the process could therefore release onboarding against an incomplete device list. Bind readiness to the container/store created for this launch (or return its initial-import completion directly from store preparation), ignore unrelated notifications, and cover that filtering with tests. (pr#160 review) - refactor(WhereUI) [quick-win]: Remove `StoredContext.CodingKeys`; it lists every property under the identical synthesized key and the installation-context sidecar has no shipped compatibility shape to preserve (`WhereUI/Sources/Launch/InstallationRecordingContextStore.swift:138-158`). Let the compiler synthesize the keys and retain the existing persistence round-trip coverage as the wire-shape guard. (pr#160 review) - feat(Where) [needs-design]: Add an optional onboarding step that backfills the current year from the GPS metadata of photos in the user's library. `OnboardingView.Phase` currently moves from region selection/customization directly to location permission (`WhereUI/Sources/Onboarding/OnboardingView.swift:30`), while `DayJournal.ingest(_:)` is the existing bulk sample path (`WhereCore/Sources/Journal/DayJournal.swift:82`). Design a PhotoKit-backed importer that requests access only after an explicit opt-in, reads location and capture time locally without uploading photo contents, previews what will be added, records photo-derived provenance rather than treating it as live GPS, deduplicates repeat imports, and makes skipping the screen frictionless. (human 2026-08-03) -- refactor(WhereCore) [needs-design]: Scope diagnostic emission so Flyover's unactivated sibling demo world cannot write its activity through the process-global `WhereLog` / `Periscope.shared` facade into the active real scope's durable diagnostic store. `WhereFlyoverWorld.build()` correctly gives the sibling a private `Periscope` and never starts its sink, but static `WhereLog` channels still bypass that injection; carry the scope's logging system through services/models or add a task-/environment-scoped routing context before treating Flyover's diagnostic activity as isolated. Domain data, preferences, widgets, notifications, and location remain in memory/no-op already. (`WhereUI/Sources/Developer/Flyover/WhereFlyoverWorld.swift`, `WhereCore/Sources/Logging/WhereLog.swift`; agent 2026-07-29) -- fix(WhereUI) [quick-win]: `CalendarDay.displayDate` resolves through `Calendar.current` (`DateRangeFormatting.swift:33`), so every day label that flows through it — relabel, logged days, resolution details, the region drill-in — renders a wrong date on a non-Gregorian device: `startOfDay(in:)` interprets the day's Gregorian Y-M-D as *that* calendar's components, so a Buddhist-era device resolves 2026-07-26 to a date ~543 years off. `DateRangeFormatting.abbreviated` (`:6`, `:19`) and `PresenceTimeline.stints` (`PresenceTimeline.swift:37`) also *default* to `.current`, and `PresenceTimelineList` (`:12`) doesn't pass `report.calendar`. Take an explicit calendar (Gregorian + current time zone) in the helper and thread the report's calendar from the call sites. The `where.gregorian_calendar` Bumper rule that should catch this is blind to the implicit-member form — filed in the root [`TODOs.md`](../TODOs.md). (audit 2026-07-26) -- fix(WhereCore) [needs-design]: `WhereServices.setPrimaryRegions(_:)` (`:285`) commits atomically but skips `DayJournal.reconcileAfterDayDataChange()` — widgets/reminders/summary don't refresh until foreground/configure. Route picker commits through the unified fan-out, or document the intentional deferral. Out of scope for [`PostWriteReconcile`](Specifications/PostWriteReconcile/README.md) until routed. (audit 2026-07-26) -- fix(WhereCore) [needs-design]: Soft-delete untracked regions. `SwiftDataStore.setTrackedRegion(false)` (`:756`, in-source TODO at `:773`) and `setPrimaryRegions` (`:833`) hard-delete the row, which drops the region from the attributor's load set — so re-aggregating a past year re-attributes that region's GPS days to `.other`. The `SwiftDataStore` TODO filed this as "when the region picker ships"; it has shipped, and both the onboarding picker and the Settings region editor now reach the delete, so this is user-reachable rather than latent. Retain the row for attribution and hide it from the pickers instead. (audit 2026-07-26) +- refactor(WhereCore) [needs-design]: Scope diagnostic emission so Flyover's unactivated sibling demo world cannot write its activity through the process-global `WhereLog` / `Periscope.shared` facade into the active real scope's durable diagnostic store. `WhereFlyoverWorld.build()` correctly gives the sibling a private `Periscope` and never starts its sink, but static `WhereLog` channels still bypass that injection; carry the scope's logging system through services/models or add a task-/environment-scoped routing context before treating Flyover's diagnostic activity as isolated. Domain data, preferences, widgets, notifications, and location remain in memory/no-op already. (`WhereUI/Sources/Developer/Flyover/WhereFlyoverWorld.swift:34-37` builds the private, sink-less `Periscope`; `WhereCore/Sources/Logging/WhereLog.swift:17-32` is the static facade that bypasses it; agent 2026-07-29, re-verified 2026-08-09) +- fix(WhereUI) [quick-win]: `CalendarDay.displayDate` resolves through `Calendar.current` (`DateRangeFormatting.swift:33`), so every day label that flows through it — relabel, logged days, resolution details, the region drill-in — renders a wrong date on a non-Gregorian device: `startOfDay(in:)` interprets the day's Gregorian Y-M-D as *that* calendar's components, so a Buddhist-era device resolves 2026-07-26 to a date ~543 years off. **Exactly four production sites remain** (re-counted 2026-08-09): the `displayDate` body above, the two `calendar: Calendar = .current` defaults on `DateRangeFormatting` (`:6`, `:19`), and the same default on `PresenceTimeline.stints` (`PresenceTimeline.swift:37`). Call sites that take those defaults rather than threading `report.calendar`: `PresenceTimelineList.swift:20` (unchanged by the PR #200 timeline rewrite) and `ResolutionView.swift:181`, plus every `displayDate` consumer (`ResolutionView.swift:183`, `:190`, `:201`, `DayRelabelView.swift:219`, `RegionDaysView.swift:134`). Take an explicit calendar (Gregorian + current time zone) in the helpers and thread the report's calendar from the call sites. A further **eight** implicit `.current` uses are inside `#if DEBUG` `SnapshotProviding`/`#Preview` fixtures (`ManualDayView.swift:510`, `:539`, `:552`; `DayRelabelView.swift:265`, `:277`; `FlightDayDetailView.swift:181`; `AbruptChangeDetailView.swift:97`, `:102`) — they can't affect a shipped label, so they are not part of this fix, but they are equally invisible to the lint rule. Do **not** count `calendar.timeZone = .current` (`YearReportModel.swift:253`, `WhereModel.swift:274`, `RemindersSettingsModel.swift:153`) — that is a `TimeZone` on an explicit Gregorian calendar, which is the correct pattern. The `where.gregorian_calendar` Bumper rule that should catch the four real sites is blind to the implicit-member form — filed in the root [`TODOs.md`](../TODOs.md). (audit 2026-07-26; re-verified 2026-08-09) +- fix(WhereCore) [needs-design]: `WhereServices.setPrimaryRegions(_:)` (`WhereServices.swift:382-386`) commits atomically but skips `DayJournal.reconcileAfterDayDataChange()` — widgets/reminders/summary don't refresh until foreground/configure. Region *attribution* does rebuild, because it observes `changes()` (`RegionAttribution.swift:72-75`), which is why the stale surfaces are only the scheduled/published ones. Route picker commits through the unified fan-out, or document the intentional deferral. Out of scope for [`PostWriteReconcile`](Specifications/PostWriteReconcile/README.md) until routed. (audit 2026-07-26; re-verified 2026-08-09) +- fix(WhereCore) [needs-design]: Soft-delete untracked regions. `SwiftDataStore.setTrackedRegion(false)` (`SwiftDataStore.swift:1773`, in-source TODO at `:1792-1800`, delete at `:1801-1803`) and `setPrimaryRegions` (`:1854`, delete-by-omission at `:1864`) hard-delete the row, which drops the region from the attributor's load set — so re-aggregating a past year re-attributes that region's GPS days to `.other` (manual days, stored as region sets, are unaffected). The `SwiftDataStore` TODO filed this as "when the region picker ships"; it has shipped, and both the onboarding picker and the Settings region editor reach it (`PrimaryRegionSelectionModel.swift:158` → `setPrimaryRegions`), so this is user-reachable rather than latent. Retain the row for attribution and hide it from the pickers instead. (audit 2026-07-26; re-verified 2026-08-09) - fix(WhereCore) [needs-design]: ~~`DayJournal.ingest(_:)` (`:70`), the bulk ingest (`:82`), and `addManualSample` (`:93`) publish widgets but skip the reminder/issue reconcile~~ — **fixed:** single-sample ingest and `addManualSample` now fan out through `reconcileIssueState()` + `publishAfterIngest(of:)`; bulk ingest uses full `reconcileAfterDayDataChange()` (see [`PostWriteReconcile`](Specifications/PostWriteReconcile/README.md)). Remaining fan-out gaps: `DailySummaryReconciler` (P0 above) and `setPrimaryRegions` (above). (audit 2026-07-26; fixed 2026-08-04) -- fix(WhereCore) [needs-design]: The retry queue evicts FIFO at capacity and drops samples with a warning only (`LocationIngestor.swift:349`). Decide the capacity policy and whether eviction warrants user-visible degradation, then document it. (audit 2026-07-26) -- fix(WhereUI) [quick-win]: `PresenceTimelineList` returns `[]` whenever `report.report` is nil (`:12`), so the Timeline segment of Your Year renders the "no stays" empty state while the year is still loading (and during a year switch) — unlike the Calendar segment beside it, which gates on `loadState`. (audit 2026-07-26) - - refactor(WhereUI) [needs-design]: Extract a shared `ReportLoadGate`. The same `YearReportModel.loadState` gate is copy-pasted across `LocationsView.swift:60`, `ElsewhereView.swift:50`, `ResolutionView.swift:58`, and `CalendarContentView.swift:60`, and `PresenceTimelineList` skipped it entirely (above). One gate view would cover all five. (audit 2026-07-26) -- fix(WhereUI) [quick-win]: The Elsewhere entry card renders raw inflection markup instead of an agreed region count — it shows literally `^[3 region](inflect: true)`. `locations.elsewhere.subtitle` is authored for automatic grammar agreement (`^[%lld region](inflect: true)`), but the string-catalog compiler passes that markup through **verbatim** into the compiled `Localizable.strings` (unlike a real plural such as `primary.elsewhereOnly.description`, which compiles to an `NSStringLocalizedFormatKey` dict), and flattening the resource to a `String` never runs the inflection engine. Pre-existing — the catalog entry is byte-identical on `main` and predates the String Catalog symbol migration. Fix by either rendering the resource directly so SwiftUI applies inflection (`Text(.locationsElsewhereSubtitle(regionCount))` in `ElsewhereSummaryCard`, dropping the `WhereFormat` hop) or replacing the markup with an explicit plural variation. `WhereFormatTests.elsewhereCardSubtitleInflectsTheRegionCount` pins the expected output behind `withKnownIssue`, so it trips as soon as this is fixed. The bug is also baked into the `locations.Loaded_iPad.png` reference (ledgered in the broken-snapshots cluster below) — re-record that image when this lands. (agent) -- refactor(WhereUI) [needs-design]: Split `WhereSession` into an always-on coordinator + a presentation view-model whose lifetime scopes its subscriptions. **Partial progress (July 2026):** `YearReportModel` is now scene-scoped in `MainTabs` — `activate()` / `deactivate()` on `scenePhase` drive `observeDataChanges()` and refresh, closing the headless-relaunch rescan leak that previously wired the subscription through launch `syncAuth`. `ResolveModel`, `BackupModel`, and `RemindersSettingsModel` are already view-scoped. Remaining: the coordinator is still ~460 lines mixing tracking intent, authorization, reset, and region-style mirrors; finish extracting presentation collaborators and drive any leftover reactive work from scene lifetime. (agent) -- test(WhereUI) [quick-win]: `ManualDayView`'s range mode has no test coverage — including its capture-only code. The deleted `manualDayViewHostsAddModes` hosted a *range-prefilled* add (two `DatePicker`s), but the `addPrefill` snapshot case in `ManualDayView.swift` is a single day (`start == end` → `dateSpan = .singleDay`), so no test ever renders the `.range` branch — live or stand-in. The range stand-in code has never executed, and the From/Through picker row rendering is unpinned. Fix: add an `AddRange` snapshot case with a multi-day `MissingDayRange` prefill (the Resolve backfill flow the deleted test existed for). (From the July 2026 snapshot-testing PR review.) -- test(WhereUI) [needs-design]: `RegionMapView`'s live `Map` branch is no longer constructed by any test. The deleted `regionMapViewHosts` mounted the real MapKit `Map` (polygon building via `clLocationCoordinates`, `mapStyle`); under capture the view always takes the `SnapshotMapStandIn` branch, so a crash or regression in the production map path — which every real user sees — would ship untested. The stand-in substitution is what the framework carve-out sanctions; the gap is purely coverage. Fix: keep one lightweight hosting test for the live branch in `WhereUITests` (this specific surface is the exception the "no hosting smoke tests" rule shouldn't swallow) — until then, this entry records the accepted gap. (From the July 2026 snapshot-testing PR review.) -- refactor(WhereUI) [needs-design]: Extract one shared region-selection form. `DayRelabelView.swift:108` renders a flat region list where `ManualDayView.swift:218` has grouped sections plus `loadGrouping()` (`:255`), so the two screens disagree on how regions are picked. (audit 2026-07-26) -- fix(WhereUI) [needs-design]: Notification authorization is requested during launch, with no context and unprompted. The launch's detached fan (`WhereLaunch`'s `reminders` / `summary` / `issue-alerts` steps → `session.applyReminderConfiguration()` etc. → the `services.reminders` scheduler) reaches the notification center while the app is still launching, so a fresh install shows the system "Where Would Like to Send You Notifications" alert over the splash — before the user has expressed any interest in reminders and with no in-app rationale. Observed on a fresh-install simulator screen recording: the alert lands roughly a second after the splash appears and then sits on top of the revealed app. Ask in context instead — request when the user turns reminders/summary on in Settings (or immediately after onboarding, with a sentence of explanation) — and have the launch fan only *reconcile* schedules against authorization that was already granted, never trigger the prompt. (agent) +- fix(WhereCore) [needs-design]: The retry queue evicts FIFO at its 1000-sample capacity and drops samples with a warning only (`LocationIngestor.swift:497-502`, event at `LocationIngestorLog.swift:76-77`). Decide the capacity policy and whether eviction warrants user-visible degradation, then document it. (audit 2026-07-26; re-verified 2026-08-09) +- fix(WhereUI) [quick-win]: `PresenceTimelineList` returns `[]` whenever `report.report` is nil (`PresenceTimelineList.swift:19-22`), so the Timeline segment of Your Year renders the "no stays" empty state while the year is still loading (and during a year switch) — unlike the Calendar segment beside it, which gates on `loadState` (`CalendarContentView.swift:52-54`). Survived the PR #200 timeline rewrite untouched. (audit 2026-07-26; re-verified 2026-08-09) + - refactor(WhereUI) [needs-design]: Extract a shared `ReportLoadGate`. The same `YearReportModel.loadState` gate is copy-pasted across `LocationsView.swift:81`, `ElsewhereView.swift:50`, `ResolutionView.swift:58`, and `CalendarContentView.swift:52`, and `PresenceTimelineList` skips it entirely (above). One gate view would cover all five. (audit 2026-07-26) +- fix(WhereUI) [quick-win]: The Elsewhere entry card renders raw inflection markup instead of an agreed region count — it shows literally `^[3 region](inflect: true)`. `locations.elsewhere.subtitle` is authored for automatic grammar agreement (`^[%lld region](inflect: true)`), but the string-catalog compiler passes that markup through **verbatim** into the compiled `Localizable.strings` (unlike a real plural such as `primary.elsewhereOnly.description`, which compiles to an `NSStringLocalizedFormatKey` dict), and flattening the resource to a `String` never runs the inflection engine. Pre-existing — the catalog entry is byte-identical on `main` and predates the String Catalog symbol migration. Fix by either rendering the resource directly so SwiftUI applies inflection (`Text(.locationsElsewhereSubtitle(regionCount))` in `ElsewhereSummaryCard.swift:28`, dropping the `WhereFormat.elsewhereCardSubtitle` hop at `WhereFormat.swift:49-50`) or replacing the markup with an explicit plural variation. `WhereFormatTests.elsewhereCardSubtitleInflectsTheRegionCount` (`WhereFormatTests.swift:43-52`) pins the expected output behind `withKnownIssue`, so it trips as soon as this is fixed. The bug is also baked into the `locations.Loaded_iPad.png` reference (ledgered in the broken-snapshots cluster below) — re-record that image when this lands. (agent) +- refactor(WhereUI) [needs-design]: Split `WhereSession` into an always-on coordinator + a presentation view-model whose lifetime scopes its subscriptions. **Partial progress (July 2026):** `YearReportModel` is now scene-scoped in `MainTabs` — `activate()` / `deactivate()` on `scenePhase` drive `observeDataChanges()` and refresh, closing the headless-relaunch rescan leak that previously wired the subscription through launch `syncAuth`. `ResolveModel`, `BackupModel`, `RemindersSettingsModel`, and now `DevicesSettingsModel` are view-scoped. Remaining: the coordinator is **636 lines** as of 2026-08-09 — it has *grown* past the ~460 recorded when this was filed, because PR #160's multi-device recording landed on it — and still mixes recording runtime, authorization, reset, the launch-time notification reconcile, region-style mirrors, and device rejoin (its own header comment inventories them at `WhereSession.swift:6-30`). Finish extracting presentation collaborators and drive any leftover reactive work from scene lifetime. (agent; re-measured 2026-08-09) +- test(WhereUI) [quick-win]: `ManualDayView`'s range mode has no test coverage — including its capture-only code. The deleted `manualDayViewHostsAddModes` hosted a *range-prefilled* add (two `DatePicker`s), but the `addPrefill` snapshot case is still a single day (`ManualDayView.swift:509-511`, `start == end` → `dateSpan = .singleDay`), so no test ever renders the `.range` branch (`:198-211`) — live or stand-in. The range stand-in code has never executed, and the From/Through picker row rendering is unpinned. Fix: add an `AddRange` snapshot case with a multi-day `MissingDayRange` prefill (the Resolve backfill flow the deleted test existed for). (From the July 2026 snapshot-testing PR review.) +- test(WhereUI) [needs-design]: `RegionMapView`'s live `Map` branch is no longer constructed by any test. The deleted `regionMapViewHosts` mounted the real MapKit `Map` (polygon building via `clLocationCoordinates`, `mapStyle`); under capture the view always takes the `SnapshotMapStandIn` branch (`RegionMapView.swift:244-247`), so a crash or regression in the production map path — which every real user sees — would ship untested. The stand-in substitution is what the framework carve-out sanctions; the gap is purely coverage. Fix: keep one lightweight hosting test for the live branch in `WhereUITests` (this specific surface is the exception the "no hosting smoke tests" rule shouldn't swallow) — until then, this entry records the accepted gap. (From the July 2026 snapshot-testing PR review.) +- refactor(WhereUI) [needs-design]: Extract one shared region-selection form. `DayRelabelView.swift:108-110` renders a flat `ForEach(regionSelection.items)` where `ManualDayView.swift:218-240` has `GroupedRegionSections` plus `loadGrouping()`, so the two screens disagree on how regions are picked. (audit 2026-07-26; re-verified 2026-08-09) +- fix(WhereUI) [needs-design]: Notification authorization is requested during launch, with no context and unprompted. The chain, re-verified 2026-08-09: the launch's detached `reminders` / `summary` / `issue-alerts` steps (`WhereLaunchSteps.swift:175`, `:186`, `:198`) call `WhereSession.apply*Configuration()` (`WhereSession.swift:533-594`), which calls each reconciler's `configure`, and each one requests authorization whenever its feature is enabled (`ReminderReconciler.swift:82`, `DailySummaryReconciler.swift:48`, `DataIssueAlertReconciler.swift:49` → `UNUserNotificationCenter.requestAuthorization`). **All three preferences default to `true` on a fresh install** (`WherePreferences.remindersEnabled`, `summaryEnabled`, `issueAlertsEnabled`, each `?? true` and documented as "active out of the box"), which is *why* a first launch prompts — so the fix has to reckon with the defaults, not just the call site. It reaches the notification center while the app is still launching, so a fresh install shows the system "Where Would Like to Send You Notifications" alert over the splash — before the user has expressed any interest in reminders and with no in-app rationale. Observed on a fresh-install simulator screen recording: the alert lands roughly a second after the splash appears and then sits on top of the revealed app. Ask in context instead — request when the user turns reminders/summary on in Settings (or immediately after onboarding, with a sentence of explanation) — and have the launch fan only *reconcile* schedules against authorization that was already granted, never trigger the prompt. (agent) - refactor(WhereCore) [needs-design]: Rewrite the controller layer as a state machine so invariants can't exist. (human) -- test(WhereIntents) [quick-win]: The per-intent `perform()` glue — guards, snippet wiring, error→dialog mapping — is untested, because `@Dependency` traps outside the perform flow. Either extract a thin testable seam or say so in `README.md`; the reader/writer seams themselves are now well covered. (audit 2026-07-26) -- fix(WhereUI) [needs-design]: Give the feature-discovery widget gallery a complete VoiceOver pass. `WidgetPreviewFrame` exposes only its widget content (`WhereUI/Sources/Settings/FeaturePreviews/Widgets/WidgetPreviewFrame.swift:15-22`), while the Home Screen and Lock Screen galleries repeat that frame without identifying the family (`FeaturePreviews/Widgets/FeatureHomeScreenPreview.swift:13-28`, `FeaturePreviews/Widgets/FeatureLockScreenPreview.swift:20-37`), so VoiceOver announces duplicate content such as “Days in 2026” without distinguishing Small from Medium or Inline from Circular and Rectangular. Define localized, combined accessibility elements for every framed preview, including its widget kind and system family, and review the surrounding miniature-screen semantics alongside the other planned widget accessibility improvements. (pr#204 review) +- test(WhereIntents) [quick-win]: The per-intent `perform()` glue — guards, snippet wiring, error→dialog mapping — is untested, because `@Dependency` traps outside the perform flow: no test in `Where/WhereIntents/Tests/` calls an intent's `perform()`. **Half closed:** `WhereIntents/AGENTS.md:95-108` now states the rule and explains the framework trap, so the "say so" option is satisfied for agents — but `README.md:106-112` still reads as though the coverage is complete and never mentions the limitation, so a human reading only the README is misled. Either extract a thin testable seam, or carry the AGENTS.md caveat into the README. (audit 2026-07-26; re-verified 2026-08-09) +- fix(WhereUI) [needs-design]: Give the feature-discovery widget gallery a complete VoiceOver pass. `WidgetExampleFrame` (renamed from `WidgetPreviewFrame` since this was filed) exposes only its widget content with no accessibility of its own, while the Home Screen and Lock Screen galleries label the *whole* miniature screen rather than each family (`FeatureHomeScreenExample.swift:44-45`, `FeatureLockScreenExample.swift:55-56`), so VoiceOver announces duplicate content such as “Days in 2026” without distinguishing Small from Medium or Inline from Circular and Rectangular. Define localized, combined accessibility elements for every framed example, including its widget kind and system family, and review the surrounding miniature-screen semantics alongside the other planned widget accessibility improvements. (pr#204 review; re-verified 2026-08-09) + - fix(WhereUI) [quick-win]: The evidence feature-discovery panels have the same gap in a weaker form: `FeatureShareSheetPreview.swift:70` and `FeatureEvidenceArchivePreview.swift:65` apply `.accessibilityElement(children: .combine)` with **no** `.accessibilityLabel`, so VoiceOver reads each walkthrough step as one undifferentiated blob of its concatenated text. `FeatureEvidenceComposePreview.swift:44` uses `.contain`, which is better but still unlabeled. `SiriIntentCard.swift:31-36` is the pattern to copy — it sets both a label and a value. (audit 2026-08-09) - refactor(WhereShareExtension) [needs-design]: Consolidate the share/add evidence form. `ShareEvidenceView.swift:68` and `AddEvidenceView.swift:37` are parallel implementations over parallel catalog namespaces (`share.form.*` / `evidence.form.*`). (audit 2026-07-26) -- perf(WhereCore) [needs-design]: Consider incremental year-report reads or memoization for the widget/reminder/summary hot paths — `ReportReader.yearReport:27` and `WidgetDataReader.snapshot:85` re-aggregate a full year each time. (audit 2026-07-26) +- perf(WhereCore) [needs-design]: Consider incremental year-report reads or memoization for the widget/reminder/summary hot paths — `ReportReader.yearReport` (`ReportReader.swift:37`) and `WidgetDataReader.snapshot(asOf:)` (`WidgetDataReader.swift:88-103`) re-aggregate a full year each time. (audit 2026-07-26; re-verified 2026-08-09) - refactor(WhereUI): What's with all the `.accessibilityIdentifier(…)` modifiers, do we need them? (human) - feat(WhereUI): Add a UI that represents where you currently are — maybe a border on the current location card? (human) - refactor(WhereCore) [needs-design]: Per-entity schema versioning + lazy upcasting for CloudKit sync drift. There is intentionally **no** boot-time data migration or on-read legacy recovery (removed pre-release as over-built for a single dev's data). Today a data-shape change relies solely on a one-time manual backup **export → transform (`Tools/upgrade-backup.rb`) → replace-import** to rewrite rows into the current shape; `SD….toValue()` reads only the current shape and drops (fault-logs) a row it can't place (e.g. an `SDManualDay` with no `dayKey`). Gaps this leaves, which a general mechanism should close: an old-build device can sync in an old-shaped entity at any time (not just at launch), and until it's re-imported such a row is dropped on read rather than upcast. Replace with: (agent) @@ -52,42 +53,39 @@ The item format and the placement rule live in the root - refactor(WhereCore): Durable write-back is **read-repair**, decoupled from read correctness: opportunistically (batched, on `.NSPersistentStoreRemoteChange` + launch) rewrite stale records to the current version and stamp it, so old builds can honor exclusion. Transforms must be deterministic + commutative so two devices healing the same record via CloudKit converge (LWW-safe). (agent) - design(WhereCore): Open question — the exclusion UX, where an older device progressively hides days a newer device has touched, needs a deliberate warning surface, not a silent drop. (agent) - fix(WhereUI) [needs-design]: broken-snapshots — the snapshot suite pinned genuinely broken renderings as references, flagged with `[Fix later]` review comments on PR #101 and merged anyway to land the suite. These are not flaky captures (those have their own ledger below) — each is a faithful, reproducible image of something actually wrong, so re-recording is never the fix. Fix the view, the capture frame, or the pipeline as each item says, then re-record just that reference under `Where/WhereUI/SnapshotTests/__Snapshots__/`. Most cluster on the accessibility axes `.screenDefaults` added — the ax5 Dynamic Type and VoiceOver-annotated configurations that nothing rendered before this suite existed. (pr#101 review) - - fix(WhereUI) [needs-design]: broken-snapshots: the VoiceOver-annotated calendar captures are blank. `CalendarContentViewSnapshotTests/calendarContent.WithData_iPhone_accessibility.png` (66 KB) and `..._iPad_accessibility.png` (171 KB) are solid white inside their border, against 205 KB–2.2 MB for every other screen-sized `_accessibility` reference — so it's specific to `CalendarContentView`, not the annotation pipeline. `AccessibilitySnapshotViewController` renders the wrapper with `viewRenderingMode: .drawHierarchyInRect` (`Shared/SnapshotKitTesting/Sources/AccessibilitySnapshotViewController.swift:36`), and `parseAccessibility()` claims failures "surface loudly rather than producing a blank image" (`:44`) — whatever this is slips past every `ImageRenderingError` guard there. Two configurations' worth of accessibility coverage currently assert a blank image, so a real regression in them can't fail. (pr#101 review) - - fix(WhereUI) [quick-win]: broken-snapshots: the calendar day grid breaks at accessibility Dynamic Type. Every two-digit date truncates to its first digit — the 10th–31st render as "1", "2", or "3" — because the day number is clamped to a fixed square (`CalendarContentView.swift:440`, `.frame(width: calendar.day.numberSize, height:)`), and the weekday header row wraps mid-word ("Sun" over two lines, "Wed" over three) because each symbol is a plain `Text` in an equal-width grid column (`:282`). Both show in `calendarContent.WithData_iPhone_ax5.png`; the digit truncation also hits `..._iPad_ax5.png`, where the extra width goes to inter-column gaps instead of the numbers. Showing "1" where the date is 10 is wrong content, not merely tight layout. (pr#101 review) - - fix(WhereUI) [quick-win]: broken-snapshots: the presence timeline's leading accent bar doesn't scale with Dynamic Type. `StintRow` sizes its `Capsule` from the fixed stylesheet tokens `timeline.accentWidth`/`accentHeight` — 4×34 (`Sources/Shared/WhereStylesheet.swift:719`, `:720`) applied at `PresenceTimelineList.swift:54`–`:57` — so at ax5 it stays a 4pt stub beside ~40pt text (`presenceTimeline.WithData_iPhone_ax5.png`, `..._iPad_ax5.png`). Scale it with `@ScaledMetric`, or derive it in `WhereStylesheet.init(context:)` the way the day-grid tap targets already do. (pr#101 review) - - fix(WhereUI) [needs-design]: broken-snapshots: the presence timeline row squishes horizontally at ax5 instead of restacking. `StintRow` keeps accent bar, emoji, name/date stack, and day count on one `HStack` line at every type size (`PresenceTimelineList.swift:51`), so `presenceTimeline.WithData_iPhone_ax5.png` renders "California" hyphenated over three lines beside a two-line "148 days". Switch to a `ViewThatFits` / `AnyLayout` that stacks vertically at accessibility sizes. (pr#101 review) - - fix(WhereUI) [needs-design]: broken-snapshots: `YearView` overflows horizontally at ax5. In `year.Loaded_iPhone_ax5.png` the month title reads "nuary", the day grid is clipped on both edges, and the Calendar/Timeline pill runs off the trailing edge. The suspect is `YearModePicker`, a fixed-width `HStack` of two labelled segments pinned as a bottom `safeAreaInset` (`YearView.swift:36`–`:38`, `:79`), which is wider than the screen at ax5. Confirm the oversized inset is what widens the layout beneath it, then make the picker fit at accessibility sizes (icon-only, wrapped, or scrollable). (pr#101 review) - - fix(WhereUI) [quick-win]: broken-snapshots: the Resolve toolbar badge sits awkwardly on the iOS 26 glass toolbar button. `ResolveToolbarLabel` hand-rolls the badge as a red `Capsule` overlaid on the `checklist` symbol and pushes it out with a fixed `.offset(x: spacing.small, y: -spacing.small)` (`LocationsView.swift:203`–`:217`), landing it half outside the button's own glass capsule — visible in `root.LoggedIn_iPhone.png`. Use SwiftUI's `.badge()` on the toolbar item, or offset against the resolved chrome rather than a fixed spacing token. (pr#101 review) - - test(WhereUI) [quick-win]: broken-snapshots: the Resolve sheet's ax5 reference is cut off mid-content. `resolution.WithIssues_iPhone_ax5.png` slices the second issue card's subtitle at the frame's bottom edge, because the case captures at the fixed `.iPhone` device frame via `.screenDefaults` (`ResolutionView.swift:221`), which clips rather than growing. `SnapshotConfiguration.Frame.fullContent` exists for exactly this (see `CalendarContentView.swift:592`) — add a full-content ax5 case so the whole sheet is pinned instead of its first screenful. (pr#101 review) + - fix(WhereUI) [quick-win]: broken-snapshots: the calendar day grid breaks at accessibility Dynamic Type. Every two-digit date truncates to its first digit — the 10th–31st render as "1", "2", or "3" — because the day number is clamped to a fixed square (`CalendarContentView.swift:335-339`, `.frame(width: calendar.day.numberSize, height: calendar.day.numberSize)`), and the weekday header row wraps mid-word ("Sun" over two lines, "Wed" over three) because each symbol is a plain `Text` in an equal-width grid column (`:182-186`). Both show in `calendarContent.WithData_iPhone_ax5.png`; the digit truncation also hits `..._iPad_ax5.png`, where the extra width goes to inter-column gaps instead of the numbers. Showing "1" where the date is 10 is wrong content, not merely tight layout. **The references were re-recorded by PR #196 with the layout code unchanged, so they now pin the same defect at a new size** — re-check the current image before fixing, and re-record after. (pr#101 review; re-verified 2026-08-09) + - fix(WhereUI) [needs-design]: broken-snapshots: `YearView` overflows horizontally at ax5. In `year.Loaded_iPhone_ax5.png` the month title reads "nuary", the day grid is clipped on both edges, and the Calendar/Timeline pill runs off the trailing edge. The suspect is `YearModePicker`, whose segment labels take their intrinsic width via `.fixedSize()` (`YearView.swift:118-124`, with an in-source comment explaining it keeps labels from truncating mid-animation) inside a bottom `safeAreaInset` (`:41-43`), making it wider than the screen at ax5. Confirm the oversized inset is what widens the layout beneath it, then make the picker fit at accessibility sizes (icon-only, wrapped, or scrollable) — note the `.fixedSize()` is deliberate, so the fix has to keep animation from truncating too. (pr#101 review; re-verified 2026-08-09) + - fix(WhereUI) [quick-win]: broken-snapshots: the Resolve toolbar badge sits awkwardly on the iOS 26 glass toolbar button. `ResolveToolbarLabel` hand-rolls the badge as a red `Capsule` overlaid on the `checklist` symbol and pushes it out with a fixed `.offset(x: spacing.small, y: -spacing.small)` (`LocationsView.swift:260-268`), landing it half outside the button's own glass capsule — visible in `root.LoggedIn_iPhone.png`. Use SwiftUI's `.badge()` on the toolbar item, or offset against the resolved chrome rather than a fixed spacing token. (pr#101 review; re-verified 2026-08-09) - fix(WhereUI): broken-snapshots: `locations.Loaded_iPad.png` bakes in raw inflection markup — the Elsewhere card's subtitle renders literally as `^[3 region](inflect: true)`. This is the `locations.elsewhere.subtitle` P1 filed above, now pinned as a reference; recorded here so the image isn't mistaken for correct output, and so that reference is re-recorded when the fix lands. (pr#101 review) ## P2s (Nice to have) -- feat(Where): Consider the user-assigned device-name entitlement and matching provisioning-profile support so the Devices screen can offer a better initial label than the generic hardware family. Keep the current generic name until the entitlement is intentionally provisioned; never silently depend on an entitlement absent from developer signing. (`FileInstallationRecordingContextStore`; PR #160 review) +- feat(Where): Consider the user-assigned device-name entitlement and matching provisioning-profile support so the Devices screen can offer a better initial label than the generic hardware family. Keep the current generic name until the entitlement is intentionally provisioned; never silently depend on an entitlement absent from developer signing. (`InstallationRecordingContextStore.swift:227-242` still derives `systemName` from `UIDevice.current.model`, and `Project.swift` declares no such entitlement; PR #160 review, re-verified 2026-08-09) - feat(WhereUI) [needs-design]: Give the app a branded launch screen. `UILaunchScreen` is an empty dictionary (`Project.swift`), so the pre-main frame is plain white. Measured from a fresh-install simulator recording, a first run reads as ~1.7s of white → ~0.25s of the dark `LaunchSplashView` → the light onboarding screen, so the splash registers as a quarter-second dark blip between two light screens rather than as the app opening. A launch screen matching the splash's background + icon would make that continuous. Note this is the right layer to fix it at: the splash's own `minimumSplashDuration` hold deliberately gates only the `.ready` reveal, not a gate transition like onboarding, so lengthening the hold would just delay interactive UI. (agent) - refactor(WhereUI) [needs-design]: Make the scene-scoped model wiring compiler-checked rather than an `@Environment` lookup that fails silently. `WhereSession` (the always-on coordinator) is read from the environment, so a screen mounted without a parent injecting it resolves to a runtime fallback/precondition instead of a compile error. The scoped models (`YearReportModel`, `ResolveModel`, `BackupModel`, `RemindersSettingsModel`) are already constructor-injected; explore threading the coordinator the same way (or a non-defaulting typed `EnvironmentKey`) so a broken wiring can't build. Follow-up from the `WhereSession` split. (agent) - refactor(WhereUI) [needs-design]: Split `YearReportModel` further. Post-split it still fuses several roles for the selected year: the loaded report + everything derived from it (ranking, missing days, calendar inputs, tracked-day count), the Resolve badge *count*, the day-write intents (`setManualDay(s)`, `overrideDay`, `clearManualDay`, `clearSelectedYear`), and the Elsewhere drill-in reads (`days(in:)`, `locations(in:)`, `representativeCoordinates()`). The read-only presentation state and the write-intent/drill-in surface could be separate collaborators so a view only holds what it uses. Follow-up from the `WhereSession` split. (agent) - refactor(WhereCore) [needs-design]: Move `RegionDays` / `RegionRanking` down from `WhereUI` into `WhereCore` so `DataIssueScanner` can derive primary regions itself instead of `WhereSession` passing `primaryRegions` in. Reverses the current "ranking is a presentation concept" placement; check the widget/UI call sites still compile. (agent) -- feat(WhereUI) [needs-design]: Animate the Locations ranking reorder. `RegionSummaryCard` now morphs its day count when it changes on screen (`CardStyles.DayCountStyle`), but a change that also flips the two primary regions' order still snaps the cards into their new positions — an animated count landing in a hard-cut reorder. Needs an `.animation(_:value:)` keyed on the ranking around the `ForEach` in `LocationsView`, checked against the card's `matchedTransitionSource` zoom so the two don't fight. (agent) -- fix(WhereCore) [quick-win]: Two Core failure paths report a benign-looking default instead of an honest one. `BackupService.importBackup` logs and continues when an evidence asset's bytes can't be read, importing metadata-only evidence with `blob: nil` (`BackupService.swift:188`) — a partial import that reads as a complete one; and `ReminderReconciler` contributes `0` to the badge when the issue scan throws (`ReminderReconciler.swift:193`), which is indistinguishable from "no issues to resolve". Surface partial-import state, and preserve the last good badge count (or a scan-failed state). (audit 2026-07-26) -- fix(WhereUI) [needs-design]: Make `LocationNamer` cancellation-aware. `ElsewhereView.loadPlaceNames()` (`:34`) has `.task(id:)` plus a post-await `Task.isCancelled` guard, but the namer itself (`LocationNamer.swift:64`) keeps geocoding after the year changes; `RegionDaysView`'s `DayRow` (`:126`) also fires an uncapped `.task` per row, so a long day list can spawn N concurrent reverse-geocode requests (the cache only helps duplicates). Add cancellation to `name(for:)` and batch the unique coordinates on the parent view. (audit 2026-07-26) -- localization(WhereUI) [quick-win]: `IntentSnippets` composes its production caption from hardcoded English (`" in "` / `" · "`, `IntentSnippets.swift:66`) rather than a catalog key with placeholders. The `#Preview` in the same file (`:190`) also hardcodes `Button("Log today here")` when `snippet.logTodayHere` already exists. (audit 2026-07-26) -- fix(WhereCore) [quick-win]: Surface the `applicationSupport()` → `NoOpLocationOutbox` fallback (`LocationOutbox.swift:59`); it silently disables cross-launch retry durability. Either report it to the launch wiring or treat it as a programmer error. (audit 2026-07-26) -- test(WhereShareExtension, WhereWidgets) [quick-win]: Close the two extension-shaped test gaps that don't need a new bundle pattern decision — `ShareEvidenceModel.buildPendingEvidence()` (`ShareEvidenceModel.swift:126`, exposed for testing, nothing tests it) and `WhereWidgetProvider`'s midnight reload policy (`WhereWidgetProvider.swift:36`, the extension's core scheduling logic, untested in any target). (audit 2026-07-26) -- test(RegionKit) [quick-win]: Add `GeoJSONTests.swift`. The unsupported-geometry throw (`GeoJSON.swift:62`) and malformed-coordinate drop (`:124`) are untested, and `RegionCatalog.loadFromBundle()`'s degrade-to-empty-catalog behavior (`RegionCatalog.swift:97`) is asserted only at the log-event level in `RegionLogTests` rather than at runtime. (audit 2026-07-26) +- feat(WhereUI) [needs-design]: Animate the Locations ranking reorder. `RegionSummaryCard` now morphs its day count when it changes on screen (via `LocationDayCountPresentationModel`), but a change that also flips the two primary regions' order still snaps the cards into their new positions — an animated count landing in a hard-cut reorder. Needs an `.animation(_:value:)` keyed on the ranking around the `ForEach(report.ranking.primary)` at `LocationsView.swift:116`, checked against the card's `matchedTransitionSource` zoom so the two don't fight. (agent) +- fix(WhereCore) [quick-win]: `ReminderReconciler` contributes `0` to the badge when the issue scan throws (`ReminderReconciler.swift:201-203`), which is indistinguishable from "no issues to resolve". Preserve the last good badge count, or carry a scan-failed state. (Filed as two paths; the backup half closed — see Completed issues.) (audit 2026-07-26; re-verified 2026-08-09) +- fix(WhereCore) [quick-win]: `WidgetSnapshotStore.read()` answers `nil` three ways but only tells two of them apart. `guard let data = try? Data(contentsOf: fileURL) else { return nil }` (`WidgetSnapshotStore.swift:72`) collapses "never published" and a genuine read failure — a permissions or I/O error on an existing file — into the same silent `nil`, while a decode failure does warn (`:75-80`). The doc comment directly above states that "the two ways of answering `nil` are told apart in the log" (`:64-70`), so the file asserts a guarantee it doesn't keep, and a widget stuck on its placeholder because the App Group container became unreadable leaves no signal. Test the file's existence to separate the two, and warn on a thrown read the way the decode path does. (audit 2026-08-09) +- fix(WhereUI) [needs-design]: Make `LocationNamer` cancellation-aware. `ElsewhereView.loadPlaceNames()` (`:34-45`) has `.task(id:)` plus a post-await `Task.isCancelled` guard, but the namer itself (`LocationNamer.swift:65-75`) keeps geocoding after the year changes; `RegionDaysView`'s `DayRow` (`:126-128`) also fires an uncapped `.task` per row, so a long day list can spawn N concurrent reverse-geocode requests (the cache only helps duplicates). Add cancellation to `name(for:)` and batch the unique coordinates on the parent view. (audit 2026-07-26) +- localization(WhereUI) [quick-win]: `IntentSnippets` composes its production caption from hardcoded English (`" in "` / `" · "`, now `Sources/Intents/IntentSnippets.swift:63-67`) rather than a catalog key with placeholders. (The `#Preview` half is closed — it now uses `.snippetLogTodayHere` instead of a literal.) (audit 2026-07-26; re-verified 2026-08-09) +- fix(WhereCore) [quick-win]: Surface the `applicationSupport()` → `NoOpLocationOutbox` fallback (`LocationOutbox.swift:118-125`); it logs `.noApplicationSupport` and then silently disables cross-launch retry durability, so samples are lost across process death with nothing above Periscope aware of it. Either report it to the launch wiring (a startup health flag the Data/About screens can read) or treat it as a programmer error. Weightier since PR #160 made the outbox generation-stamped and load-bearing. (audit 2026-07-26; re-verified 2026-08-09) +- test(WhereShareExtension, WhereWidgets) [quick-win]: Close the two extension-shaped test gaps that don't need a new bundle pattern decision — `ShareEvidenceModel.buildPendingEvidence()` (`ShareEvidenceModel.swift:126-132`, documented as exposed for testing at `:124-125`, nothing tests it) and `WhereWidgetProvider`'s midnight reload policy (`WhereWidgetProvider.swift:34-41`, `.after(nextMidnight)` — the extension's core scheduling logic, untested in any target). (audit 2026-07-26) +- test(RegionKit) [quick-win]: Add `GeoJSONTests.swift`. The unsupported-geometry throw (`GeoJSON.swift:62-67`) and malformed-coordinate drop (`:124-128`) are untested, and `RegionCatalog.loadFromBundle()`'s degrade-to-empty-catalog behavior is asserted only at the log-event level (`RegionLogTests.swift:27-30`) rather than at runtime. `RegionKit/README.md:161-162` meanwhile claims GeoJSON decoding *is* covered — filed in the docs item below, and closing this item is what would make the README true. (audit 2026-07-26; re-verified 2026-08-09) - test(WhereUI) [quick-win]: Add the missing namesake tests for `LocationNamer` (cache / coalescing) and `CalendarContentView`'s `scrolledForYear` scroll-reveal gate, which has hosting smoke only. (audit 2026-07-26) -- test(WhereIntents) [quick-win]: Test `RegionSpotlightIndexer` and `WhereIntentReader.recentActivity`. (audit 2026-07-26) -- test(WhereCore) [needs-design]: Close the namesake-test debt — 28 of the implementation files have no `*Tests.swift` (notably `FoundationModelSummaryGenerator`, `WherePreferences`, `WidgetTimelineRefresher`, `BackupArchive`), and `WhereCoreTests.swift` is an omnibus still holding `YearReportTests`. Split by concern as those files change rather than in one pass. (audit 2026-07-26) -- docs(WhereCore) [quick-win]: Refresh stale doc claims — `WhereCore/README.md:48`, `:161` claim every write reconciles and errors are never swallowed; `LocationIngestor.swift:334` says it logs through `os.Logger` when it emits typed `WhereLog` events; `RegionViewer/README.md:14` describes a monolithic `us-states.geojson` and a hand-listed region set. `RegionKit/README.md:144` also claims GeoJSON decoding is covered, which the test item above would make true instead. (Two halves already closed: the `RootView.swift` "four screens" doc now reads three tabs, and the `WhereShareExtension/AGENTS.md:21` compose-model credit was fixed 2026-07-27.) (audit 2026-07-26) -- refactor(WhereCore) [quick-win]: Drop the remaining Core-API parameter defaults — `DayJournal.addEvidence(_:blob:)` (`:216`), `WherePreferences.init(store:)` (`:14`), `SwiftDataStore.make(storage:)` (`:179`), and `WidgetDataReader`'s aggregator/attributor (`:74`). The composition root already knows each value. (audit 2026-07-26) -- convention(WhereIntents) [quick-win]: Small polish — register `LogTripIntent` in `WhereShortcuts` (`:11` registers five) or document Shortcuts-only discovery; use `Calendar.whereIntents` for `LogDayIntent`'s default day instead of `Date()` (`:39`, no data impact today since `DayJournal` buckets Gregorian); log the App Group open failure behind `WhereIntentReader.todaySnapshot`'s `try?` (`:17`); and wrap `RegionViewer`'s `RegionMapView` in `.whereBroadwayRoot()` (`RegionViewerApp.swift:15`) so the dev tool renders with app styling. (audit 2026-07-26) +- test(WhereIntents) [quick-win]: Test `RegionSpotlightIndexer` and `WhereIntentReader.recentActivity`. The indexer **shipped in PR #210** (`RegionEntity+Spotlight.swift:16-33`, wired in `Where/Where/Sources/RegularApplicationRuntime.swift` with a demo-mode skip) and arrived with no test of its own: nothing references `RegionSpotlightIndexer` or `indexRegions`, so neither the success log, the degraded failure log, nor the demo skip is verified. `RegionEntityTests.swift:27-45` covers `RegionEntity.tracked(from:)` — the indexer's *input* — which is easy to mistake for coverage of the indexer. `WhereIntentReaderTests` still has no `recentActivity` case. (audit 2026-07-26; re-verified 2026-08-09) +- test(WhereCore) [needs-design]: Close the namesake-test debt — **59** of the 118 implementation files have no same-named `*Tests.swift` (re-derived by basename 2026-08-09; was 28 of 87 at the July 26 audit, so the debt grew with the module rather than being worked down). Still uncovered among the originally named files: `FoundationModelSummaryGenerator`, `WidgetTimelineRefresher`, `BackupArchive` (`WherePreferences` closed 2026-08-05). `WhereCoreTests.swift` is an omnibus holding five suites, not just `YearReportTests` — also `SwiftDataStoreFactoryTests`, `SDLocationSampleTests`, `EvidenceKindTests`, `SampleSourceTests`. The basename count is a proxy: `Sources/Logging/*` event types and `SD*` record shells reasonably have no namesake file, so treat it as a trend line, not a work list. Split by concern as those files change rather than in one pass. (audit 2026-07-26) +- docs(WhereCore) [quick-win]: Refresh stale doc claims — `WhereCore/README.md:66-67` claims every write reconciles (it omits summary and `setPrimaryRegions`, both filed above) and `:271-272` claims errors are never swallowed into an empty default (the badge path above contradicts it); `LocationIngestor.swift:473-474` says it logs through `os.Logger` when the code immediately below emits typed `WhereLog` events (`:477-481`); `RegionViewer/README.md:17-18` describes its **Source** mode as decoding "all US-state features in `us-states.geojson`", but `RegionGeometryCatalog.buildSourceOutlines()` (`:146`) walks `RegionCatalog.shared.entries` and decodes each region's **bundled per-region** GeoJSON — 54 files under `RegionKit/Sources/Resources/regions/`. The monolith still exists at `RegionKit/Tools/source/us-states.geojson`, but it is a build-time input to the extraction tooling, never bundled and never read at runtime, so the README describes the pipeline's input as if it were the app's data. The same file's hand-listed region set is stale for the same reason. `RegionKit/README.md:161-162` also claims GeoJSON decoding is covered, which the test item above would make true instead. (Two halves already closed: the `RootView.swift` "four screens" doc now reads three tabs, and the `WhereShareExtension/AGENTS.md:21` compose-model credit was fixed 2026-07-27.) (audit 2026-07-26; citations refreshed 2026-08-09) +- refactor(WhereCore) [quick-win]: Drop the remaining Core-API parameter defaults — `DayJournal.addEvidence(_:blob:)` (`DayJournal.swift:271`) and `WidgetDataReader`'s aggregator/attributor (`WidgetDataReader.swift:77-78`). The composition root already knows each value. (Two of the four filed are now done: `WherePreferences.init(store:)` and `SwiftDataStore.make(storage:)` both require the argument.) (audit 2026-07-26; re-verified 2026-08-09) +- convention(WhereIntents) [quick-win]: Small polish, all four parts still open — register `LogTripIntent` in `WhereShortcuts` (`Where/Where/Sources/WhereShortcuts.swift:11-56` registers five, no trip backfill) or document Shortcuts-only discovery; use `Calendar.whereIntents` for `LogDayIntent`'s default day instead of `date ?? Date()` (`LogDayIntent.swift:39`, no data impact today since `DayJournal` buckets Gregorian); log the App Group open failure behind `WhereIntentReader.todaySnapshot`'s `try?` (`WhereIntentReader.swift:17-18`); and wrap `RegionViewer`'s `RegionMapView` in `.whereBroadwayRoot()` (`RegionViewerApp.swift:15-18`) so the dev tool renders with app styling. (audit 2026-07-26; re-verified 2026-08-09) - convention(RegionKit) [quick-win]: Reference a generated catalog symbol for `region.other` instead of the raw `String(localized:)` key (`RegionCatalog.swift:65`). (audit 2026-07-26) -- perf(WhereUI) [needs-design]: Profile the `RegionSummaryCard` Canvas rosette — `ringCount` derives from size with no cap (`:106`). Cap or pre-render if it shows up. (audit 2026-07-26) -- fix(WhereUI) [quick-win]: Three literals in source get auto-extracted into the catalogs as value-less entries, which is why an IDE build had anything to write back at all (see the serialization normalization PR). They're committed as Xcode writes them; removing an entry for good means removing the literal. `Marker("", coordinate:)` in `RecordedPointsMap` produces the empty `""` key (an unlabeled dev-map pin — `Annotation` with an explicit accessibility label would say what it means); `Text("\(group.outlineCount)")` in `RegionMapView` and `Text("\(day.dayOfMonth)")` in `CalendarContentView` produce `%lld` and bypass `WhereFormat`'s number styling. (A fourth such entry, `App content`, came from a `LifecycleContainer` `#Preview`; it's gone — that preview now uses `Text(verbatim:)`, which isn't extracted.) (agent) +- perf(WhereUI) [needs-design]: Profile the security-print rosette — `ringCount` still derives from size with no cap (`SecurityPrintRosette.swift:42-43`, `Int(max(w, h) / spacing)` driving an unbounded loop). The code moved out of `RegionSummaryCard` into its own view and is now drawn by the passport surfaces too, so the hot path is wider than when this was filed. Cap or pre-render if it shows up. (audit 2026-07-26; re-verified 2026-08-09) +- fix(WhereUI) [quick-win]: Three literals in source get auto-extracted into the catalogs as value-less entries, which is why an IDE build had anything to write back at all (see the serialization normalization PR). They're committed as Xcode writes them; removing an entry for good means removing the literal. `Marker("", coordinate:)` in `RecordedPointsMap.swift:45` produces the empty `""` key (an unlabeled dev-map pin — `Annotation` with an explicit accessibility label would say what it means); `Text("\(group.outlineCount)")` — now in `RegionMapLegend.swift:47`, extracted out of `RegionMapView` in this window — and `Text("\(day.dayOfMonth)")` in `CalendarContentView.swift:335` produce `%lld` and bypass `WhereFormat`'s number styling. (A fourth such entry, `App content`, came from a `LifecycleContainer` `#Preview`; it's gone — that preview now uses `Text(verbatim:)`, which isn't extracted.) (agent) - feat(WhereUI): Raw data browser (similar to the SwiftData browser). (human) - docs(WhereUI): Add comments to strings in the xcstrings files. (human) -- refactor(WhereUI) [needs-design]: Remove `CalendarYearGrid`'s capture-time scroll skip. `scrollToCurrentMonth` guards on `\.isCapturingSnapshot` so a capture pins the deterministic top-of-year state instead of a nondeterministic scroll landing (`CalendarContentView.swift`). It's the one remaining product-code read of the snapshot flag that isn't a substituted stand-in — a smell (product behavior forking on "are we snapshotting"). Prefer making the scroll itself deterministic under capture (or driving the capture from a pre-scrolled fixture) so the guard can go. (From the July 2026 snapshot-testing PR review.) -- refactor(WhereUI) [quick-win]: The widget fixtures' pinned instant is still `1_770_000_000` (02:40 UTC / Feb 1 evening Pacific, near a day boundary). Moving it safely off midnight was skipped to avoid re-recording the widget references; the main-merge re-record makes that cheap now. (From the July 2026 snapshot-testing PR review.) -- test(WhereUI) [needs-design]: Snapshot matrix gaps — `LocationsView`/`YearView`'s empty states, `RecentActivitySummaryView.loading` (the sole user of `AppIconActivityIndicator`, so its `@MotionIsStatic` pinning is the one motion adoption without direct capture coverage), and `ManualDayView`'s range-mode add have no snapshot case. (From the July 2026 snapshot-testing PR review.) +- refactor(WhereUI) [quick-win]: The widget fixtures' pinned instant is still `1_770_000_000` (02:40 UTC / Feb 1 evening Pacific, near a day boundary) at `PreviewSupport.swift:611` and `:652`. Moving it safely off midnight was skipped to avoid re-recording the widget references; PR #196's re-record makes that cheap now. (From the July 2026 snapshot-testing PR review; re-verified 2026-08-09) +- test(WhereUI) [needs-design]: Snapshot matrix gaps — `RecentActivitySummaryView.loading` (the sole user of `AppIconActivityIndicator`, so its `@MotionIsStatic` pinning is the one motion adoption without direct capture coverage; the suite covers only Loaded/Empty/Unavailable/Failed at `:148-171`), an explicit `LocationsView` empty state, and `ManualDayView`'s range-mode add have no snapshot case. (`YearView` gained its `Empty` case at `YearView.swift:149-151`.) (From the July 2026 snapshot-testing PR review; re-verified 2026-08-09) +- test(WhereUI) [quick-win]: Three screens have a `#Preview` but no `SnapshotProviding` conformance, so no image pins them — against the module convention that an image bundle, not a hosting smoke test, owns "does this screen render". They are the only Settings drill-ins without coverage: `AlertsSettingsView` (reminders, daily summary, issue alerts, the drift threshold, and a manual "find issues now"), `VisibleYearSettingsView`, and `RemovedDeviceView` (`Devices/RemovedDeviceView.swift`), the blocking CloudKit-removal recovery gate with the rejoin call to action — a screen a user only reaches when something has already gone wrong, which is the worst place for an unpinned regression. **Two of the three are not new debt:** Alerts and VisibleYear have been uncovered since PR #111 landed the drill-in restyle, so three prior audits missed them; only `RemovedDeviceView` arrived with PR #160. Every other screen added in this window — About, Devices, License, LifecycleFailure, ShareEvidence, Siri, Widget, CardDesignerStudio, Flyover root — did get coverage, so the convention holds and these are the exceptions. Add cases following `Settings/DevicesSettingsView.swift` (for Alerts, an authorized and a denied-notifications variant, toggles on and off). (audit 2026-08-09) ## Deferred snapshot-test flakiness Known nondeterminism in the WhereUI image suites, accepted for now — scattered @@ -102,10 +100,19 @@ re-recording: # Completed issues -- fix(WhereCore): Push data-generation membership into SwiftData fetch predicates. (Resolved 2026-08-08: `GenerationScopedFetch` now builds generation-aware descriptors for all seven scoped entity types, composes membership with identity/range filters before materialization, and preserves legacy `nil == .initial` rows. `SwiftDataStoreTests` covers mixed-generation reads, updates, and rotation deletes.) -- fix(WhereCore): Make the raw-location retry outbox crash-safe and surface failed durable writes. (Resolved 2026-08-04: `LocationOutbox` now journals complete queue snapshots through JournalKit, recovers the newest intact snapshot after a torn tail, migrates the previous JSON format once, and stops recording with the sample retained in memory if the durable checkpoint fails. `LocationOutboxTests` and `LocationIngestorTests.failedOutboxWriteStopsRecordingWithTheSampleStillInMemory` cover recovery and failure.) -- fix(WhereCore): Remove the cross-device assignment DAG and its clock-skew/compaction liabilities. (Resolved 2026-08-03: automatic-recording consent is now installation-local; CloudKit syncs only profiles, nickname events, advisory check-ins, and append-only removal tombstones. A removal retains the intentional remote history cutoff without a mutable authority timeline.) -- fix(WhereUI): Serialize automatic-recording changes and separate desired from effective state. (Resolved 2026-08-03: the fire-and-forget binding became an awaited, installation-local intent persisted beside the backup-excluded device identity. Refreshes cannot manufacture commands, and the Devices UI renders remote status read-only.) +## broken-snapshots (four of the eight closed 2026-08-09) +- fix(WhereUI) [needs-design]: broken-snapshots: the VoiceOver-annotated calendar captures are blank — `calendarContent.WithData_iPhone_accessibility.png` (66 KB) and `..._iPad_accessibility.png` (171 KB) were solid white inside their border, so two configurations' worth of accessibility coverage asserted a blank image. (Resolved by PR #196's intrinsic-height scroll captures: both references were re-recorded and their LFS `size` fields now read **3,204,875** and **3,633,520** bytes — a solid-white PNG cannot be 3.2 MB, so the captures now carry real content. Verified from the LFS pointers only; a macOS `./test --review` would confirm the pixels, which a Linux pass cannot. The suspicion that the blankness was specific to `CalendarContentView` rather than the annotation pipeline held up: nothing in `AccessibilitySnapshotViewController` changed.) +- fix(WhereUI) [quick-win]: broken-snapshots: the presence timeline's leading accent bar didn't scale with Dynamic Type — `StintRow` sized its `Capsule` from the fixed `timeline.accentWidth`/`accentHeight` tokens (4×34), so at ax5 it stayed a 4pt stub beside ~40pt text. (**No longer applicable:** PR #200's Your Year timeline refresh deleted `StintRow` and both tokens outright. The row is now `PresenceJourneyRow` / `PresenceJourneyRail`, and the references were re-recorded. Closed as obsolete rather than fixed — nobody scaled the capsule; the capsule is gone.) +- fix(WhereUI) [needs-design]: broken-snapshots: the presence timeline row squished horizontally at ax5 instead of restacking — "California" hyphenated over three lines beside a two-line "148 days". (Resolved by the same PR #200 redesign, and in the way this item asked for: `WhereStylesheet.init(context:)` sets `timeline.row.stacksDayCount = true` at accessibility sizes (`WhereStylesheet.swift:43-45`) and `PresenceJourneyRow` switches its layout on it (`PresenceJourneyRow.swift:25`), so the day count stacks instead of competing for the same line.) +- test(WhereUI) [quick-win]: broken-snapshots: the Resolve sheet's ax5 reference was cut off mid-content, because the case captured at the fixed `.iPhone` device frame via `.screenDefaults`. (Resolved as suggested: the case now declares `.fullContentScreenDefaults` (`ResolutionView.swift:221`) and the reference was re-recorded, so the whole sheet is pinned instead of its first screenful. This is the same `Frame.fullContent` mechanism the root `AGENTS.md` now requires for any snapshot containing scrolling content.) +- refactor(WhereUI) [needs-design]: Remove `CalendarYearGrid`'s capture-time scroll skip — the one remaining product-code read of `\.isCapturingSnapshot` that wasn't a substituted stand-in. (Resolved: no `isCapturingSnapshot` or `scrollToCurrentMonth` guard remains in `CalendarContentView.swift`, so the calendar scrolls normally under capture and product behavior no longer forks on "are we snapshotting". Landed alongside PR #182's latest-month-first ordering and PR #196's intrinsic-height captures, which together made the deterministic capture possible.) + +## Other +- fix(WhereCore): Push data-generation membership into SwiftData fetch predicates. (Resolved 2026-08-08 in PR #209: `GenerationScopedFetch` now builds generation-aware descriptors for all seven scoped entity types, composes membership with identity/range filters before materialization, and preserves legacy `nil == .initial` rows. `SwiftDataStoreTests` covers mixed-generation reads, updates, and rotation deletes — including `generationRotationDeletesOnlyCurrentRowsAcrossEveryScopedTable` and `legacyRowsWithoutGenerationIDsBelongToTheInitialGeneration`.) +- fix(WhereCore): Read an unreadable backup asset as a failure, not as metadata-only evidence. (Resolved: `BackupService.loadAssets` logs `.assetMissing` and **rethrows** (`BackupService.swift:227-231`), so an archive whose asset bytes can't be read fails the import instead of committing evidence with `blob: nil`. Import itself moved to `BackupCoordinator.importBackup`. Filed as one half of the two-benign-defaults item; the reminder-badge half is still open above.) +- fix(WhereCore): Make the raw-location retry outbox crash-safe and surface failed durable writes. (Resolved in PR #160, merged 2026-08-08: `LocationOutbox` now journals complete queue snapshots through JournalKit, recovers the newest intact snapshot after a torn tail, migrates the previous JSON format once, and stops recording with the sample retained in memory if the durable checkpoint fails. `LocationOutboxTests` and `LocationIngestorTests.failedOutboxWriteStopsRecordingWithTheSampleStillInMemory` cover recovery and failure. The date originally recorded here, 2026-08-04, was the branch's, not the merge's.) +- fix(WhereCore): Remove the cross-device assignment DAG and its clock-skew/compaction liabilities. (Resolved in PR #160, merged 2026-08-08: automatic-recording consent is now installation-local; CloudKit syncs only profiles, nickname events, advisory check-ins, and append-only removal tombstones. A removal retains the intentional remote history cutoff without a mutable authority timeline.) +- fix(WhereUI): Serialize automatic-recording changes and separate desired from effective state. (Resolved in PR #160, merged 2026-08-08: the fire-and-forget binding became an awaited, installation-local intent persisted beside the backup-excluded device identity — `WhereSession.setRecordingEnabled` drops superseded awaits with a monotonic `recordingIntentSequence` (`WhereSession.swift:478-498`). Refreshes cannot manufacture commands, and the Devices UI renders remote status read-only.) - test(WhereCore) [quick-win]: Add `WherePreferencesTests` over `InMemoryKeyValueStore`. (Resolved 2026-08-05: `WherePreferencesTests` now pins every first-install default, year-isolated Location-card snapshot persistence, and reset clearing both the existing settings and the new presentation history.) - fix(WhereUI) [quick-win]: `resolution.Empty_iPhone` and `..._dark` baked in the **real-world date** and drifted every day — the reference read "Jan 1 – Jul 25 / 206 days" because that is when it was recorded, and it had been silently wrong every day since, passing only because two digit glyphs are 0.046% of the image. (Resolved: `PreviewSupport.previewServices()` now passes `now: { referenceNow }`, which `WhereServices` already threads into every collaborator including the `DataIssueScanner` that computes the missing-days range. `referenceNow`'s own doc comment names "missing-day math" as a reason it exists, so this was a fixture bug against a documented intent rather than a new pin. The two references were re-recorded once and now read "Jan 1 – Jul 14 / 195 days", derived from the pinned instant. Surfaced by `./test --review`, which reported it at max channel delta 255 while the suite still reported green.) - fix(WhereUI): `resolution.Empty` never rendered the empty state, and its capture raced a live store scan — which turned `main` red (run 30402846712) the first time CI lost that race, baking the `AppIconLoadingView` placeholder over 91.7% of `Empty_iPhone`. `PreviewSupport.resolveModel(seededWithIssues: false)` skipped `setDataIssues` entirely, so the fixture came back with `hasLoaded == false` — which `ResolutionView` can't distinguish from "the first scan hasn't landed" — and the view showed the placeholder until its `.task(id:)` scan of the empty in-memory store returned the whole year as missing days. So the *reference* was that scan's output (a populated list titled "Missing days"), not the all-clear state the case names, and every capture was a race the settle loop can't see: a pixel-stable placeholder settles clean, exactly as in the `root.LoggedIn` entry below. Previously masked by the ~1s that `drainInFlightAnimations` wasted per capture; removing that waste (#151) exposed it. (Resolved: both fixture modes now seed — `setDataIssues([])` for the empty one, which is what marks it loaded *and* `isSeeded`, so the view's `load(...)` is a no-op and the first rendered frame is final. The case is now fully synchronous, independent of the store and of `now`, and the two references were re-recorded once to the "All clear" state — coverage the suite never had, since `WithIssues` already pins the populated list. `ResolveModelTests` gained two guards: the fixture is loaded up front in both modes, and `load(...)` leaves a seeded fixture alone against a store whose scan does find issues.)