transceiver-db/sql/025-blog-sll.sql

128 lines
5.9 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- Migration 025: Blog Self-Learning Loop (SLL v1.0)
-- Tracks LinkedIn engagement per post, extracts winning/losing patterns,
-- feeds insights back into the generation pipeline automatically.
-- ─────────────────────────────────────────────────────────
-- Blog Performance: LinkedIn/platform engagement per post
-- ─────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS blog_performance (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
blog_id UUID REFERENCES blog_drafts(id) ON DELETE CASCADE,
platform TEXT DEFAULT 'linkedin',
-- Raw engagement metrics (LinkedIn: saves > shares > comments > likes)
impressions INTEGER,
comments INTEGER DEFAULT 0,
shares INTEGER DEFAULT 0,
saves INTEGER DEFAULT 0,
likes INTEGER DEFAULT 0,
-- SLL score formula: (comments×3) + (shares×2) + (saves×2), likes = 0 weight
engagement_score INTEGER GENERATED ALWAYS AS (
COALESCE(comments,0)*3 + COALESCE(shares,0)*2 + COALESCE(saves,0)*2
) STORED,
-- Snapshot of article at time of measurement (for pattern extraction)
hook_text TEXT, -- first ~120 chars of the LinkedIn post
blog_type TEXT, -- tutorial | market_alert | comparison | …
topic TEXT, -- topic tag used during generation
word_count INTEGER,
pipeline_version TEXT,
-- Timing
posted_at TIMESTAMPTZ,
measured_at TIMESTAMPTZ DEFAULT NOW(),
notes TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_blog_perf_blog ON blog_performance(blog_id);
CREATE INDEX IF NOT EXISTS idx_blog_perf_score ON blog_performance(engagement_score DESC);
CREATE INDEX IF NOT EXISTS idx_blog_perf_type ON blog_performance(blog_type);
CREATE INDEX IF NOT EXISTS idx_blog_perf_posted ON blog_performance(posted_at DESC);
-- ─────────────────────────────────────────────────────────
-- Learned Patterns: extracted winning / losing patterns
-- ─────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS blog_learned_patterns (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
pattern_type TEXT NOT NULL, -- hook | structure | topic | length | opening | verb_style
pattern_value TEXT NOT NULL, -- "short hook + contrast", "lab vs production", "single scenario"
performance_class TEXT NOT NULL, -- winner | loser
avg_engagement NUMERIC, -- average engagement_score across samples
sample_count INTEGER DEFAULT 1,
example_post_id UUID REFERENCES blog_drafts(id),
active BOOLEAN DEFAULT TRUE,
extracted_at TIMESTAMPTZ DEFAULT NOW(),
last_updated TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_blog_patterns_type ON blog_learned_patterns(pattern_type);
CREATE INDEX IF NOT EXISTS idx_blog_patterns_class ON blog_learned_patterns(performance_class);
CREATE INDEX IF NOT EXISTS idx_blog_patterns_active ON blog_learned_patterns(active) WHERE active = TRUE;
-- ─────────────────────────────────────────────────────────
-- SLL State: current active learning snapshot (weekly)
-- ─────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS blog_sll_state (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
week_start DATE NOT NULL UNIQUE,
-- Top-performing patterns (JSON arrays of strings)
winner_patterns JSONB DEFAULT '[]',
loser_patterns JSONB DEFAULT '[]',
-- Topic performance
top_topics JSONB DEFAULT '[]',
avoid_topics JSONB DEFAULT '[]',
-- Length optimization
optimal_length_min INTEGER,
optimal_length_max INTEGER,
-- Hook patterns that convert
best_hook_patterns JSONB DEFAULT '[]',
-- Meta
posts_analyzed INTEGER DEFAULT 0,
avg_engagement_score NUMERIC,
generated_at TIMESTAMPTZ DEFAULT NOW(),
generated_by TEXT DEFAULT 'sll-auto'
);
-- ─────────────────────────────────────────────────────────
-- View: performance enriched with blog metadata
-- ─────────────────────────────────────────────────────────
CREATE OR REPLACE VIEW v_blog_performance AS
SELECT
p.id,
p.blog_id,
d.title,
d.topic,
d.pipeline_version,
d.word_count,
p.platform,
p.impressions,
p.comments,
p.shares,
p.saves,
p.likes,
p.engagement_score,
p.hook_text,
p.blog_type,
p.posted_at,
p.measured_at,
-- Performance tier based on engagement score
CASE
WHEN p.engagement_score >= 20 THEN 'gold'
WHEN p.engagement_score >= 10 THEN 'silver'
WHEN p.engagement_score >= 4 THEN 'bronze'
ELSE 'miss'
END AS performance_tier
FROM blog_performance p
JOIN blog_drafts d ON d.id = p.blog_id;
COMMENT ON TABLE blog_performance IS 'SLL v1.0: LinkedIn engagement tracking per blog post';
COMMENT ON TABLE blog_learned_patterns IS 'SLL v1.0: Extracted winning/losing content patterns';
COMMENT ON TABLE blog_sll_state IS 'SLL v1.0: Weekly learning state snapshot for pipeline injection';