mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-09-17 00:26:26 +00:00
feat(rank): persist triage gaps and strengths into seen_jobs.json (#263)
/rank's Step 2 scoring agents already return strengths and gaps per job, but Step 4 only persisted rank_score/rank_verdict/rank_date - both arrays were printed once in Step 5 and then discarded. Store them verbatim in seen_jobs.json (replaced, not accumulated, on --all re-ranks) so downstream consumers can read real triage findings instead of re-deriving them. Discussed in #258.
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
"""Guards for the /rank command spec.
|
||||
|
||||
The command is a markdown spec (the spec IS the implementation), so these
|
||||
tests pin the invariants that would break silently: the header format that
|
||||
lint_skills.py enforces, and the persistence of scoring-agent gaps/strengths
|
||||
into seen_jobs.json (previously computed in Step 2 and thrown away after
|
||||
Step 5's terminal output).
|
||||
"""
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import yaml # noqa: F401 - only probing availability for the lint integration test
|
||||
_HAVE_YAML = True
|
||||
except ImportError:
|
||||
_HAVE_YAML = False
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
COMMAND = REPO / ".claude" / "commands" / "rank.md"
|
||||
SCRAPER_SKILL = REPO / ".claude" / "skills" / "job-scraper" / "SKILL.md"
|
||||
|
||||
|
||||
def _sections(text: str) -> dict[str, str]:
|
||||
"""Split a command spec into {heading: body} by '##' headers.
|
||||
|
||||
Splitting this way lets a fork's extra sections (e.g. this fork's
|
||||
'## Blocker logging') sit between the ones under test without shifting
|
||||
which text a given assertion sees.
|
||||
"""
|
||||
parts = text.split("\n## ")
|
||||
result = {}
|
||||
for part in parts[1:]:
|
||||
heading, _, body = part.partition("\n")
|
||||
result[heading.strip()] = body
|
||||
return result
|
||||
|
||||
|
||||
class RankCommandSpec(unittest.TestCase):
|
||||
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]
|
||||
self.assertTrue(
|
||||
first_line.startswith("# /rank"),
|
||||
f"header must start with '# /rank' (lint_skills.py enforces it), got: {first_line!r}",
|
||||
)
|
||||
|
||||
def test_step4_persists_gaps_and_strengths(self):
|
||||
sections = _sections(COMMAND.read_text(encoding="utf-8"))
|
||||
step4 = sections.get("Step 4: Update State", "")
|
||||
self.assertIn('"gaps"', step4, "Step 4 must persist the gaps array into seen_jobs.json")
|
||||
self.assertIn('"strengths"', step4, "Step 4 must persist the strengths array into seen_jobs.json")
|
||||
|
||||
def test_step4_documents_verbatim_no_accumulate_and_untrusted_data_rules(self):
|
||||
sections = _sections(COMMAND.read_text(encoding="utf-8"))
|
||||
step4 = sections.get("Step 4: Update State", "")
|
||||
self.assertIn("verbatim", step4, "Step 4 must require storing gaps/strengths verbatim, never reformatted")
|
||||
self.assertIn("replaces", step4, "Step 4 must state that --all re-scoring replaces, not accumulates, the arrays")
|
||||
self.assertIn("untrusted data", step4, "Step 4 must restate that stored gaps/strengths are untrusted data")
|
||||
|
||||
def test_important_rules_link_honest_scoring_to_persistence(self):
|
||||
sections = _sections(COMMAND.read_text(encoding="utf-8"))
|
||||
rules = sections.get("Important Rules", "")
|
||||
self.assertIn(
|
||||
"persisted with it",
|
||||
rules,
|
||||
"Rule 5 must note that gaps are persisted (Step 4), not just printed (Step 5)",
|
||||
)
|
||||
|
||||
def test_job_scraper_schema_note_mentions_strengths_and_gaps(self):
|
||||
text = SCRAPER_SKILL.read_text(encoding="utf-8")
|
||||
self.assertIn("strengths", text)
|
||||
self.assertIn("gaps", text)
|
||||
self.assertIn(
|
||||
"readers tolerate their absence",
|
||||
text,
|
||||
"schema note must say old entries lacking strengths/gaps are tolerated, never backfilled",
|
||||
)
|
||||
|
||||
@unittest.skipUnless(
|
||||
_HAVE_YAML,
|
||||
"PyYAML not installed (the CI Python-test job omits it; the lint job runs lint_skills.py directly)",
|
||||
)
|
||||
def test_lint_skills_passes(self):
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(REPO / "tools" / "lint_skills.py")],
|
||||
cwd=REPO,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, f"lint_skills.py failed:\n{result.stdout}{result.stderr}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user