transit PR #206 head 2854f73c21da base 4e6957c35cbf commits 3 files 5 touched lines +188 / -5

PR #206 — Fix T-1799: Reject invalid calendar date filters

Regenerated from the exact candidate worktree for PR #206 / T-1799; no source code, push, or merge operations were performed.

At a glance

  • Exact refs: candidate 2854f73c21da50f704189fbf529c7cdacc48941e and base 4e6957c35cbfeb31085a8e84c6c98af93a5bde22; the candidate worktree is clean.
  • Fix: absolute YYYY-MM-DD filters now require exact ASCII shape, non-lenient parsing, and formatter round-trip equality, rejecting silently normalized calendar dates.
  • Focused evidence: the isolated macOS date suites passed 33/33 with 0 failures and 0 skips; make test-quick and make lint passed.
  • Full-suite comparison: candidate 1,188 passed / 3 failed / 0 skipped; exact base 1,184 / 3 / 0; both report testClearAll, testEditViewPreservesTaskMilestone, and DataMaintenanceUITests.testDataMaintenanceGoldenPath.
  • Scope and review: five changed files, no UI-path changes, exact-head claude-review success, and zero GitHub review threads.

Verdict

Ready to push

The exact candidate head is clean and contains only strict date-filter parsing, focused regressions, changelog, and bugfix-report changes. The focused date suites pass 33/33; make lint and make test-quick pass. The candidate full iOS result bundle records 1,188 passed / 3 failed / 0 skipped, while the exact-base bundle records 1,184 / 3 / 0 with the same three UI failure identities. Those failures are therefore baseline-only, and the diff contains no UI-path changes. Exact-head claude-review passes and GitHub reports zero review threads.

Author's PR description

Shown verbatim — the markdown the author wrote, unmodified.

@ArjenSchwarz · 2026-08-02
## Summary
- Require exact ASCII `YYYY-MM-DD` absolute date filters in `DateFilterHelpers`.
- Use explicit Gregorian/local-time-zone parsing with non-lenient round-trip validation, while preserving inclusive absolute ranges and relative filters.
- Add helper and `QueryTasksIntent` regressions for leap years, calendar boundaries, non-ASCII, and non-padded values.

## Root cause
`DateFormatter(dateFormat: "yyyy-MM-dd")` accepted loose lexical forms and normalized invalid calendar components, so malformed filters were treated as valid dates.

## Verification
- `make test-quick`
- `make lint`

Bugfix report: `specs/bugfixes/query-tasks-intent-accepts-invalid-calendar-date-strings/report.md`

Closes T-1799

Commits

Three-level explanation

The bug was that a permissive Foundation date formatter could accept inputs such as 2026-02-30 and silently normalize them to another date. The fix first checks for exactly ten ASCII bytes in YYYY-MM-DD form, then parses without leniency, and finally formats the result back to require an exact round trip. Tests cover valid leap days, invalid calendar boundaries, non-padded values, Unicode lookalikes, both QueryTasksIntent date fields, and preserved relative/inclusive behavior.

DateFilterHelpers keeps a cached local-day calendar based on Calendar.current, with the current time zone and POSIX locale. Absolute parsing and inclusive comparison use that same calendar; relative ranges retain their existing Calendar.current behavior. Invalid parsed values return nil, which lets QueryTasksIntent surface the established INVALID_INPUT response before query execution. The final commit preserves the repository-required current-calendar semantics after the earlier implementation was corrected.

hasExactDateShape validates UTF-8 bytes directly, so full-width digits and Unicode separator lookalikes cannot reach Foundation parsing. isLenient = false rejects impossible components, while string(from:) == value independently rejects residual normalization or lexical drift. The parser and absolute range comparison share localDayCalendar, avoiding parser/comparator disagreement around local day boundaries and DST. The change is limited to the shared helper and its regressions; no UI execution path is modified.

Important changes — detailed

DateFilterHelpers: enforce exact calendar-date input

Transit/Transit/Intents/Shared/Utilities/DateFilterHelpers.swift

Why it matters. Closes the externally visible bug by preventing malformed absolute filters from being accepted or silently normalized.

What to look at. DateFilterHelpers.swift:16-173

Takeaway. Validate fixed-width machine-readable input before handing it to a permissive platform parser.
Rationale. The bugfix report identifies Foundation's lenient parsing and missing lexical/round-trip guards as the root cause.

DateFilterHelpers: align parsing and comparison calendars

Transit/Transit/Intents/Shared/Utilities/DateFilterHelpers.swift

Why it matters. Ensures parsed absolute dates and inclusive day comparisons use the same local calendar/time-zone semantics.

What to look at. DateFilterHelpers.swift:17-35 and dateInRange(_:range:now:)

Takeaway. When parsing date-only values, share the configured calendar with the comparison path to avoid boundary mismatches.
Rationale. The final commit restores the documented <code>Calendar.current</code> identifier while explicitly retaining local time-zone and POSIX-locale configuration.

DateFilterHelpersTests: cover lexical and calendar boundaries

Transit/TransitTests/DateFilterHelpersTests.swift

Why it matters. Locks down leap-year behavior, month/day bounds, exact padding, and rejection of non-ASCII lookalikes.

What to look at. DateFilterHelpersTests.swift:44-80

Takeaway. Parameterize parser regressions around both semantic validity and the documented wire format.
Rationale. The report lists the pre-fix reproductions and the valid leap-day case as the parser-level regression contract.

QueryTasksIntentDateFilterTests: verify public error mapping

Transit/TransitTests/QueryTasksIntentDateFilterTests.swift

Why it matters. Confirms malformed values in both public absolute date fields become <code>INVALID_INPUT</code> rather than broadening or altering query results.

What to look at. QueryTasksIntentDateFilterTests.swift:270-289

Takeaway. Test shared validation at the public boundary as well as at the helper unit level.
Rationale. Both JSON date fields share the helper, so the intent-level test protects the externally documented error contract.

Key decisions

Use three independent parser guards

The final implementation combines exact UTF-8 shape validation, DateFormatter.isLenient = false, and formatter round-trip equality. This is stronger than relying on any one Foundation behavior and preserves the documented ASCII wire contract.

Preserve Calendar.current semantics

The final head retains the user's current calendar identifier while explicitly configuring the local time zone and POSIX locale. This corrects the compatibility regression identified during review without weakening strict date validation.

Keep the existing helper boundary

The implementation hardens DateFilterHelpers.dateFromString rather than introducing a separate date-format abstraction. The bug report records that an ISO-8601 format-style replacement would change date-only/time-zone semantics unnecessarily.

Treat the three iOS failures as baseline-only

The candidate and exact-base full-suite result bundles contain the same three UI failure identities, while the candidate diff has no UI-path changes. The failures remain documented as environment/baseline evidence rather than candidate blockers.

Per-file diffs

Click to expand.

CHANGELOG.md Modified +1 / -0
diff --git a/CHANGELOG.md b/CHANGELOG.mdindex 0365791..bd359a9 100644--- a/CHANGELOG.md+++ b/CHANGELOG.md@@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased]  ### Fixed+- T-1799: QueryTasksIntent absolute date filters now require exact ASCII `YYYY-MM-DD` values representing valid dates in the user's current calendar. Local `Calendar.current` parsing, non-lenient formatter round-trip validation, and regressions reject normalized invalid days/months, non-padded values, and non-ASCII input while preserving inclusive ranges and relative filters. - MCP `create_task`, `create_milestone`, `CreateTaskIntent`, and `CreateMilestoneIntent` now distinguish missing required fields from present non-string `name`/`type` values (T-1598). Numeric, boolean, array, object, and null inputs return field-specific `name must be a string` / `type must be a string` validation errors before mutation, while missing-field and invalid-string-type behavior remains unchanged. Symmetric regression coverage verifies all four create paths and confirms malformed requests create no records. - `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). - `Color.hexString` now rounds resolved RGB components to the nearest byte after finite-checking and clamping instead of truncating values just below byte boundaries (T-1807). Project colors no longer darken when an editor saves an unchanged color. Regression coverage exercises every `0...255` RGB value, deterministic sRGB resolved components immediately below each boundary, and the existing uppercase six-digit RGB/no-alpha format.
Transit/Transit/Intents/Shared/Utilities/DateFilterHelpers.swift Modified +33 / -5
diff --git a/Transit/Transit/Intents/Shared/Utilities/DateFilterHelpers.swift b/Transit/Transit/Intents/Shared/Utilities/DateFilterHelpers.swiftindex 624bca7..7d1fbbc 100644--- a/Transit/Transit/Intents/Shared/Utilities/DateFilterHelpers.swift+++ b/Transit/Transit/Intents/Shared/Utilities/DateFilterHelpers.swift@@ -14,12 +14,23 @@ enum DateFilterHelpers {         case absolute(from: Date?, toDate: Date?)     } +    // Preserve the user's calendar identifier while fixing the locale and timezone used+    // for machine-readable date-only values. This matches the documented Calendar.current+    // semantics and keeps absolute comparisons aligned with the parser across DST changes.+    private static let localDayCalendar: Calendar = {+        var calendar = Calendar.current+        calendar.locale = Locale(identifier: "en_US_POSIX")+        calendar.timeZone = TimeZone.current+        return calendar+    }()+     private static let localDayFormatter: DateFormatter = {         let formatter = DateFormatter()-        formatter.dateFormat = "yyyy-MM-dd"-        formatter.calendar = Calendar.current-        formatter.timeZone = TimeZone.current+        formatter.calendar = localDayCalendar+        formatter.timeZone = localDayCalendar.timeZone         formatter.locale = Locale(identifier: "en_US_POSIX")+        formatter.dateFormat = "yyyy-MM-dd"+        formatter.isLenient = false         return formatter     }() @@ -97,7 +108,7 @@ enum DateFilterHelpers {             return dateInPreviousPeriod(date, component: .year, now: now, calendar: calendar)          case .absolute(let from, let toDate):-            return dateInAbsoluteRange(date, from: from, toDate: toDate, calendar: calendar)+            return dateInAbsoluteRange(date, from: from, toDate: toDate, calendar: localDayCalendar)         }     } @@ -139,6 +150,23 @@ enum DateFilterHelpers {     }      private static func dateFromString(_ value: String) -> Date? {-        localDayFormatter.date(from: value)+        guard hasExactDateShape(value),+              let date = localDayFormatter.date(from: value),+              localDayFormatter.string(from: date) == value else {+            return nil+        }+        return date+    }++    private static func hasExactDateShape(_ value: String) -> Bool {+        let bytes = Array(value.utf8)+        guard bytes.count == 10, bytes[4] == 45, bytes[7] == 45 else {+            return false+        }++        for (index, byte) in bytes.enumerated() where index != 4 && index != 7 {+            guard (48...57).contains(byte) else { return false }+        }+        return true     } }
Transit/TransitTests/DateFilterHelpersTests.swift Modified +39 / -0
diff --git a/Transit/TransitTests/DateFilterHelpersTests.swift b/Transit/TransitTests/DateFilterHelpersTests.swiftindex bb793c9..d473a40 100644--- a/Transit/TransitTests/DateFilterHelpersTests.swift+++ b/Transit/TransitTests/DateFilterHelpersTests.swift@@ -41,6 +41,45 @@ struct DateFilterHelpersTests {         #expect(parsed == nil)     } +    @Test("parse absolute filter accepts real Gregorian leap-day dates")+    func parseAbsoluteFilterAcceptsLeapDay() {+        let parsed = DateFilterHelpers.parseDateFilter([+            "from": "2024-02-29",+            "to": "2024-03-01"+        ])++        guard case .absolute(let from, let toDate) = parsed else {+            Issue.record("Expected absolute range")+            return+        }+        #expect(from != nil)+        #expect(toDate != nil)+    }++    @Test("parse absolute filter rejects non-calendar and non-ASCII date strings", arguments: [+        "2023-02-29", // Non-leap year day overflow+        "2024-02-30", // Leap-year month day overflow+        "2026-04-31", // Month day overflow+        "2026-00-01", // Month below lower boundary+        "2026-13-01", // Month above upper boundary+        "2026-02-00", // Day below lower boundary+        "0000-01-01", // Gregorian year below lower boundary+        "2026-2-01", // Full-width non-ASCII digit+        "2026−02−01" // Unicode minus separators+    ])+    func parseAbsoluteFilterRejectsInvalidCalendarDateStrings(value: String) {+        #expect(DateFilterHelpers.parseDateFilter(["from": value]) == nil)+    }++    @Test("parse absolute filter rejects non-padded date strings", arguments: [+        "2026-2-01",+        "2026-02-1",+        "2026-2-1"+    ])+    func parseAbsoluteFilterRejectsNonPaddedDateStrings(value: String) {+        #expect(DateFilterHelpers.parseDateFilter(["from": value]) == nil)+    }+     @Test func absoluteRangeComparisonIsInclusive() {         let calendar = Calendar.current         let from = calendar.startOfDay(for: Date.now)
Transit/TransitTests/QueryTasksIntentDateFilterTests.swift Modified +23 / -0
diff --git a/Transit/TransitTests/QueryTasksIntentDateFilterTests.swift b/Transit/TransitTests/QueryTasksIntentDateFilterTests.swiftindex c52c1df..22e40b5 100644--- a/Transit/TransitTests/QueryTasksIntentDateFilterTests.swift+++ b/Transit/TransitTests/QueryTasksIntentDateFilterTests.swift@@ -4,6 +4,7 @@ import Testing @testable import Transit  @MainActor @Suite(.serialized)+// swiftlint:disable:next type_body_length struct QueryTasksIntentDateFilterTests {      private struct Services {@@ -266,6 +267,28 @@ struct QueryTasksIntentDateFilterTests {         #expect(parsed["error"] as? String == "INVALID_INPUT")     } +    @Test("QueryTasksIntent rejects malformed absolute date strings", arguments: [+        "2026-02-30", // Normalized invalid day+        "2026-13-01", // Invalid month boundary+        "2026-2-01", // Non-padded month+        "2026-02-1" // Non-ASCII digit+    ])+    func malformedAbsoluteDateStringsReturnInvalidInput(value: String) throws {+        let svc = try makeServices()++        for field in ["completionDate", "lastStatusChangeDate"] {+            let result = QueryTasksIntent.execute(+                input: "{\"\(field)\":{\"from\":\"\(value)\"}}",+                projectService: svc.project,+                taskService: svc.task,+                milestoneService: svc.milestone+            )++            let parsed = try parseJSON(result)+            #expect(parsed["error"] as? String == "INVALID_INPUT")+        }+    }+     @Test func existingQueriesRemainCompatibleWithoutDateFilters() throws {         let svc = try makeServices()         let project = makeProject(in: svc.context)
specs/bugfixes/query-tasks-intent-accepts-invalid-calendar-date-strings/report.md Added +92 / -0
diff --git a/specs/bugfixes/query-tasks-intent-accepts-invalid-calendar-date-strings/report.md b/specs/bugfixes/query-tasks-intent-accepts-invalid-calendar-date-strings/report.mdnew file mode 100644index 0000000..c155f0d--- /dev/null+++ b/specs/bugfixes/query-tasks-intent-accepts-invalid-calendar-date-strings/report.md@@ -0,0 +1,92 @@+# Bugfix Report: Query Tasks Intent Accepts Invalid Calendar Date Strings++**Date:** 2026-08-02+**Status:** Fixed++## Description of the Issue++`QueryTasksIntent` documents absolute date filters as `YYYY-MM-DD`, but `DateFilterHelpers` delegates parsing to a lenient `DateFormatter`. Foundation accepts non-padded values and normalizes invalid calendar components, so inputs such as `2026-02-30` can be treated as a different valid date instead of returning `INVALID_INPUT`.++**Reproduction steps:**+1. Invoke `QueryTasksIntent` with `{"completionDate":{"from":"2026-02-30"}}` or a non-padded value such as `2026-2-01`.+2. Observe that the date filter is accepted rather than rejected.+3. Compare with the documented exact `YYYY-MM-DD` contract.++**Impact:** CLI, Shortcuts, and agent callers can receive results for a silently altered date filter. The issue affects absolute `completionDate` and `lastStatusChangeDate` filters; relative filters are unaffected.++## Investigation Summary++- **Symptoms examined:** Invalid days/months, non-padded fields, Unicode digits/separators, valid leap-day input, inclusive absolute ranges, and relative date filters.+- **Code inspected:** `DateFilterHelpers`, `QueryTasksIntent` validation/application paths, `FindTasksIntent` shared-helper usage, and existing date-filter tests.+- **Hypotheses tested:** The defect is in shared date parsing rather than intent JSON decoding or task filtering. Focused pre-fix regressions fail for normalized invalid days, non-padded values, and non-ASCII input while existing valid and relative cases pass.++## Discovered Root Cause++`DateFilterHelpers.dateFromString` calls `DateFormatter.date(from:)` with only a `yyyy-MM-dd` format. That API does not enforce exact lexical width/ASCII characters and can normalize invalid date components before returning a `Date`. `QueryTasksIntent` treats any non-`nil` parsed date as valid.++**Defect type:** Missing input validation and date normalization.++**Why it occurred:** The formatter was configured for output shape but not strict input validation. No lexical check or parsed-date round-trip check guarded the public filter boundary.++**Contributing factors:** Foundation date parsing is permissive; both JSON date fields share the helper, so a single parser defect affects both absolute filters.++## Resolution for the Issue++**Changes made:**+- `Transit/Transit/Intents/Shared/Utilities/DateFilterHelpers.swift` - Preserved `Calendar.current` with the current local time zone and POSIX locale; rejected every input without exactly ten ASCII bytes in `YYYY-MM-DD` shape; disabled formatter leniency; and required formatter round-trip equality so normalized invalid dates are rejected. Absolute range comparisons use the same local current-calendar semantics as parsing, while relative filters retain their existing behavior.+- `Transit/TransitTests/DateFilterHelpersTests.swift` - Added valid leap-day coverage plus invalid leap-year, month/day boundary, non-ASCII, and non-padded regressions.+- `Transit/TransitTests/QueryTasksIntentDateFilterTests.swift` - Added intent-level `INVALID_INPUT` coverage for malformed values in both absolute date-filter fields.++**Approach rationale:** Exact byte-shape validation prevents Foundation from accepting alternate lexical forms, and round-trip equality detects dates that Foundation normalizes. Explicit locale/time-zone configuration preserves the documented `Calendar.current` date semantics and keeps parsing and inclusive comparisons aligned across local DST changes without changing relative filters.++**Alternatives considered:**+- Relying only on `DateFormatter.isLenient = false` - Not sufficient for the documented exact ASCII lexical contract, so shape validation and round-trip equality are both required.+- Parsing with `Date.ISO8601FormatStyle` - Would introduce different date-only/time-zone semantics and is unnecessary for this existing helper boundary.++## Regression Test++**Test files:**+- `Transit/TransitTests/DateFilterHelpersTests.swift`+- `Transit/TransitTests/QueryTasksIntentDateFilterTests.swift`++**Test names:**+- `parseAbsoluteFilterAcceptsLeapDay()`+- `parseAbsoluteFilterRejectsInvalidCalendarDateStrings(value:)`+- `parseAbsoluteFilterRejectsNonPaddedDateStrings(value:)`+- `malformedAbsoluteDateStringsReturnInvalidInput(value:)`++**What it verifies:** Absolute filters accept real Gregorian leap days, reject invalid month/day boundaries and non-leap-year dates, reject non-ASCII and non-padded values, and surface `INVALID_INPUT` through both QueryTasksIntent date fields.++**Run command:** `make test-quick`++## Affected Files++| File | Change |+|------|--------|+| `Transit/Transit/Intents/Shared/Utilities/DateFilterHelpers.swift` | Strict exact-shape, current-calendar, local-time-zone, round-trip date parsing |+| `Transit/TransitTests/DateFilterHelpersTests.swift` | Parser regressions |+| `Transit/TransitTests/QueryTasksIntentDateFilterTests.swift` | Intent-level regressions |+| `specs/bugfixes/query-tasks-intent-accepts-invalid-calendar-date-strings/report.md` | Investigation and resolution report |+| `CHANGELOG.md` | Unreleased fix entry |++## Verification++**Automated:**+- [x] Focused helper and QueryTasksIntent tests pass on macOS+- [x] Full `make test-quick` suite passes+- [x] `make lint` and the SwiftData ownership validator pass++**Manual verification:**+- The pre-fix focused run reproduced failures for normalized invalid days, non-padded values, non-ASCII input, and the intent-level validation path.+- The post-fix focused run passed all existing relative/inclusive cases and all new regressions.++## Prevention++- Validate exact input shape before invoking Foundation date parsing.+- Preserve `Calendar.current` while explicitly configuring locale and local time-zone semantics for machine-readable dates.+- Require formatter round-trip equality before accepting a parsed calendar date.+- Keep parser-level and intent-level regressions for externally documented input formats.++## Related++- Transit ticket T-1799

Things to double-check

Exact candidate and clean worktree

git status --short --branch is clean in /Users/arjen/projects/personal/transit-worktrees/T-1799; local HEAD and the PR head both resolve to 2854f73c21da50f704189fbf529c7cdacc48941e. The requested base resolves to 4e6957c35cbfeb31085a8e84c6c98af93a5bde22.

Focused date validation

Explicit macOS xcodebuild result bundle /tmp/transit-t1799-focused.xcresult reports 33 passed / 0 failed / 0 skipped, result Passed, for DateFilterHelpersTests and QueryTasksIntentDateFilterTests.

Lint and fast unit suite

make lint passed, including the SwiftData ownership guard. make test-quick passed; its successful result bundle reports 1,653 passed / 0 failed / 0 skipped.

Full iOS candidate versus exact base

Candidate bundle /Users/arjen/projects/personal/transit-worktrees/T-1799/DerivedData/Logs/Test/Test-Transit-2026.08.02_15-45-14-+1000.xcresult reports 1,188 passed / 3 failed / 0 skipped. Exact-base bundle /Users/arjen/projects/personal/transit-worktrees/W17-base-4e6957c/DerivedData/Logs/Test/Test-Transit-2026.08.02_15-34-06-+1000.xcresult reports 1,184 / 3 / 0. Both identify TransitUITests/testClearAll(), TransitUITests/testEditViewPreservesTaskMilestone(), and DataMaintenanceUITests/testDataMaintenanceGoldenPath(); the diff has no UI-path changes.

Exact-head CI and qualifying review

The GitHub check-runs API for commit 2854f73c21da50f704189fbf529c7cdacc48941e reports claude-review completed with conclusion success. GraphQL review-thread inspection reports 0 threads and 0 unresolved threads.