Independent review of ArjenSchwarz/transit#202 at the exact final head c24f9f9118127d38e7f01f83e989f0610f238ae4.
275ac5cfc063f1196041470fba115448d8f2dd29 and its final head is c24f9f9118127d38e7f01f83e989f0610f238ae4.completionDate and lastStatusChangeDate reject null relative, from, and to fields before decoding, while omitted bounds remain open-ended.make test-quick, make lint, and git diff --check passed; fresh claude-review CI passed on the exact head.Ready to push
The T-1644 change is ready to push from a correctness perspective: the exact PR head is verified, the nested-null contract is implemented for both date filters, the focused regression suites pass, macOS unit tests and lint pass, the qualifying GitHub check is green, and the PR has zero review threads. The full iOS suite reported three unrelated UI failures; one was reproduced on the PR base and the other two are outside the changed intent/test paths. This remains a validation caveat to track separately, not a T-1644 blocker.
9f5512d T-1644: Add nested date filter null regressions 65ba778 T-1644: Reject nested date filter nulls c8ac752 T-1644: Strengthen nested date filter regressions c24f9f9 T-1644: Apply review formatting fixes Query task filters accept date ranges such as {"from": "2026-01-01"}. Before this fix, a nested value such as {"from": null} could be decoded as an absent value and accidentally broaden the query. The implementation now checks the raw JSON and returns INVALID_INPUT with a field-specific hint whenever a supported nested date-filter field is explicitly null.
Explicit null is different from omitting a key. Omission keeps the existing open-ended range behavior; null is rejected so callers cannot silently get a less restrictive query than they requested.
QueryTasksIntent.parseInput already parses the input through JSONSerialization before decoding the typed request. The change uses that raw object to inspect completionDate and lastStatusChangeDate, requires each present nested filter to be a JSON object, and rejects NSNull for relative, from, or to. Valid objects then continue through the existing Codable path.
The validation is intentionally local to the two date-filter keys. It preserves existing error handling for malformed shapes and existing date-range semantics, while tests cover both filters, all three fields, omitted bounds, malformed values, and the no-broadening guarantee.
DateRangeFilter stores optional relative, from, and toDate, with toDate mapped from the external to key. Swift decoding of optionals cannot distinguish omitted keys from explicit JSON null, so raw-object validation is the correct layer for preserving that distinction. The guard rejects scalar/array nested shapes with Expected valid JSON object and rejects explicit null with Filter "key.field" must not be null. Values with only one bound still flow to DateFilterHelpers, which models the missing side as open. The diff adds no production path outside QueryTasksIntent and no schema or persistence change.
Transit/Transit/Intents/QueryTasksIntent.swift
Why it matters. Preserves the API distinction between an omitted optional bound and an explicitly null bound, preventing silently broadened task queries.
What to look at. QueryTasksIntent.parseInput: nested date-filter validation
Transit/TransitTests/QueryTasksIntentNullFilterTests.swift
Why it matters. The regression surface is symmetric across completion and last-status-change dates and across relative/from/to.
What to look at. Six exact nested-null cases plus malformed-shape and no-broadening cases
Transit/TransitTests/QueryTasksIntentDateFilterTests.swift
Why it matters. Rejecting null must not change the valid behavior of omitting one side of a date range.
What to look at. completionDateFromOnlyKeepsOpenUpperBound and completionDateToOnlyKeepsOpenLowerBound
specs/bugfixes/query-tasks-nested-date-filter-null-fields/report.md
Why it matters. The behavior change and validation evidence remain discoverable for future callers and reviewers.
What to look at. Bugfix report and CHANGELOG entry
This keeps the typed model's optional properties and existing open-bound behavior unchanged while allowing the parser to distinguish explicit null from omission. The decision is inferred from the existing JSONSerialization-then-Codable flow and the narrow production diff.
A present date-filter value must be a JSON object. Scalars and arrays return Expected valid JSON object rather than reaching a decoder failure or being ignored, preserving fail-closed input handling.
The full iOS run exposed three UI failures outside the changed intent/date-filter paths. No source or test code was changed during this review; the failures are recorded as validation caveats and should be tracked separately.
(inferred — not stated by the author.)| Severity | Area | Finding | Resolution |
|---|---|---|---|
| low | Full iOS simulator validation | <code>make test</code> completed with 1,166 passed and 3 failed UI tests: <code>TransitUITests.testClearAll()</code>, <code>TransitUITests.testEditViewPreservesTaskMilestone()</code>, and <code>DataMaintenanceUITests.testDataMaintenanceGoldenPath()</code>. The failures are outside the changed QueryTasksIntent/date-filter paths. A PR-base reproduction using the corrected DataMaintenance selector reproduced the DataMaintenance failure on base. | Skipped as a T-1644 blocker because the relevant intent tests, macOS suite, lint, diff check, and fresh head CI are green; track the pre-existing/orthogonal UI failures separately. |
Click to expand.
diff --git a/CHANGELOG.md b/CHANGELOG.mdindex 321243e..275e494 100644--- a/CHANGELOG.md+++ b/CHANGELOG.md@@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Fixed +- `QueryTasksIntent` now rejects explicit JSON `null` in nested `completionDate` and `lastStatusChangeDate` fields (`relative`, `from`, and `to`) before Codable decoding can erase presence. Malformed nested date-filter shapes retain the established `INVALID_INPUT` contract, omitted fields remain unchanged, and regressions cover both filters and no-broadening behavior (T-1644). - MCP `update_task_status` now returns the exact comment created by its atomic status-plus-comment mutation (T-1823). `TaskService.updateStatus` propagates the created `Comment` to the handler for direct serialization instead of refetching every task comment and taking the last `creationDate`; a future-dated existing or concurrently imported comment can no longer replace the mutation result. If the atomic save fails, the newly inserted comment is deleted before the task state is rolled back so a later save cannot resurrect it. Regressions verify exact UUID propagation, future-clock-skew behavior, and service/MCP failure recovery. - Cross-device task and milestone promotion now re-probes committed SwiftData state after display-ID allocation and skips assignment when a peer has already supplied a permanent ID (T-2020). Missing or unreadable records fail closed, save-failure recovery preserves peer-merged values, and deterministic two-context tests pin both record types. Because SwiftData has no field-level compare-and-set, this closes the peer-merge-during-await window but cannot prevent two devices whose final local probes both precede either CloudKit merge from racing; the limitation and owner-reservation alternative are documented.
diff --git a/Transit/Transit/Intents/QueryTasksIntent.swift b/Transit/Transit/Intents/QueryTasksIntent.swiftindex be582c2..f470012 100644--- a/Transit/Transit/Intents/QueryTasksIntent.swift+++ b/Transit/Transit/Intents/QueryTasksIntent.swift@@ -236,6 +236,15 @@ struct QueryTasksIntent: AppIntent { if let nullKey = object.first(where: { $0.value is NSNull })?.key { return .failure(.invalidInput(hint: "Filter \"\(nullKey)\" must not be null")) }+ for filterKey in ["completionDate", "lastStatusChangeDate"] {+ guard let rawDateFilter = object[filterKey] else { continue }+ guard let dateFilter = rawDateFilter as? [String: Any] else {+ return .failure(.invalidInput(hint: "Expected valid JSON object"))+ }+ for fieldKey in ["relative", "from", "to"] where dateFilter[fieldKey] is NSNull {+ return .failure(.invalidInput(hint: "Filter \"\(filterKey).\(fieldKey)\" must not be null"))+ }+ } guard let filters = try? JSONDecoder().decode(QueryFilters.self, from: data) else { return .failure(.invalidInput(hint: "Expected valid JSON object")) }@@ -386,7 +395,6 @@ struct QueryTasksIntent: AppIntent { toDateString: filter.toDate ) }- } // swiftlint:enable type_body_length
diff --git a/Transit/TransitTests/QueryTasksIntentDateFilterTests.swift b/Transit/TransitTests/QueryTasksIntentDateFilterTests.swiftindex f911994..c52c1df 100644--- a/Transit/TransitTests/QueryTasksIntentDateFilterTests.swift+++ b/Transit/TransitTests/QueryTasksIntentDateFilterTests.swift@@ -194,6 +194,64 @@ struct QueryTasksIntentDateFilterTests { #expect(parsed.first?["name"] as? String == "Today Done") } + @Test func completionDateFromOnlyKeepsOpenUpperBound() throws {+ let svc = try makeServices()+ let project = makeProject(in: svc.context)+ let calendar = Calendar.current+ let today = calendar.startOfDay(for: Date.now)+ let fromDate = calendar.date(byAdding: .day, value: -1, to: today)!+ let beforeFrom = calendar.date(byAdding: .day, value: -2, to: today)!++ makeTask(in: svc.context, project: project, name: "Before From", displayId: 1, completionDate: beforeFrom)+ makeTask(in: svc.context, project: project, name: "At From", displayId: 2, completionDate: fromDate)+ makeTask(in: svc.context, project: project, name: "After From", displayId: 3, completionDate: today)++ let dateFormatter = DateFormatter()+ dateFormatter.dateFormat = "yyyy-MM-dd"+ dateFormatter.locale = Locale(identifier: "en_US_POSIX")+ dateFormatter.timeZone = TimeZone.current+ let fromDateString = dateFormatter.string(from: fromDate)++ let result = QueryTasksIntent.execute(+ input: "{\"completionDate\":{\"from\":\"\(fromDateString)\"}}",+ projectService: svc.project,+ taskService: svc.task,+ milestoneService: svc.milestone+ )++ let names = Set(try parseJSONArray(result).compactMap { $0["name"] as? String })+ #expect(names == ["At From", "After From"])+ }++ @Test func completionDateToOnlyKeepsOpenLowerBound() throws {+ let svc = try makeServices()+ let project = makeProject(in: svc.context)+ let calendar = Calendar.current+ let today = calendar.startOfDay(for: Date.now)+ let toDate = calendar.date(byAdding: .day, value: -1, to: today)!+ let beforeTo = calendar.date(byAdding: .day, value: -2, to: today)!++ makeTask(in: svc.context, project: project, name: "Before To", displayId: 1, completionDate: beforeTo)+ makeTask(in: svc.context, project: project, name: "At To", displayId: 2, completionDate: toDate)+ makeTask(in: svc.context, project: project, name: "After To", displayId: 3, completionDate: today)++ let dateFormatter = DateFormatter()+ dateFormatter.dateFormat = "yyyy-MM-dd"+ dateFormatter.locale = Locale(identifier: "en_US_POSIX")+ dateFormatter.timeZone = TimeZone.current+ let toDateString = dateFormatter.string(from: toDate)++ let result = QueryTasksIntent.execute(+ input: "{\"completionDate\":{\"to\":\"\(toDateString)\"}}",+ projectService: svc.project,+ taskService: svc.task,+ milestoneService: svc.milestone+ )++ let names = Set(try parseJSONArray(result).compactMap { $0["name"] as? String })+ #expect(names == ["Before To", "At To"])+ }+ @Test func invalidDateFilterReturnsInvalidInputError() throws { let svc = try makeServices()
diff --git a/Transit/TransitTests/QueryTasksIntentNullFilterTests.swift b/Transit/TransitTests/QueryTasksIntentNullFilterTests.swiftindex 7f9cfb2..de2eb62 100644--- a/Transit/TransitTests/QueryTasksIntentNullFilterTests.swift+++ b/Transit/TransitTests/QueryTasksIntentNullFilterTests.swift@@ -63,7 +63,7 @@ struct QueryTasksIntentNullFilterTests { return svc } - private func expectInvalidInput(_ input: String) throws {+ private func expectInvalidInput(_ input: String, expectedHint: String? = nil) throws { let svc = try makeSeededServices() let result = QueryTasksIntent.execute( input: input, projectService: svc.project, taskService: svc.task,@@ -71,6 +71,9 @@ struct QueryTasksIntentNullFilterTests { ) let parsed = try parseJSON(result) #expect(parsed["error"] as? String == "INVALID_INPUT", "input \(input) should be rejected")+ if let expectedHint {+ #expect(parsed["hint"] as? String == expectedHint, "input \(input) returned an unstable error hint")+ } } // MARK: - Explicit Null Rejection@@ -104,6 +107,92 @@ struct QueryTasksIntentNullFilterTests { try expectInvalidInput("{\"lastStatusChangeDate\":null}") } + @Test func nullCompletionDateRelativeIsRejected() throws {+ try expectInvalidInput(+ "{\"completionDate\":{\"relative\":null,\"from\":\"2026-01-01\",\"to\":\"2026-01-31\"}}",+ expectedHint: "Filter \"completionDate.relative\" must not be null"+ )+ }++ @Test func nullCompletionDateFromIsRejected() throws {+ try expectInvalidInput(+ "{\"completionDate\":{\"from\":null,\"to\":\"2026-01-31\"}}",+ expectedHint: "Filter \"completionDate.from\" must not be null"+ )+ }++ @Test func nullCompletionDateToIsRejected() throws {+ try expectInvalidInput(+ "{\"completionDate\":{\"from\":\"2026-01-01\",\"to\":null}}",+ expectedHint: "Filter \"completionDate.to\" must not be null"+ )+ }++ @Test func nullLastStatusChangeDateRelativeIsRejected() throws {+ try expectInvalidInput(+ "{\"lastStatusChangeDate\":{\"relative\":null,\"from\":\"2026-01-01\",\"to\":\"2026-01-31\"}}",+ expectedHint: "Filter \"lastStatusChangeDate.relative\" must not be null"+ )+ }++ @Test func nullLastStatusChangeDateFromIsRejected() throws {+ try expectInvalidInput(+ "{\"lastStatusChangeDate\":{\"from\":null,\"to\":\"2026-01-31\"}}",+ expectedHint: "Filter \"lastStatusChangeDate.from\" must not be null"+ )+ }++ @Test func nullLastStatusChangeDateToIsRejected() throws {+ try expectInvalidInput(+ "{\"lastStatusChangeDate\":{\"from\":\"2026-01-01\",\"to\":null}}",+ expectedHint: "Filter \"lastStatusChangeDate.to\" must not be null"+ )+ }++ @Test func malformedNestedDateFilterFieldsAreRejected() throws {+ let inputs = [+ "{\"completionDate\":{\"relative\":false}}",+ "{\"completionDate\":{\"from\":42}}",+ "{\"completionDate\":{\"to\":[]}}",+ "{\"lastStatusChangeDate\":{\"relative\":{}}}",+ "{\"lastStatusChangeDate\":{\"from\":false}}",+ "{\"lastStatusChangeDate\":{\"to\":42}}"+ ]++ for input in inputs {+ try expectInvalidInput(input, expectedHint: "Expected valid JSON object")+ }+ }++ @Test func scalarCompletionDateFilterIsRejected() throws {+ try expectInvalidInput("{\"completionDate\":\"today\"}", expectedHint: "Expected valid JSON object")+ }++ @Test func arrayLastStatusChangeDateFilterIsRejected() throws {+ try expectInvalidInput("{\"lastStatusChangeDate\":[]}", expectedHint: "Expected valid JSON object")+ }++ @Test func malformedNestedDateFiltersDoNotBroadenResults() throws {+ let inputs = [+ (+ "{\"completionDate\":{\"from\":null,\"to\":\"2026-01-31\"}}",+ "Filter \"completionDate.from\" must not be null"+ ),+ (+ "{\"lastStatusChangeDate\":{\"from\":\"2026-01-01\",\"to\":null}}",+ "Filter \"lastStatusChangeDate.to\" must not be null"+ ),+ ("{\"completionDate\":\"today\"}", "Expected valid JSON object"),+ ("{\"lastStatusChangeDate\":[]}", "Expected valid JSON object")+ ]++ for (input, expectedHint) in inputs {+ // Parsing the response as an object proves the malformed request did not+ // become a successful (and potentially broader) task array.+ try expectInvalidInput(input, expectedHint: expectedHint)+ }+ }+ @Test func nullMilestoneIsRejected() throws { try expectInvalidInput("{\"milestone\":null}") }
diff --git a/specs/bugfixes/query-tasks-nested-date-filter-null-fields/report.md b/specs/bugfixes/query-tasks-nested-date-filter-null-fields/report.mdnew file mode 100644index 0000000..dc8419f--- /dev/null+++ b/specs/bugfixes/query-tasks-nested-date-filter-null-fields/report.md@@ -0,0 +1,111 @@+# Bugfix Report: Query Tasks Nested Date Filter Null Fields++**Date:** 2026-08-02+**Status:** Fixed+**Ticket:** T-1644++## Description of the Issue++`QueryTasksIntent` rejects explicit JSON `null` values for top-level filters, but it accepts explicit `null` values inside the `completionDate` and `lastStatusChangeDate` filter objects. `JSONDecoder` decodes those present nulls into optional `nil` values, making them indistinguishable from omitted fields.++For example, `{"completionDate":{"from":null}}` can become an open-ended date filter rather than a malformed request. A missing bound can broaden the result set compared with the caller's intended bounded query.++**Reproduction steps:**+1. Seed tasks with dates inside and outside a bounded date range.+2. Call `QueryTasksIntent` with `{"completionDate":{"from":null}}` or the equivalent `lastStatusChangeDate` payload.+3. Observe that the nested null is not rejected before date-filter parsing.++**Impact:** Medium correctness issue for Shortcut/JSON callers. Malformed date filters can be accepted with altered or broader query semantics. No task data is mutated.++## Investigation Summary++The investigation followed the project's systematic debugging workflow: inspect the input boundary, trace decoded values through validation and filtering, and compare the behavior with the existing top-level null contract.++- **Symptoms examined:** Nested `relative`, `from`, or `to` nulls are accepted while top-level null filters return `INVALID_INPUT`; malformed date-filter shapes must not turn into an unfiltered query.+- **Code inspected:** `Transit/Transit/Intents/QueryTasksIntent.swift`, `QueryTasksIntentNullFilterTests.swift`, `QueryTasksIntentDateFilterTests.swift`, `DateFilterHelpers.swift`, `TransitShortcuts.swift`, and all direct `QueryTasksIntent.execute` callers.+- **Hypotheses tested:** The issue is not in date range calculation or task filtering. It is caused by presence information being erased during Codable decoding. Non-object nested filters already fail through JSONDecoder and must retain their existing generic `INVALID_INPUT` contract.++## Discovered Root Cause++`QueryTasksIntent.parseInput` checks only the top-level `[String: Any]` dictionary for `NSNull` values, then decodes into `QueryFilters`. The nested `DateRangeFilter` fields are optional strings, so synthesized `decodeIfPresent` maps a present JSON null to `nil`, exactly as it does for an omitted field. `dateRange(from:)` then passes those nil values to `DateFilterHelpers.parseDateFilter`, which can interpret a missing bound as an open-ended range.++**Defect type:** Missing presence-aware validation / data transformation issue.++**Why it occurred:** The original null guard was added at the filter-object level but did not recurse into the two nested date-filter dictionaries before decoding.++**Contributing factors:** Codable's optional decoding intentionally erases the distinction between omitted and explicit null values. The date filter API intentionally allows omitted bounds, so the erased value can be interpreted as valid input with different query semantics.++## Resolution for the Issue++**Changes made:**+- `Transit/Transit/Intents/QueryTasksIntent.swift` — Added a raw JSON preflight for `completionDate` and `lastStatusChangeDate`. Each recognized date filter must be an object, and `relative`, `from`, and `to` are checked for `NSNull` before `JSONDecoder` runs. Non-object shapes retain the existing `Expected valid JSON object` error contract, while unknown nested keys remain ignored.+- `Transit/TransitTests/QueryTasksIntentNullFilterTests.swift` — Added six nested-null regressions with exact error-hint assertions, malformed nested-field type regressions, malformed object-shape regressions, and a no-broadening regression covering both filters.+- `Transit/TransitTests/QueryTasksIntentDateFilterTests.swift` — Added query-level regressions proving omitted `from` and `to` bounds preserve open-ended range semantics.+- `CHANGELOG.md` — Added the T-1644 fixed entry under `Unreleased`.++**Approach rationale:** The raw `JSONSerialization` object is already available for top-level null validation, so extending that preflight preserves the existing Codable model and date parsing semantics while retaining presence information only where required. Fixed field iteration order makes the nested null error deterministic.++**Alternatives considered:**+- Add custom `Decodable` presence tracking to `DateRangeFilter` — rejected as a larger change than the existing raw-object validation pattern.+- Reject every null-valued unknown nested key — rejected to avoid changing Codable's established behavior for ignored fields.+- Treat nested null as omitted — rejected because it can widen a bounded date query.++## Regression Test++**Test files:**+- `Transit/TransitTests/QueryTasksIntentNullFilterTests.swift`+- `Transit/TransitTests/QueryTasksIntentDateFilterTests.swift`++**Test names:**+- `nullCompletionDateRelativeIsRejected`+- `nullCompletionDateFromIsRejected`+- `nullCompletionDateToIsRejected`+- `nullLastStatusChangeDateRelativeIsRejected`+- `nullLastStatusChangeDateFromIsRejected`+- `nullLastStatusChangeDateToIsRejected`+- `malformedNestedDateFilterFieldsAreRejected`+- `scalarCompletionDateFilterIsRejected`+- `arrayLastStatusChangeDateFilterIsRejected`+- `malformedNestedDateFiltersDoNotBroadenResults`+- `completionDateFromOnlyKeepsOpenUpperBound`+- `completionDateToOnlyKeepsOpenLowerBound`++**What it verifies:** Every recognized nested date-filter field rejects explicit JSON null under both date filters with stable field-path hints. Malformed scalar/array nested fields and date-filter shapes retain `INVALID_INPUT`, and malformed requests produce an error object rather than a broader task result. Query-level open-bound tests prove omitted `from` and `to` fields retain existing inclusive semantics.++**Run command:** `make test-quick`++## Affected Files++| File | Change |+|------|--------|+| `Transit/Transit/Intents/QueryTasksIntent.swift` | Presence-aware nested date-filter validation before Codable decoding |+| `Transit/TransitTests/QueryTasksIntentNullFilterTests.swift` | Nested null, malformed-type, stable-error, and no-broadening regressions |+| `Transit/TransitTests/QueryTasksIntentDateFilterTests.swift` | Open-ended `from`/`to` range-semantics regressions |+| `specs/bugfixes/query-tasks-nested-date-filter-null-fields/report.md` | Investigation and resolution record |+| `CHANGELOG.md` | Unreleased T-1644 fixed entry |++## Verification++**Automated:**+- [x] Red regression run confirmed all six nested-null cases failed before the fix (`xcodebuild ... -only-testing:TransitTests/QueryTasksIntentNullFilterTests`, exit 65).+- [x] Focused regression suite passes after the fix (all `QueryTasksIntentNullFilterTests` cases passed).+- [x] Full unit test suite passes (`make test-quick`, exit 0).+- [x] Linters/validators pass (`make lint`, exit 0; ownership guard and SwiftLint pass).++**Manual verification:**+- Existing `omittedKeysStillReturnAllTasks` confirms omitted filters retain existing semantics, while `completionDateFromOnlyKeepsOpenUpperBound` and `completionDateToOnlyKeepsOpenLowerBound` verify omitted nested bounds remain open and inclusive.+- Exact nested-null hints are asserted for all six `relative`/`from`/`to` cases under both date filters.+- Existing top-level null regressions continue to pass, preserving their established `Filter \"<key>\" must not be null` contract.+- Malformed nested field types and scalar/array date-filter shapes continue to return `INVALID_INPUT` rather than broadening results; the no-broadening regression parses the response as an error object, not a task array.++## Prevention++**Recommendations to avoid similar bugs:**+- Validate presence-sensitive JSON fields from the `JSONSerialization` object before decoding into optional Codable properties.+- Treat nested filter objects as a separate validation boundary and preserve the existing error contract for malformed shapes.+- Add paired omitted-versus-explicit-null regressions whenever optional JSON fields are introduced.++## Related++- T-1644 — QueryTasksIntent accepts nested null date filter fields.+- `QueryTasksIntentNullFilterTests` — existing top-level explicit-null contract.
Verified PR #202 is OPEN, mergedAt is null, base is main at 275ac5cfc063f1196041470fba115448d8f2dd29, and head branch T-1644/bugfix-query-tasks-intent-accepts-nested-null-date-filter-fields points to c24f9f9118127d38e7f01f83e989f0610f238ae4. The head is a commit, its parent is c8ac752da6a69dd9dc55af4c53ca75fedfb7405d, and the base/head merge-base equals the PR base SHA.
Both completionDate and lastStatusChangeDate reject explicit null for relative, from, and to with field-specific invalid-input hints. The six exact cases pass in QueryTasksIntentNullFilterTests.
From-only and to-only date ranges remain open-ended through the existing date-filter helper. Both positive tests pass in QueryTasksIntentDateFilterTests.
Nested scalar/array shapes are rejected as invalid JSON objects, malformed nested fields do not broaden results, and omitted keys remain valid. These focused regression cases pass.
Focused changed suites passed 28/28. make test-quick, make lint, and git diff --check passed. The full iOS suite is not fully green because of the three unrelated UI failures documented above.
Fresh claude-review CI passed for c24f9f9 in run 30726148196. GitHub review-thread GraphQL returned count: 0 and unresolved: 0.
The detached exact-head validation worktree is clean. The original checkout was not altered and retains its pre-existing modified docs/agent-notes files and untracked .kiro/settings/lsp.json; those local changes are not part of PR #202.