From f0958e010742669129dd28ea1bd8fd5371837a61 Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Mon, 7 Sep 2026 09:13:39 -0700 Subject: [PATCH 1/2] Reconcile the weekly backlog with current source and documented behavior Archive shipped scope and local fan-out work plus the corrected WhereCore documentation cluster. File welcome foreground and snapshot coverage follow-ups, narrow partial requests, correct stale evidence, and disclose snapshot model sharing. Validation: SwiftFormat, ShellCheck, attribution, snapshot-shard validation, agent sync, and git diff --check pass. Swift diffs are comments only; simulator, architecture, and retained-tool suites skipped because no executable behavior changed. --- AGENTS.md | 4 +- Ledger/TODOs.md | 2 +- Shared/JournalKit/TODOs.md | 2 +- Shared/Periscope/TODOs.md | 10 ++-- Shared/SnapshotKit/AGENTS.md | 4 +- Shared/SnapshotKit/README.md | 5 +- Shared/SnapshotKit/Sources/SnapshotCase.swift | 4 +- Shared/SnapshotKit/TODOs.md | 9 ++-- Shared/SnapshotKitTesting/AGENTS.md | 2 +- Shared/SnapshotKitTesting/TODOs.md | 20 +++---- TODOs.md | 12 ++--- Where/TODOs.md | 52 +++++++++---------- Where/WhereCore/AGENTS.md | 11 ++-- Where/WhereCore/README.md | 9 ++-- .../Sources/Location/LocationIngestor.swift | 2 +- 15 files changed, 75 insertions(+), 73 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ff24b24d3..4589b5092 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,8 +49,8 @@ The executables in the repo root are the dev scripts. They are `ide`, `test`, `swiftformat`, `sf-symbols`, `sync-agents`, `profile`, `icons`, `flaky`, `simulator`, `worktree`, `xcstrings`, `attribution`, `shellcheck`, `codex-watchdog`, `tla-check`, `circleci-artifacts`, `snapshot-shards`, `loc`. Each takes `--help`. Use one of these scripts instead of -hand-rolling its job. `./test` is the only way to run tests (see [Running -tests](#running-tests)). `./icons`, `./attribution`, and `./simulator` own state that is +hand-rolling its job. `./test` owns iOS tests; the native-macOS Ledger scheme is the exception +(see [Running tests](#running-tests)). `./icons`, `./attribution`, and `./simulator` own state that is easy to corrupt by hand. `./simulator` owns a per-checkout device (see the [`running-tests`](.agents/skills/running-tests/SKILL.md) skill). diff --git a/Ledger/TODOs.md b/Ledger/TODOs.md index 2ae0e8736..0c11ee464 100644 --- a/Ledger/TODOs.md +++ b/Ledger/TODOs.md @@ -12,7 +12,7 @@ The item format and the placement rule live in the root ## P2s (Nice to have) - fix(LedgerCore) [quick-win]: `LedgerServices` resolves its calendar from the device (`LedgerServices.swift:157`, `calendar: Calendar = .current` on the `@_spi(Testing)` init) and uses it for the today/this-week spend deltas (`:303-308`), so on a non-Gregorian system calendar the window boundaries `SpendHistory` differences against move — the same defect class Where forbids outright, and the parameter default also violates the repo's "avoid parameter defaults on Core APIs" rule, since the composition root already knows the value. Inject an explicit Gregorian calendar with the current time zone from the app, and pass it in tests rather than relying on the default. Lower severity than Where's equivalent: this shifts a spend window rather than corrupting stored day identity, and no value is persisted against it. (audit 2026-08-09; re-verified 2026-08-30) -- test(LedgerCore) [quick-win]: Three implementation files have no namesake test — `LedgerLog.swift`, `LedgerSettings.swift`, and `SpendSnapshot.swift`. Each is exercised indirectly through `LedgerServicesTests`, so this is 1:1-convention debt rather than untested behavior; close it as those files change rather than in one pass. The rest of the module is genuinely well covered (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(LedgerCore) [quick-win]: Three implementation files have no namesake test — `LedgerLog.swift`, `LedgerSettings.swift`, and `SpendSnapshot.swift`. Each is exercised indirectly through `LedgerServicesTests`, so this is 1:1-convention debt rather than untested behavior; close it as those files change rather than in one pass. The rest of the module is genuinely well covered (14 test/support files over 16 sources, including the API, Keychain, token-source, and history seams). The September 7 recount confirms 16 source and 14 test/support files; the three namesake gaps remain unchanged. (audit 2026-08-09; re-verified 2026-09-06) - test(Ledger) [needs-design]: The `Ledger` app target ships no test bundle, so the eight sources in the SwiftUI/AppKit shell — `MenuBarLabel`, `SpendView`, `SettingsView`, `LedgerSession`, `CurrencyFormat`, `WindowVisibilityReader` — are compile-only in CI (`Ledger-macOS-Tests` builds the app but runs only `LedgerCoreTests`, `Project.swift:712-717`). This matches how the Where extension targets are treated and is documented in [`Ledger/AGENTS.md`](Ledger/AGENTS.md), so it is a deliberate gap rather than an oversight; the decision worth making is whether `CurrencyFormat` and the menu-bar label's formatting deserve a hostless bundle of their own, since they are pure value transforms that a test could pin cheaply. (audit 2026-08-09; re-verified 2026-08-30) # Completed issues diff --git a/Shared/JournalKit/TODOs.md b/Shared/JournalKit/TODOs.md index 4529ecce1..77cc41c56 100644 --- a/Shared/JournalKit/TODOs.md +++ b/Shared/JournalKit/TODOs.md @@ -7,7 +7,7 @@ here. # Open issues ## P2s (Nice to have) -- test [quick-win]: The concurrent-append test discards append errors with `try?` (`JournalTests.swift:186`), so it would pass with fewer entries than it asserts were written. Surface the error instead. (audit 2026-07-26; re-verified 2026-08-30) +- test [quick-win]: The concurrent-append test discards append errors with `try?` (`JournalTests.swift:186`). Its recovered-count, uniqueness, and per-writer-order assertions (`:194-201`) already fail when entries go missing; the old claim that missing records would pass was wrong. Surface append failures through a throwing task group so the test reports the original I/O error rather than only downstream count mismatches. (audit 2026-07-26; corrected 2026-09-07) - test [quick-win]: `.full` sync durability is exercised by a single append (`JournalTests.swift:15`, inside `appendsRoundTripInOrder` — there is no dedicated `F_FULLFSYNC` regression); widen it to something that would actually catch a regression. (audit 2026-07-26; re-verified 2026-08-30) # Completed issues diff --git a/Shared/Periscope/TODOs.md b/Shared/Periscope/TODOs.md index b60a2772a..535ae5df5 100644 --- a/Shared/Periscope/TODOs.md +++ b/Shared/Periscope/TODOs.md @@ -12,9 +12,9 @@ here. ## P0s (Must do) - design(PeriscopeCore) [needs-design]: Span record modeling — consider `enum { case span(Span), case event(Event) }` or a dedicated span record type instead of discriminating spans by downcast. Plan/build loop. **Correction (2026-08-09, re-confirmed 2026-08-30):** as filed, this said `spanID`/`spanExit` were "bolted onto every `LogRecord`". They aren't. Both are *computed* accessors that downcast the record's `event` (`LogSpan.swift:253-269`, which also carries `spanRelaunchPolicy`), so `LogRecord` stores exactly one span-related field — `bypassesFloors` (`LogRecord.swift:56`) — and the denormalized columns live on the persistence and journal shapes on purpose, so the sweep and queries read an indexed value instead of decoding payloads (`PeriscopeSchema.swift:50`, `:57`; `StoredLogEvent.swift:58`, `:61`; `LogJournalEntry.swift:94-98`). One detail to keep straight when reading those shapes: `StoredLogEvent` carries `spanID` and `spanExitMode` but **not** `spanRelaunchPolicy`, which lives only on `SDLogEvent` and the journal entry. That reframes the item: the case discrimination already exists as optional downcasts, so the real questions are whether to make it a typed enum and whether `bypassesFloors` belongs on the record at all — not whether to unpick stored span fields. (agent) -- design(PeriscopeCore) [needs-design]: Decompose `Periscope` (the type and its flat `State` — group watchdog/inspect/ambient/live-observer state into sub-structs) and `PeriscopeStore` into children per behavioral area. Plan/build loop. (agent) -- design(PeriscopeCore) [needs-design]: `ScopeID` derivation — hash-derived vs a concatenated, human-readable path that preserves the input for debugging. Plan/build loop. (agent) -- design(PeriscopeCore) [needs-design]: `LogContextProviding` parent hierarchy — instance logs need a way to nest under a container's context (e.g. a controller inside another controller). Plan/build loop. (agent) +- design(PeriscopeCore) [needs-design]: Decompose `Periscope` (the type and its flat `State` — group watchdog/inspect/ambient/live-observer state into sub-structs) and `PeriscopeStore` into children per behavioral area (`PeriscopeCore/Sources/Pipeline/Periscope.swift:149` owns the flat state; `PeriscopeCore/Sources/Store/PeriscopeStore.swift:30` owns persistence). Preserve ordering and durability guards while splitting responsibilities. (agent) +- design(PeriscopeCore) [needs-design]: `ScopeID` derivation — compare the existing SHA-256-derived UUID (`PeriscopeCore/Sources/Loggers/ScopeID.swift:22-30`) with a readable path. Scope IDs are persisted, so any alternative must preserve existing identities or specify migration; the readability request alone does not establish a correctness bug. (agent) +- design(PeriscopeCore) [needs-design]: `LogContextProviding` parent hierarchy — instance logs need a way to nest under a container's context (e.g. a controller inside another controller). The protocol currently exposes only the system, and `instanceLog(for:)` derives a type-root/instance pair (`PeriscopeCore/Sources/Context/LogContextProviding.swift:25-31`, `:48-55`). Design a parent-context input that preserves stable instance identity and avoids cycles. (agent) - feat(PeriscopeCore) [needs-design]: Implement `SpanRelaunchPolicy.survivesRelaunch` resume mechanics. The policy is recorded on `SpanBegan` payloads *and* persisted as the `SDLogEvent.spanRelaunchPolicy` column, and the relaunch sweep honors it (surviving spans are left open, not orphan-closed), but nothing re-seeds them: `end(for:)` in the new process warns "without a matching begin" (policy: `SpanExit.swift:86-89`; the store-side half and its guard: `PeriscopeStore.swift:215-219`, `:242-245`, `PeriscopeStoreTests.swift:477-493`; the "without a matching begin" warning is `end(for:)` at `LogSpan.swift:643`, which this item previously mis-described as a relaunch-path warning). Needs an async bootstrap step at store/system startup that queries unmatched surviving `SpanBegan` events and re-opens them in `Periscope.openSpans` — plus wall-clock durations for resumed spans (`ContinuousClock` instants don't survive reboot; `SpanEnded.duration` is already optional for this) and accepting that signpost intervals can't resume. (audit 2026-07-26) - feat(PeriscopeCore) [needs-design]: Never drop the pre-store-attach window — journal from process start. `PeriscopeStore.make` is `async` (`PeriscopeStore.swift:118`), and the journal installs only when a store sink is added (`Periscope.swift:236-241`, install at `:293-297`), so events emitted between process launch and that call — early launch steps, ambient start-up snapshots — reach neither the store nor today's journal (journaling only begins once an on-disk store is attached); they survive only in the in-memory recent buffer and OSLog, and are lost from the durable record. We must never drop or lose events. Fix: write to a **short-lived journal from app start, before the store is registered, reusing the JournalKit infra**; when the store attaches, ingest that bootstrap journal (dedupe by event ID like the crash-journal ingest) and delete it. Composes with — but is stronger than — a recent-buffer replay into a late-added sink (that only covers what's still buffered, not a slow/large pre-attach burst). Related: the "No eager store handle" P2 below. (pr#94 review) @@ -33,8 +33,8 @@ here. - refactor(PeriscopeCore) [needs-design]: Reconsider the `callAsFunction` scope-derivation API. `log(SomeLog.self)` / `log(for: id)` derivation reads as an opaque function call at declaration sites; a named form (`log.scope(SomeLog.self)` / `log.subcatalog(for: id)` / `log.child(_:)`) would read clearer. Constraint: the one-expression derive-and-emit (`log(PhotoLogs.self) { … }`) exists *because* `callAsFunction` lets Swift resolve the type arg + trailing closure as one application — a named method splits it, so the emit ergonomics need a paired design (a method that also takes the trailing closure) before renaming. Affects every derivation call site + all Periscope consumers. (pr#94 review) - feat(PeriscopeCore) [quick-win]: Add non-closure emit overloads alongside the `{}` form. Today emit is only `log { .event }` / `log(attachments:) { .event }`; the closure is nice for multi-line payload builds but heavy for a bare event. Add a value form — either `log.emit(.event)` (named, no overload ambiguity) or a `log(.event)` value overload — keeping `{}` for multi-line. Additive; pairs with the derivation-naming item above. (pr#94 review) - feat(PeriscopeTools) [needs-design]: Inspect-by-object is scope-granular, not instance-granular. `.logInspectable(_:)` keys the badge/inspector to a `Log`'s *scope*, so tagging a list row (Where tags `EvidenceRow` with `WhereLog.evidence`, `LocationStatusRow` with `WhereLog.session`) surfaces the whole scope's recent events, not that one row's. Events already carry `externalID` for object correlation, but the inspector can't filter by it — a per-instance child scope (blocked on the `LogContextProviding` parent-hierarchy P0) or an `externalID`-scoped inspect entry would make true row-/object-level inspection work. (pr#94 review) -- design(PeriscopeCore) [needs-design]: No eager store handle — `PeriscopeStore.make` being `async` forces an "optional store, observe until it lands" dance on consumers. Where 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: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) +- design(PeriscopeCore) [needs-design]: No eager store handle — `PeriscopeStore.make` being `async` forces an "optional store, observe until it lands" dance on consumers. Where already models the lifecycle honestly as `WhereModel.LogStoreState` (`Where/WhereUI/Sources/Model/WhereModel.swift:32-39`: opening/ready/unavailable/failed); `logStore` is only a computed optional projection (`:114-120`). `RootView` still watches that projection's identity (`Where/WhereUI/Sources/RootView.swift:203`) to wire the viewer/inspector/alerter. The remaining design question is the async consumer handoff, not replacing a bare optional state machine. A synchronous pending-store handle (usable immediately, resolves in the background) or an `await`-readiness accessor would remove the optional-and-observe boilerplate every app repeats. (agent) +- test(PeriscopeTools) [needs-design]: broken-snapshots — replace the hosting smoke tests with image snapshots. **Twenty** tests across **ten** files assert nothing but "the hosted view reached a window" (filed as eighteen across nine; grew to twenty across ten when PR #152 added a file; **held at twenty across ten** on the 2026-08-16 and 2026-08-30 recounts, so the debt has now stopped growing for two consecutive windows without being worked down): `#expect(await waitUntil { host.view.window != nil })` in `LogEventListTests.swift:30`, `:41`, `LogHierarchyViewHostingTests.swift:23`, `:34`, `PeriscopeViewerHostingTests.swift:29`, `:42`, `ScopeEventsViewHostingTests.swift:25`, `:38`, `SpanHistoryViewHostingTests.swift:23`, `:34`, `SpanTreeViewHostingTests.swift:26`, `:37`, `LogEventDetailViewHostingTests.swift:30`, `:43`, and the `try waitFor { host.view.window != nil }` spelling in `LogInspectableHostingTests.swift:25`, `:38`, `:50`, `LogTraceViewHostingTests.swift:23`, `OpenSpansViewHostingTests.swift:27`, `:38`. The predicate restates what `show`/`showHosted` already guarantee, so each test proves only that construction didn't crash — never what rendered, which is the part the elaborate seeding sets up (`LogHierarchyView`'s outline, the comfortable density `PeriscopeViewerHostingTests` injects, the "No Events" state `ScopeEventsViewHostingTests` documents at `:29`). The repo convention is now that an image bundle, not a hosting smoke test, owns "does this screen render" (see [`Where/WhereUI/AGENTS.md`](../../Where/WhereUI/AGENTS.md#testing) and the WhereUI suite that replaced its own smoke tests). Convert them to image snapshots over the same seeded stores, keeping any assertion that isn't the window check and deleting the files left empty. **The plumbing is already in place**: [`SnapshotTests/`](PeriscopeTools/SnapshotTests) exists and `PeriscopeViewerSnapshotTests` is *still* the only file in it (re-checked 2026-08-30, 2 reference images), so none of the conversion has happened; the bundle and its `SnapshotKitTesting` link are wired at `Project.swift:627-633` — add a file per view beside it, and it compiles into the module's own `PeriscopeToolsSnapshotTests` bundle (one image bundle per module, gathered into the shared `StuffSnapshotTests` scheme — root [`AGENTS.md`](../../AGENTS.md#targets)) while recording references here. The remaining work is per-view authoring, not wiring: each view needs a deterministic fixture (a frozen store, as `PeriscopeViewerSnapshotTests` does) and ideally a `SnapshotProviding` conformance in its own source file — which needs a `SnapshotKit` dependency on PeriscopeTools, since the module has no `#Preview`s at all today. `OpenSpansView` is the one view with a genuine determinism problem: its `TimelineView(.periodic(from: .now, by: 1))` ticking ages (`OpenSpansView.swift:20`) need the `\.isCapturingSnapshot` treatment. (Note the two `window != nil` checks in `Shared/LifecycleKit/Tests/` are *not* in scope: they assert the hosting helper's own lifecycle contract, which is the one place the check is the point. Inspector has separate image-coverage gaps in [`Shared/Inspector/TODOs.md`](../Inspector/TODOs.md); that file does not claim this same hosting-smoke conversion debt.) (pr#101 review; re-verified 2026-09-06 — still 20 assertions across 10 files, third consecutive audit at that count) # Completed issues diff --git a/Shared/SnapshotKit/AGENTS.md b/Shared/SnapshotKit/AGENTS.md index 1ee374a35..ceda95f75 100644 --- a/Shared/SnapshotKit/AGENTS.md +++ b/Shared/SnapshotKit/AGENTS.md @@ -19,7 +19,9 @@ Read the root [`AGENTS.md`](../../AGENTS.md) first. - **Stuff keeps AccessibilitySnapshot's annotation renderer in the test-only `SnapshotKitTesting` product.** It does not link into shipping UI modules. - **Accessibility configs only render as snapshot tests.** Do not "fix" previews to include them without a preview-only dependency boundary. - **Keep `SnapshotCase` content builders lazy.** Constructing a provider's descriptor array must not instantiate every view or model. -- **Each content access creates the independent value rendered by that configuration.** +- **Do not assume captured models are isolated between configurations.** The current + `SnapshotKitTesting` runner evaluates content once per case and re-hosts it; + per-configuration model isolation remains in [`TODOs.md`](TODOs.md). - **Use `\.isCapturingSnapshot` for motion end-states only.** A view may read it only to freeze motion at a deterministic phase. - **Never use it to change layout, content, or behavior.** - **One carve-out (documented on the property):** content no settle window can make deterministic may substitute a placeholder of identical layout. diff --git a/Shared/SnapshotKit/README.md b/Shared/SnapshotKit/README.md index 8627bca8c..aff3e1a5a 100644 --- a/Shared/SnapshotKit/README.md +++ b/Shared/SnapshotKit/README.md @@ -52,7 +52,10 @@ capture + comparison pipeline lives in the sibling - **`SnapshotProviding`** — a type declares its variants via `static var snapshots: [SnapshotCase]`. - **`SnapshotCase`** — a named group of configurations plus a lazy content - builder. declaring a matrix does not instantiate its views or models. It is + builder. Declaring a matrix does not instantiate its views or models. The + current test runner evaluates the builder once per case and re-hosts that + value across configurations, so captured reference models are shared (see + [`TODOs.md`](TODOs.md)). It is also a `View`, so `snapshotPreviews` can render the whole matrix as a scrollable cutsheet inside a `#Preview`. Its `settle` axis (`SnapshotSettle`) declares whether the content needs the capture pipeline's diff --git a/Shared/SnapshotKit/Sources/SnapshotCase.swift b/Shared/SnapshotKit/Sources/SnapshotCase.swift index 86623ce92..038ca8e8b 100644 --- a/Shared/SnapshotKit/Sources/SnapshotCase.swift +++ b/Shared/SnapshotKit/Sources/SnapshotCase.swift @@ -66,8 +66,8 @@ public struct SnapshotCase: Identifiable { /// The content rendered under each configuration. /// /// The builder stays lazy so describing a snapshot matrix does not also - /// instantiate every view and its model. Each access creates an independent - /// view value for its configuration. + /// instantiate every view and its model. Each access invokes the builder; + /// the current test runner accesses it once per case, sharing captured models. @MainActor public var content: AnyView { contentFactory() } diff --git a/Shared/SnapshotKit/TODOs.md b/Shared/SnapshotKit/TODOs.md index 2c04da316..aa9ee56af 100644 --- a/Shared/SnapshotKit/TODOs.md +++ b/Shared/SnapshotKit/TODOs.md @@ -1,14 +1,11 @@ # SnapshotKit todos -## Usage -- Tag issues with conventional commit semantics: feat, fix, refactor, perf, test, docs - - Eg "- fix: Rebuild case content per configuration" -- Nest tasks that depend on other tasks. -- Don't delete completed tasks, move them to the "Completed issues" section at the bottom. +The item format and placement rule live in the root +[`TODOs.md`](../../TODOs.md); raw notes go in [`INBOX.md`](../../INBOX.md). # 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-09-06) +- fix [needs-design]: Isolate captured models between snapshot configurations — `SnapshotCase.content` invokes its factory (`Sources/SnapshotCase.swift:68-73`), but the runner evaluates it once per case (`../SnapshotKitTesting/Sources/AssertSnapshots.swift:49-51`) and re-hosts the same value for each configuration (`:121-122`). SwiftUI state reinitializes per host, while reference models captured by the builder and mutations from `onReadyToSnapshot` carry into later variants. Rebuild content inside the configuration loop and cover a mutating hook so one variant cannot affect another. The README, AGENTS.md, and accessor comment now describe the actual sharing; correcting the docs does not close the isolation work. (pr review, July 2026; re-verified 2026-09-07) # Completed issues diff --git a/Shared/SnapshotKitTesting/AGENTS.md b/Shared/SnapshotKitTesting/AGENTS.md index 3a42f1e0a..3a9321688 100644 --- a/Shared/SnapshotKitTesting/AGENTS.md +++ b/Shared/SnapshotKitTesting/AGENTS.md @@ -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 — 490 as of 2026-09-06.** The counts below are that run's, not current. +- **Run in `both` mode (2026-07-28) over 260 references of the time; current inventory is in [`MODULE_AUDIT.md`](../../MODULE_AUDIT.md).** 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 37dcd339f..825942b81 100644 --- a/Shared/SnapshotKitTesting/TODOs.md +++ b/Shared/SnapshotKitTesting/TODOs.md @@ -1,30 +1,26 @@ # SnapshotKitTesting todos -## Usage -- Tag issues with conventional commit semantics: feat, fix, refactor, perf, test, docs - - Eg "- fix: Surface the settle-loop timeout" -- Nest tasks that depend on other tasks. -- Don't delete completed tasks, move them to the "Completed issues" section at the bottom. +The item format and placement rule live in the root +[`TODOs.md`](../../TODOs.md); raw notes go in [`INBOX.md`](../../INBOX.md). # Open issues ## P1s (Should do) -- fix: Accessibility-parse failures kill the whole host process instead of failing one test — all four `parseAccessibility()` catch arms use `preconditionFailure` (`Sources/AccessibilitySnapshotViewController.swift:60-63`, `:65-67`, `:69-71`, `:73`; a fifth at `:15-17` is outside the parse). PR #290's raised-floor stabilization reworked this file and added `AccessibilitySnapshotViewControllerTests` for the window-attachment timing, but left every arm a `preconditionFailure`, but at least `containedViewExceedsMaximumSize` and `containedViewHasZeroSize` are reachable from ordinary test-author declarations (a large `.fullContent` frame with `snapshotType: .accessibility`, or a view that measures to zero) — user-level failures per the repo's "distinguish user failures from programmer errors" rule. Because all tests share one `StuffTestHost` process, one oversized accessibility case crashes the entire suite run on CI, with the diagnosis buried in a crash report. Fix: convert the declarable cases to a recorded `Issue` (returning a failure the caller skips), keeping `preconditionFailure` only for the genuinely impossible arms. (From the July 2026 snapshot-testing PR review.) -- test: Close the missing regression coverage for load-bearing pipeline behaviors. (From the July 2026 snapshot-testing PR review.) +- fix [quick-win]: Accessibility-parse failures kill the whole host process instead of failing one test — all four `parseAccessibility()` catch arms use `preconditionFailure` (`Sources/AccessibilitySnapshotViewController.swift:60-63`, `:65-67`, `:69-71`, `:73`; a fifth at `:15-17` is outside the parse). PR #290's raised-floor stabilization reworked this file and added `AccessibilitySnapshotViewControllerTests` for the window-attachment timing, but left every arm a `preconditionFailure`. At least `containedViewExceedsMaximumSize` and `containedViewHasZeroSize` are reachable from ordinary test-author declarations (a large `.fullContent` frame with `snapshotType: .accessibility`, or a view that measures to zero) — user-level failures per the repo's "distinguish user failures from programmer errors" rule. Each bundle gets its own `StuffTestHost` process; one oversized accessibility case crashes the remaining tests in that bundle, rather than every other bundle, with the diagnosis buried in a crash report. Fix: convert the declarable cases to a recorded `Issue` (returning a failure the caller skips), keeping `preconditionFailure` only for the genuinely impossible arms. (pr review, July 2026) +- test [needs-design]: Close the missing regression coverage for load-bearing pipeline behaviors. (pr review, July 2026) - Nonzero safe-area preset: `ConcurrentCaptureTests` now pixel-probes the swizzle's nonzero branch (20pt override through `renderSnapshotImage`, `ConcurrentCaptureTests.swift:97`), but the `iPhoneNotched` preset still has no coverage flowing through `assertSnapshots`' config mapping — no test or reference image exercises `SnapshotConfiguration.device.safeAreaInsets` end to end. - `fullContent` lazy-container iteration: **partially closed.** The 10-iteration cap now has coverage — `LargeViewCaptureTests.rejectsNonConvergingBoundedScrollMeasurement` (`LargeViewCaptureTests.swift:269-300`) exercises the non-converging path. Still uncovered: the *multi-iteration convergence* path in `resolveContentSize` (`Sources/SnapshotImageRendering.swift`) for lazy stacks that under-report until rows materialize, since the remaining cases use non-lazy content that converges on pass 1 — so a toolchain change to `LazyVStack` estimation would regress "year cut off mid-October" undetected except via the app-side `calendar.FullYear` reference. - Env parsing is untestable as written: `simulatorMatchesSnapshotExpectations`, `environmentRecordMode`, and `environmentDiffTool` (`Sources/AssertSnapshots.swift`) read `ProcessInfo` directly and are `private` — zero tests, no seam to inject an environment dictionary. The record-mode-typo path ("must not quietly assert") deserves a test. - Tile seams: `LargeViewCaptureTests` probes at unit-y 0.1/0.9 of 800pt and 3000pt views (`: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 **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. +- perf [needs-design]: The settle floor is the single largest remaining cost in the suite, and only about a third of it looks addressable. **The seconds below are stale — re-measure before acting on them; the conclusion (the floor dominates) is what to rely on.** They were taken with `SNAPSHOT_TIMING=1` over the **260** references of the time; the suite holds **495** as of 2026-09-07 (484 WhereUI, 5 Flyover, 4 Inspector, 2 PeriscopeTools; up from 490 in the prior report). Note the August 9 audit published 361 for that date and was wrong; 381 was the tree's real count, which is why the two figures in [`AGENTS.md`](AGENTS.md) disagreed. As measured then: 192 captures are floor-bound (0.25-0.35s), the `minDuration` floor accounts for ~70s of the ~84s of settle time — roughly 54% of total capture time — and every render pass in the suite combined is ~14s. So making passes cheaper is worth little and the floor is worth everything, but introspection cannot shorten a floor (see the rejected experiments in [`AGENTS.md`](AGENTS.md)): the only route is a deterministic completion signal awaited from `onReadyToSnapshot`, as `root.LoggedIn` already does with `await launcher.run()`. + **Addressable — 39 configurations, not 22** (re-derived 2026-09-06 — the two prior enumerations published 37 and both missed `RankingAnimationLabView`, whose `settledAtLeast(1.0)` over `.fullContentPhoneLightDark` (`Developer/RankingAnimation/RankingAnimationLabView.swift:55-62`) contributes 2 and predates the August 30 audit; it belongs in the split for the same reason Flyover's DEBUG canvas does): `YearView.Loaded` (`YearView.swift:128-132`) and `LocationsView.Loaded` (`Primary/LocationsView.swift:395-399`) at 10 each and 1.0s; `AboutSettingsView.Default` (`AboutSettingsView.swift:246-253`) at 10 and 0.75s, which is new since the split was written; `RootView.LoggedIn` and `RootView.RecordingConfigurationWarning` (`RootView.swift:352`, `:364`) at 2 each and 1.5s; the Ranking Animation Lab at 2 and 1.0s; and Flyover's canvas captures at 3 and 1.5s (`FlyoverSnapshotTests.swift:29`, `:52`). The 10-per-case figure is the additive `defaults()` matrix crossed with iPhone and iPad full-content presets (`SnapshotConfiguration+Combinations.swift:76-86`); the 2 is light + dark iPhone (`WhereSnapshot.swift:45-49`). The `root.LoggedIn` seam is already specced in [`Where/TODOs.md`](../../Where/TODOs.md); `YearView`, `LocationsView`, and `AboutSettingsView` need an equivalent "the report finished loading" signal on `YearReportModel`, and Flyover already has one — its `waitUntilVisiblePreviewsAreLoaded()` hook covers preview loading, so its remaining floor is only the glass adaptation. **Probably not addressable:** the default 0.25s floor on the remaining screen captures. An earlier version of this item proposed dropping it for component-level cases that host no navigation or tab-bar chrome — **that group is empty.** Every `.component*` case already declares `settle: .immediate`, so the captures still paying the default floor are all screen-level, where the floor is waiting for exactly the iOS 26 glass toolbar/tab-bar material adaptation it was added for. Shortening it there needs either a per-case seam for each of ~40 cases or a signal for chrome adaptation that UIKit does not publish. Worth re-checking if one appears. (agent; re-enumerated 2026-09-06) ## P2s (Nice to have) - perf [needs-design]: A byte-equality fast path around `assertSnapshot` was measured and **declined** — recorded so it isn't re-proposed without new numbers. 49 of 52 captures are byte-identical to their references, so the hit rate is there, but the comparison is only ~7% of a capture (mean 35ms) once the drain stall is gone, capping the win at ~6% of the suite. Paying for it means letting `snapshotReferenceURL`'s replication of swift-snapshot-testing's private layout gate the pass/fail verdict: a wrong path there currently degrades to a `referenceMissing` diff line (harmless), but on the verdict path it would skip a real comparison and read as a pass. Revisit if the comparison's share grows or the library exposes its reference URL. (agent) -- fix: Cancellation mid-settle proceeds to capture and assert — `settleContent` now reports `.cancelled` but `reportIfUnsettled` deliberately stays quiet on it (`Sources/SnapshotRenderingSupport.swift:66-69`), so a cancelled test (e.g. a future time-limit trait) still captures half-settled content and records a spurious image mismatch on top of the cancellation. Propagate the outcome out of `renderSnapshotImage` so `assertSnapshots` can skip the comparison entirely, keeping cancelled tests clean. (From the July 2026 snapshot-testing PR review.) -- fix: The duplicate-identifier guard only protects the provider overload of `assertSnapshots` (`Sources/AssertSnapshots.swift:36-47`) — the inline `assertSnapshots(of:named:configurations:)` overload (`:71-85`) accepts a `configurations` array containing duplicates and silently compares the second against the first's recording. Run the same guard over `[SnapshotCase(name:configurations:)]` there. (From the July 2026 snapshot-testing PR review.) +- fix [quick-win]: Cancellation mid-settle proceeds to capture and assert — `settleContent` now reports `.cancelled` but `reportIfUnsettled` deliberately stays quiet on it (`Sources/SnapshotRenderingSupport.swift:66-69`), so a cancelled test (e.g. a future time-limit trait) still captures half-settled content and records a spurious image mismatch on top of the cancellation. Propagate the outcome out of `renderSnapshotImage` so `assertSnapshots` can skip the comparison entirely, keeping cancelled tests clean. (pr review, July 2026) +- fix [quick-win]: The duplicate-identifier guard only protects the provider overload of `assertSnapshots` (`Sources/AssertSnapshots.swift:36-47`) — the inline `assertSnapshots(of:named:configurations:)` overload (`:71-85`) accepts a `configurations` array containing duplicates and silently compares the second against the first's recording. Run the same guard over `[SnapshotCase(name:configurations:)]` there. (pr review, July 2026) # Completed issues - fix: The reporting tests wrote **fabricated rows into the reports** — `./test --review` listed a reference that does not exist, at the top of the list, and `--timings` counted captures that never happened. `./test` recovers both channels by grepping `SNAPSHOT_DIFF` / `SNAPSHOT_TIMING` out of the run logs (and counts timing lines as images for the progress line), while `SnapshotDiffReporting.report(...)` and `SnapshotCaptureTiming.emit()` each encoded *and* printed in one function — so the tests pinning those wire formats emitted real lines. The diff fixture sorted first, because its numbers were borrowed from the genuine `swiftDataInspector` regression (max delta 203, 7430 pixels, 0.235%), which made the one row most demanding investigation the one that wasn't real; `SnapshotCaptureTimingTests` contributed five invented captures, so `./test --only SnapshotKitTestingTests --timings` reported "5 captures, 0.1s total, 0.024s per image" for a run that captured nothing, and `--everything --timings` blended those into the aggregate the suite's perf decisions are read off. The `SNAPSHOT_DIFF` env gate never helped: it is checked by the *pipeline*, not inside `report`. (Resolved: each channel is split so printing is the pipeline's alone and the payload is separately askable — `SnapshotDiffReporting.line(describing:…)`, `SnapshotCaptureTiming.line()`, and `SnapshotSettleReporting.line(…)` return the JSON without emitting, and all four test sites call those. `SnapshotSettleReporting` got the same treatment for symmetry, though nothing aggregates that channel yet. Verified both ways: a unit-only run now reports no timing lines and no differing captures, while a real snapshot run still produces the full phase breakdown and diff table. The rule is now an invariant in [`AGENTS.md`](AGENTS.md).) diff --git a/TODOs.md b/TODOs.md index 9c40e2ab7..2ba3cd6ce 100644 --- a/TODOs.md +++ b/TODOs.md @@ -106,16 +106,16 @@ inbox rather than here. - **A system Ruby at `/usr/bin`.** `PublicCommandContractTest` runs each of the 21 public commands under `PATH=/usr/bin:/bin` with `unsetenv_others: true` (`Tools/Tests/public_command_contract_test.rb:93-107`) to prove `--help` is dependency-free. That is a good test of a real contract, but it resolves the Ruby-implemented commands only where the OS ships `/usr/bin/ruby`, so on Linux twelve of its assertions fail with `/usr/bin/env: 'ruby': No such file or directory` rather than with anything about the commands. Either add mise's Ruby to the hermetic `PATH`, or state the macOS requirement on the test so a Linux failure reads as "wrong host" instead of "broken command". - **`./sync-agents` can't find Ruby on a bare Linux shell, and its sibling can.** It is `#!/usr/bin/env ruby`, so it dies with ``/usr/bin/env: 'ruby': No such file or directory`` unless mise's shims are already on `PATH` — and [`.cursor/install.sh`](.cursor/install.sh) puts only `~/.local/bin` there (`:18`), so a cloud agent following the committed setup has mise but not `ruby`. `./attribution` solves exactly this: it is a bash wrapper that resolves the pinned Ruby through `mise exec --` when mise is available (`attribution:75-78`). Give `sync-agents` the same treatment and the "Agent file sync" row in root [`AGENTS.md`](AGENTS.md#what-works-on-linux) becomes true as written instead of true-with-a-prefix. - **Running the suite dirties the tree.** `.gitignore` has no `__pycache__/` or `*.pyc` entry, so `python3 -m unittest discover -s Tools/Tests` leaves untracked `Tools/__pycache__/` and `Tools/Tests/__pycache__/` behind on any platform. That is new only in the sense that PR #283 turned an occasional command into one people will now run before pushing; it also puts a stray directory in front of anyone reading `git status` to check their own change. One `.gitignore` line, alongside the generated-artifact rules in root [`AGENTS.md`](AGENTS.md#repo-hygiene). - 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, 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) + These are separate portability and hygiene gaps: shell exit-status differences, Ruby discovery in the two command paths, and ignored Python bytecode. The missing system Ruby explains only the Ruby failures; it does not explain the shell status or untracked caches. Fixing them makes `python3 -m unittest discover -s Tools/Tests` and `./sync-agents` checks a cloud agent or a Linux contributor can run without knowing the workaround. Until they land, root [`AGENTS.md`](AGENTS.md#what-works-on-linux) should keep saying which half runs where. (audit 2026-08-30) +- docs [quick-win]: Delete the temporary `kve-stuff` CI benchmark organization after its paid plan ends — the [`Stuff-CI-Benchmark`](https://github.com/kve-stuff/Stuff-CI-Benchmark) repository exists only to retain the runner experiment, and GitHub Team is scheduled to downgrade to Free on September 9, 2026 — **two days after the 2026-09-07 audit** (the repository still exists and is not archived; the saved downgrade date has not been independently re-confirmed against billing). After the downgrade, preserve [the final benchmark report](https://github.com/kve-stuff/Stuff-CI-Benchmark/pull/3) in this repo if it is still useful, verify that the organization has no billable usage or installed integrations, then delete the organization. (human 2026-08-09) - 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) +- docs [quick-win]: Two feature-group folders are missing the doc pair the root [`AGENTS.md`](AGENTS.md#per-module-docs) requires of a module group spanning several targets. `Where/` has an `AGENTS.md` but **no `README.md`** — so the app with nine module folders and by far the most surface has no human-facing entry point at its root, while `Shared/Broadway/` and `Shared/Periscope/` both carry the pair. `Ledger/` has **neither**, though it groups the app target and `LedgerCore` (each of which has its own complete pair). Write the group-level `README.md` for `Where/` and both files for `Ledger/`, covering only what the group shares — the module graph and the invariants no single module owns — per the group rule, and without restating what the leaf docs already say. PR #172's repo-wide doc rewrite did not add either, and neither did this window, which touched fifteen Markdown files including four leaf `README.md`/`AGENTS.md` pairs — so the group level has now been skipped by every doc pass since the rule was written. The convention holds firmly at the leaves: all 27 module folders carry the pair (recounted 2026-09-07). (audit 2026-08-09; re-verified 2026-08-30) ## P2s (Nice to have) - perf(CI) [needs-design]: Re-evaluate caching Git LFS snapshot objects without fighting CircleCI's checkout hydration — at the time of that measurement the built-in checkout - downloaded all 377 then-current objects (358.58 MiB; the tree holds 490 - reference images as of 2026-09-06, so re-measure before retrying) before + downloaded all 377 then-current objects (358.58 MiB; the tree holds 495 + reference images as of 2026-09-07, so re-measure before retrying) before [`.circleci/config.yml:20-32`](.circleci/config.yml), while PR #245's first cache attempt encountered a cold miss and then made `git lfs prune --no-verify-remote` fail because the blobless clone lacked historical objects @@ -124,7 +124,7 @@ inbox rather than here. incomplete history, and benchmark Circle cache restore against the native checkout; land it only if both cold and warm snapshot jobs stay correct and get faster. (pr#245 review) -- feat(Scripts) [needs-design]: Teach [`test`](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) +- feat(Scripts) [needs-design]: Teach `./test` the native-macOS tier — the 722-line script selects iOS bundles and simulator destinations only (`test:337-338`, `:486-499`), while `LedgerCoreTests` is wired into `Ledger-macOS-Tests` (`Project.swift:712-717`). The documentation half is complete: root `AGENTS.md` and the `running-tests` skill now explicitly name the native-macOS exception. What remains is deciding whether to add a macOS tier so one command can run all repository tests. Preserve the hostless destination and separate CI scheme if it does. (audit 2026-08-09; narrowed 2026-09-07) - 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 2fa89b821..cf55342ab 100644 --- a/Where/TODOs.md +++ b/Where/TODOs.md @@ -10,43 +10,42 @@ The item format and the placement rule live in the root # Open issues ## PX (Exploratory) -- design(WhereCore): File system containerization for more structured storage. (human) +- design(WhereCore): Explore one explicit filesystem layout for store and sidecars — `WhereCore/Sources/Location/LocationOutbox.swift:115-130` places the outbox under Application Support, while the installation context is owned by `WhereUI/Sources/Launch/InstallationRecordingContextStore.swift`. Define the intended grouping and backup/reset boundaries before moving data. This is a storage-layout direction, not a verified data-loss bug. (human) - design(WhereCore): Re-shape the write path as an explicit pipeline. Every write into the DB changes various "views" and outputs, but nothing changes if no write happened. What if it were: 1) write into the DB, 2) kick off async jobs to re-evaluate DB contents, 3) write views out into a table, 4) consumers consume those view changes. This feels like it might give a cleaner pipeline-shaped layout, and importantly short-circuit a lot of work. (human) -- design(WhereCore): Make `WhereServices`' sub-services optional based on the current state of the application — worth trying to see what happens. Or better yet, decompose it all into an enum representing "logged in" vs "logged out". (human) -- design(WhereCore): For logging out / resetting, why do we need to delete all the DB entries? Could we just write the DB into a folder, and on reset move to another one? (human) -- 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) +- design(WhereCore): Explore retaining a prior store in another folder on reset — current `WhereServices.reset()` (`WhereCore/Sources/WhereServices.swift:398-412`) revokes recording and delegates a generation-scoped erase; old synced rows may remain as history (`WhereCore/README.md`, destructive-operations contract). It is no longer accurate to assume every reset physically deletes every row. A folder rotation remains unimplemented; define how retained local worlds interact with CloudKit generations, installation retirement, and recovery before replacing the current reset. (human; narrowed 2026-09-07) +- fix(WhereCore): Explore coverage for a stationary day when the app is never foregrounded — `LocationIngestor.captureTodayIfNeeded(now:)` (`WhereCore/Sources/Location/LocationIngestor.swift:350-367`) already requests a best-effort fix on launch/foreground when today lacks GPS. The remaining question is background delivery without passive movement events, not whether opening the app can fill today. Measure the no-foreground case and define the desired fallback without promising an OS-guaranteed daily wakeup. (human; narrowed 2026-09-07) ## 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: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) +- fix(WhereCore) [needs-design]: `DailySummaryReconciler.reconcile()` is absent from the post-day-change fan-out. `DayJournal.reconcileAfterDayDataChange()` (`DayJournal.swift:70-74`) fans out to issue state and widgets only, and the live-GPS hot path (`WhereServices.swift:198-217`) does the same, so the daily notification body stays stale until a foreground re-`configure`; backup, remote-import, recording-policy, and reset paths do reconcile summary — the first three through the composition root's `DerivedDataReconciler` (`WhereServices.swift:13-21` with `summary.reconcile()` at `:19`, wired at `:228`, `:247-259`, `:267-269`), and `reset()` by calling `summary.reconcile()` directly at `:409`. Add it to the local fan-out or document the foreground-only policy. The canonical ordering in [`Specifications/PostWriteReconcile`](Specifications/PostWriteReconcile/README.md:36) deliberately excludes summary until it is routed, so the spec is not the authority for closing this. (audit 2026-07-26; PR #160 narrowed scope; re-verified 2026-09-06) - test(WhereCore) [quick-win]: Mutate data and assert the summary notification body updates without a re-`configure`. The nearest existing guard is `WhereServicesTests.remoteDayImportReconcilesNotificationsAndWidgets` (`WhereServicesTests.swift:519-562`), which covers the *remote-import* derived-data fan-out rather than a local day mutation, so it would not catch this. (audit 2026-07-26; citation refreshed 2026-09-06) -- perf(WhereCore) [needs-design]: Performance pass — how often is the app booting? Can we only do it on changes of, say, 1 km or more? (human) +- perf(WhereCore) [needs-design]: Measure automatic launch and GPS-write frequency before changing the movement threshold — `LocationIngestor` owns passive sample admission and the explicit foreground one-shot (`WhereCore/Sources/Location/LocationIngestor.swift:347-367`); launch spans are declared in `WhereUI/Sources/Launch/WhereLaunchSteps.swift:177-210`. Use those diagnostics to establish boot/write rates, then decide whether distance filtering can reduce work without losing day boundaries or region crossings. A 1 km threshold remains a human proposal, not a measured requirement. (human) ## P1s (Should do) +- fix(WhereUI) [quick-win]: Refresh the live-region welcome when the scene becomes active — the only lookup is `.task(id: isWelcomeLookupActive)` (`Primary/LocationsView.swift:43-49`, `:122-125`), keyed by view visibility and covering UI, with no `scenePhase` input. `MainTabs` refreshes only the report on foreground (`Sources/MainTabs.swift:88-97`). After dismissing a region, backgrounding, travelling, and resuming with the Locations hierarchy retained, no input changes to request a new region; a prior unavailable fix likewise waits for a tab or covering-UI change. Key the visible lookup by scene activity and cancel it on background; add a regression for foreground re-entry after a nil or previously acknowledged result. This is a static lifecycle gap; reproduce the retained-tab path on a device before choosing the final trigger. (audit 2026-09-07, PR #309) - 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. The onboarding phases currently move from region selection/customization directly to location permission (`OnboardingFlowModel.Phase`, `WhereUI/Sources/Onboarding/OnboardingFlowModel.swift:11-17` — the previously cited `OnboardingView.swift:30` no longer holds the enum), while `DayJournal.ingest(_:)` is the existing bulk sample path (`WhereCore/Sources/Journal/DayJournal.swift:99`). Design a PhotoKit-backed importer that requests access only after an explicit opt-in, reads location and capture time locally without uploading photo contents, previews what will be added, records photo-derived provenance rather than treating it as live GPS, deduplicates repeat imports, and makes skipping the screen frictionless. (human 2026-08-03) - refactor(WhereCore) [needs-design]: Scope diagnostic emission so Flyover's unactivated sibling demo world cannot write its activity through the process-global `WhereLog` / `Periscope.shared` facade into the active real scope's durable diagnostic store. `WhereFlyoverWorld.build()` correctly gives the sibling a private `Periscope` and never starts its sink, but static `WhereLog` channels still bypass that injection; carry the scope's logging system through services/models or add a task-/environment-scoped routing context before treating Flyover's diagnostic activity as isolated. Domain data, preferences, widgets, notifications, and location remain in memory/no-op already. (`WhereUI/Sources/Developer/Flyover/WhereFlyoverWorld.swift:34-37` builds the private, sink-less `Periscope` — PR #301 touched the file but the wiring is unchanged; `WhereCore/Sources/Logging/WhereLog.swift:17-32` is the static facade that bypasses it; agent 2026-07-29, re-verified 2026-09-06) -- fix(WhereUI) [quick-win]: `CalendarDay.displayDate` resolves through `Calendar.current` (`DateRangeFormatting.swift:33`), so every day label that flows through it — relabel, logged days, resolution details, the region drill-in — renders a wrong date on a non-Gregorian device: `startOfDay(in:)` interprets the day's Gregorian Y-M-D as *that* calendar's components, so a Buddhist-era device resolves 2026-07-26 to a date ~543 years off. **Exactly four production sites remain** (re-counted 2026-08-30): the `displayDate` body above, the two `calendar: Calendar = .current` defaults on `DateRangeFormatting` (`:6`, `:19`), and the same default on `PresenceTimeline.stints` (`PresenceTimeline.swift:37`). Call sites that take those defaults rather than threading `report.calendar`: `PresenceTimelineList.swift:31` and `ResolutionView.swift:182`, plus every `displayDate` consumer (`ResolutionView.swift:184`, `:191`, `:202`, `DayRelabelView.swift:220`, `RegionDaysView.swift:135`). Take an explicit calendar (Gregorian + current time zone) in the helpers and thread the report's calendar from the call sites. A further **eight** implicit `.current` uses are inside `#if DEBUG` `SnapshotProviding`/`#Preview` fixtures (`ManualDayView.swift:511`, `:540`, `:553`; `DayRelabelView.swift:266`, `:278`; `FlightDayDetailView.swift:182`; `AbruptChangeDetailView.swift:98`, `:103`) — they can't affect a shipped label, so they are not part of this fix, but they are equally invisible to the lint rule. Do **not** count `calendar.timeZone = .current` (still five sites: `WhereFormat.swift:80`, `YearReportModel.swift: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 PR #307's new joined-Timeline code also introduces no `Calendar.current`. The `where.gregorian_calendar` Bumper rule that should catch the four real sites is blind to the implicit-member form — filed in the root [`TODOs.md`](../TODOs.md). (audit 2026-07-26; re-verified 2026-09-06 after PR #307) -- fix(WhereCore) [needs-design]: `WhereServices.setPrimaryRegions(_:)` (`WhereServices.swift: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(WhereUI) [quick-win]: `CalendarDay.displayDate` resolves through `Calendar.current` (`DateRangeFormatting.swift:33`), so every day label that flows through it — relabel, logged days, resolution details, the region drill-in — renders a wrong date on a non-Gregorian device: `startOfDay(in:)` interprets the day's Gregorian Y-M-D as *that* calendar's components, so a Buddhist-era device resolves 2026-07-26 to a date ~543 years off. **Exactly four production sites remain** (re-counted 2026-08-30): the `displayDate` body above, the two `calendar: Calendar = .current` defaults on `DateRangeFormatting` (`:6`, `:19`), and the same default on `PresenceTimeline.stints` (`PresenceTimeline.swift:37`). Call sites that take those defaults rather than threading `report.calendar`: `PresenceTimelineList.swift:31` and `ResolutionView.swift:182`, plus every `displayDate` consumer (`ResolutionView.swift:184`, `:191`, `:202`, `DayRelabelView.swift:220`, `RegionDaysView.swift:135`). Take an explicit calendar (Gregorian + current time zone) in the helpers and thread the report's calendar from the call sites. A further **eight** implicit `.current` uses are inside `#if DEBUG` `SnapshotProviding`/`#Preview` fixtures (`ManualDayView.swift:511`, `:540`, `:553`; `DayRelabelView.swift:266`, `:278`; `FlightDayDetailView.swift:182`; `AbruptChangeDetailView.swift:98`, `:103`) — they can't affect a shipped label, so they are not part of this fix, but they are equally invisible to the lint rule. Do **not** count `calendar.timeZone = .current` (still five sites: `WhereFormat.swift:80`, `YearReportModel.swift:300`, `WhereModel.swift:331`, `RemindersSettingsModel.swift:153`, `PreviewSupport.swift:513`) — that is a `TimeZone` on an explicit Gregorian calendar, which is the correct pattern, and it has been miscounted as drift by three separate passes. `PresenceJourneyRow` / `PlannedPresenceJourneyRow` likewise thread `calendar:` explicitly and are correctly outside the count. The forecasting work held the line: `Forecasting/LocationForecastProgress.swift:65` builds a fresh `Calendar(identifier: .gregorian)` rather than reaching for the device's, and PR #307's new joined-Timeline code also introduces no `Calendar.current`. The `where.gregorian_calendar` Bumper rule that should catch the four real sites is blind to the implicit-member form — filed in the root [`TODOs.md`](../TODOs.md). (audit 2026-07-26; re-verified 2026-09-06 after PR #307) +- fix(WhereCore) [needs-design]: `WhereServices.setPrimaryRegions(_:)` (`WhereServices.swift:384-388`) commits atomically but skips `DayJournal.reconcileAfterDayDataChange()` — widgets/reminders/summary don't refresh until foreground/configure. Region *attribution* does rebuild, because it observes `changes()` (`RegionAttribution.swift:72-75`), which is why the stale surfaces are only the scheduled/published ones. Route picker commits through the unified fan-out, or document the intentional deferral. Out of scope for [`PostWriteReconcile`](Specifications/PostWriteReconcile/README.md) until routed. (audit 2026-07-26; re-verified 2026-08-30) - fix(WhereCore) [needs-design]: Soft-delete untracked regions. `SwiftDataStore.setTrackedRegion(false)` (`SwiftDataStore.swift:1844-1875`, in-source TODO at `:1863-1871`, delete at `:1872-1874`) and `setPrimaryRegions` (delete-by-omission at `:1925-1935`) hard-delete the row, which drops the region from the attributor's load set — so re-aggregating a past year re-attributes that region's GPS days to `.other` (manual days, stored as region sets, are unaffected). The `SwiftDataStore` TODO filed this as "when the region picker ships"; it has shipped, and both the onboarding picker and the Settings region editor reach it (`PrimaryRegionSelectionModel.swift:158` → `setPrimaryRegions`), so this is user-reachable rather than latent. Retain the row for attribution and hide it from the pickers instead. (audit 2026-07-26; re-verified 2026-08-30) -- fix(WhereCore) [needs-design]: ~~`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(WhereCore) [needs-design]: The retry queue evicts FIFO at its 1000-sample capacity and drops samples with a warning only (`LocationIngestor.swift:502-507`, capacity default at `:112`, event case at `LocationIngestorLog.swift:53` with its `warning` level at `:64-65` and message at `:91-92` — the previously cited `:76-77` was a different event's message). Decide the capacity policy and whether eviction warrants user-visible degradation, then document it. (audit 2026-07-26; re-verified 2026-08-30) - fix(WhereUI) [quick-win]: `PresenceTimelineList` returns `[]` whenever `report.report` is nil (`PresenceTimelineList.swift:30-31`, empty-state branch at `:43`), so the Timeline segment of Your Year renders the "no stays" empty state while the year is still loading (and during a year switch) — unlike the Calendar segment beside it, which gates on `loadState` (`CalendarContentView.swift:64-66`). PR #307 moved the branch while adding joined planned stays and the estimate panel but left the missing load gate unchanged. (audit 2026-07-26; re-verified 2026-09-06 after PR #307) - - refactor(WhereUI) [needs-design]: Extract a shared `ReportLoadGate`. The same `YearReportModel.loadState` gate is copy-pasted across `LocationsView.swift: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) + - refactor(WhereUI) [needs-design]: Extract a shared `ReportLoadGate`. The same `YearReportModel.loadState` gate is copy-pasted across `LocationsView.swift:151-176`, `ElsewhereView.swift:51-70`, `ResolutionView.swift:59-89`, and `CalendarContentView.swift:64-74`, and `PresenceTimelineList` skips it entirely (above). One gate view would cover all five. (audit 2026-07-26; re-verified 2026-09-06) - 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-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) +- fix(WhereUI) [needs-design]: Notification authorization is requested during launch, with no context and unprompted. The chain, re-verified 2026-09-06: the launch's detached `reminders` / `summary` / `issue-alerts` steps (`WhereLaunchSteps.swift:177-210` — moved when PR #301 prepended the demo step) call `WhereSession.apply*Configuration()` (`WhereSession.swift:533-594`), which calls each reconciler's `configure`, and each one requests authorization whenever its feature is enabled (`ReminderReconciler.swift:81-82`, `DailySummaryReconciler.swift:47-48`, `DataIssueAlertReconciler.swift:48-49` → `UNUserNotificationCenter.requestAuthorization`). **All three preferences default to `true` on a fresh install** (`WherePreferences.swift:89`, `:112`, `:136`, each `?? true` and documented as "active out of the box"), which is *why* a first launch prompts — so the fix has to reckon with the defaults, not just the call site. It reaches the notification center while the app is still launching, so a fresh install shows the system "Where Would Like to Send You Notifications" alert over the splash — before the user has expressed any interest in reminders and with no in-app rationale. Observed on a fresh-install simulator screen recording: the alert lands roughly a second after the splash appears and then sits on top of the revealed app. Ask in context instead — request when the user turns reminders/summary on in Settings (or immediately after onboarding, with a sentence of explanation) — and have the launch fan only *reconcile* schedules against authorization that was already granted, never trigger the prompt. (agent) +- refactor(WhereCore) [needs-design]: Identify remaining invalid controller states before another state-machine rewrite — scope ownership is already `WhereModel.ScopeState` (`WhereUI/Sources/Model/WhereModel.swift:49-58`), and recording admission is gated in `LocationIngestor` (`WhereCore/Sources/Location/LocationIngestor.swift:329`). Keep this design direction scoped to a concrete illegal state discovered in the remaining controller orchestration; do not replace working typed state solely to satisfy the old note. (human; narrowed 2026-09-07) - test(WhereIntents) [quick-win]: The per-intent `perform()` glue — guards, snippet wiring, error→dialog mapping — is untested, because `@Dependency` traps outside the perform flow: no test in `Where/WhereIntents/Tests/` calls an intent's `perform()`. **Half closed:** `WhereIntents/AGENTS.md:79-90` now states the rule and explains the framework trap, so the "say so" option is satisfied for agents — but the README's Testing section (`README.md:83-87`) still reads as though the coverage is complete and never mentions the limitation, so a human reading only the README is misled. (The citations previously here — `AGENTS.md:95-108`, `README.md:106-112` — pointed past the end of both files after doc rewrites; corrected 2026-09-06.) Either extract a thin testable seam, or carry the AGENTS.md caveat into the README. (audit 2026-07-26; re-verified 2026-09-06) - fix(WhereUI) [needs-design]: Give the feature-discovery widget gallery a complete VoiceOver pass. `WidgetExampleFrame` (renamed from `WidgetPreviewFrame` since this was filed) exposes only its widget content with no accessibility of its own, while the Home Screen and Lock Screen galleries label the *whole* miniature screen rather than each family (`FeatureHomeScreenExample.swift:44-45`, `FeatureLockScreenExample.swift:55-56`), so VoiceOver announces duplicate content such as “Days in 2026” without distinguishing Small from Medium or Inline from Circular and Rectangular. Define localized, combined accessibility elements for every framed example, including its widget kind and system family, and review the surrounding miniature-screen semantics alongside the other planned widget accessibility improvements. (pr#204 review; re-verified 2026-08-09) - fix(WhereUI) [quick-win]: The evidence feature-discovery panels have the same gap in a weaker form: `FeatureShareSheetPreview.swift:71` and `FeatureEvidenceArchivePreview.swift:66` apply `.accessibilityElement(children: .combine)` with **no** `.accessibilityLabel`, so VoiceOver reads each walkthrough step as one undifferentiated blob of its concatenated text. `FeatureEvidenceComposePreview.swift:45` uses `.contain`, which is better but still unlabeled. `SiriIntentCard.swift:35-37` is the pattern to copy — it sets both a label and a value. **Scope note (2026-08-16, re-checked 2026-08-30):** an unlabeled `.combine` is not automatically a defect, and this pass checked the new surfaces against that. PR #267's privacy passport rows combine without a label too (`PrivacyPassportDisclosureRowLabel.swift:41`) but are *correct* — their symbol is `.accessibilityHidden(true)` (`PrivacyPassportDisclosureSymbol.swift:17`) and the combined children are a title plus its one-line detail, which reads as a sentence. The same goes for the two snippet cards (`IntentSnippets.swift:61`, `:141`) and for this window's `StampBanner`, whose combined children are one localized sentence. What makes the three panels above defective is that they concatenate a multi-step walkthrough, not that they combine. (audit 2026-08-09; re-verified 2026-08-30) - refactor(WhereShareExtension) [needs-design]: Consolidate the share/add evidence form. `ShareEvidenceView.swift:65-89` and `AddEvidenceView.swift:37` are parallel implementations over parallel catalog namespaces (now the generated `shareForm*` / `evidenceForm*` symbols — the split itself is unchanged). (audit 2026-07-26; citations refreshed 2026-09-06) - perf(WhereCore) [needs-design]: Consider incremental year-report reads or memoization for the widget/reminder/summary hot paths — `ReportReader.yearReport` (`ReportReader.swift:37`) and `WidgetDataReader.snapshot(asOf:)` (`WidgetDataReader.swift:88-103`) re-aggregate a full year each time (`ReportReader.swift:37-49`, `WidgetDataReader.swift:87-103`). (audit 2026-07-26; re-verified 2026-08-30) - refactor(WhereUI): What's with all the `.accessibilityIdentifier(…)` modifiers, do we need them? (human) -- feat(WhereUI): Add a UI that represents where you currently are — maybe a border on the current location card? (human) +- feat(WhereUI) [needs-design]: Keep the current region visible after dismissing its welcome — PR #309 added a live-region acknowledgement (`Primary/LocationWelcomeModel.swift:32-50`, `Primary/LocationsView.swift:114-119`), partially satisfying this request. Dismissal removes it (`LocationWelcomeModel.swift:54-58`), and the ranked cards still have no persistent current-region marker. Decide whether a border or localized current-location badge should remain on the matching card, using the existing `CurrentRegionResolver` rather than another location source. (human; narrowed 2026-09-07) - refactor(WhereCore) [needs-design]: Per-entity schema versioning + lazy upcasting for CloudKit sync drift. There is intentionally **no** boot-time data migration or on-read legacy recovery (removed pre-release as over-built for a single dev's data). Today a data-shape change relies solely on a one-time manual backup **export → transform (`Tools/upgrade-backup.rb`) → replace-import** to rewrite rows into the current shape; `SD….toValue()` reads only the current shape and drops (fault-logs) a row it can't place (e.g. an `SDManualDay` with no `dayKey`). Gaps this leaves, which a general mechanism should close: an old-build device can sync in an old-shaped entity at any time (not just at launch), and until it's re-imported such a row is dropped on read rather than upcast. Replace with: (agent) - refactor(WhereCore): Make record→value conversion (`SD….toValue()`) a version-aware **upcaster**: each `@Model` carries its written schema version, and `toValue()` applies an ordered, pure, idempotent `vN → vN+1` chain, so every read is correct regardless of stored version — no import hook or scan needed (there is no per-record CloudKit import callback anyway). This is the "lazy migration / event-sourcing upcaster" pattern. Make the *filtered* reads (`manualDays(in:)`) upcast-aware too, so a not-yet-rewritten row isn't dropped by a column predicate. (agent) - refactor(WhereCore): Persist a **minReaderVersion** per entity, not just a version. Additive (expand/contract) changes leave it low so old builds keep reading via the retained old field (tolerant reader); only a genuinely forward-incompatible change bumps it. Readers exclude entities whose `minReaderVersion > appVersion` and surface a "some data needs a newer app" warning — the only case that actually needs exclusion. (agent) @@ -55,10 +54,11 @@ The item format and the placement rule live in the root - fix(WhereUI) [needs-design]: broken-snapshots — the snapshot suite pinned genuinely broken renderings as references, flagged with `[Fix later]` review comments on PR #101 and merged anyway to land the suite. These are not flaky captures (those have their own ledger below) — each is a faithful, reproducible image of something actually wrong, so re-recording is never the fix. Fix the view, the capture frame, or the pipeline as each item says, then re-record just that reference under `Where/WhereUI/SnapshotTests/__Snapshots__/`. Most cluster on the accessibility axes `.screenDefaults` added — the ax5 Dynamic Type and VoiceOver-annotated configurations that nothing rendered before this suite existed. (pr#101 review) - fix(WhereUI) [quick-win]: broken-snapshots: the calendar day grid breaks at accessibility Dynamic Type. Every two-digit date truncates to its first digit — the 10th–31st render as "1", "2", or "3" — because the day number is clamped to a fixed square (`DayCell` at `CalendarContentView.swift:489-493`, `.frame(width: calendar.day.numberSize, height: calendar.day.numberSize)`), and the weekday header row wraps mid-word ("Sun" over two lines, "Wed" over three) because each symbol is a plain `Text` in an equal-width grid column (`:313-317`). Both show in `calendarContent.WithData_iPhone_ax5.png`; the digit truncation also hits `..._iPad_ax5.png`, where the extra width goes to inter-column gaps instead of the numbers. Showing "1" where the date is 10 is wrong content, not merely tight layout. **The references have now been re-recorded twice with the layout code unchanged — by PR #196 and again by PR #297's Xcode 27 beta 6 refresh — so they pin the same defect at a third recording.** Re-check the current image before fixing, and re-record after. (pr#101 review; re-verified 2026-09-06) - fix(WhereUI) [needs-design]: broken-snapshots: `YearView` overflows horizontally at ax5. In `year.Loaded_iPhone_ax5.png` the month title reads "nuary", the day grid is clipped on both edges, and the Calendar/Timeline pill runs off the trailing edge. The suspect is `YearModePicker`, whose segment labels take their intrinsic width via `.fixedSize()` (`YearView.swift:110`, with an in-source comment explaining it keeps labels from truncating mid-animation) inside a bottom `safeAreaInset` (`:40-43`), making it wider than the screen at ax5. Confirm the oversized inset is what widens the layout beneath it, then make the picker fit at accessibility sizes (icon-only, wrapped, or scrollable) — note the `.fixedSize()` is deliberate, so the fix has to keep animation from truncating too. (pr#101 review; re-verified 2026-08-09) - - fix(WhereUI) [quick-win]: broken-snapshots: the Resolve toolbar badge sits awkwardly on the iOS 26 glass toolbar button. `ResolveToolbarLabel` hand-rolls the badge as a red `Capsule` overlaid on the `checklist` symbol and pushes it out with a fixed `.offset(x: spacing.small, y: -spacing.small)` (`LocationsView.swift: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) [quick-win]: broken-snapshots: the Resolve toolbar badge sits awkwardly on the iOS 26 glass toolbar button. `ResolveToolbarLabel` hand-rolls the badge as a red `Capsule` overlaid on the `checklist` symbol and pushes it out with a fixed `.offset(x: spacing.small, y: -spacing.small)` (`LocationsView.swift:367-382`, the red capsule + offset at `:380-381` — moved by PR #302's card work), landing it half outside the button's own glass capsule — visible in `root.LoggedIn_iPhone.png`. Use SwiftUI's `.badge()` on the toolbar item, or offset against the resolved chrome rather than a fixed spacing token. (pr#101 review; re-verified 2026-09-06) - fix(WhereUI): broken-snapshots: `locations.Loaded_iPad.png` bakes in raw inflection markup — the Elsewhere card's subtitle renders literally as `^[3 region](inflect: true)`. This is the `locations.elsewhere.subtitle` P1 filed above, now pinned as a reference; recorded here so the image isn't mistaken for correct output, and so that reference is re-recorded when the fix lands. (pr#101 review) ## P2s (Nice to have) +- test(WhereUI) [quick-win]: Cover the welcome overlay's scrolling and modal semantics — `WelcomeFirst` / `WelcomeBack` use fixed `.phoneLightDark` frames and one fixed AX5 frame (`Primary/LocationsView.swift:447-462`) while the modal contains a `ScrollView` inside a greedy `GeometryReader` (`Primary/LocationWelcomeOverlay.swift:25-39`). No welcome configuration uses the semantic `.accessibility` capture or an iPad frame. Capture the shared scrolling child with full-content sizing if the bounded modal cannot converge, and add a semantic modal case plus iPad coverage. Keep the production modal/focus wiring; a fixed AX5 image cannot prove that all scrollable controls or VoiceOver elements remain reachable. (audit 2026-09-07, PRs #309/#311) - feat(Where): Consider the user-assigned device-name entitlement and matching provisioning-profile support so the Devices screen can offer a better initial label than the generic hardware family. Keep the current generic name until the entitlement is intentionally provisioned; never silently depend on an entitlement absent from developer signing. (`InstallationRecordingContextStore.swift:227-242` still derives `systemName` from `UIDevice.current.model`, and `Project.swift` declares no such entitlement; PR #160 review, re-verified 2026-08-09) - feat(WhereUI) [needs-design]: Give the app a branded launch screen. `UILaunchScreen` is an empty dictionary (`Project.swift`), so the pre-main frame is plain white. Measured from a fresh-install simulator recording, a first run reads as ~1.7s of white → ~0.25s of the dark `LaunchSplashView` → the light onboarding screen, so the splash registers as a quarter-second dark blip between two light screens rather than as the app opening. A launch screen matching the splash's background + icon would make that continuous. Note this is the right layer to fix it at: the splash's own `minimumSplashDuration` hold deliberately gates only the `.ready` reveal, not a gate transition like onboarding, so lengthening the hold would just delay interactive UI. (agent) - refactor(WhereUI) [needs-design]: Make the scene-scoped model wiring compiler-checked rather than an `@Environment` lookup that fails silently. `WhereSession` (the always-on coordinator) is read from the environment, so a screen mounted without a parent injecting it resolves to a runtime fallback/precondition instead of a compile error. The scoped models (`YearReportModel`, `ResolveModel`, `BackupModel`, `RemindersSettingsModel`) are already constructor-injected; explore threading the coordinator the same way (or a non-defaulting typed `EnvironmentKey`) so a broken wiring can't build. Follow-up from the `WhereSession` split. (agent) @@ -72,16 +72,11 @@ The item format and the placement rule live in the root - refactor(WhereUI) [quick-win]: PR #302's sticker redesign orphaned `WhereFormat.locationCardEstimatedDays` — the card now composes its estimate from `WhereFormat.dayCount` plus `Text(.locationCardEstimateLabel)` (`Primary/LocationCardEstimateSticker.swift:25-28`), leaving the old `"Estimated · %@"` helper (`Shared/WhereFormat.swift:97-98`), its `locationCard.estimatedDays` catalog key, and its test (`WhereFormatTests.swift:70`) with no production caller. Delete all three together (removing the source symbol reference is what lets the catalog entry go — see the auto-extraction item above), or keep them deliberately if an alternate surface is planned. Dead-but-tested format helpers read as live API. (audit 2026-09-06) - fix(WhereCore) [quick-win]: Surface the `applicationSupport()` → `NoOpLocationOutbox` fallback (`LocationOutbox.swift:118-125`); it logs `.noApplicationSupport` and then silently disables cross-launch retry durability, so samples are lost across process death with nothing above Periscope aware of it. Either report it to the launch wiring (a startup health flag the Data/About screens can read) or treat it as a programmer error. Weightier since PR #160 made the outbox generation-stamped and load-bearing. (audit 2026-07-26; re-verified 2026-08-30) - test(WhereShareExtension, WhereWidgets) [quick-win]: Close the two extension-shaped test gaps that don't need a new bundle pattern decision — `ShareEvidenceModel.buildPendingEvidence()` (`ShareEvidenceModel.swift:126-132`, documented as exposed for testing at `:124-125`, nothing tests it) and `WhereWidgetProvider`'s midnight reload policy (`WhereWidgetProvider.swift:41-48`, `.after(nextMidnight)` — the extension's core scheduling logic, untested in any target). (audit 2026-07-26; citations refreshed 2026-09-06) -- test(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(RegionKit) [quick-win]: Add `GeoJSONTests.swift`. The unsupported-geometry throw (`GeoJSON.swift:62-67`) and malformed-coordinate drop (`:124-128`) are untested, and `RegionCatalog.loadFromBundle()`'s degrade-to-empty-catalog behavior is asserted only at the log-event level (`RegionLogTests.swift:27-30`) rather than at runtime. `RegionKit/README.md:165-168` now explicitly documents the missing decoding coverage. The earlier overclaim was fixed by PR #172; the tests remain absent (re-verified 2026-09-07). (audit 2026-07-26; re-verified 2026-08-09) - test(WhereUI) [quick-win]: Add the missing namesake test for `LocationNamer` (cache / coalescing — the file now lives at `Sources/Shared/LocationNamer.swift`). **The other half of this item is obsolete:** `CalendarContentView.scrolledForYear` no longer exists — the scroll reveal is now a `scrollPosition.scrollTo` inside `.task(id:)` (`CalendarContentView.swift:175-180`), so there is no gate left to test as filed; narrowed rather than closed because the `LocationNamer` half is untouched. (audit 2026-07-26; narrowed 2026-09-06) - test(WhereIntents) [quick-win]: Test `RegionSpotlightIndexer`. The indexer **shipped in PR #210** (`RegionEntity+Spotlight.swift:16-33`, wired in `Where/Where/Sources/RegularApplicationRuntime.swift` with a demo-mode skip) and arrived with no test of its own: nothing references `RegionSpotlightIndexer` or `indexRegions`, so neither the success log, the degraded failure log, nor the demo skip is verified. `RegionEntityTests.swift:27-45` covers `RegionEntity.tracked(from:)` — the indexer's *input* — which is easy to mistake for coverage of the indexer. (audit 2026-07-26; re-verified 2026-08-09) -- test(WhereCore) [needs-design]: Close the namesake-test debt — **60** of the 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-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: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) +- test(WhereCore) [needs-design]: Close the namesake-test debt — **60** of the 129 implementation files have no same-named `*Tests.swift` (re-derived by basename 2026-09-07; 60 of 128 at August 30, 60 of 127 at August 16, 57 of 114 at August 9, and 28 of 87 at July 26 — so for the first time the uncovered count held flat while the module grew, because both `PlannedStayLocationVerifier.swift` and the newer `CurrentRegionResolver.swift` arrived with namesake test files). Still uncovered among the originally named files: `WidgetTimelineRefresher`, `BackupArchive` (`WherePreferences` closed 2026-08-05). `WhereCoreTests.swift` is an omnibus holding five suites, not just `YearReportTests` — also `SwiftDataStoreFactoryTests`, `SDLocationSampleTests`, `EvidenceKindTests`, `SampleSourceTests`. The basename count is a proxy: `Sources/Logging/*` event types and `SD*` record shells reasonably have no namesake file, so treat it as a trend line, not a work list. Split by concern as those files change rather than in one pass. (audit 2026-07-26) + - refactor(WhereCore) [quick-win]: Drop the remaining Core-API parameter defaults — `DayJournal.addEvidence(_:blob:)` (`DayJournal.swift:271`) and `WidgetDataReader`'s aggregator/attributor (`WidgetDataReader.swift:76-77`). The composition root already knows each value. (Two of the four filed are now done: `WherePreferences.init(store:)` and `SwiftDataStore.make(storage:)` both require the argument. This window's new Core API held the rule — `PlannedStayLocationVerifier.status` takes every argument explicitly.) (audit 2026-07-26; re-verified 2026-08-30) - convention(WhereIntents) [quick-win]: Small polish, all four parts still open — register `LogTripIntent` in `WhereShortcuts` (`Where/Where/Sources/WhereShortcuts.swift:11-48` registers **four** — "five" was stale since PR #230 retired the recent-activity shortcut — still no trip backfill) or document Shortcuts-only discovery; use `Calendar.whereIntents` for `LogDayIntent`'s default day instead of `date ?? Date()` (`LogDayIntent.swift:39`, no data impact today since `DayJournal` buckets Gregorian); log the App Group open failure behind `WhereIntentReader.todaySnapshot`'s `try?` (`WhereIntentReader.swift:17-18`); and wrap `RegionViewer`'s `RegionMapView` in `.whereBroadwayRoot()` (`RegionViewerApp.swift:15-18`) so the dev tool renders with app styling. (audit 2026-07-26; re-verified 2026-08-09) - convention(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) @@ -90,12 +85,12 @@ The item format and the placement rule live in the root - feat(WhereUI): Raw data browser (similar to the SwiftData browser). (human) - docs(WhereUI): Add comments to strings in the xcstrings files. (human) - convention(WhereUI) [quick-win]: `DeveloperDemoLaunchSheet`'s snapshot case uses the fixed `.phoneLightDark` frame on a `NavigationStack` + `Form` (`Developer/DeveloperDemoLaunchSheet.swift:130`, the `Form` at `:55`), against the root rule that snapshots containing scrolling content use full-content sizing — its sibling DEBUG form `DeveloperCrashTestingView` declares `.fullContentPhoneLightDark` for exactly this shape. A category list taller than one screen would silently pin a clipped capture. Switch the preset and re-record the two references. (audit 2026-09-06, from PR #301's new surface) -- refactor(WhereUI) [quick-win]: The widget fixtures' pinned instant is still `1_770_000_000` (02:40 UTC / Feb 1 evening Pacific, near a day boundary) at `PreviewSupport.swift: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: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) +- refactor(WhereUI) [quick-win]: The widget fixtures' pinned instant is still `1_770_000_000` (02:40 UTC / Feb 1 evening Pacific, near a day boundary) at `PreviewSupport.swift:709` and `:804`. Moving it safely off midnight was skipped to avoid re-recording the widget references; two whole-suite re-records have passed since (PR #196 and PR #297), each of which would have made the change nearly free. (From the July 2026 snapshot-testing PR review; re-verified 2026-08-30) +- test(WhereUI) [needs-design]: Snapshot matrix gaps — `ManualDayView`'s range-mode add still has no snapshot case (the same gap the range-coverage item above describes). **The `LocationsView` half is closed and this item was stale in saying otherwise:** `LocationsView` declares nine cases (`LocationsView.swift:395-463`) — `Loaded`, `PlannedStay`, `ForecastsHidden`, `Empty`, `MissingDays`, `ElsewhereOnly`, `DotsHidden`, `WelcomeFirst`, `WelcomeBack` — so PR #187/#252's forecasting surfaces arrived with image coverage in both the shown and hidden states, and PR #292's endorsement redesign and PR #302's sticker redesign re-recorded them rather than adding a gap. `YearView` holds its two cases (`Loaded`, `Empty`) at `YearView.swift:124-142`. (From the July 2026 snapshot-testing PR review; corrected 2026-08-16, citations refreshed 2026-09-06) - test(WhereUI) [quick-win]: **Four** Settings-reachable screens have a `#Preview` but no `SnapshotProviding` conformance, so no image pins them — against the module convention that an image bundle, not a hosting smoke test, owns "does this screen render". **Down from five: PR #305's Settings region editor closed the `RegionsSettingsView` gap** — the rewritten screen declares `SnapshotProviding` (`Regions/RegionsSettingsView.swift:211-220`, `.fullContentScreenDefaults`) and arrived with `RegionsSettingsViewSnapshotTests` and 10 references, the second time in three windows a feature team closed part of this item by touching the screen. Re-derive the list, don't carry it: enumerate every `*View.swift` under `WhereUI/Sources` that contains a `#Preview` and no `SnapshotProviding`, then cross-check each against `SettingsView.destination(for:)` (`Settings/SettingsView.swift:259-313`). Still uncovered (re-derived 2026-09-06): `AlertsSettingsView` (`:262` — reminders, daily summary, issue alerts, the drift threshold, and a manual "find issues now"), `VisibleYearSettingsView` (`:55`), `RemovedDeviceView` (`Devices/RemovedDeviceView.swift:44`, the blocking CloudKit-removal recovery gate with the rejoin call to action — a screen a user only reaches when something has already gone wrong, which is the worst place for an unpinned regression), and **`EvidenceListView`** (the `.attachments` destination). **The "ten further `*View.swift` files with a bare `#Preview` and no image case anywhere" list needs one correction:** `WhereFlyoverView` *is* image-pinned — `WhereFlyoverViewSnapshotTests` hosts `FlyoverView(catalog:)` directly rather than through a `SnapshotProviding` conformance — so it has coverage without the conformance, which is a different (and acceptable) shape. The other nine still hold: `AppIconLoadingView`, `AbruptChangeDetailView`, `AddEvidenceView`, `EvidenceDetailView`, `RegionPickerView`, `DeveloperToolView`, `DeveloperToolUnavailableView`, `WhereFlyoverLogView`, `WhereFlyoverPresentationView`. Several are components or DEBUG tooling rather than screens, so they are not all debt. (PR #301's `DeveloperDemoModeRow` carries a bare `#Preview` too, but it is an overlay row pinned through the `DeveloperOverlay` snapshots — deliberately not counted, same as the other overlay rows.) Add cases for the four Settings screens following `Settings/DevicesSettingsView.swift` (for Alerts, an authorized and a denied-notifications variant, toggles on and off), and decide explicitly which of the nine others are screens. (audit 2026-08-09; re-counted and corrected 2026-08-30; RegionsSettingsView closed by pr#305, re-derived 2026-09-06) -- test(WhereUI) [quick-win]: Pin the joined Timeline card at accessibility Dynamic Type. `PlannedPresenceJourneyCardContent` has a dedicated stacked branch when `timeline.row.stacksDayCount` is true (`Primary/PlannedPresenceJourneyCardContent.swift:18-20`, branch at `:28-49`; the stylesheet enables it at `Shared/WhereStylesheet.swift:50-54`), but the `PlannedStay` case varies only light/dark plus a VoiceOver-annotation capture (`Primary/PresenceTimelineList.swift:195-202`). The representative `.fullContentScreenDefaults` case has AX5 coverage but no planned stay, so the new joined branch has no image that executes it. Add an iPhone full-content `.accessibility5` configuration to `PlannedStay`; keep the existing semantic accessibility capture. (audit 2026-09-06, from PR #307's new surface) +- test(WhereUI) [quick-win]: Pin the joined Timeline card at accessibility Dynamic Type. `PlannedPresenceJourneyCardContent` has a dedicated stacked branch when `timeline.row.stacksDayCount` is true (`Primary/PlannedPresenceJourneyCardContent.swift:18-20`, branch at `:28-49`; the stylesheet enables it at `Shared/WhereStylesheet.swift:51-55`), but the `PlannedStay` case varies only light/dark plus a VoiceOver-annotation capture (`Primary/PresenceTimelineList.swift:195-202`). The representative `.fullContentScreenDefaults` case has AX5 coverage but no planned stay, so the new joined branch has no image that executes it. Add an iPhone full-content `.accessibility5` configuration to `PlannedStay`; keep the existing semantic accessibility capture. (audit 2026-09-06, from PR #307's new surface) ## Deferred snapshot-test flakiness Known nondeterminism in the WhereUI image suites, accepted for now — scattered @@ -110,6 +105,11 @@ re-recording: # Completed issues +- docs(WhereCore) [quick-win]: Refresh stale doc claims. Closed 2026-09-07: the module AGENTS.md names the existing reconciliation exceptions; the README states the missing daily-summary fan-out and the failed-badge zero fallback; and the ingestor comment credits typed WhereLog events. PR #172 had already corrected the RegionViewer data-source description and RegionKit decoding-coverage claim; the earlier RootView and share-extension doc corrections remain shipped. The underlying summary, picker, badge, and GeoJSON-test items stay open. (audit 2026-07-26) + +- design(WhereCore): Model logged-in versus logged-out service ownership. Closed by PR #150's scope design: `WhereModel.ScopeState` (`WhereUI/Sources/Model/WhereModel.swift:50-58`) carries logged-out bootstrap, real scope, or demo scope. `WhereSession` exists only behind a resolved scope and holds non-optional services (`Model/WhereSession.swift:22-30`). The implementation makes the whole world optional rather than individual sub-services. (human; archived 2026-09-07) +- fix(WhereCore) [needs-design]: Route local ingest and manual samples through reminder/issue reconciliation and widget publication. Closed by the August 4 fan-out implementation: `DayJournal.swift:79-118` routes single/bulk ingest and `addManualSample`; `Specifications/PostWriteReconcile/README.md` records the ordering. Summary reconciliation and `setPrimaryRegions` remain separately open above. (audit 2026-07-26; shipped 2026-08-04; archived 2026-09-07) + - docs(WhereUI) [quick-win]: Correct the claim that `OnboardingGate` roots the launch trunk. (Resolved 2026-09-06: the gate documentation in `WhereLaunchSteps`, `OnboardingView`, `RootView`, `WhereLaunchTests`, and the module README now says it follows the side-effect-free demo preflight and precedes every world-building step. The underlying no-store/no-session invariant is unchanged.) ## broken-snapshots (four of the eight closed 2026-08-09) diff --git a/Where/WhereCore/AGENTS.md b/Where/WhereCore/AGENTS.md index f4d8fda8a..9b730bc9b 100644 --- a/Where/WhereCore/AGENTS.md +++ b/Where/WhereCore/AGENTS.md @@ -104,11 +104,12 @@ internal shape. - **Filter persistent-store remote-change notifications by the Where store URL and the store instance's transaction author.** Never let Periscope or Where's own local saves enter `remoteChanges()`. Guard: `StoreRemoteChangeSourceTests`. -- **Post-write reconciliation is defined once.** Every write and import - routes through `DayJournal.reconcileAfterDayDataChange()` (or its widget-less - subset `reconcileIssueState()`). Never copy the fan-out into a new write - path. Cross-collaborator hooks take a single closure wired at the - composition root (`BackupCoordinator.ImportLifecycle.didCommit`). +- **Route new writes through the existing reconciliation seams.** Use + `DayJournal.reconcileAfterDayDataChange()` or its widget-less subset + `reconcileIssueState()`; cross-collaborator hooks take a single closure + wired at the composition root (`BackupCoordinator.ImportLifecycle.didCommit`). + Existing exceptions are `setPrimaryRegions` and the local summary fan-out, + tracked in [`../TODOs.md`](../TODOs.md). Do not copy those omissions. - **Detectors read aggregated input. The speed-based one needs raw fixes.** `DataIssueInput.daySamples` carries per-day GPS fixes only (`.gpsVisit` / `.gpsSignificantChange`, sorted). Manual and evidence-implied samples are diff --git a/Where/WhereCore/README.md b/Where/WhereCore/README.md index 76b8e2fbc..db1f579f2 100644 --- a/Where/WhereCore/README.md +++ b/Where/WhereCore/README.md @@ -62,8 +62,9 @@ one it belongs to rather than to a god-object: - **`DayJournal`** — the user-sourced writes: manual-day overlays (`addManualDay` / `overrideDay` / `addManualDays`), clears (`clearManualDay` / `clearYear` / `eraseAllData`), evidence, and issue - dismissals. Each write commits, then awaits its reminder reconcile + widget - publish so the next reader sees a fully-applied change. + dismissals. Writes await reminder/issue reconciliation and widget publication + after committing. The local fan-out does not yet refresh daily summaries; + that gap is tracked in [`../TODOs.md`](../TODOs.md). - **`PlannedStayCoordinator`** — the synced, generation-scoped last-writer register behind “I’ll be here through…”. Clears and expiry write tombstones, and annual forecasts consume its current value without coupling projection math to persistence. @@ -289,7 +290,9 @@ rotates to a Reset child generation, and discards the retry queue only after com generation. Concurrent unjoined resets select a synthetic empty generation. An incomplete causal generation DAG fails closed instead of mixing old and new state. - **Failures surface.** Store methods are `async throws`. Errors are logged via - `WhereLog` and left observable — never swallowed into an empty default. + `WhereLog`. Most callers propagate failure or retain honest failed state. + The reminder badge still logs a failed scan and returns zero; that exception + is tracked in [`../TODOs.md`](../TODOs.md). ## Testing Swift Testing in [`Tests/`](Tests) (`WhereCoreTests`), hosted in `StuffTestHost`. diff --git a/Where/WhereCore/Sources/Location/LocationIngestor.swift b/Where/WhereCore/Sources/Location/LocationIngestor.swift index 7c8adba94..e4759b5d7 100644 --- a/Where/WhereCore/Sources/Location/LocationIngestor.swift +++ b/Where/WhereCore/Sources/Location/LocationIngestor.swift @@ -476,7 +476,7 @@ public actor LocationIngestor { await closeRecordingAuthority(ifAuthorizedFor: dataGenerationID) } catch { // Persistence failures (SwiftData save, CloudKit, etc.) are surfaced - // via `os.Logger` rather than silently dropped. The stream keeps + // via typed `WhereLog` events rather than silently dropped. The stream keeps // running so a transient error doesn't stop tracking, and the sample // is queued for retry on the next save attempt. Self.logger(attachments: [.error(error, name: "persist-error")]) { From a0dc653150a21cbb5a7496c0ccb909c2b25b4c18 Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Mon, 7 Sep 2026 09:14:29 -0700 Subject: [PATCH 2/2] Refresh the September 7 module audit from the reconciled backlog Record the precise covered source boundary, both welcome PRs, per-module static checks, inventory changes, supported check results, and runtime/Linux limitations. The report contains pointers to durable backlog items rather than duplicate work lists. --- MODULE_AUDIT.md | 478 ++++++++++++++++++++---------------------------- 1 file changed, 194 insertions(+), 284 deletions(-) diff --git a/MODULE_AUDIT.md b/MODULE_AUDIT.md index 0016ca897..ea4156378 100644 --- a/MODULE_AUDIT.md +++ b/MODULE_AUDIT.md @@ -1,292 +1,202 @@ # Swift Module Audit Report -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 (704 source / 369 test / 49 image-snapshot Swift files across shipped targets, plus 2 unwired prototype sources). No production behavior was changed. - -**Date:** September 6, 2026 -**Method:** Re-verification of every open finding in all 12 `TODOs.md` files against current source, using the prior same-day audit as the baseline for paths untouched since it; a fresh file, reference-image, and suite count; and code plus visual review of the one commit landed after that audit. Executed on this pass: `./swiftformat --lint`, `./shellcheck`, `./attribution --check`, `./sync-agents`, both retained-tool test suites, and `./snapshot-shards check`. Representative joined, separated, and AX5 Timeline references were inspected directly. -**Prior audit:** September 6, 2026, merged as PR #308 at `3daba8fb`. This same-day follow-up covers the one later commit, PR #307 (`1e9c9289`). - -> **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 -> which file covers which area. Read this one for *shape and drift* — what each -> module verified clean, the themes running across the backlog, and how the tree -> moved since the last pass — and take the work itself from the `TODOs.md` files. -> It is true as of the header date above, **not** as of `HEAD`. - ---- - -## Executive summary - -**A same-day follow-up caught one merged feature that landed after the weekly audit.** PR #307 rewrote the Timeline's planned-stay presentation, added the Estimated Time panel, four focused rendering helpers, and six references after PR #308's audit baseline. The change moved two open-item citations without closing either issue, and introduced one narrow coverage gap: the joined planned-stay card has an accessibility-only layout branch but no AX5 image case. The pass also finished the stale onboarding-gate documentation item filed by the prior audit, correcting every repeated claim rather than only the originally cited comment. - -What the pass found, in order of how much it should change your reading of the backlog: - -- **PR #307 held the domain boundary.** Its adjacency decision uses typed `CalendarDay` values and the injected report calendar; persistence, forecast math, and plan mutation remain in `LocationForecastModel`/WhereCore. It added no `Calendar.current`, raw SF Symbol names, user-facing string literals, or store access. -- **The visual states checked clean.** The joined New York continuation reads as one segmented card, the different-region case keeps full corners and rail separation, and the full-content captures include the trailing Estimated Time panel without clipping. The source keeps VoiceOver order aligned with visual order and preserves a semantic accessibility capture. -- **The accessibility rendering branch is unpinned.** `PlannedPresenceJourneyCardContent` restacks its joined labels and day count when the stylesheet resolves AX Dynamic Type, but the planned-stay case covers only standard-size light/dark plus a semantic accessibility capture. The existing AX5 Timeline case contains no plan, so it cannot execute the new branch. Filed as a WhereUI quick win. -- **The previous audit's documentation finding is closed.** `OnboardingGate` follows a side-effect-free demo preflight and still precedes every store/session-building step. `WhereLaunchSteps`, `OnboardingView`, `RootView`, `WhereLaunchTests`, and the WhereUI README now state that precise boundary. -- **Counts moved only where PR #307 moved them.** WhereUI is 285 source files, the repo is 704, and the six new planned-stay images take the LFS reference total from 484 to 490. Test files, image-suite files, suite assignments, and test bundles are unchanged. - ---- +**Date:** September 7, 2026 +**Reviewed source:** `5f65b9f0` (PR #311), fetched from `origin/main`. +**Prior audit:** September 6, 2026, report merged in PR #310 (`1f6162ba`), whose stated source coverage ended at PR #307 (`1e9c9289`). + +This report is derived from all 12 `TODOs.md` files and carries no actionable +items. The root [`TODOs.md`](TODOs.md) owns their format and placement. This +report is true as of the header date and source boundary, not as of later HEADs. + +## Method and changes since the previous audit + +The pass read every open area backlog, checked cited current source and test +seams, compared the covered source boundary with current main, and enumerated +tracked sources, tests, references, module docs, and manifest wiring. Unchanged +areas were checked against their current cited mechanisms and the unchanged +source diff; this was not a fresh line-by-line review of all 709 source files. +Exploratory directions remain decisions, rather than being promoted to bugs. + +The commit window is `1e9c9289..5f65b9f0`: PR #309's welcome feature, PR #310's +audit/documentation changes, and PR #311's welcome motion/reset changes. The +prior report missed #309 even though it was already an ancestor of that report's +merge. Both feature PR bodies were read to distinguish intent and reported +validation from what this pass itself verified. + +| Area | Prior report | September 7 state | +|---|---|---| +| Source / test-support / image-suite files | 704 / 369 / 49 | **709 / 371 / 49** | +| WhereCore | 128 / 83 | **129 / 84**; resolver and namesake tests added | +| WhereUI | 285 / 103 | **289 / 104**; four welcome types and model tests added | +| Reference images | 490 | **495**; five welcome references added, ten Appearance references updated | +| Module / test bundle count | 27 / 25 | **27 / 25**, unchanged | +| Inbox | Empty | **Empty**; no notes to promote or decline | +| Backlog | Some shipped/overstated claims still open | **Three entries archived**, two welcome findings filed; partial requests and citations corrected | +| Documentation | Reconciliation and snapshot-isolation overclaims | Current behavior and remaining exceptions explicitly documented | + +The three archived entries are logged-in/out scope modeling (shipped via +PR #150), local ingest/manual-sample fan-out (shipped August 4), and the stale +WhereCore documentation cluster corrected in this pass. The missing daily +summary and picker fan-outs remain open. The current-location UI request is +partly fulfilled by #309; its persistent-marker decision remains open. + +Other corrections distinguish actual consequences from inherited claims: +JournalKit's append test detects missing records but loses the original error; +WhereModel already has a typed log-store state; accessibility parse failure can +kill the current bundle's host, not every bundle's host; Ledger has 14 +test/support files; RegionKit's README already admits its decoding-test gap; +and the tool-portability failures do not all share one missing-Ruby cause. +Snapshot backlog headers now link to the root format instead of maintaining +separate instructions. + +## New-surface review + +**Verified OK in source and existing tests:** `CurrentRegionResolver` checks +recording authority before and after acquiring a fix, rejects `.other`, and +reuses the composition root's ingestor and attributor. The new test file covers +missing/outside fixes and revocation during an awaited request. +`LocationWelcomeModel` rejects cancellation, a disabled preference, and stale +request sequences before publishing; dismissal alone persists the region. +Its tests cover cancellation, disabling during lookup, replay after the DEBUG +reset, and suppression of the acknowledged region. Preferences and the report +mirror have existing round-trip/reset and visibility tests. + +The UI uses generated localized copy, typed region values, the existing planned +stay editor, an independent scrim layer, modal accessibility traits and +screen-change notifications. The stylesheet supplies separate arrival/departure +motion and a nonspatial Reduce Motion alternative. The reset is DEBUG-only and +clears only the acknowledged region. These are source-level checks, not a claim +that live transition timing or VoiceOver focus was exercised here. + +**Filed:** “Refresh the live-region welcome when the scene becomes active” +(Where P1), and “Cover the welcome overlay's scrolling and modal semantics” +(Where P2). Both are in [`Where/TODOs.md`](Where/TODOs.md); the report does not +duplicate their implementation proposals. The former needs a retained-tab +foreground reproduction; the latter records the fixed-frame, semantic-capture, +and iPad coverage gaps without claiming a screenshot proves broken rendering. ## Top findings -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 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 | -| 5 | CI / docs | The snapshot suite's "never parallelize this" warning is still absent from the file that now reads `parallelism: 4` | [`TODOs.md`](TODOs.md) P1 | -| 6 | Scripts | The retained-tool suites are CI's only Xcode-free gate and pass only on macOS — a hardcoded `126` exit status, a hermetic `PATH` that needs a system Ruby, and `sync-agents` unable to find Ruby at all | [`TODOs.md`](TODOs.md) P1 | -| 7 | WhereUI | Notification authorization is requested unprompted during launch, and all three preferences default to `true` on a fresh install | [`Where/TODOs.md`](Where/TODOs.md) P1 | -| 8 | WhereCore | Untracking a region hard-deletes the row, so re-aggregating a past year re-attributes its GPS days to `.other`; both shipped pickers reach it | [`Where/TODOs.md`](Where/TODOs.md) P1 | -| 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. - ---- +Pointers only; evidence and proposed fixes live in the backlog. + +| Area | Finding | Backlog | +|---|---|---| +| Bumper | Gregorian rule misses implicit `.current` | [Root P0](TODOs.md) | +| WhereCore | Daily summary absent from local fan-out | [Where P0](Where/TODOs.md) | +| PeriscopeCore | Pre-store-attach records absent from durable log | [Periscope P0](Shared/Periscope/TODOs.md) | +| WhereUI | Four production Gregorian-calendar defaults/helpers remain | [Where P1](Where/TODOs.md) | +| WhereCore | Picker fan-out and hard-deleting untracked regions | [Where P1](Where/TODOs.md) | +| WhereUI | Launch-time notification permission prompt | [Where P1](Where/TODOs.md) | +| SnapshotKit | Captured models shared across configurations | [SnapshotKit P1](Shared/SnapshotKit/TODOs.md) | +| WhereUI | Welcome lookup lacks foreground refresh trigger | [Where P1](Where/TODOs.md) | +| CI / scripts | Serial-axis documentation and Linux portability gaps | [Root P1](TODOs.md) | +| Repository | Missing group doc pairs for Where and Ledger | [Root P1](TODOs.md) | + +The nearest dated external task remains the benchmark organization cleanup, +after the saved **September 9** plan downgrade (two days from this audit). +`gh repo view` confirms the benchmark repository exists and is not archived. +Billing state, installed integrations, and downgrade scheduling were not +independently verified; no deletion or billing action was taken. ## Cross-cutting themes -The synthesis across items that no single item shows. - -### Same-day audits need a commit boundary, not only a date - -The existing header date was still current when PR #307 landed after PR #308. A date-only window would therefore have reported no new surface even though 43 files had changed. This pass names both boundary commits and treats `3daba8fb..1e9c9289` as the source window. Weekly dates remain useful for cadence; commit boundaries are the only unambiguous input when merges race the audit on the same day. - -### Coverage follows representative states, so conditional branches can hide inside a covered screen - -`PresenceTimelineList` remains one of the best-covered screens: its representative state uses the full screen-default matrix, its planned states use focused light/dark cases, and the joined state has a semantic accessibility capture. That still leaves the new AX-only layout branch unexecuted because the representative state contains no plan and the planned state contains no AX Dynamic Type configuration. "The screen has AX coverage" and "this AX branch has coverage" are different claims. The new backlog item names the branch and the smallest matrix addition that reaches it. - -### Documentation drift propagates by repetition - -The onboarding-gate item cited one stale comment, but the same "roots the trunk" claim had spread into `OnboardingView`, `RootView`, `WhereLaunchTests`, and the module README. Fixing only the cited line would have left four authoritative-looking copies to reintroduce it. The completed item now records the semantic statement worth preserving: the gate follows a side-effect-free preflight and precedes every step that can build a user world. - ---- - -## Per-module notes - -What each module was checked for and found clean, plus the trade-offs this pass accepted as deliberate. Open work is in the linked `TODOs.md`. - -### Repository tooling — dev scripts, retained Python/Ruby, CI - -Nothing shipped. **18 root commands**, `./test` at 722 lines, 7 Python + 5 Ruby retained modules with 21 test files under `Tools/Tests`. - -**Verified OK, by running it:** `./swiftformat --lint` reports 0 of 1,129 files needing formatting; `./shellcheck` is silent; `./attribution --check` reports the report up to date at 12 credits; `./sync-agents` refreshes the generated SnapshotKitTesting instructions; `./snapshot-shards check` validates 49 suites (13/15/18 planned + 3 intake); and all 64 retained Python tests pass. The 75-test Ruby suite has one environment-only failure: under the test's isolated `HOME`, Apple's `/usr/bin/python3` emits Xcode cache/FSEvents diagnostics before `snapshot-shards --help`; the same command exits cleanly and prints only usage outside that harness. No command contract changed in this window. - -**Files:** 18 root commands · 7 Python / 5 Ruby retained modules · 21 tool test files · Open: [`TODOs.md`](TODOs.md) - ---- - -### Bumper Bowling — architecture lint - -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. - -**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) - ---- - -### WhereCore - -Nothing shipped after the prior audit. PR #307 consumes existing typed forecast and planned-stay APIs without changing them. - -**Verified OK:** the new Timeline surface keeps adjacency presentation-only and passes typed `CalendarDay`, `Region`, and the injected Gregorian report calendar through the view boundary. Forecast math, persistence, and mutations remain in the existing model/Core seams. - -**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) - ---- - -### WhereUI - -PR #307 added four Timeline rendering helpers, expanded `PresenceTimelineList`, and added six references. Tests and snapshot-suite files are unchanged. - -**Verified OK:** joined stays require both the same typed region and next-day adjacency; nonconsecutive and different-region plans remain standalone. The estimate panel reuses the Calendar component and the same model actions. Representative joined, separated, and AX5 references show intact full-content sizing, rail order, forecast chrome, and Dynamic Type behavior for the non-plan state. No new code uses `Calendar.current`, raw SF Symbol strings, unlocalized user copy, store I/O, or ad-hoc design constants outside the owning stylesheet. - -**Re-verified:** the Timeline still maps a missing report to an empty stint list and renders the no-stays state while loading; its citations moved. The four production plus eight DEBUG-fixture Gregorian-calendar sites are unchanged. - -**Filed:** the joined planned-stay card's AX-only stacked layout has no AX5 image configuration. - -**Closed:** the stale onboarding-gate wording, across every repeated comment and the module README. - -**Accepted:** `WhereSession` remains exactly 636 lines — stable, but not being worked down. - -**Files:** 285 source / 103 test / 46 image-snapshot · README ✓ · AGENTS ✓ · Open: [`Where/TODOs.md`](Where/TODOs.md) - ---- - -### WhereCrashReporting - -Nothing shipped. **Files:** 3 source / 2 test · README ✓ · AGENTS ✓ · Open: nothing filed - ---- - -### PeriscopeCore, PeriscopeUI, PeriscopeTools - -Nothing shipped. All 20 items still open; every key citation verified, one drifted (`PeriscopeToolsSnapshotTests` wiring is `Project.swift:627-633`). - -**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) - ---- - -### Flyover - -Nothing shipped after the prior audit. 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) - ---- - -### Inspector - -Nothing shipped; all four items remain open and their citations hold. 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). - -**Files:** 23 source / 14 test / 1 image source, 4 references · README ✓ · AGENTS ✓ · Open: [`Shared/Inspector/TODOs.md`](Shared/Inspector/TODOs.md) - ---- - -### SnapshotKit & SnapshotKitTesting - -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; 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. - -**Refreshed here:** the settle-floor split remains **39** addressable configurations, and the dated reference counts in `AGENTS.md` now reflect the six additions from PR #307: 490 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) - ---- - -### LifecycleKit & LifecycleKitUI - -Nothing shipped. The single P2 stands, citations unchanged. - -**Files:** LifecycleKit 8/10 · LifecycleKitUI 6/4 · README ✓ · AGENTS ✓ · Open: [`Shared/LifecycleKit/TODOs.md`](Shared/LifecycleKit/TODOs.md) (LifecycleKitUI's items live in LifecycleKit's file by design) - ---- - -### Broadway (BroadwayCore, BroadwayUI, BroadwayCatalog) - -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) - ---- - -### Ledger, LedgerCore - -Nothing shipped after the prior audit; all three P2s and their citations are unchanged. - -**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) - ---- - -### RegionKit & RegionViewer - -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) - ---- - -### WhereIntents, WhereWidgets, WhereShareExtension, Where app - -Nothing shipped in these targets after the prior audit. PR #307 is contained in WhereUI. - -**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. - -**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) - ---- - -### CreditKit, JournalKit, TestHostSupport, StuffTestHost - -**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 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 - -**StuffTestHost:** unchanged. **Files:** 2/0 · nothing open - ---- - -## Limitations - -- **Mostly static, partly executed.** This local macOS pass ran agent sync, SwiftFormat, ShellCheck, attribution, the retained tool suites, and the snapshot-shard validator. `./test` was skipped because this PR changes Markdown and comments only; no executable Swift or rendered copy changed. The architecture lint and simulator suites were likewise not run. -- **One retained Ruby contract test failed because of the automation sandbox, not repository output.** Its deliberately isolated `HOME` makes Apple's `/usr/bin/python3` emit Xcode cache/FSEvents diagnostics before `snapshot-shards --help`. Running that command normally in the same checkout is clean. The 64 Python tests pass. -- **The "the Gregorian rule finds nothing" conclusion rests on unchanged source and green main CI**, not on running the lint here. The mechanism (the rule's filter plus its one-sided mutation test) remains sufficient on its own; CI is corroboration. -- **Snapshot review was representative, not exhaustive.** Joined, different-region, and AX5 Timeline references were inspected. Reference counts come from full file enumeration, but all 33 modified and 6 added images from PR #307 were not opened individually. -- **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. - ---- - -## Modules reviewed - -### SPM library targets - -| Module | Path | Source | Test | Image | README | AGENTS | -|--------|------|-------:|-----:|------:|:------:|:------:| -| CreditKit | `Shared/CreditKit/` | 2 | 3 | — | ✓ | ✓ | -| JournalKit | `Shared/JournalKit/` | 2 | 3 | — | ✓ | ✓ | -| LifecycleKit | `Shared/LifecycleKit/` | 8 | 10 | — | ✓ | ✓ | -| LifecycleKitUI | `Shared/LifecycleKitUI/` | 6 | 4 | — | ✓ | ✓ | -| SnapshotKit | `Shared/SnapshotKit/` | 8 | 3 | — | ✓ | ✓ | -| SnapshotKitTesting | `Shared/SnapshotKitTesting/` | 16 | 16 | — | ✓ | ✓ | -| Inspector | `Shared/Inspector/` | 23 | 14 | 1 | ✓ | ✓ | -| Flyover | `Shared/Flyover/` | 54 | 14 | 1 | ✓ | ✓ | -| TestHostSupport | `Shared/TestHostSupport/` | 1 | 0 | — | ✓ | ✓ | -| BroadwayCore | `Shared/Broadway/BroadwayCore/` | 17 | 10 | — | ✓ | ✓ | -| BroadwayUI | `Shared/Broadway/BroadwayUI/` | 6 | 4 | — | ✓ | ✓ | -| PeriscopeCore | `Shared/Periscope/PeriscopeCore/` | 38 | 33 | — | ✓ | ✓ | -| PeriscopeUI | `Shared/Periscope/PeriscopeUI/` | 1 | 2 | — | ✓ | ✓ | -| PeriscopeTools | `Shared/Periscope/PeriscopeTools/` | 27 | 27 | 1 | ✓ | ✓ | -| RegionKit | `Where/RegionKit/` | 15 | 10 | — | ✓ | ✓ | -| WhereCore | `Where/WhereCore/` | 128 | 83 | — | ✓ | ✓ | -| WhereUI | `Where/WhereUI/` | 285 | 103 | 46 | ✓ | ✓ | -| WhereIntents | `Where/WhereIntents/` | 15 | 9 | — | ✓ | ✓ | -| WhereCrashReporting | `Where/WhereCrashReporting/` | 3 | 2 | — | ✓ | ✓ | -| LedgerCore | `Ledger/LedgerCore/` | 16 | 14 | — | ✓ | ✓ | - -### Tuist app / extension targets - -| Target | Path | Source | Test | README | AGENTS | -|--------|------|-------:|-----:|:------:|:------:| -| Where | `Where/Where/` | 8 | 4 | ✓ | ✓ | -| WhereWidgets | `Where/WhereWidgets/` | 7 | 0 | ✓ | ✓ | -| WhereShareExtension | `Where/WhereShareExtension/` | 5 | 0 | ✓ | ✓ | -| RegionViewer | `Where/RegionViewer/` | 1 | 0 | ✓ | ✓ | -| Ledger | `Ledger/Ledger/` | 8 | 0 | ✓ | ✓ | -| StuffTestHost | `Shared/StuffTestHost/` | 2 | 0 | ✓ | ✓ | -| BroadwayCatalog | `Shared/Broadway/BroadwayCatalog/` | 2 | 1 | ✓ | ✓ | - -**Totals:** 704 source · 369 test · 49 image-snapshot Swift files across shipped targets (plus 4 Bumper rule/test sources and 2 unwired prototype sources). **490** LFS-backed reference images (479 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. - -**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 the September 6, 2026 audit at `3daba8fb` - -| Area | Prior same-day audit | Current state | -|------|----------------------|---------------| -| New surface | Through PR #308's base | **PR #307** — joined continuous planned Timeline stays and a trailing Estimated Time panel | -| File count | 700 source / 369 test / 49 image-snapshot | **704 / 369 / 49** — four focused WhereUI rendering helpers added | -| Backlog movement | 12 files | **1 filed, 1 closed, 2 citations refreshed** — joined-card AX5 coverage filed; onboarding-gate wording closed; Timeline loading/Gregorian citations moved | -| Image coverage | 4 bundles, 484 references, 49 suites | **4 bundles, 490 references, 49 suites** — six focused planned-stay references; shard plan remains 13/15/18/3 | -| Test bundles | 25 | **25**, unchanged | -| Documentation | Onboarding gate still described as the trunk root | **Preflight/gate boundary corrected** in source docs, tests, and the WhereUI README | -| Backlog files | 12 `TODOs.md` | **12**, unchanged | +- **A report date does not identify its source coverage.** Use the explicit + covered commit, including same-day merges the prior report omitted. +- **Passing tests and good coverage are different claims.** The journal test + catches loss despite poor diagnostics; welcome model coverage does not prove + foreground wiring, scroll reachability, or live motion. +- **Describe present behavior separately from intended invariants.** The + corrected reconciliation and snapshot docs now name the limitations that + remain filed. Documentation repairs do not imply runtime fixes. +- **Keep historical measurements historical.** Current references are 495; + the 260-reference settle measurements still require remeasurement. The + addressable raised-floor set remains 39 configurations, not a new timing result. + +## Module inventory and Verified OK + +Counts are tracked `.swift` files under each module's `Sources/`, `Tests/`, and +`SnapshotTests/`; tests include fixtures/support files and do not equal test +cases. All **27 leaf modules** have both README.md and AGENTS.md. The following +checks are static unless explicitly identified as executed. + +| Module | Source | Test/support | Image suite | Verified OK / bounded result | +|---|---:|---:|---:|---| +| [Ledger](Ledger/Ledger/README.md) | 8 | 0 | — | Native macOS app and hostless LedgerCore scheme remain separate from iOS; app has no test bundle by design. | +| [LedgerCore](Ledger/LedgerCore/README.md) | 16 | 14 | — | Explicit refresh-generation guard and scripted API/Keychain seams retained; 14 test/support files, with the three filed namesake gaps. | +| [BroadwayCatalog](Shared/Broadway/BroadwayCatalog/README.md) | 2 | 1 | — | Catalog target is in the iOS scheme; its placeholder and empty test are still accurately filed, not counted as behavior coverage. | +| [BroadwayCore](Shared/Broadway/BroadwayCore/README.md) | 17 | 10 | — | Cache and unchanged-value invalidation sites match the existing backlog; manifest remains free of app dependencies. | +| [BroadwayUI](Shared/Broadway/BroadwayUI/README.md) | 6 | 4 | — | Depends downward on BroadwayCore; nested-observer TODO remains at the cited source. | +| [CreditKit](Shared/CreditKit/README.md) | 2 | 3 | — | Foundation-only value layer; attribution report passes at 12 credits. Generator slug issue remains localized to parsing. | +| [Flyover](Shared/Flyover/README.md) | 54 | 14 | 1 | Manifest has no Where dependency; 14 test/support files and one image suite remain. Canvas math coverage is distinct from interactive coverage. | +| [Inspector](Shared/Inspector/README.md) | 23 | 14 | 1 | One image suite has four references; relationship branch and three silent fetch defaults still match the backlog. | +| [JournalKit](Shared/JournalKit/README.md) | 2 | 3 | — | Concurrent append test checks recovered count, uniqueness, and writer order; swallowed error diagnostic is the actual remaining gap. | +| [LifecycleKit](Shared/LifecycleKit/README.md) | 8 | 10 | — | Duplicate-ID precondition is present; existing test files still cover typed launch and cancellation. Duplicate-ID exit-test gap remains. | +| [LifecycleKitUI](Shared/LifecycleKitUI/README.md) | 6 | 4 | — | Gate-registration uniqueness guard remains present; view-level splash ownership is unchanged. | +| [PeriscopeCore](Shared/Periscope/PeriscopeCore/README.md) | 38 | 33 | — | Span accessors downcast rather than store parallel span fields; journal still installs with the store. No new source in the window. | +| [PeriscopeTools](Shared/Periscope/PeriscopeTools/README.md) | 27 | 27 | 1 | Hierarchy count/query asymmetry is explicitly pinned; 20 hosting-only assertions across 10 files and two image references remain. | +| [PeriscopeUI](Shared/Periscope/PeriscopeUI/README.md) | 1 | 2 | — | Single SwiftUI environment adapter imports only PeriscopeCore and SwiftUI; test/support inventory unchanged. | +| [SnapshotKit](Shared/SnapshotKit/README.md) | 8 | 3 | — | Shippable matrix remains separate from comparison engine; docs now disclose that the runner shares captured models across configurations. | +| [SnapshotKitTesting](Shared/SnapshotKitTesting/README.md) | 16 | 16 | — | Provider duplicate guard, cancellation outcome, parse failure paths and config loop match filed issues; shard plan validates all 49 suites. | +| [StuffTestHost](Shared/StuffTestHost/README.md) | 2 | 0 | — | UIKit shell delegates test-window setup to TestHostSupport; no WhereCore import or new source. | +| [TestHostSupport](Shared/TestHostSupport/README.md) | 1 | 0 | — | UIKit/Objective-C hosting seam remains app-independent; host smoke contract is exercised from LifecycleKit tests. | +| [RegionKit](Where/RegionKit/README.md) | 15 | 10 | — | GeoJSON decoding gap is honestly documented; source still throws for unsupported geometry. No new source in the window. | +| [RegionViewer](Where/RegionViewer/README.md) | 1 | 0 | — | Bundled per-region data description remains correct; missing Broadway root is still filed in Where. | +| [Where](Where/Where/README.md) | 8 | 4 | — | Runtime selection and intent handoff remain in the app shell; new welcome work did not add a second store or runtime. | +| [WhereCore](Where/WhereCore/README.md) | 129 | 84 | — | New resolver rechecks recording authority after suspension and uses the injected attributor; revoked-authorization regression exists. | +| [WhereCrashReporting](Where/WhereCrashReporting/README.md) | 3 | 2 | — | Capture SDK stays behind the dedicated adapter target; no source, dependency, or test changes in this window. | +| [WhereIntents](Where/WhereIntents/README.md) | 15 | 9 | — | Intent services remain injected; four shortcuts and the perform-glue testing limitation match source. No new source in the window. | +| [WhereShareExtension](Where/WhereShareExtension/README.md) | 5 | 0 | — | Compose model still builds pending evidence; no test bundle was silently added. Shared form/testing gaps remain filed. | +| [WhereUI](Where/WhereUI/README.md) | 289 | 104 | 46 | Welcome cancellation/preference guards, dismissal-only persistence, localized controls, and Reduce Motion tokens have source/test evidence; see new-surface review below. | +| [WhereWidgets](Where/WhereWidgets/README.md) | 7 | 0 | — | Provider retains midnight reload policy and reads published stores; no direct new service or welcome dependency. | + +**Totals:** 709 source, 371 test/support, and 49 image-suite Swift files. +The inventory excludes two unwired Periscope journal-benchmark sources and +four Bumper rule/test files. `Package.swift` declares 20 library targets; +`Project.swift` declares seven app/extension targets and 25 test bundles: +20 unit bundles in `Stuff-iOS-Tests`, LedgerCoreTests in `Ledger-macOS-Tests`, +and four image bundles in `StuffSnapshotTests`. Neither manifest changed in +the reviewed window. + +**References:** 484 WhereUI, five Flyover, four Inspector, two PeriscopeTools, +for 495 total. The 49 addressable suites retain assignments 13 / 15 / 18 plus +three on the intake shard. The shard validator passes. WhereCore's basename +coverage proxy remains 60 sources without a namesake test among 129 sources; +logging types and record shells mean that is not a list of 60 untested behaviors. + +**Group docs:** Broadway and Periscope have both docs; Where lacks its group +README and Ledger lacks both group docs. Their leaf modules are complete. +The existing root item remains open, with its stale 28-leaf count corrected. + +**Bumper and tooling:** the ten `where.*` rules and eleven source-rule test +functions remain; the explicit-calendar filter and mutation fixtures still +miss the 12 implicit calendar sites (four production, eight DEBUG fixtures). +`component_boundary` and `forbidden_import` have mutation tests; the two other +graph assertions remain filed for missing mutation coverage. Source review +establishes that mechanism; no fresh architecture run is claimed. The 18 root +commands, retained tooling layer, and CI configuration had no executable change. + +## Verification and limitations + +- `./swiftformat --lint` — passed, 0 of 1,136 files require formatting; + 125 skipped. The sandbox prevented writing its optional cache, without + affecting the lint result. +- `./shellcheck` — passed. +- `./attribution --check` — passed, 12 credits current. +- `./snapshot-shards check` — passed, all 49 suites assigned. +- `./sync-agents` — passed after instruction edits; generated mirrors remain + ignored. `git diff --check` — passed. +- This run is on **macOS**, but the audit remains **static analysis** plus the + supported host checks above. The skill's Linux limitations still apply to + Linux runs: no Tuist, Xcode, simulator, or runtime validation is implied. +- `./test`, architecture execution, simulator/image suites, and the retained + Python/Ruby suites were skipped because this change is Markdown and Swift + comments only. There are no executable, matrix, reference, or rendered-copy + changes. Prior PR test counts were read as historical evidence, not reported + as this run's results. The prior Ruby sandbox failure was not rerun or closed. +- No fresh screenshots or live animations were inspected. Existing visual + defects and quarantines remain open; the welcome's focus, foreground + lifecycle, and motion need device/simulator validation when addressed. +- CloudKit readiness, passive background delivery, multi-process journals, + runtime diagnostic routing, and Ledger's live API/Keychain behavior were not + exercised. Historical timing/spike conclusions remain conditional on their + stated measurement environment.