Skip to content

feat(tui): Discover screen — ranked queue of scored postings - #31

Merged
Harikeshav-R merged 5 commits into
mainfrom
feat/tui-discover-queue
Aug 5, 2026
Merged

feat(tui): Discover screen — ranked queue of scored postings#31
Harikeshav-R merged 5 commits into
mainfrom
feat/tui-discover-queue

Conversation

@Harikeshav-R

Copy link
Copy Markdown
Owner

What & why

Builds the Discover screen (PROJECT.md §8 screen #2): a ranked queue of scored postings in
the TUI. Until now, everything the daemon discovers and scores piled up invisibly — the TUI
had no screen listing scored postings, so a discovered posting that was never tailored was
unreachable from the UI. This closes Journey B (background discovery → review → tailor): the
daemon finds and scores jobs, and DiscoverScreen (press w) presents them ranked by fit with
the full §8 action set.

Key commits

  1. feat(db): add job_posting.queue_status for the Discover queue — a QueueStatus
    (new/saved/dismissed) enum + the JobPosting.queue_status column, with an Alembic
    migration (server_default='new' so existing rows backfill). Posting-level triage, distinct
    from the application-level ApplicationStatus.
  2. feat(matching): rank scored postings by fitlist_scored_postings (each posting + its
    latest MatchScore via a correlated subquery, scored-only, dismissed excluded, ordered by
    score desc) + set_posting_queue_status.
  3. feat(platform): add the URL-open boundary — a UrlOpener seam (default_url_opener via
    webbrowser.open) mirroring the file opener, so the queue opens a posting's apply URL
    cross-platform while the suite stays hermetic.
  4. feat(tui): add the Discover screen with tailor/dismiss/save/open actions — the
    build_discover_queue builder + DiscoverScreen (ranked DataTable + rationale pane) with
    Enter→detail, t tailor (thread worker → new application detail), x dismiss, s save, o
    open URL; a Tailor action added to Posting detail (§8 screen docs: adopt LiteLLM + AI patterns from Resume-Matcher, add docs/agent #3); app wiring (w binding,
    action_discover, injected url_opener, set_queue_status, run_open_url).
  5. docs: record the Discover screen — STATUS / PROJECT §15 / CHANGELOG / README.

Migration

One new Alembic migration adds job_posting.queue_status (NOT NULL, server_default='new'),
chaining onto the add_cover_letter head. Existing postings backfill to new; no data
migration needed.

Reuse

AtlasApp.run_tailor(posting_id) and tailor_posting already operate on a bare posting (they
create the Application), so Tailor needed no new service; the thread-worker pattern comes from
tailor_workspace.py; PostingDetailScreen, verdict_style, and _fit_text are reused.

Testing

Fully hermetic (AGENTS.md §6.2): builder + repository tests over the in-memory db_engine
(ranking, latest-not-highest, tiebreak, dismissed/saved/unscored filters, salary-display
variants), a FakeUrlOpener for the URL boundary, and Pilot-driven TUI tests for every screen
path (mount + rank + rationale pane, empty queue, drill-in, dismiss/save persistence, open-URL
success + error, tailor → application with browse-only-disabled / worker-error / empty-queue
branches, and the same for Posting detail's tailor). Gates green: ruff format --check,
ruff check, mypy --strict (incl. --platform win32), and 895 tests at 100% line+branch
coverage
. No new dependency (webbrowser is stdlib).

Design notes for review

  • queue_status is a single enum-ish column (not two booleans): mutually-exclusive states,
    one default, one server_default. Dismissed drops out of the queue; saved stays, flagged.
  • DataTable has no native expandable rows, so the AI rationale shows in a detail pane
    updated on row-highlight (rows map to postings by cursor-row index).
  • Tailor uses push_screen (not switch_screen) so escape returns to the queue.
  • o opens the apply URL via the new UrlOpener, not the file opener (which requires an
    on-disk path).
  • Bindings: w for Discover (d/a/q were taken); x for dismiss to avoid shadowing
    the global d, and because Screen.action_dismiss already exists (the action is
    action_pass_posting).

The posting-level triage state the TUI Discover queue (PROJECT.md §8 screen #2)
needs, distinct from the application-level ApplicationStatus:

- QueueStatus StrEnum (new/saved/dismissed) in atlas.matching.structure,
  co-located with Verdict.
- JobPosting.queue_status column (default 'new'), with an Alembic migration
  chaining onto the cover_letter head. The column is NOT NULL with a
  server_default of 'new' so every existing posting backfills without a data
  migration; new inserts default to 'new' from the model. Dismissed postings
  are later filtered out of the ranked queue.

Tests: the model default, a queue_status round-trip, and that upgrade_to_head
adds the NOT NULL 'new'-defaulted column to job_posting. 100% line+branch.
The data layer beneath the TUI Discover queue (PROJECT.md §8 screen #2):

- list_scored_postings(session) -> list[(JobPosting, MatchScore)]: each posting
  paired with its LATEST score (the max-id row per posting, via a correlated
  subquery — matching get_latest_match_score's newest-append-only-row-wins
  convention, not created_at), inner-joined so unscored postings are excluded,
  dismissed postings filtered out, ordered by score desc then newest posting
  first. The subquery aliases MatchScore and correlates JobPosting explicitly to
  avoid SQLAlchemy auto-correlation.
- set_posting_queue_status(session, posting_id, status) in scrape.repository
  (co-located with JobPosting CRUD) — backs the dismiss/save actions.

Tests over db_engine: score-desc ordering, id-desc tiebreak, latest-not-highest
(a lower newer score ranks by the latest), dismissed excluded, saved included,
unscored excluded, empty; the mutator persists each status and raises on an
unknown id. 100% line+branch, mypy --strict clean (incl. win32).
The Discover queue's 'open' action launches a posting's apply URL in the
browser, which the file opener can't do (default_file_opener requires the
target to exist on disk). Add a UrlOpener seam mirroring FileOpener:

- atlas.platform.browser: UrlOpenError, a runtime-checkable UrlOpener Protocol
  (__call__(url) -> None), and default_url_opener backed by webbrowser.open
  (raises UrlOpenError when it returns False or the platform errors), the real
  launcher pragma'd like default_file_opener.
- Exported from atlas.platform.
- FakeUrlOpener (records .opened, optional raises) in the shared conftest.

Tested: the fake satisfies the Protocol, records URLs, and raises. 100%
line+branch, mypy --strict clean (incl. win32).
The ranked queue of scored postings (PROJECT.md §8 screen #2) — the piece that
makes the daemon's discovery/scoring work visible and actionable, closing
Journey B (background discovery → review → tailor).

- tui.data.build_discover_queue → DiscoverQueue/DiscoverRow: maps the ranked
  list_scored_postings into display rows (company, source, a salary-display
  string, score/verdict/rationale, queue state).
- tui.screens.discover.DiscoverScreen: a DataTable ranked by fit with the AI's
  rationale in a detail pane (updated as the cursor moves). Enter drills into
  PostingDetailScreen; x dismisses (hides from the queue); s saves; o opens the
  apply URL via the injected UrlOpener; t tailors off the event loop (the
  tailor_workspace thread-worker pattern) and, on success, pushes the new
  application's detail. Row-position indexing keeps the row→posting mapping
  simple and fully covered.
- PostingDetailScreen gains the §8 screen-#3 Tailor action (same worker pattern).
- AtlasApp: a new 'w' Discover binding + action_discover, an injected url_opener,
  and set_queue_status / run_open_url methods (Dashboard stays the landing screen).

Pilot tests drive every path: mount + rank + rationale pane, empty queue,
enter→detail, dismiss/save persistence, open-URL success + error, tailor →
application (browse-only disabled, worker error handled, empty-queue no-op), and
the same for PostingDetailScreen's tailor; plus builder tests over db_engine for
the salary-display variants and the dismissed filter. 100% line+branch, mypy
--strict clean (incl. win32).
Update the docs for the Discover-queue slice (Definition of Done, AGENTS.md §8):

- STATUS.md: bump 'Last updated' + 'Current phase'; add the 'What has landed'
  entry for the DiscoverScreen; drop the Discover-queue item from 'Next up' and
  re-order the rest (more ATS adapters, aggregators, multiple profiles, IPC);
  tick the Phase-2 progress row.
- PROJECT.md §15: check off the scored Discover queue item.
- CHANGELOG.md (Unreleased/Added): the Discover screen + actions,
  list_scored_postings, JobPosting.queue_status + migration, and the UrlOpener
  seam.
- README.md: note the Discover queue (press w) in the TUI walkthrough.
@Harikeshav-R
Harikeshav-R merged commit fe6d015 into main Aug 5, 2026
10 checks passed
@Harikeshav-R
Harikeshav-R deleted the feat/tui-discover-queue branch August 5, 2026 08:46
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