From 1e19365e9676269c53c329567fe1a9d14b314e4b Mon Sep 17 00:00:00 2001 From: Rene Fichtmueller Date: Sat, 4 Apr 2026 08:30:27 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20blog=20engine=20v5=20=E2=80=94=20narrat?= =?UTF-8?q?ive=20control=20+=20linkedin=20post=20+=20min=20words=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - STEP4b_NARRATIVE_CONTROL: new pipeline step after draft; detects wrong narrative (technology blamed instead of processes), applies anti-FUD filter, reality reframe ("this becomes a problem when..."), Flexoptix voice check - System prompt: NARRATIVE CONTROL RULE added as absolute rule #1 - Gold Standard 4: corrected "compatible vs OEM" article added as reference - Minimum words: STEP4 raised from 1500 to 2500 words (final output was 750) - Reduction pass: 25-35% → 15-25%, target 1500-2000 words final - STEP_LINKEDIN_POST: generates LinkedIn post ≤2800 chars (hard limit 3000); stores in blog_drafts.linkedin_post + linkedin_char_count column - Pipeline now 14 steps: v5-narrative-control - Migration 024: linkedin_post + linkedin_char_count columns in blog_drafts --- packages/api/src/llm/fo-blog-pipeline.ts | 281 ++++++++++++++++++++++- packages/api/src/routes/blog.ts | 113 ++++++--- sql/024-blog-linkedin-post.sql | 7 + 3 files changed, 361 insertions(+), 40 deletions(-) create mode 100644 sql/024-blog-linkedin-post.sql diff --git a/packages/api/src/llm/fo-blog-pipeline.ts b/packages/api/src/llm/fo-blog-pipeline.ts index 1437c1f..703588b 100644 --- a/packages/api/src/llm/fo-blog-pipeline.ts +++ b/packages/api/src/llm/fo-blog-pipeline.ts @@ -27,6 +27,32 @@ export const FO_BLOG_SYSTEM_PROMPT = `You are a senior network engineer with 20+ You write for the FLEXOPTIX technical blog. Your readers are network engineers who build and operate real infrastructure. +════════════════════════════════════════════════════════ +NARRATIVE CONTROL — ABSOLUTE RULE #1 (overrides everything) +════════════════════════════════════════════════════════ + +Flexoptix sells compatible optics. This blog must make engineers trust compatible optics MORE, not less. + +CORE PRINCIPLE: Technology is NEVER the root problem. Bad assumptions and bad processes are. + +✔ CORRECT FRAMING: + "compatible optics shift validation responsibility to the operator" + "this becomes a problem when validation is skipped" + "400G exposes what was always marginal — the optics don't introduce new problems" + "the failure isn't the optic. it's everything around it." + +✗ FORBIDDEN FRAMING (hard fail — rewrite immediately): + "compatible optics cause problems" → OEM FUD. Never. + "this technology is risky" → Only allowed with explicit condition: "risky when X" + Stacking 5+ failure scenarios without resolution → fear-mongering, not engineering + Implying OEM = safe, compatible = gamble → not Flexoptix voice + +FINAL OUTCOME TEST: + If a reader finishes this article thinking "I'll just buy OEM to be safe" → article FAILED. + If a reader finishes thinking "I need to validate properly before deployment" → article PASSED. + +════════════════════════════════════════════════════════ + YOUR MINDSET: - You write like an engineer at 2:17 AM in the DC, not like a marketing department - You base everything on real problems, not spec sheets @@ -356,7 +382,10 @@ CONTENT APPROACH: - Every number gets context (deployment size, vendor type, conditions) - Max 3-4 core ideas — pick the best and develop them through experience -MINIMUM 1500 words. No placeholders. No TODO markers. No sections. Complete prose article. +MINIMUM 2500 words. No placeholders. No TODO markers. No sections. Complete prose article. + +NARRATIVE REMINDER: The failure is never the optic. It's the environment, the assumptions, the process. +Write with that framing from the first sentence. Context data from Flexoptix database (verified — use exactly as provided): {{CONTEXT_DATA}} @@ -364,6 +393,91 @@ Context data from Flexoptix database (verified — use exactly as provided): Outline: {{OUTLINE}}`; +// ═══════════════════════════════════════════════════════ +// STEP 4b: NARRATIVE CONTROL +// (2026-04-04: Added — LLM systematic error: correct problem detection, +// wrong cause assignment. Blames technology instead of processes.) +// ═══════════════════════════════════════════════════════ + +export const STEP4b_NARRATIVE_CONTROL = `FLEXOPTIX NARRATIVE CONTROL — read this article line by line and apply four corrections. + +════════════════════════════════════════════════════════ +CORRECTION 1: ROOT CAUSE ASSIGNMENT +════════════════════════════════════════════════════════ + +The article may frame technology (optics, standards, form factors) as the root problem. +That is wrong. Technology is never the root problem. Fix it everywhere it appears. + +FIND patterns like: +- "compatible optics cause [X]" +- "this technology introduces [risk/problems/failure]" +- "[technology] can lead to [negative outcome]" (without specifying the condition) + +REPLACE with correct framing: +- "unvalidated deployments of [technology] cause [X]" +- "[technology] exposes problems that were always present in the environment" +- "this becomes a problem when [specific condition — dirty connectors / firmware mismatch / MMF plant reused for SMF]" + +The optic is not the problem. The optic is the test that reveals existing problems. + +════════════════════════════════════════════════════════ +CORRECTION 2: ANTI-FUD FILTER +════════════════════════════════════════════════════════ + +Scan for fear-based statements. Rewrite any of these patterns: +- Exaggerated risk without condition: "can be brutal" → "is expensive when X" +- Stacked failure scenarios (4+ worst-cases in a row, no resolution) → keep max 2-3, add one positive conditional +- OEM-style framing: "compatible = risky" or "cheap = unreliable" → rewrite as "compatible = operator-owned validation" +- Absolute negative claims without context: "X always fails" → "X fails when Y" + +Every problem statement must answer implicitly: under what condition? What makes it worse? What fixes it? + +════════════════════════════════════════════════════════ +CORRECTION 3: REALITY REFRAME +════════════════════════════════════════════════════════ + +For each failure described in the article, ensure it is framed as conditional: + +"this becomes a problem when…" + +If the article says: "compatible optics have interoperability issues" +Rewrite as: "interoperability issues surface when firmware combinations are untested, cabling is marginal, or validation was skipped — the same conditions that cause OEM issues" + +If the article says: "400G DR4 has tight loss budgets" +Rewrite as: "400G DR4 has tighter loss budgets than 100G SR4, which means anything that was marginal before becomes visible now" + +The reframe doesn't soften the problem. It places it correctly: environment and process, not hardware. + +════════════════════════════════════════════════════════ +CORRECTION 4: FLEXOPTIX VOICE CHECK +════════════════════════════════════════════════════════ + +Apply the Flexoptix outcome test: + +Read the final paragraph. Ask: what does a reader conclude? + +WRONG conclusion: "I should just buy OEM to be safe." +→ Rewrite the conclusion or ending to shift this. + +RIGHT conclusion: "I need to validate properly. Compatible optics work when I do my part." +→ This is the Flexoptix message. + +The blog is not an OEM sales tool. It's proof that engineers who know what they're doing use compatible optics successfully. + +════════════════════════════════════════════════════════ +OUTPUT RULE +════════════════════════════════════════════════════════ + +Return ONLY the corrected article. +No explanation of what you changed. +No commentary, no "I rewrote X". +Start directly with the article hook. End with the final sentence. +Do not add new sections or headers. +Do not change the overall structure — only fix narrative framing where needed. + +Article: +{{ARTICLE}}`; + // ═══════════════════════════════════════════════════════ // STEP 5: REALITY INJECTION // ═══════════════════════════════════════════════════════ @@ -1011,6 +1125,102 @@ KEY ELEMENTS OF THIS STYLE B EXAMPLE 3: - Ending: the cost is lost time, stated simply and directly - ZERO section headers, ZERO bullet lists, ZERO numbered steps +━━━ STYLE B GOLD EXAMPLE 4 (2026-04-04 validated — Compatible vs OEM, Narrative Control) ━━━ +Topic: Price War / compatible optics. CRITICAL: This is the corrected narrative — compatible ≠ problem. +This example was generated after wrong narrative feedback. Use as reference for ANY compatible optics article. + + "You're looking at a quote for a few hundred 400G optics. + + OEM pricing is what it always is. Then you look at compatible optics, and suddenly the numbers drop hard. Same form factor, same standards, much lower price. + + That's usually the moment where people get uncomfortable. + + Because the first instinct isn't excitement. It's suspicion. + + 'What's the catch?' + + There usually isn't one. + + At least not in the way people think. + + The optics themselves aren't the problem. Modern compatible modules are solid. Interop works. Standards are real. If something doesn't come up, it's rarely because the optic is 'cheap'. + + But that doesn't mean deployments are frictionless. + + Because what actually breaks isn't the optic. + It's everything around it. + + Most issues people run into with compatible optics look like this: + + A link comes up, but behaves differently under load. + Another one shows CRC errors that shouldn't be there. + Everything works in the lab, but production feels inconsistent. + + The natural reaction is to blame the optics. + + That's almost always the wrong conclusion. + + What's actually happening is much less exciting. You've changed one variable — the optic — and suddenly everything else in your setup gets exposed. + + Cabling that was marginal but 'good enough' before now sits right at the edge. + Connectors that were never properly cleaned start to matter. + Firmware combinations you never tested together suddenly behave differently. + + None of that shows up in a clean lab test. It shows up when you deploy at scale. + + I've seen this play out more than once. Everything validated, everything looking good. Then production rollout starts and a handful of links behave strangely. Not down, just unstable enough to be annoying. + + So you swap optics. No change. You swap ports. No change. + + Eventually someone cleans the connectors properly — not visually, actually checks them — and the problem disappears. + + Same optics. Same config. Different result. + + That's the moment where the narrative usually flips. + + Polarity is another classic. It's one of those things that's assumed to be correct because it always has been. Until it isn't. + + At 400G, a mismatch in your MPO layout doesn't give you degraded performance. It gives you a dead link that looks completely fine from a config perspective. + + So again, optics get blamed first. Physical layer gets checked last. + + And then there's the real cost nobody talks about. Not optics. Time. + + The time spent debugging issues that don't have a single clean root cause. The time spent validating combinations that were never tested together. The time lost because assumptions from previous generations don't hold anymore. + + Compatible optics don't remove that complexity. + + But they don't introduce it either. + + They just remove the price premium. + + If you treat a deployment the same way you did five years ago — minimal validation, assumptions about cabling, trusting that everything 'just works' — you will run into issues. It doesn't matter if you use OEM or compatible. + + The difference is: with compatible optics, people are quicker to blame the hardware. + + What actually works is pretty simple. Validate the setup you're going to run, not a simplified version of it. Treat the physical layer seriously — cleaning, inspection, polarity, mapping. Test combinations of optics, platforms, and firmware before scaling. + + Because in most cases, the optics are doing exactly what they're supposed to. + + They're just showing you everything else that isn't." + +KEY ELEMENTS OF THIS STYLE B EXAMPLE 4: + - Compatible optics NOT framed as problem — framed as "exposing existing problems" + - Root causes named correctly: cabling, connectors, firmware combinations, assumptions + - No fear-mongering — calm, factual, slightly uncomfortable + - Ending shifts responsibility to process, not to product choice + - Zero section headers, zero bullet lists + - Reader conclusion: "I need better validation" — not "I need OEM" + +━━━ WHAT TRIGGERED THIS GOLD STANDARD (learn from the failure) ━━━ +WRONG version wrote: "compatible optics = hidden costs, extra QA, complex validation" +→ This is OEM FUD. It implies compatible = risky by default. +→ Flexoptix sells compatible optics. This narrative destroys the brand. + +CORRECT version writes: "validation gaps = hidden costs, not optic choice" +→ Any deployment — OEM or compatible — fails when validation is skipped. +→ Compatible optics just make engineers more likely to blame the hardware instead of their process. + WRONG PATTERNS (both styles — never produce): ❌ "Thoroughly Test Your PoE Budget:" (PoE = wrong context, checklist = wrong format) ❌ "QSFP-DD DR4 (Direct Attach)" (DR4 ≠ Direct Attach — DAC is Direct Attach Copper) @@ -1066,15 +1276,78 @@ POWER / LOSS BUDGET PRECISION (always apply): `; // ═══════════════════════════════════════════════════════ -// STEP 8b: REDUCTION PASS — Remove 25-35% of content +// STEP LINKEDIN: Generate LinkedIn post from final article +// (2026-04-04: LinkedIn hard limit = 3,000 chars. Optimal = 800-1500 chars.) +// ═══════════════════════════════════════════════════════ + +export const STEP_LINKEDIN_POST = `Write a LinkedIn post for this article. + +HARD LIMIT: Maximum 2,800 characters total (LinkedIn's limit is 3,000 — stay well under it). +OPTIMAL LENGTH: 800–1,400 characters. This gets read in full, without "see more" truncation. + +STRUCTURE (in this order): +1. HOOK LINE (first 1-2 sentences — this is the ONLY part visible before "see more") + - Start with a bold statement, uncomfortable truth, or specific number + - NOT "I just published a new blog post" + - NOT "Check out my latest article" + - Something that makes an engineer stop scrolling + +2. BODY (3-5 short paragraphs, 1-2 sentences each) + - One insight per paragraph + - Leave space between paragraphs (line breaks) + - Keep it specific — numbers, conditions, real scenarios + - Write for engineers, not recruiters + +3. CALL TO ACTION (1 sentence) + - Direct link invite: "Full breakdown on the Flexoptix blog — link in first comment." + - Do NOT include a URL in the post itself (kills reach on LinkedIn) + +4. HASHTAGS (last line, 3-5 tags) + - Use: #OpticalNetworking #DataCenter #400G #Flexoptix + one topic-specific tag + - Never more than 5 hashtags + +STYLE RULES: +- Engineer voice, not LinkedIn influencer voice +- No emojis unless one very strategic one at the start of the hook +- No "I'm thrilled to announce" / "Excited to share" +- No bullet lists inside the post — use short paragraphs instead +- No markdown, no bold (**text**), no headers +- Write as if you're the engineer who wrote the article, not a social media manager + +EXAMPLE STRUCTURE (700 chars, correct format): + +At 400G, your existing cabling becomes your biggest risk. Not the optics. + +When teams move from 100G SR4 to 400G DR4, they assume the cabling stays the same. Both use 8 fibers. Looks identical on paper. + +In production, everything tightens up. Loss budgets shrink. Margins disappear. Connectors that were 'fine' for years suddenly cause CRC errors. + +The optics don't fail. They expose what was always marginal. + +We put together a full breakdown of what actually breaks — and what to look for before deployment. Link in first comment. + +#OpticalNetworking #400G #DataCenter #NetworkEngineering #Flexoptix + +--- + +COUNT YOUR OUTPUT: After writing, count the total characters (including spaces and hashtags). If over 2,800 — cut. + +Return ONLY the LinkedIn post text. Nothing else. No explanation. No "Here is the post:". + +Article: +{{ARTICLE}}`; + +// ═══════════════════════════════════════════════════════ +// STEP 8b: REDUCTION PASS — Remove 15-25% of content // (2026-04-04: Added based on field feedback — articles were too long, // repeated concepts, and "assembled" rather than written) // ═══════════════════════════════════════════════════════ -export const STEP8b_REDUCTION = `Cut this article by 25–35%. +export const STEP8b_REDUCTION = `Cut this article by 15–25%. -This is not optional. After the previous passes, the article has grown too long and repeats itself. +This is not optional. After the previous passes, the article has grown and accumulated some padding. The goal is a tighter, more natural text — not a shorter version of the same article. +Target: the final article should be 1500–2000 words. Do not cut below 1500 words. WHAT TO REMOVE: - Any concept explained more than once (pick its best version, cut the rest) diff --git a/packages/api/src/routes/blog.ts b/packages/api/src/routes/blog.ts index f099a3e..109ee55 100644 --- a/packages/api/src/routes/blog.ts +++ b/packages/api/src/routes/blog.ts @@ -16,8 +16,8 @@ import { pool } from "../db/client"; const pipelineProgress = new Map(); function setProgress(draftId: string, step: number, label: string): void { - const pct = Math.round((step / 12) * 92) + 2; // 2%..94% during run, 100% on complete - pipelineProgress.set(draftId, { step, total: 12, label, pct }); + const pct = Math.round((step / 14) * 92) + 2; // 2%..94% during run, 100% on complete + pipelineProgress.set(draftId, { step, total: 14, label, pct }); } function clearProgress(draftId: string): void { @@ -997,6 +997,7 @@ async function runLlmPipeline( STEP2_ANGLE_SELECTION, STEP3_OUTLINE, STEP4_MASTER_DRAFT, + STEP4b_NARRATIVE_CONTROL, STEP5_REALITY_INJECTION, STEP6_TECHNICAL_DEEPENING, STEP7_OPINION_LAYER, @@ -1005,14 +1006,15 @@ async function runLlmPipeline( STEP8c_STYLE_LOCK, STEP9_QA_CHECK, STEP10_QUALITY_SCORE, + STEP_LINKEDIN_POST, BLOG_TYPES, buildFeedbackContext, withCalibration, } = await import("../llm/fo-blog-pipeline"); - const LLM_OPTS = { temperature: 0.7, maxTokens: 6144, timeoutMs: 480000 }; + const LLM_OPTS = { temperature: 0.7, maxTokens: 8192, timeoutMs: 480000 }; const LLM_REFINE = { temperature: 0.4, maxTokens: 6144, timeoutMs: 480000 }; - const TOTAL_STEPS = 12; // 10 original + 8b Reduction + 8c Style Lock + const TOTAL_STEPS = 14; // 10 original + 4b Narrative Control + 8b Reduction + 8c Style Lock + LinkedIn let stepsCompleted = 0; try { @@ -1125,18 +1127,28 @@ async function runLlmPipeline( stepsCompleted = 4; console.log(` Draft: ${step4.text.split(/\s+/).length} words`); - // ═══ STEP 5: Reality Injection ═══ - console.log(" Step 5/10: Reality Injection..."); - setProgress(draftId, 5, "Step 5/10: Reality Injection"); - const step5 = await generate(systemPrompt, - STEP5_REALITY_INJECTION.replace("{{DRAFT}}", step4.text), + // ═══ STEP 4b: Narrative Control ═══ + console.log(" Step 5/13: Narrative Control (framing check + anti-FUD)..."); + setProgress(draftId, 5, "Step 5/13: Narrative Control"); + const step4b = await generate(systemPrompt, + STEP4b_NARRATIVE_CONTROL.replace("{{ARTICLE}}", step4.text), LLM_REFINE ); stepsCompleted = 5; + console.log(` After narrative control: ${step4b.text.split(/\s+/).length} words`); + + // ═══ STEP 5: Reality Injection ═══ + console.log(" Step 6/13: Reality Injection..."); + setProgress(draftId, 6, "Step 6/13: Reality Injection"); + const step5 = await generate(systemPrompt, + STEP5_REALITY_INJECTION.replace("{{DRAFT}}", step4b.text), + LLM_REFINE + ); + stepsCompleted = 6; // ═══ STEP 6: Technical Deepening ═══ - console.log(" Step 6/10: Technical Deepening..."); - setProgress(draftId, 6, "Step 6/10: Technical Deepening"); + console.log(" Step 7/13: Technical Deepening..."); + setProgress(draftId, 7, "Step 7/13: Technical Deepening"); const step6 = await generate(systemPrompt, STEP6_TECHNICAL_DEEPENING.replace("{{ARTICLE}}", step5.text), LLM_REFINE @@ -1144,54 +1156,54 @@ async function runLlmPipeline( stepsCompleted = 6; // ═══ STEP 7: Opinion Layer ═══ - console.log(" Step 7/10: Opinion Layer..."); - setProgress(draftId, 7, "Step 7/10: Opinion Layer"); + console.log(" Step 8/13: Opinion Layer..."); + setProgress(draftId, 8, "Step 8/13: Opinion Layer"); const step7 = await generate(systemPrompt, STEP7_OPINION_LAYER.replace("{{ARTICLE}}", step6.text), LLM_REFINE ); - stepsCompleted = 7; + stepsCompleted = 8; // ═══ STEP 8: Kill AI Tone ═══ - console.log(" Step 8/10: Kill AI Tone..."); - setProgress(draftId, 8, "Step 8/10: Kill AI Tone"); + console.log(" Step 9/13: Kill AI Tone..."); + setProgress(draftId, 9, "Step 9/13: Kill AI Tone"); const step8 = await generate(systemPrompt, STEP8_KILL_AI_TONE.replace("{{ARTICLE}}", step7.text), LLM_REFINE ); - stepsCompleted = 8; + stepsCompleted = 9; // ═══ STEP 8b: Reduction Pass ═══ - console.log(" Step 9/12: Reduction Pass (remove 25-35%)..."); - setProgress(draftId, 9, "Step 9/12: Reduction Pass"); + console.log(" Step 10/13: Reduction Pass (remove 15-25%, keep ≥1500 words)..."); + setProgress(draftId, 10, "Step 10/13: Reduction Pass"); const step8b = await generate(systemPrompt, STEP8b_REDUCTION.replace("{{ARTICLE}}", step8.text), LLM_REFINE ); - stepsCompleted = 9; + stepsCompleted = 10; console.log(` After reduction: ${step8b.text.split(/\s+/).length} words (was ${step8.text.split(/\s+/).length})`); // ═══ STEP 8c: Style Lock ═══ - console.log(" Step 10/12: Style Lock (tone consistency + scope/SKU fixes)..."); - setProgress(draftId, 10, "Step 10/12: Style Lock"); + console.log(" Step 11/13: Style Lock (tone consistency + scope/SKU fixes)..."); + setProgress(draftId, 11, "Step 11/13: Style Lock"); const step8c = await generate(systemPrompt, STEP8c_STYLE_LOCK.replace("{{ARTICLE}}", step8b.text), LLM_REFINE ); - stepsCompleted = 10; + stepsCompleted = 11; // ═══ STEP 9: QA Check ═══ - console.log(" Step 11/12: QA Check..."); - setProgress(draftId, 11, "Step 11/12: QA Check"); + console.log(" Step 12/13: QA Check..."); + setProgress(draftId, 12, "Step 12/13: QA Check"); const step9 = await generate(systemPrompt, STEP9_QA_CHECK.replace("{{ARTICLE}}", step8c.text), LLM_REFINE ); - stepsCompleted = 11; + stepsCompleted = 12; // ═══ STEP 10: Quality Score ═══ - console.log(" Step 12/12: Quality Score..."); - setProgress(draftId, 12, "Step 12/12: Quality Score"); + console.log(" Step 13/13: Quality Score..."); + setProgress(draftId, 13, "Step 13/13: Quality Score"); let autoQaScore: Record | null = null; try { const step10 = await generate(systemPrompt, @@ -1207,7 +1219,32 @@ async function runLlmPipeline( } catch { console.log(" Quality scoring skipped (parse error)"); } - stepsCompleted = 12; + stepsCompleted = 13; + + // ═══ LinkedIn Post ═══ + console.log(" Step 14/14: LinkedIn Post (max 2,800 chars)..."); + setProgress(draftId, 14, "Step 14/14: LinkedIn Post"); + let linkedinPost: string | null = null; + let linkedinCharCount: number | null = null; + try { + const stepLinkedIn = await generate(systemPrompt, + STEP_LINKEDIN_POST.replace("{{ARTICLE}}", step9.text), + { temperature: 0.6, maxTokens: 1024, timeoutMs: 120000 } + ); + linkedinPost = stepLinkedIn.text.trim(); + linkedinCharCount = linkedinPost.length; + // Enforce hard limit — truncate at last sentence before 2800 if too long + if (linkedinCharCount > 2800) { + linkedinPost = linkedinPost.slice(0, 2800).replace(/[^.!?]*$/, "").trim(); + linkedinCharCount = linkedinPost.length; + console.log(` LinkedIn post truncated to ${linkedinCharCount} chars`); + } else { + console.log(` LinkedIn post: ${linkedinCharCount} chars`); + } + } catch { + console.log(" LinkedIn post generation skipped"); + } + stepsCompleted = 14; // Extract only the article from STEP9 output (QA returns review + fixed article) // Look for "COMPLETE FIXED ARTICLE" marker and take everything after it @@ -1244,26 +1281,30 @@ async function runLlmPipeline( await pool.query( `UPDATE blog_drafts SET draft_content = $1, word_count = $2, - generated_by = 'fo-blog-engine-v4', - pipeline_version = 'v4-reduction-stylelock', + generated_by = 'fo-blog-engine-v5', + pipeline_version = 'v5-narrative-control', pipeline_steps_completed = $3, auto_qa_score = $4, outline = $5, + linkedin_post = $6, + linkedin_char_count = $7, status = 'draft', updated_at = NOW() - WHERE id = $6::uuid`, + WHERE id = $8::uuid`, [ draftContent, wordCount, stepsCompleted, autoQaScore ? JSON.stringify(autoQaScore) : null, JSON.stringify({ - generation_method: "fo-pipeline-v3", + generation_method: "fo-pipeline-v5", steps_completed: stepsCompleted, blog_type: selectedTopic, quality_issues: finalIssues, feedback_entries_used: feedbackContext ? feedbackContext.split("\n").length : 0, }), + linkedinPost, + linkedinCharCount, draftId, ], ); @@ -1285,13 +1326,13 @@ async function runLlmPipeline( } clearProgress(draftId); - console.log(`Blog FO Pipeline: ${draftId} complete — ${wordCount} words, ${stepsCompleted}/10 steps, QA: ${(autoQaScore as any)?.overall || "N/A"}/10`); + console.log(`Blog FO Pipeline: ${draftId} complete — ${wordCount} words, ${stepsCompleted}/14 steps, QA: ${(autoQaScore as any)?.overall || "N/A"}/10, LinkedIn: ${linkedinCharCount ?? "n/a"} chars`); } catch (llmErr) { clearProgress(draftId); - console.warn(`Blog FO Pipeline failed at step ${stepsCompleted + 1}/10 for ${draftId}: ${(llmErr as Error).message}`); + console.warn(`Blog FO Pipeline failed at step ${stepsCompleted + 1}/14 for ${draftId}: ${(llmErr as Error).message}`); // Update with partial progress await pool.query( - `UPDATE blog_drafts SET pipeline_steps_completed = $1, pipeline_version = 'v3-flexoptix-style', + `UPDATE blog_drafts SET pipeline_steps_completed = $1, pipeline_version = 'v5-narrative-control', outline = $2, updated_at = NOW() WHERE id = $3::uuid`, [stepsCompleted, JSON.stringify({ error: (llmErr as Error).message, steps_completed: stepsCompleted }), draftId] ).catch(() => {}); diff --git a/sql/024-blog-linkedin-post.sql b/sql/024-blog-linkedin-post.sql new file mode 100644 index 0000000..3cbff16 --- /dev/null +++ b/sql/024-blog-linkedin-post.sql @@ -0,0 +1,7 @@ +-- Migration 024: Add linkedin_post column to blog_drafts +-- LinkedIn hard limit: 3,000 characters +-- Optimal visibility (before "see more"): ~210 chars preview + +ALTER TABLE blog_drafts + ADD COLUMN IF NOT EXISTS linkedin_post TEXT, + ADD COLUMN IF NOT EXISTS linkedin_char_count INTEGER;