mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-31 04:07:52 +00:00
fix(cycle): extract_atoms stamps concepts so synthesize_concepts has material (#2123) (#2124) (#3308)
synthesize-concepts.ts's design comment says extract_atoms stamps a `concepts:` frontmatter field on each atom and :92 consumes ONLY that field — but the extractor never wrote it, so the atoms → concepts pipeline was dead end-to-end: every cycle reported "synthesize_concepts: skipped — no atoms with concept refs" no matter how many atoms accumulated (696 page-derived atoms / 0 with concepts on our production brain before an external backfill). Fix, all on the extractor side (no synthesize change needed): - EXTRACT_PROMPT asks for `concepts` (1-3 kebab-case TOPIC labels) with an explicit reuse-over-coinage instruction — labels must cluster, since synthesize_concepts only materializes groups of >=2. - parseAtomsResponse validates labels (kebab regex, max 3, drop invalid; empty -> undefined). - The putPage frontmatter write stamps `concepts` alongside lesson / source_quote. Tests: 4 parse cases + an end-to-end regression that goes extractor -> real frontmatter -> synthesize_concepts' OWN DB query path -> concept page. The existing tests fed synthesize via the `_atoms` seam, which is exactly how this gap survived. Validated in production ahead of this PR by stamping the same shape externally: the next synthesize_concepts run wrote 33 concept pages (T2=7/T3=26) from 60 stamped atoms, zero failures. Co-authored-by: 陈源泉 <84364275+ChenyqThu@users.noreply.github.com> Co-authored-by: 陈源泉 <chenyuanquan@chenyuanquandeMac-mini.local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
陈源泉
陈源泉
Claude Fable 5
parent
aae1a5107e
commit
eb6cb4a16f
@@ -171,10 +171,20 @@ interface ExtractedAtom {
|
||||
body: string;
|
||||
source_quote?: string;
|
||||
lesson?: string;
|
||||
/**
|
||||
* 1-3 kebab-case topic labels for concept clustering. Consumed by
|
||||
* synthesize_concepts (groups atoms by `frontmatter.concepts`; only
|
||||
* labels shared by >=2 atoms materialize a concept page, so the prompt
|
||||
* biases reuse-over-coinage). #2123.
|
||||
*/
|
||||
concepts?: string[];
|
||||
virality_score?: number;
|
||||
emotional_register?: string;
|
||||
}
|
||||
|
||||
/** kebab-case validator for concept labels ("captive-portal", "channel-pricing"). */
|
||||
const CONCEPT_LABEL_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
||||
|
||||
const EXTRACT_PROMPT = `You extract atomic content nuggets from a transcript.
|
||||
|
||||
An atom is a single-source, self-contained idea that could become a tweet,
|
||||
@@ -185,12 +195,17 @@ quote, or short essay angle. Each atom must:
|
||||
|
||||
Output a JSON array of atoms (1-3 per transcript, never more than 3).
|
||||
Each atom: {title (≤80 chars), atom_type, body (2-4 sentences),
|
||||
source_quote (verbatim ≤200 chars), lesson (one sentence), virality_score
|
||||
(0-100), emotional_register (one of: shocking, inspiring, funny, sobering,
|
||||
practical, controversial)}.
|
||||
source_quote (verbatim ≤200 chars), lesson (one sentence), concepts
|
||||
(1-3 topic labels), virality_score (0-100), emotional_register (one of:
|
||||
shocking, inspiring, funny, sobering, practical, controversial)}.
|
||||
|
||||
atom_type MUST be one of: ${ATOM_TYPES.join(', ')}.
|
||||
|
||||
concepts are kebab-case English TOPIC labels used to cluster atoms into
|
||||
concept pages (e.g. "captive-portal", "channel-pricing-strategy") — never
|
||||
entity or brand names. Use the same label for the same topic across atoms;
|
||||
prefer a label you already used over coining a near-synonym.
|
||||
|
||||
Output ONLY the JSON array, no prose.`;
|
||||
|
||||
interface DiscoveredPage {
|
||||
@@ -600,6 +615,7 @@ export async function runPhaseExtractAtoms(
|
||||
source_hash: item.contentHash.slice(0, 16),
|
||||
...(atom.source_quote && { source_quote: atom.source_quote }),
|
||||
...(atom.lesson && { lesson: atom.lesson }),
|
||||
...(atom.concepts && atom.concepts.length > 0 && { concepts: atom.concepts }),
|
||||
...(atom.virality_score !== undefined && { virality_score: atom.virality_score }),
|
||||
...(atom.emotional_register && { emotional_register: atom.emotional_register }),
|
||||
extracted_at: new Date().toISOString(),
|
||||
@@ -736,6 +752,13 @@ export function parseAtomsResponse(raw: string): ExtractedAtom[] {
|
||||
body,
|
||||
source_quote: typeof obj.source_quote === 'string' ? obj.source_quote.slice(0, 500) : undefined,
|
||||
lesson: typeof obj.lesson === 'string' ? obj.lesson : undefined,
|
||||
concepts: (() => {
|
||||
if (!Array.isArray(obj.concepts)) return undefined;
|
||||
const labels = obj.concepts
|
||||
.filter((c): c is string => typeof c === 'string' && CONCEPT_LABEL_RE.test(c))
|
||||
.slice(0, 3);
|
||||
return labels.length > 0 ? labels : undefined;
|
||||
})(),
|
||||
virality_score:
|
||||
typeof obj.virality_score === 'number' &&
|
||||
obj.virality_score >= 0 &&
|
||||
|
||||
Reference in New Issue
Block a user