- 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
19 KiB
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
- Code Analysis Functions
- Export Format Functions
- Project Management Functions
- Creative Writing Functions
- 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:
- Copy the function you need
- Modify it for your specific requirements
- 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!