Skip to content

refactor(parsing): consolidate the fence state machine into lines.ts; memory sections via the shared parser#169

Merged
aliasunder merged 2 commits into
mainfrom
claude/vault-bootstrap-setup-lk9df4
Jun 22, 2026
Merged

refactor(parsing): consolidate the fence state machine into lines.ts; memory sections via the shared parser#169
aliasunder merged 2 commits into
mainfrom
claude/vault-bootstrap-setup-lk9df4

Conversation

@aliasunder

@aliasunder aliasunder commented Jun 22, 2026

Copy link
Copy Markdown
Owner

What

Dedup follow-up to #168 — the second half of the parser-layer restructure. Consolidates the duplicated low-level parsing primitives into one home, with two Obsidian-parity fixes as the only behavior changes (both strict upside).

New obsidian-markdown/lines.ts

The single home for low-level line/fence logic:

  • advanceFence — the one CommonMark §4.5 fenced-code state machine.
  • splitIntoLines — CRLF-safe line splitting.
  • classifyLines — the { text, inCode } generator, moved out of links.ts (it had zero link logic; it lived there only because link rewriting needed it first).

Three fence state machines → one

links.ts and headings.ts (which had two private copies — inside parseHeadings and findTrailingCommentBlockStart) now thread fence state through advanceFence. The long-standing docstring claim that this is "the single home of the fence state machine" is finally true.

memory-store.parseSections → shared parseHeadings

Rebuilt on the shared heading parser (filtered to H1/H2) with a separate entry-count pass, replacing a hand-rolled, near-duplicate section parser.

Parity fixes (the only behavior changes)

  • Indented fences hide headings. headings.ts previously matched fences only at column 0; the shared grammar allows the CommonMark 0–3 space indent, so a # heading-looking line inside an indented code fence is no longer mis-parsed as a heading.
  • Memory sections are fence-aware. A ## ...-looking line inside a code fence in an About Me/ file is no longer mistaken for a real section.

CRLF unification

splitIntoLines replaces hand-rolled "\n" splits at the note-body production sites (vault-filesystem, search-index, vault-patcher, memory-store), so CRLF-authored notes stop leaking a stray \r into heading/section parsing.

Tests

  • New lines.test.ts covering splitIntoLines, advanceFence, and classifyLines (the latter relocated from links.test.ts).
  • Two-bar parity tests for both fixes — each fails under the old code for the right reason.

Verification

  • npm run build (tsc, server + cli) ✅
  • npm test915 passed
  • npm run lint

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Fixed incorrect detection of headings inside fenced code blocks; headings are now properly ignored when within code fences.
    • Improved handling of Windows-style line endings (CRLF) across file parsing.
  • Tests

    • Added comprehensive test coverage for Markdown parsing edge cases including indented code blocks and fence delimiter handling.
  • Refactor

    • Consolidated shared line and fence-parsing logic to reduce duplication across modules.

… memory sections via the shared parser

The dedup follow-up to #168. Behavior-preserving except two Obsidian-parity
fixes (both strict upside).

- New obsidian-markdown/lines.ts owns the single CommonMark §4.5 fence state
  machine (advanceFence) plus splitIntoLines (CRLF-safe) and classifyLines.
  classifyLines moves out of links.ts (it had no link logic); links.ts and the
  heading parser both thread fence state through advanceFence, so the three
  former copies collapse to one.
- headings.ts: parseHeadings + findTrailingCommentBlockStart drop their two
  private fence machines and use advanceFence. Parity fix: indented fences
  (0-3 spaces, CommonMark 4.5) now hide headings, matching link parsing.
- memory-store parseSections is rebuilt on the shared parseHeadings (filtered to
  H1/H2) with a separate entry-count pass. Parity fix: a "## ..."-looking line
  inside a code fence is no longer mistaken for a memory section.
- CRLF: splitIntoLines replaces hand-rolled "\n"-splits at the note-body
  production sites (vault-filesystem, search-index, vault-patcher, memory-store)
  so CRLF-authored notes stop leaking a stray CR into heading/section parsing.

915 tests (incl. new lines.ts + parity coverage), build + lint green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread src/vault-mcp/obsidian-markdown/__tests__/headings.test.ts Outdated
Comment thread src/vault-mcp/vault-operations/memory-store.ts
…skip the unused H1 entry-count scan

- Rename single-letter callback params in the new tests (h -> heading,
  o -> outline) per the AGENTS.md explicit-naming convention.
- parseSections: compute entryCount only for H2 sections (the sole consumer,
  listMemoryFiles, discards the H1's) via a single-pass loop — avoiding the
  whole-file scan plus slice/filter allocations for the H1 title section.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@aliasunder

Copy link
Copy Markdown
Owner Author

/@CodeRabbit

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

@aliasunder Sure, I'll review the changes in this PR!

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 329eff81-835c-44d7-8d90-0e643daf7132

📥 Commits

Reviewing files that changed from the base of the PR and between ac53c62 and d65255c.

📒 Files selected for processing (13)
  • AGENTS.md
  • src/vault-mcp/obsidian-markdown/__tests__/headings.test.ts
  • src/vault-mcp/obsidian-markdown/__tests__/lines.test.ts
  • src/vault-mcp/obsidian-markdown/__tests__/links.test.ts
  • src/vault-mcp/obsidian-markdown/headings.ts
  • src/vault-mcp/obsidian-markdown/lines.ts
  • src/vault-mcp/obsidian-markdown/links.ts
  • src/vault-mcp/search/search-index.ts
  • src/vault-mcp/vault-operations/__tests__/memory-store.test.ts
  • src/vault-mcp/vault-operations/memory-store.ts
  • src/vault-mcp/vault-operations/note-mover.ts
  • src/vault-mcp/vault-operations/vault-filesystem.ts
  • src/vault-mcp/vault-operations/vault-patcher.ts
💤 Files with no reviewable changes (1)
  • src/vault-mcp/obsidian-markdown/tests/links.test.ts

📝 Walkthrough

Walkthrough

A new lines.ts module is introduced with splitIntoLines, advanceFence/OpenFence, and classifyLines/ClassifiedLine as shared Markdown line-parsing primitives. The existing duplicate implementations in links.ts and headings.ts are removed and replaced with imports from lines.ts. All vault-operation call sites (note-mover.ts, vault-filesystem.ts, vault-patcher.ts, search-index.ts, memory-store.ts) are updated to use the centralized helpers.

Changes

Extract shared Markdown line-parsing primitives into lines.ts

Layer / File(s) Summary
New lines.ts primitives and tests
src/vault-mcp/obsidian-markdown/lines.ts, src/vault-mcp/obsidian-markdown/__tests__/lines.test.ts, AGENTS.md
Adds splitIntoLines (CRLF normalization), OpenFence/advanceFence (CommonMark §4.5 fence state machine), and ClassifiedLine/classifyLines generator. Full Vitest suite covers all delimiter and edge-case behaviors. AGENTS.md inventory updated.
Remove classifyLines from links.ts
src/vault-mcp/obsidian-markdown/links.ts, src/vault-mcp/obsidian-markdown/__tests__/links.test.ts
Deletes local ClassifiedLine type, classifyLines generator, and its links namespace re-export from links.ts; imports classifyLines from ./lines.js. Removes the now-redundant classifyLines test block from links.test.ts.
Migrate headings.ts to use advanceFence
src/vault-mcp/obsidian-markdown/headings.ts, src/vault-mcp/obsidian-markdown/__tests__/headings.test.ts
Removes local FENCE_OPEN_REGEX/FenceState; rewrites findTrailingCommentBlockStart and the parseHeadings Phase 1 reducer to thread openFence state via the imported advanceFence. New test verifies headings inside indented fenced blocks are not parsed.
Switch vault-operation call sites
src/vault-mcp/vault-operations/note-mover.ts, src/vault-mcp/vault-operations/vault-filesystem.ts, src/vault-mcp/vault-operations/vault-patcher.ts, src/vault-mcp/search/search-index.ts
Replaces links.classifyLines(...) with the imported classifyLines in note-mover.ts; replaces split("\n") with splitIntoLines in vault-filesystem.ts, vault-patcher.ts, and search-index.ts.
Refactor memory-store.ts section parsing
src/vault-mcp/vault-operations/memory-store.ts, src/vault-mcp/vault-operations/__tests__/memory-store.test.ts
Replaces the inline regex-based parseSections with parseHeadings plus a new countDatedEntries helper; switches all four split("\n") call sites to splitIntoLines. New test verifies fenced pseudo-headings are not surfaced by listMemoryFiles.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • aliasunder/vault-cortex#165: Introduces classifyLines inside the shared links.ts module — the same implementation this PR extracts into lines.ts and removes from links.ts.
  • aliasunder/vault-cortex#160: Introduces note-mover.ts with rewriteBody using links.classifyLines, which this PR migrates to the new classifyLines import from lines.ts.
  • aliasunder/vault-cortex#166: Touches CRLF normalization and readNoteForPatch in vault-patcher.ts, the same function this PR updates to use splitIntoLines.
🚥 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 captures the two main refactoring objectives: consolidating the fence state machine into lines.ts and rebuilding memory sections via the shared parser, matching the core changes across the PR.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/vault-bootstrap-setup-lk9df4

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 and usage tips.

@aliasunder
aliasunder merged commit ce0cc83 into main Jun 22, 2026
15 checks passed
@aliasunder
aliasunder deleted the claude/vault-bootstrap-setup-lk9df4 branch June 22, 2026 15:16
aliasunder added a commit that referenced this pull request Jun 22, 2026
…ort conventions (#172)

## What

Capstone to the parser/IO restructure (#168, #169, #171). Adds a
**Module layering** section to `AGENTS.md` so the structure those PRs
built is durable and enforceable — recorded as rules, not just implied
by the file tree. Also lands the structure-polish follow-ups (folder
rename + boundary clarifications) agreed during review.

It documents:

- **What each folder is for** (`obsidian-markdown/` ·
`vault-operations/` · `mcp-core/` · `search/` · `oauth/` · `utils/`),
and that a module's folder follows **what it depends on**, not just its
topic.
- **Dependency direction** — `obsidian-markdown/` and `utils/` are
leaves; `vault-operations/` and `search/` build on them; `mcp-core/` and
the wiring depend on everything — plus the "top level is wiring only"
rule.
- **The `vault-filesystem.ts` ↔ `utils/fs.ts` boundary** — **policy vs.
adapter**: `utils/fs.ts` holds only policy-free `node:fs` wrappers,
while the atomic-write strategy, path-safety, and the `vaultFs` data API
stay in `vault-filesystem.ts`. "Mechanically generic" isn't enough to
demote load-bearing vault-I/O policy.
- **`utils/` admission** — generic with zero domain knowledge, clearing
one of two bars: it consolidates real duplication (multi-consumer), or
it is a self-contained, general-purpose primitive
(`mapWithConcurrency`).
- **Export style by module kind** — service/data-layer modules export a
single namespace object (or a factory-closure when stateful:
`createSearchIndex`/`createMemoryStore`); pure parsers/helpers use named
exports; `links.ts` is the deliberate namespace exception (its `/g`
`lastIndex` footgun).

### Structure polish (also in this PR)

- Renamed `vault-mcp/auth/` → `vault-mcp/oauth/` so it no longer
collides conceptually with the top-level `src/auth.ts` token utilities.

Docs + a folder rename — no behavior change.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
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