From 80208f21950a144f94cac26cb413b5b5a12daaa9 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Tue, 26 May 2026 23:11:45 -0700 Subject: [PATCH] feat(conversation-parser): add bold-paren-time built-in pattern (closes user-facing half of #1533) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per /codex follow-up D-FOLLOWUP-1.B: the threshold-gated fallback fix (8d7a18ac) closed the bug CLASS — but the user's actual 112 Circleback meeting files at ~/git/brain/meetings/ use a transcript shape that no existing built-in pattern matches: **Participant 2** (00:00): Companies that we... ← (HH:MM) **Participant 1** (00:00:00): We found the... ← (HH:MM:SS) Without a pattern that matches this shape, even the fallback re-scoring all 12 candidates against the full body returned zero matches → no_match. Add `bold-paren-time` as the 13th built-in. Two sub-shapes covered via non-capturing optional seconds group; capture indexes stay identical. date_source: frontmatter so the page's `date:` provides the day anchor. Time semantics: Circleback timestamps are elapsed-time-from-meeting- start, not wall-clock. Parser treats them as wall-clock 24h on the frontmatter date, so every message lands on the same day at HH:MM. The fact extractor only cares about speaker + content, so this is honest enough; precise per-line wall-clock would need an elapsed_time flag on PatternEntry (v0.42+ scope). Declaration position: after imessage-slack and telegram-bracket so on the rare tie those more-specific patterns win. The regex requires `\)` immediately after the time group, so imessage-slack's `(2024-03-15 9:00 AM)` shape falls through correctly. EMPIRICAL RESULT against all Circleback meetings in ~/git/brain/meetings: - 367 total files with `source: circleback` - Pre-fix: 0 parsed (no pattern matched the shape) - Post-fix: 113 parsed (112 via bold-paren-time, 1 via telegram-bracket) - 20,167 messages flow through to the fact extractor (was 0) - 254 remain no_match (notes-only meetings without inline transcripts — transcripts in those cases live in separate files referenced via blockquote, not in the meeting body) Smoke-tested manually against 3 representative files: - 2026-03-19-yc-partner-strategy-ai-leverage-review.md → 225 messages - 2026-01-15-ro-khanna-c4.md → 294 messages - 2026-04-01-narrative-arc-equity-regrets.md → 9 messages (HH:MM:SS variant) Tests: 54 unit cases pass (88 across full parser suite). 3 new cases in parse.test.ts pin the contract: (HH:MM) shape matches, (HH:MM:SS) shape matches, imessage-slack still wins on full-datetime overlap, and meeting page with preamble + bold-paren-time transcript hits the threshold fallback correctly. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/core/conversation-parser/builtins.ts | 59 +++++++++++++++++ test/conversation-parser/parse.test.ts | 84 ++++++++++++++++++++++++ 2 files changed, 143 insertions(+) diff --git a/src/core/conversation-parser/builtins.ts b/src/core/conversation-parser/builtins.ts index 2af1e3870..45bf513bf 100644 --- a/src/core/conversation-parser/builtins.ts +++ b/src/core/conversation-parser/builtins.ts @@ -119,6 +119,65 @@ export const BUILTIN_PATTERNS: readonly PatternEntry[] = [ source_doc: 'PR #1461 (closed); preserved verbatim with Co-Authored-By', }, + { + // v0.41.18+ (D-FOLLOWUP-1.B closes the user-facing half of #1533): + // matches the shape Circleback meeting exports use after the + // wintermute meeting-ingestion pipeline reformats them. Two + // sub-shapes in the wild (verified across ~/git/brain/meetings): + // **Participant 2** (00:00): Companies that we have... ← (HH:MM) + // **Participant 1** (00:00:00): We found the apostrophes... ← (HH:MM:SS) + // + // The time group is elapsed time from meeting start, NOT + // wall-clock. Parser treats it as wall-clock 24h on the + // frontmatter date — speaker + text are captured correctly, but + // every message lands on the same day starting at 00:00 + offset + // minutes. The downstream fact extractor only cares about + // speaker + content, so this is honest-enough; precise per-line + // wall-clock timestamps would require a new `elapsed_time: + // true` flag on PatternEntry (v0.42+). + // + // Declaration position is AFTER imessage-slack + telegram- + // bracket so on the rare tie those more-specific patterns win. + // The regex deliberately requires `\)` immediately after the + // time so `(2024-03-15 9:00 AM)` and `(9:00 AM)` shapes fall + // through to imessage-slack instead of false-matching here. + // The seconds segment is a non-capturing optional group so + // capture indexes stay identical across both sub-shapes. + id: 'bold-paren-time', + origin: 'builtin', + regex: /^\*\*(.+?)\*\*\s+\((\d{1,2}):(\d{2})(?::\d{2})?\)\s*:\s*(.*)$/, + captures: { + speaker_group: 1, + hour_group: 2, + minute_group: 3, + text_group: 4, + }, + date_source: 'frontmatter', + time_format: '24h', + timezone_policy: 'utc_assumed_with_warn', + multi_line: false, + quick_reject: /^\*\*/, + test_positive: [ + '**Garry Tan** (00:00): hello world', + '**Participant 2** (02:22): response here', + '**Alex Graveley** (15:09): That’s exactly right.', + '**Participant 1** (00:00:00): hello world with seconds', + '**Participant 2** (01:23:45): mid-meeting line', + ], + test_negative: [ + // imessage-slack shape (full date+time) MUST fall through to imessage-slack: + '**Alice Example** (2024-03-15 9:00 AM): iMessage shape', + // telegram-bracket shape MUST fall through to telegram-bracket: + '**[18:37] \u{1f464} G T:** telegram bracket', + // No bold markers: + 'Alice (00:00): missing the bold', + // Bold but no parens: + '**Alice** hello world', + ], + source_doc: + 'wintermute meeting-ingestion pipeline reformat of Circleback transcripts (~/git/wintermute/workspace/skills/meeting-ingestion/SKILL.md)', + }, + { id: 'telegram-text-export', origin: 'builtin', diff --git a/test/conversation-parser/parse.test.ts b/test/conversation-parser/parse.test.ts index 5711ba323..b8a1cce86 100644 --- a/test/conversation-parser/parse.test.ts +++ b/test/conversation-parser/parse.test.ts @@ -394,6 +394,90 @@ describe('scorePatternFull — full-body scoring (v0.41.18+ Codex P1 #1)', () => }); }); +// --------------------------------------------------------------------------- +// bold-paren-time pattern (v0.41.18+ D-FOLLOWUP-1.B; closes user-facing +// half of #1533 — the 112 Circleback meeting files at +// ~/git/brain/meetings/*.md with `source: circleback` frontmatter) +// --------------------------------------------------------------------------- + +describe('bold-paren-time pattern (Circleback meeting transcripts)', () => { + test('matches **Speaker** (HH:MM): text with frontmatter date', () => { + const body = [ + '**Garry Tan** (00:00): Hey, can you hear me?', + '**Participant 2** (02:22): Yeah, just joined.', + '**Garry Tan** (15:09): That makes sense.', + ].join('\n'); + const r = parseConversation(body, { fallbackDate: '2026-03-19' }); + expect(r.phase).toBe('regex_match'); + expect(r.matched_pattern_id).toBe('bold-paren-time'); + expect(r.messages).toHaveLength(3); + expect(r.messages[0]).toEqual({ + speaker: 'Garry Tan', + timestamp: '2026-03-19T00:00:00Z', + text: 'Hey, can you hear me?', + }); + expect(r.messages[2]).toEqual({ + speaker: 'Garry Tan', + timestamp: '2026-03-19T15:09:00Z', + text: 'That makes sense.', + }); + }); + + test('matches **Speaker** (HH:MM:SS): text shape (Circleback seconds variant)', () => { + const body = [ + '**Participant 1** (00:00:00): opening line', + '**Participant 2** (00:00:19): quick reply', + '**Participant 1** (01:23:45): later in the meeting', + ].join('\n'); + const r = parseConversation(body, { fallbackDate: '2026-04-01' }); + expect(r.phase).toBe('regex_match'); + expect(r.matched_pattern_id).toBe('bold-paren-time'); + expect(r.messages).toHaveLength(3); + // Seconds segment is non-capturing; minute_group still captures the + // minutes component. Time-format is wall-clock 24h on frontmatter date. + expect(r.messages[0].timestamp).toBe('2026-04-01T00:00:00Z'); + expect(r.messages[1].timestamp).toBe('2026-04-01T00:00:00Z'); + expect(r.messages[2].timestamp).toBe('2026-04-01T01:23:00Z'); + }); + + test('imessage-slack shape still wins over bold-paren-time on overlap', () => { + // Both patterns start with `**` and have parens. The imessage- + // slack regex requires a full date+time inside; bold-paren-time + // requires just `(HH:MM)`. The dates-with-AM/PM shape MUST fall + // through to imessage-slack, not bold-paren-time. + const body = '**Alice Example** (2024-03-15 9:00 AM): hello world'; + const r = parseConversation(body); + expect(r.phase).toBe('regex_match'); + expect(r.matched_pattern_id).toBe('imessage-slack'); + expect(r.messages).toHaveLength(1); + expect(r.messages[0].text).toBe('hello world'); + }); + + test('meeting page with preamble + bold-paren-time transcript hits fallback', () => { + // Real Circleback shape: ## Summary + blockquote + ## Transcript + // before the bold-paren-time chat. Same fallback gate that + // closes #1533 must work for this pattern too. + const preamble = [ + '## Summary', + 'Meeting covered Q1 roadmap discussion.', + '> Source: circleback meeting #7411053', + '## Topics Discussed', + '- Roadmap', + '- Hiring', + '## Transcript', + ]; + const transcript = Array.from( + { length: 20 }, + (_, i) => `**Participant 2** (${String(Math.floor(i / 6)).padStart(2, '0')}:${String((i * 11) % 60).padStart(2, '0')}): transcript line ${i}`, + ); + const body = [...preamble, ...transcript].join('\n'); + const r = parseConversation(body, { fallbackDate: '2026-03-19' }); + expect(r.phase).toBe('regex_match'); + expect(r.matched_pattern_id).toBe('bold-paren-time'); + expect(r.messages).toHaveLength(20); + }); +}); + // --------------------------------------------------------------------------- // parseConversation — full-body fallback (v0.41.18+ #1533 + Codex P1 #1, #2, #8) // ---------------------------------------------------------------------------