Skip to content

Phase 4: 検索アーキテクチャ - #4

Merged
tukuyomil032 merged 7 commits into
mainfrom
phase4-search-architecture
Aug 23, 2026
Merged

Phase 4: 検索アーキテクチャ#4
tukuyomil032 merged 7 commits into
mainfrom
phase4-search-architecture

Conversation

@tukuyomil032

Copy link
Copy Markdown
Owner

Summary

  • SearchViewModel: デバウンス300ms・直近同一クエリ5秒以内は再送なし・stale-while-revalidate(キャッシュ即時反映→バックグラウンドAPI検索→ノイズ除去/分類→キャッシュ反映)を実装
  • 縮小版CacheRefreshScheduler: 起動時に前回トップカテゴリのみ1回軽量更新、詳細画面のReleases(TTL1h)/README(TTL24h)遅延再検証
  • CachedRepositorylastReadmeFetchedAt追加、Defaultsパッケージ初導入(前回トップカテゴリ永続化、読み出しのみ・書き込みはPhase5)
  • CategoryClassifierclassify(topics:name:readme:)オーバーロードを追加(CacheRefreshSchedulerからのSwiftDataモデル経由の再分類用)
  • 時間依存ロジック(デバウンス/TTL)はClock注入・固定nowクロージャで実待機なしにテスト

Test plan

  • swift build成功
  • swift test(79テスト全通過、複数回実行してflakinessなし確認済み)
  • just format / swiftlintクリーン
  • swift run Cairnで起動クラッシュなしを確認

詳細な設計判断(NoiseFilter適用タイミング、README失敗時のTTL扱い等、ユーザー確認済み事項)はdocs/progress.mdのPhase4セクション参照。

🤖 Generated with Claude Code

https://claude.ai/code/session_01Wqgz7yinLxemFVe6f3NVCg

tukuyomil032 and others added 7 commits August 23, 2026 10:03
README fetch TTL (24h) tracking requires its own timestamp field, separate from lastReleaseCheckedAt. Adds an optional Date field with a lightweight-migration-friendly nil default.

Phase4 prep task 1/6. See docs/progress.md Phase4 and the approved plan.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CacheRefreshScheduler needs to persist which category was last shown so it can do a single lightweight refresh of it on launch. Category now conforms to Defaults.Serializable so it can be stored via the Defaults package.

Read-only usage for now (Phase4). The write side (updating the key when a user selects a category) is added in Phase5's Discovery UI.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… search

Adds the core Phase4 search flow: query input is debounced 300ms, an identical query sent within the last 5s is not resent, and the cache is shown immediately while an API search runs in the background to reconcile it. NoiseFilter (dmg/zip asset check) is applied at the list stage per the user's explicit choice, even though this costs one Releases API call per result.

Time-dependent logic (debounce/dedup) is tested via an injected Clock protocol rather than real sleeps: SearchViewModel is generic over ClockType, with a ContinuousClock convenience init for production and a hand-rolled ManualClock for tests (Tests/CairnTests/Features/Search/ManualClock.swift) that advances time on demand. MockGitHubClient is an actor to safely record call counts/queries across the @mainactor view model boundary.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…fier

CacheRefreshScheduler needs to reclassify a CachedRepository (a SwiftData model) after fetching its README, but it doesn't have a Repository (API response struct) to hand to the existing classify(repository:readme:) method. Adds a classify(topics:name:readme:) overload that the existing method now delegates to, so both call sites share the same scoring logic.

CategoryClassifying is the only conformer of its own protocol today, so adding the new requirement is not a breaking change in practice. Existing CategoryClassifierTests pass unchanged; one new test covers the overload directly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…validation

Implements the other half of Phase4: a one-shot lightweight refresh of the last top category on launch (read-only against Defaults.lastTopCategory; the write side lands with Phase5's Discovery UI), plus independent TTL-gated revalidation of Releases (1h) and README (24h) for a repository's detail view. A failed README fetch does not update its TTL or touch the existing classification, so the next visit can retry rather than being stuck with a stale-but-fresh-looking category for 24h.

Extracts the GitHub search query construction (repo-language filter) shared by SearchViewModel and this scheduler into GitHubSearchQueryBuilder, since SearchViewModel's version was tied to its generic ClockType for no reason. CacheRefreshSchedulerTests cover the launch refresh, the no-op when no category is set, and TTL boundary behavior for both Releases and README using a fixed now() closure (same injection pattern as AuthenticationState).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds the minimal call site for the launch-time top-category refresh, without touching CairnEnvironment's structure (its full DI expansion is Phase5's job). ContentView's .task builds a GitHubClient/CacheRefreshScheduler on the spot and calls refreshTopCategoryOnLaunch() once.

Verified via 'swift run Cairn' that the app still launches without crashing with this wired in.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Checks off the Phase4 checklist in docs/progress.md and records the design decisions confirmed with the user during planning (DI approach, lastReadmeFetchedAt field, NoiseFilter applied at list stage, README failure TTL handling, launch-refresh wiring) plus a summary of the added test coverage.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@tukuyomil032
tukuyomil032 merged commit a7a0fb9 into main Aug 23, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant