You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
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).
Add the test: leaf imports nothing; title-shape.ts still imports nothing; every title-shape.ts keyword is in the leaf.
Build the richer title predicate from the leaf and use it in disambiguateCompanyTitle only.
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.
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).
Part (d) of #653. The one part of #653 that is an accuracy lever rather than pure hygiene.
Decisions (2026-09-29)
src/lib/heuristics/(name it for what it holds, e.g.extract/title-surface-forms.ts).job-search/role-profiles.tsimports/derives from it — the dependency points from job-search into heuristics, never the reverse.title-shape.tsis not changed to import anything: it keeps its own ~62-keywordTITLE_KEYWORDS_REand its "imports nothing" contract. Their join is a test-asserted subset: everytitle-shape.tskeyword must be present in the leaf's inventory. Say in the PR why shape 2.disambiguateCompanyTitle(src/lib/heuristics/extract/experience-disambiguate.ts, defined ~:1397, plus thelooksLikeTitlecalls it reaches in that file). Do it with a new predicate built from the leaf, used only there — do not change the sharedlooksLikeTitle(it is also called byedit/headline.ts,extract/shared.tsand other paths on the eager graph).sections.tsis untouched. Nothing else in the parser changes.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), behindlooksLikeTitle, 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 intoROLE_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, andjob-search→ heuristics is the only allowed direction. Do not maketitle-shape.tsimportrole-profiles.ts. The test-asserted join precedent issrc/lib/job-search/role-profiles.test.ts(the "references only skill IDs that resolve against jd-match SKILLS" test).Implementation plan
role-profiles.ts's forms from it (dedupe) without changingROLE_PROFILESordering orROLE_PROFILES_VERSIONsemantics (prevalence order and the tie-break contract must hold; job-search test suite unchanged).title-shape.tsstill imports nothing; everytitle-shape.tskeyword is in the leaf.disambiguateCompanyTitleonly.Acceptance criteria
title-shape.tsstill imports nothing, asserted by a test (no such test exists today — write it); the new leaf is asserted the same way.title-shape.tskeywords are a subset of the leaf inventory, androle-profiles.tsderives from the leaf.role-profiles.tsbehaviour is unchanged (existing job-search tests green;ROLE_PROFILESorder preserved).disambiguateCompanyTitleonly, or — if any corpus field regressed — the wiring was abandoned and parser behaviour is unchanged.sections.tsuntouched; sharedlooksLikeTitleunchanged;npm run check:eagerandnpm run verifygreen.Parent: #653. Roadmap item 5(d) of #646. Related: #492, #543.
Done in #1150 (merged 2026-10-03 as
0129316). Outcome: the wiring indisambiguateCompanyTitlewas tried and abandoned on theexperience.role-commaregression, so parser behaviour is unchanged and the corpus is byte-identical;title-surface-forms.tsis a pure import-nothing leaf,role-profiles.tsderives from it at runtime viatitleHasKnownSurfaceForm, andtitle-shape.tsstill imports nothing (both asserted by tests). Scoreboard before/after quoted in the PR body (title 100.0% / 93.0%). The@offlinecv/coreparser-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).