Files
docs/social_branding/plan-brand-score-component-model.md
-Puter e6685203fe feat: initial docs repo with project inventory and all documentation
- Added REPO_INVENTORY.md with all repos, branches, remotes, and staging info
- Added .gitignore
- Synced all existing docs from local workspace
- Centralized documentation hub for GrowQR team
2026-06-22 15:04:27 +05:30

15 KiB
Raw Permalink Blame History

Implementation plan — Brand Score: full PRD component model + breakdown

Date: 2026-03-27
Scope: PRD additions item (1) (from prd-additions.md)
Non-goals (explicit PRD exclusions for this phase): posting, executing actions on platforms, adding new platforms beyond LinkedIn-first.


Checklist (requirements coverage)

  • Add remaining Brand Score components as first-class computed fields (not just a single audit-derived score):
    • Content Consistency (20%)
    • Audience Growth (15%)
    • Engagement Rate (15%)
    • Recruiter Visibility (15%)
    • Content Quality (10%)
    • Cross-Platform Consistency (5%) (schema-ready; compute from available platforms only)
  • Store/persist component breakdown and PRD-required “objective, measurable signals” explanations
  • Implement Brand Score trend line (history storage + API response)
  • Implement v1 proxies + renormalization for LinkedIn-private analytics gaps
  • Add metrics snapshot persistence (followers/connections over time) so Audience Growth + trendline are computable

1) Outcome + contract

Inputs

Per user_id and a computation window (rolling), compute with:

  • LinkedIn profile snapshot: headline/about/experience/skills (as currently ingested)
  • Post/activity list over a window (suggest 30 days): timestamps, text, and any available post metrics
  • Metrics snapshots over time (if present/collectible): followers/connections, impressions, engagements, profile views, search appearances
  • Persona context (if present): target persona/role signals used in content quality and recruiter visibility proxy

v1 data reality (current repo ingestion):

  • Profile snapshot fields + followers_count/connections_count are present.
  • activity exists but is typically limited (date/title/url + sometimes likes/comments).
  • Impressions/reach, profile views, and search appearances are typically not available in LinkedIn-first v1; treat as proxy/N.A.

Outputs

  • brand_score_total (0100)
  • brand_score_components[] each with:
    • key (stable enum)
    • weight (PRD)
    • score (0100)
    • weighted_points (score × weight)
    • signals (structured numeric strings/values for auditability)
    • explanation (human-readable, objective/measurable)
    • insufficient_data (boolean)
  • brand_score_history[] trendline points (date, total score)

Error + missing data behavior

Pick one explicit policy so scores dont jitter unpredictably:

  • Recommended v1: weight renormalization.
    • If component lacks minimum data, mark insufficient_data=true.
    • Exclude it from total weighting and renormalize remaining weights to sum to 1.0.
    • Still return the missing component in breakdown (so UI can display “N/A”).

v1 proxy rule: if a PRD metric is not collectible on LinkedIn (e.g., impressions, profile views, search appearances), compute a deterministic proxy where possible; otherwise mark the component insufficient_data=true and rely on renormalization.

Versioning

Persist a scoring version string (e.g. 2026-03-27.v1) with each snapshot to support future formula tweaks.


2) Component model (PRD weights + measurable signals)

Scores are 0100. Each component must produce structured signals and a plain-language explanation referencing those signals.

2.1 Content Consistency — 20%

Goal: consistency of posting frequency + tone consistency over a rolling window.

Signals (v1):

  • posts_last_30d
  • days_between_posts_avg
  • days_between_posts_stddev
  • weeks_meeting_target_cadence (e.g., >=2 posts/week or configured target)
  • tone_label_distribution (optional v1) or tone_consistency_score

v1 note: cadence is computable from activity[].date if present.

Important v1 filter: Bright Data “activity” often includes interactions (e.g., “Liked by X”), not just authored posts. Use activity[].type (populated from Bright Data interaction when available) to exclude non-authored items from cadence calculations.

Tone is optional and should be insufficient_data=true unless post text is available.

Tone consistency (addendum — v1.1 implementation checklist)

We currently score cadence deterministically from activity dates; this addendum covers adding the optional “tone consistency” portion without destabilizing totals.

  • Extend ingestion/normalization so activity items include authored post text when available

    • Prefer a stable field name in the normalized profile dict (e.g. activity[].text).
    • Ensure non-authored items are filtered (continue using activity[].type/interaction heuristics).
  • Add tone classification (deterministic-first, AI optional)

    • Add adapter method (or reuse existing AI adapter) to classify tone labels for a post: e.g. {tone: "educational"|"opinion"|"personal"|"promo"|...}
    • Cache tone labels per post URL/id when possible to avoid re-scoring.
  • Compute tone consistency signals

    • tone_label_distribution (counts per label)
    • tone_entropy (or similar) as the numeric signal
    • tone_consistency_score mapped 0100
    • If fewer than N posts have text (suggest N=3), mark tone as insufficient_data=true.
  • Merge cadence + tone into the existing content_consistency score

    • Recommended split: cadence 70%, tone 30% (configurable)
    • Keep overall component deterministic when AI is unavailable:
      • If AI tone classification fails, fall back to insufficient_data=true for tone and score cadence only.
  • Update explanation string to include tone when scored

    • Example: “Cadence variance is X days; tone consistency is Y/100 across N posts.”
  • Add tests

    • Test cadence-only path remains unchanged.
    • Test tone-scored path produces stable distribution + score.

Computation idea (v1 deterministic + optional AI):

  • Frequency score derived from cadence stability:
    • reward meeting minimum weekly target
    • penalize high variance in gaps
  • Tone consistency (optional): classify each post tone (adapter AI), compute entropy / variance; map to 0100.

Explanation template:

  • “You posted {posts_last_30d} times in 30 days. Cadence variance is {stddev} days; {weeks_meeting_target_cadence}/4 weeks meet target.”

2.2 Audience Growth — 15%

Goal: follower/connection growth rate over last 30 days.

Signals:

  • followers_start_30d
  • followers_end_30d
  • follower_growth_abs
  • follower_growth_rate

v1 requirement: this component needs historical follower counts. Implement via metrics snapshots (see §3.3).

Computation (v1):

  • growth_rate = (end - start) / max(start, 1)
  • Map to 0100 using bands (configurable), e.g.:
    • <=0% → 10
    • 01% → 40
    • 13% → 70
    • 36% → 85
    • 6% → 95

Explanation:

  • “Followers changed from {start} → {end} (+{abs}, {rate:.1%}) in 30 days; target band is {target_band}.”

2.3 Engagement Rate — 15%

Goal: likes/comments/shares/saves as % of reach.

Signals:

  • posts_counted
  • impressions_total (or reach_total if you have it)
  • engagements_total
  • engagement_rate_weighted_avg
  • engagement_rate_p50 / p75 (optional)

Computation (v1):

  • Prefer weighted average: sum(engagements)/sum(impressions) across posts with impressions.
  • Guardrails for low denom.

v1 LinkedIn-first proxy: if impressions/reach arent available, use a deterministic proxy:

  • engagements_total = sum(likes + comments) across recent activity items with those fields
  • engagements_per_post_avg = engagements_total / max(posts_counted, 1)
  • Optional normalization: engagements_per_post_per_1k_followers If engagement signals are missing/too sparse, mark insufficient_data=true and renormalize.

Important v1 filter: only count authored posts (exclude “liked/commented/shared by user” items based on activity[].type).

Explanation:

  • “Avg engagement rate is {rate:.2%} across {posts_counted} posts; benchmark target band {target_band}.”

2.4 Recruiter Visibility — 15%

Goal: profile appearance rate in recruiter/talent searches.

Signals (best effort LinkedIn-first):

  • Preferred if collectible:
    • search_appearances_7d, search_appearances_30d
    • profile_views_7d, profile_views_30d
  • Proxy if not collectible:
    • keyword_coverage_score (0100)
    • missing_keywords_count

Computation (v1):

  • If search appearances/profile views available: score by percentile bands.
  • Else: compute proxy keyword coverage vs target role/persona keyword set.

v1 default: treat search appearances/profile views as unavailable and use keyword coverage proxy.

Explanation:

  • “Search appearances (7d): {x}. Keyword coverage {coverage}/100; missing {n} high-signal recruiter keywords.”

2.5 Content Quality — 10%

Goal: AI-assessed relevance/originality/alignment to persona framework.

Signals:

  • ai_quality_score_avg
  • ai_relevance_avg (optional detail)
  • ai_originality_avg (optional detail)
  • ai_persona_alignment_avg (optional detail)
  • low_quality_flags (e.g., repetitive template, too short)

Computation (v1):

  • Use AI adapter to score posts (sample last N posts).
  • Average to 0100; apply small penalties for repeated/low-effort patterns.

v1 fallback: if post text isnt collectible, score using profile text (headline + summary + recent roles) and reflect that in signals.

Explanation:

  • “Quality score {score}/100 across {n} posts: relevance {r}/100, originality {o}/100, persona alignment {p}/100.”

2.6 Cross-Platform Consistency — 5% (schema-ready)

Goal: messaging/voice alignment across connected platforms.

Signals:

  • connected_platforms
  • platforms_scored
  • cross_platform_similarity (if >1 platform)

Computation (v1):

  • If only LinkedIn connected: mark insufficient_data=true and exclude via renormalization.
  • If multiple: compare embedding similarity of bios/headlines + recent posts themes.

Explanation:

  • “Only LinkedIn connected; cross-platform consistency isnt scored yet.”

3) Persistence + history (trend line)

3.1 Storage model

Add a snapshot table to store history and component breakdown for auditability.

Recommended table: brand_score_snapshots

  • id
  • user_id
  • platform (string/enum)
  • window_start, window_end
  • score_total (numeric)
  • components_json (JSON/JSONB)
  • scoring_version (string)
  • created_at

Also store a “latest” summary on the user profile (fast reads):

  • latest_brand_score_total
  • latest_brand_score_components_json
  • latest_brand_score_calculated_at

3.2 Trendline

  • Provide daily points (last N days).
  • If multiple snapshots/day: use latest per day (simple, stable).

3.3 Metrics snapshots (Audience Growth input)

Add a small metrics history table so we can compute change-over-time deterministically.

Recommended table: social_metrics_snapshots

  • id
  • user_id
  • platform (string/enum; v1 = linkedin)
  • followers_count (int nullable)
  • connections_count (int nullable)
  • captured_at (timestamp)

Capture policy (v1):

  • Insert a snapshot after each successful LinkedIn sync.
  • Optional dedupe: keep at most 1 snapshot/day per user+platform.

4) Engine design (code structure)

4.1 Orchestrator

In app/engine/brand_score_engine.py (or equivalent), implement:

  • compute_brand_score(user_id, platform, window_days=30) -> BrandScoreResult
  • compute_and_persist_brand_score(...) -> BrandScoreSnapshot

4.2 Component functions

Create pure functions (easy to test):

  • score_content_consistency(data) -> ComponentResult
  • score_audience_growth(data) -> ComponentResult
  • score_engagement_rate(data) -> ComponentResult
  • score_recruiter_visibility(data) -> ComponentResult
  • score_content_quality(data) -> ComponentResult
  • score_cross_platform_consistency(data) -> ComponentResult

4.3 Config + thresholds

Centralize mapping bands and targets so theyre easy to tune:

  • engagement target bands
  • growth target bands
  • cadence target
  • minimum sample sizes (e.g., >=3 posts for some components)

5) API changes

5.1 Endpoints (or dashboard response augmentation)

Add (or extend existing dashboard routes):

  1. GET /brand-score/latest
  • Returns: total + components breakdown + scoring_version + window.
  1. GET /brand-score/trend?days=30
  • Returns: array of {date, score_total} (optionally include component totals later).

5.2 Response schemas

Add Pydantic models:

  • BrandScoreComponentBreakdown
  • BrandScoreSnapshotResponse
  • BrandScoreTrendResponse

6) Triggering computations

Compute snapshot when inputs change.

Recommended triggers:

  • After each LinkedIn sync job completes
  • Optionally: nightly job (backup) to ensure fresh trendline

Implementation:

  • Hook into job dispatcher / sync pipeline to call compute_and_persist_brand_score(user_id, "linkedin").

v1 addition: after LinkedIn sync, also write a social_metrics_snapshots row (followers/connections) so Audience Growth works.


7) Tests (minimum set)

Add unit tests in tests/ to stabilize the scoring math:

  1. test_brand_score_components_happy_path
  • Fake dataset with posts + metrics
  • Assert all 6 component keys present
  • Assert weights consistent with PRD
  • Assert total score within expected range and deterministic
  1. test_brand_score_missing_data_renormalization
  • Omit recruiter visibility metrics & cross-platform
  • Assert insufficient_data flagged
  • Assert total calculation uses renormalized weights
  1. test_brand_score_snapshot_persistence
  • Compute-and-persist writes a snapshot and updates “latest” summary
  1. test_metrics_snapshots_enable_audience_growth
  • Insert two snapshots in-window
  • Assert the Audience Growth component derives start/end from snapshots

  1. Add DB migration(s): brand_score_snapshots + latest fields on user summary 1b) Add DB migration: social_metrics_snapshots
  2. Implement/extend scoring engine with component spec + versioning
  3. Add repository methods for snapshot insert + trend queries
  4. Add API endpoints/schemas for latest + trendline
  5. Wire compute trigger after sync
  6. Add tests and make them green

9) Notes / open questions (non-blocking)

  • Recruiter visibility and search appearances might be hard to collect reliably without LinkedIn API access; v1 can use a proxy keyword coverage score.
  • Engagement rate impressions/reach may be unavailable on LinkedIn-first v1; use engagement proxies or mark insufficient data + renormalize.
  • Cross-platform consistency should be marked “insufficient data” unless multiple platforms are connected.
  • If you already have brand score summary fields on social_users, reuse them; otherwise add them.