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
On font-flattening renderers (Google Docs/Skia, WeasyPrint/Cairo, and similar), section-header detection effectively loses its font signal — measured header font ratios sit at r = 1.00–1.09, below the VISUAL_HEADER_FONT_RATIO = 1.15 gate. Detection then rests entirely on the vocabulary rules (keyword/anchor) plus the all-caps pattern rule. A header that misses both — specifically a single-word, ALL-CAPS, unknown-vocabulary header like INTERNSHIPS — is matched by nothing and gets silently absorbed into the preceding section, losing the boundary.
This is a confirmed, measured repro (see Evidence), not a theoretical one.
Repro
Fixtures: tests/fixtures/pdfs/google-docs/google-docs-skia-proxy-nonstandard-headers.pdf and tests/fixtures/pdfs/unknown/weasyprint-cairo-nonstandard-headers.pdf.
Run extractFromPdfBytes → groupIntoLines → splitIntoSections and inspect the INTERNSHIPS line:
kw=- r=1.09 caps=1 w=1 | INTERNSHIPS
It opens no section. Its content folds into the prior experience bucket.
Root cause
classifyLine (src/lib/heuristics/sections.ts:746) is a disjunction of binary accept-rules. Categorize the header rules by the gate each imposes:
Rule
Vocab gate
Casing gate
Font gate
matchSectionHeader exact keyword
known alias
—
—
split-letter rejoin
known alias
—
—
matchAnchorFallback (head-noun anchor)
anchor set
Title-case
—
isVisualHeader 2a (font+short)
free
free
needs 1.15×
isTextPatternHeader 2b (all-caps)
free
ALL-CAPS
—
The only vocab-free and casing-free rule is 2a, and it's the one font-gated rule. On flattening renderers, real headers measure r≈1.0–1.09, so 2a never fires. INTERNSHIPS then needs 2b — which requires ≥2 words (isTextPatternHeader, sections.ts:599, the single-token FP guard from #112) — so a single-word all-caps unknown header falls through to APPEND.
Net: nothing positively detects "this is a header" once font is flat and the word is unknown vocabulary; the line is absorbed and the section boundary is lost.
Why it matters
Header detection on the most common flattening renderers (Google Docs especially) has no font-independent, vocabulary-independent positive signal. Any unknown-vocabulary header that also misses the casing/word-count rules silently pollutes its neighbor. INTERNSHIPS is the reproduced instance; the class is broader (e.g. a single-word AFFILIATIONS, CERTIFICATIONS variant, etc.).
Harm in the current fixtures is mild — INTERNSHIPS absorbs into experience, a semantically adjacent section, so its entries still reach the experience-bullet pool. The latent risk is the same mechanism absorbing a header into an unrelated prior section, mis-attributing or dropping its content from the score.
Proposed direction
Add font-independent positive signals so header detection doesn't collapse when font is flat, rather than widening the keyword list (exact-match coverage will never generalize):
Vertical gap above — gap to the previous line vs. body line-height. Geometric, survives flattening. Strong header cue.
Left-alignment / isolation — line at the body left margin, alone on its band (hasEOL).
Graded casing — ALL-CAPS strong, Title-case moderate (credit, not a hard gate).
Keep font prominence as a positive feature (credit when present, zero when absent — never negative).
This is the narrow, evidence-backed slice of the larger "positive header classifier" idea: rather than a disjunction of binary rules where each failure cascades, score a line from orthogonal features and threshold. Detection (is-this-a-boundary) stays separate from naming (matchSectionHeader → which section, or other).
Safety net is the corpus.test.ts golden snapshots — but golden-regen can mask regressions, so hand-verify snapshot diffs, never blind-regen.
No line-level header unit fixtures exist today (isVisualHeader / isTextPatternHeader / isHeaderShort have no covering unit tests). A labeled header/non-header line set is a prerequisite and makes the signals tunable in isolation.
Acceptance criteria
INTERNSHIPS (and the single-word all-caps unknown-header class) opens a section boundary on the two nonstandard-headers fixtures, instead of being absorbed.
Header detection uses at least one font-independent positive signal (vertical gap and/or left-alignment).
Font prominence absence is never a negative contribution.
Line-level header/non-header unit fixtures added for the touched signals.
Out of scope / follow-on
The Title-case unknown-vocabulary header class is not reproduced by any current fixture (the Title-case lines in these PDFs are body/role lines, correctly appended). Confirming that class needs a new fixture exercising a title-case unknown header + an unrelated-neighbor pollution case — separate fixture-gap task before treating it as real.
Name-inference for confident-but-unnamed (other) headers — content-shape classification of an unnamed boundary's lines — is a separate issue (overlaps the post-section sanity pass).
Summary
On font-flattening renderers (Google Docs/Skia, WeasyPrint/Cairo, and similar), section-header detection effectively loses its font signal — measured header font ratios sit at r = 1.00–1.09, below the
VISUAL_HEADER_FONT_RATIO = 1.15gate. Detection then rests entirely on the vocabulary rules (keyword/anchor) plus the all-caps pattern rule. A header that misses both — specifically a single-word, ALL-CAPS, unknown-vocabulary header likeINTERNSHIPS— is matched by nothing and gets silently absorbed into the preceding section, losing the boundary.This is a confirmed, measured repro (see Evidence), not a theoretical one.
Repro
Fixtures:
tests/fixtures/pdfs/google-docs/google-docs-skia-proxy-nonstandard-headers.pdfandtests/fixtures/pdfs/unknown/weasyprint-cairo-nonstandard-headers.pdf.Run
extractFromPdfBytes→groupIntoLines→splitIntoSectionsand inspect theINTERNSHIPSline:It opens no section. Its content folds into the prior
experiencebucket.Root cause
classifyLine(src/lib/heuristics/sections.ts:746) is a disjunction of binary accept-rules. Categorize the header rules by the gate each imposes:matchSectionHeaderexact keywordmatchAnchorFallback(head-noun anchor)isVisualHeader2a (font+short)isTextPatternHeader2b (all-caps)The only vocab-free and casing-free rule is 2a, and it's the one font-gated rule. On flattening renderers, real headers measure r≈1.0–1.09, so 2a never fires.
INTERNSHIPSthen needs 2b — which requires ≥2 words (isTextPatternHeader,sections.ts:599, the single-token FP guard from #112) — so a single-word all-caps unknown header falls through to APPEND.Net: nothing positively detects "this is a header" once font is flat and the word is unknown vocabulary; the line is absorbed and the section boundary is lost.
Why it matters
Header detection on the most common flattening renderers (Google Docs especially) has no font-independent, vocabulary-independent positive signal. Any unknown-vocabulary header that also misses the casing/word-count rules silently pollutes its neighbor.
INTERNSHIPSis the reproduced instance; the class is broader (e.g. a single-wordAFFILIATIONS,CERTIFICATIONSvariant, etc.).Harm in the current fixtures is mild —
INTERNSHIPSabsorbs intoexperience, a semantically adjacent section, so its entries still reach the experience-bullet pool. The latent risk is the same mechanism absorbing a header into an unrelated prior section, mis-attributing or dropping its content from the score.Proposed direction
Add font-independent positive signals so header detection doesn't collapse when font is flat, rather than widening the keyword list (exact-match coverage will never generalize):
hasEOL).≥2 wordsgate inisTextPatternHeaderfor the single-word all-caps case, paired with the gap/isolation signals to keep the L3 — Visual-primary boundary detection + name/contact disambiguation #112 FP class closed.This is the narrow, evidence-backed slice of the larger "positive header classifier" idea: rather than a disjunction of binary rules where each failure cascades, score a line from orthogonal features and threshold. Detection (is-this-a-boundary) stays separate from naming (
matchSectionHeader→ which section, orother).Calibration / risk
corpus.test.tsgolden snapshots — but golden-regen can mask regressions, so hand-verify snapshot diffs, never blind-regen.isVisualHeader/isTextPatternHeader/isHeaderShorthave no covering unit tests). A labeled header/non-header line set is a prerequisite and makes the signals tunable in isolation.Acceptance criteria
INTERNSHIPS(and the single-word all-caps unknown-header class) opens a section boundary on the twononstandard-headersfixtures, instead of being absorbed.corpus.test.tssnapshot diff hand-verified — no regression in the L3 — Visual-primary boundary detection + name/contact disambiguation #112 single-token false-positive class.Out of scope / follow-on
other) headers — content-shape classification of an unnamed boundary's lines — is a separate issue (overlaps the post-section sanity pass).