Skip to content

fix(heuristics): Word-template parsing + identity-link, education & experience extraction - #125

Merged
s-annam merged 8 commits into
mainfrom
fix/word-template-extraction-29
Jun 20, 2026
Merged

fix(heuristics): Word-template parsing + identity-link, education & experience extraction#125
s-annam merged 8 commits into
mainfrom
fix/word-template-extraction-29

Conversation

@rohithgollapalli

Copy link
Copy Markdown
Collaborator

Summary

Branch of contact / education / experience parsing fixes surfaced from
real-resume bug reports, on top of the earlier Word-template work (#29) and
scoring fixes (#30, #31). Extraction-layer only (src/lib); the sole UI
change is the contact card adapting its row count.

Contact / identity links

  • LinkedIn & GitHub recovered document-wide (footer / "Links" blocks),
    promoted into the contact card, de-duplicated out of the rendered body, and
    kept out of Skills (isSkillToken rejects profile URLs + bare labels).
  • LinkedIn matching now symmetric with GitHub: any linkedin.com/<handle>
    profile (incl. bare vanity URLs) resolves; /company, /jobs, etc. excluded.
  • GitHub is an optional contact row — absent → no "not detected" gap, no
    penalty to the detected/total ratio.
  • DOCX header/footer hyperlinks extracted. mammoth only converts the body,
    so a header-placed "LinkedIn | GitHub" row was lost; we read the
    word/header*.xml / footer*.xml parts from the DOCX zip (jszip, added as a
    direct dep) and resolve their relationship-based targets.

Education — multi-qualification sections now extract every degree (was:
only the first, often corrupted). One chunk per qualification via a dual
boundary; handles degree-first / institution-first orders and acronym schools
(MIT, UC Berkeley).

Experience

  • Company vs. designation: a university/college (incl. plural "Schools") counts
    as the company; expanded title keywords (Assistant, Intern, …) so designations
    are recognized.
  • Boundary integrity: a wrapped bullet's marker-less tail (indented past the
    bullet marker) no longer leaks into the next entry's header.

Snapshots re-baked where wrapped-bullet / doc-wide-link changes shift counts.

Closes #29
Closes #30
Closes #31

Test plan

  • npm run typecheck clean
  • npm run test green (494 tests)
  • npm run lint clean
  • Manually verified in npm run dev (contact card, projects, skills, DOCX header links)
  • Fixture personas verified synthetic — no real PII (Step 3.5): Chanchal Sharma, @example.com, (718) 555-0100

Known follow-ups (not in this PR)

  • Per-entry experience location; is_current on en-dash "Present"
  • Merging wrapped-bullet tails back into responsibility text (needs a corpus re-bake)

rohithgollapalli and others added 5 commits June 18, 2026 11:09
…29)

Microsoft Word résumé templates break three single-pass parser assumptions
at once. On the Chanchal Word fixture this drove completeness to 12/30 with
name, phone, and skills all reported missing.

- Name: Word stacks the given and family name on two separate single-word
  lines ("Chanchal" / "Sharma"), each rejected by extractName's >=2-word
  guard, so a two-word tagline ("Office Manager") won the slot. extractName
  now offers an adjacent single-word pair as one merged candidate, guarded
  against section headers and doc-title boilerplate. Single-line layouts are
  scored byte-identically (the candidate list is just the top-N lines).

- Phone: the number uses an en-dash separator ("(718) 555-0100", U+2013).
  mightHavePhone's ASCII-only PHONE_RE pre-gate dropped it before the
  libphonenumber call (which parses Unicode dashes natively) ever ran. The
  pre-filter now folds en/em/figure dashes to "-"; PHONE_RE is unchanged.

- Skills: a borderless multi-column table arrives as one PdfLine because
  pdfjs fills each inter-column gap with a wide blank "spacer" item (so the
  edge-gap column split never fires). extractSkills now splits a skills line
  at those spacer items into one cell per column, recovering individual
  skills without a blind \s+ split that would shred multi-word skills.
  mergeItemText is exported from sections.ts to rejoin each cell.

Adds the synthetic, PII-verified chanchal-sharma-sample Word fixture
(completeness 12->21/30; only the genuinely-absent LinkedIn / role-dates /
summary remain missing). Two existing two-column fixtures and awesome-cv-cv
gain skills (column rows that were previously merged into one token now split
correctly); their snapshots are re-baked.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The anonymous scorer's bullet pool was section-unaware: extractBulletsFromText
scanned the whole rawText for marker lines, so a bulleted skills section
("• Project management, Data analysis") entered the same pool as experience
bullets and was judged by startsWithActionVerb / hasMetric / wellFormedLength —
checks a skill fails by design — then surfaced as failures in PerBulletFeedback.

- Section-aware pool: the cascade now passes the detected skills-section text
  (skillsSectionText, plumbed openresume -> CascadeResult) into the scorer,
  which drops any bullet whose marker-stripped text matches a skills-section
  line. score.ts stays a pure, zero-dependency module — it never re-derives
  sections from rawText; the cascade owns section detection.

- Lone-bullet merge: a line that is only a bullet glyph now adopts the next
  non-empty line as its text before scoring, recovering Word-table layouts that
  split the glyph and its text into separate cells. pdfjs renders the motivating
  fixture's bullets inline (so this path isn't hit there), so it's covered by a
  unit test rather than a fixture.

Completeness is untouched (it reads parsed.skills, not the bullet pool), per the
issue's explicit boundary. Adds the synthetic, PII-verified
chanchal-sharma-bulleted-skills Word fixture (bulletCount 2 -> 0; the two skills
bullets no longer pollute the pool). Four existing fixtures with bulleted skills
re-bake: their skills bullets leave the pool, so bulletCount drops and the
specificity/structure ratios rise to reflect real experience bullets only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Résumé templates ship dates as redaction stubs ("August 20XX – March 20XX").
The placeholder year is correctly rejected as a date, so the role's date was
scored identically to a role with no date text at all — incomplete, with no
explanation to the user.

- Detect year-position redaction stubs (20XX, XXXX, ####, 20--) in a date
  context: 20XX/20-- are matched bare; XXXX/#### only when anchored to a month
  ("August XXXX") or a range dash, so a stray "####" elsewhere doesn't trip it.
  REDACTED_DATE_RE lives in the pure scorer (no heuristics dependency).
- Completeness now gives a failing date check half credit when the text carries
  a redaction stub, distinct from the zero credit a wholly-missing date earns.
  The role still counts as incomplete (it stays in `missing`); the check shape
  gains an optional fractional `credit`.
- Expose `completeness.redactedDates`; AtsScoreReadout appends "Dates appear
  redacted — use 4-digit years for best results." to the completeness hint.

The all-20XX chanchal-sharma-sample fixture moves completeness 21 -> 23 (half
credit) while still listing "role dates" as missing. Bumps
ATS_SCORE_ALGO_VERSION 1.0 -> 1.1 (covers the #29/#30/#31 score-affecting
changes); all fixture snapshots carry the version field.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Batch of contact, education, and experience parsing fixes surfaced from
real-resume bug reports. All extraction-layer (src/lib), no UI behavior
change beyond the contact card adapting its row count.

Contact / identity links
- LinkedIn & GitHub are recovered document-wide (footer / "Links" blocks),
  promoted into the contact card, de-duplicated out of the rendered body,
  and kept out of Skills (isSkillToken rejects profile URLs + bare labels).
- LinkedIn matching is now symmetric with GitHub: any linkedin.com/<handle>
  profile (incl. bare vanity URLs) resolves, excluding /company,/jobs,etc.
- GitHub is an optional contact row: absent → no "not detected" gap and no
  penalty to the detected/total ratio.
- DOCX header/footer hyperlinks are now extracted. mammoth only converts the
  body, so a header-placed "LinkedIn | GitHub" row was lost; we read the
  word/header*.xml and footer*.xml parts from the DOCX zip (jszip) and
  resolve their relationship-based targets. Adds jszip as a direct dep.

Education
- Multi-qualification sections now extract every degree (was: only the
  first, often corrupted). Group one chunk per qualification by dual
  boundary (a second degree OR a second institution starts a new entry);
  handles degree-first and institution-first orders and acronym schools
  (MIT, UC Berkeley) that carry no "University"/"College" word.

Experience
- Company vs. designation: a university/college (INSTITUTION_HINTS, incl.
  plural "Schools") counts as the company; expanded title keywords
  (Assistant, Intern, Coordinator, ...) so designations are recognized.
- Boundary integrity: a wrapped bullet's marker-less tail (indented past
  the bullet marker) no longer leaks into the next entry's header — reuses
  the x-based continuation signal from collectAnchors.

Snapshots re-baked where the wrapped-bullet and doc-wide-link changes
shift counts; no PII in snapshots. 494 tests pass; typecheck + lint clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…raction-29

# Conflicts:
#	src/lib/score/score.ts
#	tests/fixtures/pdfs/google-docs/google-docs-skia-proxy-nonstandard-headers.expected.json
#	tests/fixtures/pdfs/google-docs/google-docs-skia-proxy-two-column.expected.json
#	tests/fixtures/pdfs/latex/awesome-cv-cv.expected.json
#	tests/fixtures/pdfs/unknown/openresume-react-pdf.expected.json
#	tests/fixtures/pdfs/unknown/student-projects-activities-singlecol.expected.json
#	tests/fixtures/pdfs/unknown/weasyprint-cairo-nonstandard-headers.expected.json
#	tests/fixtures/pdfs/unknown/weasyprint-cairo-two-column.expected.json
Comment thread src/lib/score/score.ts
* matching lines are dropped so bulleted skills never enter the pool (#30).
*/
function extractBulletsFromText(text: string): string[] {
function extractBulletsFromText(

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deferred — extractBulletsFromText is pre-existing code from main (#121), not introduced by this PR, and fallow runs report-only so it won't fail verify. Leaving this open; the lone-bullet-merge extraction is a sensible follow-up cleanup but out of scope for this review pass.

@s-annam s-annam left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review: Word-template parsing + identity-link, education & experience extraction

Gates (run on checked-out branch): typecheck ✅ · test ✅ 575 passed (40 files) · lint ✅ · fixture PII ✅ (synthetic: Chanchal Sharma, chanchals@example.com, (718) 555-0100; no Info-dict/author leak).

Summary

Strong, well-commented extraction branch. Logic is sound, the cascade→scorer skillsSectionText wiring is fully threaded, contact-row optionality is a clean refactor, snapshots re-baked. One trivial-but-real blocker (a stray NUL byte); otherwise mergeable.

Highlights

  • skillsSectionLinesskillsSectionText correctly threaded openresumecascade (both PDF + markdown paths) → useResumeAnalysis → scorer buildSkillsExclusion; the pure scorer never re-derives sections.
  • splitColumnCells reuses the now-exported mergeItemText rather than a blind \s+ split — multi-word skills survive.
  • URLISH_RE's path-slash requirement deliberately spares dotted skills (Node.js, ASP.NET).
  • phone en-dash fold is pre-gate-only; findPhoneNumbersInText still parses Unicode natively — matches the (718) 555–0100 fixture end-to-end.
  • Education multi-degree chunking + educationFromChunk cleanly replaces the first-degree-only loop.

Key Findings

1. [Blocking] — src/lib/ingest/docx.ts:129 contains a raw NUL byte (U+0000) in source.
The dedup key is `${text}\x00${url}` with a literal NUL, not an escape. Consequence: git classifies the file as binary (the PR shows Bin 2468 → 7846 bytes), so the entire ~5KB of new DOCX header/footer logic is invisible in the PR diff and to code-scanning. NUL bytes in source are also an editor/formatter footgun. It works at runtime and tests pass — this is a reviewability/tooling blocker, not a correctness one. Trivial fix, identical runtime string:

const key = `${text}\u0000${url}`;   // keeps the file UTF-8 text + diffable

Confirmed three ways: python3 byte dump, file reporting data, and the binary diff. Introduced by this PR (base was plain text).

2. [Suggestion] — extractBulletsFromText cognitive complexity 20 (>15). Already surfaced by the github-advanced-security bot on score.ts:612. CI's fallow step is report-only (non-blocking), so this won't fail verify — but lifting the lone-bullet merge into a small helper would clear it.

3. [Nit] — entry-blocks.ts:140 — local let bulletMarkerX shadows the module-level bulletMarkerX(lines) (l.163) and re-inlines the identical computation; call the helper. (Inline below.)

Verdict

Action: REQUEST_CHANGES
Rationale: Single blocking item (#1) — fix the NUL byte so docx.ts stays diffable in main's history. Trivial change. #2/#3 optional. Everything else is solid.

Comment thread src/lib/heuristics/entry-blocks.ts Outdated
@s-annam

s-annam commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Two follow-ups from the architecture pass — neither blocks this PR:

The heuristic accretion in this PR is disciplined (pure named predicates + "non-trigger byte-identical" + fixtures), so this is about the next refactor, not this diff.

One note tied to the blocking NUL-byte item: because the literal NUL character (U+0000) makes git treat docx.ts as binary, the ~5KB of new DOCX header/footer logic never showed in the PR diff (I read the full file directly — logic is sound). Once the separator is written as the JavaScript escape \u0000 (backslash-u-0000) instead of a raw NUL byte, the file becomes UTF-8 text and diffable; worth a normal diff pass on it then.

A U+0000 had been written instead of a space in the header/footer link
dedup key, which made git treat docx.ts as binary (diff shown as
'Binary files differ', unreviewable). Behavior-equivalent — NUL was only
a key separator — but a NUL in a source file is bad hygiene.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@rohithgollapalli rohithgollapalli left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Self-review pass by the branch author — still needs a second reviewer's approval to merge.

Overview

Extraction-layer fixes addressing a series of real-résumé bug reports: document-wide LinkedIn/GitHub recovery + DOCX header/footer hyperlink extraction (new jszip dep), multi-degree education extraction, experience company-vs-designation disambiguation, and wrapped-bullet-tail boundary integrity. Bundles the prior #29/#30/#31 commits and a merge of main (#119/#121/#123) with re-baked snapshots + algo bump to 1.2.

Correctness

  • 🔴 Found & fixed during review (6ec3d8f): a stray NUL byte (U+0000) in docx.ts where a space belonged (a dedup-key separator). Behavior-equivalent so tests passed, but it made git classify the file binary → its diff was invisible in this PR. Now textual and reviewable.
  • new RegExp from a URL slug in openresume.ts uses escapeRegex — no injection/ReDoS.
  • ✅ Scanned all touched source: no other binary/NUL files, no console.log/TODO/.only/debugger leftovers.
  • ✅ Merge resolution sound: score.ts keeps main's unexported const (dead-code gate), merges both changelogs, bumps 1.1→1.2; re-baked snapshots match merged behavior (awesome-cv-cv verified equal to main's #119 output).

Code quality & conventions

  • Adheres to repo norms: SPDX headers, @design-system imports, domain logic in src/lib, dynamic imports for jszip/mammoth/turndown to keep the entry chunk lean, hand-rolled minimal interfaces instead of @types/jszip.
  • Helpers are small, well-named, and commented with the why (looksLikeCompany, isWrappedContinuation, isLinkedinProfileUrl).
  • Minor: anywhereOnDoc = () => true is a no-op band predicate — reads slightly oddly but clear in context.

Test coverage — strong

New unit tests for every behavior (contact rows, skills exclusion + dotted-skill guard, vanity LinkedIn, multi-degree education across both orderings + acronym schools, title/company disambiguation, x-positioned wrapped-bullet boundary, end-to-end DOCX-header zip round-trip). Full suite 575 green, typecheck + lint clean.

Risks / observations (non-blocking)

  • ⚠️ PR size: 51 files bundling ~6 distinct fixes + 4 prior commits + a merge is a lot for one reviewer. Suggest a file-by-file skim.
  • XML-via-regex: parseHeaderFooterHyperlinks parses DOCX header XML with regex, not a real parser — pragmatic and tested, fragile against unusual <w:hyperlink> nestings.
  • Heuristic constants: splitColumnCells spacer width and isWrappedContinuation markerX + 2 — commented, low risk.
  • Documented follow-ups (not in PR): per-entry experience location, is_current on en-dash "Present", and merging wrapped-bullet tails back into responsibility text.

Security

No secrets, no new network calls (jszip operates on in-browser DOCX bytes; everything stays client-side). Fixture PII preflight passed — synthetic personas.

Verdict

Approve-worthy once a second reviewer signs off. One real defect (NUL byte) surfaced and fixed in this pass; the rest is well-tested and convention-clean. Recommend a reviewer skim the now-visible docx.ts and the extract-fields.ts education/disambiguation hunks specifically.

@Samhit21 Samhit21 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review: fix(heuristics): Word-template parsing + identity-link, education & experience extraction

Summary

Re-read post the NUL-byte fix (6ec3d8f) so docx.ts is diffable. The architecture is clean — promoted-link dedup is well-isolated in openresume.ts, the optional-row pattern in contact.ts carries
its own docstring, and splitColumnCells reusing mergeItemText is the right call. s-annam's review covered the load-bearing items (NUL byte, extractBulletsFromText complexity, bulletMarkerX
shadowing) and split out the right follow-up architecture issues (#126 / #127). Two net-new suggestions below — both real, neither blocking.

Findings

  1. [Suggestion] decodeXml at docx.ts:88 misses numeric XML entity references (&#38;, &#xA0;). Same gap that #117 closed in fetch-jd.ts; cheap fix is two extra .replace() calls before the
    named-entity pass. Hyperlink URLs and visible text from some Word save paths round-trip corrupted today.
  2. [Suggestion] stripPromotedUrls' slug regex at openresume.ts:144 uses lookahead (?![\w/]), which doesn't exclude - or . — a user whose GitHub slug is a prefix of another handle in the
    body (e.g. foo vs foo-bar) gets chopped mid-token. Same regex also lacks /g, so a line that mentions the same identity link twice keeps the second copy. Hoist+widen the lookahead and add /g.

Verdict

Action: COMMENT — neither finding is merge-blocking. #1 is the higher-value catch (closer parallel to a recently-fixed bug elsewhere in the lib); #2 is the more interesting reasoning bug but
probability of triggering in the wild is low.

Comment thread src/lib/ingest/docx.ts Outdated
Comment thread src/lib/heuristics/openresume.ts Outdated
- entry-blocks: drop the local bulletMarkerX that shadowed and re-inlined
  the module-level bulletMarkerX(lines) helper; call the helper instead.
- docx: decodeXml now decodes numeric character references (&#38;, &#xA0;)
  before the named-entity pass, matching the htmlToPlaintext fix (#117).
- openresume: stripPromotedUrls slug lookahead now also rejects '.' and '-'
  so a longer handle sharing a prefix (github.com/jane vs github.com/jane-doe)
  is not chopped; regexes hoisted out of the per-line loop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@rohithgollapalli

Copy link
Copy Markdown
Collaborator Author

Revision pushed: 1ccef64

Addressed the review feedback:

Thread Resolution
NUL byte in docx.ts (s-annam, blocking) ✅ Fixed earlier in 6ec3d8f — file is UTF-8 text + diffable now (Samhit21 confirmed)
bulletMarkerX shadow (s-annam, nit) ✅ Fixed — calls the module-level helper
decodeXml numeric entities (Samhit21) ✅ Fixed + test (decimal & hex refs)
stripPromotedUrls slug lookahead (Samhit21) ✅ Fixed + test (longer prefix-sharing handle preserved); regex hoisted
extractBulletsFromText complexity (bot) ↪️ Deferred — pre-existing from #121, fallow is report-only; left open

Gates green on the branch: typecheck ✅ · test ✅ 577 passed · lint ✅. Re-requesting review — thanks @s-annam @Samhit21.

…raction-29

# Conflicts:
#	package-lock.json
#	package.json
@s-annam

s-annam commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

@rohithgollapalli — re-reviewed the revision (1ccef64). Nice work clearing the four prior threads (NUL byte, bulletMarkerX shadow, decodeXml numeric entities, stripPromotedUrls lookahead) — all addressed and tested. Gates green locally: typecheck ✅ · test 616 passed ✅ · lint ✅. Browser smoke on a Word fixture parses end-to-end (contact, experience, education, skills all populate, no crash).

Holding approval on one thing — the corpus regression surface. This PR regenerates ~25 *.expected.json snapshots, so a green corpus run proves "output matches the new golden," not "no regression vs main." I diffed every changed snapshot against origin/main. Most deltas check out:

  • bulletCount/totalBullets ↓ → wrapped-bullet-tail merge (expected from this PR)
  • student-projects projectsCount 4→2 → verified correct (PDF has exactly 2 real projects; 4 was an over-count)
  • skillsCount ↑, github_url recovered → improvements
  • algoVersion 1.1→1.2 → intended ATS_SCORE_ALGO_VERSION bump

One delta is a real field loss I want you to confirm:

  • unknown/weasyprint-cairo-two-column: achievementsCount 1→0, heuristic_achievements dropped from fieldsPopulated. I checked it against the PDF (it has an AWARDS section) and ran the cascade on both branches: main extracts it as [{title:"", year:"2021", description:"Acme Innovation Prize, 2023"}] — mangled by the two-column layout, but present; this branch drops it entirely (text stays in rawText, no structured field).

Four questions before I approve:

  1. What corpus testing did you do beyond re-baking? Did you diff old vs new *.expected.json and eyeball each delta, or regenerate and confirm green?
  2. weasyprint-cairo-two-column achievements drop — intended? If yes, what's the rationale; if no, let's fix it or file a follow-up.
  3. The other projectsCount 2→1 decreases (google-docs-skia-proxy-nonstandard, weasyprint-cairo-nonstandard, google-docs-skia-proxy-two-column) — please confirm each is a corrected over-count, not a dropped real section.
  4. With 51 files / ~6 fixes in one PR, can you give a per-fix → affected-fixtures map? That lets us verify blast radius per change rather than as one blob.

Everything else looks solid — once these are answered I'll approve.

@rohithgollapalli

Copy link
Copy Markdown
Collaborator Author

@s-annam — thanks for the careful diff; you found the one real problem. Answers below.

1. Corpus testing beyond re-baking. During development I diffed each changed snapshot against my pre-change HEAD and eyeballed the deltas, and live-verified the headline fixtures (e.g. dumped parsed projects for student-projects 4→2). What I did not do rigorously was diff the final merged state field-by-field against origin/main — which is exactly where the weasyprint-cairo-two-column achievement slipped through. Fair catch.

2. weasyprint-cairo-two-column achievements drop — not intended; filed as #131. Root cause: my new collectAnchors rule rejects first-line anchors at x > bulletMarkerX. This fixture is a two-column flatten with two bullet margins (awards at x≈48, experience at x≈249); the global bulletMarkerX=48 makes the award's "2021" line (x=54) and the whole right column read as wrapped-bullet tails → the section yields 0 blocks. On main the same section extracts {title:"", year:"2021", description:"Acme Innovation Prize, 2023"} — an empty-title entry that mis-merges two distinct awards (the 2021 is "Globex…", the description is "Acme…"). So it's mangled-noise → nothing, not usable → lost. The principled fix is column-aware segmentation (your #127), which makes the section parse correctly rather than either count being right. I filed #131 (linked to #127/#118) rather than bolt a fragile single-column special-case onto this PR. If you'd prefer I preserve the count now I can add a fallback (first_line yields 0 anchors → revert to the prevIsBullet rule), but that just restores the empty-title noise — I lean toward the follow-up.

3. The other projectsCount 2→1 drops — confirmed corrected over-counts (dumped the raw project sections):

  • google-docs-skia-proxy-nonstandard & weasyprint-cairo-nonstandard: one real tinydb project; the dropped 2nd was the wrapped bullet tail "hardware." (x=65, past marker 59).
  • google-docs-skia-proxy-two-column: one real opentrace project; dropped 2nd was the tail "OpenTelemetry export." (x=254).

All single-column wrapped tails that main mis-promoted to phantom headers. The surviving project per fixture is the real, clean one.

4. Per-fix → affected-fixtures map:

Fix Snapshot effect Fixtures
#29 column-skills split skillsCount awesome-cv-cv (27→32), chromium-two-column-sidebar (10→18), two-column-achievements-sidebar (8→18); + new chanchal-sharma×2
Wrapped-bullet phantom removal projectsCount ↓ (corrected over-counts) google-docs-skia-proxy-nonstandard, google-docs-skia-proxy-two-column, weasyprint-cairo-nonstandard (each 2→1), student-projects (4→2)
Boundary header-recovery (flip side) projectsCount chromium-two-column-sidebar (2→3) — real header after a wrapped bullet now opens an entry
Same rule on a 2-col flatten (regression → #131) achievementsCount 1→0 weasyprint-cairo-two-column
#30 / #31 scoring specificity / structure / completeness bulleted-skills + redacted-date fixtures
algoVersion bump algoVersion 1.1→1.2 all snapshots
doc-wide identity links github_url in fieldsPopulated deedy-resume-macfonts/openfonts

Net: every count delta is an intended improvement or a corrected over-count, except the #131 two-column-achievements regression — which I'd like to take as a follow-up since the principled fix is segmentation (#127), not a per-section x-tolerance patch. Happy to switch to the fallback instead if you'd rather not reduce the count in this PR.

@s-annam s-annam left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVE

Re-reviewed the revision (1ccef64 / merged de04de6). All four held questions answered, and I verified the load-bearing ones against the code:

  • weasyprint-cairo-two-column achievements 1→0 — not a real loss. collectAnchors (entry-blocks.ts:146, if (lines[i].x > markerX) continue;) uses the global min bulletMarkerX, which can't represent a two-column flatten with two bullet margins — confirmed. main's output there was mangled empty-title noise ({title:"", year:"2021", description:"Acme Innovation Prize, 2023"}, mis-merging two awards), so 0 ≥ noise. Correctly filed as #131; right fix is column-aware segmentation (#127, now wired as blocked_by). Accepting the deferral — a single-column fallback would just restore the noise.
  • Other projectsCount 2→1 drops — confirmed corrected over-counts (wrapped-bullet tails main mis-promoted to phantom headers).
  • Per-fix → fixture map — delivered; every count delta is an intended improvement or corrected over-count.

Gates green on the branch: typecheck ✅ · test 616 passed ✅ · lint ✅ · fixture PII ✅ · verify + fallow CI ✅. Browser smoke on a Word fixture parses end-to-end.

One process note, not blocking: the per-fix → fixture blast-radius map should ship in the PR body next time, not after a reviewer digs for it — that's the cost of a 51-file / 6-fix bundle. Ties into the #126 split. Nice work clearing the threads.

@s-annam
s-annam merged commit d851657 into main Jun 20, 2026
2 checks passed
@s-annam
s-annam deleted the fix/word-template-extraction-29 branch June 20, 2026 17:08
s-annam added a commit that referenced this pull request Jun 21, 2026
…tion boundary

Replace two segmentation-imprecision workarounds in contact extraction with
the proper signals now available from the typed SectionedResume (#138).

- #134: retire the stripPromotedUrls after-the-fact slug-subtraction scrub
  (isPromotedUrl, PROMOTED_LABEL_RE, urlSlug, per-section strip driver) in
  favor of a line-level ownership model. extractContact now returns
  consumedLines: ReadonlySet<PdfLine>; buildHeuristicResult strips owned
  lines from body pools before extraction, so a promoted identity link is
  claimed by contact and never re-renders as a phantom project/achievement.
  #125 identity-link fixtures pass; zero corpus-golden movement.

- #135: replace extractContact's geometric y-band header proxy
  (inHeaderRegion: ann.yTop < 280) with inProfileSection, a real boundary
  derived from the profile section's line extents. portfolio_url/website_url
  annotation lookups consult it; the document-wide LinkedIn/GitHub identity
  match (anywhereOnDoc) is retained by design (spike §1.4). No PDF-points
  magic number remains; contact-field counts unchanged on all fixtures.

Verified: typecheck clean, 616/616 tests pass, 24/24 corpus snapshots
unchanged. Refs #109, #127.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GstfZc8CpvugDY85bS541Y
s-annam pushed a commit that referenced this pull request Jun 25, 2026
…xperience extraction (#125)

Document-wide LinkedIn/GitHub recovery + DOCX header/footer hyperlink
extraction (new jszip dep), multi-field Word-template parsing, education/
experience boundary fixes, column-aware skills split, and distinct scoring
for redacted role dates. Extraction-layer only (src/lib); ATS_SCORE_ALGO_VERSION
bumped 1.1→1.2.

Resolves #29
Resolves #30
Resolves #31

Co-Authored-By: Rohith Gollapalli <rohithgollapalli@users.noreply.github.com>
s-annam pushed a commit that referenced this pull request Jun 28, 2026
…xperience extraction (#125)

Document-wide LinkedIn/GitHub recovery + DOCX header/footer hyperlink
extraction (new jszip dep), multi-field Word-template parsing, education/
experience boundary fixes, column-aware skills split, and distinct scoring
for redacted role dates. Extraction-layer only (src/lib); ATS_SCORE_ALGO_VERSION
bumped 1.1→1.2.

Resolves #29
Resolves #30
Resolves #31

Co-Authored-By: Rohith Gollapalli <rohithgollapalli@users.noreply.github.com>
s-annam pushed a commit to qtjg/OfflineCV that referenced this pull request Aug 24, 2026
… class (offlinecv#787) (offlinecv#863)

Four source files carried a raw 0x00 where the six-character JS escape
belongs. Identical at runtime — the byte is only ever reached for as a
collision-free join separator, and the reasoning is sound each time — but it
makes grep and git grep exit 1 with no output at all over the whole file, and
renders the blob as Bin on GitHub whenever it lands in the first ~8000 bytes.
JobRepostArchiveDialog.tsx (offset 4149) was on the dark side of that
threshold; the other three read as normal text diffs while every search over
them lied.

Encoding-only, so no test moves.

Add scripts/check-no-literal-nul.mjs to stop the class coming back. offlinecv#786
shipped a fifth instance through review and PR offlinecv#125 fixed a sixth in June: the
construct is individually correct every time someone reaches for it, so a rule
does not hold and a gate has to. It walks git ls-files, skips binary formats by
declared extension — by path, deliberately, since content sniffing keys on the
very byte being hunted and would skip exactly the files it exists to catch —
and reports file:line:column with the byte offset.

Wired into verify and into verify:quick. It belongs in the inner loop by that
gate's own criterion: it costs ~50ms, and all four occurrences were in .ts
under src/, which is precisely what the Stop sentinel fires on.

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

Labels

None yet

Projects

None yet

4 participants