Skip to content

fix(wiki): drop a leading BOM on a page with no frontmatter - #663

Merged
akitaonrails merged 1 commit into
akitaonrails:mainfrom
MaxFreedomPollard:fix/bom-in-frontmatterless-page-body
Sep 7, 2026
Merged

akitaonrails merged 1 commit into
akitaonrails:mainfrom
MaxFreedomPollard:fix/bom-in-frontmatterless-page-body

Conversation

@MaxFreedomPollard

Copy link
Copy Markdown
Contributor

What changed

markdown::parse strips a leading UTF-8 BOM before it looks for the frontmatter fence, but the no-frontmatter path returned the untouched input as the body instead of the stripped trimmed (crates/ai-memory-wiki/src/markdown.rs:34-52). A page with a BOM and no frontmatter therefore kept the mark in front of its first line, where it is not a byte-order mark any more. It now returns trimmed on both paths, which is what the frontmatter path already did.

Before: parse("\u{FEFF}# Hand written\n\nBody.\n").body == "\u{FEFF}# Hand written\n\nBody.\n". After: "# Hand written\n\nBody.\n". A page that already had frontmatter is unchanged, and so is every page with no BOM.

Why

Two observable consequences.

derive_title picks a title by scanning body lines for a # prefix. With the mark sitting in front of the # the H1 stops matching, so reindex_page and restore_page_from_checkpoint index the page under its filename stem rather than its heading. A hand-edited notes/hand-written.md starting # Hand written came back as "hand-written".

The OKF v0.2 file pass is worse, because it writes. Its contract is "conform every wiki .md in place (body untouched)" (crates/ai-memory-wiki/src/migrations/m2026_09_okf_conformance.rs:8), and conform_file implements that by parsing, filling the frontmatter, and writing emit(&md) back through an atomic rename. Fed a BOM-prefixed page with no frontmatter it produced:

---
type: Note
generated:
  by: process:ai-memory/2.1.0
  at: 2026-09-06T22:38:30Z
---
\u{feff}# Hand written

Body.

The mark is now after the closing fence, inside the body, where it is a stray zero-width no-break space ahead of the H1 rather than a byte-order mark. That is a permanent edit to the source of truth, and the migration is one-shot.

The wiki is meant to be hand-editable and the project supports Windows, where a BOM is what several editors and PowerShell write by default. The CLI hook path already handles this deliberately (crates/ai-memory-cli/src/commands/hook.rs:822, docs/windows.md:512); this is the same class of input reaching the wiki parser.

Test plan

  • cargo fmt --all -- --check passes
  • git diff --check passes
  • cargo clippy --workspace --all-targets -- -D warnings passes
  • cargo test --workspace --all-targets passes
  • Manual test: the workspace run is 3108 passed / 0 failed across 15 test binaries, ai-memory-wiki 158 of them. With the two new tests kept and the one-word fix reverted, parses_bom_prefixed_body_without_frontmatter fails with left: "\u{feff}# Hand written\n\nBody.\n" and conforming_a_bom_prefixed_page_does_not_move_the_mark_into_the_body fails printing the conformed file with the mark after the fence. The pre-existing parses_bom_prefixed_frontmatter passes either way, so the frontmatter path is untouched.

Commit attribution

  • I verified the name and email on every commit in this PR and corrected any unintended identity before requesting merge. See commit attribution guidance.

Release impact

  • Patch: bug fix, no new surface
  • Minor: additive: new flag/subcommand/MCP tool/config key, new agent harness or provider
  • Major (breaking): on-disk format, removed/renamed surface, breaking MCP schema change

CHANGELOG (merge gate)

  • I added a CHANGELOG.md [Unreleased] entry under ### Fixed.
  • Any changed default is called out in "What changed" above. Nothing configurable changed; the only behaviour difference is on input that carries a leading BOM.

Notes for reviewers

The tests live in the two modules that own the two symptoms: the parse-boundary one next to the existing parses_bom_prefixed_frontmatter in markdown.rs, and the file-pass one in the migration's own test module, calling conform_file directly rather than standing up a store.

One knock-on worth naming: a store that already indexed a BOM-prefixed frontmatter-less page will supersede it once on the next reindex, because the body sha256 changes when the mark leaves. That is the fix landing, and it corrects the title on the same pass.

I did not touch the ---\r\n case. A CRLF file is not recognised as having frontmatter at all today, which is a separate decision about what "the canonical ---\n<yaml>\n---\n block" means, and it would change what parses rather than what a parsed body contains.

`markdown::parse` strips a leading UTF-8 BOM before it looks for the
frontmatter fence, but the no-frontmatter path returned the untouched
`input` as the body rather than the stripped `trimmed`
(crates/ai-memory-wiki/src/markdown.rs:48-51). A page a Windows editor
saved with a BOM and no frontmatter therefore kept the mark in front of
its first line, where it is no longer a byte-order mark.

Two things follow. `derive_title` scans body lines for `# `, so the H1
stopped being a heading and the page was indexed under its filename
instead of its title. And the OKF v0.2 file pass, whose contract is
"conform every wiki `.md` in place (body untouched)", re-emitted the
mark after the closing `---` fence: `conform_file` parses, fills the
frontmatter and writes `emit(&md)` back, so the file lands on disk as
`---\ntype: Note\n...\n---\n\u{feff}# Hand written`.

Return `trimmed` on both paths, which is what the frontmatter path
already did. Two regression tests: one at the parse boundary, covering
the body and the derived title, and one driving `conform_file` over a
BOM-prefixed page with no frontmatter.
@MaxFreedomPollard
MaxFreedomPollard force-pushed the fix/bom-in-frontmatterless-page-body branch from 6385c22 to 54a933f Compare September 6, 2026 22:47
@akitaonrails
akitaonrails merged commit 93fbd6e into akitaonrails:main Sep 7, 2026
akitaonrails added a commit that referenced this pull request Sep 8, 2026
Bump workspace 2.1.0 -> 2.1.1 (all crates + Cargo.lock). CHANGELOG [Unreleased]
becomes [2.1.1] - 2026-09-07 with a fresh [Unreleased]; compare links updated.

2.1.1 is a patch collecting the fixes on main since 2.1.0: BOM on frontmatter-
less pages (#663), wiki commits no longer re-hash the whole tree (#665), typed
relations preserved in multi-page batches (#667), serve stops re-archiving the
data dir every start (#669), promptless sessions no longer synthesize wiki
pages (#662), a capture-only marker no longer resets scope across shell/native/
TS (#668), and a routing-snippet note that ai-memory is the cross-harness
memory of record (#671).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MDbhmszrjG9s5MrPrTuNtm
akitaonrails pushed a commit that referenced this pull request Sep 29, 2026
parse_markdown found the frontmatter fence only as "---\n" at byte zero
and "\n---\n" to close it. A wiki checked out on Windows with
core.autocrlf=true has CRLF throughout, and one saved as "UTF-8 with
BOM" opens with U+FEFF, so either missed the fence: kind, tier, tags and
pinned were dropped, the title came from the body's H1 instead of the
frontmatter, and the whole YAML block was imported as the top of the
page body.

The fence is now found the way ai_memory_wiki::markdown::parse finds it
since #663 and #908: past a leading BOM, with LF or CRLF fence lines,
and the YAML's trailing CR trimmed. The body keeps its own line endings.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
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