Ariadne: CV parsing research and a parser spine
A dependency-free Python parser that reports what the extraction lost, with typed failure codes, beside a research corpus where every claim carries a source id and a grade.
Evidence tier: Verified | code for the parser and the research corpus described below, at version 0.0.1 in Ariadne. Nothing on this page claims a hosted service, a user, or a measurement against a real employer’s system, because none exists.
Added 2026-10-26 and linked from the homepage as a project card. It is a card and not a case study on purpose: the code is the artifact, and a case study would imply a deployment the repository does not have.
What it is
A research repository plus a small Python package, built around one narrow question: what happens to a CV between “upload” and “a human reads it”, and what can honestly be measured about that. Two halves, deliberately kept apart.
docs/ is the research: ten pages, about 1,640 lines, every claim carrying a source id and a grade of
A, B, C or U, every page a freshness date, and a source register that CI checks in both
directions so a citation cannot dangle and a source cannot go uncited.
src/ariadne_ats/ is the implementation of only the parts that can be built without a model: text
normalisation, layout-damage detection with named failure codes, section segmentation with per-section
confidence, a fact ledger, a job-description requirement extractor, and a TF-IDF comparison that
prints the terms behind its number. 1,443 lines, 4 test modules, 6 fixtures.
Architecture
cv.docx / cv.pdf / cv.txt
|
v
extract DOCX through the stdlib (a DOCX is a zip of XML: zipfile plus ElementTree, no
dependency at all). PDF through an optional backend (pymupdf, else pypdf).
An image-only PDF reaches OCR only if an OCR backend is installed.
|
v
textutil normalisation: whitespace, hyphenation, ligatures, the quiet damage a converter does
|
v
layout damage detection with named codes: multicolumn flattening, a header/footer band
leaking into the body, folded ligatures, text yield below a usable floor
|
v
segment section boundaries, each with a confidence rather than a bare True
|
v
evidence the ledger: fact id, source span, confidence, and two hashes per fact
|
v
jd, lexical requirement extraction with weights and spans, then TF-IDF cosine that prints
the terms that produced the number
errors runs under all of it: a Code plus a Severity (BLOCKING or DEGRADED) per finding.
Key decisions
Deterministic, and dependency-free on purpose. dependencies = [] in the package metadata, with
pytest the only dev extra, Python 3.10 or newer, and no network call in the baseline path. The
reasoning: a measurement tool whose output depends on a model revision or a network round trip is not
a measurement tool, and the extraction that needs a third-party library says so with a typed failure
instead of quietly degrading.
Failures are typed, because a plausible empty result is the dangerous output. PDF_BACKEND_MISSING,
OCR_BACKEND_MISSING, NO_TEXT_LAYER, CORRUPT_DOCUMENT, UNSUPPORTED_FORMAT, EMPTY_INPUT,
LOW_TEXT_YIELD, MULTICOLUMN_FLATTENED, HEADER_FOOTER_BAND, LIGATURE_FOLDED. A document that
cannot be read produces a code and a severity, not a page of blanks that reads like a sparse CV.
No scoring, and not by taste. The published evidence this project graded A is that a ranking
function of this class reverses the order of candidates on 29.6 % of pairs where competence was
deliberately preserved (validity 0.781 on matched-pair reversals, docs/03 section 3.4). So there is
no “score my CV” command in the CLI at all: adding a flag that implied a score would be a user-facing
claim. What ships instead is compare, which prints a cosine and the terms that produced it, so
the number can be argued with.
Grades are enforced by CI, not by discipline. Every claim in docs/ carries a source id and one of
four grades, and the register is checked in both directions by the build. U means “attempted,
unanswered” and is a real value a page can carry; it is not a placeholder waiting to be quietly
deleted.
Person-attribute inference is refused structurally. The contract rejects it, and
tests/test_no_profiling.py fails the build on it. A rule that only exists in a policy file is a rule
that decays.
What broke
The authoritative sources answered with everything except an answer. Eight applicant-tracking
help endpoints were queried for their parser identity and accepted formats. The responses were a 404, a
401, a Cloudflare JavaScript shell, and a connection failure, and the vendor-identity query returned
zero results: 0 of 8 targets answered, 7 of 8 unknown on accepted formats. Those failures are
recorded and graded A as records of failure, and the unknowns are written as U. The tempting
alternative was to summarise what the internet believes about ATS vendors, which would have produced a
confident page built on nobody’s knowledge.
The one official-grade artifact held is a sandbox subdomain. It is labelled as a sandbox on the page that cites it, because a sandbox limit is not a production limit and the difference is the conclusion.
A DOCX is a zip of XML, so the zero-dependency path works, and a malformed one raises
CORRUPT_DOCUMENT. An image-only PDF has no text layer, and before the typed codes existed that
document parsed “successfully” into nothing. The fix was not better extraction but a code that says
what happened.
Known limitations
- Not an ATS, and not a way to beat one. No vendor’s acceptance or ranking rules are known, and the pages say which eight attempts produced no answer.
- No score exists, so nothing here predicts or guarantees an employer’s decision. Any match number in the code is this project’s own construct, not a reproduction of anyone’s ranking.
- Version 0.0.1: research plus parser spine. No hosted service, no generation, no user accounts, no deployment.
- PDF and OCR paths need optional backends. With none installed the result is a typed failure, not a degraded success, which is the correct behaviour and still not a working extraction.
- The forbidden-sentence list is part of the design.
docs/09section 8 lists the claims this project may never make, including “AI Act compliant” and a blanket “GDPR compliant”; compliance work here is documented as an assessment, not a certificate. - Research freshness is a standing cost. A source register with grades needs a cadence, so the repo runs a weekly source probe and a monthly freshness pass; an unfreshened unknown may already have been answered.
What is not claimed
- No measurement against any production ATS, and no claim about how any specific employer’s system reads a CV.
- No users, no traffic, no availability target. This is a repository, not a service.
- No compliance certification of any kind.
- No infrastructure story: no pipeline, no estate, no on-call. It is a parsing and research project, and it is listed as a line in a CV rather than as a case study for exactly that reason.