Summary
This is a decision record, not a bug report. Two ground-truth entries in the fixture corpus record a parser/truth disagreement that is probably not a defect — the parser's behaviour looks defensible and the contract it implements has never been written down. This issue asks for that contract to be written, so the two entries can be resolved as accepted rather than sitting as undescribed debt. If the adjudication goes the other way, this becomes a normal bug and the entries flip to open instead.
Context — why the entries exist
Each corpus fixture under tests/fixtures/pdfs/ may carry a hand-authored ground-truth sidecar (*.truth.json, minted under #654). Its knownWrong block records, per field, a place where the parser disagrees with what the page draws. Every entry carries a status:
open — a live bug; the cited issue must still be open;
accepted — a recorded by-design decision. The baseline files' _readme is explicit that accepted is "for a tradeoff someone WROTE DOWN, never for silencing the script";
unfiled — measured, but not yet filed (issue: null).
unfiled is capped at UNFILED_TRUTH_CEILING = 10 (src/lib/heuristics/corpus.test.ts:186) and is saturated at 10/10, so nothing new can be recorded until some are resolved. Two of the ten are this one question, measured on two unrelated fixtures.
The disagreement
An employer line names an organisation and a sub-organisation. The parser routes the outer org to company and the inner one to team; ground truth records the whole drawn phrase, because a truth file's job is to state what the page says and let a human adjudicate.
Measurement 1 — tests/fixtures/pdfs/google-docs/google-docs-skia-proxy-role-first-experience.pdf
Drawn:
August 2022 – Present
Multicultural Engineering Program – State Polytechnic University
Truth records experience.company for role 2 as the full phrase. The parser returns company = "State Polytechnic University" and puts Multicultural Engineering Program on team. Note the drawn order is sub-org first, and the parser still picks the university as the company.
Measurement 2 — tests/fixtures/pdfs/unknown/single-column-title-below-anchor.pdf
Drawn:
Ohio Valley State University — IT Service Desk — Columbus, Ohio
The parser returns company = "Ohio Valley State University", team = "IT Service Desk", and peels Columbus, Ohio onto location. Here the drawn order is org first.
Both entries are status: "unfiled", and both notes say the same thing: "Possibly a defensible org/team split rather than a defect — recorded rather than assumed."
Why this needs a decision rather than a fix
Nothing is lost in either case. The sub-org lands on team, a real field in the canonical model. The disagreement exists only because the truth scoreboard scores experience.company as a standalone field, so a correct-but-split parse reads as a company-recall miss.
Arguments that the split is right:
company is the entity a reader or an ATS matches against — the university, not the program inside it. A company of "Multicultural Engineering Program – State Polytechnic University" matches nothing.
team exists precisely for the inner org, and src/lib/heuristics/extract/experience.ts:105-121 shows the model already leans on that separation: the docblock there calls the team requirement "load-bearing, not decorative", pinning a shape with "an internal team/sub-org in team".
- The Download-PDF exporter round-trips the split faithfully —
docs/canonical-resume-model.md:258 records that "the comma marks the location boundary, the middot marks the team boundary" in the emitted header, and the re-parse recovers the same two fields.
Arguments that it is wrong, or at least under-specified:
- The two fixtures draw the two halves in opposite orders (
Program – University vs University — Service Desk) and the parser reaches the same routing on both. That is either a well-designed org-hint rule or a coincidence of these two strings, and nobody has written down which. A contract that only holds by accident will break the first time a fixture disagrees.
- The scoreboard currently measures
experience.company against the whole drawn phrase. If the split is correct, the measurement is what should change — and today there is no written rule saying what experience.company's truth value ought to be for an org/sub-org line.
docs/canonical-resume-model.md:270-273 explicitly scopes dash-separated parser INPUT out of the export separator contract ("a source résumé using | or — between title and company parses worse today"), so the dash form in measurement 2 is standing on undocumented ground.
What to do
-
Adjudicate the contract. Decide, in one sentence, what company holds when an employer line names an org and a sub-org — including whether drawn ORDER matters.
-
Write it into docs/canonical-resume-model.md, in or beside the separator-contract section (around lines 250-275), which already documents the sibling rules for location and team boundaries. Name the routing rule the parser actually implements and cite src/lib/heuristics/extract/experience.ts where it lives. Do not describe it from memory — read the code and state what it does, then say whether that is the intended contract.
-
Say what a truth file should record for this shape. The provenance rule is fixed — a truth value states what the page draws, and is never edited to match the parser. So if the split is correct, the resolution is that experience.company's truth value is the COMPANY the page names (the outer org), with the sub-org recorded under the truth's team/experience structure — not that the parser changes. Write the rule down in tests/fixtures/pdfs/README.md, which already documents the knownWrong convention (see its line ~145).
-
Resolve the two entries.
- If the split is the contract: flip both to
status: "accepted" citing this issue (the _readme expects an accepted entry's issue to be closed — close this issue when the doc lands), and correct the two truth values only if step 3's rule says the recorded value was mis-authored.
- If the split is a defect: flip both to
status: "open" citing this issue, keep it open, and add the fix plan here.
-
Lower UNFILED_TRUTH_CEILING (src/lib/heuristics/corpus.test.ts:186) by 2 either way. Sibling issues from the same audit also lower it — on a rebase conflict, take the LOWER number.
Acceptance criteria
Summary
This is a decision record, not a bug report. Two ground-truth entries in the fixture corpus record a parser/truth disagreement that is probably not a defect — the parser's behaviour looks defensible and the contract it implements has never been written down. This issue asks for that contract to be written, so the two entries can be resolved as
acceptedrather than sitting as undescribed debt. If the adjudication goes the other way, this becomes a normal bug and the entries flip toopeninstead.Context — why the entries exist
Each corpus fixture under
tests/fixtures/pdfs/may carry a hand-authored ground-truth sidecar (*.truth.json, minted under #654). ItsknownWrongblock records, per field, a place where the parser disagrees with what the page draws. Every entry carries astatus:open— a live bug; the cited issue must still be open;accepted— a recorded by-design decision. The baseline files'_readmeis explicit thatacceptedis "for a tradeoff someone WROTE DOWN, never for silencing the script";unfiled— measured, but not yet filed (issue: null).unfiledis capped atUNFILED_TRUTH_CEILING = 10(src/lib/heuristics/corpus.test.ts:186) and is saturated at 10/10, so nothing new can be recorded until some are resolved. Two of the ten are this one question, measured on two unrelated fixtures.The disagreement
An employer line names an organisation and a sub-organisation. The parser routes the outer org to
companyand the inner one toteam; ground truth records the whole drawn phrase, because a truth file's job is to state what the page says and let a human adjudicate.Measurement 1 —
tests/fixtures/pdfs/google-docs/google-docs-skia-proxy-role-first-experience.pdfDrawn:
Truth records
experience.companyfor role 2 as the full phrase. The parser returnscompany = "State Polytechnic University"and putsMulticultural Engineering Programonteam. Note the drawn order is sub-org first, and the parser still picks the university as the company.Measurement 2 —
tests/fixtures/pdfs/unknown/single-column-title-below-anchor.pdfDrawn:
The parser returns
company = "Ohio Valley State University",team = "IT Service Desk", and peelsColumbus, Ohioontolocation. Here the drawn order is org first.Both entries are
status: "unfiled", and both notes say the same thing: "Possibly a defensible org/team split rather than a defect — recorded rather than assumed."Why this needs a decision rather than a fix
Nothing is lost in either case. The sub-org lands on
team, a real field in the canonical model. The disagreement exists only because the truth scoreboard scoresexperience.companyas a standalone field, so a correct-but-split parse reads as a company-recall miss.Arguments that the split is right:
companyis the entity a reader or an ATS matches against — the university, not the program inside it. Acompanyof"Multicultural Engineering Program – State Polytechnic University"matches nothing.teamexists precisely for the inner org, andsrc/lib/heuristics/extract/experience.ts:105-121shows the model already leans on that separation: the docblock there calls theteamrequirement "load-bearing, not decorative", pinning a shape with "an internal team/sub-org inteam".docs/canonical-resume-model.md:258records that "the comma marks the location boundary, the middot marks the team boundary" in the emitted header, and the re-parse recovers the same two fields.Arguments that it is wrong, or at least under-specified:
Program – UniversityvsUniversity — Service Desk) and the parser reaches the same routing on both. That is either a well-designed org-hint rule or a coincidence of these two strings, and nobody has written down which. A contract that only holds by accident will break the first time a fixture disagrees.experience.companyagainst the whole drawn phrase. If the split is correct, the measurement is what should change — and today there is no written rule saying whatexperience.company's truth value ought to be for an org/sub-org line.docs/canonical-resume-model.md:270-273explicitly scopes dash-separated parser INPUT out of the export separator contract ("a source résumé using|or—between title and company parses worse today"), so the dash form in measurement 2 is standing on undocumented ground.What to do
Adjudicate the contract. Decide, in one sentence, what
companyholds when an employer line names an org and a sub-org — including whether drawn ORDER matters.Write it into
docs/canonical-resume-model.md, in or beside the separator-contract section (around lines 250-275), which already documents the sibling rules for location and team boundaries. Name the routing rule the parser actually implements and citesrc/lib/heuristics/extract/experience.tswhere it lives. Do not describe it from memory — read the code and state what it does, then say whether that is the intended contract.Say what a truth file should record for this shape. The provenance rule is fixed — a truth value states what the page draws, and is never edited to match the parser. So if the split is correct, the resolution is that
experience.company's truth value is the COMPANY the page names (the outer org), with the sub-org recorded under the truth's team/experience structure — not that the parser changes. Write the rule down intests/fixtures/pdfs/README.md, which already documents theknownWrongconvention (see its line ~145).Resolve the two entries.
status: "accepted"citing this issue (the_readmeexpects anacceptedentry's issue to be closed — close this issue when the doc lands), and correct the two truth values only if step 3's rule says the recorded value was mis-authored.status: "open"citing this issue, keep it open, and add the fix plan here.Lower
UNFILED_TRUTH_CEILING(src/lib/heuristics/corpus.test.ts:186) by 2 either way. Sibling issues from the same audit also lower it — on a rebase conflict, take the LOWER number.Acceptance criteria
docs/canonical-resume-model.mdstates the org / sub-org routing contract forcompanyvsteamin one paragraph, with a code citation, and says whether drawn order is load-bearing.tests/fixtures/pdfs/README.mdstates what a truth sidecar records for an org/sub-org employer line.knownWrongentries —google-docs/google-docs-skia-proxy-role-first-experience.truth.json→experience.company, andunknown/single-column-title-below-anchor.truth.json→experience.company— carry a non-nullissueand astatusofacceptedoropen, with anotethat points at the written contract rather than restating the argument.UNFILED_TRUTH_CEILINGis 2 lower.npm run check:baselinespasses and reports 2 fewer unfiled entries.Program – UniversityandUniversity — Service Desk), so the contract is enforced and not merely documented.npm run verifypasses.