mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-09-17 00:26:26 +00:00
fix(ci): co-locate placeholder sentinels with the data they guard
cv/main_example.tex's sentinel was [YOUR_NAME] - a header comment and
the pdftitle, neither of which /setup's documented personalization
touches, so CI reported the file clean while it carried a real name,
address, phone and email (the review proved this end to end; the file
is the one CV the gitignore deliberately allows to be committed). The
guard now checks the \name{} and \email{} data lines, and 01's sentinel
moves from the <!-- SETUP comment onto [YOUR_EMAIL]. New test simulates
the /setup edit and requires every checked CV sentinel to be destroyed
by it. Review finding F28 (2026-08-19).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3bfd525cc4
commit
07cec1f227
@@ -210,8 +210,9 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
check CLAUDE.md '\[YOUR_NAME\]'
|
check CLAUDE.md '\[YOUR_NAME\]'
|
||||||
check cv/main_example.tex '\[YOUR_NAME\]'
|
check cv/main_example.tex '\\name{\[First\]}{\[Last\]}'
|
||||||
|
check cv/main_example.tex '\\email{\[your\.email@example\.com\]}'
|
||||||
check cover_letters/cover_example.tex '\[YOUR NAME\]'
|
check cover_letters/cover_example.tex '\[YOUR NAME\]'
|
||||||
check .claude/skills/job-application-assistant/01-candidate-profile.md '<!-- SETUP'
|
check .claude/skills/job-application-assistant/01-candidate-profile.md '\[YOUR_EMAIL\]'
|
||||||
check .claude/skills/job-application-assistant/04-job-evaluation.md '\[YOUR_PRIMARY_SKILLS\]'
|
check .claude/skills/job-application-assistant/04-job-evaluation.md '\[YOUR_PRIMARY_SKILLS\]'
|
||||||
exit $fail
|
exit $fail
|
||||||
|
|||||||
@@ -120,6 +120,16 @@ per-file diff commands.
|
|||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- **CI's placeholder guard now watches the CV's actual personal-data lines** - the
|
||||||
|
sentinel for `cv/main_example.tex` was `[YOUR_NAME]`, whose only occurrences are a
|
||||||
|
header comment and the hyperref `pdftitle`; `/setup`'s documented edit replaces the
|
||||||
|
`\name{}`/`\address{}`/`\phone{}`/`\email{}` data and touches neither, so a fully
|
||||||
|
personalized CV with a real name, address, phone and email passed the check (proven
|
||||||
|
empirically in the review). The guard now asserts sentinels inside the `\name{}` and
|
||||||
|
`\email{}` lines, and `01-candidate-profile.md`'s sentinel moves from the `<!-- SETUP`
|
||||||
|
header comment onto the `[YOUR_EMAIL]` Identity field for the same reason. The new
|
||||||
|
`tests/test_placeholder_integrity.py` simulates the `/setup` edit and requires the
|
||||||
|
guard to fire on it.
|
||||||
- **`jobindex-search` maps ASAP postings' deadline to `null`** - the portal's
|
- **`jobindex-search` maps ASAP postings' deadline to `null`** - the portal's
|
||||||
`apply_deadline_asap` flag was emitted as the literal string `"ASAP"` on roughly half
|
`apply_deadline_asap` flag was emitted as the literal string `"ASAP"` on roughly half
|
||||||
of live results, contradicting the CLI's own README ("date string; null if not
|
of live results, contradicting the CLI's own README ("date string; null if not
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
"""Guards for CI's placeholder-integrity sentinels.
|
||||||
|
|
||||||
|
The job exists to catch personal data committed to the upstream template.
|
||||||
|
That only works when each sentinel sits IN the data /setup replaces: the
|
||||||
|
CV's old sentinel was `[YOUR_NAME]`, whose only occurrences were a header
|
||||||
|
comment and the hyperref pdftitle - /setup's documented edit ("replace
|
||||||
|
placeholder personal data with their actual name, contact info") touches
|
||||||
|
neither, so a fully personalized CV with a real name, address, phone and
|
||||||
|
email passed the check (review finding F28, 2026-08-19; proven
|
||||||
|
empirically). Same weakness for 01-candidate-profile.md's `<!-- SETUP`
|
||||||
|
comment sentinel.
|
||||||
|
|
||||||
|
These tests pin (a) that ci.yml checks data-located sentinels, (b) that
|
||||||
|
the sentinels exist in the pristine files, and (c) that simulating the
|
||||||
|
/setup edit destroys at least one checked sentinel per file - i.e. the
|
||||||
|
guard actually fires on the failure it exists to catch.
|
||||||
|
"""
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPO = Path(__file__).resolve().parent.parent
|
||||||
|
CI = REPO / ".github" / "workflows" / "ci.yml"
|
||||||
|
EXAMPLE_CV = REPO / "cv" / "main_example.tex"
|
||||||
|
PROFILE = REPO / ".claude" / "skills" / "job-application-assistant" / "01-candidate-profile.md"
|
||||||
|
|
||||||
|
# The literal sentinel strings (unescaped) that ci.yml's grep patterns match.
|
||||||
|
CV_SENTINELS = ["\\name{[First]}{[Last]}", "\\email{[your.email@example.com]}"]
|
||||||
|
PROFILE_SENTINEL = "[YOUR_EMAIL]"
|
||||||
|
|
||||||
|
|
||||||
|
def personalize_cv(text: str) -> str:
|
||||||
|
"""Apply /setup Step 3.7's documented edit: replace placeholder personal
|
||||||
|
data with a real name and contact info. Header comments and hyperref
|
||||||
|
metadata are not personal data, so they are deliberately left alone -
|
||||||
|
that is exactly why a comment-located sentinel guards nothing."""
|
||||||
|
return (
|
||||||
|
text.replace("\\name{[First]}{[Last]}", "\\name{Jane}{Doe}")
|
||||||
|
.replace("[Your Address, City, Country]", "Some Street 1, Aarhus, Denmark")
|
||||||
|
.replace("[+XX XXXXXXXXXX]", "+45 12345678")
|
||||||
|
.replace("[your.email@example.com]", "jane.doe@example.org")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCvSentinelsAreDataLocated(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.ci = CI.read_text(encoding="utf-8")
|
||||||
|
self.cv = EXAMPLE_CV.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
def test_ci_checks_the_name_and_email_data_lines(self):
|
||||||
|
self.assertIn(
|
||||||
|
"check cv/main_example.tex '\\\\name{\\[First\\]}{\\[Last\\]}'",
|
||||||
|
self.ci,
|
||||||
|
"ci.yml must assert the sentinel inside the \\name{} data line",
|
||||||
|
)
|
||||||
|
self.assertIn(
|
||||||
|
"check cv/main_example.tex '\\\\email{\\[your\\.email@example\\.com\\]}'",
|
||||||
|
self.ci,
|
||||||
|
"ci.yml must assert the sentinel inside the \\email{} data line",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_pristine_cv_carries_both_sentinels(self):
|
||||||
|
for sentinel in CV_SENTINELS:
|
||||||
|
self.assertIn(sentinel, self.cv)
|
||||||
|
|
||||||
|
def test_setup_edit_destroys_the_sentinels(self):
|
||||||
|
personalized = personalize_cv(self.cv)
|
||||||
|
self.assertNotEqual(personalized, self.cv, "the simulated /setup edit must change the file")
|
||||||
|
surviving = [s for s in CV_SENTINELS if s in personalized]
|
||||||
|
self.assertEqual(
|
||||||
|
surviving,
|
||||||
|
[],
|
||||||
|
"a sentinel survived the documented /setup personalization - the "
|
||||||
|
f"guard would pass on committed personal data: {surviving}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestProfileSentinelIsDataLocated(unittest.TestCase):
|
||||||
|
def test_ci_checks_a_data_placeholder_not_the_header_comment(self):
|
||||||
|
ci = CI.read_text(encoding="utf-8")
|
||||||
|
self.assertIn(
|
||||||
|
"check .claude/skills/job-application-assistant/01-candidate-profile.md '\\[YOUR_EMAIL\\]'",
|
||||||
|
ci,
|
||||||
|
"01's sentinel must sit in the Identity data /setup fills, not in "
|
||||||
|
"a header comment the model may leave untouched",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_pristine_profile_carries_the_sentinel(self):
|
||||||
|
self.assertIn(PROFILE_SENTINEL, PROFILE.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user