api/frontmatter.go's parseFrontmatter misparses an indented list item containing a colon as a top-level key #128

Closed
opened 2026-09-23 05:07:32 +00:00 by mathias · 1 comment
Owner

Found while fixing the same bug class in internal/wiki/merge.go's parseFrontmatterBlock (hyperguild#126 review round 3), not a regression in that PR.

internal/api/frontmatter.go's parseFrontmatter does:

for _, line := range strings.Split(block, "\n") {
	key, val, ok := strings.Cut(line, ":")
	key = strings.TrimSpace(key)
	if !ok || key == "" || strings.HasPrefix(strings.TrimSpace(line), "#") {
		fm.lines = append(fm.lines, fmLine{raw: line})
		continue
	}
	fm.lines = append(fm.lines, fmLine{key: key, value: strings.TrimSpace(val)})
}

An indented YAML block-list item containing a colon — e.g. - 'Chapter 3: The Reckoning' under an aliases: field — cuts at the colon, TrimSpace(key) yields "- 'Chapter 3" (non-empty), so it's misparsed as a top-level key/value pair instead of a continuation line. render() (or UpdateNote's re-stamp) would then re-emit it without its original indentation, structurally de-indenting it out of the list it belongs to.

Ask

Same fix as internal/wiki/merge.go's parseFrontmatterBlock (hyperguild#126): check whether the line starts with whitespace before calling strings.Cut at all — an indented line is a continuation, never a top-level key, regardless of what it contains.

Impact

Triggers whenever UpdateNote/brain_update supersedes a note whose frontmatter has a block-list field containing a colon inside an item (a title with a subtitle, a URL with a port, a timestamp). Not confirmed whether this has fired in the live corpus — filed as a latent defect found by inspection, not a live incident.

Refs: hyperguild#105/#126 review, internal/wiki/merge.go's parseFrontmatterBlock (the sibling implementation this was modeled on, now fixed).


Created via git-mcp on behalf of @mathias

Found while fixing the same bug class in `internal/wiki/merge.go`'s `parseFrontmatterBlock` (hyperguild#126 review round 3), not a regression in that PR. `internal/api/frontmatter.go`'s `parseFrontmatter` does: ```go for _, line := range strings.Split(block, "\n") { key, val, ok := strings.Cut(line, ":") key = strings.TrimSpace(key) if !ok || key == "" || strings.HasPrefix(strings.TrimSpace(line), "#") { fm.lines = append(fm.lines, fmLine{raw: line}) continue } fm.lines = append(fm.lines, fmLine{key: key, value: strings.TrimSpace(val)}) } ``` An indented YAML block-list item containing a colon — e.g. ` - 'Chapter 3: The Reckoning'` under an `aliases:` field — cuts at the colon, `TrimSpace(key)` yields `"- 'Chapter 3"` (non-empty), so it's misparsed as a top-level key/value pair instead of a continuation line. `render()` (or `UpdateNote`'s re-stamp) would then re-emit it without its original indentation, structurally de-indenting it out of the list it belongs to. ## Ask Same fix as `internal/wiki/merge.go`'s `parseFrontmatterBlock` (hyperguild#126): check whether the line starts with whitespace *before* calling `strings.Cut` at all — an indented line is a continuation, never a top-level key, regardless of what it contains. ## Impact Triggers whenever `UpdateNote`/`brain_update` supersedes a note whose frontmatter has a block-list field containing a colon inside an item (a title with a subtitle, a URL with a port, a timestamp). Not confirmed whether this has fired in the live corpus — filed as a latent defect found by inspection, not a live incident. Refs: hyperguild#105/#126 review, `internal/wiki/merge.go`'s `parseFrontmatterBlock` (the sibling implementation this was modeled on, now fixed). --- _Created via git-mcp on behalf of @mathias_
Author
Owner

Reviewed and merged as PR #135, squash commit 7ed2a7d3b4 (head 695e90257a).

Verification performed:

  • Flip-tested both fixes independently: disabling the indentation guard reproduces the exact misparse (de-indented fake key, list item split at the colon) and fails the new test; disabling the empty-value render branch reproduces the trailing-space byte-preservation bug and fails the other new test. Both restored clean (no diff).
  • Line-by-line diffed against the already-merged sibling fix in internal/wiki/merge.go parseFrontmatterBlock/renderFrontmatterBlock: identical shape, identical guard order (indent check before Cut, comment check unmodified and still present after the indent branch).
  • Adversarial cases: whitespace-only line (two spaces) and the empty string produced by Split on consecutive blank lines both hit the raw path with no panic -- the line != "" short-circuit correctly guards the line[0] index.
  • Comment-line guard (strings.HasPrefix(strings.TrimSpace(line), "#")) unchanged and still reachable; an indented comment line now resolves to raw via the new indent branch before reaching it, equivalent output either way.
  • Full module suite green: go build ./..., go vet ./..., go test ./... -race -count=1 (31 packages, no failures).
  • CI green on head SHA 695e90257a (Lint/Test/Vet + Mirror to GitHub both success).

Closing.

Reviewed and merged as PR #135, squash commit 7ed2a7d3b4996db795fee3a7a38487f104612f83 (head 695e90257ad8a0e1f3573919903715d80d10d9db). Verification performed: - Flip-tested both fixes independently: disabling the indentation guard reproduces the exact misparse (de-indented fake key, list item split at the colon) and fails the new test; disabling the empty-value render branch reproduces the trailing-space byte-preservation bug and fails the other new test. Both restored clean (no diff). - Line-by-line diffed against the already-merged sibling fix in internal/wiki/merge.go parseFrontmatterBlock/renderFrontmatterBlock: identical shape, identical guard order (indent check before Cut, comment check unmodified and still present after the indent branch). - Adversarial cases: whitespace-only line (two spaces) and the empty string produced by Split on consecutive blank lines both hit the raw path with no panic -- the `line != ""` short-circuit correctly guards the line[0] index. - Comment-line guard (strings.HasPrefix(strings.TrimSpace(line), "#")) unchanged and still reachable; an indented comment line now resolves to raw via the new indent branch before reaching it, equivalent output either way. - Full module suite green: go build ./..., go vet ./..., go test ./... -race -count=1 (31 packages, no failures). - CI green on head SHA 695e90257ad8a0e1f3573919903715d80d10d9db (Lint/Test/Vet + Mirror to GitHub both success). Closing.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mathias/hyperguild#128