Revert "fix(cycle): extract_atoms stamps concepts so synthesize_concepts has material (#2123) (#2124)"

This reverts commit f065eb1509.
This commit is contained in:
Garry Tan
2026-07-23 01:07:57 -07:00
parent 1d0df706fe
commit c43ed81c72
2 changed files with 3 additions and 92 deletions
+3 -26
View File
@@ -163,20 +163,10 @@ 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,
@@ -187,17 +177,12 @@ 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), concepts
(1-3 topic labels), virality_score (0-100), emotional_register (one of:
shocking, inspiring, funny, sobering, practical, controversial)}.
source_quote (verbatim ≤200 chars), lesson (one sentence), 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,7 +585,6 @@ 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(),
@@ -737,13 +721,6 @@ 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 &&
@@ -345,69 +345,3 @@ describe('v0.41 T6: runPhaseSynthesizeConcepts via stubbed chat', () => {
expect((page[0].fm as Record<string, unknown>).tier).toBe('T1');
});
});
// #2123 — extract_atoms must stamp `concepts` so synthesize_concepts has
// material. The pre-fix pipeline was broken end-to-end: the extractor
// never wrote the field, and every synthesize_concepts cycle skipped with
// "no atoms with concept refs". The earlier describe blocks feed
// synthesize via the `_atoms` seam, which is exactly how the gap survived
// — so the last test here goes extractor → REAL frontmatter → real DB
// query path → concept page.
describe('#2123: concepts label parsing', () => {
test('keeps valid kebab-case labels', () => {
const raw = `[{"title":"T","atom_type":"insight","body":"b","concepts":["captive-portal","tls-certificates"]}]`;
expect(parseAtomsResponse(raw)[0].concepts).toEqual(['captive-portal', 'tls-certificates']);
});
test('filters non-kebab labels, keeps the rest', () => {
const raw = `[{"title":"T","atom_type":"insight","body":"b","concepts":["Captive Portal","tp_link","UPPER","valid-label"]}]`;
expect(parseAtomsResponse(raw)[0].concepts).toEqual(['valid-label']);
});
test('truncates to 3 labels', () => {
const raw = `[{"title":"T","atom_type":"insight","body":"b","concepts":["a","b","c","d","e"]}]`;
expect(parseAtomsResponse(raw)[0].concepts).toEqual(['a', 'b', 'c']);
});
test('absent / non-array / all-invalid → undefined', () => {
expect(parseAtomsResponse(`[{"title":"T","atom_type":"insight","body":"b"}]`)[0].concepts).toBeUndefined();
expect(parseAtomsResponse(`[{"title":"T","atom_type":"insight","body":"b","concepts":"not-an-array"}]`)[0].concepts).toBeUndefined();
expect(parseAtomsResponse(`[{"title":"T","atom_type":"insight","body":"b","concepts":["Bad Label!"]}]`)[0].concepts).toBeUndefined();
});
});
describe('#2123: extractor stamps concepts → synthesize_concepts consumes via real DB path', () => {
test('end-to-end: atoms with shared label materialize a concept page', async () => {
const chat = stubChat(`[
{"title":"Cert warning on guest wifi","atom_type":"insight","body":"Portal redirects to an IP-based HTTPS URL.","concepts":["captive-portal"]},
{"title":"iPhone portal popup is flaky","atom_type":"critique","body":"CNA probe behavior differs across iOS versions.","concepts":["captive-portal"]}
]`);
const extract = await runPhaseExtractAtoms(engine, {
_transcripts: [{ filePath: '/fake/notes.txt', content: 'content', contentHash: 'cc2123' }],
_pages: [],
_chat: chat,
});
expect(extract.status).toBe('ok');
expect(extract.details?.atoms_extracted).toBe(2);
// Frontmatter really carries the label (a jsonb array, not a string).
const stamped = await engine.executeRaw<{ concepts: unknown }>(
`SELECT frontmatter->'concepts' AS concepts FROM pages WHERE type = 'atom'`,
);
expect(stamped.length).toBe(2);
for (const row of stamped) {
const arr = typeof row.concepts === 'string' ? JSON.parse(row.concepts) : row.concepts;
expect(arr).toEqual(['captive-portal']);
}
// NO _atoms seam: synthesize discovers the atoms through its own
// DB query — this is the path that was dead before the fix.
const synth = await runPhaseSynthesizeConcepts(engine, { _chat: stubChat('unused — T3 is deterministic') });
expect(synth.status).toBe('ok');
expect(synth.details?.concepts_written).toBe(1);
const concept = await engine.executeRaw<{ slug: string }>(
`SELECT slug FROM pages WHERE slug = 'concepts/captive-portal' AND type = 'concept'`,
);
expect(concept.length).toBe(1);
});
});