diff --git a/Ledger/TODOs.md b/Ledger/TODOs.md index 8eda25382..2ae0e8736 100644 --- a/Ledger/TODOs.md +++ b/Ledger/TODOs.md @@ -11,8 +11,8 @@ The item format and the placement rule live in the root # Open issues ## P2s (Nice to have) -- fix(LedgerCore) [quick-win]: `LedgerServices` resolves its calendar from the device (`LedgerServices.swift:157`, `calendar: Calendar = .current` on the `@_spi(Testing)` init) and uses it for the today/this-week spend deltas (`:303-307`), so on a non-Gregorian system calendar the window boundaries `SpendHistory` differences against move — the same defect class Where forbids outright, and the parameter default also violates the repo's "avoid parameter defaults on Core APIs" rule, since the composition root already knows the value. Inject an explicit Gregorian calendar with the current time zone from the app, and pass it in tests rather than relying on the default. Lower severity than Where's equivalent: this shifts a spend window rather than corrupting stored day identity, and no value is persisted against it. (audit 2026-08-09; 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 (13 test files over 16 sources, including the API, Keychain, token-source, and history seams). Ledger has now shipped nothing for three consecutive windows, so all three counts are unchanged. (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:718-722`). 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) +- 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 (13 test files over 16 sources, including the API, Keychain, token-source, and history seams). Ledger has now shipped nothing for four consecutive windows, so all three counts are 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) # Completed issues diff --git a/MODULE_AUDIT.md b/MODULE_AUDIT.md index aafd570dd..2e042ea36 100644 --- a/MODULE_AUDIT.md +++ b/MODULE_AUDIT.md @@ -1,10 +1,10 @@ # Swift Module Audit Report -Read-only review of all **20 SPM library targets**, **7 Tuist app/extension targets**, **26 test bundles**, the repo-owned **Bumper Bowling** architecture rules, and the retained Python/Ruby tooling layer (694 source / 368 test / 47 image-snapshot Swift files across shipped targets, plus 2 unwired prototype sources). No production code was changed. +Read-only review of all **20 SPM library targets**, **7 Tuist app/extension targets**, **25 test bundles**, the repo-owned **Bumper Bowling** architecture rules, and the retained Python/Ruby tooling layer (700 source / 369 test / 49 image-snapshot Swift files across shipped targets, plus 2 unwired prototype sources). No production code was changed. -**Date:** August 30, 2026 -**Method:** Read-only verification of every open finding in all 12 `TODOs.md` files against current source, module-by-module, with each citation re-derived; file, reference-image, and suite-count refresh; new-surface review of the 11 commits since the last audit; and a pass over the tree against the repo's own written rules. Unlike every prior edition, this pass also **executed** the Linux-capable half of CI's `format` job — `./swiftformat --lint`, `./shellcheck`, `./attribution --check`, and both retained-tool test suites — plus `./snapshot-shards check`, which is how its one new finding was discovered rather than read. Two further candidate findings were raised and rejected against source; both are recorded below. Unlike every prior edition, this pass also **executed** the Linux-capable half of CI's `format` job — `./swiftformat --lint`, `./shellcheck`, `./attribution --check`, and both retained-tool test suites — plus `./snapshot-shards check`, which is how its one new finding was discovered rather than read. Two further candidate findings were raised and rejected against source; both are recorded below. -**Prior audit:** August 16, 2026 (677 source / 361 test), merged as PR #282. **This is a two-week window, not one** — the previous audit landed on `main`, so its numbers are the real baseline, but no audit ran on August 23. +**Date:** September 6, 2026 +**Method:** Read-only verification of every open finding in all 12 `TODOs.md` files against current source, module-by-module, with each citation re-derived; file, reference-image, and suite-count refresh; new-surface review of the 4 commits since the last audit; and a pass over the window's new code against the repo's own written rules. Executed on this pass: `./swiftformat --lint`, `./shellcheck`, `./attribution --check`, both retained-tool test suites, and `./snapshot-shards check` — the last of which confirmed by running it that the window's two new snapshot suites landed on the intake shard exactly as the sharding design intends. Several candidate findings from the new surface were raised and rejected against source; the load-bearing rejections are recorded below. +**Prior audit:** August 30, 2026, merged as PR #299. A one-week window of 4 commits — the smallest since this report began. > **This report carries no actionable items.** Every finding it describes is filed > in a `TODOs.md`; the root [`TODOs.md`](TODOs.md) owns the item format and says @@ -17,16 +17,15 @@ Read-only review of all **20 SPM library targets**, **7 Tuist app/extension targ ## Executive summary -**Two items closed, one filed, and one closure came from a direction the backlog did not predict.** After a window in which nothing closed at all, PR #289 resolved the Locations ranking-reorder P2 — and resolved it by building something larger than the item asked for. The item proposed wrapping the card `ForEach` in an `.animation(_:value:)`; what shipped is an explicit interpolated layout, a keyframe-driven stack, and a single reconciliation that now releases counts, order, flourish, persistence, and haptics together. PR #300 closed the StuffCore tautological-test P2 the simpler way the backlog had been pointing at for weeks — by deleting the empty module rather than growing it — which also removed the twelfth area `TODOs.md`. That is the useful signal in this pass: what moved was moved by feature work and a small refactor landing on `main`, not by anyone reading the backlog. +**The quietest window yet — and the loudest week for the audit's own error ledger.** Four PRs landed: a DEBUG next-launch demo mode (#301), a visa-sticker redesign of the Locations card estimates (#302), a Settings region editor (#305), and a snapshot-pipeline test made deterministic (#303). Nothing closed a backlog item outright, but #305 shrank the largest counted item in the backlog — the Settings screens with no image coverage — from five to four by giving the rewritten `RegionsSettingsView` a `SnapshotProviding` conformance and a ten-reference suite on arrival. That is the third window out of four in which backlog movement came from feature work touching the cited code rather than from anyone reading the list. What the pass found, in order of how much it should change your reading of the backlog: -- **The audit ran real checks for the first time, and one failed.** PR #283 made ShellCheck and the retained Python/Ruby tool suites steps in CI's `format` job — the first CI gates in this repo that need neither Xcode nor a Swift toolchain. Running them here surfaced a test that can only pass on macOS: it asserts bash's `126` exit status for an unlaunchable command, where Linux reports `127`. CI cannot report this, because CI only runs on macOS. Filed as one item with four halves — three more macOS assumptions turned up beside it, all from the same cause. -- **Two candidate findings were rejected, and rejecting them is the point.** A hand-rolled Reduce Motion read in the new Ranking Animation Lab looked like a violation of the `@MotionIsStatic` rule; it is not, because that rule is scoped to motion that never settles, and nothing in the lab plays without a tap. An `assertionFailure` in the new card-reconciliation modifier looked like swallowed error handling; it is the arm the repo's own rule prescribes, on a path `Task.sleep` cannot actually reach. Both are recorded here so the next pass does not re-file them. -- **A published count was wrong in a way the backlog had been repeating for five audits.** The `SnapshotProviding` item claimed three Settings drill-ins lacked image coverage and that they were "the only" ones. Enumerating every `*View.swift` with a `#Preview` and cross-checking against `SettingsView.destination(for:)` found **five**, and ten further views outside Settings. Corrected in place, with the method written down so it can be re-derived rather than carried. -- **The snapshot job is now parallel, and the warning against parallelizing it is still missing.** PR #276 shards snapshots across four CircleCI containers by suite. That is the safe axis. The in-container axis is the unsafe one, and the paragraph explaining why never travelled from GitHub Actions. A reader now arrives at a job that visibly *is* parallelized with nothing marking the line. The filed item was sharpened rather than merely re-dated. -- **`./test` shrank by 219 lines by moving its logic out rather than losing it.** The four Scripts PRs lifted report parsing and bundle selection into importable, directly tested Python under `Tools/`. That retires the "fragile embedded parser" premise of a standing P2 — the parser is now covered by name — leaving only the design question the item was really asking. -- **The renderer is pinned now.** PR #297 introduced `.xcode-build-version` (`27A5252f`), gated in both `./test` and the CircleCI runner check. A snapshot suite whose references depend on one toolchain finally says which one. +- **Five of the previous audit's published numbers were wrong at its own date.** Re-deriving every count from the tree (rather than diffing forward from the report) found: WhereUI held 276 sources and 102 test files on August 30, published as 274 and 101; the repo-wide totals were 695/368, published as 694/368 with a module table summing to 693; the test-bundle count was 25 — PR #300's StuffCore removal had already taken `Stuff-iOS-Tests` to 20 targets — published as "26, unchanged" with "21 in Stuff-iOS-Tests"; the settle-floor item's "37 addressable configurations, re-derived and unchanged" missed the Ranking Animation Lab's 2, so the real figure was 39 then and is 39 now; and the report's own Method line shipped with a sentence duplicated wholesale. None of these changes a priority, but four of the five had been *re-derived* by the prior pass and still came out wrong — the recount discipline is necessary but evidently not sufficient when the recount trusts the previous edition's scope. +- **A stale count five windows old surfaced only because a subagent counted rather than confirmed.** The `WhereShortcuts` polish item has said "registers five" since it was filed; the file has registered four since PR #230 retired the recent-activity shortcut in mid-August. Every intervening pass verified the item's *claim* (LogTripIntent unregistered — true) without re-deriving its *counts*. Corrected in place. +- **PR #301's demo mode is the window's model citizen, verified rather than assumed.** The next-launch latch copies the Inspector runtime's DI pattern (a dedicated `UserDefaults` suite, mutual exclusivity, one-shot consumption before the onboarding gate), the demo scope stays fully in-memory, Spotlight indexing is skipped, the new sheet arrived with snapshot coverage and localized DEBUG copy, and the module docs were updated in the same change and match the code. The pass raised and rejected five false-alarm candidates against it (recorded below) and filed two small real ones: its snapshot case captures a scrolling `Form` at a fixed device frame against the full-content rule, and the `OnboardingGate` doc comment still says the gate roots the trunk that the demo step now heads. +- **PR #302 orphaned a formatter.** The sticker redesign builds the estimate from `WhereFormat.dayCount` plus a catalog label, leaving `WhereFormat.locationCardEstimatedDays`, its `"Estimated · %@"` key, and its test with no production caller — dead-but-tested API that reads as live. Filed. +- **The sharding design's first live exercise worked.** `./snapshot-shards check` reports 49 suites with the intake shard at 3 — the two new suites (`DeveloperDemoLaunchSheetSnapshotTests`, `RegionsSettingsViewSnapshotTests`) were picked up by intake rather than silently going unrun, which is precisely the behavior the intake shard exists to provide. First time this pass could verify it by execution rather than by reading the plan. --- @@ -36,7 +35,7 @@ Pointers only — each one's evidence and suggested fix live in the linked file. | # | Module | Issue | Filed in | |---|--------|-------|----------| -| 1 | Bumper Bowling | `where.gregorian_calendar` matches only an explicit `Calendar` base, so it reports none of the 12 implicit `.current` sites — and its own mutation test only feeds it the explicit form, which is why it has now survived five audits | [`TODOs.md`](TODOs.md) P0 | +| 1 | Bumper Bowling | `where.gregorian_calendar` matches only an explicit `Calendar` base, so it reports none of the 12 implicit `.current` sites — and its own mutation test only feeds it the explicit form, which is why it has now survived six audits | [`TODOs.md`](TODOs.md) P0 | | 2 | WhereCore | `DailySummaryReconciler.reconcile()` is absent from the post-day-change fan-out — the daily notification body stays stale until a foreground re-`configure` | [`Where/TODOs.md`](Where/TODOs.md) P0 | | 3 | PeriscopeCore | Records emitted before the store attaches reach neither the store nor the journal — the durable log has a hole at every launch | [`Shared/Periscope/TODOs.md`](Shared/Periscope/TODOs.md) P0 | | 4 | WhereUI | `CalendarDay.displayDate` resolves through `Calendar.current`; four production sites remain, and every day label flows through them | [`Where/TODOs.md`](Where/TODOs.md) P1 | @@ -47,31 +46,29 @@ Pointers only — each one's evidence and suggested fix live in the linked file. | 9 | SnapshotKit | A case's content is built once and re-hosted for every configuration, while both the type's doc comment *and* its `AGENTS.md` tell authors each access is independent | [`Shared/SnapshotKit/TODOs.md`](Shared/SnapshotKit/TODOs.md) P1 | | 10 | Bumper Bowling | `duplicate_ownership` and `declared_dependency_cycle` have no mutation test, so neither has been shown to fail on a violating tree | [`TODOs.md`](TODOs.md) P1 | +The nearest dated deadline in the backlog is external: the `kve-stuff` benchmark organization's paid plan downgrades on **September 9, 2026** — three days after this audit — and its deletion item is a root P1. + --- ## Cross-cutting themes The synthesis across items that no single item shows. -### Running the checks found what reading the code could not - -Every prior edition of this report said "nothing in this report was executed". This one ran five of the `format` job's checks plus `./snapshot-shards check`, and the single new finding came out of the one that failed. It is not a subtle defect — a test asserts exit status `126` where this machine produces `127` — but no amount of reading would have produced it, because both numbers look equally plausible in source and the difference lives in bash's behaviour, not in the repo. The general shape is worth keeping: PR #283 created the repo's first CI gates that need neither Xcode nor a Swift toolchain, which means a class of check moved from "unverifiable from Linux" to "verifiable from Linux" without anyone updating the docs that said otherwise. Both the root `AGENTS.md` Linux table and the `running-tests` skill claimed Linux could run SwiftFormat and `sync-agents` and nothing else; both are corrected in this pass. The rule this suggests: when a gate stops depending on a platform, the platform-capability docs are part of the change. +### The audit's error ledger is now its own dataset, and it has a shape -### Rejecting a finding is as much of a result as filing one +Six editions in, every pass has corrected numbers the previous one published, and this pass corrected five (see the executive summary). The errors are not random: every one is an *undercount or a stale carry* in a figure whose claim ("re-derived", "unchanged") asserted freshness. The two prior enumerations of addressable settle-floor configurations both re-derived the same 37 and both missed the same case, because re-derivation used the previous edition's *list of places to look* rather than a scope query; the bundle count said "unchanged" in the same edition whose own summary described the PR that changed it. The correction that works is the one the `SnapshotProviding` item adopted two windows ago — record the derivation *procedure* in the item, not the result — and this pass extended it to the settle-floor split (the item now says why the lab case belongs in scope) and the bundle count (derived from the scheme's `testAction` list, quoted in the item's evidence). A number without its derivation is a claim; a number with one is a check. -Two things in the new surface looked wrong and were not. The Ranking Animation Lab reads `\.accessibilityReduceMotion` directly rather than through the shared `@MotionIsStatic` wrapper, which reads as a rule violation until you notice the rule is scoped to *continuous or looping* motion needing a static end-state, and that the lab's overtake is finite and plays only on a tap — so nothing is in motion during a capture and the capture flag has nothing to freeze. The new `LocationCardsReconciliationModifier` catches non-cancellation errors into `assertionFailure` with no log, which reads as swallowed failure until you notice that `Task.sleep` and `Task.checkCancellation` throw only `CancellationError`, making that arm the impossible-state case the repo's own rule assigns to `assertionFailure`. Both are cheap to file and expensive to un-file: a wrong item in a `TODOs.md` reads as established, and the next pass inherits it. Naming them here costs two paragraphs and saves that. +### Proximity to the code closes items that priority does not -### The backlog's counted claims are unreliable in a specific, correctable way +`RegionsSettingsView` had been one of the "screens with no image coverage" since the item was filed; PR #305 rewrote the screen for product reasons and the coverage arrived as a side effect of the module's own convention ("a new screen declares `SnapshotProviding` on arrival") being applied to what was effectively a new screen. The same mechanism closed the ranking-reorder P2 in the previous window and the `LocationsView` matrix gap before that. The three-of-four-windows pattern strengthens the standing suggestion: the backlog's leverage point is surfacing an area's open items *when a team is about to work in that area*, because the convention machinery then does the closing for free. The corollary also held this window: the areas nothing touched (Periscope, Broadway, Ledger, JournalKit, LifecycleKit, Inspector, Flyover) moved by exactly zero items. -The `SnapshotProviding` item is the sharpest case this repo has produced. It named three uncovered Settings screens and asserted they were the only ones; the tree has five, and the two it missed — `EvidenceListView` behind Settings > Attachments and `RegionsSettingsView` behind Settings > Regions — were reachable the whole time. The item survived five audits because each pass re-checked *the three it named* and found them still true, which is a different question from the one the item claims to answer. The correction is not a bigger number: it is that the item now records **how** the number was derived (list every `*View.swift` with a `#Preview` and no conformance, then intersect with `SettingsView.destination(for:)`), so the next pass re-runs a procedure instead of re-confirming a list. Elsewhere the same window found the reference count at 472 against 466 in two `AGENTS.md` bullets, Flyover's test files at 14 against 12, and `./test` at 722 lines against 941. Four of five audits have now published this theme; the difference here is that one item stopped carrying a count and started carrying a method. +### A quiet window localizes drift, and the drift map matches the diff map -### A feature team closes what a backlog reader does not +Every citation that moved this pass moved inside a file the window's four PRs rewrote — `WhereLaunch`/`WhereLaunchSteps` under the demo step, `LocationsView`/`WhereFormat`/`WhereFormatTests` under the sticker redesign, `SettingsView` under the region editor — plus two standing errors that predate the window (the WhereIntents doc cites that pointed past the end of both files, and the CreditKit generator's path). Items citing untouched files verified byte-for-byte, all the way down to line numbers in 1,235-line files. That is the strongest evidence yet that the weekly cadence is right-sized: a one-week window makes citation drift a mechanical, diff-guided fix rather than the bulk of the work it was in the two-week August 9 edition. -The one closure in this window came from PR #289, which set out to animate the Locations ranking and, in passing, satisfied a P2 that had sat open since July. It is the second time in three windows that the closure came from feature work landing on top of a filed item rather than from anyone working the list — the previous one was PR #187 accidentally supplying the `LocationsView` snapshot cases a matrix item had asked for. Read alongside the reverse case from two weeks ago, where a new store copied a defect filed against its sibling twenty lines away, the pattern is consistent: proximity to the code beats priority in the file. That argues for the same thing in both directions — surface the open items for the module a team is *about to work in*, because that is when they get closed and when their absence gets copied. +### DEBUG surfaces are held to production conventions, and it shows in both directions -### Shape now travels with the thing it constrains - -Three mechanisms landed this window that each pin a previously implicit assumption, and all three shipped their own enforcement rather than a note. `.xcode-build-version` pins the snapshot renderer to `27A5252f` and is checked by `./test` before it starts a simulator and again by the CircleCI runner validation. `./snapshot-shards` owns the suite-to-container assignment and *verifies after the fact* that each worker ran exactly its assignment. The retained-tool layer moved shell-embedded Python into importable modules and covered each by name. The contrast with the standing CI-docs item is stark: the one piece of shape that did **not** travel — the reasoning for keeping the snapshot suite serial inside a container — is the one that has now been missing through two migrations of the job it constrains. +The window's entire feature surface is developer tooling or DEBUG-adjacent, and the conventions held anyway: the demo sheet's copy is localized (the WhereUI standard for DEBUG UI, unlike the shared Flyover/Inspector tools' deliberate English), the launch controller is injected rather than global, the lab and sheet ship image coverage. The two real findings the window produced are also convention findings on DEBUG code — a fixed frame on a scrolling `Form`, a stale doc comment — which is the system working: the rules are cheap to apply at authoring time and expensive to retrofit, so a repo that applies them to its throwaway surfaces keeps them enforceable on its shipping ones. --- @@ -81,27 +78,19 @@ What each module was checked for and found clean, plus the trade-offs this pass ### Repository tooling — dev scripts, retained Python/Ruby, CI -The busiest area of the window by a wide margin: four Scripts PRs (#283, #284, #287, #288) plus #276's shard planner. **18 root commands** now (16 at the last audit — `shellcheck` and `snapshot-shards` are new), over a retained layer of 7 Python modules and 5 Ruby modules with 22 test files under `Tools/Tests`. - -**Verified OK, by running it:** `./swiftformat --lint` reports 0 of 1,116 files needing formatting; `./shellcheck` is silent across every tracked shell file; `./attribution --check` reports the report up to date at 12 credits — up from 11 because PR #283 pinned ShellCheck as a development tool *and* re-ran the generator in the same change, which is exactly the discipline the attribution rule exists to enforce. Every retained Python and Ruby module has a matching test file; spot-checking three found real fixture-shaped assertions rather than placeholders. +Nothing shipped. **18 root commands**, `./test` at 722 lines, 7 Python + 5 Ruby retained modules with 21 test files under `Tools/Tests`. -**New this pass:** the retained-tool gate passes only on macOS — one hardcoded shell exit status and one hermetic `PATH` that assumes a system Ruby — and running it leaves untracked `__pycache__/` directories, which `.gitignore` does not cover. Filed as one item with three halves. +**Verified OK, by running it:** `./swiftformat --lint` reports 0 of 1,125 files needing formatting; `./shellcheck` is silent; `./attribution --check` reports the report up to date at 12 credits; `./snapshot-shards check` validates the plan at 49 suites (13/15/18 planned + 3 intake). The retained-tool suites reproduce exactly the failure signature the root P1 documents — 1 of 64 Python and 12+1 of 75 Ruby cases fail on macOS assumptions, not on the commands under test — so the filed item is still an accurate description of the gate's Linux behavior, and no *new* platform assumption appeared. -**One more thing the run turned up, and it is the tidiest illustration of the cause:** `./sync-agents` is `#!/usr/bin/env ruby` and fails on a bare Linux shell with ``/usr/bin/env: 'ruby': No such file or directory``, because `.cursor/install.sh` puts mise on `PATH` but not its shims. Its sibling `./attribution` is a bash wrapper that reaches the pinned Ruby through `mise exec --` (`attribution:75-78`) and works fine. Two Ruby-implemented commands, one documented as working on Linux, and only one of them arranged to. Folded into the same filed item, which now has four halves and one root cause. - -**Accepted:** `./test` is 722 lines, down from 941, having shed its report parsing and bundle selection to `Tools/`. The affected-bundle parser still infers declaration boundaries from indent level, but it is now importable and covered by name, which retires the argument the standing P2 was built on. - -**Files:** 18 root commands · 7 Python / 5 Ruby retained modules · 22 tool test files · Open: [`TODOs.md`](TODOs.md) +**Files:** 18 root commands · 7 Python / 5 Ruby retained modules · 21 tool test files · Open: [`TODOs.md`](TODOs.md) --- ### Bumper Bowling — architecture lint -Covers **Where production sources only** (`BumperBowling.swift:15-23`). CI hard-gates it through `./test --architecture-only` (`.github/workflows/ci.yml:72-73`, reaching `config`/`test`/`lint` through `test:253-261`), and the CircleCI iOS jobs pass `--skip-architecture` so it does not run twice. - -**Verified OK:** all ten `where.*` rules appear in `.bumper/RULES.md`, and each has a mutation test — eleven test functions for ten rules, since `where.checked_concurrency_boundaries` gets one per escape hatch. `component_boundary` and `forbidden_import` are mutation-tested. +Nothing shipped. Covers **Where production sources only** (`BumperBowling.swift:15-23`); CI hard-gates it through `./test --architecture-only` (`.github/workflows/ci.yml:72-73`), and CircleCI passes `--skip-architecture` so it does not run twice. -**Not covered, by design:** everything under `Shared/`, all of `Ledger/`, test bundles, and `Where/Specifications/`. Worth knowing when reading a green lint as a whole-repo signal. +**Verified OK:** still ten `where.*` rules, each in `.bumper/RULES.md`, with eleven mutation-test functions; `component_boundary` and `forbidden_import` mutation-tested. The two graph assertions without mutation tests are unchanged and filed. **Files:** 4 rule/test sources · RULES.md ✓ · Open: [`TODOs.md`](TODOs.md) @@ -109,9 +98,11 @@ Covers **Where production sources only** (`BumperBowling.swift:15-23`). CI hard- ### WhereCore -**Verified OK:** the window's one new source, `PlannedStayLocationVerifier.swift`, arrived with its namesake test, takes every argument explicitly rather than defaulting them, and models its advisory outcome as one enum rather than parallel optionals — so for the first time in four audits the namesake-test debt held flat (60 uncovered) while the module grew. No new `try?`, empty `catch`, or `Calendar.current` in the changed surface. +Two files touched (#301): `DataIssue.swift` gained the `DataIssueCategory` raw values the demo latch persists, and `DemoDataBuilder` gained a `Configuration` whose synthetic clock advances only the demo world. -**Standing:** three fan-out and lifecycle items have now survived every pass since July 26 — the summary reconcile, `setPrimaryRegions`, and the hard-deleting untrack. All three were re-confirmed against current source rather than assumed, and this pass corrected two citations that had drifted by roughly 70 lines inside `SwiftDataStore` and one in `reset()`. +**Verified OK:** the new configuration surface models issue selection as a `Set` of a typed enum rather than parallel flags; the raw values feed only the DEBUG developer latch, not any backup or CloudKit path; `DemoDataBuilderTests` covers the full 16-combination category matrix at two clock positions. A convenience `init(now:calendar:)` forwarding to `.standard` was weighed against the no-parameter-defaults rule and accepted — it is an explicit overload on demo fixture code, not a silent default on a store API. + +**Standing:** the three fan-out and lifecycle items (summary reconcile, `setPrimaryRegions`, hard-deleting untrack) have now survived every pass since July 26, re-confirmed against current source with no drift — WhereCore's citations, down to `SwiftDataStore.swift:1844-1875`, verified exactly. **Files:** 128 source / 83 test · README ✓ · AGENTS ✓ · Open: [`Where/TODOs.md`](Where/TODOs.md) @@ -119,33 +110,31 @@ Covers **Where production sources only** (`BumperBowling.swift:15-23`). CI hard- ### WhereUI -Again the busiest module: +16 sources across the endorsement redesign (#292), the ranking-overtake animation (#289), and the planned-stay warning (#286). +The whole window landed here: +5 sources (the demo launch trio, the two sticker views), +1 test file, +2 snapshot suites, +12 references, across #301/#302/#305. -**Verified OK:** `Calendar.current` is still confined to four helper defaults and eight DEBUG fixtures, with the five `calendar.timeZone = .current` sites correctly *not* counted, and the new forecasting code holding the line (`LocationForecastProgress.swift:65` builds an explicit Gregorian calendar); no raw SF Symbol strings; production copy resolves through catalog symbols; the new accessibility work is correct rather than defective — `LocationForecastRow` composes an explicit label over ignored children, and `StampBanner`'s unlabeled `.combine` reads as one localized sentence. +**Verified OK:** no `Calendar.current` in any new code (the count holds at 4 production + 8 DEBUG-fixture sites, with the five `calendar.timeZone = .current` sites still correctly excluded); no raw SF Symbol strings; all new catalog keys are manual entries; `RegionsSettingsView`'s two catches log typed events and keep the draft open (an improvement over the flow it replaced, which dismissed on error); the new launch controller follows the Inspector DI pattern and is consumed exactly once before the onboarding gate, pinned by `WhereLaunchTests.requestedDemoActivatesBeforeOnboardingAndOpensNoRealStore`. -**Closed:** the Locations ranking-reorder P2, by PR #289. The `matchedTransitionSource` conflict the item flagged was real and is now a written rule keeping the ranking layout out of the calendar zoom namespace. +**Narrowed:** the Settings image-coverage item (five screens → four, PR #305) and the namesake-test item (`scrolledForYear` no longer exists; `LocationNamer` remains). -**Corrected here:** the `SnapshotProviding` gap is five Settings-reachable screens, not three, plus ten further views with a bare `#Preview`. +**Filed:** the demo sheet's fixed-frame `Form` snapshot, the orphaned estimate formatter, the stale `OnboardingGate` comment. -**Accepted, and documented as such:** the overtake transition has no intermediate-frame coverage. A settled snapshot cannot prove a transition, so `WhereUI/AGENTS.md` prescribes playing two overtakes in the lab by hand after any ranking-motion change. That is a deliberate manual step, not a filed gap. +**Accepted:** `WhereSession` held at exactly 636 lines for a fourth consecutive window — stable, but not being worked down. -**Files:** 274 source / 101 test / 44 image-snapshot · README ✓ · AGENTS ✓ · Open: [`Where/TODOs.md`](Where/TODOs.md) +**Files:** 281 source / 103 test / 46 image-snapshot · README ✓ · AGENTS ✓ · Open: [`Where/TODOs.md`](Where/TODOs.md) --- ### WhereCrashReporting -Nothing shipped this window. **Files:** 3 source / 2 test · README ✓ · AGENTS ✓ · Open: nothing filed +Nothing shipped. **Files:** 3 source / 2 test · README ✓ · AGENTS ✓ · Open: nothing filed --- ### PeriscopeCore, PeriscopeUI, PeriscopeTools -Nothing shipped. All 19 items still open, citations refreshed. - -**Verified OK:** the Broadway dependency still stops at PeriscopeTools; no test touches `Periscope.shared`; the hosting-smoke debt **held at 20 tests across 10 files** for a second consecutive audit, so it has stopped growing without being worked down; `PeriscopeViewerSnapshotTests` is still the only file in the module's image bundle, at 2 references. +Nothing shipped. All 20 items still open; every key citation verified, one drifted (`PeriscopeToolsSnapshotTests` wiring is `Project.swift:627-633`). -**Corrected here:** three citations in the span-record P0 and the relaunch P0. `StoredLogEvent` carries `spanID` and `spanExitMode` but not `spanRelaunchPolicy`, which lives only on `SDLogEvent` and the journal entry; the warning the relaunch item cited at `LogSpan.swift:643` is `end(for:)`'s "without a matching begin", not a relaunch-path warning; and the journal-ingest deletes are at `:61` and `:71`, not `:42-44`. +**Verified OK:** the hosting-smoke debt held at **20 tests across 10 files for a third consecutive audit** — every cited site re-confirmed individually; `PeriscopeViewerSnapshotTests` is still the only file in the module's image bundle (2 references); `Periscope.swift` and `PeriscopeStore.swift` held at 935 and 1,235 lines; `StoredLogEvent` still carries `spanID` and `spanExitMode` but not `spanRelaunchPolicy`, exactly as the span-record item's 2026-08-09 correction states. **Files:** PeriscopeCore 38/33 · PeriscopeUI 1/2 · PeriscopeTools 27/27 (+1 image source, 2 references) · README ✓ · AGENTS ✓ · Open: [`Shared/Periscope/TODOs.md`](Shared/Periscope/TODOs.md) @@ -153,11 +142,7 @@ Nothing shipped. All 19 items still open, citations refreshed. ### Flyover -Nothing shipped, after two feature PRs in the previous window. - -**Corrected here:** the standing coverage item said twelve test files; there are **fourteen**. It undercounted `FlyoverContentLoadCoordinatorTests` and `FlyoverConnectorGeometryTests`, both of which the item's own body credits by name — a count and a body disagreeing inside one bullet. - -**Accepted, with a note:** references held at 5 and the single `canvasAndList` image case is unchanged, so the module's engine-proven/surface-unpinned split is stable rather than widening for a third window. +Nothing shipped for a second consecutive window (PR #301's `WhereFlyoverWorld` change is a WhereUI integration file, not this module). Counts re-confirmed: 14 test files, 5 references, one image case. **Files:** 54 source / 14 test / 1 image source, 5 references · README ✓ · AGENTS ✓ · Open: [`Shared/Flyover/TODOs.md`](Shared/Flyover/TODOs.md) @@ -165,9 +150,7 @@ Nothing shipped, after two feature PRs in the previous window. ### Inspector -Nothing shipped; all four items open, citations hold exactly. - -**Verified OK:** the `withKnownIssue` quarantine on the dark SwiftData capture is still one of exactly **two** in the whole repo (the other guards WhereUI's Elsewhere inflection bug) — re-counted rather than assumed. The bundle still produces four references from one case. +Nothing shipped; all four items open, citations hold exactly. The `withKnownIssue` quarantine on the dark SwiftData capture is still one of exactly **two** in the repo (the other guards WhereUI's Elsewhere inflection bug, whose test moved to `WhereFormatTests.swift:98` this window). **Files:** 23 source / 14 test / 1 image source, 4 references · README ✓ · AGENTS ✓ · Open: [`Shared/Inspector/TODOs.md`](Shared/Inspector/TODOs.md) @@ -175,11 +158,11 @@ Nothing shipped; all four items open, citations hold exactly. ### SnapshotKit & SnapshotKitTesting -PR #290 stabilized raised-floor accessibility captures and added `AccessibilitySnapshotViewControllerTests` for the window-attachment timing; PR #297 re-recorded the suite against Xcode 27 beta 6 and introduced the `.xcode-build-version` pin. +PR #303 replaced the async-capture regression's wall-clock-delayed placeholder with settle-pass accounting — the test now proves the final settle loop ran by counting passes through the non-emitting `SnapshotCaptureTiming` payload instead of racing a 100 ms task against the pixel loop. That is the module's own documented philosophy ("pixel stability cannot become a readiness signal") applied to its own test, and it closes nothing in the backlog because nothing had filed it. -**Verified OK:** the framework halves stay split as documented; each image bundle lists only `SnapshotKitTesting` in `extraPackageProducts`; the reporting channels still separate `report(...)`/`emit()` from `line(...)`, so no test can fabricate a row into `--review` or `--timings`; the raised-floor double-parse invariant matches the code that now implements it. +**Verified OK:** the framework halves stay split; the reporting channels still separate `report(...)`/`emit()` from `line(...)`; the content-built-once contradiction (item 9 above) is byte-for-byte unchanged in the code, the doc comment, and the `AGENTS.md` bullet. -**Corrected here:** the reference count (472 — `AGENTS.md` carried 466 in two bullets, both now refreshed) and four drifted citations in the pipeline items, including `rejectsNonConvergingBoundedScrollMeasurement`, which moved by roughly 95 lines. The 37-configuration settle-floor split was re-derived and is unchanged; the item now also records where the 10-per-case and 2-per-case figures come from, so the next pass can re-derive rather than trust them. +**Corrected here:** the settle-floor split is **39** addressable configurations, not the 37 two prior passes published — both missed the Ranking Animation Lab's 2 (present since PR #289) — and the dated reference counts in `AGENTS.md` were refreshed to 484 as of this audit. **Files:** SnapshotKit 8/3 · SnapshotKitTesting 16/16 · README ✓ · AGENTS ✓ · Open: [`Shared/SnapshotKit/TODOs.md`](Shared/SnapshotKit/TODOs.md), [`Shared/SnapshotKitTesting/TODOs.md`](Shared/SnapshotKitTesting/TODOs.md) @@ -195,7 +178,7 @@ Nothing shipped. The single P2 stands, citations unchanged. ### Broadway (BroadwayCore, BroadwayUI, BroadwayCatalog) -Nothing shipped. All eight items still open, two citations corrected (`BStylesheets.swift:91`, and the empty catalog test bundle is listed twice in `Project.swift`). +Nothing shipped. All eight items still open; the two `Project.swift` citations drifted (target block now `:660-669`, scheme listings `:748`/`:771`) because targets above them were removed with StuffCore, not because anything Broadway changed. **Files:** BroadwayCore 17/10 · BroadwayUI 6/4 · BroadwayCatalog 2/1 · README ✓ · AGENTS ✓ · Open: [`Shared/Broadway/TODOs.md`](Shared/Broadway/TODOs.md) @@ -203,9 +186,7 @@ Nothing shipped. All eight items still open, two citations corrected (`BStyleshe ### Ledger, LedgerCore -Nothing shipped for a third consecutive window; all three P2s unchanged. - -**Verified OK:** still 13 test files over 16 sources, still the three named files without a namesake test, and `Ledger-macOS-Tests` still builds the app while running only `LedgerCoreTests`. The module remains outside Bumper's scope and outside `./test`'s reach — the second of which is filed as a root P2, whose line-count evidence this pass had to revise downward twice. +Nothing shipped for a **fourth** consecutive window (`git diff 13b136d5..HEAD -- Ledger/` is empty); all three P2s unchanged, with two citations refreshed (`LedgerServices.swift:303-308`, scheme at `Project.swift:712-717`). **Files:** LedgerCore 16/14 · Ledger 8/0 · README ✓ · AGENTS ✓ (leaf modules; the **group** folder is missing both — filed) · Open: [`Ledger/TODOs.md`](Ledger/TODOs.md) @@ -213,7 +194,7 @@ Nothing shipped for a third consecutive window; all three P2s unchanged. ### RegionKit & RegionViewer -Nothing shipped; the doc claims PR #172 corrected remain correct. +Nothing shipped; citations hold, including the honestly-stated GeoJSON coverage gap. **Files:** RegionKit 15/10 · RegionViewer 1/0 · README ✓ · AGENTS ✓ · Open: [`Where/TODOs.md`](Where/TODOs.md) @@ -221,11 +202,11 @@ Nothing shipped; the doc claims PR #172 corrected remain correct. ### WhereIntents, WhereWidgets, WhereShareExtension, Where app -Nothing shipped. +The Where app took #301's runtime-selection change: `AppDelegate` now consults `WhereDeveloperLaunchController` (which wraps the Inspector controller) instead of the Inspector controller directly, and Spotlight indexing is skipped in demo mode. -**Verified OK:** the `IntentServices` handoff still has no self-creating fallback. +**Verified OK:** runtime selection still happens exactly once at process initialization; Inspector recovery remains authoritative over a conflicting demo request (`completePendingStoreErasures` re-schedules Inspector on failure); the `IntentServices` handoff still has no self-creating fallback. -**Accepted:** all four parts of the `convention(WhereIntents)` polish item are still open, verified individually rather than as a group. +**Corrected here:** the WhereIntents polish item's doc citations pointed past the end of both rewritten files (`AGENTS.md` is 90 lines; the item cited `:95-108`), and `WhereShortcuts` has registered four shortcuts, not five, since PR #230 — a count no pass had re-derived in five audits. **Files:** WhereIntents 15/9 · WhereWidgets 7/0 · WhereShareExtension 5/0 · Where 8/4 · README ✓ · AGENTS ✓ · Open: [`Where/TODOs.md`](Where/TODOs.md) @@ -233,9 +214,9 @@ Nothing shipped. ### CreditKit, JournalKit, TestHostSupport, StuffTestHost -**CreditKit:** `./attribution --check` passes at **12** credits, now including ShellCheck as a pinned development tool. **Files:** 2/3 · Open: [`Shared/CreditKit/TODOs.md`](Shared/CreditKit/TODOs.md) +**CreditKit:** `./attribution --check` passes at **12** credits. The one open item's path citation was wrong — the generator lives at `Shared/CreditKit/Tools/generate-attribution.rb`, not root `Tools/` — corrected. **Files:** 2/3 · Open: [`Shared/CreditKit/TODOs.md`](Shared/CreditKit/TODOs.md) -**JournalKit:** nothing shipped. Both test items still open. **Files:** 2/3 · Open: [`Shared/JournalKit/TODOs.md`](Shared/JournalKit/TODOs.md) +**JournalKit:** nothing shipped. Both test items still open at their exact lines. **Files:** 2/3 · Open: [`Shared/JournalKit/TODOs.md`](Shared/JournalKit/TODOs.md) **TestHostSupport:** dependency-free UIKit helpers, no bundle by design. **Files:** 1/0 · nothing open @@ -245,12 +226,11 @@ Nothing shipped. ## Limitations -- **Mostly static, but no longer entirely.** The cloud agent runs Linux with no Swift toolchain, so no `tuist test`, no simulator, no `./test --architecture-only`, and no `./xcstrings --lint`. What *was* executed for this report: `./swiftformat --lint`, `./shellcheck`, `./attribution --check`, the retained Python tool tests, the retained Ruby tool tests, and `./snapshot-shards check`. Everything else here is read from source. -- **The two retained-tool failures this pass observed are the finding, not noise.** The Python suite failed one of 64 tests, and the Ruby suite reported 12 failures and one error across 75 runs, in both cases on macOS assumptions rather than on the commands under test. Both are filed; neither indicates a broken command. +- **Mostly static, partly executed.** The cloud agent runs Linux with no Swift toolchain, so no `tuist test`, no simulator, no `./test --architecture-only`, and no `./xcstrings --lint`. What *was* executed for this report: `./swiftformat --lint`, `./shellcheck`, `./attribution --check`, the retained Python tool tests, the retained Ruby tool tests, and `./snapshot-shards check`. Everything else here is read from source. +- **The retained-tool failures observed (1 of 64 Python, 12+1 of 75 Ruby) match the filed root P1 exactly** — macOS assumptions, not broken commands. If those counts ever change, check for a new platform assumption or a real regression before reporting either. - **The "the Gregorian rule finds nothing" conclusion rests on CI being green**, not on running the lint here. The mechanism (the rule's filter plus its one-sided mutation test) is read from source and is sufficient on its own; the green gate is corroboration. -- **No snapshot pixels were inspected.** Reference counts come from file enumeration and Git LFS pointers, never from decoded images. Whether PR #297's re-recording against Xcode 27 beta 6 preserved or re-baked any of the four open broken-snapshot defects is unanswerable here; a macOS `./test --review` would settle it. -- **Runtime-dependent items are unconfirmed by design**: the launch-time notification prompt, Flyover's log routing, multi-process journal coordination, the CloudKit import-readiness race, Spotlight indexing, whether Bitdrift receives anything on a device, and Ledger's live API and Keychain paths. Each says so in its own entry. -- **The new-surface review was a rule-by-rule read, not a proof.** The window's 17 net new Swift sources were checked against the repo's written rules one by one; the ranking-overtake transition in particular cannot be verified without running it, which is what `WhereUI/AGENTS.md` says to do by hand. +- **No snapshot pixels were inspected.** Reference counts come from file enumeration and Git LFS pointers. Whether PR #302's re-recorded `locations.Loaded_iPad.png` still bakes the inflection markup is highly likely (the broken hop is untouched) but unverifiable here; a macOS `./test --review` would settle it. +- **Runtime-dependent items are unconfirmed by design**: the launch-time notification prompt, Flyover's log routing, multi-process journal coordination, the CloudKit import-readiness race, the demo mode's end-to-end cold launch beyond what its unit tests prove, and Ledger's live API and Keychain paths. Each says so in its own entry. - **No item counts by severity.** They could not be reconciled against the backlog in earlier revisions and remain deliberately omitted rather than estimated. - `Shared/Periscope/Prototypes/JournalBenchmark` (2 sources) is wired into no target and is excluded from every count here. @@ -278,7 +258,7 @@ Nothing shipped. | PeriscopeTools | `Shared/Periscope/PeriscopeTools/` | 27 | 27 | 1 | ✓ | ✓ | | RegionKit | `Where/RegionKit/` | 15 | 10 | — | ✓ | ✓ | | WhereCore | `Where/WhereCore/` | 128 | 83 | — | ✓ | ✓ | -| WhereUI | `Where/WhereUI/` | 274 | 101 | 44 | ✓ | ✓ | +| WhereUI | `Where/WhereUI/` | 281 | 103 | 46 | ✓ | ✓ | | WhereIntents | `Where/WhereIntents/` | 15 | 9 | — | ✓ | ✓ | | WhereCrashReporting | `Where/WhereCrashReporting/` | 3 | 2 | — | ✓ | ✓ | | LedgerCore | `Ledger/LedgerCore/` | 16 | 14 | — | ✓ | ✓ | @@ -295,26 +275,23 @@ Nothing shipped. | StuffTestHost | `Shared/StuffTestHost/` | 2 | 0 | ✓ | ✓ | | BroadwayCatalog | `Shared/Broadway/BroadwayCatalog/` | 2 | 1 | ✓ | ✓ | -**Totals:** 694 source · 368 test · 47 image-snapshot Swift files across shipped targets (plus 4 Bumper rule/test sources and 2 unwired prototype sources). **472** LFS-backed reference images (461 WhereUI, 5 Flyover, 4 Inspector, 2 PeriscopeTools) across **47** snapshot suites. **26** test bundles: 22 unit (21 in `Stuff-iOS-Tests`, plus `LedgerCoreTests` in `Ledger-macOS-Tests`) and 4 image (`StuffSnapshotTests`) — every one is a member of a CI scheme. +**Totals:** 700 source · 369 test · 49 image-snapshot Swift files across shipped targets (plus 4 Bumper rule/test sources and 2 unwired prototype sources). **484** LFS-backed reference images (473 WhereUI, 5 Flyover, 4 Inspector, 2 PeriscopeTools) across **49** snapshot suites (48 suite files plus the cross-boundary flag probe). **25** test bundles: 21 unit (20 in `Stuff-iOS-Tests` — the scheme's `testAction` list — plus `LedgerCoreTests` in `Ledger-macOS-Tests`) and 4 image (`StuffSnapshotTests`) — every one is a member of a CI scheme. The August 30 edition published 26/22/21 for these; that was already wrong at its date (PR #300 had removed `StuffCoreTests`), so the bundle change belongs to the *previous* window, not this one. -**Group-folder docs:** every one of the 28 module folders above carries its `README.md` + `AGENTS.md` pair. At the *group* level, `Shared/Broadway/` and `Shared/Periscope/` carry the required pair; `Where/` has `AGENTS.md` but **no `README.md`**, and `Ledger/` has **neither** — both filed in the root [`TODOs.md`](TODOs.md). +**Group-folder docs:** every one of the 27 module folders above carries its `README.md` + `AGENTS.md` pair. At the *group* level, `Shared/Broadway/` and `Shared/Periscope/` carry the required pair; `Where/` has `AGENTS.md` but **no `README.md`**, and `Ledger/` has **neither** — both filed in the root [`TODOs.md`](TODOs.md). --- -## Changes since August 16, 2026 audit - -| Area | August 16 state | August 30 state | -|------|-----------------|-----------------| -| File count | 677 source / 361 test | **694 / 368** (WhereUI 258 → 274, WhereCore 127 → 128) | -| Backlog movement | 0 closed, 5 filed | **2 closed, 1 filed** — #289 closed the Locations ranking-reorder P2; #300 closed the StuffCore tautological-test P2 by removing the module; one new item covers four macOS-coupled edges in the retained-tool layer | -| Dev scripts | 16 | **18** — `shellcheck` (#283) and `snapshot-shards` (#276) | -| CI `format` job | 4 checking steps | **6** — ShellCheck and the retained Python/Ruby tool suites joined (#283) | -| CI iOS topology | One `test-ios` job, one serial `snapshot` job | **Build once, attach everywhere** — `build-ios-tests` hands products to `test-ios` and a `parallelism: 4` `snapshot` job sharded by suite (#276) | -| Snapshot renderer | Unpinned | **`.xcode-build-version` = `27A5252f`** (#297), gated in `test:449-452` and validated on the CircleCI runner | -| CI opt-out | None | **`NO-CI` in a PR title** skips both systems; main pushes and manual full-gate runs are unaffected (#294) | -| Retained tooling | Report policy and bundle selection embedded in `./test` (941 lines) | **7 Python + 5 Ruby modules, 22 test files** under `Tools/`; `./test` down to **722** lines (#283/#284/#287/#288) | -| Image suites | 4 bundles, 466 references | **4 bundles, 472 references, 47 suites** — WhereUI 455 → 461, re-recorded whole for Xcode 27 beta 6 (#297) | -| Attribution | 11 credits | **12** — ShellCheck, pinned and credited in the same PR that added it | -| Formal specs | 10 TLA+ specifications | **10**, unchanged | -| Test bundles | 26 | **26**, unchanged | -| Backlog | 13 `TODOs.md` | **12** — StuffCore's area file went with the module (#300); no new area needed one | +## Changes since August 30, 2026 audit + +| Area | August 30 state (as published / as corrected) | September 6 state | +|------|-----------------------------------------------|-------------------| +| File count | published 694 / 368; **really 695 / 368** (WhereUI 276/102, not 274/101) | **700 / 369** — +5 WhereUI sources (#301, #302), +1 test file | +| Backlog movement | 2 closed, 1 filed | **0 closed, 2 narrowed, 3 filed** — #305 shrank the Settings-coverage item to four screens; the `scrolledForYear` half of a test item is obsolete; the demo-sheet frame, orphaned formatter, and stale gate comment are new P2s | +| Test bundles | published "26, unchanged"; **really 25** (StuffCoreTests went with #300) | **25**, genuinely unchanged this window | +| Image suites | 4 bundles, 472 references, 47 suites | **4 bundles, 484 references, 49 suites** — `DeveloperDemoLaunchSheetSnapshotTests` (#301) and `RegionsSettingsViewSnapshotTests` (#305), both on the intake shard, verified by running `./snapshot-shards check` (13/15/18/3) | +| Settle-floor split | published "37 addressable, re-derived and unchanged"; **really 39** (Ranking Animation Lab missed twice) | **39**, item now records why the lab is in scope | +| DEBUG boot modes | Inspector only | **Inspector + one-shot demo** (#301) — `WhereDeveloperLaunchController` wraps `InspectorModeController`, mutually exclusive, consumed before the onboarding gate | +| Where launch trunk | `resolve-scope` first | **`ActivateLaunchDemoStep` first** (#301) — the onboarding gate is second; its doc comment still says "head" (filed) | +| Locations card estimates | Text line under the count | **Visa-sticker endorsement** (#302); 12 references re-recorded; the old formatter is now orphaned (filed) | +| Settings regions flow | Full onboarding picker reused | **Overview + per-region editor** (#305), with snapshot coverage on arrival | +| Backlog files | 12 `TODOs.md` | **12**, unchanged | diff --git a/Shared/Broadway/TODOs.md b/Shared/Broadway/TODOs.md index a053d2060..35ea77065 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:665-675` — 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: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) - 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:755`, and again at `:779`), so CI runs it and it asserts nothing. Replace it with a launch smoke test. (audit 2026-07-26; re-verified 2026-08-30) +- 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) - 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/CreditKit/TODOs.md b/Shared/CreditKit/TODOs.md index fa7cd744a..5c06a1f60 100644 --- a/Shared/CreditKit/TODOs.md +++ b/Shared/CreditKit/TODOs.md @@ -7,6 +7,6 @@ here. # Open issues ## P2s (Nice to have) -- fix [quick-win]: `github_slug` accepts anything after the host, so a malformed pin becomes a malformed API path instead of a clear error. It captures `.+?` (`Tools/generate-attribution.rb:96-97`) and the result is interpolated straight into `repos/#{slug}/license?ref=#{ref}` (`:88`), so a `location` of `https://github.com/foo/bar?x=y` asks for `repos/foo/bar?x=y/license?ref=…` and fails with whatever `gh` makes of that. Not a security issue: both inputs are repo-controlled (`Package.resolved`, `.agents/external-skills.json`) and `Open3.capture3` passes argv with no shell, so nothing is injectable. Constrain the capture to `[\w.-]+/[\w.-]+` so a bad pin fails as a bad pin. `Tools/Tests/generate_attribution_test.rb` covers the generator but not this path. (pr#140 review; re-verified 2026-08-30) +- fix [quick-win]: `github_slug` accepts anything after the host, so a malformed pin becomes a malformed API path instead of a clear error. It captures `.+?` (`Shared/CreditKit/Tools/generate-attribution.rb:96-97` — the previously cited root `Tools/` path was wrong; the generator lives inside this module) and the result is interpolated straight into `repos/#{slug}/license?ref=#{ref}` (`:88`), so a `location` of `https://github.com/foo/bar?x=y` asks for `repos/foo/bar?x=y/license?ref=…` and fails with whatever `gh` makes of that. Not a security issue: both inputs are repo-controlled (`Package.resolved`, `.agents/external-skills.json`) and `Open3.capture3` passes argv with no shell, so nothing is injectable. Constrain the capture to `[\w.-]+/[\w.-]+` so a bad pin fails as a bad pin. `Tools/Tests/generate_attribution_test.rb` covers the generator but not this path. (pr#140 review; re-verified 2026-09-06) # Completed issues diff --git a/Shared/Flyover/TODOs.md b/Shared/Flyover/TODOs.md index 3a6287d3c..f9fb73ed2 100644 --- a/Shared/Flyover/TODOs.md +++ b/Shared/Flyover/TODOs.md @@ -14,6 +14,6 @@ because the fix is Where's to make. # Open issues ## P2s (Nice to have) -- test [needs-design]: The engine is well covered but the interactive surfaces are not. **Fourteen** test files pin what the module computes (re-counted 2026-08-30 — the item said twelve, which undercounted `FlyoverContentLoadCoordinatorTests` and `FlyoverConnectorGeometryTests`; it was ten when filed) — the catalog, layout, model, canvas render and zoom plans, connector geometry, preview readiness, the serial content-load coordinator, and the stylesheet — while the UI it drives is verified only by the single `canvasAndList` image case, now **5** references. Untested: the focused inspector, the viewport and appearance menus, and the overview↔focus transition edge cases. **This window widened the gap rather than closing it:** PR #257 (horizontal canvas groups, two-axis full-content capture) and PR #268 (viewport-centered zoom) both landed with unit coverage of the new *plans* — `FlyoverConnectorGeometryTests`, `FlyoverCanvasZoomPlanTests` — and #257 added one reference (`canvasAndList.FlyoverCanvasFullContent_iPad.png`), so the module keeps proving its math while the surfaces it drives stay unpinned. Flyover shipped nothing in the 2026-08-30 window, so the gap is unchanged rather than widened again. Acceptable for a DEBUG-only tool, and deliberately not a hosting-smoke-test gap (the repo's convention is that an image bundle owns "does this screen render"), so the shape of the fix is more `SnapshotProviding` cases in [`SnapshotTests/`](SnapshotTests) rather than new unit tests — decide which surfaces are worth pinning before adding them wholesale. (audit 2026-08-09; re-verified 2026-08-30) +- test [needs-design]: The engine is well covered but the interactive surfaces are not. **Fourteen** test files pin what the module computes (re-counted 2026-08-30 — the item said twelve, which undercounted `FlyoverContentLoadCoordinatorTests` and `FlyoverConnectorGeometryTests`; it was ten when filed) — the catalog, layout, model, canvas render and zoom plans, connector geometry, preview readiness, the serial content-load coordinator, and the stylesheet — while the UI it drives is verified only by the single `canvasAndList` image case, now **5** references. Untested: the focused inspector, the viewport and appearance menus, and the overview↔focus transition edge cases. **This window widened the gap rather than closing it:** PR #257 (horizontal canvas groups, two-axis full-content capture) and PR #268 (viewport-centered zoom) both landed with unit coverage of the new *plans* — `FlyoverConnectorGeometryTests`, `FlyoverCanvasZoomPlanTests` — and #257 added one reference (`canvasAndList.FlyoverCanvasFullContent_iPad.png`), so the module keeps proving its math while the surfaces it drives stay unpinned. Flyover shipped nothing in the 2026-08-30 or 2026-09-06 windows, so the gap is unchanged rather than widened again (still 14 test files, 5 references). Acceptable for a DEBUG-only tool, and deliberately not a hosting-smoke-test gap (the repo's convention is that an image bundle owns "does this screen render"), so the shape of the fix is more `SnapshotProviding` cases in [`SnapshotTests/`](SnapshotTests) rather than new unit tests — decide which surfaces are worth pinning before adding them wholesale. (audit 2026-08-09; re-verified 2026-09-06) # Completed issues diff --git a/Shared/Periscope/TODOs.md b/Shared/Periscope/TODOs.md index ca0e1b26f..b60a2772a 100644 --- a/Shared/Periscope/TODOs.md +++ b/Shared/Periscope/TODOs.md @@ -34,7 +34,7 @@ here. - feat(PeriscopeCore) [quick-win]: Add non-closure emit overloads alongside the `{}` form. Today emit is only `log { .event }` / `log(attachments:) { .event }`; the closure is nice for multi-line payload builds but heavy for a bare event. Add a value form — either `log.emit(.event)` (named, no overload ambiguity) or a `log(.event)` value overload — keeping `{}` for multi-line. Additive; pairs with the derivation-naming item above. (pr#94 review) - feat(PeriscopeTools) [needs-design]: Inspect-by-object is scope-granular, not instance-granular. `.logInspectable(_:)` keys the badge/inspector to a `Log`'s *scope*, so tagging a list row (Where tags `EvidenceRow` with `WhereLog.evidence`, `LocationStatusRow` with `WhereLog.session`) surfaces the whole scope's recent events, not that one row's. Events already carry `externalID` for object correlation, but the inspector can't filter by it — a per-instance child scope (blocked on the `LogContextProviding` parent-hierarchy P0) or an `externalID`-scoped inspect entry would make true row-/object-level inspection work. (pr#94 review) - design(PeriscopeCore) [needs-design]: No eager store handle — `PeriscopeStore.make` being `async` forces an "optional store, observe until it lands" dance on consumers. Where exposes an `Optional` on `WhereModel` that stays `nil` until the bootstrap `Task` completes, and `RootView` has to watch the transition (`.onChange` of the store identity) to wire the viewer/inspector/alerter. A synchronous pending-store handle (usable immediately, resolves in the background) or an `await`-readiness accessor would remove the optional-and-observe boilerplate every app repeats. (agent) -- test(PeriscopeTools) [needs-design]: broken-snapshots — replace the hosting smoke tests with image snapshots. **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:632-638` — 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 carries the same hosting-smoke debt — see [`Shared/Inspector/TODOs.md`](../Inspector/TODOs.md).) (pr#101 review; re-verified 2026-08-30) +- 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 carries the same hosting-smoke debt — see [`Shared/Inspector/TODOs.md`](../Inspector/TODOs.md).) (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/SnapshotKit/TODOs.md b/Shared/SnapshotKit/TODOs.md index 365eb87c2..2c04da316 100644 --- a/Shared/SnapshotKit/TODOs.md +++ b/Shared/SnapshotKit/TODOs.md @@ -9,6 +9,6 @@ # Open issues ## P1s (Should do) -- fix: A case's content and captured models are instantiated once and shared across every configuration. `SnapshotCase.content` is a lazy `AnyView` accessor (`Sources/SnapshotCase.swift:71-73`), but the runner reads it **once** per case (`SnapshotKitTesting/Sources/AssertSnapshots.swift:49-51`) and re-hosts that same value — and re-runs the same `onReadyToSnapshot` closure — for each of up to 10+ configurations (`:121-122`). `@State` re-initializes per hosting, but reference-type models captured in the builder (`PreviewSupport.loadedYearReportModel()` and every provider like it) are shared: a `.task` side effect or a pre-capture hook mutation persists into all later configurations of the case — deterministic but surprising (variant N's reference bakes in variant 1's mutations). **The docs make it worse rather than merely silent, and PR #172 widened the blast radius:** `SnapshotCase.swift:68-70` states "Each access creates an independent view value for its configuration", which is true of the accessor and false of how the runner uses it, so a test author reading the type is actively told the isolation exists — and the rewrite promoted the same sentence to its own standalone bullet in [`AGENTS.md`](AGENTS.md) ("Each content access creates the independent value rendered by that configuration"), which is the file agents preserve against the code. Fix: rebuild per configuration by accessing `content` inside the configuration loop (isolating state), or correct both statements and document the one-instance-per-case contract loudly on `SnapshotCase` and the hook. (From the July 2026 snapshot-testing PR review; doc contradiction found 2026-08-09, second copy found 2026-08-16; both statements re-confirmed present and unchanged 2026-08-30) +- fix: A case's content and captured models are instantiated once and shared across every configuration. `SnapshotCase.content` is a lazy `AnyView` accessor (`Sources/SnapshotCase.swift:71-73`), but the runner reads it **once** per case (`SnapshotKitTesting/Sources/AssertSnapshots.swift:49-51`) and re-hosts that same value — and re-runs the same `onReadyToSnapshot` closure — for each of up to 10+ configurations (`:121-122`). `@State` re-initializes per hosting, but reference-type models captured in the builder (`PreviewSupport.loadedYearReportModel()` and every provider like it) are shared: a `.task` side effect or a pre-capture hook mutation persists into all later configurations of the case — deterministic but surprising (variant N's reference bakes in variant 1's mutations). **The docs make it worse rather than merely silent, and PR #172 widened the blast radius:** `SnapshotCase.swift:68-70` states "Each access creates an independent view value for its configuration", which is true of the accessor and false of how the runner uses it, so a test author reading the type is actively told the isolation exists — and the rewrite promoted the same sentence to its own standalone bullet in [`AGENTS.md`](AGENTS.md) ("Each content access creates the independent value rendered by that configuration"), which is the file agents preserve against the code. Fix: rebuild per configuration by accessing `content` inside the configuration loop (isolating state), or correct both statements and document the one-instance-per-case contract loudly on `SnapshotCase` and the hook. (From the July 2026 snapshot-testing PR review; doc contradiction found 2026-08-09, second copy found 2026-08-16; both statements re-confirmed present and unchanged 2026-09-06) # Completed issues diff --git a/Shared/SnapshotKitTesting/AGENTS.md b/Shared/SnapshotKitTesting/AGENTS.md index 2bd1d80f8..7e523467b 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 472 as of 2026-08-30. Re-measure before acting on the split below. +- **Measured 2026-07-28 with `SNAPSHOT_TIMING=1` over 260 references of the time.** The suite holds 484 as of 2026-09-06. 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%. @@ -118,7 +118,7 @@ Read the root [`AGENTS.md`](../../AGENTS.md) first. - **Quiescence cannot replace the pixel digest.** - **`SNAPSHOT_SETTLE` selects `pixel` (default), `quiescence`, or `both`.** Quiescence uses a `beforeWaiting` run-loop observer plus a recursive `needsLayout`/`needsDisplay`/`animationKeys` walk. - **`both` runs them together and reports disagreements.** -- **Run in `both` mode (2026-07-28) over 260 references of the time — 472 as of 2026-08-30.** The counts below are that run's, not current. +- **Run in `both` mode (2026-07-28) over 260 references of the time — 484 as of 2026-09-06.** The counts below are that run's, not current. - **That run had 226 settle phases, 134 with some disagreement.** - **8 cases had quiescence declare settled *earlier* than the digest.** Every one was a `Loaded_*` case whose content arrives late. - **That is the one dangerous direction.** It would capture a frame no reference recorded. diff --git a/Shared/SnapshotKitTesting/TODOs.md b/Shared/SnapshotKitTesting/TODOs.md index 0f0173352..37dcd339f 100644 --- a/Shared/SnapshotKitTesting/TODOs.md +++ b/Shared/SnapshotKitTesting/TODOs.md @@ -17,9 +17,9 @@ - Tile seams: `LargeViewCaptureTests` probes at unit-y 0.1/0.9 of 800pt and 3000pt views (`:22-47`), so a stitching error localized at the 2000pt seam (or a view exactly 2000pt tall — the single-tile/threshold edge) would pass. A probe pair straddling y = 2000 would pin it. ## P1s (Should do) -- perf [needs-design]: The settle floor is the single largest remaining cost in the suite, and only about a third of it looks addressable. **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 **472** today (re-counted 2026-08-30: 461 WhereUI, 5 Flyover, 4 Inspector, 2 PeriscopeTools — 466 at the August 16 audit and **381** at August 9, so it has grown 82% since the measurement). 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 — 37 configurations, not 22** (enumerated 2026-08-16, re-derived 2026-08-30 and unchanged, so the split below is current even though the seconds aren't): `YearView.Loaded` (`YearView.swift:128-132`) and `LocationsView.Loaded` (`LocationsView.swift:324-328`) 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:340-343`, `:352-355`) at 2 each and 1.5s; 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-08-30) +- 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 **484** today (re-counted 2026-09-06: 473 WhereUI, 5 Flyover, 4 Inspector, 2 PeriscopeTools — 472 at the August 30 audit and **381** at August 9, so it has grown 86% since the measurement). 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:340-344`) 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) ## 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 91cdd908a..05d68834c 100644 --- a/TODOs.md +++ b/TODOs.md @@ -95,7 +95,7 @@ inbox rather than here. - feat: Update the deployment target to iOS 27 — this lets us use `HistoryObserver` for CloudKit/SwiftData instead of the notification. Spans every target's minimum OS (`Package.swift`, `Project.swift`), so it sits here rather than in `Where/TODOs.md`. (human) ## P0s (Must do) -- fix(Bumper) [quick-win]: `where.gregorian_calendar` matches only an explicit `Calendar` base, so it enforces nothing. It filters `MemberAccessExprSyntax` on `base?.trimmedDescription == "Calendar"` (`.bumper/Sources/WhereProjectRules.swift: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 five 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-08-30) +- 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) ## 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**: @@ -107,14 +107,15 @@ inbox rather than here. - **`./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). All four share one root cause — Linux ships no `/usr/bin/ruby` and the repo's pinned one is reachable only through mise — and all four are small: two characters for the exit status, a wrapper copied from `attribution`, one `.gitignore` line, and a decision about how hermetic the `PATH` contract should be. 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 — **now the nearest dated deadline in this backlog, ten days out as of this audit**. 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 — **now the nearest dated deadline in this backlog, three days out as of the 2026-09-06 audit** (the organization and repository still existed on that date). 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 28 module folders carry the pair. (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 — the built-in checkout already - downloaded all 377 current objects (358.58 MiB) before + 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 484 + reference images as of 2026-09-06, 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 @@ -123,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`](test) the native-macOS tier, or stop stating that it is the only entry point. The root [`AGENTS.md`](AGENTS.md#running-tests) says "**Use `./test`** — the only way to run tests. Never hand-roll `tuist test` or `xcodebuild`", but `./test` contains no reference to Ledger or a macOS destination anywhere in its **722** lines (it went 869 → 941 as PR #271 added `--architecture-only`, then 941 → 722 as PRs #283/#284 moved its report parsing and bundle selection into directly tested Python under `Tools/` — so the script is both absorbing tiers and shedding embedded logic, and still not this tier), so `LedgerCoreTests` — a real bundle with its own CI job — simply cannot be run through it. The [`running-tests`](.agents/skills/running-tests/SKILL.md) skill already documents the exception with the raw command (`SKILL.md:127-129`, `tuist test Ledger-macOS-Tests -- -destination 'platform=macOS'`), and `LedgerCore/AGENTS.md` gives its own variant, so the truth lives in two places while the always-applied root rule contradicts both. An agent that reads only the root file — which is the one guaranteed to be loaded — concludes Ledger's tests go through `./test`, and nothing tells it otherwise until the command fails. Either add a macOS tier to `./test` (it already resolves destinations through [`simulator`](simulator) for iOS, and a macOS run needs no device at all, so this is the smaller change than it looks) or make the root rule name the carve-out explicitly. **The doc half is done** — root `AGENTS.md` now points at the skill for the macOS bundle — so what remains is deciding whether the script should absorb the tier. (audit 2026-08-09) +- feat(Scripts) [needs-design]: Teach [`test`](test) the native-macOS tier, or stop stating that it is the only entry point. The root [`AGENTS.md`](AGENTS.md#running-tests) says "**Use `./test`** — the only way to run tests. Never hand-roll `tuist test` or `xcodebuild`", but `./test` contains no reference to Ledger or a macOS destination anywhere in its **722** lines (it went 869 → 941 as PR #271 added `--architecture-only`, then 941 → 722 as PRs #283/#284 moved its report parsing and bundle selection into directly tested Python under `Tools/` — so the script is both absorbing tiers and shedding embedded logic, and still not this tier), so `LedgerCoreTests` — a real bundle with its own CI job — simply cannot be run through it. The [`running-tests`](.agents/skills/running-tests/SKILL.md) skill already documents the exception with the raw command (`SKILL.md:127-129`, `tuist test Ledger-macOS-Tests -- -destination 'platform=macOS'`), and `LedgerCore/AGENTS.md` gives its own variant, so the truth lives in two places while the always-applied root rule contradicts both. An agent that reads only the root file — which is the one guaranteed to be loaded — concludes Ledger's tests go through `./test`, and nothing tells it otherwise until the command fails. Either add a macOS tier to `./test` (it already resolves destinations through [`simulator`](simulator) for iOS, and a macOS run needs no device at all, so this is the smaller change than it looks) or make the root rule name the carve-out explicitly. **The doc half is mostly done** — root `AGENTS.md`'s Running-tests section names the carve-out and points at the skill — though its Build-system section still says "`./test` is the only way to run tests" with no exception, so the always-loaded file contradicts itself a section apart. What remains is that one sentence plus deciding whether the script should absorb the tier. (audit 2026-08-09; re-checked 2026-09-06) - 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 42f2f4736..cb6fa4446 100644 --- a/Where/TODOs.md +++ b/Where/TODOs.md @@ -17,33 +17,33 @@ The item format and the placement rule live in the root - fix(WhereCore): Nothing gets recorded on a day with no movement — presumably because background updates ride on GPS. Any way to guarantee a daily boot outside of GPS? (human) ## P0s (Must do) -- fix(WhereCore) [needs-design]: `DailySummaryReconciler.reconcile()` is absent from the post-day-change fan-out. `DayJournal.reconcileAfterDayDataChange()` (`DayJournal.swift:70-74`) fans out to issue state and widgets only, and the live-GPS hot path (`WhereServices.swift:197-215`) 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 `:226`, `:245-257`, `:265-267`), and `reset()` by calling `summary.reconcile()` directly at `:405`. Add it to the local fan-out or document the foreground-only policy. The canonical ordering in [`Specifications/PostWriteReconcile`](Specifications/PostWriteReconcile/README.md:36) deliberately excludes summary until it is routed, so the spec is not the authority for closing this. (audit 2026-07-26; PR #160 narrowed scope; re-verified 2026-08-30) - - 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-560`), 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-08-30) +- 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:196-215`) 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 `:226`, `:245-257`, `:265-267`), and `reset()` by calling `summary.reconcile()` directly at `:405`. 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]: Performance pass — how often is the app booting? Can we only do it on changes of, say, 1 km or more? (human) ## P1s (Should do) -- fix(WhereCore) [needs-design]: Scope initial CloudKit-import readiness to Where's expected store/container. `CloudKitImportReadiness.start()` observes `NSPersistentCloudKitContainer.eventChangedNotification` with `object: nil` (`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:327` — two lines before `prepareStore()` creates the intended store at `:329`. 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) +- 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) - 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. `OnboardingView.Phase` currently moves from region selection/customization directly to location permission (`WhereUI/Sources/Onboarding/OnboardingView.swift:30`), while `DayJournal.ingest(_:)` is the existing bulk sample path (`WhereCore/Sources/Journal/DayJournal.swift:82`). Design a PhotoKit-backed importer that requests access only after an explicit opt-in, reads location and capture time locally without uploading photo contents, previews what will be added, records photo-derived provenance rather than treating it as live GPS, deduplicates repeat imports, and makes skipping the screen frictionless. (human 2026-08-03) -- refactor(WhereCore) [needs-design]: Scope diagnostic emission so Flyover's unactivated sibling demo world cannot write its activity through the process-global `WhereLog` / `Periscope.shared` facade into the active real scope's durable diagnostic store. `WhereFlyoverWorld.build()` correctly gives the sibling a private `Periscope` and never starts its sink, but static `WhereLog` channels still bypass that injection; carry the scope's logging system through services/models or add a task-/environment-scoped routing context before treating Flyover's diagnostic activity as isolated. Domain data, preferences, widgets, notifications, and location remain in memory/no-op already. (`WhereUI/Sources/Developer/Flyover/WhereFlyoverWorld.swift:34-37` builds the private, sink-less `Periscope`; `WhereCore/Sources/Logging/WhereLog.swift:17-32` is the static facade that bypasses it; agent 2026-07-29, re-verified 2026-08-09) -- fix(WhereUI) [quick-win]: `CalendarDay.displayDate` resolves through `Calendar.current` (`DateRangeFormatting.swift:33`), so every day label that flows through it — relabel, logged days, resolution details, the region drill-in — renders a wrong date on a non-Gregorian device: `startOfDay(in:)` interprets the day's Gregorian Y-M-D as *that* calendar's components, so a Buddhist-era device resolves 2026-07-26 to a date ~543 years off. **Exactly four production sites remain** (re-counted 2026-08-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:21` 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:277`, `WhereModel.swift:317`, `RemindersSettingsModel.swift:153`, `PreviewSupport.swift:510`) — 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. This window's forecasting work held the line: `LocationForecastProgress.swift:65` builds a fresh `Calendar(identifier: .gregorian)` rather than reaching for the device's. The `where.gregorian_calendar` Bumper rule that should catch the four real sites is blind to the implicit-member form — filed in the root [`TODOs.md`](../TODOs.md). (audit 2026-07-26; re-verified 2026-08-30) +- 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:21` 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:277`, `WhereModel.swift:331`, `RemindersSettingsModel.swift:153`, `PreviewSupport.swift:510`) — 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 nothing in the 2026-09-06 window's new code (PRs #301/#302/#305) touches `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) - fix(WhereCore) [needs-design]: `WhereServices.setPrimaryRegions(_:)` (`WhereServices.swift:380-384`) 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]: ~~`DayJournal.ingest(_:)` (`:70`), the bulk ingest (`:82`), and `addManualSample` (`:93`) publish widgets but skip the reminder/issue reconcile~~ — **fixed:** single-sample ingest and `addManualSample` now fan out through `reconcileIssueState()` + `publishAfterIngest(of:)`; bulk ingest uses full `reconcileAfterDayDataChange()` (see [`PostWriteReconcile`](Specifications/PostWriteReconcile/README.md)). Remaining fan-out gaps: `DailySummaryReconciler` (P0 above) and `setPrimaryRegions` (above). (audit 2026-07-26; fixed 2026-08-04) - fix(WhereCore) [needs-design]: The retry queue evicts FIFO at its 1000-sample capacity and drops samples with a warning only (`LocationIngestor.swift:497-502`, 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:20-21`, empty-state branch at `:26`), 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`). Survived the PR #200 timeline rewrite, the PR #252 planned-stay additions, and this window's PR #286 planned-stay warning untouched. (audit 2026-07-26; re-verified 2026-08-30) - - refactor(WhereUI) [needs-design]: Extract a shared `ReportLoadGate`. The same `YearReportModel.loadState` gate is copy-pasted across `LocationsView.swift:85-110`, `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-08-30) -- 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:83-92`) pins the expected output behind `withKnownIssue`, so it trips as soon as this is fixed. The bug is also baked into the `locations.Loaded_iPad.png` reference (ledgered in the broken-snapshots cluster below) — re-record that image when this lands. (agent) -- refactor(WhereUI) [needs-design]: Split `WhereSession` into an always-on coordinator + a presentation view-model whose lifetime scopes its subscriptions. **Partial progress (July 2026):** `YearReportModel` is now scene-scoped in `MainTabs` — `activate()` / `deactivate()` on `scenePhase` drive `observeDataChanges()` and refresh, closing the headless-relaunch rescan leak that previously wired the subscription through launch `syncAuth`. `ResolveModel`, `BackupModel`, `RemindersSettingsModel`, and now `DevicesSettingsModel` are view-scoped. Remaining: the coordinator is **still exactly 636 lines** as of 2026-08-30 (`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 two windows of privacy, theming, forecasting, ranking-motion, and endorsement 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-08-30) +- fix(WhereUI) [quick-win]: `PresenceTimelineList` returns `[]` whenever `report.report` is nil (`PresenceTimelineList.swift:20-21`, empty-state branch at `:26`), 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`). Survived the PR #200 timeline rewrite, the PR #252 planned-stay additions, and the PR #286 planned-stay warning untouched. (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:106-131`, `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.) -- test(WhereUI) [needs-design]: `RegionMapView`'s live `Map` branch is no longer constructed by any test. The deleted `regionMapViewHosts` mounted the real MapKit `Map` (polygon building via `clLocationCoordinates`, `mapStyle`); under capture the view always takes the `SnapshotMapStandIn` branch (`RegionMapView.swift:248-254`, the live `Map` at `:255-261`), so a crash or regression in the production map path — which every real user sees — would ship untested. The stand-in substitution is what the framework carve-out sanctions; the gap is purely coverage. Fix: keep one lightweight hosting test for the live branch in `WhereUITests` (this specific surface is the exception the "no hosting smoke tests" rule shouldn't swallow) — until then, this entry records the accepted gap. (From the July 2026 snapshot-testing PR review.) -- refactor(WhereUI) [needs-design]: Extract one shared region-selection form. `DayRelabelView.swift:109-110` renders a flat `ForEach(regionSelection.items)` where `ManualDayView.swift:220-230` has `GroupedRegionSections` plus `loadGrouping()` (`:256-273`), so the two screens disagree on how regions are picked. (audit 2026-07-26; re-verified 2026-08-30) -- fix(WhereUI) [needs-design]: Notification authorization is requested during launch, with no context and unprompted. The chain, re-verified 2026-08-30: the launch's detached `reminders` / `summary` / `issue-alerts` steps (`WhereLaunchSteps.swift:174-198`) call `WhereSession.apply*Configuration()` (`WhereSession.swift:533-594`), which calls each reconciler's `configure`, and each one requests authorization whenever its feature is enabled (`ReminderReconciler.swift:81-82`, `DailySummaryReconciler.swift:47-48`, `DataIssueAlertReconciler.swift:48-49` → `UNUserNotificationCenter.requestAuthorization`). **All three preferences default to `true` on a fresh install** (`WherePreferences.swift:82`, `:105`, `:129`, 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) +- test(WhereUI) [needs-design]: `RegionMapView`'s live `Map` branch is no longer constructed by any test. The deleted `regionMapViewHosts` mounted the real MapKit `Map` (polygon building via `clLocationCoordinates`, `mapStyle`); under capture the view always takes the `SnapshotMapStandIn` branch (`RegionMapView.swift:248-253`, the live `Map` at `:255-262`), so a crash or regression in the production map path — which every real user sees — would ship untested. The stand-in substitution is what the framework carve-out sanctions; the gap is purely coverage. Fix: keep one lightweight hosting test for the live branch in `WhereUITests` (this specific surface is the exception the "no hosting smoke tests" rule shouldn't swallow) — until then, this entry records the accepted gap. (From the July 2026 snapshot-testing PR review.) +- refactor(WhereUI) [needs-design]: Extract one shared region-selection form. `DayRelabelView.swift: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:82`, `:105`, `:129`, each `?? true` and documented as "active out of the box"), which is *why* a first launch prompts — so the fix has to reckon with the defaults, not just the call site. It reaches the notification center while the app is still launching, so a fresh install shows the system "Where Would Like to Send You Notifications" alert over the splash — before the user has expressed any interest in reminders and with no in-app rationale. Observed on a fresh-install simulator screen recording: the alert lands roughly a second after the splash appears and then sits on top of the revealed app. Ask in context instead — request when the user turns reminders/summary on in Settings (or immediately after onboarding, with a sentence of explanation) — and have the launch fan only *reconcile* schedules against authorization that was already granted, never trigger the prompt. (agent) - refactor(WhereCore) [needs-design]: Rewrite the controller layer as a state machine so invariants can't exist. (human) -- test(WhereIntents) [quick-win]: The per-intent `perform()` glue — guards, snippet wiring, error→dialog mapping — is untested, because `@Dependency` traps outside the perform flow: no test in `Where/WhereIntents/Tests/` calls an intent's `perform()`. **Half closed:** `WhereIntents/AGENTS.md:95-108` now states the rule and explains the framework trap, so the "say so" option is satisfied for agents — but `README.md:106-112` still reads as though the coverage is complete and never mentions the limitation, so a human reading only the README is misled. Either extract a thin testable seam, or carry the AGENTS.md caveat into the README. (audit 2026-07-26; re-verified 2026-08-09) +- 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:68` and `AddEvidenceView.swift:37` are parallel implementations over parallel catalog namespaces (`share.form.*` / `evidence.form.*`). (audit 2026-07-26) +- 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): Add a UI that represents where you currently are — maybe a border on the current location card? (human) @@ -53,9 +53,9 @@ The item format and the placement rule live in the root - refactor(WhereCore): Durable write-back is **read-repair**, decoupled from read correctness: opportunistically (batched, on `.NSPersistentStoreRemoteChange` + launch) rewrite stale records to the current version and stamp it, so old builds can honor exclusion. Transforms must be deterministic + commutative so two devices healing the same record via CloudKit converge (LWW-safe). (agent) - design(WhereCore): Open question — the exclusion UX, where an older device progressively hides days a newer device has touched, needs a deliberate warning surface, not a silent drop. (agent) - fix(WhereUI) [needs-design]: broken-snapshots — the snapshot suite pinned genuinely broken renderings as references, flagged with `[Fix later]` review comments on PR #101 and merged anyway to land the suite. These are not flaky captures (those have their own ledger below) — each is a faithful, reproducible image of something actually wrong, so re-recording is never the fix. Fix the view, the capture frame, or the pipeline as each item says, then re-record just that reference under `Where/WhereUI/SnapshotTests/__Snapshots__/`. Most cluster on the accessibility axes `.screenDefaults` added — the ax5 Dynamic Type and VoiceOver-annotated configurations that nothing rendered before this suite existed. (pr#101 review) - - fix(WhereUI) [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:488-492`, `.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 (`:312-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-08-30) + - 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:296-315`, the red capsule at `:309`), landing it half outside the button's own glass capsule — visible in `root.LoggedIn_iPhone.png`. Use SwiftUI's `.badge()` on the toolbar item, or offset against the resolved chrome rather than a fixed spacing token. (pr#101 review; re-verified 2026-08-09) + - fix(WhereUI) [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:312-327`, the red capsule + offset at `:325-326` — 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) @@ -69,30 +69,33 @@ The item format and the placement rule live in the root - 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:34-41`, `.after(nextMidnight)` — the extension's core scheduling logic, untested in any target). (audit 2026-07-26) +- test(WhereShareExtension, WhereWidgets) [quick-win]: Close the two extension-shaped test gaps that don't need a new bundle pattern decision — `ShareEvidenceModel.buildPendingEvidence()` (`ShareEvidenceModel.swift:126-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(RegionKit) [quick-win]: Add `GeoJSONTests.swift`. The unsupported-geometry throw (`GeoJSON.swift:62-67`) and malformed-coordinate drop (`:124-128`) are untested, and `RegionCatalog.loadFromBundle()`'s degrade-to-empty-catalog behavior is asserted only at the log-event level (`RegionLogTests.swift:27-30`) rather than at runtime. `RegionKit/README.md:161-162` meanwhile claims GeoJSON decoding *is* covered — filed in the docs item below, and closing this item is what would make the README true. (audit 2026-07-26; re-verified 2026-08-09) -- test(WhereUI) [quick-win]: Add the missing namesake tests for `LocationNamer` (cache / coalescing) and `CalendarContentView`'s `scrolledForYear` scroll-reveal gate, which has hosting smoke only. (audit 2026-07-26) +- test(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 128 implementation files have no same-named `*Tests.swift` (re-derived by basename 2026-08-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 this window's one new Core source, `PlannedStayLocationVerifier.swift`, arrived with `PlannedStayLocationVerifierTests.swift`). 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) - docs(WhereCore) [quick-win]: Refresh stale doc claims. Three remain, and one is in an `AGENTS.md`, which is the worst place for a false claim because agents preserve it against the code: - - **`WhereCore/AGENTS.md:107-111`** states "Every write and import routes through `DayJournal.reconcileAfterDayDataChange()` (or its widget-less subset `reconcileIssueState()`). Never copy the fan-out into a new write path." `WhereServices.setPrimaryRegions` (`WhereServices.swift:380-384`) writes to the store without reaching `DayJournal` at all, so the rule reads as enforced while the exception filed above goes unmentioned. Either qualify the rule with the exception or route the commit. (found 2026-08-16; citation refreshed 2026-08-30) + - **`WhereCore/AGENTS.md:107-110`** states "Every write and import routes through `DayJournal.reconcileAfterDayDataChange()` (or its widget-less subset `reconcileIssueState()`). Never copy the fan-out into a new write path." `WhereServices.setPrimaryRegions` (`WhereServices.swift:380-384`) writes to the store without reaching `DayJournal` at all, so the rule reads as enforced while the exception filed above goes unmentioned. Either qualify the rule with the exception or route the commit. (found 2026-08-16; citation refreshed 2026-09-06) - **`WhereCore/README.md:65-66`** says each `DayJournal` write "commits, then awaits its reminder reconcile + widget publish so the next reader sees a fully-applied change" — the summary reconcile is not in that fan-out (P0 above), so "fully-applied" overstates it. - - **`WhereCore/README.md:286-287`** claims errors are "never swallowed into an empty default", which the reminder-badge path above contradicts. + - **`WhereCore/README.md:289-290`** claims errors are "never swallowed into an empty default", which the reminder-badge path above contradicts (the claim moved a few lines when PR #301 touched the README). - **`LocationIngestor.swift:473-474`** says persistence failures are "surfaced via `os.Logger`" when the code immediately below emits typed `WhereLog` events (`:477-481`). - **Two halves closed by PR #172's doc rewrite** (verified 2026-08-16): `RegionViewer/README.md:14-16` now describes the bundled per-region GeoJSON rather than the tooling-only `us-states.geojson` monolith, and `RegionKit/README.md:165-168` now states plainly that GeoJSON decoding is **not** covered — so that file no longer overclaims, and the RegionKit test item above is what would make the honest statement obsolete rather than what would make a false one true. (Two earlier halves closed before that: the `RootView.swift` "four screens" doc, and the `WhereShareExtension/AGENTS.md:21` compose-model credit, 2026-07-27.) (audit 2026-07-26; citations refreshed 2026-08-16) - 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-56` registers five, no trip backfill) or document Shortcuts-only discovery; use `Calendar.whereIntents` for `LogDayIntent`'s default day instead of `date ?? Date()` (`LogDayIntent.swift:39`, no data impact today since `DayJournal` buckets Gregorian); log the App Group open failure behind `WhereIntentReader.todaySnapshot`'s `try?` (`WhereIntentReader.swift:17-18`); and wrap `RegionViewer`'s `RegionMapView` in `.whereBroadwayRoot()` (`RegionViewerApp.swift:15-18`) so the dev tool renders with app styling. (audit 2026-07-26; re-verified 2026-08-09) +- convention(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(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:488` 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) +- 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) +- docs(WhereUI) [quick-win]: The `OnboardingGate` doc comment still claims the gate is "Rooted at the trunk's head" (`Launch/WhereLaunchSteps.swift:37-40`), but PR #301 prepended `ActivateLaunchDemoStep` to the trunk (`Launch/WhereLaunch.swift:164`, order pinned by `WhereLaunchTests.swift:161-163`), so the gate is now second. The comment's real claim — nothing behind the gate builds until the user chooses — still holds (the demo step only consumes the DEBUG latch); reword the "head" sentence to match the new shape. (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:706` and `:801`. 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 seven cases (`LocationsView.swift:324-375`) — `Loaded`, `PlannedStay`, `ForecastsHidden`, `Empty`, `MissingDays`, `ElsewhereOnly`, `DotsHidden` — so PR #187/#252's forecasting surfaces arrived with image coverage in both the shown and hidden states, and PR #292's endorsement redesign re-recorded them rather than adding a gap. `YearView` holds its two cases (`Loaded`, `Empty`) at `YearView.swift:128-142`. (From the July 2026 snapshot-testing PR review; corrected 2026-08-16, citations refreshed 2026-08-30) -- test(WhereUI) [quick-win]: **Five** 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". **The count was wrong in every prior edition of this item, and this pass re-derived it rather than carrying it forward.** Enumerated 2026-08-30 by listing every `*View.swift` under `WhereUI/Sources` that contains a `#Preview` and no `SnapshotProviding`, then cross-checking each against `SettingsView.destination(for:)` (`Settings/SettingsView.swift:258-312`): `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), **`EvidenceListView`** (the `.attachments` destination at `SettingsView.swift:261-262`), and **`RegionsSettingsView`** (the `.regions` destination, presented as a sheet). The last two were never listed here, so "they are the only Settings drill-ins without coverage" was false for at least five audits. - **The "convention keeps holding around them" claim was also overstated.** It is true of Settings drill-ins added recently — About, Devices, License, LifecycleFailure, ShareEvidence, Siri, Widget, CardDesignerStudio, `PrivacyDiagnosticsSettingsView` (`:185`), `DeveloperCrashTestingView` (`:22`), and this window's `PlannedStayEditor` and `RankingAnimationLabView` all declared `SnapshotProviding` on arrival — but **ten further `*View.swift` files** carry a bare `#Preview` with no conformance and no image case anywhere: `AppIconLoadingView`, `AbruptChangeDetailView`, `AddEvidenceView`, `EvidenceDetailView`, `RegionPickerView`, `DeveloperToolView`, `DeveloperToolUnavailableView`, and the three Flyover surfaces (`WhereFlyoverView`, `WhereFlyoverLogView`, `WhereFlyoverPresentationView`). Several are components or DEBUG tooling rather than screens, so they are not all debt — but the sweeping claim that everything else conforms is not what the tree says. - Add cases for the five 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 ten others are screens. (audit 2026-08-09; re-counted and corrected 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 seven cases (`LocationsView.swift:341-384`) — `Loaded`, `PlannedStay`, `ForecastsHidden`, `Empty`, `MissingDays`, `ElsewhereOnly`, `DotsHidden` — 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). + **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) ## Deferred snapshot-test flakiness Known nondeterminism in the WhereUI image suites, accepted for now — scattered