diff --git a/AGENTS.md b/AGENTS.md index e5471392a..af883c316 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -645,9 +645,8 @@ external agent skills. Those skills are gitignored and absent from a bare checko Four of those rows — SwiftFormat, ShellCheck, attribution, and the Python tool tests — are four of the six checking steps in CI's `format` job, so a Linux agent -can pre-flight most of it. Three carve-outs, all filed together in -[`TODOs.md`](TODOs.md), all from the same cause — Linux ships no -`/usr/bin/ruby`, and the pinned one is reachable only through mise. One Python +can pre-flight most of it. Three portability carve-outs remain filed together in +[`TODOs.md`](TODOs.md). They have distinct causes. One Python contract test pins macOS bash's `126` exit status for an unlaunchable command and reports `127` here. The Ruby retained-tool tests run each command under a hermetic `PATH=/usr/bin:/bin`, which resolves a `#!/usr/bin/env ruby` script diff --git a/Ledger/TODOs.md b/Ledger/TODOs.md index 0c11ee464..da9980fb1 100644 --- a/Ledger/TODOs.md +++ b/Ledger/TODOs.md @@ -13,6 +13,6 @@ The item format and the placement rule live in the root ## 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-308`), 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; re-verified 2026-08-30) - 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 (14 test/support files over 16 sources, including the API, Keychain, token-source, and history seams). The September 7 recount confirms 16 source and 14 test/support files; the three namesake gaps remain unchanged. (audit 2026-08-09; re-verified 2026-09-06) -- 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:712-717`). 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; re-verified 2026-08-30) +- 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:869-874`). 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; re-verified 2026-08-30) # Completed issues diff --git a/MODULE_AUDIT.md b/MODULE_AUDIT.md index ea4156378..3f3fe7072 100644 --- a/MODULE_AUDIT.md +++ b/MODULE_AUDIT.md @@ -1,8 +1,8 @@ # Swift Module Audit Report -**Date:** September 7, 2026 -**Reviewed source:** `5f65b9f0` (PR #311), fetched from `origin/main`. -**Prior audit:** September 6, 2026, report merged in PR #310 (`1f6162ba`), whose stated source coverage ended at PR #307 (`1e9c9289`). +**Date:** September 21, 2026 +**Reviewed source:** `a32b8b05` (PR #318), fetched from `origin/main`. +**Prior audit:** September 7, 2026, report merged in PR #313 (`c62c1afb`), whose stated source coverage ended at PR #311 (`5f65b9f0`). This report is derived from all 12 `TODOs.md` files and carries no actionable items. The root [`TODOs.md`](TODOs.md) owns their format and placement. This @@ -10,70 +10,84 @@ report is true as of the header date and source boundary, not as of later HEADs. ## Method and changes since the previous audit -The pass read every open area backlog, checked cited current source and test -seams, compared the covered source boundary with current main, and enumerated -tracked sources, tests, references, module docs, and manifest wiring. Unchanged -areas were checked against their current cited mechanisms and the unchanged -source diff; this was not a fresh line-by-line review of all 709 source files. -Exploratory directions remain decisions, rather than being promoted to bugs. - -The commit window is `1e9c9289..5f65b9f0`: PR #309's welcome feature, PR #310's -audit/documentation changes, and PR #311's welcome motion/reset changes. The -prior report missed #309 even though it was already an ancestor of that report's -merge. Both feature PR bodies were read to distinguish intent and reported -validation from what this pass itself verified. - -| Area | Prior report | September 7 state | +The pass read every open area backlog, checked current cited source and test +seams, reviewed changes from the covered source boundary to current main, and +enumerated tracked sources, tests, references, module docs, and manifest wiring. +Unchanged areas were checked against their cited mechanisms and the unchanged +source diff; this was not a new line-by-line review of all 717 source files. +Exploratory directions remain decisions rather than being promoted to bugs. + +The commit window is `5f65b9f0..a32b8b05`: PR #313's audit, PR #174's audience +builds, PR #314's foreground welcome and current-location confidence flow, and +PR #318's iOS 27 minimum. It includes changes since the previous report even +though the scheduled automation also lists a September 14 run. Feature PR bodies +were read to distinguish reported validation from this pass's own checks. +PR #319's HistoryObserver work is open, not merged, at this audit boundary. + +| Area | September 7 report | September 21 state | |---|---|---| -| Source / test-support / image-suite files | 704 / 369 / 49 | **709 / 371 / 49** | -| WhereCore | 128 / 83 | **129 / 84**; resolver and namesake tests added | -| WhereUI | 285 / 103 | **289 / 104**; four welcome types and model tests added | -| Reference images | 490 | **495**; five welcome references added, ten Appearance references updated | +| Source / test-support / image-suite files | 709 / 371 / 49 | **717 / 374 / 50** | +| WhereCore | 129 / 84 | **132 / 86**; typed location outcomes, confidence telemetry, and fake-driver tests | +| WhereUI | 289 / 104 | **291 / 104**; app-shell welcome/accessory state replaces tab-local ownership | +| App / share extension / widgets sources | 8 / 5 / 7 | **9 / 6 / 8**; audience environments added | +| Reference images | 495 | **506**; 16 MainTabs references replace five Locations welcome references | | Module / test bundle count | 27 / 25 | **27 / 25**, unchanged | | Inbox | Empty | **Empty**; no notes to promote or decline | -| Backlog | Some shipped/overstated claims still open | **Three entries archived**, two welcome findings filed; partial requests and citations corrected | -| Documentation | Reconciliation and snapshot-isolation overclaims | Current behavior and remaining exceptions explicitly documented | - -The three archived entries are logged-in/out scope modeling (shipped via -PR #150), local ingest/manual-sample fan-out (shipped August 4), and the stale -WhereCore documentation cluster corrected in this pass. The missing daily -summary and picker fan-outs remain open. The current-location UI request is -partly fulfilled by #309; its persistent-marker decision remains open. - -Other corrections distinguish actual consequences from inherited claims: -JournalKit's append test detects missing records but loses the original error; -WhereModel already has a typed log-store state; accessibility parse failure can -kill the current bundle's host, not every bundle's host; Ledger has 14 -test/support files; RegionKit's README already admits its decoding-test gap; -and the tool-portability failures do not all share one missing-Ruby cause. -Snapshot backlog headers now link to the root format instead of maintaining -separate instructions. +| Backlog | Partial requests and stale claims | **One item archived**, one test synchronization finding filed; partial requests narrowed | +| Deployment / build selection | iOS 26; single Where audience | **iOS 27**; Development, Beta, and App Store audiences; macOS remains 26 | + +The per-intent `perform()` testing item is archived through its documented- +limitation option: the README now agrees with the existing agent guidance. +No new intent runtime coverage is claimed. Foreground welcome refresh was +already archived by PR #314; its completion reference is corrected here. +The persistent current-region marker remains a separate product decision. +Welcome semantic captures now exist, while scrolling, iPad, and first-greeting +coverage remain open. The intent App Group-open logging subtask shipped in +PR #174; the widget stores' separate silent file-read failures remain open. + +Documentation now admits the injected-driver exception to the CoreLocation +unit-test prohibition, locates welcome ownership in the app shell, and points +to this derived report for the current snapshot inventory. The Linux section +no longer attributes all portability failures to a missing Ruby. Backlog +citations follow the new manifests, and the CI recipe item no longer claims +that `./test --everything` omits architecture validation. ## New-surface review -**Verified OK in source and existing tests:** `CurrentRegionResolver` checks -recording authority before and after acquiring a fix, rejects `.other`, and -reuses the composition root's ingestor and attributor. The new test file covers -missing/outside fixes and revocation during an awaited request. -`LocationWelcomeModel` rejects cancellation, a disabled preference, and stale -request sequences before publishing; dismissal alone persists the region. -Its tests cover cancellation, disabling during lookup, replay after the DEBUG -reset, and suppression of the acknowledged region. Preferences and the report -mirror have existing round-trip/reset and visibility tests. - -The UI uses generated localized copy, typed region values, the existing planned -stay editor, an independent scrim layer, modal accessibility traits and -screen-change notifications. The stylesheet supplies separate arrival/departure -motion and a nonspatial Reduce Motion alternative. The reset is DEBUG-only and -clears only the acknowledged region. These are source-level checks, not a claim -that live transition timing or VoiceOver focus was exercised here. - -**Filed:** “Refresh the live-region welcome when the scene becomes active” -(Where P1), and “Cover the welcome overlay's scrolling and modal semantics” -(Where P2). Both are in [`Where/TODOs.md`](Where/TODOs.md); the report does not -duplicate their implementation proposals. The former needs a retained-tab -foreground reproduction; the latter records the fixed-frame, semantic-capture, -and iPad coverage gaps without claiming a screenshot proves broken rendering. +**Verified OK in source and existing tests:** audience descriptors centralize +bundle IDs, App Groups, primary icons, configuration, and host compiler +conditions. The app validates the condition/plist selection and injects storage, +group, widget refresher, and icon selection from one environment. Development +uses its isolated local store; Beta and App Store share the production family. +The share extension uses local-only access to its matching group, and widgets +read published stores from that group. Hosted app tests use in-memory storage +and a no-op refresher. The icon tool guards every configured primary asset. +CircleCI now contains two release-audience build-only shards alongside the +existing simulator test pipeline; this pass did not run those builds. + +The current-location source has explicit idle/pending request state, coalesces +waiters, stops the request when its final waiter cancels, and checks request +identity when timing out. Stale or negative-accuracy callbacks do not complete +a request. Its fake-driver tests cover reduced precision, denied authorization, +provider failure, timeout, cancellation, retry, and synchronous completion. +`CurrentRegionResolver` checks recording authority before and after suspension, +rejects fixes older than 60 seconds or worse than 1 km accuracy, and requires +accuracy to fit within the region-boundary distance. Telemetry uses bounded +outcome and accuracy categories. These are source/test-inspection results. + +`MainTabs` keys welcome lookup to scene activity and the enabled preference, +independently of selected tab. The model rejects cancellation and stale request +sequences and persists acknowledgement only on dismissal. The accessory can +surface a retry/status action. The modal hides underlying tabs from accessibility, +and the four app-shell image cases include semantic captures. Existing tests +exercise the model's cancellation, confidence/status, acknowledgement, and +repeat-resolution paths. Live lifecycle delivery, VoiceOver focus, and animation +were not exercised in this pass. + +**Filed:** the new coalesced-waiter cancellation test waits for one system +request, which does not prove both callers have registered. Its scheduling +assumption and unbounded yield helper are recorded in the Where P2 backlog. +This is a static synchronization finding, not a reproduced CI failure. ## Top findings @@ -88,29 +102,33 @@ Pointers only; evidence and proposed fixes live in the backlog. | WhereCore | Picker fan-out and hard-deleting untracked regions | [Where P1](Where/TODOs.md) | | WhereUI | Launch-time notification permission prompt | [Where P1](Where/TODOs.md) | | SnapshotKit | Captured models shared across configurations | [SnapshotKit P1](Shared/SnapshotKit/TODOs.md) | -| WhereUI | Welcome lookup lacks foreground refresh trigger | [Where P1](Where/TODOs.md) | +| WhereCore tests | Coalesced cancellation lacks a two-waiter handshake | [Where P2](Where/TODOs.md) | +| WhereUI snapshots | Welcome scrolling, iPad, and first-greeting gaps | [Where P2](Where/TODOs.md) | | CI / scripts | Serial-axis documentation and Linux portability gaps | [Root P1](TODOs.md) | | Repository | Missing group doc pairs for Where and Ledger | [Root P1](TODOs.md) | -The nearest dated external task remains the benchmark organization cleanup, -after the saved **September 9** plan downgrade (two days from this audit). -`gh repo view` confirms the benchmark repository exists and is not archived. -Billing state, installed integrations, and downgrade scheduling were not -independently verified; no deletion or billing action was taken. +The benchmark organization cleanup remains open after the saved **September 9** +plan-downgrade date. `gh repo view` confirms the benchmark repository still +exists and is not archived on September 21. The actual downgrade, billing, and +installed integrations were not independently verified; no deletion or billing +action was taken. ## Cross-cutting themes -- **A report date does not identify its source coverage.** Use the explicit - covered commit, including same-day merges the prior report omitted. -- **Passing tests and good coverage are different claims.** The journal test - catches loss despite poor diagnostics; welcome model coverage does not prove - foreground wiring, scroll reachability, or live motion. -- **Describe present behavior separately from intended invariants.** The - corrected reconciliation and snapshot docs now name the limitations that - remain filed. Documentation repairs do not imply runtime fixes. -- **Keep historical measurements historical.** Current references are 495; - the 260-reference settle measurements still require remeasurement. The - addressable raised-floor set remains 39 configurations, not a new timing result. +- **Partial completion needs a precise remainder.** The iOS uplift is shipped, + HistoryObserver is pending, semantic welcome captures exist, and the matrix + still lacks other coverage. Original priorities and origins are preserved. +- **Injection makes a test seam possible, not automatically deterministic.** + The fake location driver avoids live requests; the cancellation test still + needs to establish its two-waiter precondition before testing it. +- **Opening a store and reading a file are different failure boundaries.** + The intent now logs group-open failure; the widget stores' read defaults + remain a separate visibility gap. +- **Model tests and image references establish different facts.** Neither + establishes live scene delivery, scroll reachability, or VoiceOver focus. +- **Keep historical performance measurements historical.** There are now + 506 references and 43 raised-floor configurations; neither count refresh + remeasures the 260-reference timing experiment or proves a floor removable. ## Module inventory and Verified OK @@ -135,67 +153,71 @@ checks are static unless explicitly identified as executed. | [PeriscopeCore](Shared/Periscope/PeriscopeCore/README.md) | 38 | 33 | — | Span accessors downcast rather than store parallel span fields; journal still installs with the store. No new source in the window. | | [PeriscopeTools](Shared/Periscope/PeriscopeTools/README.md) | 27 | 27 | 1 | Hierarchy count/query asymmetry is explicitly pinned; 20 hosting-only assertions across 10 files and two image references remain. | | [PeriscopeUI](Shared/Periscope/PeriscopeUI/README.md) | 1 | 2 | — | Single SwiftUI environment adapter imports only PeriscopeCore and SwiftUI; test/support inventory unchanged. | -| [SnapshotKit](Shared/SnapshotKit/README.md) | 8 | 3 | — | Shippable matrix remains separate from comparison engine; docs now disclose that the runner shares captured models across configurations. | -| [SnapshotKitTesting](Shared/SnapshotKitTesting/README.md) | 16 | 16 | — | Provider duplicate guard, cancellation outcome, parse failure paths and config loop match filed issues; shard plan validates all 49 suites. | +| [SnapshotKit](Shared/SnapshotKit/README.md) | 8 | 3 | — | Shippable matrix remains separate from comparison engine; docs continue to disclose that the runner shares captured models across configurations. | +| [SnapshotKitTesting](Shared/SnapshotKitTesting/README.md) | 16 | 16 | — | Provider duplicate guard, cancellation outcome, parse failure paths and config loop match filed issues; shard plan validates all 50 suites. | | [StuffTestHost](Shared/StuffTestHost/README.md) | 2 | 0 | — | UIKit shell delegates test-window setup to TestHostSupport; no WhereCore import or new source. | | [TestHostSupport](Shared/TestHostSupport/README.md) | 1 | 0 | — | UIKit/Objective-C hosting seam remains app-independent; host smoke contract is exercised from LifecycleKit tests. | | [RegionKit](Where/RegionKit/README.md) | 15 | 10 | — | GeoJSON decoding gap is honestly documented; source still throws for unsupported geometry. No new source in the window. | | [RegionViewer](Where/RegionViewer/README.md) | 1 | 0 | — | Bundled per-region data description remains correct; missing Broadway root is still filed in Where. | -| [Where](Where/Where/README.md) | 8 | 4 | — | Runtime selection and intent handoff remain in the app shell; new welcome work did not add a second store or runtime. | -| [WhereCore](Where/WhereCore/README.md) | 129 | 84 | — | New resolver rechecks recording authority after suspension and uses the injected attributor; revoked-authorization regression exists. | +| [Where](Where/Where/README.md) | 9 | 5 | — | Audience selection validates compiler condition against plist metadata and injects one environment into launch and intents; hosted tests select the in-memory/no-op path. | +| [WhereCore](Where/WhereCore/README.md) | 132 | 86 | — | Resolver rechecks authority after suspension and gates freshness, accuracy, and boundary confidence; fake-driver tests cover explicit one-shot outcomes. The new synchronization finding concerns test orchestration. | | [WhereCrashReporting](Where/WhereCrashReporting/README.md) | 3 | 2 | — | Capture SDK stays behind the dedicated adapter target; no source, dependency, or test changes in this window. | -| [WhereIntents](Where/WhereIntents/README.md) | 15 | 9 | — | Intent services remain injected; four shortcuts and the perform-glue testing limitation match source. No new source in the window. | -| [WhereShareExtension](Where/WhereShareExtension/README.md) | 5 | 0 | — | Compose model still builds pending evidence; no test bundle was silently added. Shared form/testing gaps remain filed. | -| [WhereUI](Where/WhereUI/README.md) | 289 | 104 | 46 | Welcome cancellation/preference guards, dismissal-only persistence, localized controls, and Reduce Motion tokens have source/test evidence; see new-surface review below. | -| [WhereWidgets](Where/WhereWidgets/README.md) | 7 | 0 | — | Provider retains midnight reload policy and reads published stores; no direct new service or welcome dependency. | +| [WhereIntents](Where/WhereIntents/README.md) | 15 | 9 | — | Injected audience group reaches the snapshot reader; App Group-open failure is logged before report fallback. README now states the perform-glue testing limitation. | +| [WhereShareExtension](Where/WhereShareExtension/README.md) | 6 | 0 | — | Extension validates its audience and uses local-only storage in that audience’s App Group. Pending-evidence construction and the filed form/testing gaps are unchanged. | +| [WhereUI](Where/WhereUI/README.md) | 291 | 104 | 47 | MainTabs keys welcome lookup to active scene + enabled preference; model guards stale sequences and dismissal-only acknowledgement. Modal hides underlying tab accessibility; matrix now includes semantic captures. | +| [WhereWidgets](Where/WhereWidgets/README.md) | 8 | 0 | — | Audience-specific group is injected into both snapshot and presentation stores; provider retains the midnight reload policy without opening the app’s SwiftData store. | -**Totals:** 709 source, 371 test/support, and 49 image-suite Swift files. +**Totals:** 717 source, 374 test/support, and 50 image-suite Swift files. The inventory excludes two unwired Periscope journal-benchmark sources and four Bumper rule/test files. `Package.swift` declares 20 library targets; `Project.swift` declares seven app/extension targets and 25 test bundles: 20 unit bundles in `Stuff-iOS-Tests`, LedgerCoreTests in `Ledger-macOS-Tests`, -and four image bundles in `StuffSnapshotTests`. Neither manifest changed in -the reviewed window. +and four image bundles in `StuffSnapshotTests`. Manifest changes add audience +configuration and raise the iOS minimum, without adding test bundles. -**References:** 484 WhereUI, five Flyover, four Inspector, two PeriscopeTools, -for 495 total. The 49 addressable suites retain assignments 13 / 15 / 18 plus -three on the intake shard. The shard validator passes. WhereCore's basename -coverage proxy remains 60 sources without a namesake test among 129 sources; -logging types and record shells mean that is not a list of 60 untested behaviors. +**References:** 495 WhereUI, five Flyover, four Inspector, two PeriscopeTools, +for 506 total. The 50 suites retain assignments 13 / 15 / 18 plus four on the +intake shard. The shard validator passes. WhereCore's basename coverage proxy +is 61 sources without a namesake test among 132 sources, previously 60 of 129. +Logging types and record shells mean this is not a count of untested behaviors. +The coalesced-source and resolver-log tests add namesake coverage; two new typed +location outcomes are tested through consumers rather than namesake files. **Group docs:** Broadway and Periscope have both docs; Where lacks its group README and Ledger lacks both group docs. Their leaf modules are complete. -The existing root item remains open, with its stale 28-leaf count corrected. +The existing root item remains open. **Bumper and tooling:** the ten `where.*` rules and eleven source-rule test functions remain; the explicit-calendar filter and mutation fixtures still miss the 12 implicit calendar sites (four production, eight DEBUG fixtures). -`component_boundary` and `forbidden_import` have mutation tests; the two other -graph assertions remain filed for missing mutation coverage. Source review -establishes that mechanism; no fresh architecture run is claimed. The 18 root -commands, retained tooling layer, and CI configuration had no executable change. +`component_boundary` and `forbidden_import` have mutation tests; the other two +graph assertions remain filed for missing mutation coverage. This is source +inspection, not a fresh architecture execution. The icon tooling now understands +audience primaries, CircleCI has release build shards, and the snapshot renderer +pin changed in PR #314. Retained tooling suites were not rerun for this audit's +documentation-only edits. ## Verification and limitations -- `./swiftformat --lint` — passed, 0 of 1,136 files require formatting; - 125 skipped. The sandbox prevented writing its optional cache, without +- `./swiftformat --lint` — passed, 0 of 1,148 files require formatting; + 125 skipped. The sandbox prevented writing its optional cache without affecting the lint result. - `./shellcheck` — passed. - `./attribution --check` — passed, 12 credits current. -- `./snapshot-shards check` — passed, all 49 suites assigned. +- `./snapshot-shards check` — passed, all 50 suites assigned. - `./sync-agents` — passed after instruction edits; generated mirrors remain ignored. `git diff --check` — passed. - This run is on **macOS**, but the audit remains **static analysis** plus the supported host checks above. The skill's Linux limitations still apply to Linux runs: no Tuist, Xcode, simulator, or runtime validation is implied. -- `./test`, architecture execution, simulator/image suites, and the retained +- `./test`, architecture execution, simulator/image suites, and retained Python/Ruby suites were skipped because this change is Markdown and Swift comments only. There are no executable, matrix, reference, or rendered-copy - changes. Prior PR test counts were read as historical evidence, not reported - as this run's results. The prior Ruby sandbox failure was not rerun or closed. + changes. Prior PR test counts are historical evidence, not this run's results. + Existing Linux/tool-portability findings were not reproduced or closed. - No fresh screenshots or live animations were inspected. Existing visual - defects and quarantines remain open; the welcome's focus, foreground - lifecycle, and motion need device/simulator validation when addressed. + defects and the Inspector/Elsewhere quarantines remain open; the new welcome + still needs device/simulator validation for lifecycle, focus, and scrolling. - CloudKit readiness, passive background delivery, multi-process journals, runtime diagnostic routing, and Ledger's live API/Keychain behavior were not exercised. Historical timing/spike conclusions remain conditional on their diff --git a/Shared/Broadway/TODOs.md b/Shared/Broadway/TODOs.md index 35ea77065..06e1c1111 100644 --- a/Shared/Broadway/TODOs.md +++ b/Shared/Broadway/TODOs.md @@ -10,9 +10,9 @@ 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:660-669` — 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) +- 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:817-826` — 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 `./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:748`, and again at `:771`), so CI runs it and it asserts nothing. Replace it with a launch smoke test. (audit 2026-07-26; re-verified 2026-09-06) +- test(BroadwayCatalog) [quick-win]: `Tests/BroadwayCatalogTests.swift:4` is an empty `struct BroadwayCatalogTests {}` wired into the `Stuff-iOS-Tests` scheme (`Project.swift:905`, and again at `:928`), so CI runs it and it asserts nothing. Replace it with a launch smoke test. (audit 2026-07-26; re-verified 2026-09-06) - 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) diff --git a/Shared/Periscope/TODOs.md b/Shared/Periscope/TODOs.md index 535ae5df5..703b3b696 100644 --- a/Shared/Periscope/TODOs.md +++ b/Shared/Periscope/TODOs.md @@ -33,8 +33,8 @@ here. - 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 already models the lifecycle honestly as `WhereModel.LogStoreState` (`Where/WhereUI/Sources/Model/WhereModel.swift:32-39`: opening/ready/unavailable/failed); `logStore` is only a computed optional projection (`:114-120`). `RootView` still watches that projection's identity (`Where/WhereUI/Sources/RootView.swift:203`) to wire the viewer/inspector/alerter. The remaining design question is the async consumer handoff, not replacing a bare optional state machine. 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. **Twenty** tests across **ten** files assert nothing but "the hosted view reached a window" (filed as eighteen across nine; grew to twenty across ten when PR #152 added a file; **held at twenty across ten** on the 2026-08-16 and 2026-08-30 recounts, so the debt has now stopped growing for two consecutive windows without being worked down): `#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 (re-checked 2026-08-30, 2 reference images), so none of the conversion has happened; the bundle and its `SnapshotKitTesting` link are wired at `Project.swift:627-633` — 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:20`) 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 has separate image-coverage gaps in [`Shared/Inspector/TODOs.md`](../Inspector/TODOs.md); that file does not claim this same hosting-smoke conversion debt.) (pr#101 review; re-verified 2026-09-06 — still 20 assertions across 10 files, third consecutive audit at that count) +- design(PeriscopeCore) [needs-design]: No eager store handle — `PeriscopeStore.make` being `async` forces an "optional store, observe until it lands" dance on consumers. Where already models the lifecycle honestly as `WhereModel.LogStoreState` (`Where/WhereUI/Sources/Model/WhereModel.swift:32-39`: opening/ready/unavailable/failed); `logStore` is only a computed optional projection (`:114-120`). `RootView` still watches that projection's identity (`Where/WhereUI/Sources/RootView.swift:212`) to wire the viewer/inspector/alerter. The remaining design question is the async consumer handoff, not replacing a bare optional state machine. 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. **Twenty** tests across **ten** files assert nothing but "the hosted view reached a window" (filed as eighteen across nine; grew to twenty across ten when PR #152 added a file; **held at twenty across ten** on the 2026-08-16 and 2026-08-30 recounts, so the debt has now stopped growing for two consecutive windows without being worked down): `#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 (re-checked 2026-08-30, 2 reference images), so none of the conversion has happened; the bundle and its `SnapshotKitTesting` link are wired at `Project.swift:784-790` — 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:20`) 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 has separate image-coverage gaps in [`Shared/Inspector/TODOs.md`](../Inspector/TODOs.md); that file does not claim this same hosting-smoke conversion debt.) (pr#101 review; re-verified 2026-09-06 — still 20 assertions across 10 files, third consecutive audit at that count) # Completed issues diff --git a/Shared/SnapshotKitTesting/AGENTS.md b/Shared/SnapshotKitTesting/AGENTS.md index 3a9321688..9155b4897 100644 --- a/Shared/SnapshotKitTesting/AGENTS.md +++ b/Shared/SnapshotKitTesting/AGENTS.md @@ -95,7 +95,7 @@ Read the root [`AGENTS.md`](../../AGENTS.md) first. - **Immediate measurement never shortens final capture settling.** It skips only the intrinsic-sizing probe's settle for synchronously sized fixtures. - **The final `.settled` / `.settledAtLeast` policy still runs.** Guards: `AsyncContentCaptureTests`. - **A settle phase costs its floor, not its passes.** -- **Measured 2026-07-28 with `SNAPSHOT_TIMING=1` over 260 references of the time.** The suite holds 490 as of 2026-09-06. Re-measure before acting on the split below. +- **Measured 2026-07-28 with `SNAPSHOT_TIMING=1` over 260 references of the time.** Current inventory lives in [`MODULE_AUDIT.md`](../../MODULE_AUDIT.md). Re-measure before acting on the split below. - **The conclusion (the floor dominates) is what to rely on, not the seconds.** - **192 captures sat at 0.25-0.35s — the `minDuration` floor plus a pass or two.** The floor accounts for ~70s of the ~84s of settle time. - **The render passes themselves are ~14s across the whole suite.** Making passes cheaper is worth ~11%. Removing floors is worth ~54%. diff --git a/Shared/SnapshotKitTesting/TODOs.md b/Shared/SnapshotKitTesting/TODOs.md index 825942b81..8fef9e554 100644 --- a/Shared/SnapshotKitTesting/TODOs.md +++ b/Shared/SnapshotKitTesting/TODOs.md @@ -13,9 +13,9 @@ The item format and placement rule live in the root - 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 (`: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. -- 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 seconds below are stale — re-measure before acting on them; the conclusion (the floor dominates) is what to rely on.** They were taken with `SNAPSHOT_TIMING=1` over the **260** references of the time; the suite holds **495** as of 2026-09-07 (484 WhereUI, 5 Flyover, 4 Inspector, 2 PeriscopeTools; up from 490 in the prior report). Note the August 9 audit published 361 for that date and was wrong; 381 was the tree's real count, which is why the two figures in [`AGENTS.md`](AGENTS.md) disagreed. 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 — 39 configurations, not 22** (re-derived 2026-09-06 — the two prior enumerations published 37 and both missed `RankingAnimationLabView`, whose `settledAtLeast(1.0)` over `.fullContentPhoneLightDark` (`Developer/RankingAnimation/RankingAnimationLabView.swift:55-62`) contributes 2 and predates the August 30 audit; it belongs in the split for the same reason Flyover's DEBUG canvas does): `YearView.Loaded` (`YearView.swift:128-132`) and `LocationsView.Loaded` (`Primary/LocationsView.swift:395-399`) at 10 each and 1.0s; `AboutSettingsView.Default` (`AboutSettingsView.swift:246-253`) at 10 and 0.75s, which is new since the split was written; `RootView.LoggedIn` and `RootView.RecordingConfigurationWarning` (`RootView.swift:352`, `:364`) at 2 each and 1.5s; the Ranking Animation Lab at 2 and 1.0s; and Flyover's canvas captures at 3 and 1.5s (`FlyoverSnapshotTests.swift:29`, `:52`). The 10-per-case figure is the additive `defaults()` matrix crossed with iPhone and iPad full-content presets (`SnapshotConfiguration+Combinations.swift:76-86`); the 2 is light + dark iPhone (`WhereSnapshot.swift:45-49`). The `root.LoggedIn` seam is already specced in [`Where/TODOs.md`](../../Where/TODOs.md); `YearView`, `LocationsView`, and `AboutSettingsView` need an equivalent "the report finished loading" signal on `YearReportModel`, and Flyover already has one — its `waitUntilVisiblePreviewsAreLoaded()` hook covers preview loading, so its remaining floor is only the glass adaptation. - **Probably not addressable:** the default 0.25s floor on the 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; re-enumerated 2026-09-06) +- 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 seconds below are stale — re-measure before acting on them; the conclusion (the floor dominates) is what to rely on.** They were taken with `SNAPSHOT_TIMING=1` over the **260** references of the time; the suite holds **506** as of 2026-09-21 (495 WhereUI, 5 Flyover, 4 Inspector, 2 PeriscopeTools; previously 495 total). Note the August 9 audit published 361 for that date and was wrong; 381 was the tree's real count, which is why the two figures in [`AGENTS.md`](AGENTS.md) disagreed. 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()`. + **Raised-floor candidates — 43 configurations** (re-derived 2026-09-21; previously 39 — the two prior enumerations published 37 and both missed `RankingAnimationLabView`, whose `settledAtLeast(1.0)` over `.fullContentPhoneLightDark` (`Developer/RankingAnimation/RankingAnimationLabView.swift:55-62`) contributes 2 and predates the August 30 audit; it belongs in the split for the same reason Flyover's DEBUG canvas does): `YearView.Loaded` (`YearView.swift:128-132`) and `LocationsView.Loaded` (`Primary/LocationsView.swift:341-345`) at 10 each and 1.0s; `AboutSettingsView.Default` (`AboutSettingsView.swift:246-253`) at 10 and 1.5s (raised from 0.75s in PR #174); `RootView.LoggedIn` and `RootView.RecordingConfigurationWarning` (`RootView.swift:364`, `:377`) at 2 each and 1.5s; the Ranking Animation Lab at 2 and 1.0s; Flyover's canvas captures at 3 and 1.5s (`FlyoverSnapshotTests.swift:29`, `:52`); and the four new `MainTabs` AX5 configurations at 1.0s (`MainTabs.swift:244-277`). The latter use pre-seeded welcome/accessory state, so their floor is not evidence of an unfinished location request. The 10-per-case figure is the additive `defaults()` matrix crossed with iPhone and iPad full-content presets (`SnapshotConfiguration+Combinations.swift:76-86`); the 2 is light + dark iPhone (`WhereSnapshot.swift:45-49`). The `root.LoggedIn` seam is already specced in [`Where/TODOs.md`](../../Where/TODOs.md); `YearView`, `LocationsView`, and `AboutSettingsView` need an equivalent "the report finished loading" signal on `YearReportModel`, and Flyover already has one — its `waitUntilVisiblePreviewsAreLoaded()` hook covers preview loading, so its remaining floor is only the glass adaptation. + **Probably not addressable:** the default 0.25s floor on the 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 native glass toolbar/tab-bar material adaptation (still required on the iOS 27 target). 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; re-enumerated 2026-09-21; no new timing measurement) ## 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) diff --git a/TODOs.md b/TODOs.md index 2ba3cd6ce..d49f094a6 100644 --- a/TODOs.md +++ b/TODOs.md @@ -92,30 +92,30 @@ inbox rather than here. # Open issues ## PX (Exploratory) -- 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) +- feat: Complete the iOS 27 / HistoryObserver direction — PR #318 (`a32b8b05`) shipped the iOS 27 minimum in `Package.swift:8` and `Project.swift:4`; macOS remains 26. The remaining part is replacing WhereCore's remote-change notification/history adapter (`Where/WhereCore/Sources/Persistence/StoreRemoteChangeSource.swift`) with HistoryObserver. PR #319 is open, not merged, as of 2026-09-21. Keep this originating cross-target item open until that follow-on lands; do not repeat the deployment uplift. (human; narrowed 2026-09-21) ## 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:124-125`, rule at `:117-137`, `severity: .error` at `:119`), 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: **still 12 sites** (re-counted 2026-08-30), 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 still hard-gates the lint and is green, which confirms the rule reports none of them — the `architecture` job at `.github/workflows/ci.yml:72-73` reaches `bumper config`/`test`/`lint` through `test:253-261`. **Why it has survived six audits:** the rule's own mutation test only ever feeds it a spelled-out `Calendar.current` (`.bumper/Tests/WhereProjectRulesTests.swift:154-196`, both rejection fixtures at `:170` and `:177`), 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-09-06 — still 12 implicit sites, none reported) +- 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:124-125`, rule at `:117-137`, `severity: .error` at `:119`), 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: **still 12 sites** (re-counted 2026-09-21), 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 still hard-gates the lint, but the source filter cannot match these implicit sites — the `architecture` job at `.github/workflows/ci.yml:72-73` reaches `bumper config`/`test`/`lint` through `test:253-261`. **The missing regression:** the rule's own mutation test only ever feeds it a spelled-out `Calendar.current` (`.bumper/Tests/WhereProjectRulesTests.swift:154-196`, both rejection fixtures at `:170` and `:177`), 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 statically 2026-09-21 — still 12 implicit sites; no fresh lint execution claimed) ## P1s (Should do) -- docs(CI) [quick-win]: Finish reconciling the repo's description of CI with the two-system split PR #237 created. **iOS unit tests and snapshots run on CircleCI** (`build-ios-tests`, `test-ios`, and a 4-way-sharded `snapshot`, `m4pro` executors, Xcode 27.0 — `.circleci/config.yml:263-376`), while GitHub Actions keeps `format`, `architecture`, and `test-macos` (`.github/workflows/ci.yml:24-121`); CircleCI passes `--skip-architecture` so Bumper is not run twice. Two halves were corrected on 2026-08-16 (root [`AGENTS.md`](AGENTS.md)'s "Full build & test" section, which claimed CI *is* `.github/workflows/ci.yml`, and that workflow's own `test-macos` comment). What remains, and **both halves got worse this window rather than staying put**: - - **Restore the warning that PR #237 dropped — now urgent, because PR #276 made the snapshot job visibly parallel.** The old GitHub Actions snapshot job carried a paragraph explaining why the suite must not be parallelized: `-parallel-testing-enabled` distributes XCTest *classes* and Swift Testing presents none, so everything lands on one worker and Swift Testing's own in-process parallelism interleaves captures inside a single `StuffTestHost` and corrupts them (`41bdb0e5:.github/workflows/ci.yml:147-154`). That comment never travelled to `.circleci/config.yml`, and the file now reads `parallelism: 4` (`.circleci/config.yml:321`) with a per-container suite assignment from [`snapshot-shards`](snapshot-shards) — so a reader arrives at a job that *is* parallelized across containers, with nothing saying that the one remaining serial axis (inside each container) is load-bearing. The distinction is the whole point: sharding by suite across isolated VMs is safe and is what PR #276 landed; parallelizing *within* a container is not. Put a short version back beside the CircleCI snapshot job, naming both, and point at the measured detail in [`SnapshotKitTesting/AGENTS.md`](Shared/SnapshotKitTesting/AGENTS.md). - - **Reconcile the "what CI runs" claims — the gap widened by two gates.** `./test --everything` is described as "what CI runs" ([`test`](test):102, `:144`) and the [`running-tests`](.agents/skills/running-tests/SKILL.md) skill heads its recipe "Full macOS validation (matches CI)" (`SKILL.md:125-135`), but neither covers the `format` job's `./sf-symbols --lint`, `./xcstrings --lint`, `./attribution --check`, nor — added by PR #283 this window — `./shellcheck` and the retained-tool test suites (`.github/workflows/ci.yml:32-37`), nor the `architecture` job's `./test --architecture-only`. The format job is now six checking steps deep and the recipe names one of them. Either widen the recipe or stop calling it a match. (audit 2026-08-16; both halves re-verified and sharpened 2026-08-30) +- docs(CI) [quick-win]: Finish reconciling the repo's description of CI with the two-system split PR #237 created. **iOS unit tests and snapshots run on CircleCI** (`build-ios-tests`, `test-ios`, and a 4-way-sharded `snapshot`, `m4pro` executors, Xcode 27.0 — `.circleci/config.yml:290-437`), while GitHub Actions keeps `format`, `architecture`, and `test-macos` (`.github/workflows/ci.yml:24-121`); CircleCI passes `--skip-architecture` so Bumper is not run twice. Two halves were corrected on 2026-08-16 (root [`AGENTS.md`](AGENTS.md)'s "Full build & test" section, which claimed CI *is* `.github/workflows/ci.yml`, and that workflow's own `test-macos` comment). Two documentation gaps remain: + - **Restore the warning that PR #237 dropped — now urgent, because PR #276 made the snapshot job visibly parallel.** The old GitHub Actions snapshot job carried a paragraph explaining why the suite must not be parallelized: `-parallel-testing-enabled` distributes XCTest *classes* and Swift Testing presents none, so everything lands on one worker and Swift Testing's own in-process parallelism interleaves captures inside a single `StuffTestHost` and corrupts them (`41bdb0e5:.github/workflows/ci.yml:147-154`). That comment never travelled to `.circleci/config.yml`, and the file now reads `parallelism: 4` (`.circleci/config.yml:382`) with a per-container suite assignment from [`snapshot-shards`](snapshot-shards) — so a reader arrives at a job that *is* parallelized across containers, with nothing saying that the one remaining serial axis (inside each container) is load-bearing. The distinction is the whole point: sharding by suite across isolated VMs is safe and is what PR #276 landed; parallelizing *within* a container is not. Put a short version back beside the CircleCI snapshot job, naming both, and point at the measured detail in [`SnapshotKitTesting/AGENTS.md`](Shared/SnapshotKitTesting/AGENTS.md). + - **Reconcile the "what CI runs" claims.** `./test --everything` is described as "what CI runs" ([`test`](test):102, `:144`) and the [`running-tests`](.agents/skills/running-tests/SKILL.md) skill heads its recipe "Full macOS validation (matches CI)" (`SKILL.md:125-135`), but neither covers the `format` job's `./sf-symbols --lint`, `./xcstrings --lint`, `./attribution --check`, nor — added by PR #283 — `./shellcheck` and the retained-tool test suites (`.github/workflows/ci.yml:32-37`), nor the two release-audience build shards added in PR #174 (`.circleci/config.yml:345-378`). The format job is now six checking steps deep and the recipe names one of them. Either widen the recipe or stop calling it a match. The architecture sequence already runs by default inside `./test --everything`; it is not a missing gate in that recipe. (audit 2026-08-16; re-verified and corrected 2026-09-21) - test(Scripts) [quick-win]: The retained-tool test suites are the one CI gate that needs no Xcode, and they only pass on macOS. PR #283 made them a gate (`.github/workflows/ci.yml:34-37`, in the `format` job), which is the right call — but two assumptions in the tests are macOS-only, and CI can never report either because it only ever runs there. Both were found by running the gate on Linux during this audit: - **A hardcoded shell exit status.** `test_unlaunchable_xcodebuild_surfaces_shell_compatible_status` writes a fixture `xcodebuild` whose shebang points at `#!/missing/interpreter`, then asserts the status is exactly `126` (`Tools/Tests/test_xcode_command_contracts.py:399`). Bash 3.2 on macOS reports 126 ("found, cannot execute"); bash 5 and dash on Linux both report **127** ("command not found") for the same fixture — verified by probe, and it is the only failure in an otherwise green 64-test Python run. What the test is really pinning is that `./test` propagates the shell's status instead of masking it, and both values satisfy that, so `assertIn(result.returncode, (126, 127))` keeps the contract while making the test portable. - **A system Ruby at `/usr/bin`.** `PublicCommandContractTest` runs each of the 21 public commands under `PATH=/usr/bin:/bin` with `unsetenv_others: true` (`Tools/Tests/public_command_contract_test.rb:93-107`) to prove `--help` is dependency-free. That is a good test of a real contract, but it resolves the Ruby-implemented commands only where the OS ships `/usr/bin/ruby`, so on Linux twelve of its assertions fail with `/usr/bin/env: 'ruby': No such file or directory` rather than with anything about the commands. Either add mise's Ruby to the hermetic `PATH`, or state the macOS requirement on the test so a Linux failure reads as "wrong host" instead of "broken command". - **`./sync-agents` can't find Ruby on a bare Linux shell, and its sibling can.** It is `#!/usr/bin/env ruby`, so it dies with ``/usr/bin/env: 'ruby': No such file or directory`` unless mise's shims are already on `PATH` — and [`.cursor/install.sh`](.cursor/install.sh) puts only `~/.local/bin` there (`:18`), so a cloud agent following the committed setup has mise but not `ruby`. `./attribution` solves exactly this: it is a bash wrapper that resolves the pinned Ruby through `mise exec --` when mise is available (`attribution:75-78`). Give `sync-agents` the same treatment and the "Agent file sync" row in root [`AGENTS.md`](AGENTS.md#what-works-on-linux) becomes true as written instead of true-with-a-prefix. - **Running the suite dirties the tree.** `.gitignore` has no `__pycache__/` or `*.pyc` entry, so `python3 -m unittest discover -s Tools/Tests` leaves untracked `Tools/__pycache__/` and `Tools/Tests/__pycache__/` behind on any platform. That is new only in the sense that PR #283 turned an occasional command into one people will now run before pushing; it also puts a stray directory in front of anyone reading `git status` to check their own change. One `.gitignore` line, alongside the generated-artifact rules in root [`AGENTS.md`](AGENTS.md#repo-hygiene). These are separate portability and hygiene gaps: shell exit-status differences, Ruby discovery in the two command paths, and ignored Python bytecode. The missing system Ruby explains only the Ruby failures; it does not explain the shell status or untracked caches. Fixing them makes `python3 -m unittest discover -s Tools/Tests` and `./sync-agents` checks a cloud agent or a Linux contributor can run without knowing the workaround. Until they land, root [`AGENTS.md`](AGENTS.md#what-works-on-linux) should keep saying which half runs where. (audit 2026-08-30) -- docs [quick-win]: Delete the temporary `kve-stuff` CI benchmark organization after its paid plan ends — the [`Stuff-CI-Benchmark`](https://github.com/kve-stuff/Stuff-CI-Benchmark) repository exists only to retain the runner experiment, and GitHub Team is scheduled to downgrade to Free on September 9, 2026 — **two days after the 2026-09-07 audit** (the repository still exists and is not archived; the saved downgrade date has not been independently re-confirmed against billing). After the downgrade, preserve [the final benchmark report](https://github.com/kve-stuff/Stuff-CI-Benchmark/pull/3) in this repo if it is still useful, verify that the organization has no billable usage or installed integrations, then delete the organization. (human 2026-08-09) +- docs [quick-win]: Delete the temporary `kve-stuff` CI benchmark organization after its paid plan ends — the [`Stuff-CI-Benchmark`](https://github.com/kve-stuff/Stuff-CI-Benchmark) repository exists only to retain the runner experiment, and GitHub Team is scheduled to downgrade to Free on September 9, 2026 — **the saved date has now passed** (on 2026-09-21 the repository still exists and is not archived; actual downgrade, billing, and integration state remain unverified). After the downgrade, preserve [the final benchmark report](https://github.com/kve-stuff/Stuff-CI-Benchmark/pull/3) in this repo if it is still useful, verify that the organization has no billable usage or installed integrations, then delete the organization. (human 2026-08-09) - test(Bumper) [quick-win]: Two of the four architecture-graph assertions have no mutation test. The assertion set is three shapes producing four rule IDs (`.bumper/Sources/WhereArchitecture.swift:26-30` — `DependencyBoundaries`, `SingleOwner`, `AcyclicDeclaredDependencies`). `.bumper/Tests/` covers `component_boundary` (`WhereArchitectureTests.swift:27-46`) and `forbidden_import` (`:48-67`, `:69-91`), and every one of the ten source-level `where.*` rules has a mutation test in `WhereProjectRulesTests.swift` (eleven test functions for ten rules — `where.checked_concurrency_boundaries` has two, one per escape hatch) — 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:39` 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; recounted 2026-08-30 — still ten `where.*` rules, still nothing for the two graph assertions) -- 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 nine module folders 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. PR #172's repo-wide doc rewrite did not add either, and neither did this window, which touched fifteen Markdown files including four leaf `README.md`/`AGENTS.md` pairs — so the group level has now been skipped by every doc pass since the rule was written. The convention holds firmly at the leaves: all 27 module folders carry the pair (recounted 2026-09-07). (audit 2026-08-09; re-verified 2026-08-30) +- 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 nine module folders 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. PR #172's repo-wide doc rewrite did not add either; both group gaps remain as of 2026-09-21. The convention holds firmly at the leaves: all 27 module folders carry the pair (recounted 2026-09-21). (audit 2026-08-09; re-verified 2026-08-30) ## P2s (Nice to have) - perf(CI) [needs-design]: Re-evaluate caching Git LFS snapshot objects without fighting CircleCI's checkout hydration — at the time of that measurement the built-in checkout - downloaded all 377 then-current objects (358.58 MiB; the tree holds 495 - reference images as of 2026-09-07, so re-measure before retrying) before + downloaded all 377 then-current objects (358.58 MiB; the tree holds 506 + reference images as of 2026-09-21, so re-measure before retrying) before [`.circleci/config.yml:20-32`](.circleci/config.yml), while PR #245's first cache attempt encountered a cold miss and then made `git lfs prune --no-verify-remote` fail because the blobless clone lacked historical objects @@ -124,7 +124,7 @@ inbox rather than here. incomplete history, and benchmark Circle cache restore against the native checkout; land it only if both cold and warm snapshot jobs stay correct and get faster. (pr#245 review) -- feat(Scripts) [needs-design]: Teach `./test` the native-macOS tier — the 722-line script selects iOS bundles and simulator destinations only (`test:337-338`, `:486-499`), while `LedgerCoreTests` is wired into `Ledger-macOS-Tests` (`Project.swift:712-717`). The documentation half is complete: root `AGENTS.md` and the `running-tests` skill now explicitly name the native-macOS exception. What remains is deciding whether to add a macOS tier so one command can run all repository tests. Preserve the hostless destination and separate CI scheme if it does. (audit 2026-08-09; narrowed 2026-09-07) +- feat(Scripts) [needs-design]: Teach `./test` the native-macOS tier — the 722-line script selects iOS bundles and simulator destinations only (`test:337-338`, `:486-499`), while `LedgerCoreTests` is wired into `Ledger-macOS-Tests` (`Project.swift:869-874`). The documentation half is complete: root `AGENTS.md` and the `running-tests` skill now explicitly name the native-macOS exception. What remains is deciding whether to add a macOS tier so one command can run all repository tests. Preserve the hostless destination and separate CI scheme if it does. (audit 2026-08-09; narrowed 2026-09-07) - refactor(Scripts) [needs-design]: Evaluate `tuist xcodebuild test-without-building` as a way to retire `./test`'s affected-bundle parser. **The "fragile" half of this item is now obsolete and the decision half is what remains.** When filed, `affected_bundles` was ~164 lines of Python embedded in an 869-line shell script, and that embedding was the argument: nothing could test it. PR #284 moved it to `Tools/test_runner.py:35-120`, reachable from `./test:337`, with direct coverage in `Tools/Tests/test_test_runner.py:29-64` — including the over-selection of global paths and the short-parse rejection. It still infers declaration boundaries from indent level (`_declarations` at `Tools/test_runner.py:306-317`, with `_parse_problems` at `:351-361` failing rather than degrading to "no bundle covers these changes"), and it is still **local convenience only** — CI runs `--all` / `--snapshots`, so nothing in the pipeline depends on it. So the question is no longer "can we test this" but "is a regex over `Project.swift` the right source of truth at all". 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; premise corrected 2026-08-30) - 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. diff --git a/Where/TODOs.md b/Where/TODOs.md index c375572e7..44bfd32ba 100644 --- a/Where/TODOs.md +++ b/Where/TODOs.md @@ -13,24 +13,24 @@ The item format and the placement rule live in the root - design(WhereCore): Explore one explicit filesystem layout for store and sidecars — `WhereCore/Sources/Location/LocationOutbox.swift:115-130` places the outbox under Application Support, while the installation context is owned by `WhereUI/Sources/Launch/InstallationRecordingContextStore.swift`. Define the intended grouping and backup/reset boundaries before moving data. This is a storage-layout direction, not a verified data-loss bug. (human) - design(WhereCore): Re-shape the write path as an explicit pipeline. Every write into the DB changes various "views" and outputs, but nothing changes if no write happened. What if it were: 1) write into the DB, 2) kick off async jobs to re-evaluate DB contents, 3) write views out into a table, 4) consumers consume those view changes. This feels like it might give a cleaner pipeline-shaped layout, and importantly short-circuit a lot of work. (human) - design(WhereCore): Explore retaining a prior store in another folder on reset — current `WhereServices.reset()` (`WhereCore/Sources/WhereServices.swift:398-412`) revokes recording and delegates a generation-scoped erase; old synced rows may remain as history (`WhereCore/README.md`, destructive-operations contract). It is no longer accurate to assume every reset physically deletes every row. A folder rotation remains unimplemented; define how retained local worlds interact with CloudKit generations, installation retirement, and recovery before replacing the current reset. (human; narrowed 2026-09-07) -- fix(WhereCore): Explore coverage for a stationary day when the app is never foregrounded — `LocationIngestor.captureTodayIfNeeded(now:)` (`WhereCore/Sources/Location/LocationIngestor.swift:350-367`) already requests a best-effort fix on launch/foreground when today lacks GPS. The remaining question is background delivery without passive movement events, not whether opening the app can fill today. Measure the no-foreground case and define the desired fallback without promising an OS-guaranteed daily wakeup. (human; narrowed 2026-09-07) +- fix(WhereCore): Explore coverage for a stationary day when the app is never foregrounded — `LocationIngestor.captureTodayIfNeeded(now:)` (`WhereCore/Sources/Location/LocationIngestor.swift:348-366`) already requests a best-effort fix on launch/foreground when today lacks GPS. The remaining question is background delivery without passive movement events, not whether opening the app can fill today. Measure the no-foreground case and define the desired fallback without promising an OS-guaranteed daily wakeup. (human; narrowed 2026-09-07) ## P0s (Must do) - 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:198-217`) does the same, so the daily notification body stays stale until a foreground re-`configure`; backup, remote-import, recording-policy, and reset paths do reconcile summary — the first three through the composition root's `DerivedDataReconciler` (`WhereServices.swift:13-21` with `summary.reconcile()` at `:19`, wired at `:228`, `:247-259`, `:267-269`), and `reset()` by calling `summary.reconcile()` directly at `:409`. 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-09-06) - test(WhereCore) [quick-win]: Mutate data and assert the summary notification body updates without a re-`configure`. The nearest existing guard is `WhereServicesTests.remoteDayImportReconcilesNotificationsAndWidgets` (`WhereServicesTests.swift:519-562`), which covers the *remote-import* derived-data fan-out rather than a local day mutation, so it would not catch this. (audit 2026-07-26; citation refreshed 2026-09-06) -- perf(WhereCore) [needs-design]: Measure automatic launch and GPS-write frequency before changing the movement threshold — `LocationIngestor` owns passive sample admission and the explicit foreground one-shot (`WhereCore/Sources/Location/LocationIngestor.swift:347-367`); launch spans are declared in `WhereUI/Sources/Launch/WhereLaunchSteps.swift:177-210`. Use those diagnostics to establish boot/write rates, then decide whether distance filtering can reduce work without losing day boundaries or region crossings. A 1 km threshold remains a human proposal, not a measured requirement. (human) +- perf(WhereCore) [needs-design]: Measure automatic launch and GPS-write frequency before changing the movement threshold — `LocationIngestor` owns passive sample admission and the explicit foreground one-shot (`WhereCore/Sources/Location/LocationIngestor.swift:348-366`); launch spans are declared in `WhereUI/Sources/Launch/WhereLaunchSteps.swift:177-210`. Use those diagnostics to establish boot/write rates, then decide whether distance filtering can reduce work without losing day boundaries or region crossings. A 1 km threshold remains a human proposal, not a measured requirement. (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` (`WhereCore/Sources/Persistence/CloudKitImportReadiness.swift:19-26`), and `eventChanged(_:)` accepts any successful completed import (`:42-49`), while discovery starts that observer at `WhereUI/Sources/Launch/WhereLaunch.swift:332-333` — one line before `prepareStore()` creates the intended store at `:334` (lines moved when PR #301 prepended the demo step; the observer-before-store ordering is unchanged). 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; citations refreshed 2026-09-06) +- fix(WhereCore) [needs-design]: Scope initial CloudKit-import readiness to Where's expected store/container. `CloudKitImportReadiness.start()` observes `NSPersistentCloudKitContainer.eventChangedNotification` with `object: nil` (`WhereCore/Sources/Persistence/CloudKitImportReadiness.swift:19-26`), and `eventChanged(_:)` accepts any successful completed import (`:42-49`), while discovery starts that observer at `WhereUI/Sources/Launch/WhereLaunch.swift:335-336` — one line before `prepareStore()` creates the intended store at `:337` (PR #174 changed audience selection; the observer-before-store ordering is unchanged). 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; citations refreshed 2026-09-06) - 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:148-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. The onboarding phases currently move from region selection/customization directly to location permission (`OnboardingFlowModel.Phase`, `WhereUI/Sources/Onboarding/OnboardingFlowModel.swift:11-17` — the previously cited `OnboardingView.swift:30` no longer holds the enum), while `DayJournal.ingest(_:)` is the existing bulk sample path (`WhereCore/Sources/Journal/DayJournal.swift:99`). 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:34-37` builds the private, sink-less `Periscope` — PR #301 touched the file but the wiring is unchanged; `WhereCore/Sources/Logging/WhereLog.swift:17-32` is the static facade that bypasses it; agent 2026-07-29, re-verified 2026-09-06) -- 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-30): 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:31` and `ResolutionView.swift:182`, plus every `displayDate` consumer (`ResolutionView.swift:184`, `:191`, `:202`, `DayRelabelView.swift:220`, `RegionDaysView.swift:135`). 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:511`, `:540`, `:553`; `DayRelabelView.swift:266`, `:278`; `FlightDayDetailView.swift:182`; `AbruptChangeDetailView.swift:98`, `:103`) — 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` (still five sites: `WhereFormat.swift:80`, `YearReportModel.swift:300`, `WhereModel.swift:331`, `RemindersSettingsModel.swift:153`, `PreviewSupport.swift:513`) — that is a `TimeZone` on an explicit Gregorian calendar, which is the correct pattern, and it has been miscounted as drift by three separate passes. `PresenceJourneyRow` / `PlannedPresenceJourneyRow` likewise thread `calendar:` explicitly and are correctly outside the count. The forecasting work held the line: `Forecasting/LocationForecastProgress.swift:65` builds a fresh `Calendar(identifier: .gregorian)` rather than reaching for the device's, and PR #307's new joined-Timeline code also introduces no `Calendar.current`. 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-09-06 after PR #307) +- 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-30): 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:31` and `ResolutionView.swift:182`, plus every `displayDate` consumer (`ResolutionView.swift:184`, `:191`, `:202`, `DayRelabelView.swift:220`, `RegionDaysView.swift:135`). 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:511`, `:540`, `:553`; `DayRelabelView.swift:266`, `:278`; `FlightDayDetailView.swift:182`; `AbruptChangeDetailView.swift:98`, `:103`) — 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` (still five sites: `WhereFormat.swift:80`, `YearReportModel.swift:300`, `WhereModel.swift:331`, `RemindersSettingsModel.swift:153`, `PreviewSupport.swift:518`) — that is a `TimeZone` on an explicit Gregorian calendar, which is the correct pattern, and it has been miscounted as drift by three separate passes. `PresenceJourneyRow` / `PlannedPresenceJourneyRow` likewise thread `calendar:` explicitly and are correctly outside the count. The forecasting work held the line: `Forecasting/LocationForecastProgress.swift:65` builds a fresh `Calendar(identifier: .gregorian)` rather than reaching for the device's, and PR #307's new joined-Timeline code also introduces no `Calendar.current`. 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-09-06 after PR #307) - fix(WhereCore) [needs-design]: `WhereServices.setPrimaryRegions(_:)` (`WhereServices.swift:384-388`) 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-30) -- fix(WhereCore) [needs-design]: Soft-delete untracked regions. `SwiftDataStore.setTrackedRegion(false)` (`SwiftDataStore.swift:1844-1875`, in-source TODO at `:1863-1871`, delete at `:1872-1874`) and `setPrimaryRegions` (delete-by-omission at `:1925-1935`) 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-30) -- fix(WhereCore) [needs-design]: The retry queue evicts FIFO at its 1000-sample capacity and drops samples with a warning only (`LocationIngestor.swift:502-507`, capacity default at `:112`, event case at `LocationIngestorLog.swift:53` with its `warning` level at `:64-65` and message at `:91-92` — the previously cited `:76-77` was a different event's message). Decide the capacity policy and whether eviction warrants user-visible degradation, then document it. (audit 2026-07-26; re-verified 2026-08-30) +- fix(WhereCore) [needs-design]: Soft-delete untracked regions. `SwiftDataStore.setTrackedRegion(false)` (`SwiftDataStore.swift:1862-1893`, in-source TODO at `:1881-1889`, delete at `:1890-1892`) and `setPrimaryRegions` (delete-by-omission at `:1943-1954`) 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-30) +- fix(WhereCore) [needs-design]: The retry queue evicts FIFO at its 1000-sample capacity and drops samples with a warning only (`LocationIngestor.swift:500-505`, capacity default at `:112`, event case at `LocationIngestorLog.swift:53` with its `warning` level at `:64-65` and message at `:91-92` — the previously cited `:76-77` was a different event's message). Decide the capacity policy and whether eviction warrants user-visible degradation, then document it. (audit 2026-07-26; re-verified 2026-08-30) - fix(WhereUI) [quick-win]: `PresenceTimelineList` returns `[]` whenever `report.report` is nil (`PresenceTimelineList.swift:30-31`, empty-state branch at `:43`), 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:64-66`). PR #307 moved the branch while adding joined planned stays and the estimate panel but left the missing load gate unchanged. (audit 2026-07-26; re-verified 2026-09-06 after PR #307) - - refactor(WhereUI) [needs-design]: Extract a shared `ReportLoadGate`. The same `YearReportModel.loadState` gate is copy-pasted across `LocationsView.swift:151-176`, `ElsewhereView.swift:51-70`, `ResolutionView.swift:59-89`, and `CalendarContentView.swift:64-74`, and `PresenceTimelineList` skips it entirely (above). One gate view would cover all five. (audit 2026-07-26; re-verified 2026-09-06) + - refactor(WhereUI) [needs-design]: Extract a shared `ReportLoadGate`. The same `YearReportModel.loadState` gate is copy-pasted across `LocationsView.swift:107-132`, `ElsewhereView.swift:51-70`, `ResolutionView.swift:59-89`, and `CalendarContentView.swift:64-74`, and `PresenceTimelineList` skips it entirely (above). One gate view would cover all five. (audit 2026-07-26; re-verified 2026-09-06) - 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:29`, dropping the `WhereFormat.elsewhereCardSubtitle` hop at `WhereFormat.swift:49-50`) or replacing the markup with an explicit plural variation. `WhereFormatTests.elsewhereCardSubtitleInflectsTheRegionCount` (`WhereFormatTests.swift:92-101`, the `withKnownIssue` at `:98` — moved when PR #302 added format tests above it) 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) — PR #302 re-recorded the Locations references for the sticker redesign with the broken hop untouched, so re-record that image again when this lands. (agent; citations refreshed 2026-09-06) - 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 **still exactly 636 lines** as of 2026-09-06 (`Model/WhereSession.swift`) — it grew past the ~460 recorded when this was filed because PR #160's multi-device recording landed on it, and it has now held at 636 through three windows of privacy, theming, forecasting, ranking-motion, endorsement, and demo-launch work, so it is neither growing nor being worked down — 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-09-06) - 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:510-512`, `start == end` → `dayCount: 1`), so no test ever renders the `.range` branch (`:199-212`) — 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.) @@ -38,13 +38,12 @@ The item format and the placement rule live in the root - refactor(WhereUI) [needs-design]: Extract one shared region-selection form. `DayRelabelView.swift:109-110` renders a flat `ForEach(regionSelection.items)` where `ManualDayView.swift:223-228` has `GroupedRegionSections` plus `loadGrouping()` (`:256-273`), so the two screens disagree on how regions are picked. (audit 2026-07-26; re-verified 2026-09-06) - fix(WhereUI) [needs-design]: Notification authorization is requested during launch, with no context and unprompted. The chain, re-verified 2026-09-06: the launch's detached `reminders` / `summary` / `issue-alerts` steps (`WhereLaunchSteps.swift:177-210` — moved when PR #301 prepended the demo step) 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:81-82`, `DailySummaryReconciler.swift:47-48`, `DataIssueAlertReconciler.swift:48-49` → `UNUserNotificationCenter.requestAuthorization`). **All three preferences default to `true` on a fresh install** (`WherePreferences.swift:89`, `:112`, `:136`, 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]: Identify remaining invalid controller states before another state-machine rewrite — scope ownership is already `WhereModel.ScopeState` (`WhereUI/Sources/Model/WhereModel.swift:49-58`), and recording admission is gated in `LocationIngestor` (`WhereCore/Sources/Location/LocationIngestor.swift:329`). Keep this design direction scoped to a concrete illegal state discovered in the remaining controller orchestration; do not replace working typed state solely to satisfy the old note. (human; narrowed 2026-09-07) -- 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:79-90` now states the rule and explains the framework trap, so the "say so" option is satisfied for agents — but the README's Testing section (`README.md:83-87`) still reads as though the coverage is complete and never mentions the limitation, so a human reading only the README is misled. (The citations previously here — `AGENTS.md:95-108`, `README.md:106-112` — pointed past the end of both files after doc rewrites; corrected 2026-09-06.) Either extract a thin testable seam, or carry the AGENTS.md caveat into the README. (audit 2026-07-26; re-verified 2026-09-06) - 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:71` and `FeatureEvidenceArchivePreview.swift:66` apply `.accessibilityElement(children: .combine)` with **no** `.accessibilityLabel`, so VoiceOver reads each walkthrough step as one undifferentiated blob of its concatenated text. `FeatureEvidenceComposePreview.swift:45` uses `.contain`, which is better but still unlabeled. `SiriIntentCard.swift:35-37` is the pattern to copy — it sets both a label and a value. **Scope note (2026-08-16, re-checked 2026-08-30):** an unlabeled `.combine` is not automatically a defect, and this pass checked the new surfaces against that. PR #267's privacy passport rows combine without a label too (`PrivacyPassportDisclosureRowLabel.swift:41`) but are *correct* — their symbol is `.accessibilityHidden(true)` (`PrivacyPassportDisclosureSymbol.swift:17`) and the combined children are a title plus its one-line detail, which reads as a sentence. The same goes for the two snippet cards (`IntentSnippets.swift:61`, `:141`) and for this window's `StampBanner`, whose combined children are one localized sentence. What makes the three panels above defective is that they concatenate a multi-step walkthrough, not that they combine. (audit 2026-08-09; re-verified 2026-08-30) - refactor(WhereShareExtension) [needs-design]: Consolidate the share/add evidence form. `ShareEvidenceView.swift:65-89` and `AddEvidenceView.swift:37` are parallel implementations over parallel catalog namespaces (now the generated `shareForm*` / `evidenceForm*` symbols — the split itself is unchanged). (audit 2026-07-26; citations refreshed 2026-09-06) - 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 (`ReportReader.swift:37-49`, `WidgetDataReader.swift:87-103`). (audit 2026-07-26; re-verified 2026-08-30) - refactor(WhereUI): What's with all the `.accessibilityIdentifier(…)` modifiers, do we need them? (human) -- feat(WhereUI) [needs-design]: Keep the current region visible after dismissing its welcome — PR #309 added a live-region acknowledgement (`Primary/LocationWelcomeModel.swift:32-50`, `Primary/LocationsView.swift:114-119`), partially satisfying this request. Dismissal removes it (`LocationWelcomeModel.swift:54-58`), and the ranked cards still have no persistent current-region marker. Decide whether a border or localized current-location badge should remain on the matching card, using the existing `CurrentRegionResolver` rather than another location source. (human; narrowed 2026-09-07) +- feat(WhereUI) [needs-design]: Keep the current region visible after dismissing its welcome — PRs #309/#314 provide a foreground acknowledgement over any selected tab (`MainTabs.swift:106-116`, `Primary/LocationWelcomeModel.swift:131-151`), partially satisfying this request. Dismissal removes it and the ranked cards still have no persistent current-region marker. Decide whether a border or localized current-location badge should remain on the matching card, using the existing `CurrentRegionResolver` rather than another location source. (human; narrowed 2026-09-21) - 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) - refactor(WhereCore): Make record→value conversion (`SD….toValue()`) a version-aware **upcaster**: each `@Model` carries its written schema version, and `toValue()` applies an ordered, pure, idempotent `vN → vN+1` chain, so every read is correct regardless of stored version — no import hook or scan needed (there is no per-record CloudKit import callback anyway). This is the "lazy migration / event-sourcing upcaster" pattern. Make the *filtered* reads (`manualDays(in:)`) upcast-aware too, so a not-yet-rewritten row isn't dropped by a column predicate. (agent) - refactor(WhereCore): Persist a **minReaderVersion** per entity, not just a version. Additive (expand/contract) changes leave it low so old builds keep reading via the retained old field (tolerant reader); only a genuinely forward-incompatible change bumps it. Readers exclude entities whose `minReaderVersion > appVersion` and surface a "some data needs a newer app" warning — the only case that actually needs exclusion. (agent) @@ -53,40 +52,41 @@ The item format and the placement rule live in the root - 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) [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 (`DayCell` at `CalendarContentView.swift:489-493`, `.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 (`:313-317`). 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 have now been re-recorded twice with the layout code unchanged — by PR #196 and again by PR #297's Xcode 27 beta 6 refresh — so they pin the same defect at a third recording.** Re-check the current image before fixing, and re-record after. (pr#101 review; re-verified 2026-09-06) - 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:110`, with an in-source comment explaining it keeps labels from truncating mid-animation) inside a bottom `safeAreaInset` (`:40-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:367-382`, the red capsule + offset at `:380-381` — moved by PR #302's card work), 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-09-06) + - 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:313-328`, the red capsule + offset at `:326-327` — moved by PR #302's card work), 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-09-06) - 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) -- test(WhereUI) [quick-win]: Cover the welcome overlay's scrolling and modal semantics — `WelcomeFirst` / `WelcomeBack` use fixed `.phoneLightDark` frames and one fixed AX5 frame (`Primary/LocationsView.swift:447-462`) while the modal contains a `ScrollView` inside a greedy `GeometryReader` (`Primary/LocationWelcomeOverlay.swift:25-39`). No welcome configuration uses the semantic `.accessibility` capture or an iPad frame. Capture the shared scrolling child with full-content sizing if the bounded modal cannot converge, and add a semantic modal case plus iPad coverage. Keep the production modal/focus wiring; a fixed AX5 image cannot prove that all scrollable controls or VoiceOver elements remain reachable. (audit 2026-09-07, PRs #309/#311) +- test(WhereCore) [quick-win]: Synchronize both coalesced callers before testing cancellation — `CoreLocationSourceTests.swift:71-85` launches two tasks but waits only for `probe.requestCount == 1`. That proves the first system request started, not that the second caller registered. If the second task has not entered before the first cancels, stopping the only registered waiter is correct, yet the test expects `stopCount == 0`. Add a test-only pending-waiter observation or a deterministic registration handshake, wait for two waiters, then cancel one. Bound the yield loop at `:156-160` so a broken registration fails locally rather than hanging the suite. This is a source-identified scheduling gap, not a reproduced CI failure. (audit 2026-09-21, PR #314) +- test(WhereUI) [quick-win]: Complete the welcome overlay's scrolling and iPad coverage — PR #314 replaced the two Locations welcome cases with four app-shell states in `MainTabs.swift:206-277`, each captured in light, dark, AX5, and semantic accessibility configurations. The missing VoiceOver-capture claim is closed. All 16 configurations still use fixed iPhone frames; the modal contains a `ScrollView` inside a greedy `GeometryReader` (`Primary/LocationWelcomeOverlay.swift:25-39`), and no iPad welcome is captured. Capture its shared scrolling child with full-content sizing if the bounded modal cannot converge, retain the app-shell semantic cases, and add iPad coverage. The new fixtures all use `.returnVisit` (`MainTabs.swift:279-282`); preserve first-greeting rendering coverage when completing the matrix. A fixed AX5 image does not prove scroll reachability. (audit 2026-09-07, PRs #309/#311; narrowed 2026-09-21 after PR #314) - 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) - 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-30) -- fix(WhereCore) [quick-win]: The two App Group widget stores each answer three ways but tell only two of them apart. `WidgetSnapshotStore.read()`'s `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-79`). The doc comment directly above states that "the two ways of answering `nil` are told apart in the log" (`:60-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. **PR #244's multi-theme work copied the shape:** `WidgetPresentationStore.readTheme()` (`WidgetPresentationStore.swift:43`) returns `.standard` on an unreadable file and warns only on a decode failure (`:46-50`), so a widget silently rendering in the default theme is indistinguishable from one whose theme file can't be read. Fix both together — test the file's existence to separate the two, and warn on a thrown read the way each decode path already does. (audit 2026-08-09; second store found 2026-08-16; re-verified 2026-08-30) +- fix(WhereCore) [quick-win]: The two App Group widget stores each answer three ways but tell only two of them apart. `WidgetSnapshotStore.read()`'s `guard let data = try? Data(contentsOf: fileURL) else { return nil }` (`WidgetSnapshotStore.swift:68`) 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 (`:71-75`). The doc comment directly above states that "the two ways of answering `nil` are told apart in the log" (`:56-66`), 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. **PR #244's multi-theme work copied the shape:** `WidgetPresentationStore.readTheme()` (`WidgetPresentationStore.swift:42`) returns `.standard` on an unreadable file and warns only on a decode failure (`:45-49`), so a widget silently rendering in the default theme is indistinguishable from one whose theme file can't be read. Fix both together — test the file's existence to separate the two, and warn on a thrown read the way each decode path already does. (audit 2026-08-09; second store found 2026-08-16; re-verified 2026-08-30) - fix(WhereUI) [quick-win]: A failed remote-logging apply is never logged, and the caption it shows the user is a Swift reflection dump. `DiagnosticReportingSettingsModel`'s apply task catches every non-cancellation error into `applyState = .failed(message: String(describing: error))` (`DiagnosticReportingSettingsModel.swift:200`) and emits nothing — the file references no `WhereLog` channel at all — so the repo's rule that a `catch` must log *and* leave state honest (root [`AGENTS.md`](../AGENTS.md#errors-and-failure)) is half kept. The state is honest, but nothing durable records that Privacy & Diagnostics failed to apply, which is a poor place to lose the signal: this is the screen that governs diagnostics, and PR #266 already added `AppDelegate.recordDiagnosticProviderFailure` for the startup half of the same story. `String(describing:)` also renders the error's default reflection rather than its `localizedDescription`, and `PrivacyDiagnosticsSettingsView.swift:65-71` puts that string on screen under the localized "couldn't apply" headline — so a user can see a bare Swift type name. Log a typed event carrying `LogAttachment.error` on that catch, and derive the caption from `localizedDescription` (or a mapped localized string). (audit 2026-08-16) - fix(WhereUI) [needs-design]: Make `LocationNamer` cancellation-aware. `ElsewhereView.loadPlaceNames()` (`:35-46`) 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` (`:127-129`) 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 — the separators are interpolated inline as `"\(unit) in \(region.localizedName) · \(yearText)"` (`Sources/Intents/IntentSnippets.swift:64-67`) rather than coming from a catalog key with placeholders, so the one string a Siri snippet shows is the one string that can't be translated. (The `#Preview` half is closed — it now uses `.snippetLogTodayHere` instead of a literal.) (audit 2026-07-26; re-verified 2026-08-30) - refactor(WhereUI) [quick-win]: PR #302's sticker redesign orphaned `WhereFormat.locationCardEstimatedDays` — the card now composes its estimate from `WhereFormat.dayCount` plus `Text(.locationCardEstimateLabel)` (`Primary/LocationCardEstimateSticker.swift:25-28`), leaving the old `"Estimated · %@"` helper (`Shared/WhereFormat.swift:97-98`), its `locationCard.estimatedDays` catalog key, and its test (`WhereFormatTests.swift:70`) with no production caller. Delete all three together (removing the source symbol reference is what lets the catalog entry go — see the auto-extraction item above), or keep them deliberately if an alternate surface is planned. Dead-but-tested format helpers read as live API. (audit 2026-09-06) - 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-30) -- 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:41-48`, `.after(nextMidnight)` — the extension's core scheduling logic, untested in any target). (audit 2026-07-26; citations refreshed 2026-09-06) +- 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:42-49`, `.after(nextMidnight)` — the extension's core scheduling logic, untested in any target). (audit 2026-07-26; citations refreshed 2026-09-06) - 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:165-168` now explicitly documents the missing decoding coverage. The earlier overclaim was fixed by PR #172; the tests remain absent (re-verified 2026-09-07). (audit 2026-07-26; re-verified 2026-08-09) - test(WhereUI) [quick-win]: Add the missing namesake test for `LocationNamer` (cache / coalescing — the file now lives at `Sources/Shared/LocationNamer.swift`). **The other half of this item is obsolete:** `CalendarContentView.scrolledForYear` no longer exists — the scroll reveal is now a `scrollPosition.scrollTo` inside `.task(id:)` (`CalendarContentView.swift:175-180`), so there is no gate left to test as filed; narrowed rather than closed because the `LocationNamer` half is untouched. (audit 2026-07-26; narrowed 2026-09-06) - test(WhereIntents) [quick-win]: Test `RegionSpotlightIndexer`. 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. (audit 2026-07-26; re-verified 2026-08-09) -- test(WhereCore) [needs-design]: Close the namesake-test debt — **60** of the 129 implementation files have no same-named `*Tests.swift` (re-derived by basename 2026-09-07; 60 of 128 at August 30, 60 of 127 at August 16, 57 of 114 at August 9, and 28 of 87 at July 26 — so for the first time the uncovered count held flat while the module grew, because both `PlannedStayLocationVerifier.swift` and the newer `CurrentRegionResolver.swift` arrived with namesake test files). Still uncovered among the originally named files: `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) +- test(WhereCore) [needs-design]: Close the namesake-test debt — **61 of 132** source files have no same-named `*Tests.swift` (basename recount 2026-09-21; previously 60 of 129). PR #314 added namesake coverage for the existing `CoreLocationSource` and the new `CurrentRegionResolverLog`; the new `CurrentLocationResult` and `CurrentRegionResolution` value types are covered through consumers rather than namesake files. `WidgetTimelineRefresher` and `BackupArchive` remain among the original gaps. `WhereCoreTests.swift` still holds five suites: `YearReportTests`, `SwiftDataStoreFactoryTests`, `SDLocationSampleTests`, `EvidenceKindTests`, and `SampleSourceTests`. Logging types and record shells make the basename count a convention proxy, not a count of untested behaviors. Split by concern as those files change. (audit 2026-07-26; re-verified 2026-09-21) - refactor(WhereCore) [quick-win]: Drop the remaining Core-API parameter defaults — `DayJournal.addEvidence(_:blob:)` (`DayJournal.swift:271`) and `WidgetDataReader`'s aggregator/attributor (`WidgetDataReader.swift:76-77`). 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. This window's new Core API held the rule — `PlannedStayLocationVerifier.status` takes every argument explicitly.) (audit 2026-07-26; re-verified 2026-08-30) -- convention(WhereIntents) [quick-win]: Small polish, all four parts still open — register `LogTripIntent` in `WhereShortcuts` (`Where/Where/Sources/WhereShortcuts.swift:11-48` registers **four** — "five" was stale since PR #230 retired the recent-activity shortcut — still 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(WhereIntents) [quick-win]: Finish the intent/viewer polish — `WhereShortcuts.swift:11-48` registers four shortcuts with no `LogTripIntent`; register trip backfill or document Shortcuts-only discovery. Decide whether `LogDayIntent.swift:39` needs an explicit Gregorian-day helper rather than `date ?? Date()` (no data impact today: `DayJournal` already buckets Gregorian). Wrap `RegionViewerApp.swift:15-18` in `.whereBroadwayRoot()` for app styling. **The fourth subtask shipped in PR #174:** `TodayRegionsIntent.swift:29-43` now injects the audience-specific widget store and logs App Group-open failures with an error attachment; `WhereIntentReader` defaults to the report fallback instead of a silent `try?` open. File-read failures inside the widget stores remain a separate item above. (audit 2026-07-26; narrowed 2026-09-21) - 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 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-30) - 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)")` in `RegionMapLegend.swift:48` and `Text("\(day.dayOfMonth)")` in `CalendarContentView.swift:489` 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) - convention(WhereUI) [quick-win]: `DeveloperDemoLaunchSheet`'s snapshot case uses the fixed `.phoneLightDark` frame on a `NavigationStack` + `Form` (`Developer/DeveloperDemoLaunchSheet.swift:130`, the `Form` at `:55`), against the root rule that snapshots containing scrolling content use full-content sizing — its sibling DEBUG form `DeveloperCrashTestingView` declares `.fullContentPhoneLightDark` for exactly this shape. A category list taller than one screen would silently pin a clipped capture. Switch the preset and re-record the two references. (audit 2026-09-06, from PR #301's new surface) -- 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:709` and `:804`. Moving it safely off midnight was skipped to avoid re-recording the widget references; two whole-suite re-records have passed since (PR #196 and PR #297), each of which would have made the change nearly free. (From the July 2026 snapshot-testing PR review; re-verified 2026-08-30) -- test(WhereUI) [needs-design]: Snapshot matrix gaps — `ManualDayView`'s range-mode add still has no snapshot case (the same gap the range-coverage item above describes). **The `LocationsView` half is closed and this item was stale in saying otherwise:** `LocationsView` declares nine cases (`LocationsView.swift:395-463`) — `Loaded`, `PlannedStay`, `ForecastsHidden`, `Empty`, `MissingDays`, `ElsewhereOnly`, `DotsHidden`, `WelcomeFirst`, `WelcomeBack` — so PR #187/#252's forecasting surfaces arrived with image coverage in both the shown and hidden states, and PR #292's endorsement redesign and PR #302's sticker redesign re-recorded them rather than adding a gap. `YearView` holds its two cases (`Loaded`, `Empty`) at `YearView.swift:124-142`. (From the July 2026 snapshot-testing PR review; corrected 2026-08-16, citations refreshed 2026-09-06) -- test(WhereUI) [quick-win]: **Four** Settings-reachable 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". **Down from five: PR #305's Settings region editor closed the `RegionsSettingsView` gap** — the rewritten screen declares `SnapshotProviding` (`Regions/RegionsSettingsView.swift:211-220`, `.fullContentScreenDefaults`) and arrived with `RegionsSettingsViewSnapshotTests` and 10 references, the second time in three windows a feature team closed part of this item by touching the screen. Re-derive the list, don't carry it: enumerate every `*View.swift` under `WhereUI/Sources` that contains a `#Preview` and no `SnapshotProviding`, then cross-check each against `SettingsView.destination(for:)` (`Settings/SettingsView.swift:259-313`). Still uncovered (re-derived 2026-09-06): `AlertsSettingsView` (`:262` — reminders, daily summary, issue alerts, the drift threshold, and a manual "find issues now"), `VisibleYearSettingsView` (`:55`), `RemovedDeviceView` (`Devices/RemovedDeviceView.swift:44`, 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), and **`EvidenceListView`** (the `.attachments` destination). +- 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:714` and `:809`. Moving it safely off midnight was skipped to avoid re-recording the widget references; two whole-suite re-records have passed since (PR #196 and PR #297), each of which would have made the change nearly free. (From the July 2026 snapshot-testing PR review; re-verified 2026-08-30) +- test(WhereUI) [needs-design]: Snapshot matrix gaps — the remaining `ManualDayView` range-mode gap is owned by the P1 range-coverage item above. `LocationsView.swift:340-391` now has seven cases (`Loaded`, `PlannedStay`, `ForecastsHidden`, `Empty`, `MissingDays`, `ElsewhereOnly`, `DotsHidden`); PR #314 moved welcome coverage into `MainTabs.swift:206-277`. `YearView.swift:124-142` retains `Loaded` and `Empty`. Keep the existing forecasting coverage while filling the range gap; do not restore the obsolete claim that Locations has no empty/forecast cases. (From the July 2026 snapshot-testing PR review; corrected 2026-09-21) +- test(WhereUI) [quick-win]: **Four** Settings-reachable 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". **Down from five: PR #305's Settings region editor closed the `RegionsSettingsView` gap** — the rewritten screen declares `SnapshotProviding` (`Regions/RegionsSettingsView.swift:211-220`, `.fullContentScreenDefaults`) and arrived with `RegionsSettingsViewSnapshotTests` and 10 references, the second time in three windows a feature team closed part of this item by touching the screen. Re-derive the list, don't carry it: enumerate every `*View.swift` under `WhereUI/Sources` that contains a `#Preview` and no `SnapshotProviding`, then cross-check each against `SettingsView.destination(for:)` (`Settings/SettingsView.swift:262-316`). Still uncovered (re-derived 2026-09-06): `AlertsSettingsView` (`:262` — reminders, daily summary, issue alerts, the drift threshold, and a manual "find issues now"), `VisibleYearSettingsView` (`:55`), `RemovedDeviceView` (`Devices/RemovedDeviceView.swift:44`, 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), and **`EvidenceListView`** (the `.attachments` destination). **The "ten further `*View.swift` files with a bare `#Preview` and no image case anywhere" list needs one correction:** `WhereFlyoverView` *is* image-pinned — `WhereFlyoverViewSnapshotTests` hosts `FlyoverView(catalog:)` directly rather than through a `SnapshotProviding` conformance — so it has coverage without the conformance, which is a different (and acceptable) shape. The other nine still hold: `AppIconLoadingView`, `AbruptChangeDetailView`, `AddEvidenceView`, `EvidenceDetailView`, `RegionPickerView`, `DeveloperToolView`, `DeveloperToolUnavailableView`, `WhereFlyoverLogView`, `WhereFlyoverPresentationView`. Several are components or DEBUG tooling rather than screens, so they are not all debt. (PR #301's `DeveloperDemoModeRow` carries a bare `#Preview` too, but it is an overlay row pinned through the `DeveloperOverlay` snapshots — deliberately not counted, same as the other overlay rows.) Add cases for the four Settings screens following `Settings/DevicesSettingsView.swift` (for Alerts, an authorized and a denied-notifications variant, toggles on and off), and decide explicitly which of the nine others are screens. (audit 2026-08-09; re-counted and corrected 2026-08-30; RegionsSettingsView closed by pr#305, re-derived 2026-09-06) - test(WhereUI) [quick-win]: Pin the joined Timeline card at accessibility Dynamic Type. `PlannedPresenceJourneyCardContent` has a dedicated stacked branch when `timeline.row.stacksDayCount` is true (`Primary/PlannedPresenceJourneyCardContent.swift:18-20`, branch at `:28-49`; the stylesheet enables it at `Shared/WhereStylesheet.swift:51-55`), but the `PlannedStay` case varies only light/dark plus a VoiceOver-annotation capture (`Primary/PresenceTimelineList.swift:195-202`). The representative `.fullContentScreenDefaults` case has AX5 coverage but no planned stay, so the new joined branch has no image that executes it. Add an iPhone full-content `.accessibility5` configuration to `PlannedStay`; keep the existing semantic accessibility capture. (audit 2026-09-06, from PR #307's new surface) @@ -104,7 +104,9 @@ re-recording: # Completed issues -- fix(WhereUI) [quick-win]: Refresh the live-region welcome when the scene becomes active. Closed 2026-09-13: `MainTabs` now owns the welcome state and keys a bounded, confidence-gated lookup to foreground activity regardless of the selected tab. The source rejects stale one-shot callbacks, the resolver enforces the 1 km and boundary confidence gates, and app-shell tests and snapshots cover cancellation, repeated resolution, actionable status, and presentation over multiple tabs. (audit 2026-09-07, PR #309) +- test(WhereIntents) [quick-win]: Document the untested per-intent `perform()` glue. Closed by `a5551b6b` (2026-09-21), carrying the existing AGENTS.md caveat into `WhereIntents/README.md`: injected reader/writer and handoff tests cover domain behavior, while framework `@Dependency` resolution, snippet wiring, and error-to-dialog mapping require Siri/Shortcuts validation. This takes the item's documentation option; no new runtime coverage is claimed. (audit 2026-07-26) + +- fix(WhereUI) [quick-win]: Refresh the live-region welcome when the scene becomes active. Closed by PR #314 (`537fadc0`, merged 2026-09-17): `MainTabs` now owns the welcome state and keys a bounded, confidence-gated lookup to foreground activity regardless of the selected tab. The source rejects stale one-shot callbacks, the resolver enforces the 1 km and boundary confidence gates, and app-shell tests and snapshots cover cancellation, repeated resolution, actionable status, and presentation over multiple tabs. (audit 2026-09-07, PR #309) - docs(WhereCore) [quick-win]: Refresh stale doc claims. Closed 2026-09-07: the module AGENTS.md names the existing reconciliation exceptions; the README states the missing daily-summary fan-out and the failed-badge zero fallback; and the ingestor comment credits typed WhereLog events. PR #172 had already corrected the RegionViewer data-source description and RegionKit decoding-coverage claim; the earlier RootView and share-extension doc corrections remain shipped. The underlying summary, picker, badge, and GeoJSON-test items stay open. (audit 2026-07-26) diff --git a/Where/WhereCore/AGENTS.md b/Where/WhereCore/AGENTS.md index 7c45f440d..2a202fcf6 100644 --- a/Where/WhereCore/AGENTS.md +++ b/Where/WhereCore/AGENTS.md @@ -185,8 +185,10 @@ internal shape. Swift Testing in [`Tests/`](Tests) (`WhereCoreTests`), hosted in `StuffTestHost`. Drive collaborators against `SwiftDataStore.inMemory()` + -`ScriptedLocationSource`. Never use the on-disk/CloudKit store or -`CoreLocationSource`. The CloudKit remote-import path uses the +`ScriptedLocationSource`. Never use the on-disk/CloudKit store or live +Core Location requests. `CoreLocationSourceTests` must replace the source’s +one-shot controls with a `CurrentLocationRequestDriving` fake before requesting +a fix; never start passive monitoring in those tests. The CloudKit remote-import path uses the `@_spi(Testing)` `inMemory(remoteChangeSource:)` + `ScriptedStoreRemoteChangeSource`. Internal types are reached via `@testable import WhereCore`. diff --git a/Where/WhereCore/README.md b/Where/WhereCore/README.md index 8b7ca2a13..5e8114e19 100644 --- a/Where/WhereCore/README.md +++ b/Where/WhereCore/README.md @@ -312,8 +312,11 @@ rotates to a Reset child generation, and discards the retry queue only after com ## Testing Swift Testing in [`Tests/`](Tests) (`WhereCoreTests`), hosted in `StuffTestHost`. -Use `SwiftDataStore.inMemory()` + `ScriptedLocationSource` — never the -on-disk/CloudKit store or `CoreLocationSource`. The CloudKit remote-import path +Use `SwiftDataStore.inMemory()` + `ScriptedLocationSource` for domain tests. +Do not open an on-disk/CloudKit store or make live Core Location requests. +`CoreLocationSourceTests` exercises the one-shot coordinator with an injected +`CurrentLocationRequestDriving` fake, without starting passive monitoring. +The CloudKit remote-import path is exercised via the `@_spi(Testing)` `inMemory(remoteChangeSource:)` + `ScriptedStoreRemoteChangeSource`. diff --git a/Where/WhereIntents/README.md b/Where/WhereIntents/README.md index 23a9474cf..d768ed232 100644 --- a/Where/WhereIntents/README.md +++ b/Where/WhereIntents/README.md @@ -19,6 +19,10 @@ lines up). They render with [`WhereUI`](../WhereUI/) snippet views. The app target (`WhereShortcuts`), not here, so App Intents metadata extraction always discovers it. +The host injects its audience-specific App Group through `IntentServices`. +`TodayRegionsIntent` uses that group for the optional widget-snapshot fast path +and logs an App Group-open failure before falling back to the store report. + ## Intents ### Query (read) @@ -85,3 +89,9 @@ The hosted `WhereIntentsTests` bundle is wired in [`Project.swift`](../../Projec Swift Testing in [`Tests/`](Tests) (`WhereIntentsTests`, hosted in `StuffTestHost`). Tests cover entity/enum ↔ `Region` mapping, and each intent's read/write logic driven against an in-memory `WhereServices` (via `PreviewSupport.previewServices()`). They verify counts, date→regions, and that action intents commit through `DayJournal`. + +Tests exercise injected readers, writers, and the `IntentServices` handoff; +they do not call an intent's `perform()`. The framework's `@Dependency` lookup +traps outside the system perform flow. Dependency resolution, snippet wiring, +and error-to-dialog mapping therefore require a Siri/Shortcuts invocation on a +device; these are not covered by the unit suite. diff --git a/Where/WhereUI/Sources/Model/YearReportModel.swift b/Where/WhereUI/Sources/Model/YearReportModel.swift index 057fe64a4..314be387c 100644 --- a/Where/WhereUI/Sources/Model/YearReportModel.swift +++ b/Where/WhereUI/Sources/Model/YearReportModel.swift @@ -185,7 +185,7 @@ public final class YearReportModel { } } - /// Whether Locations may resolve and present its live-region welcome. + /// Whether the app shell may resolve and present its live-region welcome. /// Writes persist synchronously and hide a mounted welcome immediately. var showsLocationWelcome: Bool { get { showsLocationWelcomeStorage } @@ -197,7 +197,7 @@ public final class YearReportModel { } #if DEBUG - /// Clears the acknowledgement so the next visible Locations lookup can welcome again. + /// Clears the acknowledgement so the next foreground lookup can welcome again. func resetLocationWelcome() { preferences.lastWelcomedRegion = nil }