fix(html-report): funnel from stage history, rejection rate from true rejections

The funnel was computed from current status - a state, not a history -
so an application that interviewed and was later rejected never counted
as reaching Interview, and a hired candidate produced Interview=0. The
stage checkboxes Step 1.2 already merges from outcome.md are the
history; Step 2 and chart 4 now use them. The rejection rate also
counted offer_declined (a success) and withdrawn (candidate-initiated)
as rejections and left unresolved Interview/Offer rows in the
denominator. Review findings F10 and F11 (2026-08-19).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mads Lorentzen
2026-08-19 20:54:46 +02:00
co-authored by Claude Opus 5
parent dab215073e
commit 2c3d2d8558
3 changed files with 49 additions and 3 deletions
+37
View File
@@ -94,6 +94,43 @@ class HtmlReportTrackerFieldTests(unittest.TestCase):
class HtmlReportGitignoreTests(unittest.TestCase):
"""reports/ must be gitignored — it holds personal generated output."""
def test_funnel_is_computed_from_stage_history_not_current_status(self):
"""status is a current state, not a history: an application that
interviewed and was then rejected carries status `rejected` and would
never count as having reached Interview, so a finished search reads
as though nobody ever interviewed (review finding F10, 2026-08-19).
The stage checkboxes merged from outcome.md in Step 1.2 are the
history; the funnel must be told to use them."""
text = COMMAND_FILE.read_text(encoding="utf-8")
self.assertIn(
"stage checkboxes",
text.split("## Step 3")[0].split("## Step 2")[1],
"Step 2's funnel definition must derive stage-reached from the "
"merged outcome.md stage checkboxes",
)
self.assertIn(
"not current status",
text,
"the funnel rule must say explicitly that current status alone undercounts",
)
def test_rejection_rate_excludes_declined_offers_and_withdrawals(self):
"""offer_declined is the candidate turning an offer down (a success)
and withdrawn is candidate-initiated; counting either as a rejection
inflates the rejection rate on a self-assessment dashboard (review
finding F11, 2026-08-19)."""
text = COMMAND_FILE.read_text(encoding="utf-8")
self.assertIn(
"`offer_declined`",
text.split("## Step 3")[0].split("## Step 2")[1],
"the rejection-rate definition must address offer_declined",
)
self.assertIn(
"not rejections",
text,
"the rate must exclude candidate-initiated outcomes explicitly",
)
def test_reports_folder_is_gitignored(self):
rules = {line.strip() for line in GITIGNORE.read_text(encoding="utf-8").splitlines()}
self.assertIn(