feat: section-recognition L2/L3 — anchor fallback + visual bounds - #115
Conversation
Three issues from the section-recognition epic (parent #109) land together: #114 — sections.config.ts drift-guard comment + validate() extra-key check: - Reword the drift-guard comment to correctly describe its one-directional nature (TS struct check catches missing SectionName keys; extra JSON keys slip through structurally). - Add a runtime validate() loop that enumerates actual config keys against a hardcoded VALID_SECTION_NAMES set and throws on any unexpected key, closing the gap the compile-time guard misses. #111 — L2 head-noun anchor fallback in matchSectionHeader (fixes #108): - Add matchAnchorFallback(): fires only when exact-alias and split-letter paths both miss; matches a qualified header ("Relevant Experience", "Customer Service Experience") by requiring its last token to be an anchor from SECTION_ANCHORS for an anchor-fallback-enabled section. - Seven guardrails (length ≤40, word count ≤4, head-noun-last, no terminal punctuation, not a bullet line, anchorFallback flag, header casing) prevent prose FPs confirmed against the corpus. - Export SECTION_ANCHORS and SECTION_ANCHOR_FALLBACKS from sections.config.ts (and re-export via regex.ts); skills/other stay off the fallback path so a flattened two-column sidebar label cannot strand experience entries. - Add synthetic fixture chromium-qualified-experience-headers.pdf + .expected.json (Chromium-rendered, synthetic persona). - Re-bake four corpus snapshots whose section counts improved with L2 (google-docs-skia-proxy-nonstandard-headers, chromium-two-column-sidebar, weasyprint-cairo-nonstandard-headers, openresume-laverne-word-quartz). #112 — L3 visual-primary boundary detection in splitIntoSections: - Font-ratio ≥1.2 (larger than body average) is now the sole visual signal for a section boundary; the allCaps branch is dropped after corpus validation showed it was a net FP source. - Name/contact block at the top is disambiguated from body section headers so the first non-keyword visual-distinct line after the contact block opens an experience/education section rather than being swallowed. - Add sections.test.ts: 12 direct unit tests for splitIntoSections' visual path (first direct coverage; previously exercised only transitively). Tests: 438/438 pass (vitest). Closes #108 Resolves #111 Resolves #112 Resolves #114 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…litIntoSections PR #115 fallow audit flagged two issues in the changed set: - Unused exports: SECTION_ANCHORS / SECTION_ANCHOR_FALLBACKS were re-exported from regex.ts but have no out-of-module consumer (matchAnchorFallback uses the local import). Drop the re-export; consumers import from sections.config.ts (canonical home). - High complexity: splitIntoSections (L3 #112) hit 9 cyclomatic / 16 cognitive by inlining the keyword/visual/name-contact decision in the loop. Extract a pure classifyLine() helper so the splitter loop stays flat — behavior identical (438/438 tests green), function now under threshold. orderItemsByColumn remains above threshold but is pre-existing/inherited (gate-excluded) — out of scope for this PR. Refs #115
Samhit21
left a comment
There was a problem hiding this comment.
PR Review: feat: section-recognition L2/L3 — anchor fallback + visual bounds
Summary
Clean L2 + L3 land. The 7-guardrail framing for the anchor fallback is the right shape, the name/contact disambiguation in splitIntoSections is a real fix (not a workaround), and dropping allCaps as
a visual signal after the corpus pass is the kind of decision the reasoning round-trips back into the docstrings — well-evidenced. New direct unit coverage for splitIntoSections plus the synthetic
Chromium fixture for the qualified-header path closes the right loops. validate()'s extra-key runtime check addresses the one-directional _drift gap from #114 cleanly.
Prior review activity: two fallow bot comments about SECTION_ANCHORS/SECTION_ANCHOR_FALLBACKS being unused outside the module — already addressed in the in-file comment at regex.ts:105-108 and not
re-flagging.
Findings
- [Suggestion] Guard 7's casing check at
regex.ts:172-176lets digit-led tokens slip ("5 Years Experience","10+ Years Experience")./[a-z]/.test(first)is false for digits, so the negative
check passes when the positive check wouldn't. Trivial swap to/[A-Z]/.test(first)plus a fixture row in the FP table. - [Suggestion]
validate()insections.config.ts:68-100doesn't type-check the booleans or enforce anchor uniqueness acrossanchorFallback: truesections. Future-config hazard for the
load-bearing L2 contract. - [Nit]
hasContactShapeinsections.ts:306-311mutates exported/gregex state. Works today; pattern is fragile if a future caller reorders or runs after these regexes have an in-flight
iterator. - [Question]
sections.config.jsonroutes the"qualifications"anchor toeducation. Common US-convention reading is closer to skills/profile; worth an inline comment pinning the intent
(especially if the corpus you're tuning against is residency/CV-leaning).
Verdict
Action: COMMENT — none of the four are merge-blockers. #1 is the highest-value follow-up; the FP risk is real but contained to a few uncommon header shapes.
Three reviewer threads, all closed in code:
- Guard 7 casing (regex.ts): require an UPPERCASE lead char, not merely
"not lowercase". Digit/symbol-led tokens ("5 Years Experience",
"10+ Years Experience") previously cleared the guard and could open a
false experience boundary mid-summary. +3 negative tests. This also
closes the incidental "20% Projects" column-flatten match on
chromium-two-column-sidebar (rebaked snapshot, projectsCount 2→0) —
that recovery rode the same hole and is indistinguishable from prose.
- validate() (sections.config.ts): type-check the boolean flags and
enforce cross-section anchor uniqueness for anchorFallback:true
sections (matchAnchorFallback returns on first hit). Split into
validateSection/validateAnchorUniqueness to stay under the complexity gate.
- hasContactShape (sections.ts): stop mutating shared /g regex lastIndex;
use non-global clones derived from the regex.ts .source (single source).
Refs #115
|
@Samhit21 all three review threads addressed and resolved in
Your Guard 7 catch also closed an incidental Gates green: typecheck clean · 439/439 tests · |
Lands L2 + L3 of the section-recognition epic (#109, on merged L1 #110), plus #113-review follow-up #114. - #114 — sections.config.ts _drift guard comment reworded to its true one-directional nature + runtime validate() extra-key check. - #111 (L2) — head-noun anchor fallback in matchSectionHeader behind 7 FP guardrails; exports SECTION_ANCHORS + SECTION_ANCHOR_FALLBACKS; skills/other stay off the fallback. Synthetic Chromium fixture added. - #112 (L3) — visual-primary boundary in splitIntoSections (font-ratio >= 1.2) with name/contact disambiguation; first direct unit coverage for splitIntoSections. allCaps dropped as a net-FP visual signal. Two-column flattened-header sidebar-noise regression tracked separately as #117. Closes #108 Closes #111 Closes #112 Closes #114
Lands L2 + L3 of the section-recognition epic (#109, on merged L1 #110), plus #113-review follow-up #114. - #114 — sections.config.ts _drift guard comment reworded to its true one-directional nature + runtime validate() extra-key check. - #111 (L2) — head-noun anchor fallback in matchSectionHeader behind 7 FP guardrails; exports SECTION_ANCHORS + SECTION_ANCHOR_FALLBACKS; skills/other stay off the fallback. Synthetic Chromium fixture added. - #112 (L3) — visual-primary boundary in splitIntoSections (font-ratio >= 1.2) with name/contact disambiguation; first direct unit coverage for splitIntoSections. allCaps dropped as a net-FP visual signal. Two-column flattened-header sidebar-noise regression tracked separately as #117. Closes #108 Closes #111 Closes #112 Closes #114
Summary
Lands L2 + L3 of the section-recognition epic (#109, on top of merged L1 #110), plus the #113-review follow-up #114.
sections.config.ts_driftguard comment to its true one-directional nature (TS catches missingSectionNamekeys; extra JSON keys slip through structurally) + add a runtimevalidate()extra-key check.matchSectionHeader: a qualified header ("Relevant Experience", "Customer Service Experience") matches when its last token is a section anchor, behind 7 FP guardrails (length/word-count/head-noun-last/no-terminal-punct/not-bullet/anchorFallbackflag/header-casing). ExportsSECTION_ANCHORS+SECTION_ANCHOR_FALLBACKS;skills/otherstay off the fallback so a flattened two-column sidebar label can't strand experience. Synthetic Chromium fixture added.splitIntoSections: a font-distinct line (ratio ≥ 1.2 over body baseline) opens a boundary even with no keyword, with name/contact disambiguation so the candidate name isn't promoted. First direct unit coverage forsplitIntoSections(sections.test.ts).allCapsdropped as a visual signal after a corpus pass showed it was a net FP source (acronyms / skill tokens / sidebar labels); genuine all-caps headers are already caught by the keyword/anchor path.Closes #108
Closes #111
Closes #112
Closes #114
Test plan
npm run typecheckcleannpm run lint(eslint) cleannpm run testgreen — 438/438npm run dev/npm run preview