# Social Branding Service Audit Date: 2026-03-23 ## Purpose This document compares: 1. the product BRD / PRD in [`docs/PRD.md`](./PRD.md) 2. the narrowed execution plan in [`docs/plan.md`](./plan.md) 3. the current implementation in `app/` and `tests/` It is intended as a handoff and presentation document so another engineer or stakeholder can answer: - what was planned - what was actually built - which decisions were taken to narrow scope - which assumptions phase one was built on - what is still missing versus the plan - what is still missing versus the PRD - what architecture should be used going forward Auth is intentionally excluded from blocker status for this review. Future auth is assumed to be Clerk-issued JWT at the service boundary. ## Executive Summary The current repo is best understood as a narrowed, LinkedIn-first individual-user MVP that is substantially aligned with the overnight plan, but not aligned with the full PRD. The implementation does support the plan's core demo loop: 1. import a LinkedIn profile snapshot 2. compute a LinkedIn audit and Brand Score v1 3. infer or select a persona strategy 4. generate a 2-week content calendar with an 80/20 split 5. generate LinkedIn drafts 6. move drafts through approval, rejection, regeneration, and manual publish confirmation 7. expose dashboard and local event history That flow is implemented and tested. `uv run pytest` passed with `4 passed, 1 warning` on 2026-03-23. However, the repo is not PRD-complete. The PRD describes a broader multi-platform, more automated system with richer scoring, profile rewrites, live collection, scheduled posting, analytics, engagement automation, and enterprise workflows. Those are not implemented here. The biggest architectural mismatch with the desired next version is this: - the code already has some engine-like pure logic - but the engine is not yet separated as an explicit domain package or service boundary - orchestration, persistence, and business logic are still mixed inside one application service - surfaces are currently HTTP-only in practice - Redis and the worker exist as scaffolding, not as an active runtime path ## Canonical Scope Hierarchy The repo currently has three levels of scope, and they should not be confused during presentation. ### 1. PRD scope The PRD is the broad product target: - individual plus enterprise - LinkedIn, Instagram, X, YouTube - automated connect -> collect -> analyze -> generate -> approve -> post -> track loop - Brand Score feeding RQx - richer content and analytics behavior Relevant references: - `docs/PRD.md:21-36` - `docs/PRD.md:42-70` - `docs/PRD.md:80-102` - `docs/PRD.md:183-248` - `docs/PRD.md:360-397` ### 2. Plan scope The plan is the locked overnight MVP and is the real governing doc for what this repo was trying to ship immediately: - individual only - LinkedIn only - manual import fallback instead of hard OAuth - Brand Score v1 from available LinkedIn data - content calendar, drafts, approval queue, manual publish confirmation - dashboard and local events - enterprise and multi-platform behavior deferred Relevant references: - `docs/plan.md:9-38` - `docs/plan.md:78-128` - `docs/plan.md:149-158` ### 3. Current implementation scope The code matches the narrowed plan much more closely than it matches the PRD. It is therefore more accurate to describe the current service as: "A request-driven, LinkedIn-first social branding MVP for individual users with manual ingestion, simplified scoring, deterministic content generation, approval workflow, dashboard summaries, and local event persistence." That description is also how the README frames it: - `README.md:3-23` - `README.md:81-85` ## Current Architecture Assessment ## What exists today The current architecture is: - HTTP API surface in `app/api/v1/` - one application service in `app/services/social_branding.py` - several pure or mostly pure helper modules in: - `app/services/brand_score.py` - `app/services/persona_strategy.py` - `app/services/content_calendar.py` - persistence in SQLAlchemy models in `app/models.py` - queue abstraction in `app/services/queue.py` - a worker loop in `app/worker/main.py` This is a valid MVP architecture, but it is not yet the target split you described. ## Desired target architecture For the next version, the clean split should be: ### 1. Social branding engine A pure, platform-agnostic decision engine that takes normalized inputs and produces outputs such as: - audits - scores - persona strategy decisions - calendar plans - draft suggestions - recommendation bundles This layer should not know about: - FastAPI - SQLAlchemy sessions - Clerk JWTs - Redis - Postgres - HTTP request shapes ### 2. Social branding service layer An orchestration layer that: - receives authenticated user context - loads and stores data - calls the engine - persists outputs - emits events - coordinates approval and posting workflows - exposes use cases to surfaces ### 3. Surfaces Adapters on top of the service layer, for example: - HTTP API - background worker consumers - Redis/event consumers - internal command handlers - future admin or operator surfaces ### 4. External adapters Per-platform collectors and posters: - LinkedIn import adapter - Instagram connector - X connector - YouTube connector - GitHub connector (signals + content source) - manual upload/import adapter See also: `docs/external-adapters-plan.md` for the platform-compliance contract and phased implementation plan. ### 5. Infrastructure - Postgres - Redis - storage - auth verification - observability ## How close the current code is to that shape Partially aligned: - `app/services/brand_score.py` already looks like engine logic. - `app/services/persona_strategy.py` already looks like engine logic. - `app/services/content_calendar.py` already looks like engine logic. Not yet aligned: - `app/services/social_branding.py` combines orchestration, persistence, workflow rules, and business decisions in one class. - there is no explicit normalized engine contract - there is no repository layer - there are no platform adapter interfaces - surfaces are API routes only in practical use Conclusion: The repo contains the beginnings of an engine, but the engine is still embedded inside the service implementation rather than clearly separated. ## What Was Implemented ## Data model and persistence The main persistence model is present and broadly follows the plan: - `social_accounts` - `social_profile_snapshots` - `brand_scores` - `persona_strategies` - `content_calendar_items` - `content_drafts` - `approval_queue` - `posting_records` - `analytics_snapshots` - `engagement_drafts` - `integration_events` Reference: - `app/models.py:58-235` Observations: - The model names mostly match the plan's domain model. - `posting_records` and `integration_events` were added beyond the explicit plan list and are useful. - `engagement_drafts` exists in schema only and is not actively used in the service flow. - Everything is stored through `create_all()` on startup; there is no migration system yet. ## API surface The HTTP routes listed in the README and plan are implemented: - LinkedIn import - persona select - brand score recalculate - content calendar generate/list - drafts generate - approval approve/reject/regenerate - publish confirm - dashboard - events - demo seed - health References: - `README.md:25-39` - `app/api/v1/__init__.py` - `app/api/v1/accounts.py` - `app/api/v1/brand_score.py` - `app/api/v1/content.py` - `app/api/v1/dashboard.py` - `app/api/v1/events.py` - `app/api/v1/demo.py` - `app/api/v1/health.py` - `app/api/v1/persona.py` ## LinkedIn ingest and audit Implemented: - manual LinkedIn import payload - account upsert - profile snapshot persistence - section-level audit scoring - recommendation list persistence References: - `app/services/social_branding.py:190-239` - `app/services/brand_score.py:37-88` What it actually does: - scores headline, about, experience, skills, recommendations, featured, and activity - persists overall score and recommendations - uses simple heuristic thresholds, not benchmarked analysis ## Brand Score v1 Implemented: - Brand Score computation on import - manual recalculation endpoint - score history persistence - event emission when score changes materially or when forced References: - `app/services/social_branding.py:134-188` - `app/services/social_branding.py:241-274` - `app/services/brand_score.py:91-142` What it actually does: - computes five components: - profile completeness - content consistency - engagement quality - visibility - audience growth - uses LinkedIn metrics only - stores local integration events in the service database ## Persona strategy Implemented: - six persona enums - heuristic persona inference from pathway context - manual persona override - cadence, tone, target role, and source context persistence References: - `app/models.py:35-41` - `app/services/persona_strategy.py:8-127` - `app/services/social_branding.py:67-96` What it actually does: - infers persona from role and goals - always assigns LinkedIn as the active platform - stores one active strategy at a time per user ## Content calendar and drafts Implemented: - 2-week calendar generation - 80/20 toolkit versus dynamic split - deterministic topic sequencing - LinkedIn draft generation References: - `app/services/social_branding.py:276-322` - `app/services/content_calendar.py:26-84` - `app/services/social_branding.py:360-459` - `app/services/content_calendar.py:97-144` What it actually does: - uses persona cadence to create slots over 14 days - marks some slots as dynamic using deterministic spacing - chooses from a fixed toolkit topic list per persona - generates simple template-based LinkedIn drafts ## Approval and publish flow Implemented: - queue item creation per draft - approve - reject - regenerate - posting state transitions - manual publish confirmation - local approval event emission References: - `app/services/social_branding.py:333-359` - `app/services/social_branding.py:360-418` - `app/services/social_branding.py:486-620` What it actually does: - every draft becomes an approval item and posting record - approving immediately transitions the posting record to `ready_for_manual_publish` - publish is confirmed manually by user-supplied external reference ## Dashboard and events Implemented: - latest brand score and recent trend - queue summary - posting state summary - latest analytics snapshot view - local event listing References: - `app/services/social_branding.py:622-705` - `app/services/social_branding.py:707-722` ## Worker and queue Implemented in scaffold form: - queue backend abstraction - database no-op backend - Redis backend - worker loop References: - `app/services/queue.py:1-83` - `app/worker/main.py:14-36` - `README.md:83-85` Important reality: - the current product flow is request-driven - the worker does not execute domain jobs - the Redis queue is infrastructure-ready, not feature-ready ## Test coverage Implemented: - health check - happy path full MVP flow - limited-data import path - regenerate path state behavior Reference: - `tests/test_social_branding_flow.py:32-207` Observed verification: - command run: `uv run pytest` - result: `4 passed, 1 warning` ## Phase-One Foundations: Decisions and Assumptions The user asked specifically for the assumptions and decisions behind the current phase-one implementation. Those are below. ## Decision 1: Narrow to individual plus LinkedIn only Why: - the PRD is much broader than can be implemented quickly - LinkedIn is the primary value surface in the PRD and plan - multi-platform plus enterprise would have blocked delivery Where it shows up: - `docs/plan.md:11-30` - `README.md:10-23` - `app/services/persona_strategy.py:124` - `app/services/social_branding.py:201-206` - `app/services/social_branding.py:313` Assumption: - proving one strong individual LinkedIn flow is more valuable than partial support across four platforms ## Decision 2: Use manual import instead of OAuth-first collection Why: - the PRD explicitly leaves data collection method to engineering - LinkedIn collection is the riskiest integration area - the overnight plan avoids blocking on OAuth and unstable APIs Where it shows up: - `docs/PRD.md:36` - `docs/PRD.md:240-248` - `docs/plan.md:13` - `docs/plan.md:35` - `app/schemas/social_branding.py:47-52` - `app/services/social_branding.py:190-239` Assumption: - GrowQR can seed or receive normalized LinkedIn data from a manual process, future adapter, or another service ## Decision 3: Treat Brand Score as a simplified v1 Why: - the PRD's full seven-component score depends on data that does not exist in the narrowed MVP - the plan explicitly says not to block on unavailable cross-platform metrics Where it shows up: - `docs/plan.md:36` - `app/services/brand_score.py:18-24` - `app/services/brand_score.py:91-142` Assumption: - a meaningful but incomplete score is acceptable for v1 if it is stable, stored, and trendable ## Decision 4: Use deterministic content generation instead of real AI orchestration Why: - the plan prioritizes demonstrating flow and content structure over model sophistication - deterministic output is easy to test and explain Where it shows up: - `docs/plan.md:38` - `app/services/content_calendar.py:26-84` - `app/services/content_calendar.py:97-144` Assumption: - content templates and placeholders are enough for MVP validation ## Decision 5: Make publish manual-confirmed Why: - the plan explicitly allows manual publish confirmation - live posting would introduce platform API risk and auth complexity Where it shows up: - `docs/plan.md:37` - `app/services/social_branding.py:507-508` - `app/services/social_branding.py:591-620` Assumption: - for v1, it is enough to generate approved content and let the user confirm it was posted manually ## Decision 6: Keep async infrastructure scaffolded but not operationally central Why: - Postgres and Redis are useful for future scale and service consistency - the immediate flow can be run synchronously Where it shows up: - `README.md:75-85` - `.env.example` - `.env.docker` - `app/services/queue.py:1-83` - `app/worker/main.py:14-36` Assumption: - request-driven synchronous use cases are acceptable for phase one, with queue infrastructure reserved for later work ## Decision 7: Trust caller-provided `user_id` Why: - auth is intentionally out of scope Where it shows up: - almost every request schema accepts `user_id` directly, for example `app/schemas/social_branding.py:47-52`, `87-91`, `111-115`, `153-157` Assumption: - an upstream system or future JWT layer will replace body-supplied identity ## Implementation Status Versus Plan The plan is mostly implemented, but some items are implemented in a thin MVP form rather than a durable production form. ## Phase 1: Foundations Plan status: implemented Evidence: - social branding models exist in `app/models.py:58-235` - persona enums exist in `app/models.py:35-41` - brand score component model exists in `app/models.py:89-99` - persona topic libraries exist in `app/services/persona_strategy.py:26-63` Assessment: - This phase is done for MVP purposes. - The main gap is architectural: these foundations are not yet separated into engine/service/adapters. ## Phase 2: Profile ingest and audit Plan status: implemented, simplified Evidence: - import endpoint exists - manual payload fallback is the main supported path - audit scoring and recommendations are persisted - limited-data import test exists Assessment: - Done for MVP. - Not equivalent to PRD-grade platform analysis. ## Phase 3: Brand Score and persona strategy Plan status: implemented, simplified Evidence: - brand score is computed and stored - persona inference and manual selection work - score update events are persisted Assessment: - Done for MVP. - The score model is intentionally reduced from the PRD. - Events are local persistence records, not cross-service integration delivery. ## Phase 4: Content calendar and drafts Plan status: implemented, partially shallow Evidence: - 2-week calendar exists - 80/20 split exists - LinkedIn draft generation exists Assessment: - Done for demo and workflow validation. - Dynamic triggers are represented as preselected topic types, not event-driven runtime behavior. - Drafts are deterministic templates, not model-generated or personalized beyond a few fields. ## Phase 5: Approval and publish flow Plan status: implemented Evidence: - queue, approve, reject, regenerate, publish confirmation, and state transitions all exist Assessment: - Strongly implemented for the narrowed MVP. - Still single-user only. - No scheduling engine or platform posting adapter exists. ## Phase 6: Dashboard and hardening Plan status: mostly implemented Evidence: - dashboard endpoint exists - happy-path tests exist - deferred scope is documented Assessment: - Done for MVP handoff and demo. - Not hardening-complete for production. - Missing migrations, background jobs, external integrations, auth, observability, and deeper test coverage. ## Implementation Status Versus PRD This is the most important presentation distinction: - plan alignment: mostly yes - PRD alignment: no, not yet ## PRD items that are fully or mostly covered ### Individual personas exist - six personas exist in the model - persona selection and switching are present Caveat: - active platform mix is not implemented beyond LinkedIn ### LinkedIn-first audit and content flow exists - LinkedIn import - audit - Brand Score v1 - calendar - approval queue - manual publish confirmation ### Brand Score history and dashboard exposure exist - score history is persisted - dashboard returns recent trend ## PRD items that are only partially covered ### Brand Score formula PRD expectation: - seven components - RQx formula - downstream event to Assessment and Pathways services Current implementation: - five components only - no RQx mapping - local event persistence only References: - `docs/PRD.md:82-102` - `app/services/brand_score.py:18-24` - `app/services/social_branding.py:174-188` ### Content model PRD expectation: - rich toolkit catalog and multiple dynamic triggers - platform-aware formatting and platform mix rules Current implementation: - reduced toolkit topics - three dynamic topic types - LinkedIn-only output References: - `docs/PRD.md:141-181` - `app/services/persona_strategy.py:26-63` - `app/services/content_calendar.py:38-43` ### Analytics PRD expectation: - platform analytics per sync cycle - richer performance tracking and trend lines Current implementation: - only snapshot summaries based on imported metrics - no real recurring sync - no per-content performance table References: - `docs/PRD.md:243-248` - `docs/PRD.md:371-381` - `app/models.py:198-211` - `app/services/social_branding.py:656-696` ## PRD items that are not implemented ### Multi-platform runtime support Not implemented: - Instagram runtime collection/generation/posting - X runtime collection/generation/posting - YouTube analysis/brief generation/posting pipeline Even though platform enums exist, runtime behavior does not. References: - `docs/PRD.md:21-24` - `docs/PRD.md:198-235` - `app/models.py:28-32` ### OAuth connect and recurring sync Not implemented: - platform OAuth flow - scheduled sync jobs - recurring collection cadence - delta-based collector jobs References: - `docs/PRD.md:63-70` - `docs/PRD.md:243-248` - `app/worker/main.py:14-36` ### Profile rewrites and application to platform Not implemented: - rewrite generation for headline/about/experience - approval flow for profile updates - apply-to-platform action References: - `docs/PRD.md:190-193` - no corresponding route or service use case exists ### Recruiter visibility and keyword gap analysis Not implemented beyond basic visibility scoring inputs. Missing: - target-role benchmark - keyword gap detection - profile rewrite recommendations based on gaps ### Auto-posting and scheduling Not implemented. Current system only records manual publish confirmation. References: - `docs/PRD.md:68-70` - `docs/PRD.md:179` - `docs/PRD.md:369-381` - `app/services/social_branding.py:591-620` ### Auto-approve mode Not implemented. The PRD explicitly calls for opt-in auto-approve. References: - `docs/PRD.md:179` - `docs/PRD.md:376` ### Sensitive-topic filter and tone lock Not implemented. Missing: - sensitive-topic classification before queueing - tone locking after initial brand establishment - policy-based regeneration or suppression References: - `docs/PRD.md:176-181` ### Engagement automation Not implemented. Notes: - the table `engagement_drafts` exists - no service flow populates it - no routes expose it References: - `docs/PRD.md:56` - `docs/PRD.md:168` - `docs/PRD.md:194-195` - `app/models.py:214-225` ### Enterprise social branding Not implemented. That entire scope is still absent. References: - `docs/PRD.md:250-397` - `docs/plan.md:25` ## Key Product and Technical Assumptions Embedded in the Current Repo These assumptions should be stated explicitly in any presentation so the current implementation is not oversold. ### Assumption: one service owns both business workflow and persistence The code assumes one deployable service can hold: - workflow logic - persistence rules - local event persistence - API-facing use cases This is acceptable for MVP, but it is why the engine is not yet truly reusable. ### Assumption: normalized external data will eventually arrive The import request shape assumes someone else can provide: - profile sections - aggregate metrics - pathway context without this service solving collection first. ### Assumption: demo readiness was more important than platform completeness This is visible in: - deterministic content generation - manual publish confirmation - local event storage - request-driven flow ### Assumption: Postgres and Redis are deployment options, not proof of async completeness The repo can run on Postgres and Redis in Docker, but that should not be presented as meaning: - scheduled jobs are implemented - queue processing is meaningful - Redis is part of the live product loop today ### Assumption: auth will be added upstream later Today the service trusts request bodies for `user_id`. For Clerk JWT integration, the service should move to: - derive subject from JWT - remove `user_id` from externally writable bodies where possible - enforce per-user access at the service layer ## What Is Missing to Finish the Plan Properly Strictly speaking, the plan's demo definition is met. But if "finish the plan properly" means "make the narrowed MVP robust and presentation-safe for handoff", the remaining work is: ### 1. Separate the engine from the service layer Required: - create an explicit engine package - move pure decision logic out of application orchestration - define normalized engine input and output contracts Why: - this is the biggest gap against the intended architecture - it makes later multi-surface work cleaner ### 2. Add repository and adapter boundaries Required: - persistence repositories - platform import adapters - event publisher abstraction - posting adapter abstraction Why: - right now platform, persistence, and workflow assumptions are tightly coupled ### 3. Replace startup `create_all()` with migrations Required: - Alembic or equivalent Why: - handoff and deployment reliability - production schema evolution ### 4. Turn queue and worker into real use-case execution paths Required: - actual queued job types - background handlers - idempotency rules - retry behavior Why: - the current worker is only scaffolded ### 5. Add stronger tests Required: - service-layer unit tests for engine outputs - route-level error case tests - Postgres integration tests - Redis queue tests if worker becomes real Why: - current tests prove happy-path MVP behavior only ## What Is Missing to Reach the PRD This is the longer roadmap beyond the narrowed MVP. ## Individual PRD completion Required work: - live or semi-live collection strategy per platform - recurring sync engine - full seven-component Brand Score - RQx mapping and real cross-service event delivery - profile rewrite generation and apply flow - richer toolkit library - real dynamic trigger ingestion - topic safety filtering - tone lock - optional auto-approve mode - scheduled posting adapters - platform-specific formatting - engagement queue generation - richer analytics model and trend views - platform activation recommendations per persona/archetype ## Enterprise PRD completion Required work: - enterprise account domain - company brand voice settings - governance rules and flags - multi-role approvals - enterprise content types - enterprise analytics - enterprise content calendar visibility and role actions ## Recommended Next Architecture and Work Packages If the goal is to build toward the plan and finally the PRD while matching the desired split, the implementation sequence should be: ## Work Package 1: Explicit engine extraction Create a package such as `app/engine/` or `app/domain/engine/` with: - audit engine - score engine - persona engine - calendar engine - draft engine Inputs should be normalized Python models, not SQLAlchemy rows. ## Work Package 2: Application service boundary Refactor `SocialBrandingService` into use cases such as: - import_profile - select_persona - recalculate_brand_score - generate_calendar - generate_drafts - approve_content - reject_content - regenerate_draft - confirm_publish - get_dashboard Each use case should orchestrate repositories and engine calls. ## Work Package 3: Adapter layer Add adapter interfaces for: - profile collectors - posting executors - event publishers - analytics ingestors Start with: - manual import adapter - LinkedIn placeholder adapter ## Work Package 4: Surface layer cleanup Keep surfaces thin: - HTTP API maps requests to use cases - worker consumes jobs and invokes use cases - Redis/event subscribers invoke the same use cases ## Work Package 5: Auth integration When Clerk JWT is added: - resolve user identity from token - remove body-driven identity where possible - add service-layer authorization guards ## Work Package 6: Platform expansion After the architecture split is in place: - add Instagram adapter - add X adapter - add YouTube adapter Do not add those directly into the current `SocialBrandingService` shape, or the service will become harder to evolve. ## Final Readout ## What is done The narrowed LinkedIn-first MVP is real and working: - ingest - audit - Brand Score v1 - persona strategy - 2-week calendar - LinkedIn drafts - approval queue - manual publish confirmation - dashboard - local events - demo seed - tests passing ## What decisions were taken The implementation intentionally chose: - LinkedIn-only runtime - manual import over OAuth-first - simplified Brand Score - deterministic templates - manual publish confirmation - request-driven flow - local event persistence - deferred auth ## What assumptions phase one was built on Phase one assumes: - upstream or manual data can be provided - one-user flows are enough for validation - live posting is not required - local persistence is enough to prove the workflow - Redis/Postgres readiness is useful even without full async execution - the engine can be extracted later from the current helper modules and service logic ## What is left versus the plan Very little is left for the narrowed MVP feature set itself. The main unfinished work is structural and production-grade: - engine extraction - repository/adaptor boundaries - migrations - real worker jobs - broader tests ## What is left versus the PRD A large amount remains: - multi-platform support - real collection and sync - richer scoring and RQx integration - profile rewrites - scheduled posting - engagement automation - richer analytics - safety and policy logic - enterprise workflows ## Bottom line This repo should be presented as: "Plan-complete for a narrowed LinkedIn-first MVP, but not PRD-complete. The next major step is not adding random features inside the current service. It is separating the engine from the service and surfaces so the PRD can be implemented cleanly."