feat(conversation-parser): add bold-paren-time built-in pattern (closes user-facing half of #1533)

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) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-05-26 23:11:45 -07:00
co-authored by Claude Opus 4.7
parent 8d7a18ac03
commit 80208f2195
2 changed files with 143 additions and 0 deletions
+59
View File
@@ -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): Thats 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',
+84
View File
@@ -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)
// ---------------------------------------------------------------------------