Skip to content

[decision] experience — write down the org / sub-org contract for company vs team #836

Description

@s-annam

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

  1. 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.

  2. 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.

  3. 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).

  4. 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.
  5. 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.md states the org / sub-org routing contract for company vs team in one paragraph, with a code citation, and says whether drawn order is load-bearing.
  • tests/fixtures/pdfs/README.md states what a truth sidecar records for an org/sub-org employer line.
  • Both knownWrong entries — google-docs/google-docs-skia-proxy-role-first-experience.truth.jsonexperience.company, and unknown/single-column-title-below-anchor.truth.jsonexperience.company — carry a non-null issue and a status of accepted or open, with a note that points at the written contract rather than restating the argument.
  • UNFILED_TRUTH_CEILING is 2 lower.
  • npm run check:baselines passes and reports 2 fewer unfiled entries.
  • A regression test pins whichever routing the decision blesses, on BOTH drawn orders (Program – University and University — Service Desk), so the contract is enforced and not merely documented.
  • npm run verify passes.

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentationimprovementEnhancing existing functionalityux:parsingUX program: parsing accuracy as the user experiences it

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions