feat: multiple profiles fully wired (per-profile scoring + Discover queue) - #35
Merged
Conversation
Set PRAGMA busy_timeout=5000 alongside WAL + foreign_keys in the per-connection listener, so a connection waits up to 5s for a contended write lock instead of failing immediately with "database is locked". WAL keeps readers non-blocking, but the daemon writer and a TUI-driven write can still briefly contend (PROJECT.md §4.1, §17) — the more so now that the daemon scores every profile. A test asserts the pragma on a fresh engine.
Read match_score.profile_id as a filter (it was written but never read), so scoring and the Discover queue are per-profile rather than single-active: - repository: list_unscored_postings(session, profile_id) and list_scored_postings(session, profile_id) filter by profile — a posting is "unscored"/"scored" *for a profile*, not globally; get_latest_match_score gains an optional profile_id. - service: score_posting(session, posting_id, *, profile, ...) takes the profile from the caller instead of resolving the active one internally, so the same posting can be scored under several profiles (each keeps its own append-only history). NoActiveProfileError moves to the callers. - CLI: `atlas score <id> [--profile <id>]` scores a chosen profile (active by default; unknown id → exit 1); `_score_after_add` resolves the active one. - TUI: build_discover_queue shows the active profile's queue (empty when none active); build_application_detail shows the fit for the application's own profile. The daemon poll still scores the active profile this commit (it becomes score-every-profile with the claim lease next). 100% line+branch; mypy --strict incl. win32.
The daemon poll now clears every profile's backlog, not just the active one, so each profile has a complete ranked queue the instant the user switches to it — guarded by a lightweight scoring lease (PROJECT.md §4.1's "owned by" convention) so a concurrent writer never double-scores. - ScoreClaim table + Alembic migration: one row per (posting, profile) pair being scored, unique on the pair. - matching.claims: try_claim (fresh → claim; stale past the lease → steal; live → refuse) and release_claim, pure over a session with an injected clock + lease TTL. - run_scoring_poll(session, *, provider, owner, clock): loops list_profiles and, per unscored pair, claims → scores → releases; a pair another worker holds live is skipped and counted in the new PollOutcome.claimed. No profiles → a benign empty poll. - daemon start threads its pid as the claim owner. 100% line+branch; mypy --strict incl. win32; migration verified against a temp DB (score_claim + uq_score_claim_pair present).
The Discover queue now shows the active profile's ranked scores, and a profile switcher lets the user re-rank it to another profile in place — without dropping to the CLI. Since the daemon scores every profile, the chosen profile's queue is already populated. - ProfilePickerScreen: a modal (mirroring StatusPickerScreen) listing the profiles (the active one marked); dismisses with the chosen id or None. - build_profile_choices: the pure builder behind it (testable off-Textual). - DiscoverScreen: a `p` binding → push the picker; on choice, AtlasApp's new switch_profile (set_active_profile in a session_scope) applies it and the queue re-ranks. An empty profile list is a safe no-op. 100% line+branch on the touched TUI modules; mypy --strict incl. win32; full suite green (1083 tests).
Tick the Phase 2 "Multiple profiles fully wired" roadmap item (PROJECT.md §15), record the per-profile scoring + score_claim "owned by" lease + busy_timeout under §4.1 / §6, add a STATUS.md "What has landed" entry and phase-table update, move "Next up" to the IPC surface, and add the Unreleased changelog entries.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Wires multiple search profiles all the way through scoring and the Discover queue (PROJECT.md §2.1/§5.6; the top unchecked Phase 2 roadmap item). Profiles already existed, and the schema was already multi-profile (
match_score.profile_id), butprofile_idwas written and never read — scoring and the queue behaved single-active-profile. Now the daemon scores every profile, each profile has its own ranked queue, and the user can switch profiles from the TUI.Key commits
feat(db): add busy_timeout PRAGMA— a 5 s busy timeout alongside WAL + foreign keys, so a contended daemon-writer / TUI-reader waits rather than failing with "database is locked".feat(matching): scope fit scoring to a profile—list_unscored_postings/list_scored_postings/get_latest_match_scoreare per-profile;score_posting(..., profile=...)takes the profile from its caller;atlas score --profile <id>; the Discover queue + application detail read their profile's scores.feat(daemon): score every profile with a row-claim lease—ScoreClaimtable + Alembic migration +atlas.matching.claims(PROJECT.md §4.1's "owned by" lease);run_scoring_pollloopslist_profiles, claiming each(posting, profile)pair so a concurrent writer never double-scores; the daemon's pid is the owner.feat(tui): switch profiles from the Discover queue—ProfilePickerScreenmodal (pressp),build_profile_choices, andAtlasApp.switch_profilere-rank the queue in place.docs: record multiple-profile scoring + claim convention.Design notes
match_score.profile_idalready existed; the change is reading it as a filter.Profile.activesingle-active invariant is unchanged — "active" stays the default/UI selection; scoring iterateslist_profilesexplicitly.try_claimsteals a claim older than its lease so a crashed worker can't wedge a pair.Follow-ups (out of scope)
The daemon IPC surface (the last unchecked "Daemon + scheduler + IPC" sub-item) and desktop notifications finish Phase 2.
Testing
ruff format --check+ruff check— cleanmypy --stricton Linux and win32 — no issues (274 files each)pytest --cov=atlas --cov-branch --cov-fail-under=100— 1083 passed, 100% line + branchNew/changed logic is exercised hermetically:
try_claimlease states with an injected clock;run_scoring_pollscoring two profiles × two postings and skipping a pair another owner holds; the migration verified against a temp DB (score_claim+ its unique constraint); and a TextualPilottest driving thepswitcher so the queue re-ranks to the chosen profile.Refs PROJECT.md §2.1, §4.1, §5.6, §15 (Phase 2).