nyxcore-systems
8 min read

From Scattered Notes to AI-Powered Insights: Building Our Cross-Project Consolidation Feature

We just shipped a powerful new feature that brings AI-driven insights to scattered project notes. Dive into the architectural decisions, the code, and the surprising lessons we learned building it.

AITypeScriptPrismatRPCNext.jsSoftwareArchitectureDeveloperExperienceCodeGeneration

The Never-Ending Quest for Cohesion

As developers, we're constantly generating knowledge: design docs, daily standup notes, bug reports, feature ideas, personal reflections, and more. Often, this valuable context gets fragmented across projects, individual notes, or even different tools. We recently tackled this head-on with a new feature: Cross-Project Consolidation.

Our goal was ambitious: accumulate all those scattered "letters to myself" or internal blog posts across various projects, leverage AI to extract recurring patterns, create a searchable overview, and then export these insights as prompt hints for future work. Think of it as distilling the collective wisdom of your entire development history into actionable intelligence.

I'm thrilled to report that the core feature is now complete, committed (252204a), and pushed to origin/main. TypeScript clean, zero new errors – ready for end-to-end testing! This post dives into how we built it, the architecture, and the critical lessons we learned along the way.

Under the Hood: Architecting for AI-Powered Insights

Building a feature of this scope required careful consideration across our stack (Next.js, tRPC, Prisma, TypeScript). Here's a walkthrough of the key components:

1. The Data Foundation: Prisma Models

At the heart of the system are two new Prisma models:

  • Consolidation: Represents a single consolidation run, linking to multiple projects and containing metadata.
  • ConsolidationPattern: Stores the AI-extracted patterns, linked back to the Consolidation and including details like type, severity, and the raw pattern text.
prisma
// prisma/schema.prisma
model Consolidation {
  id           String    @id @default(uuid())
  tenantId     String
  userId       String
  // ... other fields like status, name, description
  projects     Project[] // Many-to-many relationship with projects
  patterns     ConsolidationPattern[]

  @@index([tenantId, userId])
}

model ConsolidationPattern {
  id             String    @id @default(uuid())
  consolidationId String
  consolidation  Consolidation @relation(fields: [consolidationId], references: [id], onDelete: Cascade)
  type           String // e.g., "Refactor", "Improvement", "Bug"
  severity       String // e.g., "High", "Medium", "Low"
  patternText    String
  // ... other fields

  @@index([consolidationId])
}

We ensured proper tenant/user relations and onDelete: Cascade for clean data management.

2. The AI Brain: consolidation-service.ts

This is where the magic happens. Our ConsolidationService encapsulates the LLM interactions:

  • extractPatterns(inputData: string): Takes accumulated text from various projects, sends it to our AI, and expects structured JSON output. For now, it's a temp 0.2 model, but the structured output ensures consistency.
  • generatePromptHints(patterns: ConsolidationPattern[]): Transforms the extracted patterns into actionable markdown, grouped by category, ready to be used as prompt engineering hints.

3. The API Layer: consolidation.ts tRPC Router

Our tRPC router for consolidation is comprehensive, exposing a full suite of endpoints:

  • CRUD operations for consolidations.
  • generate / regenerate: Our llmProcedure handles the orchestration of data collection, AI extraction, and saving patterns. This custom tRPC procedure ensures proper context for LLM calls.
  • patterns.search: A powerful endpoint for paginated and filterable searching across extracted patterns.
  • export: Supports exporting patterns in various formats: markdown, JSON, and specific "prompt hints" markdown.
  • availableProjects: Fetches projects eligible for consolidation.
  • byProject: Lists consolidations related to a specific project.

4. The User Experience: Next.js Pages & Components

We focused on a smooth, intuitive UI for managing and interacting with consolidations:

  • Sidebar Navigation: A new "Consolidation" link with a Layers icon now lives in src/components/layout/sidebar.tsx.
  • List Page (/dashboard/consolidation): Shows all consolidations with clear status indicators and pattern badges.
  • New Consolidation Form (/dashboard/consolidation/new): A multi-step machine guides users through selecting projects and initiating the consolidation process, featuring a sticky submit button for improved UX.
  • Detail Page (/dashboard/consolidation/[id]): This is the hub, featuring three tabs:
    • Overview: Summary of the consolidation and distribution bars for pattern types/severities.
    • Patterns: The core interaction – searchable patterns with type filter chips and expandable cards for detailed views.
    • Export: Allows users to copy prompt hints to clipboard, download markdown/JSON files, and preview the output.
  • Project Integration (/dashboard/projects/[id]): We added a fourth "Analysis" tab to individual project pages, showing consolidations relevant to that project and providing a direct "Analyze Patterns" flow.

Lessons from the Trenches: What We Learned Along the Way

No feature ships without its share of challenges. Here are some critical lessons that emerged during this development cycle:

Lesson 1: Prisma's JSON Type - A Type Safety Gotcha

One of the trickiest parts was dealing with Prisma's Json type in our schema. While TypeScript allows Record<string, unknown> for flexible JSON objects, Prisma's client expects something more specific when writing to the database.

The Problem:

typescript
// This often seemed to work in dev, but failed on type checking or runtime in specific scenarios
const data: Record<string, unknown> = { key: 'value' };
// ... later, trying to assign data to a Prisma Json field

This resulted in type errors: Type 'Record<string, unknown>' is not assignable to type 'Prisma.InputJsonValue'.

The Solution: Always explicitly cast to Prisma.InputJsonValue when assigning a generic object to a Json field. For null values, use Prisma.JsonNull.

typescript
import { Prisma } from '@prisma/client';

const myJsonData: Record<string, unknown> = { someKey: 'someValue' };

// When creating or updating a record with a Json field:
await prisma.myModel.create({
  data: {
    jsonField: myJsonData as Prisma.InputJsonValue,
    // For null, use Prisma.JsonNull
    anotherJsonField: null ? Prisma.JsonNull : anotherValue as Prisma.InputJsonValue,
  },
});

This seemingly small detail can save hours of debugging type mismatches between your application logic and the Prisma client's expectations.

Lesson 2: When AI Agents Hit Their Limits - Complexity vs. Automation

We've been experimenting with AI agents to accelerate UI development (e.g., ui-new-page, ui-detail-page). While simpler tasks, like generating a list page or a new tab on an existing page, completed flawlessly, the more complex pages proved challenging.

The agents for the new form (with its step machine and multi-selector) and the detail page (with its three tabs, search, filters, and export functionality) simply stalled. They never wrote their files.

Takeaway: There's a sweet spot for AI-assisted code generation. For highly structured, repetitive, or less complex components, agents are productivity boosters. However, when the page specification involves intricate state management, complex interactions, or novel UI patterns, the current generation of agents struggles. For now, we manually wrote these more complex pages. This isn't a failure of AI, but a reminder that understanding its current limitations is key to effective integration. It means we need to refine our prompts and possibly break down complex UI tasks into smaller, more digestible chunks for the agents.

Lesson 3: Befriending the Linter - The Unsung Hero of Code Quality

This one was a pleasant surprise. Throughout the development, our linter (ESLint, Prettier) consistently applied auto-fixes that genuinely improved the codebase:

  • Removing unused utils variables from trpc.useUtils().
  • Adding type="button" to elements used as buttons but lacking the explicit type, preventing accidental form submissions.
  • Consistently using cn() for conditional Tailwind CSS classes, improving readability and maintainability.
  • Converting a PromptHintsPreview component to use useEffect with a cleanup function, ensuring proper resource management.

Takeaway: Don't fight the linter. It's not just there to enforce arbitrary rules; it's often a silent partner suggesting best practices and preventing subtle bugs. Embrace its auto-fixes, understand why it's suggesting them, and let it guide you towards a cleaner, more robust codebase. It's a fantastic pair programmer.

The Road Ahead

With the feature committed and pushed, our immediate next steps involve rigorous testing:

  1. End-to-end testing: Create a consolidation from the UI, verify AI extraction, and ensure patterns are saved correctly.
  2. Export validation: Test prompt hints copy-to-clipboard, markdown/JSON file downloads, and preview accuracy.
  3. Mobile responsiveness: Check layouts at 375px for sticky buttons, filter chip wrapping, and pattern card display.
  4. Flow testing: Validate the "Analyze Patterns" flow from the project detail page.

Looking further ahead, we're already considering integrating consolidation patterns directly into our workflow engine and adding a global search across all extracted patterns.

This feature represents a significant leap forward in how we manage and leverage knowledge across our development efforts. By turning scattered notes into AI-powered insights, we're building a system that truly learns from itself.

What are your experiences with similar knowledge consolidation challenges or AI-assisted development? Share your thoughts in the comments below!

json
{
  "thingsDone": [
    "Implemented Prisma models for Consolidation and ConsolidationPattern with relations and cascade delete.",
    "Developed `ConsolidationService` for AI pattern extraction and prompt hint generation.",
    "Created comprehensive tRPC `consolidationRouter` with CRUD, AI generation, search, and export functionalities.",
    "Integrated `consolidationRouter` into the main tRPC router.",
    "Added 'Consolidation' navigation to the sidebar with appropriate icon.",
    "Designed and implemented UI pages for consolidation listing, creation (multi-step form), and detail view (tabs for Overview, Patterns, Export).",
    "Extended project detail page with an 'Analysis' tab to show relevant consolidations."
  ],
  "pains": [
    "Encountered type incompatibility between `Record<string, unknown>` and Prisma's `Json?` type, requiring explicit casting to `Prisma.InputJsonValue` or `Prisma.JsonNull`.",
    "Experienced stalling issues with AI agents (`ui-new-page`, `ui-detail-page`) when generating complex UI components, necessitating manual development for intricate pages.",
    "Observed several linter auto-fixes that, while initially disruptive, consistently led to code quality improvements (e.g., adding `type=\"button\"`, using `cn()`, `useEffect` with cleanup)."
  ],
  "successes": [
    "Successfully shipped a complex, AI-driven cross-project feature to main.",
    "Achieved TypeScript cleanliness with zero new errors post-feature completion.",
    "Developed a robust and flexible API layer with tRPC supporting advanced search and export.",
    "Created an intuitive user interface with multi-step forms, tabbed views, and rich filtering capabilities.",
    "Gained valuable insights into Prisma's type system and the practical limitations/benefits of AI-assisted code generation."
  ],
  "techStack": [
    "Next.js",
    "TypeScript",
    "Prisma",
    "tRPC",
    "Tailwind CSS",
    "AI/LLM (internal service)",
    "ESLint",
    "Prettier"
  ]
}