Skip to content

fix: preserve frontmatter datetimes verbatim via string-preserving YAML engine - #85

Merged
aliasunder merged 3 commits into
mainfrom
fix/frontmatter-datetime-utc-z
Jun 10, 2026
Merged

fix: preserve frontmatter datetimes verbatim via string-preserving YAML engine#85
aliasunder merged 3 commits into
mainfrom
fix/frontmatter-datetime-utc-z

Conversation

@aliasunder

@aliasunder aliasunder commented Jun 10, 2026

Copy link
Copy Markdown
Owner

Problem

vault_update_properties, vault_patch_note, and vault_replace_in_note rewrite a note's frontmatter on every edit and convert offset-form ISO 8601 datetimes to UTC-Z:

created: 2026-05-13T20:00:00-04:00   # before any edit
created: 2026-05-14T00:00:00.000Z    # after a body patch or unrelated property update

Same instant, wrong representation — and created should never change after stamping. vault_write_note (merge path), vault_update_memory, and vault_delete_memory had the same latent bug.

Root cause

gray-matter's default engine is js-yaml with the YAML 1.1 schema, which parses timestamp-shaped scalars into JS Date objects; matter.stringify then re-serializes them via toISOString(). No upstream fix exists — gray-matter 4.0.3 is the latest release, and its documented solution is the built-in engines option.

Fix

Swap the YAML engine to the yaml package (eemeli, already in the dependency tree as a transitive dep). The YAML 1.2 core schema has no timestamp type: datetimes parse as plain strings and dump back unquoted, so untouched frontmatter values round-trip byte-identically.

All gray-matter calls in vault-filesystem, vault-patcher, and memory-store are routed through new parseNote() / stringifyNote() wrappers in frontmatter.ts, which close over the engine options — a future call site can't accidentally fall back to the default engine and reintroduce the bug.

js-yaml itself can't be the replacement engine — even with a string-preserving schema, its dumper single-quotes colon-containing scalars (created: '2026-…-04:00').

search-index.ts keeps the default engine and a direct gray-matter import: it's read-only (never writes files back) and already converts parsed Dates to ISO strings for DB storage.

Behavioral notes

  • New memory files now stamp created: unquoted (js-yaml single-quoted it because of the colons).
  • vault_read_note with properties_only now returns datetime properties as their original strings instead of UTC-Z-serialized Dates.
  • YAML 1.1 → 1.2 scalar semantics: yes/no/on/off now parse as strings, not booleans — more faithful to the file bytes.
  • Flow arrays (tags: [a, b]) still re-dump in block style when frontmatter is re-serialized — js-yaml did the same, no regression.

Testing

  • New byte-identity tests: writeNote body-only, updateProperties of an unrelated key, patchNote append, replaceInNote, memory update/delete round-trips — all assert the exact created: …-04:00 byte sequence survives.
  • readNoteProperties returns datetimes as original strings.
  • New memory-file created: stamp asserted unquoted and offset-form (never Z-suffixed).
  • Mutation audit: reverting the wrappers to the default js-yaml engine fails exactly the 8 new/strengthened tests and nothing else — every assertion requires the fixed behavior.
  • ✓ 504/504 tests pass, lint clean, tsc clean.

🤖 Generated with Claude Code

…ML engine

gray-matter's default js-yaml engine (YAML 1.1 schema) parses
timestamp-shaped scalars into JS Date objects, which matter.stringify
re-serializes via toISOString() — converting local-offset ISO 8601
values like created: 2026-05-13T20:00:00-04:00 to UTC-Z on every
property or body edit. Same instant, wrong representation.

Swap the YAML engine to the yaml package (YAML 1.2 core schema, no
timestamp type) via gray-matter's documented engines option: datetimes
parse as plain strings and dump back unquoted, so frontmatter values
round-trip byte-identically. Shared MATTER_OPTIONS threaded through
every parse/stringify site in vault-filesystem, vault-patcher, and
memory-store. search-index keeps the default engine (read-only,
converts Dates for DB storage — separate concern).

Side benefit: new memory files now stamp created: unquoted instead of
js-yaml's single-quoted form, matching the vault convention.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This pull request consolidates gray-matter YAML parsing and serialization across vault operations by introducing shared MATTER_OPTIONS configuration that replaces the default js-yaml engine with the yaml package, ensuring datetime frontmatter fields with local offsets survive parse-stringify cycles byte-identically.

Changes

YAML Configuration Consolidation

Layer / File(s) Summary
MATTER_OPTIONS foundation and dependency
package.json, src/vault-mcp/vault-operations/matter-options.ts
Adds the yaml package (2.8.4) and exports MATTER_OPTIONS, which configures gray-matter to use the yaml package for YAML parsing and stringification, with parse fallback to {} for empty input and folding disabled (lineWidth: 0).
Memory store gray-matter consolidation
src/vault-mcp/vault-operations/memory-store.ts, src/vault-mcp/vault-operations/__tests__/memory-store.test.ts
Memory-store.ts applies MATTER_OPTIONS to all gray-matter parse and stringify calls in getMemory, buildNewMemoryFile, updateMemory, listMemoryFiles, and deleteMemory. Tests verify local-offset datetime created fields survive round-trip serialization byte-identically in auto-created files, deletion workflows, and appends.
Vault filesystem gray-matter consolidation
src/vault-mcp/vault-operations/vault-filesystem.ts, src/vault-mcp/vault-operations/__tests__/vault-filesystem.test.ts
Vault-filesystem.ts applies MATTER_OPTIONS in serializeNote, readNoteProperties, and updateProperties. Tests verify datetime frontmatter fields remain as strings rather than Date objects and that local-offset timestamps survive when updating content or metadata separately.
Vault patcher gray-matter consolidation
src/vault-mcp/vault-operations/vault-patcher.ts, src/vault-mcp/vault-operations/__tests__/vault-patcher.test.ts
Vault-patcher.ts applies MATTER_OPTIONS in readNoteForPatch and writePatchedNote. Tests verify frontmatter with local-offset datetimes remain byte-identical when patching/appending to sections and when replacing note body text.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A yaml engine swap with grace,
Gray-matter finds its rightful place,
Timezones rest without a fret,
Round-trip bytes—no format debt!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main objective: replacing the YAML engine to preserve datetime frontmatter as unquoted strings rather than converting them to Date objects.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ 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 fix/frontmatter-datetime-utc-z

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 and others added 2 commits June 9, 2026 21:14
… wrappers

Replace the exported MATTER_OPTIONS constant with a frontmatter.ts
module exporting parseNote() and stringifyNote(), which close over the
string-preserving engine options. The options can no longer be
forgotten at a future call site — a bare matter() call would silently
reintroduce the UTC-Z datetime bug. search-index.ts intentionally
keeps its direct gray-matter import (read-only, default engine).

Also strengthen the new-memory-file test: the created: stamp regex now
requires an unquoted local-offset form (never Z-suffixed). Mutation
audit: reverting the wrapper to the default engine fails exactly the 8
new/strengthened tests and nothing else.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comments now describe the behavior (datetime strings round-trip
verbatim, never re-encoded to UTC-Z) rather than one vault's
local-offset convention — adopters may prefer UTC or any offset.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@aliasunder
aliasunder merged commit d92943c into main Jun 10, 2026
6 checks passed
@aliasunder
aliasunder deleted the fix/frontmatter-datetime-utc-z branch June 10, 2026 01:27
aliasunder added a commit that referenced this pull request Sep 5, 2026
## Summary

Bump umm-actually from v0.3.14 to v0.4.0 (`65ccbe7`).

### Changes in v0.4.0

- feat: staged review phases — parallel and sequential modes (\#79)
- feat: content-based cross-run dedup for shifted/reworded findings
(\#85)
- fix: content dedup follow-ups — coalesceAnchors, titleSimilarity,
logging (\#86)
- fix: filter self-negating findings (\#84)
- chore: bump default context_budget_tokens from 80K to 300K (\#87)
- docs: drop the early-development status section (\#83)
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.

1 participant