From 27825e9b303448cc5f3d7b5e99f82b1eeb8131ae Mon Sep 17 00:00:00 2001 From: Abou Kone Date: Wed, 21 Jan 2026 12:35:28 -0500 Subject: [PATCH 1/5] docs: update implementation status and add technical documentation - Update status markers across multiple PRDs and implementation trackers - Add new technical documentation for analysis pipeline and how vibed works - Correct Supabase Studio port number in setup guide - Document migration from Edge Functions to Inngest for job processing --- Agents.md | 10 +- docs/PRD-profile-aggregation.md | 5 + docs/PRD-vibed-ux.md | 4 +- docs/PRD-vibed.md | 22 +- docs/PRD.md | 146 ++-- docs/WorkflowJournal.md | 4 +- docs/architecture/overview.md | 7 +- docs/architecture/vibed-analysis-pipeline.md | 852 +++++++++++++++++++ docs/github-setup.md | 2 +- docs/how-vibed-works.md | 187 ++++ docs/implementation-trackers/vibed.md | 52 +- docs/prd/multi-agent-detection.md | 2 +- docs/research/ai-era-coding-personas.md | 11 +- 13 files changed, 1202 insertions(+), 102 deletions(-) create mode 100644 docs/architecture/vibed-analysis-pipeline.md create mode 100644 docs/how-vibed-works.md diff --git a/Agents.md b/Agents.md index b4ff47b..31b3ee0 100644 --- a/Agents.md +++ b/Agents.md @@ -12,10 +12,14 @@ This document provides instructions for AI agents working on the Vibed Coding co - **Backend:** Next.js Route Handlers (Vercel) - **Auth:** Supabase Auth with GitHub OAuth - **Database:** Supabase Postgres with Row Level Security -- **Background Jobs:** Supabase Edge Functions -- **LLM:** Claude API (server-side only) +- **Background Jobs:** Inngest (primary) or standalone worker (fallback) +- **LLM:** Anthropic Claude, OpenAI, or Google Gemini (server-side only, configurable) -See `docs/PRD.md` for full product requirements. +See `docs/PRD.md` for full product requirements and `docs/architecture/inngest-integration.md` for job processing details. + +**Key Reference Docs:** +- [How Vibed Works](docs/how-vibed-works.md) — Product-friendly explanation of analysis +- [Vibed Analysis Pipeline](docs/architecture/vibed-analysis-pipeline.md) — Technical architecture with algorithms and data flow --- diff --git a/docs/PRD-profile-aggregation.md b/docs/PRD-profile-aggregation.md index 4862495..0b89948 100644 --- a/docs/PRD-profile-aggregation.md +++ b/docs/PRD-profile-aggregation.md @@ -1,5 +1,10 @@ # PRD: Profile Aggregation — One Persona Per User +**Status:** Core Implementation Complete — See `docs/implementation-trackers/profile-aggregation.md` for details + +> **Reference Documentation:** For technical details on aggregation, see: +> - [Vibed Analysis Pipeline](./architecture/vibed-analysis-pipeline.md#profile-aggregation) — Aggregation algorithm details + ## The Problem We built personas per repo. The PRDs say personas should aggregate per user. diff --git a/docs/PRD-vibed-ux.md b/docs/PRD-vibed-ux.md index 73a33b8..193f032 100644 --- a/docs/PRD-vibed-ux.md +++ b/docs/PRD-vibed-ux.md @@ -1,5 +1,7 @@ # PRD: Vibed UX — The Pop Version of Developer Analytics +**Status:** Partially Implemented — Core persona detection and profile pages are live. Share cards and some UX polish items are in progress. + > "Run it once, discover something surprising about yourself, share it with a friend." --- @@ -181,7 +183,7 @@ Every page in the application must reflect the Vibed personality. Here's the com ## 1. The Vision -Vibed Coding should feel like taking a personality quiz that actually knows you — except it's based on real data from your commits. Think BuzzFeed meets Spotify Wrapped for developers. The user should laugh, nod in recognition, screenshot something, and text it to a coworker within 5 minutes of their first analysis. +Vibed Coding should feel like taking a personality quiz that actually knows you, except it's based on real data from your commits. Think BuzzFeed meets Spotify Wrapped for vibe coders. The user should laugh, nod in recognition, screenshot something, and text it to a coworker within 5 minutes of their first analysis. **What we're NOT building:** - A performance review tool diff --git a/docs/PRD-vibed.md b/docs/PRD-vibed.md index 5d2c091..5605aa7 100644 --- a/docs/PRD-vibed.md +++ b/docs/PRD-vibed.md @@ -2,6 +2,12 @@ # PRD: Vibed Narrative Layer +**Status:** Mostly Implemented — See `docs/implementation-trackers/vibed.md` for details + +> **Reference Documentation:** For technical details on how analysis works, see: +> - [How Vibed Works](./how-vibed-works.md) — Product-friendly overview +> - [Vibed Analysis Pipeline](./architecture/vibed-analysis-pipeline.md) — Technical architecture + ## 1. Context & Vision Bolokono now surfaces “vibe coding profiles” through the Vibed experience: a playful, reflective interpretation of commit history modeled on Spotify Wrapped. The goal is to make craftsmanship visible—narrative cards first, data second—while preserving trust through evidence-backed insights. This PRD defines the UX, UI, API, database, and worker investments required to deliver deterministic insights, persona-aware storytelling, and share-ready outputs. @@ -14,9 +20,21 @@ Bolokono now surfaces “vibe coding profiles” through the Vibed experience: a 4. **Evidence & depth**: Keep a deep-dive layer with raw metrics, commit samples, evidence SHAs, and persona signal explanations for power users. 5. **Persona-aware confidence**: Surface persona tags + confidence metadata so insights stay observational (“You tend to…”, “Often, etc.”). -## 3. Supporting Research Summary +## 3. Supporting Research & Attribution + +### External Sources +- **"Vibe coding"**: Term coined by [Andrej Karpathy](https://x.com/karpathy/status/1886192184808149383) (February 2025), [Collins Dictionary Word of the Year 2025](https://en.wikipedia.org/wiki/Vibe_coding). +- **Orchestrator vs Conductor patterns**: [Addy Osmani's agentic coding research](https://addyosmani.com/blog/future-agentic-coding/). +- **Code analytics**: [GitClear developer productivity studies](https://www.gitclear.com/). +- **Copilot workflows**: [GitHub Copilot documentation](https://docs.github.com/en/copilot). + +### Internal Research +- [AI-Era Coding Personas](./research/ai-era-coding-personas.md) +- [AI-Era Developer Personas (ChatGPT synthesis)](./research/ai-era-developer-personas-chatgpt.md) +- [Persona Insight Synthesis](./research/persona-insight-synthesis.md) -Sources: `docs/research/ai-era-coding-personas.md`, `docs/research/ai-era-developer-personas-chatgpt.md`, `docs/research/persona-insight-synthesis.md`. Personas to detect: Spec-Driven Architect / Cautious Traditionalist, Test-First Validator / Pragmatic Augmenter, Iterative Prototyper / Vibe Coder, Multi-Agent Orchestrator / Autonomous AI Orchestrator, Specialist Consultant, Infrastructure Architect, Hands-On Debugger / Rapid Risk-Taker. Insights should mention craft behavior, guardrails, and risk tolerance. +### Personas Detected +Spec-Driven Architect, Test-First Validator, Vibe Prototyper, Agent Orchestrator, Hands-On Debugger, Rapid Risk-Taker, Reflective Balancer. Insights emphasize craft behavior, guardrails, and risk tolerance. ## 4. Metrics & Signals to Capture diff --git a/docs/PRD.md b/docs/PRD.md index c97dfdc..6bb3aaa 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -1,10 +1,16 @@ -# PRD: Bolokono +# PRD: Bolokono (Vibed Coding) -**Product name:** Bolokono +**Product name:** Bolokono (publicly branded as **Vibed Coding**) **Author:** Abou Kone -**Status:** Draft v4 (Monorepo architecture) +**Status:** Draft v4 (Monorepo architecture) — Core features implemented **Audience:** Product, Engineering (human + AI agents) +> **Terminology Note:** This PRD uses the original "Bolokono" terminology. In the codebase and user-facing product, this has been rebranded to **Vibed Coding**. "Bolokono types" are now called **Vibe Personas**, and "Bolokono profiles" are now **Vibe Profiles**. See `docs/PRD-vibed-ux.md` for the current UX direction. + +> **Reference Documentation:** For a complete explanation of how analysis works, see: +> - [How Vibed Works](./how-vibed-works.md) — Product-friendly overview +> - [Vibed Analysis Pipeline](./architecture/vibed-analysis-pipeline.md) — Technical architecture with diagrams + --- ## 1. Product Story @@ -153,22 +159,22 @@ They summarize computed facts and cite evidence. **Includes:** - [x] Authentication (GitHub OAuth via Supabase) -- [ ] GitHub repository listing and selection -- [ ] Database schema with RLS -- [ ] Repository sync (metadata only) -- [ ] Analysis job queue and state machine -- [ ] Edge Function analyzer (commit metadata extraction) -- [ ] Metrics computation and persistence -- [ ] Minimal Bolokono profile generation -- [ ] Basic report UI +- [x] GitHub repository listing and selection +- [x] Database schema with RLS +- [x] Repository sync (metadata only) +- [x] Analysis job queue and state machine +- [x] Inngest/Worker analyzer (commit metadata extraction) +- [x] Metrics computation and persistence +- [x] Minimal Vibe profile generation +- [x] Basic report UI -**Deliverable:** +**Deliverable:** ✓ - One private repo analyzed end-to-end -- One Bolokono profile rendered with: +- One Vibe profile rendered with: - At least 5 computed metrics - Build category timeline - - One assigned Bolokono type with evidence + - One assigned Vibe persona with evidence **Exit criteria:** @@ -222,7 +228,7 @@ vibed-coding/ │ ├── supabase/ │ ├── migrations/ # SQL migrations -│ ├── functions/ # Edge Functions (optional) +│ ├── functions/ # Edge Functions (legacy, see inngest-integration.md) │ └── config.toml │ └── docs/ @@ -1372,91 +1378,91 @@ Each task depends on the previous. **Output:** Monorepo structure with apps and packages. -#### Task 0.2: Supabase Setup +#### Task 0.2: Supabase Setup ✓ - [x] Create Supabase project (production) - [x] Set up local development with Supabase CLI -- [ ] Create initial migration with full schema -- [ ] Implement RLS policies -- [ ] Create `claim_analysis_job` function -- [ ] Test RLS policies with different user contexts +- [x] Create initial migration with full schema +- [x] Implement RLS policies +- [x] Create `claim_analysis_job` function +- [x] Test RLS policies with different user contexts **Output:** Database with schema, RLS, and job claiming function. -#### Task 0.3: Authentication +#### Task 0.3: Authentication ✓ -- [ ] Configure GitHub OAuth provider in Supabase -- [ ] Create GitHub OAuth app (for additional scopes) -- [ ] Implement Supabase Auth in `apps/web` -- [ ] Create sign-in page -- [ ] Create auth callback handler -- [ ] Store GitHub token in `github_accounts` (encrypted) -- [ ] Create protected route middleware +- [x] Configure GitHub OAuth provider in Supabase +- [x] Create GitHub OAuth app (for additional scopes) +- [x] Implement Supabase Auth in `apps/web` +- [x] Create sign-in page +- [x] Create auth callback handler +- [x] Store GitHub token in `github_accounts` (encrypted) +- [x] Create protected route middleware **Output:** User can sign in with GitHub, token stored securely. -#### Task 0.4: Repository Sync +#### Task 0.4: Repository Sync ✓ -- [ ] Create GitHub API client in `apps/web` -- [ ] Implement `POST /api/github/sync-repos` endpoint -- [ ] Create repo selection UI -- [ ] Implement repo connection flow (`user_repos`) -- [ ] Implement repo disconnection with data cleanup +- [x] Create GitHub API client in `apps/web` +- [x] Implement `POST /api/github/sync-repos` endpoint +- [x] Create repo selection UI +- [x] Implement repo connection flow (`user_repos`) +- [x] Implement repo disconnection with data cleanup **Output:** User can see their repos and connect/disconnect them. -#### Task 0.5: Analysis Job System +#### Task 0.5: Analysis Job System ✓ -- [ ] Create `POST /api/analysis/start` endpoint -- [ ] Set up Supabase Realtime subscription for job status -- [ ] Create job status UI component with live updates -- [ ] Implement basic worker loop in `apps/worker` -- [ ] Test job claiming with `FOR UPDATE SKIP LOCKED` +- [x] Create `POST /api/analysis/start` endpoint +- [x] Set up Supabase Realtime subscription for job status +- [x] Create job status UI component with live updates +- [x] Implement Inngest-based job processing (primary) and worker fallback +- [x] Test job claiming with `FOR UPDATE SKIP LOCKED` **Output:** Jobs can be queued and status tracked in real-time. -#### Task 0.6: Commit Analysis (Worker) +#### Task 0.6: Commit Analysis (Worker) ✓ -- [ ] Implement GitHub API client in `apps/worker` -- [ ] Implement commit fetching (paginated, batched) -- [ ] Implement commit classification (`packages/core`) -- [ ] Implement metrics computation (`packages/core`) -- [ ] Store metrics in `analysis_metrics` -- [ ] Handle rate limits gracefully -- [ ] Handle large repos (time-distributed sampling + notice) +- [x] Implement GitHub API client in `apps/worker` +- [x] Implement commit fetching (paginated, batched) +- [x] Implement commit classification (`packages/core`) +- [x] Implement metrics computation (`packages/core`) +- [x] Store metrics in `analysis_metrics` +- [x] Handle rate limits gracefully +- [x] Handle large repos (time-distributed sampling + notice) **Output:** Worker fetches commits and computes metrics. -#### Task 0.7: Report Generation (Worker) +#### Task 0.7: Report Generation (Worker) ✓ -- [ ] Implement Bolokono type matching (`packages/core`) -- [ ] Create Claude API client in `apps/worker` -- [ ] Implement narrative generation with structured output -- [ ] Implement narrative validation (banned phrases, SHA checks) -- [ ] Store reports in `analysis_reports` -- [ ] Handle generation failures gracefully +- [x] Implement Vibe persona matching (`packages/core`) +- [x] Create multi-provider LLM client (Anthropic, OpenAI, Gemini) +- [x] Implement narrative generation with structured output +- [x] Implement narrative validation (banned phrases, SHA checks) +- [x] Store reports in `analysis_reports` +- [x] Handle generation failures gracefully with fallback narratives **Output:** Worker generates and stores reports. -#### Task 0.8: Report UI +#### Task 0.8: Report UI ✓ -- [ ] Create report page layout in `apps/web` -- [ ] Implement Bolokono type display with badge -- [ ] Implement metrics cards (key stats) -- [ ] Implement narrative sections with evidence -- [ ] Create build timeline visualization -- [ ] Handle loading/error/insufficient-data states +- [x] Create report page layout in `apps/web` +- [x] Implement Vibe persona display with badge +- [x] Implement metrics cards (key stats) +- [x] Implement narrative sections with evidence +- [x] Create build timeline visualization +- [x] Handle loading/error/insufficient-data states -**Output:** User can view their Bolokono profile. +**Output:** User can view their Vibe profile. -### Phase 0 Exit Criteria +### Phase 0 Exit Criteria ✓ -- [ ] End-to-end flow works for private repo -- [ ] All RLS policies enforced -- [ ] Worker processes jobs correctly -- [ ] Analysis completes in < 60s for < 1000 commits -- [ ] Report displays type, metrics, and narrative -- [ ] User can disconnect repo and data is deleted +- [x] End-to-end flow works for private repo +- [x] All RLS policies enforced +- [x] Worker processes jobs correctly (via Inngest + fallback) +- [x] Analysis completes in < 60s for < 1000 commits +- [x] Report displays persona, metrics, and narrative +- [x] User can disconnect repo and data is deleted --- diff --git a/docs/WorkflowJournal.md b/docs/WorkflowJournal.md index 66d611d..36f42a4 100644 --- a/docs/WorkflowJournal.md +++ b/docs/WorkflowJournal.md @@ -74,11 +74,13 @@ Format: ### 2025-01-16 - [DECISION] Edge Functions vs External Worker **Context:** Need to run analysis jobs -**Action:** Chose Supabase Edge Functions for Phase 0-1, with option to move to external worker for Phase 2+ +**Action:** Initially chose Supabase Edge Functions for Phase 0-1, with option to move to external worker for Phase 2+ **Time spent:** N/A (part of PRD) **Automation opportunity:** None **Notes:** Edge Functions have 60s timeout. Fine for < 1000 commits. Will need worker for large repos. +**UPDATE (2026-01):** Migrated to **Inngest** as primary job processor with standalone worker as fallback. Inngest provides better observability, automatic retries, and serverless scaling. See `docs/architecture/inngest-integration.md`. + --- ### 2025-01-16 - [INSIGHT] Workflow templates should be portable diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index 6950d42..6818a11 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -2,9 +2,14 @@ This document explains how the Vibed Coding stack fits together, how data flows from a logged-in user through Supabase, the worker, and the UI, and which pieces of the codebase (web, worker, packages) own each responsibility. The diagrams use Mermaid syntax so you can paste them into any renderer, but the textual explanations also narrate every interaction. +> **Related Documentation:** +> - [How Vibed Works](../how-vibed-works.md) — Product-friendly explanation +> - [Vibed Analysis Pipeline](./vibed-analysis-pipeline.md) — Detailed analysis algorithms and data flow +> - [Inngest Integration](./inngest-integration.md) — Background job processing + ## High-level Components -1. **Next.js 16 (App Router) Web App** (`apps/web`) +1. **Next.js 14+ (App Router) Web App** (`apps/web`) - Hosts the public marketing homepage (`/`) plus authenticated areas (`/repos`, `/analysis/[jobId]`, `/login`, `/auth/callback`, etc.). - Handles Supabase Auth via `proxy.ts`, guarding protected routes and synchronizing auth cookies. - Calls Supabase Route Handlers (`/api/analysis/*`, `/api/github/sync-repos`, etc.) to start jobs, list repos, fetch shared insights, export share cards, and run other backend flows. diff --git a/docs/architecture/vibed-analysis-pipeline.md b/docs/architecture/vibed-analysis-pipeline.md new file mode 100644 index 0000000..c3a696d --- /dev/null +++ b/docs/architecture/vibed-analysis-pipeline.md @@ -0,0 +1,852 @@ +# Vibed Analysis Pipeline — Technical Architecture + +> **Last Updated:** January 2026 +> **Maintainer:** Update this document when analysis logic, data models, or algorithms change. + +This document provides a comprehensive technical reference for how Vibed Coding analyzes repositories and generates user profiles. It covers data flow, algorithms, type definitions, and links to source code. + +--- + +## Table of Contents + +1. [High-Level Architecture](#high-level-architecture) +2. [Data Flow](#data-flow) +3. [Repository Analysis Pipeline](#repository-analysis-pipeline) +4. [Metrics Computation](#metrics-computation) +5. [Vibe Axes System](#vibe-axes-system) +6. [Persona Detection](#persona-detection) +7. [Insights Generation](#insights-generation) +8. [LLM Narrative Generation](#llm-narrative-generation) +9. [Profile Aggregation](#profile-aggregation) +10. [Database Schema](#database-schema) +11. [Source Code References](#source-code-references) + +--- + +## High-Level Architecture + +```mermaid +flowchart TB + subgraph Client["Web Client (Next.js)"] + UI[Analysis UI] + API[API Routes] + end + + subgraph Processing["Job Processing"] + Inngest[Inngest Function] + Worker[Fallback Worker] + end + + subgraph External["External Services"] + GitHub[GitHub API] + LLM[LLM Provider] + end + + subgraph Storage["Supabase"] + DB[(PostgreSQL)] + Auth[Supabase Auth] + end + + subgraph Core["@vibed/core"] + Metrics[Metrics Computation] + Axes[Vibe Axes] + Persona[Persona Detection] + Insights[Insights Generation] + end + + UI --> API + API --> DB + API --> Inngest + Inngest --> GitHub + Inngest --> Core + Inngest --> LLM + Inngest --> DB + Worker --> GitHub + Worker --> Core + Worker --> LLM + Worker --> DB +``` + +### Components + +| Component | Location | Responsibility | +|-----------|----------|----------------| +| Web Client | `apps/web` | UI, API routes, job triggering | +| Inngest Function | `apps/web/src/inngest/functions/analyze-repo.ts` | Primary job processor | +| Fallback Worker | `apps/worker/src/index.ts` | Self-hosted fallback | +| @vibed/core | `packages/core/src/` | All analysis logic | +| Database | `supabase/migrations/` | Persistent storage | + +--- + +## Data Flow + +```mermaid +sequenceDiagram + participant User + participant API as API Route + participant DB as Supabase + participant Inngest + participant GitHub + participant Core as @vibed/core + participant LLM + + User->>API: POST /api/analysis/start + API->>DB: Create analysis_job (status=queued) + API->>Inngest: Fire repo/analyze.requested event + API-->>User: Return job_id + + Inngest->>DB: Claim job (status=running) + Inngest->>GitHub: Fetch commits (paginated) + Inngest->>GitHub: Fetch PRs (if available) + + Inngest->>Core: filterAutomationCommits() + Inngest->>Core: computeAnalysisMetrics() + Inngest->>Core: computeAnalysisInsights() + Inngest->>Core: computeVibeFromCommits() + Inngest->>Core: detectVibePersona() + + Inngest->>DB: Store analysis_metrics + Inngest->>DB: Store analysis_insights + Inngest->>DB: Store vibe_insights + + opt LLM Available + Inngest->>LLM: Generate narrative + Inngest->>DB: Store analysis_reports + end + + Inngest->>Core: aggregateUserProfile() + Inngest->>DB: Update user_profiles + Inngest->>DB: Update job (status=done) + + User->>API: GET /api/analysis/:id + API->>DB: Fetch results + API-->>User: Return analysis data +``` + +--- + +## Repository Analysis Pipeline + +### Step 1: Job Creation + +```typescript +// API creates job record +interface AnalysisJob { + id: string; + user_id: string; + repo_id: string; + status: "queued" | "running" | "done" | "error"; + created_at: string; + started_at: string | null; + completed_at: string | null; + error_message: string | null; +} +``` + +### Step 2: Commit Fetching + +```mermaid +flowchart LR + A[Get Repo Metadata] --> B{Repo Age} + B -->|< 1 year| C[Fetch Recent 300] + B -->|> 1 year| D[Time-Distributed Sampling] + D --> E[Split into 6-24 buckets] + E --> F[Sample evenly per bucket] + C --> G[Commit List] + F --> G + G --> H[Fetch Details per Commit] + H --> I[CommitEvent Array] +``` + +**Sampling Strategy:** +- Repos are divided into time buckets (6-24 based on size) +- Each bucket is sampled evenly to capture patterns across the repo's lifetime +- Maximum 300 commits per analysis +- Commit details (file paths, stats) fetched in parallel batches + +### Step 3: Automation Filtering + +```typescript +// packages/core/src/index.ts +function isAutomationCommit(event: CommitEvent): boolean { + // Filters: dependabot, renovate, release-please, github-actions, etc. + // Based on email patterns and commit message patterns +} + +function filterAutomationCommits(events: CommitEvent[]): CommitEvent[] +``` + +**Filtered Patterns:** +- Bot emails: `dependabot`, `renovate`, `github-actions`, `[bot]` +- Message patterns: `chore(release):`, `bump version`, `[automated]` + +--- + +## Metrics Computation + +### Input Type + +```typescript +interface CommitEvent { + sha: string; + message: string; + author_date: string; + committer_date: string; + author_email: string; + files_changed: number; + additions: number; + deletions: number; + parents: string[]; + file_paths?: string[]; // For subsystem detection +} +``` + +### Output Type + +```typescript +interface AnalysisMetrics { + // Volume + total_commits: number; + total_additions: number; + total_deletions: number; + total_files_changed: number; + + // Timing + first_commit_date: string; + last_commit_date: string; + active_days: number; + span_days: number; + + // Commit Size Distribution + commit_size_p50: number; + commit_size_p90: number; + commit_size_mean: number; + commit_size_stddev: number; + + // Frequency + commits_per_active_day_mean: number; + commits_per_active_day_max: number; + + // Rhythm + hours_between_commits_p50: number; + hours_between_commits_p90: number; + burstiness_score: number; // -1 (steady) to +1 (bursty) + + // Message Analysis + message_length_p50: number; + message_length_p90: number; + conventional_commit_ratio: number; + + // Iteration Patterns + fix_commit_ratio: number; + fixup_sequence_count: number; // fix-after-feature sequences + + // Build Sequence + category_first_occurrence: Record; + category_distribution: Record; + + // Merge Behavior + merge_commit_ratio: number; + + // Confidence + data_quality_score: number; // 0-100 +} +``` + +### Build Category Classification + +```typescript +type BuildCategory = + | "setup" | "auth" | "feature" | "test" | "infra" + | "docs" | "refactor" | "fix" | "style" | "chore" | "unknown"; + +function classifyCommit(message: string): BuildCategory { + // Priority 1: Conventional commit prefixes (feat:, fix:, test:, etc.) + // Priority 2: Scope overrides (auth, setup) + // Priority 3: Keyword matching + // Priority 4: Default to "feature" for substantive commits +} +``` + +### Burstiness Score Algorithm + +```typescript +function calculateBurstiness(timestamps: Date[]): number { + // Burstiness = (stddev - mean) / (stddev + mean) + // Range: -1 (perfectly regular) to +1 (highly bursty) + const intervals = timestamps.map((t, i) => + i > 0 ? (t - timestamps[i-1]) / (1000 * 60 * 60) : 0 + ).slice(1); + + const mean = avg(intervals); + const stddev = std(intervals); + return (stddev - mean) / (stddev + mean); +} +``` + +--- + +## Vibe Axes System + +Six deterministic axes scored 0-100: + +```mermaid +graph TD + subgraph Axes["Six Vibe Axes"] + A["A: Automation Heaviness"] + B["B: Guardrail Strength"] + C["C: Iteration Loop Intensity"] + D["D: Planning Signal"] + E["E: Surface Area per Change"] + F["F: Shipping Rhythm"] + end + + subgraph Inputs["Input Signals"] + I1[Commit Size Stats] + I2[PR Chunkiness] + I3[First-Touch Percentiles] + I4[Category Distribution] + I5[Fix Sequences] + I6[Conventional Commits] + I7[Issue Linking] + I8[Episode Subsystems] + I9[Burstiness Score] + end + + I1 --> A + I2 --> A + I3 --> B + I4 --> B + I4 --> C + I5 --> C + I6 --> D + I7 --> D + I8 --> E + I9 --> F +``` + +### Axis Computation + +```typescript +interface AxisValue { + score: number; // 0-100 + level: "low" | "medium" | "high"; + why: string[]; // Evidence IDs +} + +interface VibeAxes { + automation_heaviness: AxisValue; + guardrail_strength: AxisValue; + iteration_loop_intensity: AxisValue; + planning_signal: AxisValue; + surface_area_per_change: AxisValue; + shipping_rhythm: AxisValue; +} +``` + +### Axis A: Automation Heaviness + +**Signals:** +- Average files changed per commit +- p90 commit size (additions + deletions) +- p90 PR changed files + +**Formula:** +``` +Score = 0.5 × scoreFromZ(avgFiles, mid=4, spread=4) + + 0.3 × scoreFromZ(p90Commit, mid=400, spread=400) + + 0.2 × scoreFromZ(prP90, mid=25, spread=25) +``` + +### Axis B: Guardrail Strength + +**Signals:** +- First-touch percentile for tests/CI/docs (earlier = stronger) +- Guardrail commit density (tests + docs + chore + CI) +- PR checklist presence rate + +**Formula:** +``` +Score = 0.5 × (1 - avgFirstTouch) × 100 + + 0.3 × guardrailDensity × 200 + + 0.2 × prChecklistRate × 100 +``` + +### Axis C: Iteration Loop Intensity + +**Signals:** +- Fix-after-feature sequence count +- Fix commit ratio +- Episode fix density + +**Formula:** +``` +Score = 0.5 × scoreFromZ(fixSeq/total, mid=0.12, spread=0.12) + + 0.3 × fixRatio × 100 + + 0.2 × episodeFixDensity × 100 +``` + +### Axis D: Planning Signal + +**Signals:** +- Conventional commit ratio +- PR issue linking rate +- Docs-before-feature pattern + +**Formula:** +``` +Score = 0.35 × issueLinkRate × 100 + + 0.35 × conventionalRatio × 100 + + 0.30 × docsFirst × 100 +``` + +### Axis E: Surface Area per Change + +**Signals:** +- Median subsystems touched per work episode + +**Formula:** +``` +Score = scoreFromZ(medianSubsystems, mid=3, spread=2) +``` + +### Axis F: Shipping Rhythm + +**Signals:** +- Burstiness score +- p90 commits per episode + +**Formula:** +``` +Score = 0.6 × ((burstiness + 1) / 2) × 100 + + 0.4 × scoreFromZ(episodeP90, mid=6, spread=6) +``` + +--- + +## Persona Detection + +```mermaid +flowchart TD + Axes[Six Axis Scores] --> Rules[Persona Rules] + Rules --> Strict{Strict Match?} + Strict -->|Yes| StrictPersona[Matched Persona] + Strict -->|No| Loose{Loose Match?} + Loose -->|≥50% conditions, score≥60| LoosePersona[Nearest Persona] + Loose -->|No| Fallback[Balanced Builder] + + StrictPersona --> Output[VibePersona] + LoosePersona --> Output + Fallback --> Output +``` + +### Persona Rules + +```typescript +type VibePersonaId = + | "prompt_sprinter" // Vibe Prototyper + | "guardrailed_viber" // Test-First Validator + | "spec_first_director" // Spec-Driven Architect + | "vertical_slice_shipper" // Agent Orchestrator + | "fix_loop_hacker" // Hands-On Debugger + | "rapid_risk_taker" // Rapid Risk-Taker + | "balanced_builder"; // Reflective Balancer (fallback) +``` + +| Persona | Conditions | +|---------|------------| +| **Vibe Prototyper** | A≥70, C≥65, B<40, D<45 | +| **Test-First Validator** | A≥65, B≥65, C≥40 | +| **Spec-Driven Architect** | D≥70, B≥55, A≥40 | +| **Agent Orchestrator** | E≥70, A≥60 | +| **Hands-On Debugger** | C≥80, F≥60 | +| **Rapid Risk-Taker** | A≥65, B<45, D<50, F≥40 | +| **Balanced Builder** | Fallback when no rules match | + +### Confidence Calculation + +```typescript +function getConfidence(meta: { commitCount, prCount, dataQualityScore }): Confidence { + const commitOk = meta.commitCount >= 200; + const prOk = meta.prCount >= 15; + const qualityOk = meta.dataQualityScore >= 70; + + if ((commitOk || prOk) && qualityOk && personaScore >= 75) return "high"; + if ((meta.commitCount >= 80 || meta.prCount >= 6) && personaScore >= 65) return "medium"; + return "low"; +} +``` + +--- + +## Insights Generation + +### AnalysisInsights Structure + +```typescript +interface AnalysisInsights { + version: string; + timezone: "UTC"; + generated_at: string; + + totals: { commits: number }; + + streak: { + longest_days: number; + start_day: string | null; + end_day: string | null; + confidence: Confidence; + evidence_shas: string[]; + }; + + timing: { + top_weekdays: Array<{ weekday: number; count: number }>; + peak_weekday: number | null; + peak_hour: number | null; + peak_window: "mornings" | "afternoons" | "evenings" | "late_nights" | null; + confidence: Confidence; + evidence_shas: string[]; + }; + + commits: { + top_category: BuildCategory | null; + category_counts: Record; + features: number; + fixes: number; + features_per_fix: number | null; + confidence: Confidence; + evidence_shas: string[]; + }; + + chunkiness: { + avg_files_changed: number | null; + label: "slicer" | "mixer" | "chunker" | null; + confidence: Confidence; + evidence_shas: string[]; + }; + + multi_agent_signals: { + co_author_count: number; + ai_trailer_count: number; + ai_keyword_count: number; + pr_squash_merge_rate: number | null; + pr_template_rate: number | null; + pr_checklist_rate: number | null; + pr_linked_issue_rate: number | null; + confidence: Confidence; + evidence_shas: string[]; + }; + + artifact_traceability: ArtifactTraceability; + + persona: AnalysisInsightPersona; + share_template: AnalysisInsightShareTemplate; + persona_delta: AnalysisInsightPersonaDelta[]; + tech_signals: AnalysisInsightTechSignals; + + sources: string[]; + disclaimers: string[]; +} +``` + +### Chunkiness Classification + +```typescript +function classifyChunkiness(avgFilesChanged: number): "slicer" | "mixer" | "chunker" { + if (avgFilesChanged <= 3) return "slicer"; // Focused changes + if (avgFilesChanged <= 8) return "mixer"; // Balanced + return "chunker"; // Wide scope +} +``` + +### Multi-Agent Signal Detection + +```mermaid +flowchart LR + Commits[Commit Messages] --> Trailers[Parse Trailers] + Trailers --> CoAuthor[Co-authored-by Count] + Trailers --> AITrailer[AI Trailer Count] + + Messages[Message Keywords] --> AIKeywords[Agent/Copilot/Claude mentions] + + PRs[Pull Requests] --> SquashRate[Squash Merge Rate] + PRs --> TemplateRate[Template Usage Rate] + PRs --> ChecklistRate[Checklist Rate] + PRs --> IssueRate[Issue Link Rate] + + CoAuthor --> Signals[Multi-Agent Signals] + AITrailer --> Signals + AIKeywords --> Signals + SquashRate --> Signals + TemplateRate --> Signals + ChecklistRate --> Signals + IssueRate --> Signals +``` + +--- + +## LLM Narrative Generation + +### When LLM Is Used + +```mermaid +flowchart TD + Start[Analysis Complete] --> Check{LLM Available?} + Check -->|No| Fallback[Metrics-Only Narrative] + Check -->|Yes| Resolve[Resolve LLM Config] + + Resolve --> UserKey{User API Key?} + UserKey -->|Yes| UseUser[Use User Key] + UserKey -->|No| FreeTier{Free Tier Available?} + FreeTier -->|Yes| UsePlatform[Use Platform Key] + FreeTier -->|No| Fallback + + UseUser --> Generate[Generate Narrative] + UsePlatform --> Generate + Generate --> Store[Store in analysis_reports] +``` + +### LLM Input (Privacy-Safe) + +The LLM receives: +- Metrics JSON (no commit content) +- Insights JSON (persona, timing, patterns) +- Episode summary (timestamps, categories, sizes) +- Commit lines: `timestamp sha category size files` (NO message content) + +**Never sent to LLM:** +- Commit message text +- File contents +- Project/product names +- Business domain information + +### Supported Providers + +| Provider | Models | Config | +|----------|--------|--------| +| Anthropic | claude-sonnet-4, claude-haiku | `ANTHROPIC_API_KEY` | +| OpenAI | gpt-4o, gpt-4o-mini | `OPENAI_API_KEY` | +| Google | gemini-2.0-flash, gemini-1.5-flash | `GEMINI_API_KEY` | + +--- + +## Profile Aggregation + +### Multi-Repo to Single Profile + +```mermaid +flowchart TD + subgraph Repos["Individual Repo Analyses"] + R1[Repo A: 500 commits] + R2[Repo B: 200 commits] + R3[Repo C: 100 commits] + end + + subgraph Aggregation["Aggregation Logic"] + W[Weight by Commit Count] + M[Merge Metrics] + A[Average Axes] + P[Re-detect Persona] + end + + subgraph Profile["User Profile"] + UP[Aggregated Profile] + N[Profile Narrative] + end + + R1 --> W + R2 --> W + R3 --> W + W --> M + M --> A + A --> P + P --> UP + UP --> N +``` + +### Aggregation Algorithm + +```typescript +function aggregateUserProfile(repoInsights: RepoInsightSummary[]): UserProfile { + // 1. Calculate total commits for weighting + const totalCommits = sum(repoInsights.map(r => r.commit_count)); + + // 2. Weight each repo's contribution + const weights = repoInsights.map(r => r.commit_count / totalCommits); + + // 3. Aggregate metrics (weighted average) + const aggregatedMetrics = weightedAverage(repoInsights, weights); + + // 4. Aggregate axes (weighted average) + const aggregatedAxes = weightedAverageAxes(repoInsights, weights); + + // 5. Re-detect persona from aggregated axes + const persona = detectVibePersona(aggregatedAxes, { + commitCount: totalCommits, + prCount: sum(repoInsights.map(r => r.pr_count)), + }); + + // 6. Generate profile narrative (if LLM available) + const narrative = generateProfileNarrative(aggregatedMetrics, persona); + + return { metrics: aggregatedMetrics, axes: aggregatedAxes, persona, narrative }; +} +``` + +### Profile Confidence + +| Repos Analyzed | Confidence | +|----------------|------------| +| 1 repo | Low — snapshot only | +| 2-3 repos | Medium — patterns emerging | +| 4+ repos | High — consistent patterns | + +--- + +## Database Schema + +### Core Tables + +```mermaid +erDiagram + users ||--o{ repos : owns + users ||--o{ analysis_jobs : requests + users ||--|| user_profiles : has + repos ||--o{ analysis_jobs : analyzed_by + analysis_jobs ||--|| analysis_metrics : produces + analysis_jobs ||--|| analysis_insights : produces + analysis_jobs ||--|| analysis_reports : produces + analysis_jobs ||--|| vibe_insights : produces + repos ||--o{ pull_requests : has + + users { + uuid id PK + string email + timestamp created_at + } + + repos { + uuid id PK + uuid user_id FK + string full_name + boolean is_private + timestamp last_analyzed_at + } + + analysis_jobs { + uuid id PK + uuid user_id FK + uuid repo_id FK + string status + timestamp created_at + timestamp completed_at + } + + analysis_metrics { + uuid id PK + uuid job_id FK + jsonb metrics_json + } + + analysis_insights { + uuid id PK + uuid job_id FK + jsonb insights_json + } + + vibe_insights { + uuid id PK + uuid job_id FK + jsonb axes + jsonb persona + jsonb cards + jsonb evidence_index + } + + analysis_reports { + uuid id PK + uuid job_id FK + string vibe_type + string confidence + jsonb narrative + } + + user_profiles { + uuid id PK + uuid user_id FK + jsonb aggregated_metrics + jsonb aggregated_axes + jsonb persona + jsonb narrative + int repos_analyzed + } + + pull_requests { + uuid id PK + uuid repo_id FK + int github_pr_number + string title + boolean merged + string merge_method + timestamp created_at + } +``` + +--- + +## Source Code References + +| Component | File | Key Functions | +|-----------|------|---------------| +| Metrics Computation | `packages/core/src/index.ts` | `computeAnalysisMetrics()` | +| Commit Classification | `packages/core/src/index.ts` | `classifyCommit()`, `isAutomationCommit()` | +| Insights Generation | `packages/core/src/index.ts` | `computeAnalysisInsights()` | +| Vibe Axes | `packages/core/src/vibe.ts` | `computeVibeAxes()` | +| Persona Detection | `packages/core/src/vibe.ts` | `detectVibePersona()` | +| Episode Building | `packages/core/src/vibe.ts` | `buildWorkEpisodes()` | +| Subsystem Classification | `packages/core/src/vibe.ts` | `classifySubsystem()` | +| LLM Client | `packages/core/src/llm/` | `createLLMClient()` | +| Profile Aggregation | `packages/core/src/index.ts` | `aggregateUserProfile()` | +| Inngest Function | `apps/web/src/inngest/functions/analyze-repo.ts` | Main job processor | +| Fallback Worker | `apps/worker/src/index.ts` | Self-hosted processor | + +--- + +## Related Documentation + +- [How Vibed Works (Product Overview)](../how-vibed-works.md) +- [Vibe Metrics v2](./vibe-metrics-v2.md) — Original axis design +- [Inngest Integration](./inngest-integration.md) — Job processing details +- [Architecture Overview](./overview.md) — System components +- [PRD: Multi-Agent Detection](../prd/multi-agent-detection.md) — AI workflow signals + +--- + +## Attribution + +This architecture builds on concepts and research from the developer tooling community: + +| Concept | Source | +|---------|--------| +| "Vibe coding" term | [Andrej Karpathy](https://x.com/karpathy/status/1886192184808149383) (Feb 2025) | +| Orchestrator vs Conductor patterns | [Addy Osmani](https://addyosmani.com/blog/future-agentic-coding/) | +| Code analytics methodologies | [GitClear](https://www.gitclear.com/) | +| Co-author attribution patterns | [GitHub Copilot Docs](https://docs.github.com/en/copilot) | + +See [internal research docs](../research/) for persona taxonomy synthesis. + +--- + +## Assumptions & Caveats + +1. **GitHub API Only:** Analysis is limited to what GitHub exposes (no IDE telemetry) +2. **Commit Sampling:** Large repos are sampled; not all commits analyzed +3. **Bot Filtering:** Heuristic-based; some edge cases may slip through +4. **Persona Rules:** Deterministic rules may not capture all workflow styles +5. **LLM Privacy:** Commit content never sent; only metadata and statistics +6. **Aggregation Weights:** Larger repos have more influence on profile + +--- + +*This document should be updated whenever analysis logic, data models, or algorithms change.* diff --git a/docs/github-setup.md b/docs/github-setup.md index 2bba8db..c7fc5f8 100644 --- a/docs/github-setup.md +++ b/docs/github-setup.md @@ -16,7 +16,7 @@ This document summarizes what you need to do before you can authenticate via Git This project relies on Supabase Auth using GitHub OAuth (see `docs/PRD.md`). 1. Start your local Supabase stack if it is not running: `npm run supabase:start`. -2. Open Supabase Studio at `http://127.0.0.1:54423` and navigate to **Authentication → Providers → GitHub**. +2. Open Supabase Studio at `http://127.0.0.1:54323` and navigate to **Authentication → Providers → GitHub**. 3. Paste the Client ID and Client Secret from the OAuth app you created above. 4. Select scopes required for private repo access (at least `repo` and `read:user`). diff --git a/docs/how-vibed-works.md b/docs/how-vibed-works.md new file mode 100644 index 0000000..046292a --- /dev/null +++ b/docs/how-vibed-works.md @@ -0,0 +1,187 @@ +# How Vibed Coding Works + +> **Last Updated:** January 2026 +> **Maintainer:** Update this document when analysis logic, personas, or metrics change. + +Vibed Coding analyzes your git history to reveal your coding style and patterns. Think of it as "Spotify Wrapped for vibe coders." We look at *how* you build, not *what* you build. + +--- + +## The Big Picture + +``` +Your Git History → Analysis → Vibe Profile + ↓ ↓ ↓ + Commits Metrics Persona + Insights + PRs Axes Narrative + Timing Scores Share Cards +``` + +**Key Principle:** Vibed is *observational, not judgmental*. We detect patterns, not quality. Every coding style has strengths. + +--- + +## What We Analyze + +### Data Sources (All from GitHub API) + +| Source | What We Extract | Privacy Note | +|--------|-----------------|--------------| +| **Commits** | Timestamps, file counts, additions/deletions, message structure | We never store or analyze code content | +| **Pull Requests** | Merge methods, checklists, issue links, templates | Body text parsed for structure only | +| **File Paths** | Subsystem classification (ui, api, tests, etc.) | Paths only, no file contents | + +### What We Don't Access +- Source code contents +- Private comments or discussions +- IDE activity or local history +- Unpushed commits + +--- + +## The Analysis Pipeline + +### Step 1: Fetch Commits +We retrieve up to 300 commits from your repository via GitHub API, sampling evenly across the repo's lifetime to capture patterns from start to present. + +### Step 2: Filter Automation +Bot commits (dependabot, renovate, release-please, etc.) are automatically filtered out so your analysis reflects *your* work, not automation. + +### Step 3: Compute Metrics +We calculate 25+ metrics across several dimensions: + +**Volume & Timing** +- Total commits, additions, deletions +- Active days vs span days +- Commit frequency patterns + +**Rhythm & Burstiness** +- Hours between commits (median, p90) +- Burstiness score (-1 = steady, +1 = bursty) +- Peak coding hours and days + +**Commit Patterns** +- Size distribution (p50, p90) +- Conventional commit ratio +- Fix-after-feature sequences + +**Build Categories** +- Classification: feature, fix, test, docs, infra, refactor, etc. +- Category distribution and first-occurrence order + +### Step 4: Compute Vibe Axes +Six deterministic axes (0-100 scores) capture your workflow style: + +| Axis | What It Measures | +|------|------------------| +| **Automation Heaviness** | How "agentic" your workflow looks: large commits, chunky PRs | +| **Guardrail Strength** | How much you stabilize with tests, CI, docs early | +| **Iteration Loop Intensity** | How often you do rapid fix cycles | +| **Planning Signal** | Structured commits, issue linking, docs-first patterns | +| **Surface Area per Change** | How many subsystems you touch per work session | +| **Shipping Rhythm** | Bursty vs steady shipping patterns | + +### Step 5: Detect Persona +Based on your axes, we match you to one of 7 Vibe Personas: + +| Persona | Signature Pattern | +|---------|-------------------| +| **Vibe Prototyper** | High automation, rapid iteration, minimal guardrails | +| **Test-First Validator** | Strong guardrails with automation | +| **Spec-Driven Architect** | High planning signal, early guardrails | +| **Agent Orchestrator** | Wide surface area, high automation | +| **Hands-On Debugger** | Intense fix loops, fast shipping | +| **Rapid Risk-Taker** | High automation, low guardrails, fast shipping | +| **Reflective Balancer** | Balanced across all axes (fallback) | + +### Step 6: Generate Insights +Deterministic insights are computed server-side: + +- **Longest Streak:** Consecutive days with commits +- **Peak Window:** When you code most (mornings, afternoons, evenings, late nights) +- **Chunkiness:** Slicer (focused), Mixer (balanced), or Chunker (wide scope) +- **Tech Signals:** Keywords detected in commit messages +- **Multi-Agent Signals:** Co-author trailers, AI attribution patterns + +### Step 7: LLM Narrative (Optional) +If LLM is configured, we generate a human-readable narrative about your *engineering patterns*, never about what you built, only how you built it. + +**Privacy:** The LLM only sees metadata (timestamps, categories, metrics), never commit message content or code. + +--- + +## Profile Aggregation + +### Single Repo Analysis +One repo gives you a snapshot of your coding style for that project. + +### Multi-Repo User Profile +When you analyze multiple repos, Vibed aggregates them into a single **User Profile**: + +``` +Repo A Analysis ─┐ +Repo B Analysis ─┼─→ Aggregated User Profile +Repo C Analysis ─┘ +``` + +**Aggregation Logic:** +- Metrics are weighted by commit count (larger repos contribute more) +- Axes are averaged across repos +- Persona is re-detected from aggregated axes +- Confidence increases with more repos (3+ repos = stronger signal) + +--- + +## Confidence Levels + +Every insight includes a confidence level: + +| Level | Meaning | +|-------|---------| +| **High** | 200+ commits or 15+ PRs, good data quality | +| **Medium** | 80+ commits or 6+ PRs | +| **Low** | Limited data, take insights with a grain of salt | + +--- + +## What Vibed Is NOT + +- **Not a productivity tracker:** We don't measure "good" vs "bad" +- **Not a code quality tool:** We don't analyze code, just patterns +- **Not AI detection:** We detect workflow patterns, not AI usage +- **Not surveillance:** You control what repos to analyze, data is yours + +--- + +## Privacy Principles + +1. **No code access:** We only read metadata from GitHub API +2. **No message content to LLM:** Commit messages are classified locally, never sent to AI +3. **User-controlled:** You choose which repos to analyze +4. **Deletable:** Disconnect a repo and all analysis data is removed + +--- + +## Attribution + +Vibed Coding builds on research and concepts from the developer tooling community: + +- **"Vibe coding"**: Term coined by [Andrej Karpathy](https://x.com/karpathy/status/1886192184808149383) in February 2025, later named [Collins Dictionary Word of the Year 2025](https://en.wikipedia.org/wiki/Vibe_coding). +- **Orchestrator vs Conductor patterns**: From [Addy Osmani's work on agentic coding](https://addyosmani.com/blog/future-agentic-coding/). +- **Code analytics research**: Informed by [GitClear's developer productivity studies](https://www.gitclear.com/). +- **Persona taxonomy**: Original synthesis drawing from academic TDD research, [GitHub Copilot documentation](https://docs.github.com/en/copilot), and developer workflow studies. + +Our internal research documents are available in [docs/research/](./research/). + +--- + +## Related Documentation + +- [Technical Architecture](./architecture/vibed-analysis-pipeline.md): Deep dive with Mermaid diagrams +- [Vibe Metrics v2](./architecture/vibe-metrics-v2.md): Axis computation details +- [PRD: Vibed Narrative Layer](./PRD-vibed.md): Product requirements +- [PRD: Profile Aggregation](./PRD-profile-aggregation.md): Multi-repo aggregation + +--- + +*This document should be updated whenever analysis logic, personas, or metrics change.* diff --git a/docs/implementation-trackers/vibed.md b/docs/implementation-trackers/vibed.md index e3dd2a2..0504cbc 100644 --- a/docs/implementation-trackers/vibed.md +++ b/docs/implementation-trackers/vibed.md @@ -1,54 +1,64 @@ # Implementation Tracker: Vibed Narrative Layer ## Context -This tracker accompanies `docs/PRD-vibed.md`. It captures the discrete implementation tasks needed to move Bolokono from a metrics-heavy analysis tool to the Vibed narrative experience, covering schema, worker, API, UI, and sharing layers. +This tracker accompanies `docs/PRD-vibed.md`. It captures the discrete implementation tasks needed to move from a metrics-heavy analysis tool to the Vibed narrative experience, covering schema, worker, API, UI, and sharing layers. ## Tracker ### F1. Insights & Persona Schema **Task:** Design `analysis_insights` (and related persona signals) to capture deterministic Vibed outputs. **Deliverables:** -- [ ] Schema defined (longest streak, weekday/time window, chunkiness, persona tag + confidence, tech signals, share template). -- [ ] Migration file created for `analysis_insights`. -- [ ] Story for persona evolution + deltas captured (per-job history). +- [x] Schema defined (longest streak, weekday/time window, chunkiness, persona tag + confidence, tech signals, share template). +- [x] Migration file created for `analysis_insights` (`0005_add_analysis_insights.sql`, `0006_enrich_analysis_insights.sql`). +- [x] Vibe insights table added (`0010_add_vibe_insights.sql`). +- [x] Story for persona evolution + deltas captured (per-job history via `vibe_insights`). **Blocks:** F2 ### F2. Worker & Narrative Pipeline **Task:** Extend the worker to compute `insights_json`, persist to `analysis_insights`, and feed narratives. **Deliverables:** -- [ ] Insight computation logic implemented + unit tested (streak, persona, chunkiness, tech signal detection). -- [ ] `analysis_insights` row inserted per job with persona/confidence + evidence. -- [ ] Updated Narrative LLM prompt referencing insights explicitly. -- [ ] Share-card payload generation (colors/text template) included in worker output. +- [x] Insight computation logic implemented + unit tested (streak, persona, chunkiness, tech signal detection) in `@vibed/core`. +- [x] `vibe_insights` row inserted per job with persona/confidence + evidence via Inngest function. +- [x] Updated Narrative LLM prompt referencing insights explicitly. +- [x] Share-card payload generation (colors/text template) included in worker output. **Depends on:** F1 **Blocks:** F3, F4 ### F3. API + Supabase Access **Task:** Expose insights/share data through REST endpoints and Supabase RLS. **Deliverables:** -- [ ] `GET /api/analysis/:jobId/insights` returns `analysis_insights` + key metrics. -- [ ] `GET /api/analysis/:jobId/report` includes persona badge, confidence, and share template. -- [ ] (Optional) `POST /api/analysis/:jobId/share` triggers share card export or return template data. -- [ ] Supabase policies mirror `analysis_reports` access. +- [x] API endpoints expose insights + key metrics (`/api/analysis/[id]/*`). +- [x] Report includes persona badge, confidence, and share template. +- [x] Supabase policies mirror `analysis_reports` access. **Depends on:** F2 **Blocks:** F4 ### F4. Vibed UX/UI + Share Experience -**Task:** Build the new hero/data UI plus “Show me the data” deep dive and share cards. +**Task:** Build the new hero/data UI plus "Show me the data" deep dive and share cards. **Deliverables:** -- [ ] Vibed highlights layer (streak, rhythms, chunkiness, persona archetype line, confidence text). -- [ ] Share section offering PNG/SVG export plus copyable summary, reusing precomputed templates. -- [ ] Persona timeline/history component showing persona evolution per job/repo. -- [ ] Deep dive panel linking to raw metrics/narrative/evidence and persona signals. -- [ ] Light theme/sticker aesthetic consistent with Vibed marketing. +- [x] Vibed highlights layer (streak, rhythms, chunkiness, persona archetype line, confidence text). +- [x] Persona timeline/history component showing persona evolution per job/repo. +- [x] Deep dive panel linking to raw metrics/narrative/evidence and persona signals. +- [ ] Share section offering PNG/SVG export plus copyable summary (partially implemented). +- [ ] Light theme/sticker aesthetic consistent with Vibed marketing (ongoing polish). **Depends on:** F3 ### F5. Reflection & Testing **Task:** Ensure deterministic insight generation, persona detection, and share assets behave as expected. **Deliverables:** -- [ ] Unit tests covering streak, persona classification outcomes, and chunkiness thresholds. +- [x] Unit tests covering streak, persona classification outcomes, and chunkiness thresholds (31+ tests in `@vibed/core`). - [ ] Visual regression or storybook story for share cards. -- [ ] Documented confidence thresholds + persona hints for designers. -- [ ] Workflow documentation updated (`docs/research` links, PRD updates). +- [x] Documented confidence thresholds + persona hints in `vibe.ts`. +- [x] Workflow documentation updated (`docs/research` links, PRD updates). **Depends on:** F2, F4 +## Progress Summary + +| Phase | Status | Notes | +|-------|--------|-------| +| F1. Insights & Persona Schema | ✅ Complete | Multiple migrations applied | +| F2. Worker & Narrative Pipeline | ✅ Complete | Inngest + fallback worker | +| F3. API + Supabase Access | ✅ Complete | RLS policies in place | +| F4. Vibed UX/UI + Share Experience | 🟡 Mostly Complete | Share export needs polish | +| F5. Reflection & Testing | 🟡 Mostly Complete | Visual regression pending | + diff --git a/docs/prd/multi-agent-detection.md b/docs/prd/multi-agent-detection.md index 6a2cdcf..a4e0c11 100644 --- a/docs/prd/multi-agent-detection.md +++ b/docs/prd/multi-agent-detection.md @@ -2,7 +2,7 @@ ## Overview -**Status:** Planning +**Status:** Implemented (Phases 1-5 complete) **Priority:** High **Owner:** TBD **Created:** January 2026 diff --git a/docs/research/ai-era-coding-personas.md b/docs/research/ai-era-coding-personas.md index 243676b..7e9b16e 100644 --- a/docs/research/ai-era-coding-personas.md +++ b/docs/research/ai-era-coding-personas.md @@ -42,7 +42,16 @@ The UI is doing interpretation work. This holds short-term but should move to th ## Research Sources -Primary sources include Addy Osmani’s AI workflows, Cursor 2.0 docs, GitClear 2024 research, academic TDD benchmarks (arxiv 2025), Axur Engineering, humanwhocodes.com persona guides, Cursor/Replit/Qodo tooling guides, GitHub Copilot materials, and developer persona frameworks. +Primary sources: + +- [Addy Osmani: The Future of Agentic Coding](https://addyosmani.com/blog/future-agentic-coding/) — Orchestrator vs conductor patterns +- [Andrej Karpathy's "Vibe Coding" tweet](https://x.com/karpathy/status/1886192184808149383) — Origin of the term (Feb 2025) +- [GitClear 2024 Code Quality Report](https://www.gitclear.com/) — Code churn and developer productivity metrics +- [GitHub Copilot Documentation](https://docs.github.com/en/copilot) — Co-author attribution, agent workflows +- [Cursor Documentation](https://cursor.com/docs) — AI-assisted development patterns +- [Vibe Coding Wikipedia](https://en.wikipedia.org/wiki/Vibe_coding) — Collins Dictionary Word of the Year 2025 +- Academic TDD benchmarks (arxiv 2025) +- Developer persona frameworks from humanwhocodes.com ## Final Thought From 7ec206fa56c9502029d49885d13363b29c3f869e Mon Sep 17 00:00:00 2001 From: Abou Kone Date: Wed, 21 Jan 2026 12:45:33 -0500 Subject: [PATCH 2/5] docs: update terminology in landing page and documentation Update text to clarify "vibe coding" vs "AI Assisted Engineering" terminology Make language more approachable for non-technical audiences --- apps/web/src/app/page.tsx | 6 ++++++ docs/how-vibed-works.md | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 4aec475..c2885f8 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -779,6 +779,12 @@ function MarketingLanding() { better. We surface signals from your commit history to shine a light on how you build with AI. What feels like you, what feels new, and how your workflow is evolving.

+

+ The term “vibe coding” can be polarizing, but it captures the cultural moment around + AI-shaped development. We personally prefer “AI Assisted Engineering” because it’s + more explicit about the role of AI. This is a playful side project, so we keep it + light and approachable for non-technical folks. +

diff --git a/docs/how-vibed-works.md b/docs/how-vibed-works.md index 046292a..5f93ca3 100644 --- a/docs/how-vibed-works.md +++ b/docs/how-vibed-works.md @@ -3,7 +3,9 @@ > **Last Updated:** January 2026 > **Maintainer:** Update this document when analysis logic, personas, or metrics change. -Vibed Coding analyzes your git history to reveal your coding style and patterns. Think of it as "Spotify Wrapped for vibe coders." We look at *how* you build, not *what* you build. +Vibed Coding analyzes your git history to reveal your vibe coding/ AI Assited Engineering style and patterns. Think of it as "Spotify Wrapped for vibe coders." We look at *how* you build, not *what* you build. + +Note: The term "vibe coding" can be polarizing, but it captures the cultural moment around AI-shaped development. We personally prefer "AI Assisted Engineering" because it’s more explicit about the role of AI. This is a playful side project, so we lean into the pop framing and keep it approachable for non-technical folks. --- From 411ba506074e2d35c5ec0c07aff8f824a86ef96b Mon Sep 17 00:00:00 2001 From: Abou Kone Date: Wed, 21 Jan 2026 16:37:38 -0500 Subject: [PATCH 3/5] feat: enhance profile sharing and versioning features - Introduced ProfileShareSection and ProfileVersionSelector components for improved user experience in sharing profiles. - Updated user profile schema to include narrative and LLM model information. - Enhanced admin job management to track profile updates and personas. - Added share functionality for profiles, including metrics and insights display. - Documented LLM narrative workflow and share experience improvements for better clarity and usability. --- apps/web/src/app/admin/actions.ts | 46 +- apps/web/src/app/admin/jobs/page.tsx | 15 + .../app/analysis/[jobId]/AnalysisClient.tsx | 775 ++++-------------- apps/web/src/app/page.tsx | 188 ++++- .../src/components/ProfileVersionSelector.tsx | 165 ++++ .../components/share/ProfileShareSection.tsx | 133 +++ .../web/src/components/share/ShareActions.tsx | 291 +++++++ apps/web/src/components/share/ShareCard.tsx | 93 +++ apps/web/src/components/share/index.ts | 18 + apps/web/src/components/share/share-image.ts | 242 ++++++ apps/web/src/components/share/types.ts | 83 ++ docs/architecture/llm-narrative-workflow.md | 391 +++++++++ docs/prd/share-experience-improvements.md | 617 ++++++++++++++ packages/db/src/database.types.ts | 21 + 14 files changed, 2405 insertions(+), 673 deletions(-) create mode 100644 apps/web/src/components/ProfileVersionSelector.tsx create mode 100644 apps/web/src/components/share/ProfileShareSection.tsx create mode 100644 apps/web/src/components/share/ShareActions.tsx create mode 100644 apps/web/src/components/share/ShareCard.tsx create mode 100644 apps/web/src/components/share/index.ts create mode 100644 apps/web/src/components/share/share-image.ts create mode 100644 apps/web/src/components/share/types.ts create mode 100644 docs/architecture/llm-narrative-workflow.md create mode 100644 docs/prd/share-experience-improvements.md diff --git a/apps/web/src/app/admin/actions.ts b/apps/web/src/app/admin/actions.ts index b79a164..458fe39 100644 --- a/apps/web/src/app/admin/actions.ts +++ b/apps/web/src/app/admin/actions.ts @@ -521,6 +521,8 @@ export async function getAllJobs( commit_count: number | null; created_at: string; completed_at: string | null; + profile_updated: boolean; + profile_persona: string | null; }>; total: number; }> { @@ -576,27 +578,49 @@ export async function getAllJobs( // Get user and repo names const userIds = [...new Set((jobs ?? []).map((j) => j.user_id))]; const repoIds = [...new Set((jobs ?? []).map((j) => j.repo_id).filter(Boolean))]; + const jobIds = (jobs ?? []).map((j) => j.id); - const [{ data: users }, { data: repos }] = await Promise.all([ + const [{ data: users }, { data: repos }, { data: profileHistory }] = await Promise.all([ adminSupabase.from("users").select("id, github_username").in("id", userIds), adminSupabase.from("repos").select("id, full_name").in("id", repoIds), + // Get profile history entries triggered by these jobs + adminSupabase + .from("user_profile_history") + .select("trigger_job_id, profile_snapshot") + .in("trigger_job_id", jobIds), ]); const usernameById = new Map((users ?? []).map((u) => [u.id, u.github_username])); const repoNameById = new Map((repos ?? []).map((r) => [r.id, r.full_name])); + // Map job ID to profile update info + const profileUpdateByJobId = new Map(); + for (const ph of profileHistory ?? []) { + if (ph.trigger_job_id) { + const snapshot = ph.profile_snapshot as { persona?: { name?: string } } | null; + profileUpdateByJobId.set(ph.trigger_job_id, { + persona: snapshot?.persona?.name ?? null, + }); + } + } + return { success: true, - jobs: (jobs ?? []).map((j) => ({ - id: j.id, - user_id: j.user_id, - username: usernameById.get(j.user_id) ?? null, - repo_name: repoNameById.get(j.repo_id ?? "") ?? null, - status: j.status, - commit_count: j.commit_count, - created_at: j.created_at, - completed_at: j.completed_at, - })), + jobs: (jobs ?? []).map((j) => { + const profileUpdate = profileUpdateByJobId.get(j.id); + return { + id: j.id, + user_id: j.user_id, + username: usernameById.get(j.user_id) ?? null, + repo_name: repoNameById.get(j.repo_id ?? "") ?? null, + status: j.status, + commit_count: j.commit_count, + created_at: j.created_at, + completed_at: j.completed_at, + profile_updated: Boolean(profileUpdate), + profile_persona: profileUpdate?.persona ?? null, + }; + }), total: totalCount ?? 0, }; } diff --git a/apps/web/src/app/admin/jobs/page.tsx b/apps/web/src/app/admin/jobs/page.tsx index 3a28461..d4042a2 100644 --- a/apps/web/src/app/admin/jobs/page.tsx +++ b/apps/web/src/app/admin/jobs/page.tsx @@ -88,6 +88,7 @@ export default async function AdminJobsPage({ Repo Status Commits + Profile Created Completed @@ -112,6 +113,20 @@ export default async function AdminJobsPage({ {job.commit_count?.toLocaleString() ?? "-"} + + {job.profile_updated ? ( + + + {job.profile_persona ? ( + Updated + ) : ( + "Updated" + )} + + ) : ( + + )} + {new Date(job.created_at).toLocaleString()} diff --git a/apps/web/src/app/analysis/[jobId]/AnalysisClient.tsx b/apps/web/src/app/analysis/[jobId]/AnalysisClient.tsx index 25eccc8..ceba600 100644 --- a/apps/web/src/app/analysis/[jobId]/AnalysisClient.tsx +++ b/apps/web/src/app/analysis/[jobId]/AnalysisClient.tsx @@ -5,50 +5,14 @@ import { useEffect, useMemo, useState } from "react"; import { computeAnalysisInsights } from "@vibed/core"; import type { AnalysisInsights, AnalysisMetrics, CommitEvent } from "@vibed/core"; import { formatMetricLabel, formatMetricValue } from "@/lib/format-labels"; -import { - Copy, - Check, - Link2, - Share2, - Download, - ChevronDown, -} from "lucide-react"; - -// Brand icons (Lucide doesn't include these) -const XIcon = ({ size = 16 }: { size?: number }) => ( - - - -); - -const FacebookIcon = ({ size = 16 }: { size?: number }) => ( - - - -); - -const LinkedInIcon = ({ size = 16 }: { size?: number }) => ( - - - -); - -const RedditIcon = ({ size = 16 }: { size?: number }) => ( - - - -); - -const WhatsAppIcon = ({ size = 16 }: { size?: number }) => ( - - - -); +import { ShareCard, ShareActions } from "@/components/share"; +import type { ShareImageTemplate, ShareCardMetric } from "@/components/share"; type Job = { id: string; status: string; error_message: string | null; + repo_id?: string; created_at: string; started_at: string | null; completed_at: string | null; @@ -126,10 +90,16 @@ type InsightHistoryEntry = { job_id: string; status: string; created_at: string; + repo_id?: string; persona_label?: string; persona_confidence?: string; share_template?: AnalysisInsights["share_template"]; generated_at?: string; + analysis_jobs?: { + status: string; + created_at: string; + repo_id: string | null; + }; }; function isRecord(v: unknown): v is Record { @@ -378,195 +348,6 @@ function fmtDate(iso: string | null): string { return d.toLocaleString(); } -function escapeXml(value: string): string { - return value - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); -} - -type ShareFormat = "og" | "square" | "story"; - -const SHARE_FORMATS: Record< - ShareFormat, - { - label: string; - width: number; - height: number; - pad: number; - headlineSize: number; - subheadSize: number; - metricsSize: number; - metaSize: number; - watermarkSize: number; - cardRadius: number; - } -> = { - og: { - label: "OpenGraph 1200×630", - width: 1200, - height: 630, - pad: 80, - headlineSize: 64, - subheadSize: 28, - metricsSize: 24, - metaSize: 18, - watermarkSize: 18, - cardRadius: 48, - }, - square: { - label: "Square 1080×1080", - width: 1080, - height: 1080, - pad: 88, - headlineSize: 72, - subheadSize: 32, - metricsSize: 28, - metaSize: 20, - watermarkSize: 20, - cardRadius: 64, - }, - story: { - label: "Story 1080×1920", - width: 1080, - height: 1920, - pad: 96, - headlineSize: 84, - subheadSize: 36, - metricsSize: 30, - metaSize: 22, - watermarkSize: 22, - cardRadius: 64, - }, -}; - -function wrapTextLines( - text: string, - maxCharsPerLine: number, - maxLines: number -): { lines: string[]; truncated: boolean } { - const words = text.trim().split(/\s+/).filter(Boolean); - const lines: string[] = []; - let current = ""; - - for (const word of words) { - const next = current ? `${current} ${word}` : word; - if (next.length <= maxCharsPerLine) { - current = next; - continue; - } - - if (current) { - lines.push(current); - current = word; - if (lines.length >= maxLines) { - return { lines, truncated: true }; - } - continue; - } - - lines.push(word.slice(0, Math.max(1, maxCharsPerLine))); - if (lines.length >= maxLines) { - return { lines, truncated: true }; - } - current = ""; - } - - if (current) lines.push(current); - - if (lines.length > maxLines) { - return { lines: lines.slice(0, maxLines), truncated: true }; - } - - return { lines, truncated: false }; -} - -function withEllipsis(text: string): string { - const t = text.trim(); - return t.length === 0 ? t : `${t}…`; -} - -function createShareSvg(template: AnalysisInsights["share_template"], format: ShareFormat): string { - const cfg = SHARE_FORMATS[format]; - const metricsText = template.metrics - .slice(0, 3) - .map((metric) => `${metric.label}: ${metric.value}`) - .join(" · "); - const archetypes = template.persona_archetype.archetypes.slice(0, 3).join(", "); - - const headlineChars = format === "og" ? 26 : format === "square" ? 22 : 18; - const subheadChars = format === "og" ? 46 : format === "square" ? 36 : 30; - const metricsChars = format === "og" ? 72 : format === "square" ? 60 : 46; - - const headlineWrapped = wrapTextLines(template.headline, headlineChars, 2); - const subheadWrapped = wrapTextLines(template.subhead, subheadChars, 2); - const metricsWrapped = wrapTextLines(metricsText, metricsChars, format === "story" ? 2 : 1); - - const headlineLines = headlineWrapped.lines.map((l, idx) => - escapeXml(idx === headlineWrapped.lines.length - 1 && headlineWrapped.truncated ? withEllipsis(l) : l) - ); - const subheadLines = subheadWrapped.lines.map((l, idx) => - escapeXml(idx === subheadWrapped.lines.length - 1 && subheadWrapped.truncated ? withEllipsis(l) : l) - ); - const metricsLines = metricsWrapped.lines.map((l, idx) => - escapeXml(idx === metricsWrapped.lines.length - 1 && metricsWrapped.truncated ? withEllipsis(l) : l) - ); - - const startY = - format === "story" ? Math.round(cfg.height * 0.34) : cfg.pad + Math.round(cfg.headlineSize * 0.9); - - const headlineY = startY; - const subheadY = headlineY + Math.round(cfg.headlineSize * 0.95) + 18; - const metricsY = subheadY + Math.round(cfg.subheadSize * 0.95) + 18; - const metaY = - format === "story" ? Math.round(cfg.height * 0.72) : metricsY + Math.round(cfg.metricsSize * 0.95) + 28; - const hashY = metaY + Math.round(cfg.metaSize * 1.6); - const watermarkY = cfg.height - Math.round(cfg.pad * 0.6); - - const x = cfg.pad; - const watermarkX = cfg.width - cfg.pad; - - const headlineTspans = headlineLines - .map((line, idx) => `${line}`) - .join(""); - const subheadTspans = subheadLines - .map((line, idx) => `${line}`) - .join(""); - const metricsTspans = metricsLines - .map((line, idx) => `${line}`) - .join(""); - - return ` - - - - - - - - - - ${headlineTspans} - - - ${subheadTspans} - - - ${metricsTspans} - - - Persona: ${escapeXml(template.persona_archetype.label)} · ${escapeXml(archetypes)} - - - #Vibed - - - vibed.coding - -`; -} function weekdayName(dow: number): string { return ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][dow] ?? "—"; @@ -578,13 +359,7 @@ export default function AnalysisClient({ jobId }: { jobId: string }) { const [history, setHistory] = useState([]); const [historyLoading, setHistoryLoading] = useState(false); const [historyError, setHistoryError] = useState(null); - const [copied, setCopied] = useState(false); - const [copiedLink, setCopiedLink] = useState(false); - const [shareFormat, setShareFormat] = useState("og"); - const [shareDownloadError, setShareDownloadError] = useState(null); - const [downloadingShare, setDownloadingShare] = useState(false); const [shareOrigin, setShareOrigin] = useState(""); - const [supportsNativeShare, setSupportsNativeShare] = useState(false); const [regeneratingStory, setRegeneratingStory] = useState(false); const [regenerateStoryError, setRegenerateStoryError] = useState(null); const [regenerateStoryMeta, setRegenerateStoryMeta] = useState(null); @@ -595,7 +370,6 @@ export default function AnalysisClient({ jobId }: { jobId: string }) { useEffect(() => { setShareOrigin(window.location.origin); - setSupportsNativeShare(typeof navigator !== "undefined" && "share" in navigator); }, []); useEffect(() => { @@ -679,143 +453,49 @@ export default function AnalysisClient({ jobId }: { jobId: string }) { return `${shareTemplate.headline} — ${shareTemplate.subhead}\n${metricsLine}\n#Vibed`; }, [shareTemplate]); - const handleCopyShare = async () => { - try { - await navigator.clipboard.writeText(shareText); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - } catch { - setCopied(false); - } - }; - - const handleCopyLink = async () => { - if (!shareUrl) return; - try { - await navigator.clipboard.writeText(shareUrl); - setCopiedLink(true); - setTimeout(() => setCopiedLink(false), 2000); - } catch { - setCopiedLink(false); - } - }; - - const openSharePopup = (url: string) => { - window.open(url, "_blank", "noopener,noreferrer,width=800,height=600"); - }; - - const handleShareTwitter = () => { - if (!shareUrl) return; - const u = new URL("https://twitter.com/intent/tweet"); - u.searchParams.set("text", shareCaption); - u.searchParams.set("url", shareUrl); - openSharePopup(u.toString()); - }; - - const handleShareFacebook = () => { - if (!shareUrl) return; - const u = new URL("https://www.facebook.com/sharer/sharer.php"); - u.searchParams.set("u", shareUrl); - if (shareCaption) u.searchParams.set("quote", shareCaption); - openSharePopup(u.toString()); - }; - - const handleShareLinkedIn = () => { - if (!shareUrl) return; - const u = new URL("https://www.linkedin.com/sharing/share-offsite/"); - u.searchParams.set("url", shareUrl); - openSharePopup(u.toString()); - }; - - const handleShareReddit = () => { - if (!shareUrl) return; - const u = new URL("https://www.reddit.com/submit"); - u.searchParams.set("url", shareUrl); - if (shareTemplate?.headline) u.searchParams.set("title", shareTemplate.headline); - openSharePopup(u.toString()); - }; - - const handleShareWhatsApp = () => { - if (!shareUrl) return; - const u = new URL("https://wa.me/"); - u.searchParams.set("text", `${shareCaption}\n${shareUrl}`.trim()); - openSharePopup(u.toString()); - }; - - const handleNativeShare = async () => { - if (!shareUrl || !shareTemplate) return; - if (!("share" in navigator)) return; - try { - await navigator.share({ - title: shareTemplate.headline, - text: shareCaption, - url: shareUrl, + // Build share template for ShareActions component + const shareImageTemplate: ShareImageTemplate | null = shareTemplate + ? { + colors: shareTemplate.colors, + headline: shareTemplate.headline, + subhead: shareTemplate.subhead, + metrics: shareTemplate.metrics, + persona_archetype: shareTemplate.persona_archetype, + } + : null; + + // Build metrics for ShareCard + const shareCardMetrics: ShareCardMetric[] = useMemo(() => { + const metrics: ShareCardMetric[] = []; + metrics.push({ + label: "Longest Streak", + value: `${wrapped.streak.longest_days} day${wrapped.streak.longest_days === 1 ? "" : "s"}`, + }); + if (wrapped.timing.peak_window) { + metrics.push({ + label: "Peak Window", + value: formatMetricLabel(wrapped.timing.peak_window), }); - } catch { - // no-op } - }; - - const downloadBlob = (blob: Blob, filename: string) => { - const url = URL.createObjectURL(blob); - const link = document.createElement("a"); - link.href = url; - link.download = filename; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - URL.revokeObjectURL(url); - }; - - const handleDownloadShareSvg = (format: ShareFormat) => { - if (!shareTemplate) return; - setShareDownloadError(null); - const svg = createShareSvg(shareTemplate, format); - const blob = new Blob([svg], { type: "image/svg+xml;charset=utf-8" }); - downloadBlob(blob, `vibed-coding-${jobId}-${format}.svg`); - }; - - const handleDownloadSharePng = async (format: ShareFormat) => { - if (!shareTemplate) return; - setShareDownloadError(null); - setDownloadingShare(true); - let svgUrl: string | null = null; - try { - const svg = createShareSvg(shareTemplate, format); - const cfg = SHARE_FORMATS[format]; - - const svgBlob = new Blob([svg], { type: "image/svg+xml;charset=utf-8" }); - svgUrl = URL.createObjectURL(svgBlob); - - const img = new Image(); - const loaded = new Promise((resolve, reject) => { - img.onload = () => resolve(); - img.onerror = () => reject(new Error("image_load_failed")); + metrics.push({ + label: "Commit Style", + value: wrapped.chunkiness.label ?? "—", + }); + if (wrapped.commits.features_per_fix !== null) { + metrics.push({ + label: "Feature / Fix", + value: `${formatMetricValue(wrapped.commits.features_per_fix, 1)} : 1`, }); - img.src = svgUrl; - await loaded; - - const scale = 2; - const canvas = document.createElement("canvas"); - canvas.width = cfg.width * scale; - canvas.height = cfg.height * scale; - const ctx = canvas.getContext("2d"); - if (!ctx) throw new Error("canvas_unsupported"); - ctx.setTransform(scale, 0, 0, scale, 0, 0); - ctx.drawImage(img, 0, 0, cfg.width, cfg.height); - - const pngBlob = await new Promise((resolve, reject) => { - canvas.toBlob((b) => (b ? resolve(b) : reject(new Error("png_encode_failed"))), "image/png"); + } else if (wrapped.commits.fixes_per_feature !== null) { + metrics.push({ + label: "Feature / Fix", + value: `1 : ${formatMetricValue(wrapped.commits.fixes_per_feature, 1)}`, }); - - downloadBlob(pngBlob, `vibed-coding-${jobId}-${format}.png`); - } catch (e) { - setShareDownloadError(e instanceof Error ? e.message : "download_failed"); - } finally { - if (svgUrl) URL.revokeObjectURL(svgUrl); - setDownloadingShare(false); + } else { + metrics.push({ label: "Feature / Fix", value: "Balanced" }); } - }; + return metrics; + }, [wrapped]); const triggerProfileRebuild = async () => { setRebuildingProfile(true); @@ -956,6 +636,16 @@ export default function AnalysisClient({ jobId }: { jobId: string }) { }; }, [jobStatus]); + // Filter history to show only runs for the same repo + const currentRepoId = data?.job.repo_id; + const repoHistory = useMemo(() => { + if (!currentRepoId) return []; + return history.filter((entry) => { + const entryRepoId = entry.analysis_jobs?.repo_id ?? entry.repo_id; + return entryRepoId === currentRepoId; + }); + }, [history, currentRepoId]); + if (error) return

{error}

; if (!data) return

Loading…

; @@ -1066,39 +756,105 @@ export default function AnalysisClient({ jobId }: { jobId: string }) { {job.status === "done" && events.length > 0 ? ( <> + {/* HERO: Share Card - The main action users should take */} + {shareTemplate ? ( +
+ + +
+ ) : null} + + {/* Version Selector - Switch between runs of this repo */} + {repoHistory.length > 1 ? ( +
+
+ Version: + +
+ + {repoHistory.length} runs of this repo + +
+ ) : null} + + {/* Navigation and archetypes */} +
+ + My Vibed profile + +
+ {persona.archetypes.map((arch) => ( + + {arch} + + ))} +
+
+ + {/* Detailed Analysis Card */}
-
-
-

Your vibe

-

- {persona.label} -

-

{persona.description}

- {narrative?.summary ? ( -

{narrative.summary}

- ) : null} -
- -
- - My Vibed profile - -
-
-
- {persona.archetypes.map((arch) => ( - - {arch} - - ))} +
+

Your vibe

+

+ {persona.label} +

+

{persona.description}

+ {narrative?.summary ? ( +

{narrative.summary}

+ ) : null}
@@ -1428,232 +1184,9 @@ export default function AnalysisClient({ jobId }: { jobId: string }) {
- Share, timeline, and details + Timeline and details
- {shareTemplate ? ( -
- {/* Share Card Preview */} -
-
- {/* Header */} -
-
-

- My Vibe Coding Style -

-

- {shareTemplate.headline} -

-

- {shareTemplate.subhead} -

-
-
- {data?.userAvatarUrl ? ( - User avatar - ) : ( -
- V -
- )} -
-
- - {/* Vibe Metrics Grid */} -
- {/* Streak */} -
-

- Longest Streak -

-

- {wrapped.streak.longest_days} day{wrapped.streak.longest_days === 1 ? "" : "s"} -

-
- {/* Peak Window */} -
-

- Peak Window -

-

- {wrapped.timing.peak_window ? formatMetricLabel(wrapped.timing.peak_window) : "—"} -

-
- {/* Commit Style */} -
-

- Commit Style -

-

- {wrapped.chunkiness.label ?? "—"} -

-
- {/* Build/Fix Ratio */} -
-

- Feature / Fix -

-

- {wrapped.commits.features_per_fix !== null - ? `${formatMetricValue(wrapped.commits.features_per_fix, 1)} : 1` - : wrapped.commits.fixes_per_feature !== null - ? `1 : ${formatMetricValue(wrapped.commits.fixes_per_feature, 1)}` - : "Balanced"} -

-
-
- - {/* Footer */} -
-

- {shareOrigin ? new URL(shareOrigin).hostname : "vibed.dev"} -

-

- {wrapped.totals.commits} commits{metricsJson?.active_days ? ` · ${metricsJson.active_days} active days` : ""} -

-
-
- {shareDownloadError ? ( -
-

Download failed

-
- ) : null} -
- - {/* Action Buttons - Consolidated */} -
- {/* Download Section */} -
- - - -
- - {/* Copy Section */} -
- - -
- - {/* Social Share Section */} -
- - - - - -
- - {/* Native Share Button */} - {supportsNativeShare ? ( - - ) : null} -
-
- ) : null} - {history.length > 1 ? (
diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index c2885f8..43523ca 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -3,6 +3,8 @@ import { createSupabaseServerClient } from "@/lib/supabase/server"; import type { Database } from "@vibed/db"; import { wrappedTheme } from "@/lib/theme"; import { formatMatchedRule, AXIS_LEGEND } from "@/lib/format-labels"; +import { ProfileShareSection } from "@/components/share"; +import { ProfileVersionSelector } from "@/components/ProfileVersionSelector"; import { createClient } from "@supabase/supabase-js"; import { aggregateUserProfile, @@ -14,17 +16,43 @@ import { } from "@vibed/core"; const heroFeatures = [ - "A Vibed profile built from vibe-coding signals in your commit history", + "A Vibed profile built from AI-assisted engineering signals in your commit history", "Persona snapshots that evolve as you add more repos", - "Share-ready cards with playful language and honest confidence", + "Share-ready cards that highlight your vibe coding patterns", "Deep dive metrics and evidence when you want receipts", ]; const timeline = [ - { title: "Connect GitHub", description: "Sign in, then pick the repos that feel like you." }, - { title: "Run a vibe check", description: "We read commit metadata and patterns (not your code)." }, - { title: "Get your Vibed read", description: "Highlights, categories, and insights into how you build." }, - { title: "See your persona", description: "A playful archetype that changes as your work evolves." }, + { + title: "Fetch commit metadata", + description: + "We sample up to 300 commits across your repo's lifetime (timestamps, sizes, file paths). No code content.", + }, + { + title: "Filter automation noise", + description: + "Bots like Dependabot or release workflows are filtered out so the signal reflects your work.", + }, + { + title: "Compute metrics + axes", + description: + "25+ metrics roll up into six Vibe Axes (planning, guardrails, rhythm, surface area, and more).", + }, + { + title: "Detect persona + insights", + description: + "We map your axes to a persona and generate streaks, timing, and workflow highlights.", + }, + { + title: "Optional LLM narrative", + description: + "If you opt in, we summarize patterns using metadata only—never code or message content.", + }, + { + title: "Aggregate your profile", + description: + "Multiple repos roll into a single Vibed profile, weighted by commit volume.", + }, ]; type AuthStats = { @@ -49,6 +77,7 @@ type AuthStats = { generatedAt: string | null; }; userProfile?: { + personaId: string; personaName: string; personaTagline: string | null; personaConfidence: string; @@ -62,6 +91,13 @@ type AuthStats = { commitCount: number; }>; updatedAt: string | null; + narrative?: { + headline?: string; + paragraphs?: string[]; + highlights?: string[]; + } | null; + llmModel?: string | null; + llmKeySource?: string | null; }; }; @@ -437,7 +473,7 @@ export default async function Home({ supabase .from("user_profiles") .select( - "persona_name, persona_tagline, persona_confidence, total_repos, total_commits, axes_json, repo_personas_json, updated_at, job_ids" + "persona_id, persona_name, persona_tagline, persona_confidence, total_repos, total_commits, axes_json, repo_personas_json, updated_at, job_ids, narrative_json, llm_model, llm_key_source" ) .eq("user_id", user.id) .maybeSingle(), @@ -452,6 +488,7 @@ export default async function Home({ }>; const maybeProfileData = userProfileResult.data as { + persona_id: string; persona_name: string; persona_tagline: string | null; persona_confidence: string; @@ -466,6 +503,13 @@ export default async function Home({ }>; updated_at: string | null; job_ids: unknown; + narrative_json: { + headline?: string; + paragraphs?: string[]; + highlights?: string[]; + } | null; + llm_model: string | null; + llm_key_source: string | null; } | null; const profileJobIds = Array.isArray(maybeProfileData?.job_ids) @@ -531,13 +575,14 @@ export default async function Home({ ? await supabase .from("user_profiles") .select( - "persona_name, persona_tagline, persona_confidence, total_repos, total_commits, axes_json, repo_personas_json, updated_at" + "persona_id, persona_name, persona_tagline, persona_confidence, total_repos, total_commits, axes_json, repo_personas_json, updated_at, narrative_json, llm_model, llm_key_source" ) .eq("user_id", user.id) .maybeSingle() : null; const userProfileData = (profileIsStale ? refreshedProfileResult?.data ?? null : maybeProfileData) as { + persona_id: string; persona_name: string; persona_tagline: string | null; persona_confidence: string; @@ -551,6 +596,13 @@ export default async function Home({ commitCount: number; }>; updated_at: string | null; + narrative_json: { + headline?: string; + paragraphs?: string[]; + highlights?: string[]; + } | null; + llm_model: string | null; + llm_key_source: string | null; } | null; const recentDoneJobIds = recentDoneJobs.map((job) => job.id); @@ -643,6 +695,7 @@ export default async function Home({ : undefined, userProfile: userProfileData ? { + personaId: userProfileData.persona_id, personaName: userProfileData.persona_name, personaTagline: userProfileData.persona_tagline, personaConfidence: userProfileData.persona_confidence, @@ -651,6 +704,9 @@ export default async function Home({ axes: userProfileData.axes_json ?? {}, repoPersonas: userProfileData.repo_personas_json ?? [], updatedAt: userProfileData.updated_at ?? null, + narrative: userProfileData.narrative_json ?? null, + llmModel: userProfileData.llm_model ?? null, + llmKeySource: userProfileData.llm_key_source ?? null, } : undefined, }; @@ -743,7 +799,7 @@ function MarketingLanding() { { title: "Spec-Driven Architect", description: - "Plans thoroughly before touching code; constraints show up early and often.", + "Plans thoroughly before shipping changes; constraints show up early and often.", }, { title: "Test-First Validator", @@ -771,13 +827,16 @@ function MarketingLanding() { For vibe coders

- Find your Vibed coding profile{" "} + + Find your Vibed AI-assisted engineering profile + {" "} and the personality behind your workflow

- Vibed is a playful experiment by vibe coders who want to understand themselves - better. We surface signals from your commit history to shine a light on how you build - with AI. What feels like you, what feels new, and how your workflow is evolving. + Vibed is a playful experiment by vibe coders who want to understand their AI-assisted + engineering patterns. We surface signals from your commit history to shine a light on + how you build with AI, what feels like you, what feels new, and how your workflow is + evolving.

The term “vibe coding” can be polarizing, but it captures the cultural moment around @@ -852,7 +911,7 @@ function MarketingLanding() {

Persona previews

You may see one of these (or another persona). These are lenses on your - vibe-coding style. Observations, not labels. + AI-assisted engineering (vibe-coding) style. Observations, not labels.

; - const cards = [ - { - label: "Reads captured", - value: stats.completedJobs, - helper: "Every run adds a chapter.", - }, - { - label: "Vibe shifts", - value: shiftValue, - helper: shiftHelper, - }, - { - label: "Most frequent vibe", - value: dominantPersona ?? "Still forming", - helper: - recentLabels.length === 0 - ? "Complete a vibe check to see your read." - : `Based on your last ${recentLabels.length} reads.`, - }, - ]; - function generateCrossRepoInsight(): string { if (!stats.userProfile) return "Add more repos to unlock cross-repo insights."; const repos = stats.userProfile.repoPersonas ?? []; @@ -1056,9 +1094,51 @@ function AuthenticatedDashboard({ return `Your ${stats.userProfile.personaName.toLowerCase()} profile emerges from ${repos.length} repos and ${stats.userProfile.totalCommits.toLocaleString()} commits.`; } + // Compute top axes for share card + const topAxes = stats.userProfile + ? Object.entries(stats.userProfile.axes) + .map(([key, val]) => ({ + name: axisMeta[key as keyof typeof axisMeta]?.name ?? key, + score: val.score, + })) + .sort((a, b) => b.score - a.score) + .slice(0, 4) + : []; + + // Use LLM narrative if available, otherwise fall back to deterministic insight + const narrativeFromLLM = stats.userProfile?.narrative; + const crossRepoInsight = narrativeFromLLM?.headline + ? narrativeFromLLM.headline + : generateCrossRepoInsight(); + + // Full narrative paragraphs (for expanded display) + const narrativeParagraphs = narrativeFromLLM?.paragraphs ?? []; + const narrativeHighlights = narrativeFromLLM?.highlights ?? []; + const hasLLMNarrative = Boolean(narrativeFromLLM?.headline || narrativeParagraphs.length > 0); + return (
-
+
+ {/* HERO: Share Card - The main action for users with a profile */} + {stats.userProfile ? ( + + ) : null} + + {/* Profile History - Version selector */} + {stats.userProfile ? ( + + ) : null} + {/* Unified Profile Card */}
{/* Section 1: Identity */} @@ -1142,12 +1222,38 @@ function AuthenticatedDashboard({ {/* Section 2: Insight (the juicy synthesis) */} {stats.userProfile ? (
-

- Insight -

+
+

+ Insight +

+ {hasLLMNarrative && stats.userProfile.llmModel ? ( + + AI-generated + + ) : null} +

- {generateCrossRepoInsight()} + {crossRepoInsight}

+ {narrativeParagraphs.length > 0 ? ( +
+ {narrativeParagraphs.map((paragraph, idx) => ( +

+ {paragraph} +

+ ))} +
+ ) : null} + {narrativeHighlights.length > 0 ? ( +
    + {narrativeHighlights.map((highlight, idx) => ( +
  • + + {highlight} +
  • + ))} +
+ ) : null}
) : null} @@ -1158,7 +1264,7 @@ function AuthenticatedDashboard({ Your Axes

- The 6 signals that define your vibe coding style + The 6 signals that define your AI-assisted engineering (vibe coding) style

diff --git a/apps/web/src/components/ProfileVersionSelector.tsx b/apps/web/src/components/ProfileVersionSelector.tsx new file mode 100644 index 0000000..9d26ac2 --- /dev/null +++ b/apps/web/src/components/ProfileVersionSelector.tsx @@ -0,0 +1,165 @@ +"use client"; + +import { useState, useEffect } from "react"; + +interface ProfileVersion { + id: string; + version: number; + createdAt: string; + triggerJobId: string | null; + triggerRepoName: string | null; + llmModel: string | null; + llmKeySource: string | null; + persona: { + id: string; + name: string; + tagline?: string; + } | null; + totalCommits: number; + totalRepos: number; +} + +interface ProfileVersionSelectorProps { + currentUpdatedAt: string | null; +} + +export function ProfileVersionSelector({ currentUpdatedAt }: ProfileVersionSelectorProps) { + const [versions, setVersions] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [expanded, setExpanded] = useState(false); + const [selectedVersion, setSelectedVersion] = useState(null); + + useEffect(() => { + if (!expanded) return; + + async function fetchVersions() { + setLoading(true); + setError(null); + try { + const res = await fetch("/api/profile/history", { cache: "no-store" }); + const data = (await res.json()) as { versions?: ProfileVersion[]; error?: string }; + if (!res.ok) { + setError(data.error ?? "Failed to load history"); + return; + } + setVersions(data.versions ?? []); + } catch { + setError("Failed to load history"); + } finally { + setLoading(false); + } + } + + fetchVersions(); + }, [expanded]); + + const formatDate = (dateStr: string) => { + const date = new Date(dateStr); + return date.toLocaleDateString(undefined, { + month: "short", + day: "numeric", + year: date.getFullYear() !== new Date().getFullYear() ? "numeric" : undefined, + hour: "numeric", + minute: "2-digit", + }); + }; + + return ( +
+ + + {expanded ? ( +
+ {loading ? ( +

Loading history...

+ ) : error ? ( +

{error}

+ ) : versions.length === 0 ? ( +

No history available yet.

+ ) : ( + <> +
+ View version: + +
+ + {selectedVersion ? ( +
+
+
+

+ {selectedVersion.persona?.name ?? "Unknown"} +

+ {selectedVersion.persona?.tagline ? ( +

+ {selectedVersion.persona.tagline} +

+ ) : null} +
+ {selectedVersion.llmModel ? ( + + AI: {selectedVersion.llmModel} + + ) : ( + + Non-LLM + + )} +
+
+ {selectedVersion.totalRepos} repos + {selectedVersion.totalCommits.toLocaleString()} commits + {selectedVersion.triggerRepoName ? ( + + Triggered by: {selectedVersion.triggerRepoName} + + ) : null} +
+

+ Version {selectedVersion.version} — {formatDate(selectedVersion.createdAt)} +

+
+ ) : null} + +

+ {versions.length} version{versions.length !== 1 ? "s" : ""} saved +

+ + )} +
+ ) : null} +
+ ); +} diff --git a/apps/web/src/components/share/ProfileShareSection.tsx b/apps/web/src/components/share/ProfileShareSection.tsx new file mode 100644 index 0000000..6d9b137 --- /dev/null +++ b/apps/web/src/components/share/ProfileShareSection.tsx @@ -0,0 +1,133 @@ +"use client"; + +import { useMemo, useSyncExternalStore } from "react"; +import { ShareCard, ShareActions } from "./index"; +import type { ShareCardMetric, ShareImageTemplate } from "./types"; + +// Use useSyncExternalStore to avoid SSR/CSR mismatch and comply with lint rules +function useOrigin(): string { + return useSyncExternalStore( + () => () => {}, + () => (typeof window !== "undefined" ? window.location.origin : ""), + () => "" + ); +} + +// Default persona colors for profile share cards +const PERSONA_COLORS: Record = { + "prompt_sprinter": { primary: "#c026d3", accent: "#7c3aed" }, + "guardrailed_viber": { primary: "#0891b2", accent: "#4f46e5" }, + "spec_first_director": { primary: "#4f46e5", accent: "#7c3aed" }, + "vertical_slice_shipper": { primary: "#7c3aed", accent: "#0ea5e9" }, + "fix_loop_hacker": { primary: "#dc2626", accent: "#f97316" }, + "toolsmith_viber": { primary: "#059669", accent: "#0891b2" }, + "infra_weaver": { primary: "#475569", accent: "#0891b2" }, + "rapid_risk_taker": { primary: "#f97316", accent: "#dc2626" }, + "balanced_builder": { primary: "#6366f1", accent: "#8b5cf6" }, + // Fallback for older persona IDs + "vibe-prototyper": { primary: "#c026d3", accent: "#7c3aed" }, + "test-validator": { primary: "#0891b2", accent: "#4f46e5" }, + "spec-architect": { primary: "#4f46e5", accent: "#7c3aed" }, + "agent-orchestrator": { primary: "#7c3aed", accent: "#0ea5e9" }, + "reflective-balancer": { primary: "#6366f1", accent: "#8b5cf6" }, +}; + +const DEFAULT_COLORS = { primary: "#c026d3", accent: "#06b6d4" }; + +interface ProfileShareSectionProps { + personaName: string; + personaId?: string; + personaTagline: string | null; + personaConfidence: string; + totalRepos: number; + totalCommits: number; + clarity: number; + topAxes: Array<{ + name: string; + score: number; + }>; + insight: string; +} + +export function ProfileShareSection({ + personaName, + personaId, + personaTagline, + personaConfidence, + totalRepos, + totalCommits, + clarity, + topAxes, + insight, +}: ProfileShareSectionProps) { + const shareOrigin = useOrigin(); + + const colors = PERSONA_COLORS[personaId ?? ""] ?? DEFAULT_COLORS; + + const shareCardMetrics: ShareCardMetric[] = useMemo(() => { + const metrics: ShareCardMetric[] = [ + { label: "Repos", value: String(totalRepos) }, + { label: "Commits", value: totalCommits.toLocaleString() }, + { label: "Clarity", value: `${clarity}%` }, + ]; + // Add top axis if available + if (topAxes.length > 0) { + metrics.push({ label: topAxes[0].name, value: String(topAxes[0].score) }); + } + return metrics; + }, [totalRepos, totalCommits, clarity, topAxes]); + + const shareUrl = useMemo(() => { + if (!shareOrigin) return ""; + return shareOrigin; // Profile is at root + }, [shareOrigin]); + + const shareText = useMemo(() => { + const metricsLine = `${totalRepos} repos · ${totalCommits.toLocaleString()} commits · ${clarity}% clarity`; + return `My Vibed Coding Profile: ${personaName}\n${personaTagline ?? ""}\n${metricsLine}\n#Vibed`; + }, [personaName, personaTagline, totalRepos, totalCommits, clarity]); + + const shareCaption = useMemo(() => { + return `My Vibed Coding Profile: ${personaName} — ${personaTagline ?? ""}. ${totalRepos} repos, ${totalCommits.toLocaleString()} commits. #Vibed`; + }, [personaName, personaTagline, totalRepos, totalCommits]); + + const shareImageTemplate: ShareImageTemplate = useMemo(() => { + return { + colors, + headline: `Vibed as ${personaName}`, + subhead: personaTagline ?? `${personaConfidence} confidence`, + metrics: shareCardMetrics, + persona_archetype: { + label: personaName, + archetypes: topAxes.slice(0, 3).map((a) => `${a.name}: ${a.score}`), + }, + }; + }, [personaName, personaTagline, personaConfidence, colors, shareCardMetrics, topAxes]); + + return ( +
+ + +
+ ); +} diff --git a/apps/web/src/components/share/ShareActions.tsx b/apps/web/src/components/share/ShareActions.tsx new file mode 100644 index 0000000..9455783 --- /dev/null +++ b/apps/web/src/components/share/ShareActions.tsx @@ -0,0 +1,291 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Copy, Check, Link2, Share2, Download } from "lucide-react"; +import { SHARE_FORMATS, downloadSharePng, downloadShareSvg } from "./share-image"; +import type { ShareActionsProps, ShareFormat } from "./types"; + +// Brand icons (Lucide doesn't include these) +const XIcon = ({ size = 16 }: { size?: number }) => ( + + + +); + +const LinkedInIcon = ({ size = 16 }: { size?: number }) => ( + + + +); + +const FacebookIcon = ({ size = 16 }: { size?: number }) => ( + + + +); + +const RedditIcon = ({ size = 16 }: { size?: number }) => ( + + + +); + +const WhatsAppIcon = ({ size = 16 }: { size?: number }) => ( + + + +); + +/** + * ShareActions - Reusable share action bar with copy, download, and social buttons + */ +export function ShareActions({ + shareUrl, + shareText, + shareCaption, + shareHeadline, + shareTemplate, + entityId, + disabled = false, +}: ShareActionsProps) { + const [copied, setCopied] = useState(false); + const [copiedLink, setCopiedLink] = useState(false); + const [shareFormat, setShareFormat] = useState("og"); + const [downloadError, setDownloadError] = useState(null); + const [downloading, setDownloading] = useState(false); + const [supportsNativeShare, setSupportsNativeShare] = useState(false); + + useEffect(() => { + setSupportsNativeShare(typeof navigator !== "undefined" && "share" in navigator); + }, []); + + const handleCopyText = async () => { + try { + await navigator.clipboard.writeText(shareText); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch { + setCopied(false); + } + }; + + const handleCopyLink = async () => { + if (!shareUrl) return; + try { + await navigator.clipboard.writeText(shareUrl); + setCopiedLink(true); + setTimeout(() => setCopiedLink(false), 2000); + } catch { + setCopiedLink(false); + } + }; + + const openSharePopup = (url: string) => { + window.open(url, "_blank", "noopener,noreferrer,width=800,height=600"); + }; + + const handleShareTwitter = () => { + if (!shareUrl) return; + const u = new URL("https://twitter.com/intent/tweet"); + u.searchParams.set("text", shareCaption); + u.searchParams.set("url", shareUrl); + openSharePopup(u.toString()); + }; + + const handleShareFacebook = () => { + if (!shareUrl) return; + const u = new URL("https://www.facebook.com/sharer/sharer.php"); + u.searchParams.set("u", shareUrl); + if (shareCaption) u.searchParams.set("quote", shareCaption); + openSharePopup(u.toString()); + }; + + const handleShareLinkedIn = () => { + if (!shareUrl) return; + const u = new URL("https://www.linkedin.com/sharing/share-offsite/"); + u.searchParams.set("url", shareUrl); + openSharePopup(u.toString()); + }; + + const handleShareReddit = () => { + if (!shareUrl) return; + const u = new URL("https://www.reddit.com/submit"); + u.searchParams.set("url", shareUrl); + if (shareHeadline) u.searchParams.set("title", shareHeadline); + openSharePopup(u.toString()); + }; + + const handleShareWhatsApp = () => { + if (!shareUrl) return; + const u = new URL("https://wa.me/"); + u.searchParams.set("text", `${shareCaption}\n${shareUrl}`.trim()); + openSharePopup(u.toString()); + }; + + const handleNativeShare = async () => { + if (!shareUrl || !shareHeadline) return; + if (!("share" in navigator)) return; + try { + await navigator.share({ + title: shareHeadline, + text: shareCaption, + url: shareUrl, + }); + } catch { + // User cancelled or share failed silently + } + }; + + const handleDownloadPng = async () => { + if (!shareTemplate) return; + setDownloadError(null); + setDownloading(true); + try { + await downloadSharePng(shareTemplate, shareFormat, entityId); + } catch (e) { + setDownloadError(e instanceof Error ? e.message : "download_failed"); + } finally { + setDownloading(false); + } + }; + + const handleDownloadSvg = () => { + if (!shareTemplate) return; + setDownloadError(null); + downloadShareSvg(shareTemplate, shareFormat, entityId); + }; + + const isDisabled = disabled || !shareUrl; + + return ( +
+
+ {/* Download Section */} + {shareTemplate ? ( +
+ + + +
+ ) : null} + + {/* Copy Section */} +
+ + +
+ + {/* Social Share Section */} +
+ + + + + +
+ + {/* Native Share Button */} + {supportsNativeShare ? ( + + ) : null} +
+ + {/* Download Error */} + {downloadError ? ( +

Download failed: {downloadError}

+ ) : null} +
+ ); +} diff --git a/apps/web/src/components/share/ShareCard.tsx b/apps/web/src/components/share/ShareCard.tsx new file mode 100644 index 0000000..d68fa99 --- /dev/null +++ b/apps/web/src/components/share/ShareCard.tsx @@ -0,0 +1,93 @@ +"use client"; + +import Image from "next/image"; +import type { ShareCardProps } from "./types"; + +/** + * ShareCard - A reusable, visually appealing share card component + * + * Used on both repo analysis and profile pages to display a shareable + * summary of the user's vibe coding style. + */ +export function ShareCard({ + variant, + persona, + metrics, + footer, + colors, + avatarUrl, + headerLabel, +}: ShareCardProps) { + const defaultHeader = variant === "profile" ? "My Vibed Coding Profile" : "My Vibe Coding Style"; + const header = headerLabel ?? defaultHeader; + + return ( +
+
+ {/* Header */} +
+
+

+ {header} +

+

+ {persona.label} +

+

+ {persona.tagline} +

+ {persona.confidence ? ( +

+ {persona.confidence} confidence +

+ ) : null} +
+
+ {avatarUrl ? ( + User avatar + ) : ( +
+ V +
+ )} +
+
+ + {/* Metrics Grid */} +
+ {metrics.slice(0, 4).map((metric, idx) => ( +
+

+ {metric.label} +

+

+ {metric.value} +

+
+ ))} +
+ + {/* Footer */} +
+

+ {footer.left} +

+

+ {footer.right} +

+
+
+
+ ); +} diff --git a/apps/web/src/components/share/index.ts b/apps/web/src/components/share/index.ts new file mode 100644 index 0000000..b2f0adc --- /dev/null +++ b/apps/web/src/components/share/index.ts @@ -0,0 +1,18 @@ +/** + * Share components module + */ + +export { ShareCard } from "./ShareCard"; +export { ShareActions } from "./ShareActions"; +export { ProfileShareSection } from "./ProfileShareSection"; +export { createShareSvg, downloadSharePng, downloadShareSvg, SHARE_FORMATS } from "./share-image"; +export type { + ShareCardProps, + ShareCardMetric, + ShareCardColors, + ShareCardPersona, + ShareActionsProps, + ShareImageTemplate, + ShareFormat, + ShareFormatConfig, +} from "./types"; diff --git a/apps/web/src/components/share/share-image.ts b/apps/web/src/components/share/share-image.ts new file mode 100644 index 0000000..81ec0e2 --- /dev/null +++ b/apps/web/src/components/share/share-image.ts @@ -0,0 +1,242 @@ +/** + * Share image generation utilities + */ + +import type { ShareFormat, ShareFormatConfig, ShareImageTemplate } from "./types"; + +export const SHARE_FORMATS: Record = { + og: { + label: "OpenGraph 1200×630", + width: 1200, + height: 630, + pad: 80, + headlineSize: 64, + subheadSize: 28, + metricsSize: 24, + metaSize: 18, + watermarkSize: 18, + cardRadius: 48, + }, + square: { + label: "Square 1080×1080", + width: 1080, + height: 1080, + pad: 88, + headlineSize: 72, + subheadSize: 32, + metricsSize: 28, + metaSize: 20, + watermarkSize: 20, + cardRadius: 64, + }, + story: { + label: "Story 1080×1920", + width: 1080, + height: 1920, + pad: 96, + headlineSize: 84, + subheadSize: 36, + metricsSize: 30, + metaSize: 22, + watermarkSize: 22, + cardRadius: 64, + }, +}; + +function escapeXml(value: string): string { + return value + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +function wrapTextLines( + text: string, + maxCharsPerLine: number, + maxLines: number +): { lines: string[]; truncated: boolean } { + const words = text.trim().split(/\s+/).filter(Boolean); + const lines: string[] = []; + let current = ""; + + for (const word of words) { + const next = current ? `${current} ${word}` : word; + if (next.length <= maxCharsPerLine) { + current = next; + continue; + } + + if (current) { + lines.push(current); + current = word; + if (lines.length >= maxLines) { + return { lines, truncated: true }; + } + continue; + } + + lines.push(word.slice(0, Math.max(1, maxCharsPerLine))); + if (lines.length >= maxLines) { + return { lines, truncated: true }; + } + current = ""; + } + + if (current) lines.push(current); + + if (lines.length > maxLines) { + return { lines: lines.slice(0, maxLines), truncated: true }; + } + + return { lines, truncated: false }; +} + +function withEllipsis(text: string): string { + const t = text.trim(); + return t.length === 0 ? t : `${t}…`; +} + +export function createShareSvg( + template: ShareImageTemplate, + format: ShareFormat +): string { + const cfg = SHARE_FORMATS[format]; + const metricsText = template.metrics + .slice(0, 3) + .map((metric) => `${metric.label}: ${metric.value}`) + .join(" · "); + const archetypes = template.persona_archetype.archetypes.slice(0, 3).join(", "); + + const headlineChars = format === "og" ? 26 : format === "square" ? 22 : 18; + const subheadChars = format === "og" ? 46 : format === "square" ? 36 : 30; + const metricsChars = format === "og" ? 72 : format === "square" ? 60 : 46; + + const headlineWrapped = wrapTextLines(template.headline, headlineChars, 2); + const subheadWrapped = wrapTextLines(template.subhead, subheadChars, 2); + const metricsWrapped = wrapTextLines(metricsText, metricsChars, format === "story" ? 2 : 1); + + const headlineLines = headlineWrapped.lines.map((l, idx) => + escapeXml(idx === headlineWrapped.lines.length - 1 && headlineWrapped.truncated ? withEllipsis(l) : l) + ); + const subheadLines = subheadWrapped.lines.map((l, idx) => + escapeXml(idx === subheadWrapped.lines.length - 1 && subheadWrapped.truncated ? withEllipsis(l) : l) + ); + const metricsLines = metricsWrapped.lines.map((l, idx) => + escapeXml(idx === metricsWrapped.lines.length - 1 && metricsWrapped.truncated ? withEllipsis(l) : l) + ); + + const startY = + format === "story" ? Math.round(cfg.height * 0.34) : cfg.pad + Math.round(cfg.headlineSize * 0.9); + + const headlineY = startY; + const subheadY = headlineY + Math.round(cfg.headlineSize * 0.95) + 18; + const metricsY = subheadY + Math.round(cfg.subheadSize * 0.95) + 18; + const metaY = + format === "story" ? Math.round(cfg.height * 0.72) : metricsY + Math.round(cfg.metricsSize * 0.95) + 28; + const hashY = metaY + Math.round(cfg.metaSize * 1.6); + const watermarkY = cfg.height - Math.round(cfg.pad * 0.6); + + const x = cfg.pad; + const watermarkX = cfg.width - cfg.pad; + + const headlineTspans = headlineLines + .map((line, idx) => `${line}`) + .join(""); + const subheadTspans = subheadLines + .map((line, idx) => `${line}`) + .join(""); + const metricsTspans = metricsLines + .map((line, idx) => `${line}`) + .join(""); + + return ` + + + + + + + + + + ${headlineTspans} + + + ${subheadTspans} + + + ${metricsTspans} + + + Persona: ${escapeXml(template.persona_archetype.label)} · ${escapeXml(archetypes)} + + + #Vibed + + + vibed.coding + +`; +} + +export function downloadBlob(blob: Blob, filename: string): void { + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); +} + +export async function downloadSharePng( + template: ShareImageTemplate, + format: ShareFormat, + entityId: string +): Promise { + const svg = createShareSvg(template, format); + const cfg = SHARE_FORMATS[format]; + + const svgBlob = new Blob([svg], { type: "image/svg+xml;charset=utf-8" }); + const svgUrl = URL.createObjectURL(svgBlob); + + try { + const img = new Image(); + const loaded = new Promise((resolve, reject) => { + img.onload = () => resolve(); + img.onerror = () => reject(new Error("image_load_failed")); + }); + img.src = svgUrl; + await loaded; + + const scale = 2; + const canvas = document.createElement("canvas"); + canvas.width = cfg.width * scale; + canvas.height = cfg.height * scale; + const ctx = canvas.getContext("2d"); + if (!ctx) throw new Error("canvas_unsupported"); + ctx.setTransform(scale, 0, 0, scale, 0, 0); + ctx.drawImage(img, 0, 0, cfg.width, cfg.height); + + const pngBlob = await new Promise((resolve, reject) => { + canvas.toBlob((b) => (b ? resolve(b) : reject(new Error("png_encode_failed"))), "image/png"); + }); + + downloadBlob(pngBlob, `vibed-coding-${entityId}-${format}.png`); + } finally { + URL.revokeObjectURL(svgUrl); + } +} + +export function downloadShareSvg( + template: ShareImageTemplate, + format: ShareFormat, + entityId: string +): void { + const svg = createShareSvg(template, format); + const blob = new Blob([svg], { type: "image/svg+xml;charset=utf-8" }); + downloadBlob(blob, `vibed-coding-${entityId}-${format}.svg`); +} diff --git a/apps/web/src/components/share/types.ts b/apps/web/src/components/share/types.ts new file mode 100644 index 0000000..df930c6 --- /dev/null +++ b/apps/web/src/components/share/types.ts @@ -0,0 +1,83 @@ +/** + * Share component types and interfaces + */ + +export interface ShareCardMetric { + label: string; + value: string; +} + +export interface ShareCardColors { + primary: string; + accent: string; +} + +export interface ShareCardPersona { + label: string; + tagline: string; + confidence: string; + archetypes?: string[]; +} + +export interface ShareCardProps { + /** Variant determines layout differences */ + variant: "repo" | "profile"; + /** Persona information */ + persona: ShareCardPersona; + /** Up to 4 metrics to display */ + metrics: ShareCardMetric[]; + /** Footer content */ + footer: { + left: string; + right: string; + }; + /** Gradient colors */ + colors: ShareCardColors; + /** Optional avatar URL */ + avatarUrl?: string | null; + /** Optional header label override */ + headerLabel?: string; +} + +export interface ShareActionsProps { + /** URL to share */ + shareUrl: string; + /** Text content for copy/share */ + shareText: string; + /** Caption for social platforms */ + shareCaption: string; + /** Headline for native share */ + shareHeadline: string; + /** Share template for image generation */ + shareTemplate: ShareImageTemplate | null; + /** Job ID or profile ID for filename */ + entityId: string; + /** Whether share is disabled */ + disabled?: boolean; +} + +export interface ShareImageTemplate { + colors: ShareCardColors; + headline: string; + subhead: string; + metrics: ShareCardMetric[]; + persona_archetype: { + label: string; + archetypes: string[]; + }; +} + +export type ShareFormat = "og" | "square" | "story"; + +export interface ShareFormatConfig { + label: string; + width: number; + height: number; + pad: number; + headlineSize: number; + subheadSize: number; + metricsSize: number; + metaSize: number; + watermarkSize: number; + cardRadius: number; +} diff --git a/docs/architecture/llm-narrative-workflow.md b/docs/architecture/llm-narrative-workflow.md new file mode 100644 index 0000000..1fba805 --- /dev/null +++ b/docs/architecture/llm-narrative-workflow.md @@ -0,0 +1,391 @@ +# LLM Narrative Workflow Documentation + +This document describes how LLM narratives are generated, stored, and aggregated across individual repository analyses and aggregated user profiles. + +--- + +## Overview + +Vibed generates two types of narratives: + +1. **Individual Repo Narrative** — Generated per analysis job, stored in `analysis_reports.narrative_json` +2. **Profile Narrative** — Generated from aggregated data across all repos, stored in `user_profiles.narrative_json` + +Each narrative can be: +- **LLM-generated** — Uses Claude, GPT, or Gemini to create personalized, engaging text +- **Fallback (deterministic)** — Uses templated text based on metrics (no LLM call) + +--- + +## Database Schema + +### Individual Report Narrative + +Table: `analysis_reports` +- `narrative_json` (JSONB, NOT NULL) — The structured narrative +- `llm_model` (TEXT, default 'none') — e.g., "claude-sonnet-4", "gpt-4o", "none" +- `llm_key_source` (TEXT) — "platform", "user", "sponsor", "none" + +Schema: `supabase/migrations/0001_init_core_schema.sql` + +### Profile Narrative + +Table: `user_profiles` (columns added in migration 0017) +- `narrative_json` (JSONB, nullable) — Profile-level narrative +- `llm_model` (TEXT) — Model used for profile narrative +- `llm_key_source` (TEXT) — Key source for profile narrative + +Schema: `supabase/migrations/0017_add_profile_narrative.sql` + +> **NOTE:** The TypeScript types in `packages/db/src/database.types.ts` have not been regenerated to include `narrative_json`, `llm_model`, and `llm_key_source` columns for `user_profiles`. These columns exist in the database but are not typed. + +--- + +## LLM Resolution Logic + +### For Individual Repo Analysis + +Function: `resolveLLMConfig(userId, repoId)` +Location: `apps/web/src/lib/llm-config.ts` (lines 365-401) + +Resolution order: +1. **Check opt-in** — User must have `users.llm_narrative_opt_in = true` +2. **Check platform disabled** — `llm_configs.llm_disabled` for platform scope +3. **User's own key** — Check `llm_configs` with `scope='user'` and `scope_id=userId` +4. **Platform key with limit** — Check `llm_usage` for per-repo limit (default: 1 free LLM per repo) +5. **Fallback** — Return `{ config: null, source: "none" }` + +### For Profile Aggregation + +Function: `resolveProfileLLMConfig(userId)` +Location: `apps/web/src/lib/llm-config.ts` (lines 483-519) + +Resolution order: +1. **Check opt-in** — User must have opted in +2. **Check platform disabled** +3. **User's own key** — Always allowed for profile +4. **Platform key with profile limit** — Check if `countReposWithLlmReports(userId) <= profileLlmRepoLimit` (default: 3) +5. **Fallback** — Return `{ config: null, source: "none" }` + +--- + +## Current Workflow + +### Step 1: User Triggers Analysis + +User clicks "Run Analysis" on a repository. + +### Step 2: Inngest Job Runs + +Function: `analyze-repo` +Location: `apps/web/src/inngest/functions/analyze-repo.ts` + +The job executes these steps: +1. Fetch commits from GitHub +2. Process commit metadata +3. Compute vibe metrics +4. Classify vibe/persona +5. Generate and save report (includes narrative) +6. Update user profile (includes profile narrative) + +### Step 3: Individual Report Narrative (Step 5) + +Location: `apps/web/src/inngest/functions/analyze-repo.ts` (lines 1040-1138) + +Process: +1. Always compute fallback narrative first via `toNarrativeFallback()` +2. Resolve LLM config via `resolveLLMConfig(userId, repoId)` +3. If LLM available, try to generate via `generateNarrativeWithLLM()` +4. Use whichever narrative was generated (LLM or fallback) +5. Save to `analysis_reports` with `llm_model` and `llm_key_source` tracking + +### Step 4: Profile Aggregation (Step 6) + +Location: `apps/web/src/inngest/functions/analyze-repo.ts` (lines 1192-1595) + +Process: +1. Fetch all completed jobs for user (excluding disconnected repos) +2. Fetch vibe_insights for each job +3. Build `RepoInsightSummary[]` array +4. Call `aggregateUserProfile()` to compute weighted axes and persona +5. Resolve LLM config for profile via `resolveProfileLLMConfig(userId)` +6. Generate profile narrative (LLM via `generateProfileNarrativeWithLLM()` or fallback via `toProfileNarrativeFallback()`) +7. Upsert `user_profiles` with narrative and LLM metadata +8. Save snapshot to `user_profile_history` with LLM metadata + +--- + +## Scenario Analysis + +### Scenario 1: Full LLM User (BYOK with Opt-In) + +**User state:** +- `users.llm_narrative_opt_in = true` +- Has valid API key in `llm_configs` with `scope='user'` + +**Behavior:** +- Every repo analysis generates LLM narrative (using user's key) +- Every profile aggregation generates LLM narrative (using user's key) +- No usage limits apply (user's own key) + +**Result:** +- `analysis_reports.llm_model = "claude-sonnet-4"` (or similar) +- `analysis_reports.llm_key_source = "user"` +- `user_profiles.llm_model = "claude-sonnet-4"` +- `user_profiles.llm_key_source = "user"` + +--- + +### Scenario 2: No LLM User (No Opt-In, No Key) + +**User state:** +- `users.llm_narrative_opt_in = false` (or never set) +- No API key configured + +**Behavior:** +- All repo analyses use fallback narrative +- Profile uses fallback narrative +- Metrics are still computed; only narrative text is deterministic + +**Result:** +- `analysis_reports.llm_model = "none"` +- `analysis_reports.llm_key_source = "none"` +- `user_profiles.llm_model = null` +- `user_profiles.llm_key_source = "none"` + +--- + +### Scenario 3: Platform Free Tier User (Opted In, No Key) + +**User state:** +- `users.llm_narrative_opt_in = true` +- No personal API key +- Using platform's free tier + +**Platform limits (defaults):** +- Per-repo limit: 1 free LLM analysis per repo +- Profile LLM repo limit: 3 repos + +**Behavior for first 3 repos:** +1. **Repo 1, Run 1:** LLM narrative (platform key) +2. **Repo 1, Run 2:** Fallback narrative (limit exhausted for this repo) +3. **Repo 2, Run 1:** LLM narrative (platform key) +4. **Repo 3, Run 1:** LLM narrative (platform key) +5. **Profile after Repo 3:** LLM profile narrative (3 repos with LLM ≤ limit of 3) + +**Behavior for 4th repo onwards:** +6. **Repo 4, Run 1:** LLM narrative (platform key, this is first run on this repo) +7. **Profile after Repo 4:** Fallback profile narrative (4 repos with LLM > limit of 3) + +--- + +### Scenario 4: Mixed Mode — Free Tier Exhausted, Then Non-LLM + +**User state:** +- `users.llm_narrative_opt_in = true` +- No personal API key +- Already used free LLM on 3 repos + +**Sequence:** +1. User has 3 repos with LLM-generated reports +2. User analyzes a 4th repo (first run) → LLM report (platform key) +3. User analyzes a 5th repo (first run) → LLM report (platform key) +4. User re-analyzes Repo 1 (second run) → Fallback report (per-repo limit exhausted) + +**Profile after each step:** +- After step 1: LLM profile (3 repos ≤ 3) +- After step 2: Fallback profile (4 repos > 3) +- After step 3: Fallback profile (5 repos > 3) +- After step 4: Fallback profile (still 5 repos with successful LLM) + +**Key insight:** The profile LLM decision is based on the **count of unique repos with successful LLM reports**, not the count of jobs or total LLM calls. + +--- + +### Scenario 5: Downgrade — Had Key, Key Removed/Expired + +**User state (before):** +- Opted in, had personal API key +- 5 repos all with LLM narratives + +**User state (after):** +- Key removed or expired +- Still opted in + +**Behavior on next analysis:** +1. User runs analysis on Repo 1 (second time) +2. `resolveLLMConfig` checks: no user key, check platform limit +3. Platform limit for Repo 1 already exhausted → Fallback narrative +4. Profile: 5 repos have LLM history > 3 → Fallback profile narrative + +**Result:** New analyses fall back, but existing LLM narratives are preserved in history. + +--- + +### Scenario 6: Upgrade — Was Free Tier, Added Key + +**User state (before):** +- Opted in, no personal key +- 4 repos: Repo 1-3 have LLM, Repo 4 has fallback + +**User state (after):** +- Added personal API key + +**Behavior on next analysis:** +1. User runs analysis on Repo 4 (second time) +2. `resolveLLMConfig` finds user key → LLM narrative +3. Profile: User has key → LLM profile narrative (no limit with own key) + +**Result:** Adding a key immediately unlocks LLM for all future analyses. + +--- + +## Resolved Issues + +The following issues have been fixed: + +### Issue 1: TypeScript Types Updated (FIXED) + +The `packages/db/src/database.types.ts` file has been manually updated to include: +- `user_profiles.narrative_json` +- `user_profiles.llm_model` +- `user_profiles.llm_key_source` +- `user_profiles.regenerating` +- `user_profile_history.version_number` +- `user_profile_history.llm_model` +- `user_profile_history.llm_key_source` + +### Issue 2: Profile Narrative Displayed in UI (FIXED) + +Location: `apps/web/src/app/page.tsx` + +**What was done:** +- Updated profile queries to include `narrative_json`, `llm_model`, `llm_key_source` +- Profile insight section now shows LLM narrative headline when available +- Falls back to deterministic `generateCrossRepoInsight()` when no LLM narrative +- Shows "AI-generated" badge when narrative comes from LLM +- Displays full narrative paragraphs and highlights when available + +### Issue 3: Version Selector for Reports (FIXED) + +Location: `apps/web/src/app/analysis/[jobId]/AnalysisClient.tsx` + +**What was done:** +- Added `repoHistory` computed value that filters history by the current repo +- Added a version selector dropdown that appears when multiple runs exist for the same repo +- Dropdown shows date, persona label, and navigation to switch between versions +- Styled consistently with the rest of the analysis page + +### Issue 4: Profile History Accessible (FIXED) + +Location: `apps/web/src/components/ProfileVersionSelector.tsx` + +**What was done:** +- Created new `ProfileVersionSelector` client component +- Shows "Profile History" collapsible section on the profile page +- Fetches from `/api/profile/history` when expanded +- Version selector dropdown to browse historical profile states +- Shows persona, commit counts, repo counts, LLM model for each version +- Displays trigger repo name for context + +### Issue 5: Visual Indicator for Profile Aggregation (FIXED) + +Location: `apps/web/src/app/admin/jobs/page.tsx` and `apps/web/src/app/admin/actions.ts` + +**What was done:** +- Updated `getAllJobs` action to query `user_profile_history` for profile updates triggered by jobs +- Added "Profile" column to the admin jobs table +- Shows "Updated" badge with checkmark for jobs that triggered a profile update +- Badge shows persona name on hover + +--- + +## Aggregation Logic Details + +### How Profile Narrative Changes with Mixed LLM/Non-LLM Reports + +The profile narrative is **regenerated fresh on every job completion**. It doesn't "merge" LLM narratives from individual reports. + +**What happens:** +1. Job completes +2. `aggregateUserProfile()` runs on all completed jobs (LLM and non-LLM alike) +3. `resolveProfileLLMConfig()` determines if profile gets LLM narrative +4. Either LLM generation or fallback is called +5. Result stored in `user_profiles.narrative_json` + +**Key insight:** Individual report narratives don't affect the profile narrative decision. The profile LLM decision is solely based on: +1. User opt-in status +2. User's own API key (if any) +3. Count of repos with successful LLM reports (for platform key limit) + +### What Data Goes Into Profile Narrative + +Whether LLM or fallback, the profile narrative is generated from: +- `AggregatedProfile.persona` — The aggregated persona +- `AggregatedProfile.axes` — Weighted average of all repo axes +- `AggregatedProfile.totalCommits` — Total commits across all repos +- `AggregatedProfile.totalRepos` — Number of repos analyzed +- `AggregatedProfile.repoBreakdown` — Per-repo persona breakdown + +**NOT included:** Individual repo narratives. The profile narrative is a fresh generation based on aggregated data. + +--- + +## Accessing Historical Versions + +### Current Capabilities + +**For Repo Reports:** +- Each analysis creates a new `analysis_jobs` row with unique ID +- All jobs are stored; nothing is overwritten +- API: `GET /api/analysis/history` returns all insights with job metadata +- UI: Timeline in `AnalysisClient` shows up to 8 previous runs (lines 1137-1163) + +**For Profile:** +- Each job completion creates a `user_profile_history` snapshot +- Snapshots include `version_number` (auto-incremented via trigger) +- API: `GET /api/profile/history` lists all versions +- API: `GET /api/profile/history/[id]` fetches specific version + +### What's Missing + +1. **Repo Version Selector:** A dropdown in the analysis page to switch between versions +2. **Profile Version Selector:** A dropdown on the profile page to view historical states +3. **Diff View:** Compare two versions side-by-side + +--- + +## Key File Locations + +| Component | Location | +|-----------|----------| +| LLM resolution (repo) | `apps/web/src/lib/llm-config.ts` → `resolveLLMConfig()` | +| LLM resolution (profile) | `apps/web/src/lib/llm-config.ts` → `resolveProfileLLMConfig()` | +| Repo narrative generation | `apps/web/src/inngest/functions/analyze-repo.ts` (lines 1040-1138) | +| Profile aggregation | `apps/web/src/inngest/functions/analyze-repo.ts` (lines 1192-1595) | +| Profile narrative generation | `apps/web/src/lib/profile-narrative.ts` | +| Fallback narrative (repo) | `packages/core/src/narrative.ts` → `toNarrativeFallback()` | +| Fallback narrative (profile) | `apps/web/src/lib/profile-narrative.ts` → `toProfileNarrativeFallback()` | +| Aggregation algorithm | `packages/core/src/vibe.ts` → `aggregateUserProfile()` | +| Profile history API | `apps/web/src/app/api/profile/history/route.ts` | +| Analysis history API | `apps/web/src/app/api/analysis/history/route.ts` | + +--- + +## Summary Table + +| Scenario | Repo LLM | Profile LLM | Notes | +|----------|----------|-------------|-------| +| No opt-in | Never | Never | Must opt in first | +| Opted in + own key | Always | Always | No limits | +| Opted in + platform key, <3 repos with LLM | First run per repo | Yes | Platform limits | +| Opted in + platform key, ≥4 repos with LLM | First run per repo | No | Profile limit exceeded | +| Key removed after use | Fallback | Fallback | Existing reports preserved | + +--- + +## Related Documentation + +- [Vibed Analysis Pipeline](./vibed-analysis-pipeline.md) — Full pipeline architecture +- [PRD: Profile Aggregation](../PRD-profile-aggregation.md) — Original design +- [LLM Setup Guide](../llm-setup.md) — User-facing LLM configuration diff --git a/docs/prd/share-experience-improvements.md b/docs/prd/share-experience-improvements.md new file mode 100644 index 0000000..57e3f49 --- /dev/null +++ b/docs/prd/share-experience-improvements.md @@ -0,0 +1,617 @@ +# PRD: Share Experience & Metrics Surfacing Improvements + +**Status:** Draft +**Author:** AI Assistant (with human direction) +**Priority:** P0 (Core to product value) +**Depends on:** Core analysis pipeline (complete), Profile aggregation (complete) + +--- + +## Problem Statement + +Vibed Coding generates rich insights but buries them. The share experience—our primary viral growth mechanism—is hidden behind a collapsible section, and the aggregated profile page has no share functionality at all. + +**Current issues:** + +1. **Share is hidden on repo analysis page** — buried in a `
` section labeled "Share, timeline, and details" +2. **No share on profile page** — profile aggregation is complete but share is "deferred to P7" +3. **Rich metrics computed but not displayed** — multi-agent signals, tech signals, timing patterns are computed but invisible +4. **LLM narratives unused on profile** — generated and stored but never rendered +5. **Share card is an afterthought** — not the hero experience it should be + +**Why this matters:** + +The core loop is: Analyze → Discover → Share → Friend sees → Friend analyzes. + +If sharing is hard or hidden, the loop breaks. Every friction point in sharing costs us viral growth. + +--- + +## Success Criteria + +| Metric | Current | Target | +|--------|---------|--------| +| Share action rate (repo analysis) | Unknown (hidden) | >25% of completed analyses | +| Share action rate (profile) | 0% (no feature) | >15% of profile views | +| Time from analysis complete → share | >60s (hunting for button) | <15s | +| Share card visibility | Below fold, collapsed | Above fold, hero | + +--- + +## Design Principles + +### 1. Share is the Product, Not a Feature + +The share card isn't something you do after viewing your analysis—it IS the analysis. The card should be designed first, then the rest of the page supports it. + +### 2. One Glance, One Story + +A share card must tell a complete story in 3 seconds. Persona + 3-4 key metrics + visual identity. No scrolling, no clicking, no reading paragraphs. + +### 3. Platform-Native Sharing + +Different platforms have different needs: +- Twitter/X: Punchy text + link + optional image +- LinkedIn: Professional framing + insight card +- Instagram: Story-sized visual asset +- Copy link: For Slack, Discord, direct messages + +### 4. LLM Enhances, Doesn't Gate + +Non-LLM users should get a great experience. LLM adds flavor but isn't required for a compelling share. + +--- + +## Scope + +### In Scope + +1. **Repo Analysis Page (`/analysis/[jobId]`):** + - Move share card to hero position (above fold) + - Surface additional computed metrics + - Improve LLM vs non-LLM narrative handling + +2. **Profile Page (`/` when authenticated):** + - Add share card for aggregated profile + - Surface LLM narrative if available + - Create cohesive single-card story + +3. **Share Components:** + - Extract reusable `` component + - Extract reusable `` component + - Consistent design language across both pages + +4. **Metrics Surfacing:** + - Multi-agent signals (when detected) + - Tech signals (top technologies) + - Enhanced timing patterns + +### Out of Scope + +- Public profile pages (future feature) +- OG image generation API (future enhancement) +- Team/organization profiles +- Historical profile comparison + +--- + +## Detailed Requirements + +### 1. Repo Analysis Page Improvements + +#### 1.1 Share Card as Hero + +**Current structure:** +``` +[Persona Card] → [Metrics Grid] → [Axes] → [Narrative] → [Collapsible: Share, timeline...] +``` + +**New structure:** +``` +[Share Card (hero, full-width, gradient)] → [Share Actions (prominent)] → [Metrics + Axes] → [Narrative] → [Details] +``` + +**Share Card Design:** + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ [Gradient background: persona colors] │ │ +│ │ │ │ +│ │ MY VIBE CODING STYLE │ │ +│ │ │ │ +│ │ 🎭 The Vibe Prototyper │ │ +│ │ "You build to think — code is your sketchpad" │ │ +│ │ │ │ +│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ +│ │ │ 23 days │ │ Friday │ │ Focused │ │ 2.1:1 │ │ │ +│ │ │ Streak │ │ 4-7pm │ │ Surgeon │ │ Build/Fix│ │ │ +│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │ +│ │ │ │ +│ │ 847 commits · 42 active days · vibed.coding │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ +│ [Copy text] [Copy link] [Download PNG ▾] [Twitter] [LinkedIn] │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +**Requirements:** +- [ ] Share card is the first thing users see after analysis completes +- [ ] Card is designed to look good when screenshotted +- [ ] Share actions are immediately visible, not hidden +- [ ] Download dropdown offers: OG (1200×630), Square (1080×1080), Story (1080×1920) + +#### 1.2 Additional Metrics to Surface + +Currently displayed: Streak, Peak Day, Focus, Build vs Fix, Scope + +**Add these when detected:** + +| Metric | When to Show | Display | +|--------|--------------|---------| +| Multi-agent signals | AI co-author count > 0 OR AI trailers detected | "🤖 AI-Assisted: 45% of commits show collaboration patterns" | +| Tech signals | Top 3 tech terms detected | "Your stack: React, TypeScript, Supabase" | +| Night owl / Early bird | Peak hour is before 9am or after 6pm | "🌙 Night Owl: 68% of commits after 6pm" | +| Weekend warrior | >20% of commits on Sat/Sun | "Weekend coder: 24% of your commits" | +| PR workflow style | When artifact traceability data exists | "Orchestrator: High PR coverage, structured workflow" | + +**Conditional display logic:** +- Show max 2 additional signals beyond the core 5 +- Prioritize signals with strongest values +- Only show if data quality is sufficient + +#### 1.3 LLM vs Non-LLM Narrative Handling + +**Current state:** +- Shows "Generated with {model}" or "Generated from metrics" +- Regenerate button available +- Error message if LLM fails + +**Improved state:** + +For LLM narrative: +``` +┌─────────────────────────────────────────────────────────────────┐ +│ YOUR STORY [AI-generated ✨] │ +│ │ +│ "Your commits tell the story of a builder who thinks through │ +│ code. March was intense—23 consecutive days, mostly evenings. │ +│ You started with scaffolding and iterated rapidly..." │ +│ │ +│ [Regenerate] │ +└─────────────────────────────────────────────────────────────────┘ +``` + +For non-LLM narrative: +``` +┌─────────────────────────────────────────────────────────────────┐ +│ YOUR PATTERNS │ +│ │ +│ Based on 847 commits over 42 active days, you show strong │ +│ Prototyper tendencies. Your longest streak was 23 days in │ +│ March, and you're most productive on Friday afternoons. │ +│ │ +│ [✨ Get AI-enhanced story] ← Only if user hasn't opted in │ +└─────────────────────────────────────────────────────────────────┘ +``` + +**Requirements:** +- [ ] Visual distinction between LLM and deterministic narratives +- [ ] LLM badge is subtle but present ("AI-generated ✨") +- [ ] Non-LLM users see CTA to opt in (if not already opted in) +- [ ] Both narrative types are valuable and complete + +--- + +### 2. Profile Page Share Card + +#### 2.1 Profile Share Card Design + +The profile page should have a share card that aggregates across all repos. + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ [Gradient background] │ │ +│ │ │ │ +│ │ MY VIBED CODING PROFILE │ │ +│ │ │ │ +│ │ 🎭 The Vibe Prototyper │ │ +│ │ "You build to think — code is your sketchpad" │ │ +│ │ High confidence │ │ +│ │ │ │ +│ │ ┌──────────────────────────────────────────────────┐ │ │ +│ │ │ 5 repos · 2,341 commits · 78% clarity │ │ │ +│ │ └──────────────────────────────────────────────────┘ │ │ +│ │ │ │ +│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ +│ │ │ Automation│ │ Guardrails│ │ Rhythm │ │ │ +│ │ │ 72 │ │ 45 │ │ 68 │ │ │ +│ │ └──────────┘ └──────────┘ └──────────┘ │ │ +│ │ │ │ +│ │ vibed.coding │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ +│ [Copy text] [Copy link] [Download PNG ▾] [Twitter] [LinkedIn] │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +**Requirements:** +- [ ] Profile page has a share card as prominent as repo analysis +- [ ] Card includes: persona, tagline, confidence, repo count, commit count, top 3 axes +- [ ] Same share actions as repo analysis page +- [ ] Card is designed to look good when shared + +#### 2.2 Profile Narrative (LLM) + +**Current state:** LLM narratives are generated and stored in `user_profiles.narrative_json` but never displayed. + +**New state:** + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ INSIGHT [AI-generated ✨] │ +│ │ +│ "Across 5 projects, you consistently show Prototyper │ +│ patterns—but with a twist. On acme/dashboard you lean │ +│ heavily into rapid iteration, while on acme/api you show │ +│ more Guardian tendencies. Your style adapts to context." │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +**For non-LLM users:** +Use the existing `generateCrossRepoInsight()` function but with improved copy. + +**Requirements:** +- [ ] Fetch `narrative_json` from `user_profiles` if it exists +- [ ] Display LLM narrative with badge if available +- [ ] Fall back to deterministic insight if no LLM narrative +- [ ] Offer opt-in CTA for non-LLM users + +--- + +### 3. Reusable Share Components + +#### 3.1 `` Component + +A reusable component for both repo analysis and profile pages. + +```tsx +interface ShareCardProps { + variant: 'repo' | 'profile'; + persona: { + id: string; + label: string; + tagline: string; + confidence: string; + }; + metrics: Array<{ + label: string; + value: string; + }>; + footer: { + left: string; // e.g., "vibed.coding" + right: string; // e.g., "847 commits · 42 days" + }; + colors: { + primary: string; + accent: string; + }; + avatar?: string; +} +``` + +**Requirements:** +- [ ] Single component used by both pages +- [ ] Accepts variant prop for minor layout differences +- [ ] Gradient background using persona colors +- [ ] Responsive design (works on mobile) +- [ ] Exportable to PNG/SVG + +#### 3.2 `` Component + +Reusable share action bar. + +```tsx +interface ShareActionsProps { + shareUrl: string; + shareText: string; + shareTemplate: ShareTemplate; + onDownload: (format: 'og' | 'square' | 'story') => void; +} +``` + +**Requirements:** +- [ ] Copy text button with feedback +- [ ] Copy link button with feedback +- [ ] Download dropdown (OG, Square, Story) +- [ ] Social buttons: Twitter, LinkedIn, Facebook, WhatsApp, Reddit +- [ ] Native share (on mobile) +- [ ] Consistent styling across pages + +--- + +### 4. Data Requirements + +#### 4.1 Repo Analysis Data + +Already available in `AnalysisClient.tsx`: +- `wrapped.streak`, `wrapped.timing`, `wrapped.commits`, `wrapped.chunkiness` +- `shareTemplate` from API +- `metricsJson` with full metrics + +**Needs to be added to UI:** +- Multi-agent signals from `insights_json.multi_agent_signals` +- Tech signals from `insights_json.tech_signals` +- Artifact traceability from `insights_json.artifact_traceability` + +#### 4.2 Profile Data + +Current fetch in `page.tsx`: +```sql +SELECT persona_name, persona_tagline, persona_confidence, + total_repos, total_commits, axes_json, repo_personas_json, + updated_at, job_ids +FROM user_profiles +WHERE user_id = $1 +``` + +**Needs to be added:** +```sql +SELECT ..., narrative_json, llm_model, llm_key_source +FROM user_profiles +``` + +--- + +### 5. Share Template Enhancements + +#### 5.1 Profile Share Template + +Currently `share_template` only exists for repo analysis. Add profile equivalent. + +```typescript +interface ProfileShareTemplate { + colors: { + primary: string; + accent: string; + }; + headline: string; // "My Vibed Coding Profile" + personaLabel: string; // "The Vibe Prototyper" + tagline: string; // "You build to think..." + confidence: string; // "High confidence" + stats: { + repos: number; + commits: number; + clarity: number; + }; + topAxes: Array<{ + name: string; + score: number; + }>; +} +``` + +#### 5.2 Share Image Generation + +Enhance `createShareSvg` to support profile cards: + +```typescript +function createShareSvg( + template: ShareTemplate | ProfileShareTemplate, + format: ShareFormat, + variant: 'repo' | 'profile' +): string +``` + +--- + +## Implementation Plan + +### Phase 1: Share Card Promotion (Repo Analysis) + +**Goal:** Make share the hero on repo analysis page + +| Task | Effort | Priority | +|------|--------|----------| +| Extract `` component from inline code | M | P0 | +| Extract `` component | S | P0 | +| Move share card to top of results | S | P0 | +| Make share actions visible by default (not collapsed) | S | P0 | +| Update page layout hierarchy | M | P0 | + +**Deliverable:** Share card is first thing users see on analysis page + +### Phase 2: Additional Metrics Surfacing + +**Goal:** Show more of the computed insights + +| Task | Effort | Priority | +|------|--------|----------| +| Add multi-agent signals display | M | P1 | +| Add tech signals display | S | P1 | +| Add timing personality (night owl, etc.) | S | P1 | +| Conditional display logic | M | P1 | + +**Deliverable:** Repo analysis shows 2-3 additional contextual signals + +### Phase 3: Profile Share Card + +**Goal:** Profile page has share experience parity with repo analysis + +| Task | Effort | Priority | +|------|--------|----------| +| Create profile share template type | S | P0 | +| Add share card to profile page | M | P0 | +| Fetch and display LLM narrative | M | P1 | +| Profile share image generation | M | P1 | +| Add share actions to profile | S | P0 | + +**Deliverable:** Profile page has shareable card with download/copy/social + +### Phase 4: LLM Narrative Polish + +**Goal:** Consistent, attractive LLM handling across both pages + +| Task | Effort | Priority | +|------|--------|----------| +| Visual differentiation for LLM vs deterministic | S | P1 | +| Opt-in CTA for non-LLM users | S | P2 | +| Improved non-LLM narrative copy | S | P2 | + +**Deliverable:** Both LLM and non-LLM users get polished narratives + +--- + +## UI Mockups + +### Repo Analysis Page (New Layout) + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ ← Back to profile │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌───────────────────────────────────────────────────────────┐ │ +│ │ [SHARE CARD - GRADIENT BACKGROUND] │ │ +│ │ │ │ +│ │ MY VIBE CODING STYLE [avatar if available] │ │ +│ │ │ │ +│ │ 🎭 The Vibe Prototyper │ │ +│ │ "You build to think — code is your sketchpad" │ │ +│ │ 78% confidence │ │ +│ │ │ │ +│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ +│ │ │23 days │ │ Friday │ │ Focused │ │ 2.1:1 │ │ │ +│ │ │ Streak │ │ 4-7pm │ │ Surgeon │ │Build/Fix│ │ │ +│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │ +│ │ │ │ +│ │ acme/dashboard · 847 commits · vibed.coding │ │ +│ └───────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌───────────────────────────────────────────────────────────┐ │ +│ │ [📋 Copy] [🔗 Link] [⬇️ Download ▾] [𝕏] [in] [f] [Share] │ │ +│ └───────────────────────────────────────────────────────────┘ │ +│ │ +├─────────────────────────────────────────────────────────────────┤ +│ ADDITIONAL SIGNALS │ +│ │ +│ 🤖 AI-Assisted: 45% of commits show AI collaboration │ +│ 🌙 Night Owl: 68% of commits after 6pm │ +│ │ +├─────────────────────────────────────────────────────────────────┤ +│ YOUR AXES │ +│ [6 axis cards with scores] │ +│ │ +├─────────────────────────────────────────────────────────────────┤ +│ YOUR STORY [AI-generated ✨] │ +│ [Narrative text] │ +│ │ +├─────────────────────────────────────────────────────────────────┤ +│ ▸ Timeline, metrics, and details │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### Profile Page (New Layout) + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ YOUR VIBED PROFILE [Add repo] [Vibes] │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌───────────────────────────────────────────────────────────┐ │ +│ │ [SHARE CARD - GRADIENT BACKGROUND] │ │ +│ │ │ │ +│ │ MY VIBED CODING PROFILE │ │ +│ │ │ │ +│ │ 🎭 The Vibe Prototyper │ │ +│ │ "You build to think — code is your sketchpad" │ │ +│ │ High confidence │ │ +│ │ │ │ +│ │ 5 repos · 2,341 commits · 78% clarity │ │ +│ │ │ │ +│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ +│ │ │Automation│ │Guardrails│ │ Rhythm │ │ │ +│ │ │ 72 │ │ 45 │ │ 68 │ │ │ +│ │ └──────────┘ └──────────┘ └──────────┘ │ │ +│ │ │ │ +│ │ vibed.coding │ │ +│ └───────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌───────────────────────────────────────────────────────────┐ │ +│ │ [📋 Copy] [🔗 Link] [⬇️ Download ▾] [𝕏] [in] [f] [Share] │ │ +│ └───────────────────────────────────────────────────────────┘ │ +│ │ +├─────────────────────────────────────────────────────────────────┤ +│ INSIGHT [AI-generated ✨] │ +│ │ +│ "Across 5 projects, you consistently show Prototyper │ +│ patterns. On acme/dashboard you lean into rapid iteration, │ +│ while on acme/api you show more Guardian tendencies." │ +│ │ +├─────────────────────────────────────────────────────────────────┤ +│ YOUR AXES │ +│ [6 axis cards with scores and bars] │ +│ │ +├─────────────────────────────────────────────────────────────────┤ +│ YOUR REPOS │ +│ [Repo breakdown with per-repo personas] │ +│ │ +├─────────────────────────────────────────────────────────────────┤ +│ ▸ How we calculated this │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Open Questions + +1. **Should share card include repo name?** For repo analysis, showing the repo name makes the card less portable (user might not want to share which repo). Consider making it optional or using generic "my project" language. + +2. **Profile share: include top repo?** Should the profile share card mention the dominant repo, or stay fully aggregated? + +3. **Social platform priority?** Which platforms should be most prominent? Twitter/X and LinkedIn seem highest priority for developers. + +4. **Share tracking?** Should we track share actions for analytics? Need to balance insights with privacy. + +5. **Public profiles?** This PRD focuses on owner-facing share. Public profile pages (viewable without auth) are a future feature that would build on this work. + +--- + +## Appendix: Current vs. Proposed Component Structure + +### Current (Inline) + +``` +apps/web/src/app/analysis/[jobId]/AnalysisClient.tsx +├── Share card rendered inline (~100 lines) +├── Share actions rendered inline (~150 lines) +├── createShareSvg() function (~80 lines) +└── All share state managed locally +``` + +### Proposed (Componentized) + +``` +apps/web/src/components/share/ +├── ShareCard.tsx # Reusable card component +├── ShareActions.tsx # Share button bar +├── ShareImageGenerator.ts # SVG/PNG generation +├── types.ts # Shared types +└── index.ts # Exports + +apps/web/src/app/analysis/[jobId]/AnalysisClient.tsx +└── Uses and + +apps/web/src/app/page.tsx +└── Uses and +``` + +--- + +*This PRD focuses on the share experience layer. See `PRD.md` for core analysis and `PRD-vibed-ux.md` for overall UX direction.* diff --git a/packages/db/src/database.types.ts b/packages/db/src/database.types.ts index da50750..87580dd 100644 --- a/packages/db/src/database.types.ts +++ b/packages/db/src/database.types.ts @@ -374,23 +374,32 @@ export type Database = { Row: { created_at: string | null id: string + llm_key_source: string | null + llm_model: string | null profile_snapshot: Json trigger_job_id: string | null user_id: string + version_number: number | null } Insert: { created_at?: string | null id?: string + llm_key_source?: string | null + llm_model?: string | null profile_snapshot: Json trigger_job_id?: string | null user_id: string + version_number?: number | null } Update: { created_at?: string | null id?: string + llm_key_source?: string | null + llm_model?: string | null profile_snapshot?: Json trigger_job_id?: string | null user_id?: string + version_number?: number | null } Relationships: [ { @@ -416,11 +425,15 @@ export type Database = { created_at: string | null id: string job_ids: string[] + llm_key_source: string | null + llm_model: string | null + narrative_json: Json | null persona_confidence: string persona_id: string persona_name: string persona_score: number persona_tagline: string | null + regenerating: boolean repo_personas_json: Json total_commits: number total_repos: number @@ -433,11 +446,15 @@ export type Database = { created_at?: string | null id?: string job_ids?: string[] + llm_key_source?: string | null + llm_model?: string | null + narrative_json?: Json | null persona_confidence: string persona_id: string persona_name: string persona_score?: number persona_tagline?: string | null + regenerating?: boolean repo_personas_json?: Json total_commits?: number total_repos?: number @@ -450,11 +467,15 @@ export type Database = { created_at?: string | null id?: string job_ids?: string[] + llm_key_source?: string | null + llm_model?: string | null + narrative_json?: Json | null persona_confidence?: string persona_id?: string persona_name?: string persona_score?: number persona_tagline?: string | null + regenerating?: boolean repo_personas_json?: Json total_commits?: number total_repos?: number From bfcb253624f6f99e901b4d8c6b978cb7463c7568 Mon Sep 17 00:00:00 2001 From: Abou Kone Date: Wed, 21 Jan 2026 17:03:19 -0500 Subject: [PATCH 4/5] refactor: update terminology and branding to "Vibe Coding Profile" - Renamed instances of "Vibed" to "Vibe Coding Profile" across various components and documentation for consistency. - Updated metadata, titles, and descriptions to reflect the new branding. - Enhanced user interface elements to align with the new terminology, including headers and navigation links. - Adjusted share functionality to incorporate the new branding in share cards and export formats. - Revised documentation to clarify the changes and ensure alignment with the updated branding. --- apps/web/src/app/AppHeader.tsx | 4 +- .../app/analysis/[jobId]/AnalysisClient.tsx | 26 ++++----- apps/web/src/app/analysis/page.tsx | 6 +- apps/web/src/app/layout.tsx | 14 ++--- apps/web/src/app/login/page.tsx | 2 +- apps/web/src/app/methodology/page.tsx | 2 +- apps/web/src/app/page.tsx | 36 +++++++----- apps/web/src/app/security/page.tsx | 8 +-- apps/web/src/app/settings/llm-keys/page.tsx | 2 +- .../components/share/ProfileShareSection.tsx | 8 +-- apps/web/src/components/share/ShareCard.tsx | 2 +- apps/web/src/components/share/share-image.ts | 8 +-- docs/PRD-profile-aggregation.md | 10 ++-- docs/PRD-vibed-ux.md | 58 +++++++++---------- docs/PRD-vibed.md | 18 +++--- docs/PRD.md | 14 ++--- docs/architecture/inngest-integration.md | 2 +- docs/architecture/llm-narrative-workflow.md | 4 +- docs/architecture/overview.md | 18 +++--- docs/architecture/vibed-analysis-pipeline.md | 6 +- docs/github-setup.md | 6 +- docs/how-vibed-works.md | 20 +++---- .../multi-agent-pr-metadata-ingestion.md | 2 +- docs/implementation-trackers/vibed.md | 14 ++--- docs/llm-setup.md | 10 ++-- docs/prd-llm-provider-abstraction.md | 2 +- docs/prd-multi-platform-integration.md | 16 ++--- docs/prd/multi-agent-detection.md | 2 +- docs/prd/share-experience-improvements.md | 24 ++++---- docs/research/ai-era-coding-personas.md | 2 +- .../multi-agent-vibe-coding-patterns.md | 12 ++-- docs/research/persona-archetypes.md | 4 +- docs/research/persona-insight-synthesis.md | 6 +- 33 files changed, 186 insertions(+), 182 deletions(-) diff --git a/apps/web/src/app/AppHeader.tsx b/apps/web/src/app/AppHeader.tsx index f9062c4..271f669 100644 --- a/apps/web/src/app/AppHeader.tsx +++ b/apps/web/src/app/AppHeader.tsx @@ -25,7 +25,7 @@ export default function AppHeader(props: { const baseLinks = props.isAuthed ? [ - { href: "/", label: "My Vibed" }, + { href: "/", label: "My VCP" }, { href: "/repos", label: "Repos" }, { href: "/analysis", label: "Reports" }, { href: "/settings/llm-keys", label: "Settings" }, @@ -54,7 +54,7 @@ export default function AppHeader(props: { > - Vibed Coding + Vibe Coding Profile
-

Your Vibed profile

+

Your Unified VCP

{typeof profileContribution.profileTotalRepos === "number" ? `${profileContribution.profileTotalRepos} repos` diff --git a/apps/web/src/app/analysis/page.tsx b/apps/web/src/app/analysis/page.tsx index 182f029..e187b01 100644 --- a/apps/web/src/app/analysis/page.tsx +++ b/apps/web/src/app/analysis/page.tsx @@ -102,13 +102,13 @@ export default async function AnalysisIndexPage() {

- Vibed Repos + Repo VCPs

- Your Vibed Repos + Your Repo VCPs

- Each vibed repo reveals your coding persona, confidence level, and the evidence behind it. + Each Repo VCP reveals your coding persona, confidence level, and the evidence behind it.

diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 794343b..de0eb18 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -21,24 +21,24 @@ const geistMono = Geist_Mono({ const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:8108"; export const metadata: Metadata = { - title: "Vibed Coding · Your AI coding profile", + title: "Vibe Coding Profile · Your AI coding profile", description: - "Turn your commit history into a Vibed Coding Persona — patterns, insights, and shareable profiles grounded in commit evidence.", + "Turn your commit history into a Vibe Coding Profile (VCP) — patterns, insights, and shareable profiles grounded in commit evidence.", metadataBase: new URL(appUrl), openGraph: { - title: "Vibed Coding · Your AI coding profile", + title: "Vibe Coding Profile · Your AI coding profile", description: - "A shareable Vibed Coding Persona from your commit history — patterns and insights grounded in evidence.", + "A shareable Vibe Coding Profile (VCP) from your commit history — patterns and insights grounded in evidence.", url: appUrl, locale: "en_US", type: "website", - siteName: "Vibed Coding", + siteName: "Vibe Coding Profile", }, twitter: { card: "summary_large_image", - title: "Vibed Coding · Your AI coding profile", + title: "Vibe Coding Profile · Your AI coding profile", description: - "A shareable Vibed Coding Persona from your commit history — patterns and insights grounded in evidence.", + "A shareable Vibe Coding Profile (VCP) from your commit history — patterns and insights grounded in evidence.", }, }; diff --git a/apps/web/src/app/login/page.tsx b/apps/web/src/app/login/page.tsx index f39b206..cbe9fc0 100644 --- a/apps/web/src/app/login/page.tsx +++ b/apps/web/src/app/login/page.tsx @@ -7,7 +7,7 @@ export default function LoginPage() {

- Vibed Coding + Vibe Coding Profile

Sign in to analyze your build patterns from git history. diff --git a/apps/web/src/app/methodology/page.tsx b/apps/web/src/app/methodology/page.tsx index 01a1635..888cd68 100644 --- a/apps/web/src/app/methodology/page.tsx +++ b/apps/web/src/app/methodology/page.tsx @@ -14,7 +14,7 @@ export default async function MethodologyPage() { How we compute your persona

- Vibed infers your Vibe Coding persona by spotting AI-assisted engineering patterns in the + Vibe Coding Profile infers your Vibe Coding persona by spotting AI-assisted engineering patterns in the Git history of repos you connect.

diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 43523ca..ba4d1e5 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -16,7 +16,7 @@ import { } from "@vibed/core"; const heroFeatures = [ - "A Vibed profile built from AI-assisted engineering signals in your commit history", + "A Vibe Coding Profile (VCP) built from AI-assisted engineering signals in your commit history", "Persona snapshots that evolve as you add more repos", "Share-ready cards that highlight your vibe coding patterns", "Deep dive metrics and evidence when you want receipts", @@ -49,9 +49,14 @@ const timeline = [ "If you opt in, we summarize patterns using metadata only—never code or message content.", }, { - title: "Aggregate your profile", + title: "Create Repo VCP", description: - "Multiple repos roll into a single Vibed profile, weighted by commit volume.", + "Each repo gets its own Repo VCP—a snapshot of your vibe coding style for that project.", + }, + { + title: "Aggregate into Unified VCP", + description: + "Multiple Repo VCPs roll into your Unified VCP, weighted by commit volume across all repos.", }, ]; @@ -828,13 +833,12 @@ function MarketingLanding() {

- Find your Vibed AI-assisted engineering profile + Find your Vibe Coding Profile (VCP) {" "} and the personality behind your workflow

- Vibed is a playful experiment by vibe coders who want to understand their AI-assisted - engineering patterns. We surface signals from your commit history to shine a light on + A Vibe Coding Profile (VCP) is your AI-assisted engineering persona—revealed through patterns in your commit history. We surface signals from your git history to shine a light on how you build with AI, what feels like you, what feels new, and how your workflow is evolving.

@@ -937,7 +941,7 @@ function MarketingLanding() {
-

Vibed Coding

+

Vibe Coding Profile

v0.1.0

@@ -1147,7 +1151,7 @@ function AuthenticatedDashboard({

- Your Vibed Profile + Your Unified VCP

{stats.completedJobs > 0 ? ( @@ -1158,12 +1162,12 @@ function AuthenticatedDashboard({ > Add repo - - View vibed repos - + + View Repo VCPs + ) : (

{stats.completedJobs}

- Vibed repos + Repo VCPs

@@ -1473,7 +1477,7 @@ function AuthenticatedDashboard({ Add repo - View vibed repos + View Repo VCPs )} diff --git a/apps/web/src/app/security/page.tsx b/apps/web/src/app/security/page.tsx index c064c03..8acd537 100644 --- a/apps/web/src/app/security/page.tsx +++ b/apps/web/src/app/security/page.tsx @@ -39,7 +39,7 @@ export default function SecurityPage() {

What we do

- Vibed Coding connects to GitHub, fetches commit history, computes metrics, and stores the + Vibe Coding Profile connects to GitHub, fetches commit history, computes metrics, and stores the results in Supabase Postgres. Access to rows is scoped to your authenticated account via Supabase Row Level Security (RLS) policies for user-facing reads.

@@ -66,7 +66,7 @@ export default function SecurityPage() {

Note: when fetching commit details from GitHub, the API response can include file - paths and diff snippets. Vibed Coding’s current worker extracts and stores counts/stats + paths and diff snippets. Vibe Coding Profile’s current worker extracts and stores counts/stats (like number of files and additions/deletions), not file contents.

@@ -88,7 +88,7 @@ export default function SecurityPage() {

Recommendations

- Vibed Coding is designed for personal insight. Keep the blast radius small. + Vibe Coding Profile is designed for personal insight. Keep the blast radius small.

    {recommendations.map((item) => ( @@ -106,7 +106,7 @@ export default function SecurityPage() {