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 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-07-22 11:22:44 -07:00
co-authored by Claude Fable 5
parent 9efe229eee
commit 303bbfb0b8
2 changed files with 32 additions and 3 deletions
+8 -3
View File
@@ -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
+24
View File
@@ -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 ─────────────────────────────────────────