Skip to content

Shared lexicon (d): share title surface forms between title-shape.ts and job-search ROLE_PROFILES #918

Description

@s-annam

Part (d) of #653. The one part of #653 that is an accuracy lever rather than pure hygiene.

Decisions (2026-09-29)

  • Shape 2. The shared title surface forms live in one new import-nothing leaf under src/lib/heuristics/ (name it for what it holds, e.g. extract/title-surface-forms.ts). job-search/role-profiles.ts imports/derives from it — the dependency points from job-search into heuristics, never the reverse. title-shape.ts is not changed to import anything: it keeps its own ~62-keyword TITLE_KEYWORDS_RE and its "imports nothing" contract. Their join is a test-asserted subset: every title-shape.ts keyword must be present in the leaf's inventory. Say in the PR why shape 2.
  • Wire the richer inventory into exactly ONE parser consumer: the title-shape check in disambiguateCompanyTitle (src/lib/heuristics/extract/experience-disambiguate.ts, defined ~:1397, plus the looksLikeTitle calls it reaches in that file). Do it with a new predicate built from the leaf, used only there — do not change the shared looksLikeTitle (it is also called by edit/headline.ts, extract/shared.ts and other paths on the eager graph).
  • Headerless-role detection in sections.ts is untouched. Nothing else in the parser changes.
  • Stop rule (binding). Measure per-field title precision and recall on the Ground-truth corpus scoreboard & issue-linked KNOWN_FAILURES #654 ground-truth scoreboard before and after. If wiring causes ANY corpus field regression — including title precision dropping even while recall rises — abandon the wiring: ship only the shared leaf and the dedupe/derivation, with parser behaviour byte-for-byte unchanged (corpus snapshots unchanged), and report the measured before/after numbers in the PR body. If wiring causes no regression, corpus snapshot changes are reviewed fixture by fixture and must all be improvements.

Problem

Two independent inventories of job-title surface forms exist and never talk to each other:

  • src/lib/heuristics/extract/title-shape.ts — ~62 title/seniority keywords (TITLE_KEYWORDS_RE), behind looksLikeTitle, used by the parser to decide whether a line looks like a title.
  • src/lib/job-search/role-profiles.ts (1506 LOC) — hundreds of prevalence-ordered real-world title forms, curated for job search (CURATED_ROLE_PROFILES, reordered into ROLE_PROFILES).

The job-search table is far richer, and the parser cannot see it.

Why it is an accuracy lever

A dated line that matches a known real-world title surface form is strong evidence of an entry header. Feeding the richer inventory into a title check helps #492 (role lines funnelling into a generic other) and #543 (missing titles) without touching sectioning logic at all — it is new evidence for an existing decision, not a new code path.

Constraint

title-shape.ts's contract is that it imports nothing; that must survive (it keeps the module off the eager entry graph). The new leaf must also import nothing, and job-search → heuristics is the only allowed direction. Do not make title-shape.ts import role-profiles.ts. The test-asserted join precedent is src/lib/job-search/role-profiles.test.ts (the "references only skill IDs that resolve against jd-match SKILLS" test).

Implementation plan

  1. Create the leaf holding the title surface forms; derive role-profiles.ts's forms from it (dedupe) without changing ROLE_PROFILES ordering or ROLE_PROFILES_VERSION semantics (prevalence order and the tie-break contract must hold; job-search test suite unchanged).
  2. Add the test: leaf imports nothing; title-shape.ts still imports nothing; every title-shape.ts keyword is in the leaf.
  3. Build the richer title predicate from the leaf and use it in disambiguateCompanyTitle only.
  4. Measure with the Ground-truth corpus scoreboard & issue-linked KNOWN_FAILURES #654 scoreboard; apply the stop rule above.

Acceptance criteria

  • title-shape.ts still imports nothing, asserted by a test (no such test exists today — write it); the new leaf is asserted the same way.
  • The join is test-asserted: title-shape.ts keywords are a subset of the leaf inventory, and role-profiles.ts derives from the leaf.
  • role-profiles.ts behaviour is unchanged (existing job-search tests green; ROLE_PROFILES order preserved).
  • Either the wiring shipped in disambiguateCompanyTitle only, or — if any corpus field regressed — the wiring was abandoned and parser behaviour is unchanged.
  • Title precision/recall from the Ground-truth corpus scoreboard & issue-linked KNOWN_FAILURES #654 scoreboard is quoted before and after in the PR body, in both outcomes.
  • If wired: corpus snapshot changes are reviewed fixture-by-fixture and are all improvements.
  • sections.ts untouched; shared looksLikeTitle unchanged; npm run check:eager and npm run verify green.

Parent: #653. Roadmap item 5(d) of #646. Related: #492, #543.


Done in #1150 (merged 2026-10-03 as 0129316). Outcome: the wiring in disambiguateCompanyTitle was tried and abandoned on the experience.role-comma regression, so parser behaviour is unchanged and the corpus is byte-identical; title-surface-forms.ts is a pure import-nothing leaf, role-profiles.ts derives from it at runtime via titleHasKnownSurfaceForm, and title-shape.ts still imports nothing (both asserted by tests). Scoreboard before/after quoted in the PR body (title 100.0% / 93.0%). The @offlinecv/core parser-entry closure moved 29 → 30 modules for the leaf, with the gate constant and docblocks updated in the same commit; the leaf is tree-shaken out of the / entry's eager chunk (verified by source map).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    gaalHand this issue to Gaal, the repo's coding agentimprovementEnhancing 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