mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
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:
co-authored by
Claude Opus 4.7
parent
8d7a18ac03
commit
80208f2195
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user