Skip to content

fix(swift): resolve seven reported iOS defects across weather, packs, trips, and catalog search - #2697

Merged
mikib0 merged 2 commits into
developmentfrom
worktree-swift-issues-2660-2693
Aug 11, 2026
Merged

fix(swift): resolve seven reported iOS defects across weather, packs, trips, and catalog search#2697
mikib0 merged 2 commits into
developmentfrom
worktree-swift-issues-2660-2693

Conversation

@mikib0

@mikib0 mikib0 commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Description

Fixes seven reported defects in the Swift app. Six are client-side; one (#2662) is a server-side search-ranking bug that surfaces in the app's Catalog screen.

Closes #2693
Closes #2692
Closes #2691
Closes #2690
Closes #2662
Closes #2661
Closes #2660

Issue Root cause Fix
#2693 Weather — large blank gap The search-state row was added to the List unconditionally, so it still reserved inset-grouped row height and padding while empty Add the row only when it has something to render
#2690 Empty state despite non-zero weight weightSummary read the captured let pack (a snapshot from push time) while the item list read the live currentPack Read totals from currentPack, like the rest of the view already did
#2692 Similar Gear not tappable SimilarItemCard was a plain VStack — no button, link, or gesture Tap presents CatalogItemDetailView, reusing the contentShape/onTapGesture/sheet pattern CatalogItemRow already uses
#2691 Pack row stays highlighted Compact iOS navigates via NavigationLink and the List had a selection: binding, so the tapped row stayed selected after popping back Pass nil selection on compact; split-view keeps selection for its detail pane
#2661 Weight reverts after Save The detail view rendered the PackItem snapshot it was constructed with Re-read the item from the view model; Edit re-open now prefills from the live item too
#2660 Trip "None" doesn't save Synthesised Encodable omits nil keys, so no packId was sent; the route keys off if ('packId' in data) and read the absence as "leave unchanged" Encode packId unconditionally, as an explicit null when "None" is chosen
#2662 Catalog search relevance Two causes — see below Word-boundary matching + relevance ranking

On #2662

Searching "Tent" returned leggings, tote bags and repair kits above actual tents, for two independent reasons:

  1. Unanchored ilike '%q%' matched the letters anywhere, including inside unrelated words. "consistent", "content", "intent", "patented" and "latent" are everywhere in gear marketing copy, and description was weighted the same as name. Free-text fields (name, description, categories) now require a POSIX word-boundary match. Brand and model keep substring matching, since typing a prefix is the normal way to search those.
  2. Nothing ranked by the query at all. Ordering was purely by how often an item had been added to packs, so a popular incidental match outranked an exact-name tent. Matches are now tiered by where they hit (exact name > name prefix > name word > category > brand/model > description-only), with popularity retained as a tiebreaker within a tier.

q is interpolated into a regex operator, so metacharacters are escaped — otherwise a stray ( errors the statement. An explicit caller-supplied sort still overrides relevance, and the desc(id) pagination tiebreaker is preserved on every branch.

Verified directly against Postgres using fixtures drawn from the issue:

=== q=tent : filter + ranking (new) ===        === old behaviour (unanchored ILIKE) ===
 Tent                 | 100                     Trailhead Leggings      <- "consistent"/"content"/"intent"
 Tent Footprint       |  80                     Canvas Tote Bag         <- "patented"/"latent"
 Copper Spur UL2 Tent |  60                     Fabric Repair Kit
 Fabric Repair Kit    |   0                     Copper Spur UL2 Tent
                                                Tent Footprint
                                                Tent

Both decoys drop out of the result set entirely, and the genuine description-only match (repair kit) correctly sorts last.

Type of change

  • 🐛 Bug fix
  • ✨ New feature
  • ♻️ Refactor / code improvement
  • 📝 Documentation update
  • 🔧 CI / configuration change
  • ⬆️ Dependency update
  • 🗄️ Database migration

Area(s) affected

  • Mobile app (apps/expo)
  • API / Backend (packages/api)
  • Landing page (apps/landing)
  • Guides site (apps/guides)
  • CI / CD (.github/)
  • Swift app (apps/swift) — not in the template list

Testing

  • Added / updated unit tests — five regression tests in packages/api/test/catalog.test.ts covering word-boundary matching, name-over-description ranking, exact-over-partial ranking, brand substring, and regex-metacharacter safety
  • Manually tested on iOS
  • Manually tested on Android
  • Manually tested on Web
  • API endpoints verified (e.g. curl or Postman)

What was actually run:

Important

The five new API tests have never executed, and the reviewer should know why. The packages/api integration harness is broken on development, independently of this branch: migration 0033_social_feed_tables.sql creates comment_likes with IF NOT EXISTS, then 0037_big_archangel.sql recreates it without the guard, and test/vitest.global-setup.ts applies every .sql file blindly. Startup therefore fails with relation "comment_likes" already exists on a clean volume. I confirmed this against pristine dbc84ca0e before concluding it wasn't mine.

I did not fix it: those files are Drizzle-generated and CLAUDE.md forbids hand-editing migrations, so the repair needs a regeneration decision outside this PR's scope. That is why #2662 was validated by running the SQL directly against Postgres instead. The new tests will run once the harness is repaired — worth tracking separately.

Screenshots / recordings

None. The six client-side fixes were verified by root-cause analysis and compilation rather than by running the app; each is a small, targeted change at an identified defect. The reproduction steps in each issue are the fastest way to confirm on-device.

Pre-merge checklist

  • bun format && bun lint passes with no errors
  • bun check-types passes with no errors
  • No new secrets or credentials are committed
  • Database migration included (if schema changed) — no schema change
  • Feature flag added (if this is a new feature) — bug fixes only
  • PR title follows conventional commits (feat:, fix:, chore:, etc.)

Summary by CodeRabbit

  • New Features

    • Similar pack items can now be tapped to view their catalog details.
    • Catalog searches now rank the most relevant matches first and safely support special characters.
  • Bug Fixes

    • Pack weight summaries now reflect the latest item changes.
    • Pack item details and edit screens stay up to date with current data.
    • Improved list selection behavior across different layouts.
    • Weather search status messages now appear only when relevant.
    • Trip updates now correctly preserve cleared pack selections and timestamps.
  • Tests

    • Added coverage for catalog search matching, relevance, and special-character handling.

mikib0 added 2 commits August 11, 2026 12:10
- #2693 Weather: omit the search-state row when empty. The row rendered
  unconditionally, reserving inset-grouped height and padding, which showed
  as a large gap between the search field and Saved Locations.

- #2690 Pack Details: read weight totals from `currentPack` rather than the
  captured `pack`. The latter is a snapshot from push time, so the summary
  kept showing weights for items the live pack no longer had.

- #2692 Pack Item Details: make Similar Gear cards tappable, presenting
  CatalogItemDetailView via the same contentShape/onTapGesture/sheet pattern
  CatalogItemRow already uses. The cards previously had no action at all.

- #2691 Packs list: drop the List selection binding on compact iOS, where
  navigation is a NavigationLink push. Tracking selection there left the
  tapped row rendered gray after popping back. Split-view keeps selection.

- #2661 Pack Item Details: re-read the item from the view model instead of
  rendering the construction-time snapshot, so the weight shown right after
  Save reflects the edit. Edit re-open now prefills from the live item too.

- #2660 Trip edit: encode `packId` unconditionally on update, as explicit
  null when "None" is chosen. Synthesised Encodable omits nil keys, and the
  route keys off `if ('packId' in data)`, so unassigning never persisted.
Searching "Tent" surfaced leggings, tote bags and repair kits above actual
tents. Two independent causes:

1. Unanchored `ilike '%q%'` matched the letters anywhere, including inside
   unrelated words — "consistent", "content", "intent", "patented", "latent"
   are common in gear marketing copy, and `description` was matched with the
   same weight as `name`. Free-text fields (name, description, categories)
   now require a POSIX word-boundary match, so incidental hits inside longer
   words no longer qualify. Brand and model keep substring matching, where
   typing a prefix is the normal way to search.

2. Nothing ranked by the query at all — ordering was purely by how often an
   item had been added to packs, so a popular incidental match outranked an
   exact-name tent. Matches are now tiered by where they hit (exact name >
   name prefix > name word > category > brand/model > description-only),
   with popularity kept as a tiebreaker within a tier.

The query is interpolated into a regex operator, so metacharacters in `q`
are escaped — otherwise a stray "(" would error the statement.

An explicit caller-supplied `sort` still overrides relevance, and the
`desc(id)` pagination tiebreaker is preserved on every branch.

Verified against Postgres with fixtures drawn from the issue: the leggings
and tote-bag decoys drop out of the result set entirely, and the remaining
matches order Tent > Tent Footprint > Copper Spur UL2 Tent > Fabric Repair
Kit (description-only mention, last).
@github-actions github-actions Bot added the api label Aug 11, 2026
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The PR updates Swift pack, weather, and trip request behavior. It also changes catalog search matching and relevance ordering, with regression tests for search boundaries, ranking, brand/model matching, and literal regex characters.

Changes

Swift application updates

Layer / File(s) Summary
Pack detail data and interactions
apps/swift/Sources/PackRat/Features/Packs/PackDetailView.swift, apps/swift/Sources/PackRat/Features/Packs/PackItemDetailView.swift
Pack details use current pack weights and refreshed item data. Similar-item cards can open catalog item details.
Conditional list and search state
apps/swift/Sources/PackRat/Features/Packs/PacksListView.swift, apps/swift/Sources/PackRat/Features/Weather/WeatherView.swift
List selection is disabled on compact layouts. The weather search-state row renders only when search content exists.
Trip request encoding
apps/swift/Sources/PackRat/Models/Trip.swift
UpdateTripRequest conditionally encodes optional fields and always encodes packId and localUpdatedAt.

Catalog search updates

Layer / File(s) Summary
Catalog matching and relevance ordering
packages/api/src/services/catalogService.ts, packages/api/test/catalog.test.ts
Catalog search escapes query text, applies field-specific matching, ranks results by relevance, and tests matching boundaries, ranking, brand/model searches, and regex metacharacters.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

Suggested labels: mobile

Suggested reviewers: andrew-bierman

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the pull request as fixes for seven defects across the listed Swift features and catalog search.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-swift-issues-2660-2693

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

Coverage Report for packages/units (./packages/units)

Status Category Percentage Covered / Total
🟢 Lines 100% (🎯 100%) 35 / 35
🟢 Statements 100% (🎯 100%) 35 / 35
🟢 Functions 100% (🎯 100%) 6 / 6
🟢 Branches 100% (🎯 100%) 11 / 11
File CoverageNo changed files found.
Generated in workflow #616 for commit a2b23e9 by the Vitest Coverage Report Action

@github-actions

Copy link
Copy Markdown
Contributor

Coverage Report for packages/utils (./packages/utils)

Status Category Percentage Covered / Total
🟢 Lines 100% (🎯 100%) 92 / 92
🟢 Statements 100% (🎯 100%) 92 / 92
🟢 Functions 100% (🎯 100%) 1 / 1
🟢 Branches 100% (🎯 100%) 1 / 1
File CoverageNo changed files found.
Generated in workflow #616 for commit a2b23e9 by the Vitest Coverage Report Action

@github-actions

Copy link
Copy Markdown
Contributor

Coverage Report for packages/overpass (./packages/overpass)

Status Category Percentage Covered / Total
🟢 Lines 100% (🎯 80%) 155 / 155
🟢 Statements 100% (🎯 80%) 155 / 155
🟢 Functions 100% (🎯 80%) 13 / 13
🟢 Branches 95.65% (🎯 70%) 44 / 46
File CoverageNo changed files found.
Generated in workflow #616 for commit a2b23e9 by the Vitest Coverage Report Action

@github-actions

Copy link
Copy Markdown
Contributor

Coverage Report for packages/analytics (./packages/analytics)

Status Category Percentage Covered / Total
🟢 Lines 100% (🎯 80%) 745 / 745
🟢 Statements 100% (🎯 80%) 745 / 745
🟢 Functions 100% (🎯 85%) 48 / 48
🟢 Branches 87.35% (🎯 80%) 152 / 174
File CoverageNo changed files found.
Generated in workflow #616 for commit a2b23e9 by the Vitest Coverage Report Action

@github-actions

Copy link
Copy Markdown
Contributor

Coverage Report for packages/mcp (./packages/mcp)

Status Category Percentage Covered / Total
🟢 Lines 99.28% (🎯 80%) 4316 / 4347
🟢 Statements 99.28% (🎯 80%) 4316 / 4347
🟢 Functions 100% (🎯 80%) 101 / 101
🟢 Branches 98.35% (🎯 80%) 598 / 608
File CoverageNo changed files found.
Generated in workflow #616 for commit a2b23e9 by the Vitest Coverage Report Action

@github-actions

Copy link
Copy Markdown
Contributor

Coverage Report for apps/expo (./apps/expo)

Status Category Percentage Covered / Total
🟢 Lines 97.64% (🎯 95%) 623 / 638
🟢 Statements 97.64% (🎯 95%) 623 / 638
🟢 Functions 100% (🎯 97%) 52 / 52
🟢 Branches 95.19% (🎯 92%) 218 / 229
File CoverageNo changed files found.
Generated in workflow #616 for commit a2b23e9 by the Vitest Coverage Report Action

@github-actions

Copy link
Copy Markdown
Contributor

Coverage Report for packages/api (./packages/api)

Status Category Percentage Covered / Total
🟢 Lines 99.01% (🎯 95%) 1906 / 1925
🟢 Statements 99.01% (🎯 95%) 1906 / 1925
🟢 Functions 100% (🎯 97%) 100 / 100
🟢 Branches 97.08% (🎯 92%) 665 / 685
File CoverageNo changed files found.
Generated in workflow #616 for commit a2b23e9 by the Vitest Coverage Report Action

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/swift/Sources/PackRat/Features/Packs/PackItemDetailView.swift`:
- Around line 315-323: Replace the onTapGesture interaction in the similar-item
card’s body with a Button that sets showingDetail to true, preserve the card
content and accessibility configuration, and apply .buttonStyle(.plain) to
retain the current visual appearance.

In `@apps/swift/Sources/PackRat/Models/Trip.swift`:
- Around line 41-57: Update Trip and TripService.updateTrip to represent packId
with distinct omitted, assigned, and explicitly cleared states instead of a
plain String?. Adjust encode(to:) to omit packId for the omitted state while
encoding a value or null for the other states, and add serialization tests
covering all three cases.

In `@packages/api/src/services/catalogService.ts`:
- Around line 134-152: Update the search flow around q and term so
whitespace-only input is ignored: trim q before the conditional branch, and only
construct the regex, ILIKE predicates, and searchCondition when the trimmed term
is non-empty. Preserve the no-query result behavior for empty or whitespace-only
terms.

In `@packages/api/test/catalog.test.ts`:
- Around line 166-189: Extend the “still matches brand and model by substring”
test with a fixture whose model, not brand, contains the query substring, and
assert that it is returned. Update the regex-metacharacter test to query an
encoded word-ending value such as “t.ent”, seed both a literal “T.ent” item and
a “Tent” item, and assert the literal item matches while the non-literal item
does not, while preserving the successful response assertion.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 30df1b57-f34f-4ff4-a96e-e6fa5b10f359

📥 Commits

Reviewing files that changed from the base of the PR and between d84a7a1 and a2b23e9.

📒 Files selected for processing (7)
  • apps/swift/Sources/PackRat/Features/Packs/PackDetailView.swift
  • apps/swift/Sources/PackRat/Features/Packs/PackItemDetailView.swift
  • apps/swift/Sources/PackRat/Features/Packs/PacksListView.swift
  • apps/swift/Sources/PackRat/Features/Weather/WeatherView.swift
  • apps/swift/Sources/PackRat/Models/Trip.swift
  • packages/api/src/services/catalogService.ts
  • packages/api/test/catalog.test.ts

Comment on lines 315 to +323
var body: some View {
cardContent
.contentShape(Rectangle())
.onTapGesture { showingDetail = true }
.accessibilityIdentifier("pack_item_similar_card_\(item.id)")
.accessibilityAddTraits(.isButton)
.sheet(isPresented: $showingDetail) {
CatalogItemDetailView(item: item, packsViewModel: packsViewModel)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

file="apps/swift/Sources/PackRat/Features/Packs/PackItemDetailView.swift"

printf '%s\n' '--- target and file references ---'
rg -n "SimilarItemCard|onTapGesture|accessibilityAddTraits|CatalogItemDetailView|platform|macOS|iOS" \
  apps/swift/Sources apps/swift/Package.swift apps/swift/*.xcodeproj 2>/dev/null || true

printf '%s\n' '--- relevant source ---'
sed -n '270,340p' "$file"

printf '%s\n' '--- package/project platform declarations ---'
rg -n -i "macos|ios|deploymentTarget|supportedplatform|swift-tools-version" \
  apps/swift/Package.swift apps/swift/*.pbxproj apps/swift/*.xcodeproj/project.pbxproj 2>/dev/null || true

printf '%s\n' '--- related interaction patterns ---'
rg -n -g '*.swift' "Button\s*\{|\.onTapGesture|accessibilityAddTraits\(\.isButton\)|\.buttonStyle\(\.plain\)" apps/swift/Sources apps/swift/Tests 2>/dev/null | head -200

Repository: PackRat-AI/PackRat

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
from pathlib import Path
import re

root = Path("apps/swift")
for path in root.rglob("*.swift"):
    text = path.read_text(errors="replace")
    if "SimilarItemCard" not in text:
        continue
    print(f"FILE {path}")
    for match in re.finditer(r"SimilarItemCard", text):
        start = max(0, text.rfind("\n", 0, match.start()-250))
        end = text.find("\n", match.end()+400)
        end = len(text) if end < 0 else end
        print(text[start+1:end+1])
PY

Repository: PackRat-AI/PackRat

Length of output: 1641


🌐 Web query:

Apple SwiftUI documentation onTapGesture Button keyboard activation accessibility macOS iOS

💡 Result:

In SwiftUI, using a Button is the recommended practice for interactive elements because it provides essential accessibility and keyboard activation capabilities out of the box [1][2][3][4]. Using.onTapGesture on non-button views (like Text or Image) is generally considered a significant accessibility anti-pattern because these views lack the necessary traits and behaviors required for assistive technologies and keyboard navigation [2][4][5]. Key differences include: 1. Keyboard Activation and Focus: Native SwiftUI Button elements are keyboard-accessible by default [6]. On macOS and platforms supporting Full Keyboard Access, a Button automatically receives focus and can be activated using standard keyboard inputs (such as the Space or Enter/Return keys) [6][7]. Conversely, views using.onTapGesture do not automatically receive keyboard focus and cannot be activated via standard keyboard navigation [6][2]. 2. Accessibility Traits: A Button is automatically identified by VoiceOver as a "button," informing users that the element is interactive [8][2][5]. Views with.onTapGesture do not carry this trait by default; to assistive technologies, they appear as static content [2][5]. If you must use.onTapGesture for a custom interactive view, you are required to manually add the accessibility trait using.accessibilityAddTraits(.isButton) and provide an appropriate accessibility label [6][8][2]. 3. User Experience: Button provides automatic visual feedback (such as a pressed state) that helps users understand when an interaction has been recognized [9][4]. It also handles platform-specific interaction models—like clicks on macOS or taps on iOS—consistently [9][7]. Apple explicitly advises that if you are creating a control functionally equivalent to a button, you should use the Button component, utilizing ButtonStyle to achieve custom designs rather than applying gestures to arbitrary views [9]. [6][8][1][2][9][4][7][5]

Citations:


Use Button for the similar-item card.

onTapGesture does not provide native keyboard activation or button control behavior. Replace it with a Button and retain .buttonStyle(.plain).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/swift/Sources/PackRat/Features/Packs/PackItemDetailView.swift` around
lines 315 - 323, Replace the onTapGesture interaction in the similar-item card’s
body with a Button that sets showingDetail to true, preserve the card content
and accessibility configuration, and apply .buttonStyle(.plain) to retain the
current visual appearance.

Comment on lines +41 to +57
/// `packId` is encoded unconditionally — as an explicit `null` when the user
/// picks "None" — because the update route distinguishes the two cases with
/// `if ('packId' in data)`. Synthesised `Encodable` omits nil keys entirely,
/// which the server read as "leave unchanged", so unassigning a pack never
/// saved. Every other field keeps omit-when-nil so a partial update does not
/// clobber fields the form did not touch.
func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeIfPresent(name, forKey: .name)
try container.encodeIfPresent(description, forKey: .description)
try container.encodeIfPresent(location, forKey: .location)
try container.encodeIfPresent(startDate, forKey: .startDate)
try container.encodeIfPresent(endDate, forKey: .endDate)
try container.encodeIfPresent(notes, forKey: .notes)
try container.encode(packId, forKey: .packId)
try container.encode(localUpdatedAt, forKey: .localUpdatedAt)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 6 'UpdateTripRequest|\.updateTrip\s*\(' \
  apps/swift/Sources/PackRat --glob '*.swift'

Repository: PackRat-AI/PackRat

Length of output: 6578


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- Trip.swift ---'
sed -n '1,90p' apps/swift/Sources/PackRat/Models/Trip.swift

printf '%s\n' '--- TripService.swift ---'
sed -n '35,75p' apps/swift/Sources/PackRat/Services/TripService.swift

printf '%s\n' '--- TripFormView.swift ---'
sed -n '175,215p' apps/swift/Sources/PackRat/Features/Trips/TripFormView.swift

printf '%s\n' '--- TripsViewModel.swift ---'
sed -n '205,255p' apps/swift/Sources/PackRat/Features/Trips/TripsViewModel.swift

printf '%s\n' '--- OutboxService.swift ---'
sed -n '315,355p' apps/swift/Sources/PackRat/Services/OutboxService.swift

printf '%s\n' '--- API route references ---'
rg -n -C 8 "packId|UpdateTrip|updateTrip" packages/api/src/routes/trips/index.ts

Repository: PackRat-AI/PackRat

Length of output: 12084


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- all updateTrip call sites ---'
rg -n -C 5 '\bupdateTrip\s*\(' apps/swift --glob '*.swift'

printf '%s\n' '--- form pack selection state and initialization ---'
rg -n -C 8 'selectedPackId|existingTrip|TripFormView' \
  apps/swift/Sources/PackRat/Features/Trips/TripFormView.swift

printf '%s\n' '--- view-model update method ---'
rg -n -A 95 -B 8 'func updateTrip\s*\(' \
  apps/swift/Sources/PackRat/Features/Trips/TripsViewModel.swift

printf '%s\n' '--- mutation payload definition and uses ---'
rg -n -C 8 'struct TripMutationPayload|TripMutationPayload' \
  apps/swift/Sources/PackRat --glob '*.swift'

printf '%s\n' '--- update schema ---'
rg -n -C 12 'UpdateTripBodySchema' packages --glob '*.{ts,tsx}'

Repository: PackRat-AI/PackRat

Length of output: 28711


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

trip = Path("apps/swift/Sources/PackRat/Models/Trip.swift").read_text()
service = Path("apps/swift/Sources/PackRat/Services/TripService.swift").read_text()
form = Path("apps/swift/Sources/PackRat/Features/Trips/TripFormView.swift").read_text()
route = Path("packages/api/src/routes/trips/index.ts").read_text()

assert "let packId: String?" in trip
assert "try container.encode(packId, forKey: .packId)" in trip
assert "packId: String? = nil" in service
assert "packId: selectedPackId" in form
assert "if ('packId' in data) updateData.packId = data.packId ?? null;" in route

# Model the two relevant API states produced by this request type.
def encoded_pack_id(pack_id):
    return {"packId": pack_id}  # encode(Optional.none) writes JSON null

for value, expected in [("pack-1", "set"), (None, "explicit-clear")]:
    payload = encoded_pack_id(value)
    route_action = "update" if "packId" in payload else "leave-unchanged"
    if value is None:
        assert route_action == "update"
        assert expected == "explicit-clear"

# There is no third value in String? that can represent omission.
assert encoded_pack_id(None) == {"packId": None}
print("UpdateTripRequest has no representable omitted packId state.")
print("nil serializes as packId:null, and the API treats that as unassignment.")
PY

printf '%s\n' '--- relevant tests ---'
rg -n -i 'UpdateTripRequest|packId.*null|unassign|TripService.*update' \
  apps/swift --glob '*Test*.swift' --glob '*.swift' || true

Repository: PackRat-AI/PackRat

Length of output: 865


Preserve omitted and explicit-clear states for packId.

TripService.updateTrip defaults packId to nil. The custom encoder then sends "packId": null, and the API treats that value as unassignment. A String? cannot represent both omitted and explicit “None” states. Use a three-state representation and add serialization tests for omitted, set, and clear.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/swift/Sources/PackRat/Models/Trip.swift` around lines 41 - 57, Update
Trip and TripService.updateTrip to represent packId with distinct omitted,
assigned, and explicitly cleared states instead of a plain String?. Adjust
encode(to:) to omit packId for the omitted state while encoding a value or null
for the other states, and add serialization tests covering all three cases.

Comment on lines 134 to 152
if (q) {
const term = q.trim();
// Escape regex metacharacters: `q` is user input and reaches a POSIX
// regex operator below, where an unescaped `(` or `*` would either error
// or silently change the match semantics.
const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
// `\m` / `\M` are POSIX word-boundary escapes (start/end of word).
const wordBoundary = `\\m${escaped}\\M`;
const nameWord = sql`${catalogItems.name} ~* ${wordBoundary}`;
const descriptionWord = sql`COALESCE(${catalogItems.description}, '') ~* ${wordBoundary}`;
const categoriesWord = sql`${catalogItems.categories}::text ~* ${wordBoundary}`;

const searchCondition = or(
ilike(catalogItems.name, `%${q}%`),
ilike(catalogItems.description, `%${q}%`),
ilike(catalogItems.brand, `%${q}%`),
ilike(catalogItems.model, `%${q}%`),
ilike(sql`${catalogItems.categories}::text`, `%${q}%`),
nameWord,
descriptionWord,
categoriesWord,
ilike(catalogItems.brand, `%${term}%`),
ilike(catalogItems.model, `%${term}%`),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Ignore whitespace-only search terms.

Line 134 enters the search branch for q=' '. Line 135 then creates an empty term. The brand and model predicates become ILIKE '%%', so this request applies an unintended filter and relevance ordering instead of using the no-query result set.

Trim before the branch. Only build search conditions when the trimmed term is non-empty.

Proposed fix
-    if (q) {
-      const term = q.trim();
+    const term = q?.trim();
+    if (term) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (q) {
const term = q.trim();
// Escape regex metacharacters: `q` is user input and reaches a POSIX
// regex operator below, where an unescaped `(` or `*` would either error
// or silently change the match semantics.
const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
// `\m` / `\M` are POSIX word-boundary escapes (start/end of word).
const wordBoundary = `\\m${escaped}\\M`;
const nameWord = sql`${catalogItems.name} ~* ${wordBoundary}`;
const descriptionWord = sql`COALESCE(${catalogItems.description}, '') ~* ${wordBoundary}`;
const categoriesWord = sql`${catalogItems.categories}::text ~* ${wordBoundary}`;
const searchCondition = or(
ilike(catalogItems.name, `%${q}%`),
ilike(catalogItems.description, `%${q}%`),
ilike(catalogItems.brand, `%${q}%`),
ilike(catalogItems.model, `%${q}%`),
ilike(sql`${catalogItems.categories}::text`, `%${q}%`),
nameWord,
descriptionWord,
categoriesWord,
ilike(catalogItems.brand, `%${term}%`),
ilike(catalogItems.model, `%${term}%`),
);
const term = q?.trim();
if (term) {
// Escape regex metacharacters: `q` is user input and reaches a POSIX
// regex operator below, where an unescaped `(` or `*` would either error
// or silently change the match semantics.
const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
// `\m` / `\M` are POSIX word-boundary escapes (start/end of word).
const wordBoundary = `\\m${escaped}\\M`;
const nameWord = sql`${catalogItems.name} ~* ${wordBoundary}`;
const descriptionWord = sql`COALESCE(${catalogItems.description}, '') ~* ${wordBoundary}`;
const categoriesWord = sql`${catalogItems.categories}::text ~* ${wordBoundary}`;
const searchCondition = or(
nameWord,
descriptionWord,
categoriesWord,
ilike(catalogItems.brand, `%${term}%`),
ilike(catalogItems.model, `%${term}%`),
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/api/src/services/catalogService.ts` around lines 134 - 152, Update
the search flow around q and term so whitespace-only input is ignored: trim q
before the conditional branch, and only construct the regex, ILIKE predicates,
and searchCondition when the trimmed term is non-empty. Preserve the no-query
result behavior for empty or whitespace-only terms.

Comment on lines +166 to +189
it('still matches brand and model by substring', async () => {
const item = await seedCatalogItem({
name: 'Ultralight Shelter',
brand: 'Hilleberg',
description: 'No matching word in here.',
categories: ['Shelter'],
});

const res = await apiWithAuth('/catalog?q=Hille&limit=100');

expect(res.status).toBe(200);
const data = await expectJsonResponse(res, ['items']);
const ids = data.items.map((catalogItem: { id: number }) => catalogItem.id);
expect(ids).toContain(item.id);
});

it('treats regex metacharacters in the query as literal text', async () => {
const res = await apiWithAuth(`/catalog?q=${encodeURIComponent('tent(')}&limit=10`);

// An unescaped "(" would be an invalid POSIX regex and error the query.
expect(res.status).toBe(200);
await expectJsonResponse(res);
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Test the model branch and literal matching behavior.

Line 166 describes brand and model coverage, but the fixture only matches brand. Add a fixture where only model contains the query substring.

Lines 182-189 only verify that the request succeeds. Keep this error-regression test, but add a literal-match assertion. Use a metacharacter inside a word-ending query, such as t.ent, and assert that a literal T.ent item matches while a Tent item does not.

As per path instructions, tests must “test observable behaviour.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/api/test/catalog.test.ts` around lines 166 - 189, Extend the “still
matches brand and model by substring” test with a fixture whose model, not
brand, contains the query substring, and assert that it is returned. Update the
regex-metacharacter test to query an encoded word-ending value such as “t.ent”,
seed both a literal “T.ent” item and a “Tent” item, and assert the literal item
matches while the non-literal item does not, while preserving the successful
response assertion.

Source: Path instructions

@mikib0
mikib0 merged commit 3cb8223 into development Aug 11, 2026
24 of 27 checks passed
@mikib0
mikib0 deleted the worktree-swift-issues-2660-2693 branch August 11, 2026 13:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant