Skip to content

fix(knowledge): read notes with a UTF-8 BOM correctly - #311

Merged
petertzy merged 2 commits into
petertzy:mainfrom
harsh-thakkar7:fix/knowledge-strip-utf8-bom
Oct 2, 2026
Merged

petertzy merged 2 commits into
petertzy:mainfrom
harsh-thakkar7:fix/knowledge-strip-utf8-bom

Conversation

@harsh-thakkar7

@harsh-thakkar7 harsh-thakkar7 commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Summary

A note saved with a UTF-8 BOM was indexed under the wrong title, and its
YAML frontmatter was injected verbatim into the text sent to the AI as
retrieval context.

Both come from the same thing: the BOM ends up in front of line 1.

Why a BOM matters here

U+FEFF is Unicode category Cf (format), not whitespace, so
str.strip() does not remove it. Notes with a BOM are routine — anything
authored or re-saved on Windows picks one up.

Before

path.read_text(encoding="utf-8")   # content starts with ""

Title. extract_note_title() checks content.startswith("---") for
frontmatter and then line.startswith("# ") for a heading. With a BOM in front,
both fail:

note content indexed title
# My Note my note (from the filename)
---\ntitle: Real Title\n… Heading (the H1, not the frontmatter title)

The wrong title is stored in indexed_files.title and in every chunks.title,
so it shows up in AI citations and in build_knowledge_context_for_prompt.

Frontmatter leak. chunk_markdown_document() also gates on
body.startswith("---"), so with a BOM the frontmatter is never stripped and
becomes the first chunk:

'---\ntitle: Real Title\ntags: [a]\n---'   # section "Overview"

That raw YAML then goes into the AI system prompt verbatim.

Changes

backend/knowledge_logic.py — read notes as utf-8-sig, which strips a BOM
when present and is byte-identical to utf-8 when it is not. This is the one
read site that feeds both the title and the chunker, so it fixes both symptoms
at the source rather than patching each consumer.

The encoding is exposed as a module constant (NOTE_READ_ENCODING) so the
behaviour is named, documented and testable.

No change for notes without a BOM — "utf-8-sig" decodes those identically.

Testing

tests/test_knowledge_logic.py::TestUtf8BomNotes, five cases. Each writes a real
file with BOM + body as bytes (not encoding="utf-8", which would add a
second BOM) and reads it back through NOTE_READ_ENCODING, so reverting the fix
fails these tests rather than silently passing:

  • a BOM does not hide the first # heading → My Note
  • a BOM does not hide frontmatter → Real Title
  • frontmatter is not chunked as body text (asserts the raw YAML appears in no
    chunk)
  • a note with no BOM is unchanged
  • a file containing only a BOM reads as "" and chunks without raising

Verified 3 of these fail when NOTE_READ_ENCODING is flipped back to
"utf-8".

Verification

  • backend pytest — 200 passed (was 195 on main; this PR adds 5)
  • ruff check / ruff format --check on changed files — clean
  • npm test — 23 passed, unchanged
  • npm run lint / npm run build — clean

harsh-thakkar7 and others added 2 commits October 1, 2026 11:04
A note saved with a BOM was indexed under the wrong title and had its YAML
frontmatter injected verbatim into the AI retrieval context.

U+FEFF is category Cf, not whitespace, so str.strip() does not remove it. It
sits in front of line 1, which breaks both content.startswith('---') in
extract_note_title() and chunk_markdown_document(), plus the first-heading
match. So '\xef\xbb\xbf# My Note' was titled 'my note' (from the filename),
and a frontmattered note kept '---\ntitle: X\n---' as its first chunk, which
reaches the AI system prompt verbatim.

Read as utf-8-sig, which strips a BOM when present and is byte-identical to
utf-8 when it is not. Exposed as NOTE_READ_ENCODING so the behaviour is
named and testable.

Adds 5 cases; verified 3 fail when the encoding is flipped back to utf-8.
@petertzy

petertzy commented Oct 2, 2026

Copy link
Copy Markdown
Owner

Approved. The utf-8-sig change correctly handles UTF-8 BOMs while preserving behavior for regular UTF-8 files. It fixes both incorrect title extraction and frontmatter leakage into indexed chunks. The added tests cover parsing and the end-to-end indexing path, and all 201 tests pass with Ruff checks clean. This change is safe and worthwhile to merge.

@petertzy
petertzy merged commit dc0a554 into petertzy:main Oct 2, 2026
2 checks passed
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.

2 participants