mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-09-17 00:26:26 +00:00
fix(rank): treat non-ISO stored deadlines as absent in urgency and the sweep
Rule 6's expiry sweep mutates status automatically from stored deadline
values, yet had no rule for the non-ISO shapes portals have shipped into
seen_jobs.json ("ASAP", DD.MM.YYYY, free text) - "ASAP" is incomparable
and "01.09.2026" is ambiguous between 1 Sep and 9 Jan. /outcome, which
merely displays dates, already carried the defensive-parse rule. A
non-YYYY-MM-DD stored value is now handled like an absent one (left
alone, never compared, never guessed at) and reported once with its
portal. Includes the F24-style coupling test. Review finding F17
(2026-08-19).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
9ab697de64
commit
57e82d2b59
@@ -73,8 +73,8 @@ Back in the main context, for each scored job:
|
||||
2. Map to the framework's verdict bands (Strong Fit 75+, Good Fit 60-74, Moderate Fit 45-59, Weak Fit 30-44, Poor Fit <30).
|
||||
3. **Location veto:** `FAIL` (e.g. requires relocation) excludes the job from the shortlist no matter the score - list it separately with the reason. `FLAG` (e.g. heavy travel) stays in the ranking but carries a visible ⚠ marker for the user to judge.
|
||||
4. **Language veto:** `language_gate: FAIL` (posting requires a language the candidate hasn't declared at all) excludes the job from the shortlist, same as a location FAIL - list it under "Excluded" with the quoted requirement from `language_note`. `language_gate: FLAG` (declared language, requirement reads above the declared level) stays in the ranking with a visible ⚠ marker and `language_note` shown alongside the score, same treatment as a location FLAG.
|
||||
5. **Deadline urgency:** a deadline within 7 days gets a 🔥 marker and wins ties. A deadline that has already passed moves the job to `expired`. Take the deadline from the scoring agent's Step 2 JSON for a job scored in this run, and from the stored `deadline` in `seen_jobs.json` for one that already carries it - a stored value costs no fetch, so urgency is re-derived on every run without re-reading the posting. When both exist and disagree, the freshly scored value wins and replaces the stored one.
|
||||
6. **Expiry sweep over already-ranked entries.** Before presenting, check the stored `deadline` of every `ranked` entry this run did not re-score. Any whose deadline has passed becomes `expired`; any within 7 days is listed under a short **Closing soon** heading in Step 5 with its 🔥 marker. This needs no fetch and no agent - it is a date comparison against values already on disk, and it is what finally enforces `/scrape`'s "only open positions" rule beyond the moment of fetching. **An entry with no stored `deadline` is left alone, never guessed at** - most entries predate the column, and inferring a deadline from `first_seen` would retire jobs on a date nobody set. `--all` re-scores entries of any status including `expired`, so a job the sweep retired can still be revived by a later `--all` that re-fetches it and finds the posting live: the sweep is reversible, which is what makes an automated status change acceptable here at all.
|
||||
5. **Deadline urgency:** a deadline within 7 days gets a 🔥 marker and wins ties. A deadline that has already passed moves the job to `expired`. Take the deadline from the scoring agent's Step 2 JSON for a job scored in this run, and from the stored `deadline` in `seen_jobs.json` for one that already carries it - a stored value costs no fetch, so urgency is re-derived on every run without re-reading the posting. When both exist and disagree, the freshly scored value wins and replaces the stored one. A stored value that does not parse as `YYYY-MM-DD` is skipped for urgency as well - rule 6's defensive-parse rule applies wherever a stored deadline is compared.
|
||||
6. **Expiry sweep over already-ranked entries.** Before presenting, check the stored `deadline` of every `ranked` entry this run did not re-score. Any whose deadline has passed becomes `expired`; any within 7 days is listed under a short **Closing soon** heading in Step 5 with its 🔥 marker. This needs no fetch and no agent - it is a date comparison against values already on disk, and it is what finally enforces `/scrape`'s "only open positions" rule beyond the moment of fetching. **An entry with no stored `deadline` is left alone, never guessed at** - most entries predate the column, and inferring a deadline from `first_seen` would retire jobs on a date nobody set. **Parse stored deadlines defensively:** a stored value that is not a `YYYY-MM-DD` date is treated exactly like an absent one - left alone, never compared, never guessed at - and reported once in the Step 5 summary with its portal, so the bad value gets traced to its source instead of silently steering the sweep (portals have shipped `"ASAP"`, `DD.MM.YYYY`, and free-text deadline shapes into stored data). `--all` re-scores entries of any status including `expired`, so a job the sweep retired can still be revived by a later `--all` that re-fetches it and finds the posting live: the sweep is reversible, which is what makes an automated status change acceptable here at all.
|
||||
|
||||
Sort by overall score (descending), urgency as tiebreaker.
|
||||
|
||||
|
||||
@@ -56,6 +56,13 @@ per-file diff commands.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`/rank`'s expiry sweep parses stored deadlines defensively** - the sweep changes
|
||||
status automatically from a date comparison against values on disk, but portals have
|
||||
shipped non-ISO shapes into `seen_jobs.json` (`"ASAP"`, `DD.MM.YYYY`, free text), and
|
||||
`/rank` had no rule for them while the display-only `/outcome` already did. A stored
|
||||
deadline that is not `YYYY-MM-DD` is now treated exactly like an absent one wherever a
|
||||
stored deadline is compared (urgency and sweep), and reported once with its portal.
|
||||
Pinned by `tests/test_rank_command.py`.
|
||||
- **Language Gate preamble no longer claims the gate is untracked** (`framework_version`
|
||||
1.2.3 -> 1.2.4 in `04-job-evaluation.md`) - the paragraph still said the result "is not
|
||||
a field `/scrape` or `/rank` track", written before the gate was wired into both
|
||||
|
||||
@@ -132,6 +132,25 @@ class RankCommandSpec(unittest.TestCase):
|
||||
"stale claim: the gate result IS tracked by /scrape and /rank now",
|
||||
)
|
||||
|
||||
def test_sweep_parses_stored_deadlines_defensively(self):
|
||||
"""Rule 6's expiry sweep mutates status automatically from stored
|
||||
deadline values, and portals have shipped non-ISO shapes into
|
||||
seen_jobs.json ("ASAP" from jobindex, DD.MM.YYYY from jobbank,
|
||||
free text from jobdanmark's detail fallback). /outcome carries a
|
||||
defensive date-parse rule for mere display; the command that
|
||||
silently changes state needs one at least as much."""
|
||||
text = COMMAND.read_text(encoding="utf-8")
|
||||
self.assertIn(
|
||||
"Parse stored deadlines defensively",
|
||||
text,
|
||||
"rule 6's sweep must state the defensive-parse rule",
|
||||
)
|
||||
self.assertRegex(
|
||||
text,
|
||||
r"not a `YYYY-MM-DD` date[^.]*treated exactly like an absent one",
|
||||
"a non-ISO stored deadline must be handled as absent, not compared or guessed at",
|
||||
)
|
||||
|
||||
def test_step2_schema_includes_language_gate_fields(self):
|
||||
sections = _sections(COMMAND.read_text(encoding="utf-8"))
|
||||
step2 = sections.get("Step 2: Batch-Fetch and Score", "")
|
||||
|
||||
Reference in New Issue
Block a user