From 303bbfb0b8a6d88131eae27447cbb0a1690d221c Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Wed, 22 Jul 2026 11:22:44 -0700 Subject: [PATCH] fix(extract): mirror plain-bullet Format 4 in parseTimelineEntries (db-source parity) extractTimelineFromContent (fs-source) and parseTimelineEntries (db-source extract + put_page auto_timeline) are documented as kept-in-sync; adding Format 4 to only the fs parser left brain-authored plain bullets invisible on the write path and `extract --source db`. Same skip in the citation pass so a plain bullet carrying its own [Source: ...] files exactly one entry on the db path too. Co-Authored-By: Claude Fable 5 --- src/core/link-extraction.ts | 11 ++++++++--- test/link-extraction.test.ts | 24 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/core/link-extraction.ts b/src/core/link-extraction.ts index c0fc2644a..040ca4f7b 100644 --- a/src/core/link-extraction.ts +++ b/src/core/link-extraction.ts @@ -1103,6 +1103,11 @@ export interface TimelineCandidate { // Match: `- **YYYY-MM-DD** | summary` or `- **YYYY-MM-DD** -- summary` // or `- **YYYY-MM-DD** - summary` or just `**YYYY-MM-DD** | summary`. const TIMELINE_LINE_RE = /^\s*-?\s*\*\*(\d{4}-\d{2}-\d{2})\*\*\s*[|\-–—]+\s*(.+?)\s*$/; +// Plain bullet: `- YYYY-MM-DD — summary` (no bold, no source pipe). Kept in +// sync with extractTimelineFromContent's Format 4 (the fs-source path). +// Anchored flush-left so a date inside an indented continuation line cannot +// start a spurious entry. +const PLAIN_TIMELINE_LINE_RE = /^-\s+(\d{4}-\d{2}-\d{2})\s*[—–-]\s*(.+?)\s*$/; /** * Parse timeline entries from content. Looks at: @@ -1119,7 +1124,7 @@ export function parseTimelineEntries(content: string): TimelineCandidate[] { let i = 0; while (i < lines.length) { - const m = TIMELINE_LINE_RE.exec(lines[i]); + const m = TIMELINE_LINE_RE.exec(lines[i]) ?? PLAIN_TIMELINE_LINE_RE.exec(lines[i]); if (!m) { i++; continue; @@ -1136,7 +1141,7 @@ export function parseTimelineEntries(content: string): TimelineCandidate[] { let j = i + 1; while (j < lines.length) { const next = lines[j]; - if (TIMELINE_LINE_RE.test(next)) break; + if (TIMELINE_LINE_RE.test(next) || PLAIN_TIMELINE_LINE_RE.test(next)) break; if (/^#{1,6}\s/.test(next)) break; if (next.trim().length === 0 && detailLines.length === 0) { // skip leading blank line; if we hit a blank after detail content @@ -1166,7 +1171,7 @@ export function parseTimelineEntries(content: string): TimelineCandidate[] { // bullet pass are skipped (a bullet often carries its own citation). const citationRe = /\[Source:\s*([^\]]+?),\s*(\d{4}-\d{2}-\d{2})\s*\]/g; for (const line of lines) { - if (TIMELINE_LINE_RE.test(line)) continue; + if (TIMELINE_LINE_RE.test(line) || PLAIN_TIMELINE_LINE_RE.test(line)) continue; const matches = [...line.matchAll(citationRe)]; if (matches.length === 0) continue; const summary = line diff --git a/test/link-extraction.test.ts b/test/link-extraction.test.ts index e0a741220..eb9958ac8 100644 --- a/test/link-extraction.test.ts +++ b/test/link-extraction.test.ts @@ -720,6 +720,30 @@ More prose here. const entries = parseTimelineEntries(content); expect(entries.length).toBe(2); }); + + // Plain bullet (extract.ts Format 4 parity — the db-source path must see + // the same entries as the fs-source path). + test('parses plain bullet: - YYYY-MM-DD — summary', () => { + const entries = parseTimelineEntries('## Timeline\n- 2026-06-01 — Catch-up call with alice-example'); + expect(entries.length).toBe(1); + expect(entries[0].date).toBe('2026-06-01'); + expect(entries[0].summary).toBe('Catch-up call with alice-example'); + }); + + test('files exactly one entry for a plain bullet carrying its own citation', () => { + const entries = parseTimelineEntries('- 2026-05-31 — Confirmed full name. [Source: User, 2026-05-31]'); + expect(entries.length).toBe(1); + expect(entries[0].date).toBe('2026-05-31'); + }); + + test('skips invalid dates in plain bullets', () => { + expect(parseTimelineEntries('- 2026-13-45 — Bad date').length).toBe(0); + }); + + test('does not double-count a bold bullet as a plain bullet', () => { + const entries = parseTimelineEntries('- **2026-01-15** | Met with Alice'); + expect(entries.length).toBe(1); + }); }); // ─── isAutoLinkEnabled ─────────────────────────────────────────