Skip to content

Sanitize document titles + drop repeated icon-glyph marker rows#56

Merged
hallelx2 merged 2 commits into
mainfrom
fix/title-sanitize-and-cover-furniture
Jul 4, 2026
Merged

Sanitize document titles + drop repeated icon-glyph marker rows#56
hallelx2 merged 2 commits into
mainfrom
fix/title-sanitize-and-cover-furniture

Conversation

@hallelx2

@hallelx2 hallelx2 commented Jul 4, 2026

Copy link
Copy Markdown
Owner

@

What

Two ingest/parse robustness fixes.

Title sanitize

An ingest title with invalid UTF-8 bytes (a client that mangled an em-dash in the multipart field) made the Postgres insert fail with db write failed → 500 on the whole upload. sanitizeTitle now coerces the title to valid UTF-8 + strips control chars before the DB write; the parsed-title path is likewise run through cleanForLLM. A bad title byte can no longer fail an ingest.

Cover-page furniture

isRepeatedMarkerLine drops rows that are mostly the same short token repeated — an icon glyph rendered as text and stamped once per item (e.g. the ORCID iD logo on a paper byline → iD iD iD iD), which the font heuristic otherwise surfaced as junk author-block sections. Guard: only fires when an identical ≤2-char token is ≥60% of a ≥3-token line, so real prose and author lines with names are untouched.

Scope note

GDPR-style justified-text word-splitting (r ights, par ticular) is a deeper, riskier word-rejoin and is intentionally not in this PR.

Tests

  • TestSanitizeTitle, TestIsRepeatedMarkerLine (drop/keep boundaries).
  • go build ./... + go test ./... green.
    @

Summary by CodeRabbit

  • Bug Fixes
    • Improved document title handling so uploads are less likely to fail when titles contain invalid characters or extra whitespace.
    • Cleaned up automatically extracted titles before saving them, helping keep stored titles readable and consistent.
    • Reduced noise in PDF text extraction by filtering out repeated marker-like lines, improving parsed content quality.

@
Sanitize document titles + drop repeated icon-glyph marker rows

Title sanitize: an ingest title with invalid UTF-8 bytes (e.g. a client
that mangled an em-dash in the multipart field) made the Postgres insert
fail with "db write failed" and 500 the whole upload. sanitizeTitle now
coerces the title to valid UTF-8 and strips control chars before the DB
write; the parsed-title path is likewise run through cleanForLLM. A bad
title byte can no longer fail an ingest.

Cover-page furniture: isRepeatedMarkerLine drops rows that are mostly the
same short token repeated — the artifact of an icon glyph rendered as
text and stamped once per item (e.g. the ORCID "iD" logo on a paper
byline, "iD iD iD iD"), which the font heuristic otherwise surfaced as
junk author-block sections. Real prose never repeats an identical
<=2-char token as >=60% of a >=3-token line, so this is safe.
@

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Sorry @hallelx2, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@hallelx2, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 47 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1ecbc0e7-194d-456a-a777-6c4e53f88b3a

📥 Commits

Reviewing files that changed from the base of the PR and between 10e0a39 and d04cf13.

📒 Files selected for processing (2)
  • internal/handler/sanitize_title_test.go
  • pkg/parser/repeated_marker_test.go
📝 Walkthrough

Walkthrough

This PR adds a sanitizeTitle helper in the document ingestion handler to clean document titles of invalid UTF-8 and control characters before database writes, applies similar cleaning via cleanForLLM when persisting parsed document titles, and adds a new heuristic in PDF parsing to detect and filter repeated-marker boilerplate lines, all accompanied by unit tests.

Changes

Document title sanitization

Layer / File(s) Summary
sanitizeTitle helper
internal/handler/documents.go
Adds regexp/unicode/utf8 imports and a sanitizeTitle function with multiSpaceRe regex to strip invalid UTF-8, neutralize control characters, and collapse whitespace.
Ingest flow wiring and tests
internal/handler/documents.go, internal/handler/sanitize_title_test.go
Calls sanitizeTitle on computed titles before the empty-title fallback and DB write in HandleIngestDocument; adds table-driven tests covering spacing, UTF-8 preservation, invalid byte dropping, and control-character conversion.
Persisted title cleaning
pkg/ingest/ingest.go
Writes cleanForLLM(doc.Title) instead of the raw title when persisting auto-discovered document titles in persistTree.

PDF repeated-marker boilerplate filtering

Layer / File(s) Summary
isRepeatedMarkerLine heuristic and tests
pkg/parser/pdf.go, pkg/parser/repeated_marker_test.go
Adds isRepeatedMarkerLine to flag lines dominated by a repeated short token, integrates it into isBoilerplateLine, and adds unit tests asserting drop/keep classification.

Estimated code review effort: 2 (Simple) | ~12 minutes

Sequence Diagram(s)

Not applicable — changes are isolated logic/heuristic additions without multi-component interaction flows.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the two main changes: document title sanitization and dropping repeated icon-glyph marker rows.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/title-sanitize-and-cover-furniture

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/ingest/ingest.go (1)

705-722: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Cleaned title could become empty, overwriting the seeded title with blank.

The guard at Line 711 checks doc.Title != "" on the raw title, but the value actually written is cleanForLLM(doc.Title) (Line 719). If doc.Title is non-empty but consists only of characters cleanForLLM strips (NUL / most C0 controls), the cleaned result is "", and SetDocumentTitle will overwrite the row's filename-seeded title with an empty string — worse than the pre-PR behavior of writing the (non-empty) raw title.

🛡️ Proposed fix
 	} else if doc.Title != "" && !isLikelyMojibakeTitle(doc.Title) {
 		// Otherwise only overwrite the row's title (seeded with the
 		// filename at upload time) when the parsed title looks usable.
 		// Watermarked PDFs whose overlay text shares a Y coordinate with
 		// the real title produce mojibake like "GGlloobbaall SSttrraatteeggyy"
 		// — we'd rather keep the original filename than show that to a user.
 		// cleanForLLM strips any invalid-UTF-8 bytes so the title write can
 		// never fail the persist.
-		if err := store.SetDocumentTitle(ctx, docID, cleanForLLM(doc.Title)); err != nil {
-			return err
+		if cleaned := cleanForLLM(doc.Title); cleaned != "" {
+			if err := store.SetDocumentTitle(ctx, docID, cleaned); err != nil {
+				return err
+			}
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/ingest/ingest.go` around lines 705 - 722, The title update in persistTree
can still write an empty string because the non-empty check is done on doc.Title
before cleanForLLM(doc.Title) is applied. Update the logic in
Pipeline.persistTree to validate the cleaned title instead of the raw parsed
title, and only call store.SetDocumentTitle when the cleaned value is still
non-empty (and still not a mojibake title). Keep the sticky titleOverride
behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/handler/documents.go`:
- Around line 139-156: The sanitizeTitle function is incorrectly removing valid
U+FFFD replacement characters by checking r == utf8.RuneError after
strings.ToValidUTF8 has already normalized invalid bytes. Update sanitizeTitle
so it only strips actual invalid input during the ToValidUTF8 step and does not
discard legitimate “�” runes during the rune loop; keep the control-character
handling and whitespace normalization intact.

---

Outside diff comments:
In `@pkg/ingest/ingest.go`:
- Around line 705-722: The title update in persistTree can still write an empty
string because the non-empty check is done on doc.Title before
cleanForLLM(doc.Title) is applied. Update the logic in Pipeline.persistTree to
validate the cleaned title instead of the raw parsed title, and only call
store.SetDocumentTitle when the cleaned value is still non-empty (and still not
a mojibake title). Keep the sticky titleOverride behavior unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fec40b7d-a89c-48cc-9224-3be69bc9767a

📥 Commits

Reviewing files that changed from the base of the PR and between d148f4c and 10e0a39.

📒 Files selected for processing (5)
  • internal/handler/documents.go
  • internal/handler/sanitize_title_test.go
  • pkg/ingest/ingest.go
  • pkg/parser/pdf.go
  • pkg/parser/repeated_marker_test.go

Comment on lines +139 to +156
func sanitizeTitle(s string) string {
if !utf8.ValidString(s) {
s = strings.ToValidUTF8(s, "")
}
var b strings.Builder
b.Grow(len(s))
for _, r := range s {
if r == utf8.RuneError {
continue
}
if r < 0x20 { // drop control chars incl. tab/newline — titles are one line
b.WriteRune(' ')
continue
}
b.WriteRune(r)
}
return strings.TrimSpace(multiSpaceRe.ReplaceAllString(b.String(), " "))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

RuneError check drops legitimate U+FFFD characters.

utf8.RuneError and '\uFFFD' are the same value. Since s was already coerced via strings.ToValidUTF8(s, "") above, for _, r := range s can no longer decode an actual invalid byte to RuneError here — any r == utf8.RuneError at this point is a legitimately-encoded "�" character in the input, not a decode failure. The current check silently strips these valid characters instead of a decode error.

🐛 Proposed fix
 	var b strings.Builder
 	b.Grow(len(s))
-	for _, r := range s {
-		if r == utf8.RuneError {
-			continue
-		}
-		if r < 0x20 { // drop control chars incl. tab/newline — titles are one line
+	for _, r := range s {
+		if r < 0x20 { // drop control chars incl. tab/newline — titles are one line
 			b.WriteRune(' ')
 			continue
 		}
 		b.WriteRune(r)
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func sanitizeTitle(s string) string {
if !utf8.ValidString(s) {
s = strings.ToValidUTF8(s, "")
}
var b strings.Builder
b.Grow(len(s))
for _, r := range s {
if r == utf8.RuneError {
continue
}
if r < 0x20 { // drop control chars incl. tab/newline — titles are one line
b.WriteRune(' ')
continue
}
b.WriteRune(r)
}
return strings.TrimSpace(multiSpaceRe.ReplaceAllString(b.String(), " "))
}
func sanitizeTitle(s string) string {
if !utf8.ValidString(s) {
s = strings.ToValidUTF8(s, "")
}
var b strings.Builder
b.Grow(len(s))
for _, r := range s {
if r < 0x20 { // drop control chars incl. tab/newline — titles are one line
b.WriteRune(' ')
continue
}
b.WriteRune(r)
}
return strings.TrimSpace(multiSpaceRe.ReplaceAllString(b.String(), " "))
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/handler/documents.go` around lines 139 - 156, The sanitizeTitle
function is incorrectly removing valid U+FFFD replacement characters by checking
r == utf8.RuneError after strings.ToValidUTF8 has already normalized invalid
bytes. Update sanitizeTitle so it only strips actual invalid input during the
ToValidUTF8 step and does not discard legitimate “�” runes during the rune loop;
keep the control-character handling and whitespace normalization intact.

@hallelx2 hallelx2 merged commit db6888d into main Jul 4, 2026
8 checks passed
@hallelx2 hallelx2 deleted the fix/title-sanitize-and-cover-furniture branch July 4, 2026 03:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant