Content Pipeline v2 — Final Implementation Document
Unified Brief: Socrates 🧠 + Daedalus 🎨 Responses Assimilated
Date: 2026-04-22
Status: Clarifications received, ready for implementation
Owner: Wadsworth 📋 (coordination), Socrates (backend), Daedalus (frontend)
Approver: Matt (Director) — 5 decisions required
Executive Summary
Content Pipeline v2 transforms blog generation from "topic → generic output" to "struggle → authentic narrative." Both agents have provided implementation feedback. This document resolves open questions and specifies final architecture.
Key change: Human-in-the-loop approval gate before GPU spend.
Architecture Decisions (Resolved)
1. Approval Surface — TELEGRAM DM
Decision: Async notification pattern
- Brief created → stored with status=pending
- Telegram DM to Matt: "New brief: [title] — Approve? [Yes] [No] [Edit]"
- On approval: trigger generation, notify user
- On rejection: return to brief author with feedback
Why: Matt's workflow is Telegram-centric; no new UI surface needed
2. State Persistence — SQLITE TABLE
Decision: SQLite table content_briefs_v2
CREATE TABLE content_briefs_v2 (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
struggle_angle TEXT NOT NULL,
origin_story TEXT NOT NULL,
attempts JSON NOT NULL, -- array of {attempt, why_failed}
the_moment TEXT NOT NULL,
the_fix TEXT NOT NULL,
reflection TEXT NOT NULL,
voice_checklist JSON NOT NULL, -- boolean map
status TEXT CHECK(status IN ('draft', 'pending', 'approved', 'rejected', 'generating', 'completed')),
style_reference TEXT,
target_length INTEGER,
created_by TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
approved_at TIMESTAMP,
approved_by TEXT,
generation_job_id TEXT,
struggle_score REAL,
content_output TEXT
);
Migration: Net new table, no v1 migration needed
3. Struggle Score — HEURISTIC V1
Decision: Weighted heuristic formula (not LLM judge)
def calculate_struggle_score(content: str) -> float:
checks = {
"i_statement_ratio": len(re.findall(r"^I\s", paragraphs)) / len(paragraphs),
"temporal_markers": len(re.findall(r"\d{1,2}:\d{2}|yesterday|last night", content)),
"struggle_patterns": len(re.findall(r"I thought.*but|I tried.*failed|didn't work", content)),
"cost_mentions": len(re.findall(r"hour|minute|sleep|wife|kid|Aundrea|frustrated", content)),
"admission_count": len(re.findall(r"I was wrong|I didn't expect|should have", content))
}
weights = {
"i_statement_ratio": 0.30,
"temporal_markers": 0.15,
"struggle_patterns": 0.25,
"cost_mentions": 0.20,
"admission_count": 0.10
}
score = sum(checks[k] * weights[k] for k in weights)
return min(score * 100, 100) # Cap at 100
Why: Fast, local, deterministic; v2 may add LLM judge overlay
4. Style Examples — MANUAL V1
Decision: Phase 1 = manual JSON; Phase 2 = automated extraction
Required from Matt: Provide shared/project-docs/blog/style-examples/dns-night.json
Structure:
{
"slug": "dns-night",
"title": "The Night I Broke DNS and My Wife Couldn't Reach Facebook",
"excerpt": "2 AM. Aundrea's phone wasn't loading Instagram...",
"structure": {
"hook": { "type": "timestamp_location_crisis", "content": "..." },
"struggle": { "attempts": 2, "escalation": "..." },
"moment": { "realization": "..." },
"fix": { "solution": "...", "caveats": "..." },
"reflection": { "lesson": "..." }
},
"voice_markers": {
"i_statements": 12,
"quotes": ["Aundrea said...", "I thought..."],
"cost_mentions": ["2 AM", "wife acceptance factor"]
}
}
Socrates task: Create template; Matt fills with "DNS Night" article
5. API Versioning — EXTEND CURRENT ENDPOINT
Decision: Option B — backward-compatible extension
@router.post("/admin/content/generate")
async def generate_content(
request: GenerateRequest,
brief_id: Optional[str] = None, # NEW: v2 uses brief_id
version: str = Query("v1", description="API version"),
):
if request.content_type == "struggle_first" or version == "v2":
return await generate_v2(request, brief_id)
return await generate_v1(request)
Why: Daedalus keeps existing integration; v2 is opt-in
Component Specifications (Final)
Backend (Socrates)
| Module | File | Purpose |
|---|---|---|
| Brief model | blog/models/brief.py |
SQLAlchemy model for content_briefs_v2 |
| Brief validation | blog/validation/brief.py |
Real-time field validation |
| Telegram notifier | blog/notifications/telegram.py |
Send approval DMs to Matt |
| Brief approval | blog/router/brief.py |
Endpoints for create/validate/approve |
| Generation v2 | blog/generation/v2.py |
Prompt + example injection |
| Struggle score | blog/metrics/struggle.py |
Heuristic calculation |
| Style loader | blog/style/loader.py |
Load JSON examples |
Frontend (Daedalus)
| Component | File | Purpose |
|---|---|---|
| Design tokens | shared/design-tokens/blog-pipeline/content-v2.json |
Colors, spacing, animations |
StruggleBriefForm |
/admin/components/StruggleBriefForm.jsx |
Multi-section incident capture |
VoiceChecklist |
/admin/components/VoiceChecklist.jsx |
Pre-generation validation |
ProgressBar |
/admin/components/ProgressBar.jsx |
Sticky completion indicator |
BriefReview |
/admin/screens/BriefReview.jsx |
Preview before approval |
ContentPreview |
/admin/screens/ContentPreview.jsx |
Side-by-side brief/output |
StruggleScore |
/admin/components/StruggleScore.jsx |
Bar chart visualization |
StyleSelector |
/admin/components/StyleSelector.jsx |
Published post reference |
User Flow (Complete)
USER (Daedalus/Matt) BACKEND (Socrates)
│ │
├─ Opens Magic Wand ──────────────────────►│
│ (new "Struggle-First" tab) │
│ │
├─ Fills StruggleBriefForm ───────────────►│
│ ├─ What broke? │
│ ├─ Who was affected? │
│ ├─ Attempt 1 + why failed │
│ ├─ Attempt 2 + why failed │
│ ├─ The moment (realization) │
│ ├─ The fix + caveats │
│ ├─ What you'd do differently │
│ └─ Style reference │
│ │
├─ Completes VoiceChecklist ──────────────►│
│ (all 6 items checked) │
│ │
├─ Clicks "Submit for Review" ────────────►├─ POST /api/v1/content/briefs
│ │ ├─ Validate fields
│ │ ├─ Save to SQLite
│ │ └─ status = "pending"
│ │
│◄─ Shows "Brief submitted" confirmation ──┤
│ │
│ ├─ Telegram DM to Matt
│ │ "New brief: [title]
│ │ Approve? [Yes] [No] [Edit]"
│ │
MATT (in Telegram DM) │
│ │
├─ Clicks [Yes] ─────────────────────────►├─ POST /api/v1/content/briefs/{id}/approve
│ │ ├─ status = "approved"
│ │ ├─ Trigger generation
│ │ └─ WebSocket/SSE: "generating"
│ │
│◄─ "Generation started" notification ─────┤
│ │
│ ├─ Load style example
│ ├─ Build v2 prompt
│ ├─ Call phi4:14b (local)
│ ├─ Calculate struggle_score
│ ├─ Save output
│ └─ status = "completed"
│ │
│◄─ Notification: "Content ready" ─────────┤
│ │
├─ Opens ContentPreview ──────────────────►├─ GET /api/v1/content/briefs/{id}/output
│ ├─ Left: Brief (highlighted sections) │
│ ├─ Right: Generated content │
│ ├─ Struggle score bar chart │
│ └─ [Edit Brief] [Accept] [Regenerate] │
│ │
├─ Clicks [Accept] ───────────────────────►├─ Create post from output
│ │ ├─ Save to database
│ │ └─ Trigger static build
│◄─ "Published to notes.hoffdesk.com" ───┤
Design Tokens v2 (Final)
{
"colors": {
"struggle": {
"high": "#22c55e",
"medium": "#eab308",
"low": "#ef4444"
},
"validation": {
"pass": "#22c55e",
"fail": "#ef4444",
"pending": "#6b7280",
"complete": "#3b82f6"
},
"section": {
"active": "#f8fafc",
"inactive": "#ffffff",
"border": "#e2e8f0"
}
},
"spacing": {
"brief-section-gap": "1.5rem",
"progress-sticky-top": "1rem",
"preview-pane-gap": "1rem"
},
"preview": {
"pane-width": "50%",
"min-width": "320px",
"max-content-width": "720px"
},
"animation": {
"check-pop": "200ms cubic-bezier(0.34, 1.56, 0.64, 1)",
"section-collapse": "300ms ease-out",
"button-pulse": "1.5s ease-in-out infinite"
}
}
API Contract (Final)
Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/v1/content/briefs |
Bearer | Create new brief |
| GET | /api/v1/content/briefs/{id} |
Bearer | Get brief by ID |
| POST | /api/v1/content/briefs/{id}/validate |
Bearer | Validate fields |
| POST | /api/v1/content/briefs/{id}/submit |
Bearer | Submit for approval |
| POST | /api/v1/content/briefs/{id}/approve |
Bearer (Matt only) | Approve and trigger |
| POST | /api/v1/content/briefs/{id}/reject |
Bearer (Matt only) | Reject with feedback |
| GET | /api/v1/content/briefs/{id}/output |
Bearer | Get generated content |
| GET | /api/v1/content/style-references |
Bearer | List published posts |
WebSocket Events (Optional Phase 2)
| Event | Direction | Payload |
|---|---|---|
brief.status_change |
Server → Client | {brief_id, old_status, new_status} |
generation.progress |
Server → Client | {brief_id, progress: 0.0-1.0} |
generation.complete |
Server → Client | {brief_id, content_preview} |
Implementation Phases
Phase 0: Foundation (Both can start now)
- [ ] Socrates: SQLite schema, model code
- [ ] Daedalus: Design tokens, responsive grid, wireframes
- [ ] Matt: Provide
dns-night.jsonstyle example
Phase 1: Backend Core (Socrates)
- [ ] Brief CRUD endpoints
- [ ] Telegram notification integration
- [ ] Approval workflow
- [ ] Prompt template v2 with manual style injection
Phase 2: Frontend Core (Daedalus)
- [ ]
StruggleBriefFormwith progress bar - [ ]
VoiceChecklistwith validation - [ ]
BriefReviewscreen - [ ] API integration
Phase 3: Integration (Both)
- [ ] End-to-end testing
- [ ] Struggle score verification
- [ ] Mobile responsive pass
Phase 4: Polish (Both)
- [ ] Side-by-side
ContentPreview - [ ]
StruggleScorevisualization - [ ]
StyleSelectorwith auto-extraction (Phase 2)
Success Metrics
| Metric | Baseline | Target | Measurement |
|---|---|---|---|
| Brief completion rate | N/A | >80% | Starts vs completions |
| Approval-to-publish | 3 days (manual) | <24h | Time from approve to publish |
| Struggle score | N/A | >75 avg | Post-generation metric |
| "Sounds like me" test | N/A | 80%+ | Matt qualitative rating |
Open Questions (For Matt — 5 Decisions)
✅ RESOLVED 2026-04-22
| Question | Decision |
|---|---|
| Timeline | MVP state achieved (flexible, no hard deadline) |
| Priority | P1 — quality improvement (not blocking) |
| v1 Retention | Yes — 30-day overlap, v1 works but not well |
| Approval delegation | Household-level — Aundrea can approve |
| Cloud fallback | Yes, opt-in — local default, cloud with explicit toggle |
File Locations
| Document | Path |
|---|---|
| This final brief | shared/project-docs/blog/content-pipeline-v2-final.md |
| Backend briefing | shared/project-docs/blog/content-pipeline-v2-backend-briefing.md |
| Backend response | shared/project-docs/blog/content-pipeline-v2-socrates-response.md |
| Frontend briefing | shared/project-docs/blog/content-pipeline-v2-frontend-briefing.md |
| Frontend response | (in Daedalus workspace) |
| Editorial policy | shared/project-docs/blog/content-ideas-v2.md |
| Style example template | shared/project-docs/blog/style-examples/dns-night.json (pending) |
Next Steps
- ✅ Matt: Decisions provided — implementation approved
- Socrates: Begin Phase 0 (schema) + Phase 1 (endpoints)
- Daedalus: Begin Phase 0 (tokens) + Phase 2 (components)
- Wadsworth: Coordinate daily standups, track blockers
Status: ✅ APPROVED — Implementation begins
Timeline: MVP when ready (no hard deadline)
Priority: P1 (quality improvement)
Risk: Low (v1 remains, backward compatible)
Document assimilated by: Wadsworth 📋
Sources: Socrates response, Daedalus response, Wadsworth synthesis