mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-09-17 00:26:26 +00:00
/apply Step 3's reviewer agent and /interview Step 2 each independently execute the Company Research Checklist (04-job-evaluation.md) for the same company - applying to a role and later prepping for its interview researches the company twice from scratch, same WebSearch/WebFetch cost both times, no sharing between the two commands. Adds a company_research/<normalized-name>.json cache (30-day TTL) that either consumer checks before researching and writes after a fresh pass. Defined once in 04-job-evaluation.md, next to the checklist it mirrors, so both commands point at one source instead of restating the schema. Does not change the verification model: 03-writing-style.md rule 5 already treats reviewer-agent research as a lead, not a source, requiring independent re-confirmation before any company claim ships in a final artifact - the cache stores source URLs alongside each fact so that re-confirmation stays cheap, but the requirement itself is untouched and restated in both consumers. company_research/*.json added to .gitignore and security_guards.py's REQUIRED_IGNORE_RULES as a plain rooted pattern (not **/-prefixed): the cache is referenced from commands, not a skill, so it resolves against the repo root normally, unlike job_scraper/upskill's skill-relative paths. Pinned by tests/test_company_research_cache.py, mirroring the spec-pinning pattern in test_rank_command.py and test_onboarding_privacy.py. The write-back assertions for both apply.md and interview.md were verified to actually fail against the regression they guard (the instruction stripped, confirmed the test catches it, restored) before being considered done - the write half is the one most likely to be dropped silently in a future edit, since the read half is the more obvious change to make. framework_version bumped 1.2.4 -> 1.2.5 in 04-job-evaluation.md, the only touched file inside the tracked skill set.
141 lines
5.6 KiB
Python
141 lines
5.6 KiB
Python
"""Guards for the company-research cache spec.
|
|
|
|
/apply Step 3's reviewer agent and /interview Step 2 each independently execute
|
|
the Company Research Checklist (04-job-evaluation.md) for the same company when
|
|
both commands run against the same application - confirmed by reading both
|
|
files, not assumed. The cache lets either consumer reuse a recent result
|
|
instead of repeating the search/fetch work. These are markdown specs (the spec
|
|
IS the implementation), so these tests pin the invariants that would break
|
|
silently: that the cache is actually read before researching, and - the part
|
|
most likely to be dropped in a future edit, since it is easy to add the read
|
|
half and forget the write half - that fresh research gets written back for
|
|
the next consumer to find.
|
|
"""
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
REPO = Path(__file__).resolve().parent.parent
|
|
EVALUATION = REPO / ".claude" / "skills" / "job-application-assistant" / "04-job-evaluation.md"
|
|
APPLY = REPO / ".claude" / "commands" / "apply.md"
|
|
INTERVIEW = REPO / ".claude" / "commands" / "interview.md"
|
|
|
|
|
|
def _sections(text: str, marker: str) -> dict[str, str]:
|
|
"""Split a markdown spec into {heading: body} on a given '\\n<marker> ' prefix."""
|
|
parts = text.split(f"\n{marker} ")
|
|
result = {}
|
|
for part in parts[1:]:
|
|
heading, _, body = part.partition("\n")
|
|
result[heading.strip()] = body
|
|
return result
|
|
|
|
|
|
def _apply_research_step() -> str:
|
|
"""apply.md's '### 1. Research the Company' subsection, isolated from the
|
|
other numbered subsections under Step 3."""
|
|
text = APPLY.read_text(encoding="utf-8")
|
|
sections = _sections(text, "###")
|
|
for heading, body in sections.items():
|
|
if heading.startswith("1. Research the Company"):
|
|
return body
|
|
return ""
|
|
|
|
|
|
def _interview_research_step() -> str:
|
|
text = INTERVIEW.read_text(encoding="utf-8")
|
|
sections = _sections(text, "##")
|
|
for heading, body in sections.items():
|
|
if heading.startswith("Step 2: Research the Company"):
|
|
return body
|
|
return ""
|
|
|
|
|
|
class TestCacheDefinition(unittest.TestCase):
|
|
def setUp(self):
|
|
self.text = EVALUATION.read_text(encoding="utf-8")
|
|
self.sections = _sections(self.text, "##")
|
|
|
|
def test_evaluation_file_defines_the_cache_section(self):
|
|
self.assertIn(
|
|
"Company Research Cache",
|
|
self.sections,
|
|
"04-job-evaluation.md must define a 'Company Research Cache' section",
|
|
)
|
|
|
|
def test_cache_definition_specifies_location_and_ttl(self):
|
|
body = self.sections.get("Company Research Cache", "")
|
|
self.assertIn("company_research/", body, "cache section must name the storage directory")
|
|
self.assertIn("30", body, "cache section must state the TTL (30 days)")
|
|
self.assertIn("fetched_date", body, "cache section must name the freshness field")
|
|
|
|
def test_cache_definition_preserves_the_verification_rule(self):
|
|
"""The cache must not weaken the existing 'verify before quoting' rule -
|
|
it should explicitly say a cache hit is a lead, not a substitute for it."""
|
|
body = self.sections.get("Company Research Cache", "")
|
|
self.assertIn(
|
|
"lead",
|
|
body,
|
|
"cache section must say a cache hit is a lead, matching the existing "
|
|
"reviewer-agent-research trust model, not a verified source on its own",
|
|
)
|
|
self.assertRegex(
|
|
body,
|
|
r"[Vv]erif",
|
|
"cache section must restate that final-claim verification still applies",
|
|
)
|
|
|
|
|
|
class TestApplyWiring(unittest.TestCase):
|
|
def test_reviewer_prompt_checks_cache_before_researching(self):
|
|
body = _apply_research_step()
|
|
self.assertNotEqual(body, "", "could not locate apply.md's Research the Company step")
|
|
self.assertIn("company_research/", body, "reviewer prompt must reference the cache path")
|
|
self.assertRegex(
|
|
body,
|
|
r"[Cc]heck the cache",
|
|
"reviewer prompt must instruct checking the cache before researching",
|
|
)
|
|
|
|
def test_reviewer_prompt_writes_back_after_fresh_research(self):
|
|
body = _apply_research_step()
|
|
self.assertRegex(
|
|
body,
|
|
r"write.*company_research/|company_research/.*write",
|
|
"reviewer prompt must instruct writing fresh research back to the cache "
|
|
"- the write half is the one most likely to be dropped silently",
|
|
)
|
|
|
|
|
|
class TestInterviewWiring(unittest.TestCase):
|
|
def test_step_2_checks_cache_before_researching(self):
|
|
body = _interview_research_step()
|
|
self.assertNotEqual(body, "", "could not locate interview.md's Step 2")
|
|
self.assertIn("company_research/", body, "Step 2 must reference the cache path")
|
|
self.assertRegex(
|
|
body,
|
|
r"[Cc]heck the cache",
|
|
"Step 2 must instruct checking the cache before researching",
|
|
)
|
|
|
|
def test_step_2_writes_back_after_fresh_research(self):
|
|
body = _interview_research_step()
|
|
self.assertRegex(
|
|
body,
|
|
r"write.*cache|cache file with",
|
|
"Step 2 must instruct writing fresh research back to the cache",
|
|
)
|
|
|
|
def test_step_2_still_requires_verification_before_using_a_claim(self):
|
|
"""Pre-existing rule (unrelated to this cache) that must survive: the
|
|
cache must not be presented as a substitute for it."""
|
|
body = _interview_research_step()
|
|
self.assertIn(
|
|
"Verify before using",
|
|
body,
|
|
"Step 2 must keep its existing verification requirement",
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|