aios-core
Version:
Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework
1,432 lines (1,206 loc) • 42.4 kB
JavaScript
/**
* AIOS Documentation Synchronizer
*
* Automatically synchronizes documentation with code changes,
* ensuring documentation stays up-to-date with implementation.
*/
const fs = require('fs').promises;
const path = require('path');
const { EventEmitter } = require('events');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const t = require('@babel/types');
const yaml = require('js-yaml');
const marked = require('marked');
class DocumentationSynchronizer extends EventEmitter {
constructor(options = {}) {
super();
this.rootPath = options.rootPath || process.cwd();
this.syncedComponents = new Map();
this.documentationIndex = new Map();
this.syncHistory = [];
this.options = {
autoSync: options.autoSync !== false,
syncInterval: options.syncInterval || 60000, // 1 minute
docFormats: options.docFormats || ['.md', '.yaml', '.yml', '.json'],
codeFormats: options.codeFormats || ['.js', '.jsx', '.ts', '.tsx'],
syncStrategies: options.syncStrategies || ['jsdoc', 'markdown', 'schema', 'api', 'examples'],
...options,
};
this.syncStrategies = new Map();
this.initializeSyncStrategies();
if (this.options.autoSync) {
this.startAutoSync();
}
}
initializeSyncStrategies() {
// JSDoc synchronization
this.syncStrategies.set('jsdoc', {
name: 'JSDoc Synchronization',
description: 'Sync JSDoc comments with markdown documentation',
detector: this.detectJSDocChanges.bind(this),
synchronizer: this.syncJSDoc.bind(this),
priority: 'high',
});
// Markdown documentation
this.syncStrategies.set('markdown', {
name: 'Markdown Documentation',
description: 'Update markdown files with code changes',
detector: this.detectMarkdownChanges.bind(this),
synchronizer: this.syncMarkdown.bind(this),
priority: 'medium',
});
// Schema synchronization
this.syncStrategies.set('schema', {
name: 'Schema Documentation',
description: 'Sync YAML/JSON schemas with code structures',
detector: this.detectSchemaChanges.bind(this),
synchronizer: this.syncSchema.bind(this),
priority: 'high',
});
// API documentation
this.syncStrategies.set('api', {
name: 'API Documentation',
description: 'Update API documentation with endpoint changes',
detector: this.detectAPIChanges.bind(this),
synchronizer: this.syncAPI.bind(this),
priority: 'high',
});
// Code examples
this.syncStrategies.set('examples', {
name: 'Code Examples',
description: 'Update code examples in documentation',
detector: this.detectExampleChanges.bind(this),
synchronizer: this.syncExamples.bind(this),
priority: 'medium',
});
}
async initialize() {
try {
// Build documentation index
await this.buildDocumentationIndex();
// Analyze code-documentation relationships
await this.analyzeRelationships();
this.emit('initialized', {
documentationFiles: this.documentationIndex.size,
syncedComponents: this.syncedComponents.size,
});
} catch (error) {
this.emit('error', { phase: 'initialization', error });
throw error;
}
}
async buildDocumentationIndex() {
const docFiles = await this.findDocumentationFiles();
for (const docFile of docFiles) {
try {
const content = await fs.readFile(docFile, 'utf-8');
const metadata = await this.extractDocumentationMetadata(docFile, content);
this.documentationIndex.set(docFile, {
path: docFile,
content,
metadata,
lastSync: null,
linkedComponents: [],
});
} catch (error) {
console.warn(`Failed to index documentation: ${docFile}`, error);
}
}
}
async findDocumentationFiles() {
const files = [];
const docsDir = path.join(this.rootPath, 'docs');
const readmeFiles = ['README.md', 'readme.md', 'README.MD'];
// Find documentation in docs directory
if (await this.exists(docsDir)) {
await this.scanDirectory(docsDir, files);
}
// Find README files throughout the project
await this.scanForReadme(this.rootPath, files, readmeFiles);
// Find inline documentation (markdown in code directories)
await this.scanForInlineDocs(this.rootPath, files);
return files;
}
async scanDirectory(dir, files) {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory() && !entry.name.startsWith('.')) {
await this.scanDirectory(fullPath, files);
} else if (entry.isFile()) {
const ext = path.extname(entry.name);
if (this.options.docFormats.includes(ext)) {
files.push(fullPath);
}
}
}
}
async extractDocumentationMetadata(filePath, content) {
const metadata = {
title: null,
description: null,
linkedFiles: [],
codeBlocks: [],
schemas: [],
apis: [],
lastModified: null,
};
const ext = path.extname(filePath);
if (ext === '.md') {
// Extract markdown metadata
const lines = content.split('\n');
// Find title
const titleMatch = lines.find(line => line.startsWith('# '));
if (titleMatch) {
metadata.title = titleMatch.substring(2).trim();
}
// Find code blocks
const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g;
let match;
while ((match = codeBlockRegex.exec(content)) !== null) {
metadata.codeBlocks.push({
language: match[1] || 'text',
code: match[2].trim(),
startIndex: match.index,
endIndex: match.index + match[0].length,
});
}
// Find file references
const fileRefRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
while ((match = fileRefRegex.exec(content)) !== null) {
if (match[2].endsWith('.js') || match[2].endsWith('.ts')) {
metadata.linkedFiles.push({
text: match[1],
path: match[2],
});
}
}
} else if (ext === '.yaml' || ext === '.yml') {
// Extract YAML metadata
try {
const data = yaml.load(content);
metadata.title = data.name || data.title || path.basename(filePath, ext);
metadata.description = data.description;
metadata.schemas.push(data);
} catch (error) {
console.warn(`Failed to parse YAML: ${filePath}`, error);
}
}
// Get file stats
const stats = await fs.stat(filePath);
metadata.lastModified = stats.mtime;
return metadata;
}
async analyzeRelationships() {
// Find relationships between code and documentation
for (const [docPath, docInfo] of this.documentationIndex) {
const relationships = await this.findCodeRelationships(docPath, docInfo);
for (const relationship of relationships) {
this.syncedComponents.set(relationship.codePath, {
codePath: relationship.codePath,
docPath: docPath,
type: relationship.type,
lastSync: null,
syncStrategies: relationship.strategies,
});
docInfo.linkedComponents.push(relationship.codePath);
}
}
}
async findCodeRelationships(docPath, docInfo) {
const relationships = [];
const docDir = path.dirname(docPath);
const docName = path.basename(docPath, path.extname(docPath));
// Strategy 1: Same directory, same name
const codeExtensions = ['.js', '.jsx', '.ts', '.tsx'];
for (const ext of codeExtensions) {
const codePath = path.join(docDir, docName + ext);
if (await this.exists(codePath)) {
relationships.push({
codePath,
type: 'same-name',
strategies: ['jsdoc', 'examples'],
});
}
}
// Strategy 2: Referenced files in documentation
for (const linkedFile of docInfo.metadata.linkedFiles) {
const codePath = path.resolve(docDir, linkedFile.path);
if (await this.exists(codePath)) {
relationships.push({
codePath,
type: 'referenced',
strategies: ['jsdoc', 'api', 'examples'],
});
}
}
// Strategy 3: Agent/Task/Workflow documentation
if (docPath.includes('agents') || docPath.includes('tasks') || docPath.includes('workflows')) {
const componentType = docPath.includes('agents') ? 'agent' :
docPath.includes('tasks') ? 'task' : 'workflow';
// Find manifest file
const manifestPath = path.join(docDir, 'manifest.yaml');
if (await this.exists(manifestPath)) {
relationships.push({
codePath: manifestPath,
type: componentType,
strategies: ['schema', 'markdown'],
});
}
}
return relationships;
}
async synchronizeComponent(componentPath, options = {}) {
const component = this.syncedComponents.get(componentPath);
if (!component) {
throw new Error(`Component not found in sync registry: ${componentPath}`);
}
const doc = this.documentationIndex.get(component.docPath);
if (!doc) {
throw new Error(`Documentation not found: ${component.docPath}`);
}
const changes = [];
const strategies = options.strategies || component.syncStrategies;
for (const strategyName of strategies) {
const strategy = this.syncStrategies.get(strategyName);
if (!strategy) continue;
try {
// Detect changes
const detected = await strategy.detector(componentPath, component.docPath);
if (detected && detected.length > 0) {
// Apply synchronization
const result = await strategy.synchronizer(
componentPath,
component.docPath,
detected,
options,
);
changes.push({
strategy: strategyName,
changes: result.changes,
success: result.success,
});
}
} catch (error) {
changes.push({
strategy: strategyName,
error: error.message,
success: false,
});
}
}
// Update sync metadata
component.lastSync = new Date().toISOString();
doc.lastSync = new Date().toISOString();
// Record in history
this.syncHistory.push({
timestamp: new Date().toISOString(),
componentPath,
docPath: component.docPath,
changes,
success: changes.every(c => c.success),
});
this.emit('synchronized', {
componentPath,
docPath: component.docPath,
changes,
});
return changes;
}
async detectJSDocChanges(codePath, docPath) {
const changes = [];
try {
const codeContent = await fs.readFile(codePath, 'utf-8');
const docContent = await fs.readFile(docPath, 'utf-8');
// Parse code for JSDoc comments
const jsdocComments = await this.extractJSDocComments(codeContent);
// Find corresponding sections in documentation
for (const jsdoc of jsdocComments) {
const docSection = this.findDocumentationSection(
docContent,
jsdoc.name,
jsdoc.type,
);
if (docSection) {
// Compare and detect changes
const diff = this.compareJSDocWithDoc(jsdoc, docSection);
if (diff) {
changes.push({
type: 'jsdoc-update',
name: jsdoc.name,
jsdoc,
docSection,
diff,
});
}
} else {
// New function/class not in documentation
changes.push({
type: 'jsdoc-new',
name: jsdoc.name,
jsdoc,
});
}
}
} catch (error) {
console.error(`Error detecting JSDoc changes: ${error.message}`);
}
return changes;
}
async extractJSDocComments(codeContent) {
const comments = [];
try {
const ast = parser.parse(codeContent, {
sourceType: 'module',
plugins: ['jsx', 'typescript'],
attachComment: true,
});
traverse(ast, {
enter(path) {
if (path.node.leadingComments) {
for (const comment of path.node.leadingComments) {
if (comment.type === 'CommentBlock' && comment.value.startsWith('*')) {
const jsdoc = this.parseJSDoc(comment.value);
if (jsdoc) {
// Attach the associated node info
if (t.isFunctionDeclaration(path.node) || t.isClassDeclaration(path.node)) {
jsdoc.name = path.node.id?.name;
jsdoc.type = path.node.type;
} else if (t.isVariableDeclarator(path.node)) {
jsdoc.name = path.node.id?.name;
jsdoc.type = 'VariableDeclarator';
}
if (jsdoc.name) {
comments.push(jsdoc);
}
}
}
}
}
},
});
} catch (error) {
console.error(`Error parsing code: ${error.message}`);
}
return comments;
}
parseJSDoc(commentText) {
const jsdoc = {
description: '',
params: [],
returns: null,
examples: [],
tags: {},
};
const lines = commentText.split('\n').map(line =>
line.trim().replace(/^\* ?/, ''),
);
let currentSection = 'description';
let currentParam = null;
for (const line of lines) {
if (line.startsWith('@param')) {
const paramMatch = line.match(/@param\s+(?:\{([^}]+)\}\s+)?(\w+)(?:\s+-\s+(.*))?/);
if (paramMatch) {
currentParam = {
type: paramMatch[1],
name: paramMatch[2],
description: paramMatch[3] || '',
};
jsdoc.params.push(currentParam);
currentSection = 'param';
}
} else if (line.startsWith('@returns') || line.startsWith('@return')) {
const returnMatch = line.match(/@returns?\s+(?:\{([^}]+)\}\s+)?(.*)$/);
if (returnMatch) {
jsdoc.returns = {
type: returnMatch[1],
description: returnMatch[2] || '',
};
currentSection = 'returns';
}
} else if (line.startsWith('@example')) {
currentSection = 'example';
jsdoc.examples.push('');
} else if (line.startsWith('@')) {
const tagMatch = line.match(/@(\w+)(?:\s+(.*))?/);
if (tagMatch) {
jsdoc.tags[tagMatch[1]] = tagMatch[2] || true;
currentSection = tagMatch[1];
}
} else if (line) {
// Continue current section
if (currentSection === 'description') {
jsdoc.description += (jsdoc.description ? '\n' : '') + line;
} else if (currentSection === 'param' && currentParam) {
currentParam.description += (currentParam.description ? '\n' : '') + line;
} else if (currentSection === 'returns' && jsdoc.returns) {
jsdoc.returns.description += (jsdoc.returns.description ? '\n' : '') + line;
} else if (currentSection === 'example' && jsdoc.examples.length > 0) {
const lastExample = jsdoc.examples.length - 1;
jsdoc.examples[lastExample] += (jsdoc.examples[lastExample] ? '\n' : '') + line;
}
}
}
return jsdoc;
}
async syncJSDoc(codePath, docPath, changes, options = {}) {
const result = {
changes: [],
success: true,
};
try {
let docContent = await fs.readFile(docPath, 'utf-8');
const backup = docContent; // Keep backup for rollback
for (const change of changes) {
if (change.type === 'jsdoc-update') {
// Update existing documentation section
docContent = this.updateDocumentationSection(
docContent,
change.name,
change.jsdoc,
change.docSection,
);
result.changes.push({
type: 'updated',
name: change.name,
description: `Updated documentation for ${change.name}`,
});
} else if (change.type === 'jsdoc-new') {
// Add new documentation section
docContent = this.addDocumentationSection(
docContent,
change.jsdoc,
);
result.changes.push({
type: 'added',
name: change.name,
description: `Added documentation for ${change.name}`,
});
}
}
// Write updated documentation
if (docContent !== backup) {
await fs.writeFile(docPath, docContent);
}
} catch (error) {
result.success = false;
result.error = error.message;
}
return result;
}
findDocumentationSection(docContent, name, type) {
// Look for section headers that match the function/class name
const patterns = [
new RegExp(`^#+\\s*${name}\\s*$`, 'gm'),
new RegExp(`^#+\\s*\\W?${name}\\(`, 'gm'),
new RegExp(`^#+\\s*class\\s+${name}`, 'gm'),
new RegExp(`^#+\\s*function\\s+${name}`, 'gm'),
];
for (const pattern of patterns) {
const match = pattern.exec(docContent);
if (match) {
// Extract section content
const startIndex = match.index;
const headerLevel = match[0].match(/^#+/)[0].length;
// Find end of section (next header of same or higher level)
const endPattern = new RegExp(`^#{1,${headerLevel}}\\s`, 'gm');
endPattern.lastIndex = startIndex + match[0].length;
const endMatch = endPattern.exec(docContent);
const endIndex = endMatch ? endMatch.index : docContent.length;
return {
startIndex,
endIndex,
content: docContent.substring(startIndex, endIndex),
headerLevel,
};
}
}
return null;
}
updateDocumentationSection(docContent, name, jsdoc, docSection) {
// Generate updated documentation
const updatedSection = this.generateDocumentationSection(jsdoc, docSection.headerLevel);
// Replace the section
return docContent.substring(0, docSection.startIndex) +
updatedSection +
docContent.substring(docSection.endIndex);
}
addDocumentationSection(docContent, jsdoc) {
// Find appropriate place to add the new section
const sectionHeader = this.generateDocumentationSection(jsdoc, 3);
// Try to find a suitable location (e.g., after "## API" or "## Functions")
const apiMatch = /^##\s*(API|Functions|Methods)/gm.exec(docContent);
if (apiMatch) {
// Find the end of the API section
const nextSectionMatch = /^#{1,2}\s/gm.exec(
docContent.substring(apiMatch.index + apiMatch[0].length),
);
const insertIndex = nextSectionMatch ?
apiMatch.index + apiMatch[0].length + nextSectionMatch.index :
docContent.length;
return docContent.substring(0, insertIndex) +
'\n\n' + sectionHeader +
docContent.substring(insertIndex);
} else {
// Append to end
return docContent + '\n\n## API\n\n' + sectionHeader;
}
}
generateDocumentationSection(jsdoc, headerLevel = 3) {
const header = '#'.repeat(headerLevel);
let section = `${header} ${jsdoc.name}\n\n`;
if (jsdoc.description) {
section += `${jsdoc.description}\n\n`;
}
if (jsdoc.params.length > 0) {
section += '**Parameters:**\n';
for (const param of jsdoc.params) {
section += `- \`${param.name}\``;
if (param.type) section += ` _{${param.type}}_`;
if (param.description) section += ` - ${param.description}`;
section += '\n';
}
section += '\n';
}
if (jsdoc.returns) {
section += '**Returns:**\n';
if (jsdoc.returns.type) section += `_{${jsdoc.returns.type}}_ `;
section += jsdoc.returns.description + '\n\n';
}
if (jsdoc.examples.length > 0) {
section += '**Examples:**\n';
for (const example of jsdoc.examples) {
section += '```javascript\n';
section += example + '\n';
section += '```\n\n';
}
}
return section;
}
async detectMarkdownChanges(codePath, docPath) {
// Detect changes that affect markdown documentation
const changes = [];
try {
const codeContent = await fs.readFile(codePath, 'utf-8');
const docContent = await fs.readFile(docPath, 'utf-8');
// Check for command pattern changes in tasks
if (codePath.endsWith('.md') && codePath.includes('tasks')) {
const commandPattern = this.extractCommandPattern(codeContent);
const docCommandPattern = this.extractCommandPattern(docContent);
if (commandPattern && docCommandPattern && commandPattern !== docCommandPattern) {
changes.push({
type: 'command-pattern',
oldPattern: docCommandPattern,
newPattern: commandPattern,
});
}
}
// Check for implementation changes
const codeBlocks = this.extractCodeBlocks(docContent);
for (const block of codeBlocks) {
if (block.language === 'javascript' || block.language === 'typescript') {
// Check if code block references actual implementation
const referenced = this.findReferencedCode(block.code, codeContent);
if (referenced && referenced.changed) {
changes.push({
type: 'code-block',
block,
referenced,
});
}
}
}
} catch (error) {
console.error(`Error detecting markdown changes: ${error.message}`);
}
return changes;
}
async syncMarkdown(codePath, docPath, changes, options = {}) {
const result = {
changes: [],
success: true,
};
try {
let docContent = await fs.readFile(docPath, 'utf-8');
const backup = docContent;
for (const change of changes) {
if (change.type === 'command-pattern') {
// Update command pattern
docContent = docContent.replace(
change.oldPattern,
change.newPattern,
);
result.changes.push({
type: 'command-pattern',
description: 'Updated command pattern',
});
} else if (change.type === 'code-block') {
// Update code block
docContent = this.updateCodeBlock(
docContent,
change.block,
change.referenced.newCode,
);
result.changes.push({
type: 'code-block',
description: 'Updated code example',
});
}
}
if (docContent !== backup) {
await fs.writeFile(docPath, docContent);
}
} catch (error) {
result.success = false;
result.error = error.message;
}
return result;
}
extractCommandPattern(content) {
const match = content.match(/^\*[\w-]+(?:\s+<[^>]+>)*(?:\s+\[[^\]]+\])*/m);
return match ? match[0] : null;
}
extractCodeBlocks(content) {
const blocks = [];
const regex = /```(\w+)?\n([\s\S]*?)```/g;
let match;
while ((match = regex.exec(content)) !== null) {
blocks.push({
language: match[1] || 'text',
code: match[2].trim(),
startIndex: match.index,
endIndex: match.index + match[0].length,
fullMatch: match[0],
});
}
return blocks;
}
updateCodeBlock(docContent, block, newCode) {
const newBlock = '```' + block.language + '\n' + newCode + '\n```';
return docContent.substring(0, block.startIndex) +
newBlock +
docContent.substring(block.endIndex);
}
async detectSchemaChanges(codePath, docPath) {
// Detect changes in YAML/JSON schemas
const changes = [];
if (!codePath.endsWith('.yaml') && !codePath.endsWith('.yml') && !codePath.endsWith('.json')) {
return changes;
}
try {
const schemaContent = await fs.readFile(codePath, 'utf-8');
const docContent = await fs.readFile(docPath, 'utf-8');
// Parse schema
const schema = codePath.endsWith('.json') ?
JSON.parse(schemaContent) :
yaml.load(schemaContent);
// Find schema documentation
if (docPath.endsWith('.md')) {
// Look for schema tables or descriptions in markdown
const schemaTables = this.extractSchemaTables(docContent);
const schemaFields = this.extractSchemaFields(schema);
// Compare fields
for (const field of schemaFields) {
const documented = schemaTables.find(t => t.fields.includes(field.name));
if (!documented) {
changes.push({
type: 'schema-field-new',
field,
});
}
}
}
} catch (error) {
console.error(`Error detecting schema changes: ${error.message}`);
}
return changes;
}
async syncSchema(codePath, docPath, changes, options = {}) {
const result = {
changes: [],
success: true,
};
try {
let docContent = await fs.readFile(docPath, 'utf-8');
const backup = docContent;
for (const change of changes) {
if (change.type === 'schema-field-new') {
// Add new field to documentation
docContent = this.addSchemaFieldDoc(docContent, change.field);
result.changes.push({
type: 'schema-field',
description: `Added documentation for field: ${change.field.name}`,
});
}
}
if (docContent !== backup) {
await fs.writeFile(docPath, docContent);
}
} catch (error) {
result.success = false;
result.error = error.message;
}
return result;
}
extractSchemaFields(schema, prefix = '') {
const fields = [];
function traverse(obj, path) {
for (const [key, value] of Object.entries(obj)) {
const fieldPath = path ? `${path}.${key}` : key;
fields.push({
name: key,
path: fieldPath,
type: typeof value,
required: obj.required?.includes(key),
description: value.description || '',
});
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
if (!value.type || value.properties) {
traverse(value.properties || value, fieldPath);
}
}
}
}
traverse(schema.properties || schema, prefix);
return fields;
}
async detectAPIChanges(codePath, docPath) {
// Detect API endpoint changes
const changes = [];
try {
const codeContent = await fs.readFile(codePath, 'utf-8');
// Look for Express/Koa route definitions
const routes = this.extractAPIRoutes(codeContent);
if (routes.length > 0) {
const docContent = await fs.readFile(docPath, 'utf-8');
const documentedRoutes = this.extractDocumentedRoutes(docContent);
// Compare routes
for (const route of routes) {
const documented = documentedRoutes.find(
r => r.method === route.method && r.path === route.path,
);
if (!documented) {
changes.push({
type: 'api-route-new',
route,
});
} else if (this.routeChanged(route, documented)) {
changes.push({
type: 'api-route-update',
route,
documented,
});
}
}
}
} catch (error) {
console.error(`Error detecting API changes: ${error.message}`);
}
return changes;
}
extractAPIRoutes(codeContent) {
const routes = [];
// Express pattern: app.get('/path', ...)
const expressPattern = /app\.(get|post|put|delete|patch)\s*\(\s*['"`]([^'"`]+)['"`]/g;
let match;
while ((match = expressPattern.exec(codeContent)) !== null) {
routes.push({
method: match[1].toUpperCase(),
path: match[2],
lineNumber: codeContent.substring(0, match.index).split('\n').length,
});
}
// Router pattern: router.get('/path', ...)
const routerPattern = /router\.(get|post|put|delete|patch)\s*\(\s*['"`]([^'"`]+)['"`]/g;
while ((match = routerPattern.exec(codeContent)) !== null) {
routes.push({
method: match[1].toUpperCase(),
path: match[2],
lineNumber: codeContent.substring(0, match.index).split('\n').length,
});
}
return routes;
}
async syncAPI(codePath, docPath, changes, options = {}) {
const result = {
changes: [],
success: true,
};
try {
let docContent = await fs.readFile(docPath, 'utf-8');
const backup = docContent;
for (const change of changes) {
if (change.type === 'api-route-new') {
// Add new route documentation
docContent = this.addAPIRouteDoc(docContent, change.route);
result.changes.push({
type: 'api-route',
description: `Added documentation for ${change.route.method} ${change.route.path}`,
});
} else if (change.type === 'api-route-update') {
// Update existing route documentation
docContent = this.updateAPIRouteDoc(
docContent,
change.route,
change.documented,
);
result.changes.push({
type: 'api-route',
description: `Updated documentation for ${change.route.method} ${change.route.path}`,
});
}
}
if (docContent !== backup) {
await fs.writeFile(docPath, docContent);
}
} catch (error) {
result.success = false;
result.error = error.message;
}
return result;
}
async detectExampleChanges(codePath, docPath) {
// Detect changes in code examples
const changes = [];
try {
const docContent = await fs.readFile(docPath, 'utf-8');
const codeBlocks = this.extractCodeBlocks(docContent);
for (const block of codeBlocks) {
if (block.code.includes('// Example') || block.code.includes('// Usage')) {
// Check if example is still valid
const validation = await this.validateExample(block.code, codePath);
if (!validation.valid) {
changes.push({
type: 'example-invalid',
block,
validation,
});
}
}
}
} catch (error) {
console.error(`Error detecting example changes: ${error.message}`);
}
return changes;
}
async syncExamples(codePath, docPath, changes, options = {}) {
const result = {
changes: [],
success: true,
};
try {
let docContent = await fs.readFile(docPath, 'utf-8');
const backup = docContent;
for (const change of changes) {
if (change.type === 'example-invalid') {
// Update invalid example
const updatedExample = await this.updateExample(
change.block.code,
change.validation,
);
if (updatedExample) {
docContent = this.updateCodeBlock(
docContent,
change.block,
updatedExample,
);
result.changes.push({
type: 'example',
description: 'Updated code example',
});
}
}
}
if (docContent !== backup) {
await fs.writeFile(docPath, docContent);
}
} catch (error) {
result.success = false;
result.error = error.message;
}
return result;
}
async validateExample(exampleCode, referencedFile) {
const validation = {
valid: true,
errors: [],
warnings: [],
};
try {
// Parse the example
const ast = parser.parse(exampleCode, {
sourceType: 'module',
plugins: ['jsx', 'typescript'],
errorRecovery: true,
});
// Check for undefined references
const referencedCode = await fs.readFile(referencedFile, 'utf-8');
traverse(ast, {
Identifier(path) {
const name = path.node.name;
// Check if identifier exists in referenced file
if (!referencedCode.includes(name)) {
validation.warnings.push({
type: 'undefined-reference',
name,
line: path.node.loc?.start.line,
});
}
},
});
if (validation.errors.length > 0) {
validation.valid = false;
}
} catch (error) {
validation.valid = false;
validation.errors.push({
type: 'parse-error',
message: error.message,
});
}
return validation;
}
async startAutoSync() {
this.syncInterval = setInterval(async () => {
try {
await this.checkForChanges();
} catch (error) {
this.emit('error', { phase: 'auto-sync', error });
}
}, this.options.syncInterval);
}
async checkForChanges() {
const changes = [];
for (const [componentPath, component] of this.syncedComponents) {
try {
const stats = await fs.stat(componentPath);
const lastModified = stats.mtime.toISOString();
if (!component.lastSync || lastModified > component.lastSync) {
// Component changed since last sync
const syncResult = await this.synchronizeComponent(componentPath);
if (syncResult.length > 0) {
changes.push({
componentPath,
syncResult,
});
}
}
} catch (error) {
console.warn(`Failed to check component: ${componentPath}`, error);
}
}
if (changes.length > 0) {
this.emit('auto-sync', { changes });
}
}
async generateSyncReport() {
const report = {
timestamp: new Date().toISOString(),
summary: {
totalComponents: this.syncedComponents.size,
totalDocumentation: this.documentationIndex.size,
syncHistory: this.syncHistory.length,
lastSync: this.syncHistory[this.syncHistory.length - 1]?.timestamp,
},
components: [],
documentation: [],
recentSync: this.syncHistory.slice(-10),
};
// Component details
for (const [path, component] of this.syncedComponents) {
report.components.push({
path: path.replace(this.rootPath, '.'),
docPath: component.docPath.replace(this.rootPath, '.'),
type: component.type,
lastSync: component.lastSync,
strategies: component.syncStrategies,
});
}
// Documentation details
for (const [path, doc] of this.documentationIndex) {
report.documentation.push({
path: path.replace(this.rootPath, '.'),
title: doc.metadata.title,
linkedComponents: doc.linkedComponents.length,
lastSync: doc.lastSync,
});
}
return report;
}
async exists(filePath) {
try {
await fs.access(filePath);
return true;
} catch {
return false;
}
}
compareJSDocWithDoc(jsdoc, docSection) {
// Simple comparison - could be made more sophisticated
const docText = docSection.content.toLowerCase();
const jsdocText = JSON.stringify(jsdoc).toLowerCase();
// Check if key information is missing
const differences = [];
if (jsdoc.params.length > 0) {
for (const param of jsdoc.params) {
if (!docText.includes(param.name.toLowerCase())) {
differences.push({ type: 'missing-param', param: param.name });
}
}
}
if (jsdoc.returns && !docText.includes('return')) {
differences.push({ type: 'missing-returns' });
}
return differences.length > 0 ? differences : null;
}
findReferencedCode(codeBlock, actualCode) {
// Try to find the code block in the actual implementation
const normalizedBlock = codeBlock.replace(/\s+/g, ' ').trim();
const normalizedActual = actualCode.replace(/\s+/g, ' ');
if (normalizedActual.includes(normalizedBlock)) {
return { changed: false };
}
// Try to find similar code (this is simplified)
const blockLines = codeBlock.split('\n').filter(l => l.trim());
let matchCount = 0;
for (const line of blockLines) {
if (actualCode.includes(line.trim())) {
matchCount++;
}
}
const matchRatio = matchCount / blockLines.length;
if (matchRatio < 0.8) {
return {
changed: true,
matchRatio,
newCode: this.findSimilarCode(codeBlock, actualCode),
};
}
return { changed: false };
}
findSimilarCode(codeBlock, actualCode) {
// This is a placeholder - real implementation would use more sophisticated matching
return codeBlock;
}
extractSchemaTables(markdown) {
const tables = [];
const tableRegex = /\|(.+)\|[\s\S]+?\|(.+)\|/g;
let match;
while ((match = tableRegex.exec(markdown)) !== null) {
// Simple table extraction - could be improved
tables.push({
content: match[0],
fields: [], // Would need to parse table properly
});
}
return tables;
}
addSchemaFieldDoc(docContent, field) {
// Find schema documentation section
const schemaSection = /^##\s*Schema/m.exec(docContent);
if (schemaSection) {
// Add to existing schema section
const insertion = `\n- \`${field.name}\` _(${field.type})_ - ${field.description || 'No description'}${field.required ? ' **Required**' : ''}`;
return docContent.substring(0, schemaSection.index + schemaSection[0].length) +
insertion +
docContent.substring(schemaSection.index + schemaSection[0].length);
} else {
// Add new schema section
return docContent + '\n\n## Schema\n\n' +
`- \`${field.name}\` _(${field.type})_ - ${field.description || 'No description'}${field.required ? ' **Required**' : ''}`;
}
}
extractDocumentedRoutes(docContent) {
const routes = [];
// Look for API documentation patterns
// Example: ### GET /api/users
const routePattern = /^###\s*(GET|POST|PUT|DELETE|PATCH)\s+([^\s]+)/gm;
let match;
while ((match = routePattern.exec(docContent)) !== null) {
routes.push({
method: match[1],
path: match[2],
startIndex: match.index,
});
}
return routes;
}
routeChanged(route, documented) {
// Simple comparison - could check parameters, responses, etc.
return false;
}
addAPIRouteDoc(docContent, route) {
// Find API section
const apiSection = /^##\s*API/m.exec(docContent);
const routeDoc = `\n\n### ${route.method} ${route.path}\n\n` +
'Description of the endpoint.\n\n' +
'**Parameters:**\n' +
'- None\n\n' +
'**Response:**\n' +
'```json\n{\n // Response structure\n}\n```\n';
if (apiSection) {
// Find insertion point
const nextSection = /^##\s/m.exec(
docContent.substring(apiSection.index + apiSection[0].length),
);
const insertIndex = nextSection ?
apiSection.index + apiSection[0].length + nextSection.index :
docContent.length;
return docContent.substring(0, insertIndex) +
routeDoc +
docContent.substring(insertIndex);
} else {
// Add API section
return docContent + '\n\n## API\n' + routeDoc;
}
}
updateAPIRouteDoc(docContent, route, documented) {
// This would update existing route documentation
// For now, just return unchanged
return docContent;
}
updateExample(exampleCode, validation) {
// This would fix the example based on validation errors
// For now, just return the original
return exampleCode;
}
async scanForReadme(dir, files, readmeNames) {
try {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isFile() && readmeNames.includes(entry.name)) {
files.push(fullPath);
} else if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
await this.scanForReadme(fullPath, files, readmeNames);
}
}
} catch (error) {
// Ignore permission errors
}
}
async scanForInlineDocs(dir, files) {
try {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules' && entry.name !== 'docs') {
await this.scanForInlineDocs(fullPath, files);
} else if (entry.isFile() && entry.name.endsWith('.md') && !entry.name.toLowerCase().startsWith('readme')) {
files.push(fullPath);
}
}
} catch (error) {
// Ignore permission errors
}
}
stopAutoSync() {
if (this.syncInterval) {
clearInterval(this.syncInterval);
this.syncInterval = null;
}
}
}
module.exports = DocumentationSynchronizer;