feat(scrape,rank): record and surface a posting's employment type (#619)

* feat(scrape,rank): record and surface a posting's employment type

Nullable employment_type (full_time|part_time|contract|freelance) in the
seen_jobs.json entry, read from posting text at Step 3; /rank shows it on
the shortlist line and flags a mismatch with a stated profile preference
(never a veto). No pay normalisation, no new profile field.

* fix(scrape,rank): address review on employment type

- scope the never-a-veto test to the Employment type paragraph instead of
  matching the phrase anywhere in rank.md
- count portal `detail` output as a source next to the posting text
- bracket the optional ` · <employment type>` so a null type leaves no
  dangling separator
- drop the CHANGELOG.md hunk: the file is frozen since #800 and release notes
  come from the PR title

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mv8AZjCnuGt98qsM9zuCFZ

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Mariano Gobea AlcobaandClaude Sonnet 5.5 authored and GitHub committed 2026-11-10 19:00:19 +01:00
1 parent 71c49f3d83
commit 5fe97b6ab7
3 files changed
+76 -1

No files matched your search

+68
View File
@@ -136,6 +136,74 @@ class SeenJobsPostingDateTests(unittest.TestCase):
)
RANK_COMMAND = REPO_ROOT / ".claude" / "commands" / "rank.md"
EMPLOYMENT_TYPES = ("full_time", "part_time", "contract", "freelance")
class SeenJobsEmploymentTypeTests(unittest.TestCase):
"""The engagement type a posting states must be stored and surfaced, never guessed.
Part-time, contract and freelance postings reach the shortlist
indistinguishable from permanent roles: Step 3 reads the posting but its
only outputs are a fit band and highlights, so a "6-month contract" is only
visible if the reader happens to see the title. Persisting it as a nullable
field lets /rank show it and flag a mismatch with the user's stated
preference, without any new profile field.
"""
def test_step4_schema_persists_employment_type(self):
self.assertIn(
"employment_type",
derive_stored_fields(),
"Step 4's seen_jobs.json schema stores no employment type",
)
def test_schema_lists_every_allowed_value_and_null(self):
text = SCRAPER_SKILL.read_text(encoding="utf-8")
line = next(l for l in text.splitlines() if l.strip().startswith('"employment_type"'))
for value in EMPLOYMENT_TYPES:
self.assertIn(f'"{value}"', line)
self.assertIn("null", line)
def test_employment_type_semantics_are_documented(self):
text = SCRAPER_SKILL.read_text(encoding="utf-8")
self.assertRegex(
text,
r"never infer an employment type",
"employment_type must carry the same never-backfill rule as `posted_date`",
)
@staticmethod
def _rank_employment_paragraph() -> str:
text = RANK_COMMAND.read_text(encoding="utf-8")
start = text.find("**Employment type:**")
if start == -1:
return ""
end = text.find("\n\n", start)
return text[start:] if end == -1 else text[start:end]
def test_rank_shows_it_as_a_flag_never_a_veto(self):
# Scoped to the paragraph: "never a veto" is said about age and the
# posting guard elsewhere in the file, so a file-wide search would
# keep passing if this paragraph lost the promise.
paragraph = self._rank_employment_paragraph()
self.assertIn("employment_type", paragraph, "/rank never reads the stored employment type")
self.assertRegex(paragraph, r"never a veto", "the mismatch flag must not veto")
def test_rank_marks_the_type_and_its_separator_as_optional(self):
# A null type must not leave a dangling "·" after the score.
text = RANK_COMMAND.read_text(encoding="utf-8")
self.assertIn("(78)**[ \u00b7 <employment type>]", text)
self.assertRegex(self._rank_employment_paragraph(), r"separator included")
def test_portal_detail_output_counts_as_a_source(self):
# Step 3 reads the posting text, but a portal's `detail` output is what
# Step 2 fetched; the rule must not restrict the source to one of them.
text = SCRAPER_SKILL.read_text(encoding="utf-8")
start = text.index("**Employment type:**")
self.assertIn("`detail` output", text[start:text.index("\n\n", start)])
class SeenJobsDedupContinuityTests(unittest.TestCase):
"""The new key rule must not replay jobs stored under legacy keys."""