Automating Documentation Quality with LLMs: Our Journey to the Docs Pipeline
We just shipped a major feature: a comprehensive Docs Pipeline that scans repositories, analyzes doc quality with LLMs, suggests improvements, and even creates GitHub PRs. Dive into the architectural decisions, the nitty-gritty implementation, and the lessons we learned along the way.
Documentation is the lifeblood of any growing project, but let's be honest – it's often the first thing to fall by the wayside. Keeping docs up-to-date, consistent, and high-quality across multiple repositories and projects is a Herculean task. What if we could automate a significant chunk of that effort?
That's precisely the challenge we tackled this past session, culminating in the complete implementation of our new Docs Pipeline. It's an ambitious feature designed to bring sanity to documentation management, leveraging the power of Large Language Models (LLMs) to not just find problems, but to fix them.
A Bold Vision: The Automated Docs Pipeline
Our goal was clear: build an end-to-end system that could:
- Scan repositories for all relevant documentation files.
- Analyze their quality and relevance using an LLM.
- Improve any docs falling below a defined threshold, again with LLM assistance.
- Create GitHub Pull Requests with these improvements, ready for human review.
It's a multi-phase operation, and I'm thrilled to report that all six phases are now complete and committed. The codebase is type-check clean, lint clean, and boasts a perfect 180/180 test pass rate. That's a solid 33b4a7a milestone!
Under the Hood: Deconstructing the Pipeline
Building a feature of this scope required touching almost every layer of our stack. Here's a breakdown of the key components and architectural decisions:
1. Data Model & Persistence
To track pipeline runs and individual document items, we extended our Prisma schema:
DocsRun: Represents a single execution of the pipeline, linked to a User, Tenant, Repository, and Project.DocsItem: Represents an individual documentation file processed within aDocsRun, including its status and improvement details.
// Excerpt from prisma/schema.prisma
model DocsRun {
id String @id @default(uuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
status DocsRunStatus
phase DocsPipelinePhase
progress Float
// ... relations to User, Tenant, Repository, Project
docsItems DocsItem[]
}
model DocsItem {
id String @id @default(uuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
filePath String
format DocFormat
category DocCategory
status DocsItemStatus
originalContent String?
improvedContent String?
prUrl String?
// ... relations to DocsRun
}
We also added robust Row-Level Security (RLS) policies for docs_runs and docs_items in prisma/rls.sql to ensure data isolation and security, a critical consideration for multi-tenant applications.
2. The Core Services: The Brains of the Operation
The heart of the Docs Pipeline lies in a suite of dedicated services, each responsible for a specific phase:
src/server/services/docs/doc-scanner.ts: ThisAsyncGeneratoris our repo explorer. It uses the GitHub API to traverse repository trees, identifying potential documentation files. It also intelligently classifies documents byDocCategory(e.g., README, API Reference, How-To Guide) based on heuristics.src/server/services/docs/relevance-scorer.ts: Before handing documents to the LLM, we apply heuristic scoring.memoryRelevanceandqualityRelevancehelp us prioritize which documents need attention most, saving LLM tokens for the most impactful improvements.src/server/services/docs/doc-analyzer.ts: This is where the LLM magic begins. We batch documents (respecting a 50K character budget per batch) and send them to the LLM for a comprehensive quality analysis. The LLM identifies areas for improvement, inconsistencies, and clarity issues.src/server/services/docs/doc-improver.ts: Once analyzed, documents flagged for improvement enter this service. The LLM, enriched with relevant context (memory + consolidation), generates improved versions of the documentation, focusing on clarity, completeness, and adherence to best practices.src/server/services/docs/create-prs.ts: The final mile! This service takes the improved documents and programmatically creates GitHub Pull Requests, proposing the changes back to the original repository. Each PR is linked to itsDocsItem.src/server/services/docs/pipeline.ts: The grand orchestrator. This mainAsyncGeneratorties all the above services together, managing the flow from scanning to PR creation, ensuring smooth transitions between phases and robust error handling.
3. API & User Interface
To make this pipeline accessible and observable, we built:
src/app/api/v1/events/docs-pipeline/[id]/route.ts: A Server-Sent Events (SSE) streaming endpoint. This allows our frontend to receive real-time updates on pipeline progress, showing which phase is active and the status of individualDocsItems.src/server/trpc/routers/docs-pipeline.ts: A robust tRPC router with 7 procedures:list,get,start,cancel,skipItem,retryPRs, andgenerateReport. This gives us full control over pipeline execution from the frontend.src/app/(dashboard)/dashboard/docs-pipeline/page.tsx: The main dashboard page for viewing a list of allDocsRuns and launching new ones via an intuitive dialog.src/app/(dashboard)/dashboard/docs-pipeline/[id]/page.tsx: A detailed view for a specific pipeline run, featuring the SSE-powered phase progress bar and a table displaying the status of eachDocsItem.
We also added a "Docs Pipeline" navigation item to our sidebar, making it easily discoverable within the application.
Navigating the Bumps: Lessons Learned
No complex feature development is without its quirks. Here are a couple of "aha!" moments and critical fixes that emerged during this session:
Lesson 1: LLM API Consistency is Key
- The Problem: While implementing
doc-analyzer.tsanddoc-improver.ts, I instinctively reached forllm.chat(). However, TypeScript immediately flaggedTS2339: Property 'chat' does not exist on type 'LLMProvider'. - The Fix: A quick check of our
LLMProviderinterface revealed that the standard method for general LLM calls in our codebase isllm.complete(). - Takeaway: Even with strong typing, muscle memory can lead you astray. Always refer to the defined interfaces and contracts, especially when working with shared service layers. This ensures consistency and prevents runtime surprises.
Lesson 2: Extending Union Types for Dynamic Context
- The Problem: Our
pipeline-context.tsmodule is responsible for loading summaries of previous pipeline runs. It had a predefined union type forpipelineType("autofix" | "refactor"). When I tried to use"docs"as the newpipelineType, TypeScript rightfully complained:TS2345: Argument of type '"docs"' is not assignable to parameter of type '"autofix" | "refactor"'. - The Fix: We extended the
pipelineTypeunion to include"docs"and added a newelse-ifbranch inloadPreviousRunSummaries()to query theDocsRunmodel specifically. - Takeaway: As your application grows and new features introduce new types of entities or processes, remember to revisit and extend existing type definitions, especially union types, to maintain type safety and ensure all code paths are correctly handled. This often goes hand-in-hand with updating data fetching logic.
What's Next on the Roadmap?
While the core pipeline is operational, our journey isn't over. Immediate next steps include:
- Project-Level Docs Management: Extending the
projects.docssub-router withscan,create,update,delete, andenrichprocedures to manage documentation directly within project contexts. - Enhanced Project Docs UI: Building out the UI for the project's Docs tab, allowing users to toggle all files, see relevance badges, perform CRUD actions, and trigger enrichment dialogs.
- Unit Tests for
relevance-scorer.ts: This pure function is an ideal candidate for focused unit tests to ensure its heuristics are always performing as expected. - End-to-End Test: A crucial E2E test to simulate starting a docs pipeline and verifying the SSE streaming through all phases, ensuring the entire flow works seamlessly.
- RLS Policy Application: A reminder to apply the RLS policies to the production database:
psql -f prisma/rls.sql.
This Docs Pipeline represents a significant leap forward in our mission to help developers maintain high-quality codebases. We're excited about the potential it unlocks for automated documentation, freeing up valuable developer time to focus on building new features.
{
"thingsDone": [
"Implemented DocsRun and DocsItem models with RLS",
"Created core services: doc-scanner, relevance-scorer, doc-analyzer, doc-improver, create-prs",
"Developed main AsyncGenerator orchestrator (pipeline.ts)",
"Built SSE streaming endpoint for real-time updates",
"Implemented tRPC router with 7 procedures for pipeline control",
"Designed and implemented dashboard UI for pipeline runs and details",
"Extended GitHub connector for file deletion",
"Integrated docs context into report generation",
"Updated pipeline context for 'docs' pipelineType",
"Added 'Docs Pipeline' navigation item"
],
"pains": [
"Misuse of LLMProvider.chat() instead of LLMProvider.complete()",
"Attempting to use new 'docs' pipelineType without extending existing union type in pipeline-context"
],
"successes": [
"All 6 phases of Docs Pipeline feature complete",
"Codebase is type-check clean and lint clean",
"180/180 tests passing",
"Successfully integrated LLM-powered analysis and improvement",
"Real-time progress updates via SSE",
"Robust data model and security with RLS"
],
"techStack": [
"TypeScript",
"Next.js",
"React",
"Prisma",
"PostgreSQL",
"tRPC",
"LLMs (Large Language Models)",
"GitHub API",
"SSE (Server-Sent Events)",
"AsyncGenerator"
]
}