Skip to content

fix: mislabels team as company on "Title, Team" roles under a shared-employer banner #382

Description

@s-annam

Summary

On a résumé where a single employer heads a group of roles and each role
header is Title, Team (the employer named once as a banner above the roles,
not repeated per role), the parser mislabels the team/sub-org as the company
and never attributes the true shared employer. Every role under the banner comes
back with company = <team name>, title = <role>, and no company that matches
the actual employer.

This is a distinct bug class from #355 (label-rail section recognition).
#355 got the roles segmented correctly — the residual here is purely
company/title/team field mapping inside disambiguateCompanyTitle, one layer
downstream of section routing. #355 / PR #359 fenced this off deliberately
(headerLookback: 2 doesn't reach a banner above the role group), so it needs
its own fix.

Symptom (synthetic repro shape)

Source résumé structure (employer stated once, roles below share it):

Acme Corporation
  Staff Engineer, Platform Infrastructure        Aug 2024 – Present
  Senior Engineer, Payments Core                 Jul 2022 – Aug 2024
  Engineer, Identity Services                    Aug 2020 – Jul 2022

Current parse (wrong):

Role title company team
1 Staff Engineer Platform Infrastructure
2 Senior Engineer Payments Core
3 Engineer Identity Services

Expected:

Role title company team
1 Staff Engineer Acme Corporation Platform Infrastructure
2 Senior Engineer Acme Corporation Payments Core
3 Engineer Acme Corporation Identity Services

(The last role in the observed real résumé — the one whose header carried its own
distinct employer name on the header line rather than under the shared banner —
maps correctly, which confirms the failure is specific to the shared-banner +
Title, Team shape.)

Root cause

Two coupled gaps in src/lib/heuristics/extract/experience.ts:

  1. splitRoleComma (line ~273) assumes post-comma = company. It splits
    "Staff Engineer, Platform Infrastructure" into
    ["Staff Engineer", "Platform Infrastructure"] whenever the pre-comma segment
    looksLikeTitle(...) and the post-comma segment is neither a legal suffix nor
    a location tail. There is no signal that the post-comma segment is an internal
    team/sub-org rather than an employer. Downstream in
    disambiguateCompanyTitle, looksLikeCompany("Platform Infrastructure")
    returns true (multi-word Title-Case), so chooseCompanyIdx() selects it as
    the company and the pre-comma role becomes the title. The team is consumed as
    the company; team stays empty.

  2. No shared-employer propagation. When one employer banner heads several
    consecutive roles, the employer line sits above the first role's header
    block and is not repeated per role. headerLookback: 2 (line ~48) does not
    reach a banner that precedes a group, and disambiguateCompanyTitle operates
    on a single role's header lines in isolation — so even if gap (1) were fixed
    to treat the post-comma segment as team, the roles would have no company
    at all
    . The true employer has to be carried down from the banner to each
    role in the group.

Net: the header shape <Employer banner> over N × Title, Team has no path that
lands company = <Employer> for roles 2..N.

Fix direction (not prescriptive)

Likely a two-part change; either part is useful alone, but the symptom only fully
clears with both:

  • Team-vs-company disambiguation for the comma split. When a role header is
    Title, X and the block/group has a separate employer signal, prefer mapping
    X → team rather than X → company. Options: detect a shared banner above the
    group and, in its presence, route the post-comma segment to team; or add a
    looksLikeTeam/sub-org heuristic (no legal suffix, no company-suffix word, sits
    under a stronger company signal).
  • Shared-employer propagation across a role group. Detect an employer line
    that heads a contiguous run of roles (a banner with no date anchor of its own,
    followed by ≥2 dated roles indented/below it) and attribute it as the company
    of each role in the run, with the per-role Title, Team supplying title +
    team.

Both are entry-segmentation / header-mapping concerns
(extract/experience.tsdisambiguateCompanyTitle, splitRoleComma,
parseEntryBlocks), not section routing (sections.ts) and not the
two-column detector — leave those fenced.

Validating a fix

Reproduce with a synthetic-persona PDF (fake name, @example.com, a
555-01xx phone on a real area code) built in the shared-banner + Title, Team
shape — never a real candidate résumé (this residual was found via the local
probe-experience lane on a real PII résumé; that PDF must never become a
fixture). See tests/fixtures/pdfs/README.md for the add-fixture workflow. A new
unknown/ fixture + *.expected.json snapshot exercising 3 roles under one
banner is the natural regression anchor.

Acceptance criteria

  • A résumé with one employer banner over N roles of shape Title, Team maps
    every role to company = <banner employer>, title = <role>,
    team = <post-comma segment>.
  • The Title, Company single-role shape (employer named on the header line,
    no shared banner) still maps company correctly — no regression to the
    existing comma-split behavior.
  • A synthetic-persona fixture (PII-free — verified per
    tests/fixtures/pdfs/README.md) + snapshot covers the shared-banner case.
  • npm run verify green; corpus snapshots show no unintended regressions.

Notes

Metadata

Metadata

Assignees

Labels

batch:parser-experience-2colGrouped for single-batch implementationbugSomething isn't workingimprovementEnhancing existing functionality

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions