mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-11-02 13:12:50 +00:00
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:
1 parent
1cecaec4d3
commit
f9ef82f169
3 files changed
+40
-1
No files matched your search
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in new issue
Block a user