← back to ClawCoder

src/lib/analyzer/matcher.ts

61 lines

import { PROFILES } from '../profiles/data'
import type { Profile } from '../profiles/types'

/**
 * Extract meaningful keywords from a text block for comparison.
 * Strips markdown syntax and returns lowercased unique words.
 */
function extractKeywords(text: string): Set<string> {
  const cleaned = text
    .toLowerCase()
    .replace(/[#*`\[\](){}|>_~]/g, ' ')
    .replace(/https?:\/\/\S+/g, '')
    .replace(/[^a-z0-9\s-]/g, ' ')

  const words = cleaned.split(/\s+/).filter(w => w.length > 2)
  return new Set(words)
}

/**
 * Calculate Jaccard similarity between two sets.
 */
function jaccardSimilarity(a: Set<string>, b: Set<string>): number {
  let intersection = 0
  for (const word of a) {
    if (b.has(word)) intersection++
  }
  const union = new Set([...a, ...b]).size
  return union > 0 ? intersection / union : 0
}

/**
 * Match CLAUDE.md content against all 15 profiles and return
 * the slug of the closest matching profile.
 */
export function detectProfile(content: string): string | undefined {
  const contentKeywords = extractKeywords(content)

  if (contentKeywords.size === 0) return undefined

  let bestSlug: string | undefined
  let bestScore = 0

  for (const profile of PROFILES) {
    const templateKeywords = extractKeywords(profile.template.content)

    // Also include persona keywords for better matching
    const personaKeywords = extractKeywords(profile.persona)
    const combined = new Set([...templateKeywords, ...personaKeywords])

    const score = jaccardSimilarity(contentKeywords, combined)

    if (score > bestScore) {
      bestScore = score
      bestSlug = profile.slug
    }
  }

  // Only return a match if similarity is above a minimum threshold
  return bestScore > 0.05 ? bestSlug : undefined
}