Claude Code Plugins

Community-maintained marketplace

Feedback

Composition Data Modeling

@nategarelik/composition
0
0

Design and work with composition data structures. Use when defining schemas, creating database models, building API responses, or transforming composition data between formats.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name Composition Data Modeling
description Design and work with composition data structures. Use when defining schemas, creating database models, building API responses, or transforming composition data between formats.

Composition Data Modeling Skill

Purpose

This skill provides patterns for modeling composition data - the hierarchical structure that represents what things are made of, from product down to elements.

Core Data Model

TypeScript Types

// types/composition.ts

export type ConfidenceLevel = 'verified' | 'estimated' | 'speculative'
export type CompositionType = 'product' | 'component' | 'material' | 'chemical' | 'element'

export interface CompositionNode {
  id: string
  name: string
  description?: string
  type: CompositionType

  // Quantity
  percentage: number
  percentageRange?: [number, number]  // For uncertain values
  unit?: string                        // 'weight' | 'volume' | 'count'

  // Confidence & Source
  confidence: ConfidenceLevel
  source?: string
  sourceUrl?: string

  // Hierarchy
  children?: CompositionNode[]

  // Element-specific (when type === 'element')
  symbol?: string           // Periodic table symbol
  atomicNumber?: number

  // Visual config
  visualConfig?: {
    color?: string
    material?: 'metal' | 'glass' | 'organic' | 'standard'
    modelUrl?: string
  }

  // Metadata
  metadata?: Record<string, unknown>
}

export interface Composition {
  id: string
  query: string              // Original search query
  name: string               // Identified product name
  category: string           // Product category
  description?: string

  root: CompositionNode      // Top-level composition tree

  sources: Source[]
  confidence: ConfidenceLevel  // Overall confidence

  // Timestamps
  createdAt: Date
  updatedAt: Date
  researchedAt: Date         // When AI research was performed
}

export interface Source {
  id: string
  url: string
  title: string
  type: 'official' | 'scientific' | 'analysis' | 'industry' | 'secondary'
  accessedAt: Date
  reliability: number        // 0-1 score
}

Prisma Schema

// prisma/schema.prisma

model Composition {
  id            String   @id @default(cuid())
  query         String   @db.Text
  queryNorm     String   // Normalized for search
  name          String
  category      String
  description   String?  @db.Text

  // Store full tree as JSON
  rootData      Json     @map("root_data")
  sourcesData   Json     @map("sources_data")

  confidence    String   // 'verified' | 'estimated' | 'speculative'

  // Stats
  viewCount     Int      @default(0)
  shareCount    Int      @default(0)

  // Timestamps
  createdAt     DateTime @default(now()) @map("created_at")
  updatedAt     DateTime @updatedAt @map("updated_at")
  researchedAt  DateTime @map("researched_at")

  // Relations
  shares        Share[]

  @@index([queryNorm])
  @@index([category])
  @@index([viewCount(sort: Desc)])
  @@map("compositions")
}

model Share {
  id            String      @id @default(cuid())
  shortCode     String      @unique
  compositionId String      @map("composition_id")
  composition   Composition @relation(fields: [compositionId], references: [id])

  // Share config
  depthLevel    Int         @default(4) @map("depth_level")
  viewMode      String      @default("exploded") @map("view_mode")

  // Stats
  viewCount     Int         @default(0) @map("view_count")

  createdAt     DateTime    @default(now()) @map("created_at")

  @@index([shortCode])
  @@map("shares")
}

API Response Formats

Search Response

interface SearchResponse {
  success: true
  data: {
    composition: Composition
    cached: boolean
    researchTime?: number  // ms, if freshly researched
  }
}

interface SearchErrorResponse {
  success: false
  error: {
    code: 'INVALID_QUERY' | 'RESEARCH_FAILED' | 'RATE_LIMITED'
    message: string
  }
}

// For long-running research
interface SearchPendingResponse {
  success: true
  status: 'researching'
  jobId: string
  estimatedTime: number  // seconds
  progress?: {
    stage: 'identifying' | 'researching' | 'synthesizing'
    percentage: number
  }
}

Composition Response

interface CompositionResponse {
  success: true
  data: {
    composition: Composition
    // Flattened for easier rendering
    nodes: CompositionNode[]
    maxDepth: number
    totalElements: number
  }
}

Data Transformations

Flatten Tree for Rendering

function flattenComposition(
  node: CompositionNode,
  depth = 0,
  parentId?: string
): FlatNode[] {
  const flat: FlatNode = {
    ...node,
    depth,
    parentId,
    childIds: node.children?.map(c => c.id) ?? []
  }

  const children = node.children?.flatMap(child =>
    flattenComposition(child, depth + 1, node.id)
  ) ?? []

  return [flat, ...children]
}

Calculate Totals by Depth

function calculateDepthTotals(root: CompositionNode): DepthSummary[] {
  const summaries: Map<number, DepthSummary> = new Map()

  function traverse(node: CompositionNode, depth: number) {
    const summary = summaries.get(depth) ?? {
      depth,
      nodeCount: 0,
      types: {}
    }
    summary.nodeCount++
    summary.types[node.type] = (summary.types[node.type] ?? 0) + 1
    summaries.set(depth, summary)

    node.children?.forEach(c => traverse(c, depth + 1))
  }

  traverse(root, 0)
  return Array.from(summaries.values())
}

Filter by Depth Level

function filterByDepth(
  node: CompositionNode,
  maxDepth: number,
  currentDepth = 0
): CompositionNode {
  if (currentDepth >= maxDepth) {
    return { ...node, children: undefined }
  }

  return {
    ...node,
    children: node.children?.map(c =>
      filterByDepth(c, maxDepth, currentDepth + 1)
    )
  }
}

Calculate Element Totals

function calculateElementTotals(
  node: CompositionNode,
  parentPercentage = 100
): Map<string, number> {
  const totals = new Map<string, number>()
  const effectivePercentage = (node.percentage / 100) * parentPercentage

  if (node.type === 'element' && node.symbol) {
    totals.set(node.symbol, effectivePercentage)
  }

  node.children?.forEach(child => {
    const childTotals = calculateElementTotals(child, effectivePercentage)
    childTotals.forEach((value, key) => {
      totals.set(key, (totals.get(key) ?? 0) + value)
    })
  })

  return totals
}

Caching Strategy

Redis Cache Keys

// Hot cache for popular queries
`composition:query:${normalizeQuery(query)}`  // TTL: 1 hour

// Composition by ID
`composition:id:${id}`  // TTL: 24 hours

// Popular compositions list
`compositions:popular`  // TTL: 15 minutes

// Search suggestions
`search:suggestions:${prefix}`  // TTL: 1 hour

Cache Invalidation

async function invalidateCompositionCache(id: string, query: string) {
  await redis.del([
    `composition:id:${id}`,
    `composition:query:${normalizeQuery(query)}`,
    'compositions:popular'
  ])
}

Validation

import { z } from 'zod'

const CompositionNodeSchema: z.ZodType<CompositionNode> = z.lazy(() =>
  z.object({
    id: z.string(),
    name: z.string().min(1),
    description: z.string().optional(),
    type: z.enum(['product', 'component', 'material', 'chemical', 'element']),
    percentage: z.number().min(0).max(100),
    percentageRange: z.tuple([z.number(), z.number()]).optional(),
    confidence: z.enum(['verified', 'estimated', 'speculative']),
    source: z.string().optional(),
    sourceUrl: z.string().url().optional(),
    children: z.array(CompositionNodeSchema).optional(),
    symbol: z.string().length(1, 2).optional(),
    atomicNumber: z.number().int().positive().optional(),
    visualConfig: z.object({
      color: z.string().optional(),
      material: z.enum(['metal', 'glass', 'organic', 'standard']).optional(),
      modelUrl: z.string().url().optional()
    }).optional(),
    metadata: z.record(z.unknown()).optional()
  })
)

export const CompositionSchema = z.object({
  id: z.string(),
  query: z.string().min(2),
  name: z.string().min(1),
  category: z.string(),
  description: z.string().optional(),
  root: CompositionNodeSchema,
  sources: z.array(z.object({
    id: z.string(),
    url: z.string().url(),
    title: z.string(),
    type: z.enum(['official', 'scientific', 'analysis', 'industry', 'secondary']),
    accessedAt: z.date(),
    reliability: z.number().min(0).max(1)
  })),
  confidence: z.enum(['verified', 'estimated', 'speculative']),
  createdAt: z.date(),
  updatedAt: z.date(),
  researchedAt: z.date()
})