fix(upskill): give Step 3.3 a rule for blank fit_rating rows

/outcome-created tracker rows (applications made outside the workflow)
never got a fit evaluation, so fit_rating is blank - and Step 3.3's
weight formula divides by it with no stated rule. Blank read as 0 means
weight 1.0, the maximum: the job the framework knows least about would
dominate the heatmap and the learning plan. Blank now falls back to a
matched ranked entry's rank_score, else skip+count+report once - the
same pattern the skill already applies to missing gaps. Review finding
F29 (2026-08-19).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mads Lorentzen
2026-08-19 19:54:32 +02:00
co-authored by Claude Opus 5
parent 57e82d2b59
commit 0e054f16e7
3 changed files with 29 additions and 1 deletions
+20
View File
@@ -82,6 +82,26 @@ class UpskillSkillSpec(unittest.TestCase):
self.assertIn("(100 - fit_rating) / 100", step3)
self.assertIn("(100 - rank_score) / 100", step3)
def test_step3_handles_blank_fit_rating(self):
"""/outcome creates tracker rows for applications made outside the
workflow, and no rule anywhere fills fit_rating on that path - yet
Step 3.3 divides by it. A naive read of blank as 0 yields weight 1.0
(the maximum), making the one job the framework knows nothing about
dominate the heatmap. The skill already handles missing gaps with
skip+count+report; the same pattern must cover fit_rating."""
sections = _sections(SKILL.read_text(encoding="utf-8"))
step3 = sections.get("Step 3: Pass 1 — Hard Skill Diff", "")
self.assertIn(
"blank or non-numeric `fit_rating`",
step3,
"Step 3.3 must state what happens to a row whose fit_rating is blank",
)
self.assertIn(
"Never treat a blank as 0",
step3,
"the blank-as-0 reading (weight 1.0, maximum) is the failure mode and must be forbidden explicitly",
)
def test_step5_heatmap_shows_gap_provenance(self):
sections = _sections(SKILL.read_text(encoding="utf-8"))
step5 = sections.get("Step 5: Build Gap Heatmap", "")