fix(rank): sweep deadlines when no candidates need scoring (#494)

* fix(rank): sweep deadlines when no candidates need scoring

* changelog: record the /rank empty-candidate sweep under [Unreleased] / Fixed
This commit is contained in:
Ayobami Adegoke authored and GitHub committed 2026-10-02 20:00:16 +02:00
1 parent 1cecaec4d3
commit f9ef82f169
3 files changed
+40 -1

No files matched your search

+9 -1
View File
@@ -32,7 +32,15 @@ python3 tools/rank_state.py candidates --limit 10 # add --all / --focus
It applies the status filter (`new`, or any status with `--all`), the tracker exclusion (any company+role already in `job_search_tracker.csv` is out of scope regardless of flags - it has been applied to or consciously tracked), the focus filter, and `--limit`, then prints one compact object per candidate (`key`, `title`, `company`, `url`, `portal`, `deadline`, `posted_date`) plus the counts: `eligible`, `deferred` (eligible beyond the limit, kept at their current status so a later run continues the backlog), `excluded_by_tracker`.
If it reports no candidates, say so ("Nothing new to rank - run /scrape to find fresh postings") and stop. If it exits with "not found", tell the user to run `/scrape` first and stop.
If it exits with "not found", tell the user to run `/scrape` first and stop.
If it reports no candidates, there is nothing to score, but already-ranked jobs still need Step 3's rule 6 expiry sweep. Run:
```bash
python3 tools/rank_state.py sweep --write
```
Skip profile loading and Steps 2-4 on this path: no jobs need fetching or scoring. Present a sweep-only Step 5 summary: say "Nothing new to rank", report `swept` and `newly_expired`, list `closing_soon` with deadlines and posting URLs, and report any `unparseable_deadlines` with their portals. Apply rule 6's same reporting and defensive-date rules. Then suggest `/scrape` for fresh postings and stop. An empty candidate batch, including one caused by a focus filter or tracker exclusion, never skips this sweep.
Then read the scoring framework and profile **once**:
- `.claude/skills/job-application-assistant/04-job-evaluation.md`
+16
View File
@@ -15,6 +15,22 @@ per-file diff commands.
### Fixed
- **`/rank` still sweeps deadlines when there is nothing new to score** (`.claude/commands/rank.md`
Step 1, `tests/test_rank_command.py`) - when `rank_state.py candidates` reported no eligible
jobs, Step 1 said "Nothing new to rank" and stopped before Step 3's rule 6 expiry sweep ever
ran. Once a backlog has been ranked, that is the path every later `/rank` takes, so
past-deadline jobs stayed `ranked` and approaching-deadline reminders were never shown.
Reproduced on the real CLI with four `ranked` entries (deadlines 2026-09-22, 2026-09-28,
null, `ASAP`): `candidates --today 2026-09-25` returns `eligible: 0` and master stops there,
while `sweep --write` on the same state expires the 09-22 entry, lists 09-28 under
`closing_soon` and `ASAP` under `unparseable_deadlines`, and leaves the null one alone. The
empty-candidate branch of Step 1 now runs `python3 tools/rank_state.py sweep --write`, skips
profile loading and Steps 2-4 (nothing to fetch or score; the tracker is untouched), and
presents a sweep-only Step 5 summary - `swept`, `newly_expired`, `closing_soon` with deadlines
and URLs, `unparseable_deadlines` with portals - before suggesting `/scrape`. An empty batch
caused by a focus filter or tracker exclusion takes the same path. Pinned by
`test_step1_empty_candidates_still_sweeps_and_reports`, which fails against master's `rank.md`.
- **`robots_check` no longer reads a leading BOM or an undecodable rule as permission**
(`tools/robots_check.py`, `tests/test_robots_check.py`) - two decoding edge cases failed
open, both flagged as follow-ups in #506. A robots.txt saved with a UTF-8 byte-order mark
+15
View File
@@ -43,6 +43,21 @@ def _sections(text: str) -> dict[str, str]:
class RankCommandSpec(unittest.TestCase):
def test_step1_empty_candidates_still_sweeps_and_reports(self):
"""No new jobs must not bypass expiry maintenance of ranked jobs."""
step1 = _sections(COMMAND.read_text(encoding="utf-8"))["Step 1: Load State"]
empty_branch = step1.partition("If it reports no candidates")[2].partition("Then read")[0]
self.assertIn(
"python3 tools/rank_state.py sweep --write",
empty_branch,
"an empty candidate batch must still persist rule 6's expiry sweep",
)
self.assertNotIn("--exclude", empty_branch, "no jobs were re-scored on this path")
for field in ("swept", "newly_expired", "closing_soon", "unparseable_deadlines"):
self.assertIn(field, empty_branch, f"the empty-run report must consume {field}")
self.assertIn("Skip", empty_branch)
self.assertIn("Steps 2-4", empty_branch, "an empty run must not fetch or score jobs")
def test_command_file_exists_with_lint_compliant_header(self):
self.assertTrue(COMMAND.is_file(), "command spec missing")
first_line = COMMAND.read_text(encoding="utf-8").splitlines()[0]