feat(probe): /probe-resume — one-drop résumé sweep + corpus-match engine (#469) - #471
Conversation
…ine (#469) Answers the question the six single-section probes cannot: this résumé exposes a defect — does a fixture in tests/fixtures/pdfs/ already reproduce it? Today that answer lives in a maintainer's head, so every real-résumé finding either mints a duplicate fixture or keeps the real résumé in the loop as the de-facto reproducer, which is what the PII policy forbids. One runCascade() over the résumé drives all six section localizers plus one export→re-parse hop, producing a PII-free ReproArtifact plus a boolean-only DerivedSignals bag, matched against the 45 baked corpus fixtures. Each defect prints COVERED <fixture> (stop — fix the parser against the existing fixture and never open the real résumé again) or NO FIXTURE COVERS THIS (the only case that justifies minting one). The harness is read-only: it prints the next-step command, mints nothing, commits nothing, and is inert unless RL_RESUME_PDF is set. - defect-classes.ts: 21 DefectClasses derived 1:1 from the six probes' verdicts, plus an oracle model — each class declares the oracles it requires (text, header, roundtrip) and spec() gates its predicate on them. This is load-bearing: a derived bit read off an absent cascade field returns false, and false there means "unknowable", not "observed absent". Ungated, a scanned résumé reported zero defects, and a rejected skills header silently degraded into a corpus-covered class — a false COVER, which tells a maintainer to stop when nothing reproduces the defect. Blind oracles now withhold their classes loudly; an unreadable parse refuses to print a defect report at all. - fixture-match.ts: pure matchCorpus(). A cover is exhibits() on the fixture's own parse, never artifact similarity; divergence only orders the output. Near-miss count is capped and the cap is printed, never silently truncated. - localize/: the six harnesses' localization logic extracted into pure importable functions so the sweep reuses it instead of copy-pasting a seventh detector. The six keep byte-identical output. - sweep.ts: the one place the localizers' signals merge and the oracle gate applies, shared by the harness and the corpus bake so they cannot drift. - corpus snapshots: schemaVersion 5 with baked reproArtifact + derived blocks. Both are boolean/number/enum-only by type, so the snapshots stay lossy by design and PII-free by construction — no field value can enter them. Closes #469
Deploying resumelint with
|
| Latest commit: |
325b4da
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://e11daa69.resumelint.pages.dev |
| Branch Preview URL: | https://gh-469-probe-resume.resumelint.pages.dev |
Adversarial Review (Round 4 - Final)I've carefully reviewed the changes and ran a full Specifically:
Regarding the decisions made without asking:
Verdict: Ship it. The defect class taxonomy is tight, the PII boundaries are strictly maintained through boolean-only |
|
To expand on the previous review, here are the answers and analysis regarding the changes and the previous LLM's adversarial review rounds: 1. Why did it become necessary to change so many files? 2. Why did the other LLM struggle to fix the issues it found in Rounds 1 & 2? Analysis of the final fix and whether it was done correctly: Here is why the final implementation is highly robust:
In short, while the previous LLM floundered in the early rounds by applying band-aids, the final generalization it reached is the exact right design pattern for this system. |
Vaishnavi1709
left a comment
There was a problem hiding this comment.
Reviewed under the resumelint pr-review gates. 0 blocking → approve.
Gates
| Gate | Result |
|---|---|
| 3a — fixture PII | N/A / PASS. Verified "no fixture binary was added or changed" — git diff --diff-filter=AM -- 'tests/fixtures/**' returns zero PDF/image files. Only .expected.json snapshots changed. |
| 3b — design-system | N/A. No src/components/** changes. |
| 3c — style tokens | N/A. Grep hits all #469 issue-refs in comments. |
| 3d — fallow | Skipped locally. The remaining loadCorpus complexity finding (12 cyclomatic) is the intentional fail-loud snapshot-validation ladder; report-only in CI. |
| 3e — SKILL.md as code | PASS. 7 files: probe-resume is new (196 lines); 6 siblings get only cross-link additions. probe-resume/SKILL.md has one bash command (well-formed), no gh commands, PII discipline exemplary. |
Verified claims
- "Zero pre-existing keys moved" in the 44 snapshots. Confirmed:
git diff -- '**/*.expected.json' \| grep '^-[^-]' \| sort -ureturns exactly one line —"schemaVersion": 4,. Every other change is additive. RL_RESUME_OUThard-error is real.probe-resume.test.ts:80-96executesgit check-ignore -q -- <dir>and throws with a targeted message when the override resolves inside the repo and is not gitignored.- Oracle model is structurally sound.
defect-classes.ts:436-449—spec()is the ONE choke point wrapping every row ofDEFECT_SPECS.exhibits = requires.every(oracleAvailable) && detect(...).Record<DefectClass, DefectSpec>totality guard at line 457 makes an unspec'd class a compile error. Per-classrequiresaudit clean: skills-header-unrecognized / education-header-unrecognized require["text","header"]; roundtrip value classes require["text","roundtrip"]; roundtrip-render-crash requires[]with the right rationale (gating it on the roundtrip oracle would make it unreachable). - Type-level PII discipline.
defect-classes.ts:253—DerivedSignals = { [K in DerivedSignalKey]: boolean }. Mapped-type shape means nostringslot can slip in. - Corpus loader fails loud.
corpus-snapshots.ts:85-129accumulates every degradation path (unparseable JSON, wrong schemaVersion, missingreproArtifact, missing/malformedderived, non-boolean derived key, missing sibling PDF) and throws one message. Also throws on empty corpus. runRoundtripHop— every layer insidetry. All four layers (scoreForCascade→buildAtsResumeModel→renderAtsResumePdf→runCascade) inside one try/catch, with alayervariable that names the thrower. Matches the "crash is data" claim.sweepParsegate applies once.sweep.ts:135-159—textOracleUnavailableis set AFTER the localizer spreads (so localizers can't forge it clean), thendefects = DEFECT_CLASSES.filter(c => claimed.has(c) && !isWithheld(c, derived)).withheldcomputed separately.matchCorpuscover =exhibits(), not similarity. Divergence only orders the output (.sort(), never.filter()). Near-miss capshowing N of Mis honest (M =ranked.lengthpre-slice).localize/skills.tsextraction is mechanical. InlinelooseSkillsReason, header-candidate scan, missed-header filter, orphan-block walk all reproduced. New behavior on top: emits typed defects + derived slice, correctly withholds when markdown absent. Old verdict string unchanged.
Nits (non-blocking)
N1. Micro-perf in the withheld computation. sweep.ts:159 computes isWithheld(c, derived) per class, and defect-classes.ts:794-796 recomputes withheldOracles inside it — O(classes × oracles) = 21 × 3 per sweep. Fine at this scale; could share a computed unavailableOracles(derived) set with the defects filter on the line above. Not worth changing unless it grows.
N2. renderError message can embed a résumé char. Documented at roundtrip-hop.ts:83-88 — pdf-lib's WinAnsi cannot encode "X" etc. Not persisted (only the boolean renderThrewOnRoundtrip reaches snapshots); the RL_RT_PDF local harness prints it, nothing outward-facing. Deliberately kept.
Recommendation
APPROVE. PII discipline is at the type level (mapped boolean-only DerivedSignals), the oracle gate is a single-choke-point spec(), and the corpus loader fails loud on every degradation path — which are exactly the three places a false COVER could hide. The three adversarial rounds documented in the PR body targeted the right failure modes and the fixes (advisory separation, oracle model, ⛔ PARSE UNREADABLE gate) generalize the class rather than patching instances. The 44-file expected-JSON rebake is provably additive. No blocking or secondary findings.
Summary
Adds
/probe-resume— a read-only, one-drop sweep over a real résumé — plus the corpus-match engine that answers the question the six single-section probes cannot: this résumé exposes a defect; does a fixture intests/fixtures/pdfs/already reproduce it?One
runCascade()drives all six section localizers plus one export→re-parse hop, producing a PII-freeReproArtifact+ a boolean-onlyDerivedSignalsbag, matched against the 45 baked corpus fixtures. Each defect printsCOVERED <fixture>— stop, fix the parser against the existing fixture, never open the real résumé again — orNO FIXTURE COVERS THIS, the only case that justifies minting a new one. The harness mints nothing, commits nothing, and is inert unlessRL_RESUME_PDFis set, so CI never runs it.Closes #469
What shipped
defect-classes.ts— 21DefectClasses derived 1:1 from the six probes' existing verdict strings, plus an oracle model: each class declares which oracles itrequires(text/header/roundtrip), andspec()gates its predicate on them. See Adversarial review below — this is the part that took three rounds to get right.fixture-match.ts— pure, no-I/OmatchCorpus(). A cover isexhibits()on the fixture's own parse, never artifact similarity; divergence only orders the output. The near-miss cap is printed (showing 3 of 41), never a silent truncation.localize/— the six harnesses' localization logic extracted into pure importable functions, so the sweep reuses them instead of copy-pasting a seventh set of detectors. The six sibling probes keep byte-identical output (verified by diffing both trees).sweep.ts— the single place the localizers' derived signals merge and the oracle gate applies. Both the harness and the corpus bake call it, so they cannot drift.roundtrip-hop.ts— the one render→re-parse path, shared by the bake and the sweep. A crash anywhere in it is data, not a throw.schemaVersion: 5with bakedreproArtifact+derivedblocks; all 45 regenerated..claude/skills/probe-resume/SKILL.md+ cross-links from the six siblings.PII
The repo is public and the input is a real résumé, so this is the load-bearing constraint:
ReproArtifactgained no string field —repro-artifact.test.ts's PII assertion is unmodified and passing.DerivedSignalscarries the same boolean-only assertion (with a smuggled-string test proving the guard has teeth).*.expected.jsonwere audited leaf-by-leaf: every string in the new blocks is a fixed enum (v1,markdown/regex, section names,two_column). No name, email, phone, company, school, or bullet text.internal/(gitignored); an in-repo non-gitignoredRL_RESUME_OUTis a hard error by design.Test plan
npm run verifygreen locally (typecheck → lint → coverage → build → fallow)RL_RESUME_PDFunsetCOVEREDby itself, including a value-level round-trip class (proves thederivedhalf of the chain, not just the structural half)schemaVersion4→5 and the two added blocks changed; zero pre-existing keys movedverifygreen in CIProvenance
npm run verify— green in CIAdversarial review
Three rounds ran against the local diff before this PR opened (reviewer: an independent adversarial agent, prompted to break the change rather than approve it, and to reproduce suspected defects end-to-end rather than reason about them). Every blocking finding was fixed in this PR. Recording them here because two of them are the kind of bug that would have shipped as a green test.
All three blocking findings shared one root cause. A
DerivedSignalsbit computed from an optional cascade field readsfalsewhen the field is absent — butfalsethere means "unknowable", not "observed absent". The predicate then silently doesn't fire, and the tool affirmatively reports no defect.Round 1 — 2 blocking
COVERAGE n/mwas routinely wrong. The three*-no-sectionclasses were reported as defects. "This résumé has no Awards section" is not a defect — 34 of 45 fixtures have none — yet it was listed underDEFECTS FOUND, corpus-matched, and counted in the ratio. Fixed: those classes are nowadvisory, printed in a separateINFORMATIONALblock and excluded from the ratio.skills-header-unrecognized(which no fixture reproduces) fromskills-no-section(covered by 9) is derived fromcascade.markdown, which isundefinedon scanned/sparse PDFs. So a scanned résumé with a rejected skills header silently degraded to COVERED, telling the maintainer "stop, the corpus already has this" when nothing did.Round 2 — 3 blocking (including two siblings of the same bug, and a flaw in round 1's own fix)
INFORMATIONALadvisory told the reader to "check 'Sections detected'" — a line the harness never printed (the string was copied from a sibling probe that does). Reviewer minted a PDF with a real skills block underTECHNICAL PROFICIENCIES; the skills vanished from the parse and the tool printedDEFECTS FOUND (0).DEFECTS FOUND (0) — no defect class is exhibited by this parse, for a parse that extracted zero characters — resumelint's single most severe failure mode, reported as healthy.npm run bake-fixtures, despite the file's own header promising a render crash is data.The fix generalized instead of patching. Rather than repair the three instances, the class of bug is now closed structurally: three named oracles (
text,header,roundtrip), each defect class declares which itrequires, andspec()gates the predicate on them — so a class cannot be added without declaring what it depends on. Blind oracles withhold their classes with a loud banner naming exactly which; an unreadable parse refuses to print a defect report at all. All 23 derived keys were then audited one by one.Round 3 — confirmation, clean → ship
Walked all 23 keys × 21 classes (the gate is sound and not over-gated — no false non-covers), and tried five separately-minted PDFs to hole the dead-parse gate, including a prose-only PDF with 2,510 raw chars but zero extraction. No escape found. Five nits raised; all five fixed rather than deferred. Verdict: ship.
Verified and cleared across the rounds
No PII in the 45 committed snapshots (every string leaf is a fixed enum) or in the console; no silent golden regression (per-key diff of all 45 against
HEAD— zero pre-existing keys moved); the six sibling probes' output is genuinely byte-identical after the extraction (both trees rebuilt and diffed);matchCorpusmembership is exactlyexhibits()with no distance logic in the cover decision.Left for the human reviewer
fallowreports one complexity finding (loadCorpus, 12 cyclomatic) — it is the fail-loud snapshot-validation ladder, and fallow is report-only in CI. Dead code is 0.