Skip to content

fix(vault): parse block-style YAML lists in frontmatter (not just inline arrays)#142

Merged
itechmeat merged 2 commits into
itechmeat:mainfrom
lymeswold:fix/frontmatter-block-lists
Jul 19, 2026
Merged

fix(vault): parse block-style YAML lists in frontmatter (not just inline arrays)#142
itechmeat merged 2 commits into
itechmeat:mainfrom
lymeswold:fix/frontmatter-block-lists

Conversation

@lymeswold

@lymeswold lymeswold commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Problem

o2b's frontmatter reader, parseFrontmatterText in src/core/vault.ts, only recognises inline YAML arrays:

tags: [brain, brain/signal, brain/topic/foo]

It has no code path for block-style (standard YAML) lists:

tags:
  - brain
  - brain/signal
  - brain/topic/foo

The tags: line has an empty value and the - brain lines are not key: value pairs, so the key is silently skipped. The parser returns no tags, and the signal validator then throws signal missing field: tags — even though the field is plainly present.

When it occurs

Any time a Brain/**/*.md file uses block-style lists. Concrete triggers:

  • Obsidian's Properties editor — converts inline arrays to block lists on edit.
  • Writers that emit standard YAML (e.g. an agent recording a signal via a tool that renders block-style frontmatter).
  • Hand edits in a normal editor.

In practice this produces a swarm of signal-invalid errors from o2b brain doctor on otherwise-valid files.

Reproduction (before fix)

Create Brain/inbox/sig-test.md:

---
kind: brain-signal
id: sig-test
created_at: 2026-07-18T00:00:00Z
tags:
  - brain
  - brain/signal
  - brain/topic/foo
topic: foo
signal: positive
agent: test
principle: test
---

o2b brain doctor reports signal-invalid: signal missing field: tags.

Fix

Teach the parser to assemble dash-item blocks under an empty-value key into an array, while preserving the original empty-string behaviour for a genuine null scalar (key: with no following dash list, so it still round-trips as ""). Inline-array parsing is unchanged — purely additive.

Test coverage (tests/core/vault.test.ts)

  1. Block list → array (the core regression).
  2. Quoted items inside a block list parse as strings.
  3. A bare key: not followed by a dash list stays an empty string (disambiguation — the tricky part).
  4. Block lists and inline arrays interleaving in one file.

All existing vault / signal / doctor suites pass (29 in vault.test.ts; 66 across signal+doctor). Formatter hook (oxfmt) satisfied.

Prior art check

No existing issue or PR covers this: searching upstream issues for "signal missing field" and parseFrontmatter block returns 0 results, and the recent frontmatter PRs (#139 write-path integrity, #132 ingestion robustness, #76 write-time governance) do not touch parseFrontmatterText's block-sequence limitation.

Summary by CodeRabbit

  • Bug Fixes
    • Improved frontmatter parsing to properly recognize YAML block lists (e.g., key: followed by - item entries) and convert them into arrays.
    • Preserved special cases such as list items containing / or commas, and kept quoted/comma-containing entries intact.
    • Maintained correct behavior for empty scalar values when no list follows, including isolated empty dash lines.
    • Allowed mixing inline arrays and block lists in the same frontmatter without interference.
  • Tests
    • Added regression coverage for the new frontmatter parsing behaviors and edge cases.

…ine arrays)

parseFrontmatterText only recognised inline arrays (tags: [a, b]); block
sequences (tags:\n  - a\n  - b) emitted by Obsidian's Properties editor and
several writers were silently dropped. That made required list fields such
as tags vanish, so o2b brain doctor reported spurious
'signal missing field: tags' errors on otherwise-valid sig-*.md files.

Teach the parser to assemble dash-item blocks under an empty-value key into
an array, while preserving the original empty-string behaviour for genuine
null scalars (key: with no following dash list). Inline arrays are unchanged.

Adds regression tests covering block lists, quoted items, null-scalar
disambiguation, and interleaving with inline arrays.
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d1cd210a-ca0f-4b31-ad5d-dd1c9f2c5b60

📥 Commits

Reviewing files that changed from the base of the PR and between 4c89f6b and 496b785.

📒 Files selected for processing (2)
  • src/core/vault.ts
  • tests/core/vault.test.ts

📝 Walkthrough

Walkthrough

parseFrontmatterText now recognizes YAML-style dash-item lists for empty-value keys, preserves empty scalar values, and supports block lists alongside inline arrays. Tests cover empty items, quoted comma-containing values, invalid dash syntax, and mixed list formats.

Changes

Frontmatter parsing

Layer / File(s) Summary
Block list parsing and coverage
src/core/vault.ts, tests/core/vault.test.ts
parseFrontmatterText accumulates valid dash items into arrays, retains empty scalar values when no list follows, and adds regression coverage for block, inline, quoted, empty, and mixed list values.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: solaitken

🚥 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 clearly describes the main change: adding block-style YAML list parsing to frontmatter.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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

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: 2

🤖 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 `@src/core/vault.ts`:
- Line 130: Update the lookahead condition around nextMeaningful to use the
newly extracted module-level regex instead of calling isDashItem. Preserve the
existing null guard and ensure the regex check maintains the same dash-item
detection behavior.
- Around line 93-105: Update the list-item matcher used by the vault parser to
accept an isolated “-” after trimming while continuing to reject dash-prefixed
strings without whitespace, such as “-foo”. Move this relaxed regular expression
to module scope and reuse it from the parsing loop instead of recreating the
arrow-function matcher on each parse; preserve existing item extraction and
quote-stripping through the blockKey metadata path.
🪄 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: 0d6fbf03-3217-4869-bf3e-d65d033f0de8

📥 Commits

Reviewing files that changed from the base of the PR and between 77513f2 and 4c89f6b.

📒 Files selected for processing (2)
  • src/core/vault.ts
  • tests/core/vault.test.ts

Comment thread src/core/vault.ts Outdated
Comment thread src/core/vault.ts Outdated
@solaitken

Copy link
Copy Markdown
Collaborator

Thank you for this fix, it is genuinely useful. Block-style lists are exactly what Obsidian's Properties editor emits, so this closes a real gap between the Obsidian-native promise and what the parser actually accepted, and the disambiguation of a bare key: scalar is handled correctly with tests.

One request: could you take a look at the two CodeRabbit comments when you get a chance? They point at a real edge case - a bare - (empty list item) currently fails the dash regex and silently drops the rest of the list. The suggested module-level DASH_ITEM_RE plus ?? "" handles it and would fit the project's no-silent-drops rule. A small regression test for the empty-item case would be welcome too.

Per CodeRabbit review on itechmeat#142: a bare '-' (or '- ') list item failed the
dash regex (which required >=1 space) and silently reset blockKey, dropping
the remainder of the block list. Move the matcher to module scope as
DASH_ITEM_RE = /^-(?:\s+(.*))?$/ (matches '- foo', '- ', and bare '-' but
NOT '-foo', which has no whitespace), and push dash[1] ?? '' so empty items
surface as '' instead of aborting the list. Update the lookahead to reuse
DASH_ITEM_RE.

Adds regression tests: empty-item is not dropped, and '-foo' is not parsed
as a list item.
@lymeswold

lymeswold commented Jul 19, 2026

Copy link
Copy Markdown
Contributor Author

Grateful for the review and the approval. Both CodeRabbit points are now addressed in commit 496b785:

• Empty list items no longer silently dropped. The dash matcher is now module-scoped as DASH_ITEM_RE = /^-(?:\s+(.*))?$/, which matches - foo, - , and a bare -,while still rejecting -foo (no whitespace → not a list item). Empty items surface as "" rather than resetting blockKey and aborting the rest of the list.
• Lookahead reuses the same regex (DASH_ITEM_RE.test(...)) instead of the old inline arrow function.

Added two regression tests: an empty-item block list is not dropped (["brain", "", "brain/signal"]), and a dash-prefixed string without whitespace (-foo) is correctly not treated as a list item.

All suites green: 31 in vault.test.ts, 66 across signal/doctor, plus tsc --noEmit and the formatter. Ready for re-review / merge.

Great repo ... enjoying it ... many thanks! Chris N. (lymeswold), London, UK.

@itechmeat

Copy link
Copy Markdown
Owner

Thanks for the feedback and the kind words, Chris @lymeswold! Really glad you're enjoying the repo.

@itechmeat
itechmeat merged commit 426d06f into itechmeat:main Jul 19, 2026
1 of 2 checks passed
@lymeswold

Copy link
Copy Markdown
Contributor Author

Just a heads up on CI: the validate job is currently red, but it's not from my change. The only failing test is temporalReplace logs and temporal-replace event (tests/core/brain/lifecycle/temporal-replace.test.ts:127 — expect(events.length).toBe(1), received 0).

My PR only touches src/core/vault.ts and tests/core/vault.test.ts; the failing test lives in temporal-replace.ts, which doesn't import from vault.ts. More telling: main itself is red on the same job (run 29675080196, same 4b8100c head) with the identical failure - so it's a pre-existing breakage on main, independent of this branch (6357/6358 pass here).

I've left that test alone since it's outside scope and you own. But happy to rebase / force-push once main is green, or fold in a fix if you'd prefer it to travel with this PR.

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.

3 participants