brainy/docs/vfs/USER_FUNCTIONS.md
David Snelling 7730b88618 feat: add VFS methods and fix documentation accuracy
- Add exportToJSON() for directory structure export
- Add searchEntities() for advanced entity filtering
- Add bulkWrite() for efficient batch operations
- Fix VFS documentation to accurately reflect implementation
- Add USER_FUNCTIONS.md with domain-specific templates
- Clarify Knowledge Layer augmentation pattern
- Correct GitBridge integration examples
2025-09-25 12:12:20 -07:00

19 KiB

VFS User Functions - Templates and Examples

This document provides template functions that you can implement for domain-specific needs. These functions combine VFS primitives to solve common problems.

Table of Contents

  1. Code Analysis Functions
  2. Export Format Functions
  3. Project Management Functions
  4. Creative Writing Functions
  5. Game Development Functions

Code Analysis Functions

Get Dependency Graph

/**
 * Build a dependency graph for JavaScript/TypeScript projects
 */
async function getDependencyGraph(vfs, srcPath) {
  const files = await vfs.readdir(srcPath, { recursive: true })
  const graph = {}

  for (const file of files) {
    const filePath = `${srcPath}/${file}`

    // Only process JS/TS files
    if (file.match(/\.(js|ts|jsx|tsx)$/)) {
      const content = await vfs.readFile(filePath)
      const text = content.toString()

      // Parse imports (basic regex, use proper AST parser for production)
      const imports = []
      const importRegex = /import\s+.*?\s+from\s+['"](.+?)['"]/g
      const requireRegex = /require\(['"](.+?)['"]\)/g

      let match
      while ((match = importRegex.exec(text)) !== null) {
        imports.push(match[1])
      }
      while ((match = requireRegex.exec(text)) !== null) {
        imports.push(match[1])
      }

      graph[filePath] = imports
    }
  }

  return graph
}

// Use it
const deps = await getDependencyGraph(vfs, '/src')

Find Circular Dependencies

/**
 * Detect circular dependencies in your code
 */
async function findCircularDependencies(vfs, srcPath) {
  const graph = await getDependencyGraph(vfs, srcPath)
  const cycles = []

  function detectCycle(node, visited = new Set(), stack = []) {
    if (stack.includes(node)) {
      const cycleStart = stack.indexOf(node)
      cycles.push(stack.slice(cycleStart))
      return
    }

    if (visited.has(node)) return
    visited.add(node)
    stack.push(node)

    const dependencies = graph[node] || []
    for (const dep of dependencies) {
      // Resolve relative imports
      const resolvedDep = resolvePath(node, dep)
      if (graph[resolvedDep]) {
        detectCycle(resolvedDep, visited, [...stack])
      }
    }
  }

  Object.keys(graph).forEach(node => detectCycle(node))
  return cycles
}

Find Untested Code

/**
 * Find source files without corresponding test files
 */
async function findUntestedCode(vfs, srcPath, testPath = null) {
  testPath = testPath || srcPath.replace('/src', '/tests')

  const sourceFiles = await vfs.readdir(srcPath, { recursive: true })
  const testFiles = await vfs.readdir(testPath, { recursive: true }).catch(() => [])

  const untestedFiles = []

  for (const sourceFile of sourceFiles) {
    if (!sourceFile.match(/\.(js|ts|jsx|tsx)$/)) continue

    // Look for corresponding test file
    const baseName = sourceFile.replace(/\.(js|ts|jsx|tsx)$/, '')
    const hasTest = testFiles.some(testFile =>
      testFile.includes(baseName) &&
      testFile.match(/\.(test|spec)\.(js|ts|jsx|tsx)$/)
    )

    if (!hasTest) {
      untestedFiles.push(`${srcPath}/${sourceFile}`)
    }
  }

  return untestedFiles
}

Find Similar Code (Duplicate Detection)

/**
 * Find potentially duplicate code using similarity scoring
 */
async function findSimilarCode(vfs, filePath, options = {}) {
  const threshold = options.threshold || 0.8
  const searchPath = options.searchPath || '/'

  // Get the reference file content
  const referenceContent = await vfs.readFile(filePath)
  const referenceText = referenceContent.toString()

  // Use VFS's semantic search
  const similar = await vfs.findSimilar(filePath, {
    limit: 10,
    threshold
  })

  // Additionally, do structural comparison
  const results = []
  for (const match of similar) {
    const matchContent = await vfs.readFile(match.path)
    const matchText = matchContent.toString()

    // Simple line-based similarity (use better algorithms in production)
    const similarity = calculateSimilarity(referenceText, matchText)

    if (similarity > threshold) {
      results.push({
        path: match.path,
        similarity,
        semanticScore: match.score
      })
    }
  }

  return results.sort((a, b) => b.similarity - a.similarity)
}

function calculateSimilarity(text1, text2) {
  // Simple Jaccard similarity on lines
  const lines1 = new Set(text1.split('\n').map(l => l.trim()).filter(l => l))
  const lines2 = new Set(text2.split('\n').map(l => l.trim()).filter(l => l))

  const intersection = new Set([...lines1].filter(x => lines2.has(x)))
  const union = new Set([...lines1, ...lines2])

  return intersection.size / union.size
}

Export Format Functions

Export to EPUB (for novels)

/**
 * Export a directory of markdown files to EPUB format
 */
async function exportToEpub(vfs, path, metadata = {}) {
  // First get the markdown export
  const markdown = await vfs.exportToMarkdown(path)

  // You'll need an EPUB library like epub-gen
  const Epub = require('epub-gen')

  // Convert markdown chapters to EPUB format
  const chapters = []
  const files = await vfs.readdir(path, { recursive: true })

  for (const file of files.sort()) {
    if (file.endsWith('.md')) {
      const content = await vfs.readFile(`${path}/${file}`)
      const title = file.replace('.md', '').replace(/-/g, ' ')

      chapters.push({
        title: title,
        data: content.toString()
      })
    }
  }

  const options = {
    title: metadata.title || 'My Book',
    author: metadata.author || 'Author',
    chapters: chapters
  }

  return new Epub(options)
}

Export to Static Site

/**
 * Export VFS content to static HTML site
 */
async function exportToStaticSite(vfs, sourcePath, options = {}) {
  const json = await vfs.exportToJSON(sourcePath)
  const html = []

  html.push('<!DOCTYPE html>')
  html.push('<html><head>')
  html.push(`<title>${options.title || 'Documentation'}</title>`)
  html.push('<style>/* Add your styles */</style>')
  html.push('</head><body>')

  function renderNode(node, name, depth = 0) {
    const indent = '  '.repeat(depth)

    if (node._meta?.type === 'file') {
      html.push(`${indent}<article>`)
      html.push(`${indent}  <h${Math.min(depth + 2, 6)}>${name}</h${Math.min(depth + 2, 6)}>`)

      if (typeof node._content === 'string') {
        // Convert markdown to HTML if needed
        html.push(`${indent}  <pre>${escapeHtml(node._content)}</pre>`)
      }

      html.push(`${indent}</article>`)
    } else if (node._meta?.type === 'directory') {
      html.push(`${indent}<section>`)
      html.push(`${indent}  <h${Math.min(depth + 1, 6)}>${name}</h${Math.min(depth + 1, 6)}>`)

      for (const [childName, childNode] of Object.entries(node)) {
        if (!childName.startsWith('_')) {
          renderNode(childNode, childName, depth + 1)
        }
      }

      html.push(`${indent}</section>`)
    }
  }

  renderNode(json, options.title || 'Root')

  html.push('</body></html>')
  return html.join('\n')
}

Export to GraphQL Schema

/**
 * Generate GraphQL schema from VFS entities
 */
async function exportToGraphQLSchema(vfs) {
  const entities = await vfs.listEntities()
  const types = new Map()

  // Group entities by type
  for (const entity of entities) {
    const type = entity.type || 'Unknown'
    if (!types.has(type)) {
      types.set(type, [])
    }
    types.get(type).push(entity)
  }

  // Generate schema
  let schema = 'type Query {\n'

  for (const [typeName, entities] of types) {
    schema += `  get${typeName}(id: ID!): ${typeName}\n`
    schema += `  list${typeName}s: [${typeName}!]!\n`
  }

  schema += '}\n\n'

  // Generate types
  for (const [typeName, entities] of types) {
    schema += `type ${typeName} {\n`
    schema += '  id: ID!\n'

    // Infer fields from first entity
    if (entities.length > 0) {
      const sample = entities[0]
      for (const [key, value] of Object.entries(sample)) {
        if (key !== 'id') {
          const fieldType = inferGraphQLType(value)
          schema += `  ${key}: ${fieldType}\n`
        }
      }
    }

    schema += '}\n\n'
  }

  return schema
}

Project Management Functions

Get Project Insights

/**
 * Analyze project for insights and patterns
 */
async function getProjectInsights(vfs, projectPath) {
  const stats = await vfs.getProjectStats(projectPath)
  const todos = await vfs.getAllTodos(projectPath)
  const timeline = await vfs.getTimeline({ limit: 100 })

  // Analyze activity patterns
  const activityByDay = {}
  const activityByUser = {}
  const activityByFile = {}

  for (const event of timeline) {
    const day = event.timestamp.toISOString().split('T')[0]
    activityByDay[day] = (activityByDay[day] || 0) + 1

    const user = event.user || 'system'
    activityByUser[user] = (activityByUser[user] || 0) + 1

    activityByFile[event.path] = (activityByFile[event.path] || 0) + 1
  }

  // Find hotspots (most edited files)
  const hotspots = Object.entries(activityByFile)
    .sort(([,a], [,b]) => b - a)
    .slice(0, 10)
    .map(([path, count]) => ({ path, edits: count }))

  // Todo analysis
  const todosByPriority = {}
  const todosByStatus = {}

  for (const todo of todos) {
    todosByPriority[todo.priority] = (todosByPriority[todo.priority] || 0) + 1
    todosByStatus[todo.status] = (todosByStatus[todo.status] || 0) + 1
  }

  return {
    stats,
    activity: {
      byDay: activityByDay,
      byUser: activityByUser,
      hotspots
    },
    todos: {
      total: todos.length,
      byPriority: todosByPriority,
      byStatus: todosByStatus,
      highPriority: todos.filter(t => t.priority === 'high' && t.status === 'pending')
    },
    recommendations: generateRecommendations(stats, todos, hotspots)
  }
}

function generateRecommendations(stats, todos, hotspots) {
  const recommendations = []

  if (stats.largestFile && stats.largestFile.size > 1024 * 1024) {
    recommendations.push({
      type: 'refactor',
      message: `Consider splitting ${stats.largestFile.path} (${Math.round(stats.largestFile.size / 1024)}KB)`
    })
  }

  if (todos.filter(t => t.priority === 'high' && t.status === 'pending').length > 5) {
    recommendations.push({
      type: 'priority',
      message: 'You have many high-priority pending todos'
    })
  }

  if (hotspots.length > 0 && hotspots[0].edits > 50) {
    recommendations.push({
      type: 'stability',
      message: `${hotspots[0].path} changes frequently, consider stabilizing`
    })
  }

  return recommendations
}

Generate Sprint Report

/**
 * Generate a report for the current sprint
 */
async function generateSprintReport(vfs, sprintStart, sprintEnd = new Date()) {
  const timeline = await vfs.getTimeline({
    from: sprintStart,
    to: sprintEnd
  })

  const todos = await vfs.getAllTodos()

  // Group work by user
  const workByUser = {}
  for (const event of timeline) {
    const user = event.user || 'system'
    if (!workByUser[user]) {
      workByUser[user] = {
        commits: 0,
        filesModified: new Set(),
        linesChanged: 0
      }
    }

    workByUser[user].commits++
    workByUser[user].filesModified.add(event.path)
  }

  // Calculate completion rate
  const completedTodos = todos.filter(t => t.status === 'completed').length
  const totalTodos = todos.length
  const completionRate = totalTodos > 0 ? (completedTodos / totalTodos * 100).toFixed(1) : 0

  return {
    period: {
      start: sprintStart,
      end: sprintEnd,
      days: Math.ceil((sprintEnd - sprintStart) / (1000 * 60 * 60 * 24))
    },
    team: Object.entries(workByUser).map(([user, work]) => ({
      user,
      commits: work.commits,
      filesModified: work.filesModified.size
    })),
    todos: {
      completed: completedTodos,
      total: totalTodos,
      completionRate: `${completionRate}%`,
      remaining: todos.filter(t => t.status === 'pending')
    },
    velocity: {
      commitsPerDay: (timeline.length / 7).toFixed(1),
      todosPerDay: (completedTodos / 7).toFixed(1)
    }
  }
}

Creative Writing Functions

Track Character Arcs

/**
 * Track how characters evolve throughout a story
 */
async function trackCharacterArc(vfs, characterName, storyPath = '/') {
  // Find the character entity
  const entities = await vfs.searchEntities({
    type: 'character',
    name: characterName
  })

  if (entities.length === 0) {
    throw new Error(`Character ${characterName} not found`)
  }

  const character = entities[0]
  const occurrences = await vfs.findEntityOccurrences(character.id)

  // Analyze each appearance
  const arc = []

  for (const occurrence of occurrences) {
    const content = await vfs.readFile(occurrence.path)
    const text = content.toString()

    // Find mentions of the character (basic approach)
    const mentions = text.split('\n').filter(line =>
      line.toLowerCase().includes(characterName.toLowerCase())
    )

    arc.push({
      chapter: occurrence.path,
      mentions: mentions.length,
      // Analyze emotional tone (simplified)
      mood: analyzeMood(mentions),
      // Extract key actions
      actions: extractActions(mentions, characterName)
    })
  }

  return {
    character: character.metadata,
    arc: arc,
    summary: summarizeArc(arc)
  }
}

function analyzeMood(mentions) {
  const positiveWords = ['smiled', 'laughed', 'happy', 'joy', 'love', 'success']
  const negativeWords = ['cried', 'angry', 'sad', 'fear', 'fail', 'death']

  let positive = 0, negative = 0

  for (const mention of mentions) {
    const lower = mention.toLowerCase()
    positive += positiveWords.filter(w => lower.includes(w)).length
    negative += negativeWords.filter(w => lower.includes(w)).length
  }

  if (positive > negative) return 'positive'
  if (negative > positive) return 'negative'
  return 'neutral'
}

Generate Story Bible

/**
 * Create a comprehensive reference for your story universe
 */
async function generateStoryBible(vfs, storyPath) {
  const characters = await vfs.listEntities({ type: 'character' })
  const locations = await vfs.listEntities({ type: 'location' })
  const concepts = await vfs.findConcepts({ domain: 'narrative' })

  const bible = {
    title: 'Story Bible',
    generated: new Date(),
    characters: {},
    locations: {},
    plotThreads: {},
    timeline: []
  }

  // Document characters
  for (const char of characters) {
    const occurrences = await vfs.findEntityOccurrences(char.id)
    bible.characters[char.metadata.name] = {
      ...char.metadata,
      appearances: occurrences.map(o => o.path),
      relationships: await vfs.getEntityGraph(char.id, { depth: 1 })
    }
  }

  // Document locations
  for (const loc of locations) {
    bible.locations[loc.metadata.name] = {
      ...loc.metadata,
      scenes: await vfs.findEntityOccurrences(loc.id)
    }
  }

  // Plot threads from concepts
  for (const concept of concepts) {
    if (concept.type === 'plot') {
      bible.plotThreads[concept.name] = {
        description: concept.description,
        keywords: concept.keywords,
        chapters: await vfs.findByConcept(concept.name)
      }
    }
  }

  // Generate timeline
  const events = await vfs.getTimeline({ limit: 1000 })
  bible.timeline = events.map(e => ({
    date: e.timestamp,
    event: e.description,
    chapter: e.path
  }))

  return bible
}

Game Development Functions

Validate Game Data

/**
 * Validate game configuration files for consistency
 */
async function validateGameData(vfs, gamePath) {
  const errors = []
  const warnings = []

  // Load all game data
  const gameData = await vfs.exportToJSON(gamePath)

  // Check quest references
  if (gameData.quests) {
    for (const [questName, quest] of Object.entries(gameData.quests)) {
      // Check NPC references
      if (quest._content?.questGiver) {
        const npcPath = `${gamePath}/npcs/${quest._content.questGiver}.json`
        const exists = await vfs.exists(npcPath)
        if (!exists) {
          errors.push(`Quest ${questName} references missing NPC: ${quest._content.questGiver}`)
        }
      }

      // Check item rewards
      if (quest._content?.rewards?.items) {
        for (const item of quest._content.rewards.items) {
          const itemPath = `${gamePath}/items/${item}.json`
          const exists = await vfs.exists(itemPath)
          if (!exists) {
            warnings.push(`Quest ${questName} rewards missing item: ${item}`)
          }
        }
      }
    }
  }

  // Check balance
  if (gameData.items) {
    const itemPowers = []
    for (const [itemName, item] of Object.entries(gameData.items)) {
      if (item._content?.stats) {
        const totalPower = Object.values(item._content.stats)
          .reduce((a, b) => a + b, 0)
        itemPowers.push({ name: itemName, power: totalPower })
      }
    }

    // Find outliers
    const avgPower = itemPowers.reduce((a, b) => a + b.power, 0) / itemPowers.length
    const outliers = itemPowers.filter(i => Math.abs(i.power - avgPower) > avgPower * 2)

    for (const outlier of outliers) {
      warnings.push(`Item ${outlier.name} may be imbalanced (power: ${outlier.power}, avg: ${avgPower})`)
    }
  }

  return { errors, warnings, valid: errors.length === 0 }
}

Generate Loot Tables

/**
 * Generate weighted loot tables from item definitions
 */
async function generateLootTables(vfs, itemsPath) {
  const items = await vfs.exportToJSON(itemsPath)
  const tables = {
    common: [],
    uncommon: [],
    rare: [],
    epic: [],
    legendary: []
  }

  for (const [itemName, item] of Object.entries(items)) {
    if (item._meta?.type === 'file' && item._content?.rarity) {
      const entry = {
        name: itemName.replace('.json', ''),
        weight: getWeight(item._content.rarity),
        data: item._content
      }

      tables[item._content.rarity.toLowerCase()].push(entry)
    }
  }

  // Normalize weights
  for (const table of Object.values(tables)) {
    const totalWeight = table.reduce((a, b) => a + b.weight, 0)
    for (const entry of table) {
      entry.probability = (entry.weight / totalWeight * 100).toFixed(2) + '%'
    }
  }

  return tables
}

function getWeight(rarity) {
  const weights = {
    common: 100,
    uncommon: 50,
    rare: 20,
    epic: 5,
    legendary: 1
  }
  return weights[rarity.toLowerCase()] || 10
}

Using These Functions

All these functions are templates that you can customize for your specific needs. To use them:

  1. Copy the function you need
  2. Modify it for your specific requirements
  3. Use it with your VFS instance:
import { Brainy } from '@soulcraft/brainy'

// Initialize VFS
const brain = new Brainy()
await brain.init()
const vfs = brain.vfs()
await vfs.init()

// Use your custom function
const insights = await getProjectInsights(vfs, '/my-project')
console.log(insights.recommendations)

// Combine multiple functions
const deps = await getDependencyGraph(vfs, '/src')
const cycles = await findCircularDependencies(vfs, '/src')
const untested = await findUntestedCode(vfs, '/src', '/tests')

Remember: These are starting points. The power of VFS is that you can combine its primitives to build exactly what you need for your domain!