diff --git a/.claude/commands/rank.md b/.claude/commands/rank.md index 5fbca36..1b97dff 100644 --- a/.claude/commands/rank.md +++ b/.claude/commands/rank.md @@ -12,24 +12,28 @@ Follow these steps **in order**. `$ARGUMENTS` may contain: -- Nothing → rank all jobs with status `new` in `job_scraper/seen_jobs.json` +- Nothing → rank up to 10 jobs with status `new` in `job_scraper/seen_jobs.json` - A focus area (e.g. `/rank data science`) → rank only jobs whose title or stored fit-notes match the focus - `--all` → re-rank every job that has not been applied to, including previously ranked ones (useful after the profile changes) +- `--limit ` → maximum number of jobs to score this run (default 10) - `--top ` → shortlist size (default 5) +`--limit` bounds the expensive fetch-and-score work; `--top` only bounds how many scored jobs appear in the shortlist. They are independent: jobs beyond `--limit` are deferred, not silently discarded. + --- ## Step 1: Load State 1. Read `job_scraper/seen_jobs.json`. If the file is missing or has no entries, tell the user to run `/scrape` first and stop. 2. Read `job_search_tracker.csv`. Build the exclusion set: any company+role already in the tracker is out of scope regardless of flags - it has been applied to or consciously tracked. -3. Select candidates: entries with status `new` (or entries of any status with `--all`), minus the exclusion set, filtered by the focus area if one was given. -4. If no candidates remain, say so ("Nothing new to rank - run /scrape to find fresh postings") and stop. -5. Read the scoring framework and profile **once**: +3. Select eligible candidates: entries with status `new` (or entries of any status with `--all`), minus the exclusion set, filtered by the focus area if one was given. +4. Apply `--limit` after those filters. Keep at most N eligible candidates for this run and count every remaining eligible candidate as deferred. Deferred jobs keep their current status so a later `/rank` run continues the backlog. +5. If no candidates remain, say so ("Nothing new to rank - run /scrape to find fresh postings") and stop. +6. Read the scoring framework and profile **once**: - `.claude/skills/job-application-assistant/04-job-evaluation.md` - `.claude/skills/job-application-assistant/01-candidate-profile.md` -State how many jobs will be ranked before proceeding. +State how many jobs will be ranked and how many are deferred before proceeding. --- @@ -117,6 +121,7 @@ Do not modify `job_search_tracker.csv` - that file records applications, and `/r Ranked new postings ( shortlisted, below threshold, expired/vetoed). Swept previously ranked entries ( newly expired, closing soon). + jobs deferred to the next run - re-run `/rank` to continue. ### Shortlist diff --git a/CHANGELOG.md b/CHANGELOG.md index fd15ef6..3a32c78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,11 @@ per-file diff commands. ### Fixed +- **`/rank` now bounds each scoring batch** (#395) - a bare run scores at most 10 + eligible jobs instead of attempting the entire backlog. `--limit ` controls + scoring independently of `--top`, and the report makes deferred work visible so + re-running `/rank` can continue it. + - **The portal CLIs' unknown-flag guard no longer lets a single-dash flag through** (#426) - the guard in the four bunli-based CLIs (`jobnet`, `jobbank`, `jobindex`, `jobdanmark`) inspected only tokens starting with `--`, so an undefined *short* flag bypassed it entirely: bunli diff --git a/tests/test_rank_command.py b/tests/test_rank_command.py index 583d480..9dd8069 100644 --- a/tests/test_rank_command.py +++ b/tests/test_rank_command.py @@ -484,5 +484,38 @@ class PostedDateStalenessSpec(unittest.TestCase): ) +class RankBatchLimitSpec(unittest.TestCase): + """The expensive fetch-and-score batch is bounded independently of output.""" + + def setUp(self): + self.sections = _sections(COMMAND.read_text(encoding="utf-8")) + + def test_step0_documents_default_limit_distinct_from_top(self): + step0 = self.sections.get("Step 0: Parse Input", "") + self.assertIn("`--limit `", step0) + self.assertIn("default 10", step0) + self.assertIn("`--top `", step0) + self.assertIn( + "They are independent", + step0, + "--limit must bound scoring without being confused with shortlist size", + ) + + def test_step1_applies_limit_after_eligibility_filters(self): + step1 = self.sections.get("Step 1: Load State", "") + self.assertIn("Apply `--limit` after those filters", step1) + self.assertIn("count every remaining eligible candidate as deferred", step1) + self.assertIn( + "keep their current status", + step1, + "deferred jobs must remain eligible for a later run", + ) + + def test_step5_reports_deferral_and_how_to_continue(self): + report = self.sections.get("Job Ranking - YYYY-MM-DD", "") + self.assertIn("jobs deferred", report) + self.assertIn("re-run `/rank` to continue", report) + + if __name__ == "__main__": unittest.main()