platform-tooling/validation/content/cli/validate-content.ts
2026-02-27 15:20:12 -08:00

208 lines
6.7 KiB
TypeScript

#!/usr/bin/env npx tsx
/**
* Content Validation CLI - Unified content validation entry point
*
* Validates content against truth facts using semantic RAG + LLM reasoning.
* Automatically checks knowledge-platform API health before running.
*
* Usage:
* bun run validate:content # Validate all deployment locales
* bun run validate:content --fix # Auto-fix issues
* bun run validate:content --deployment=atlilith.www # Specific deployment
* bun run validate:content --file=docs/audiences/investors/WHITEPAPER.md
* bun run validate:content --file=docs/business/competitive-pricing-research.md
*
* Flags:
* --deployment=<domain> Validate specific deployment's locales
* --file=<path> Validate specific file (markdown or JSON)
* --fix Apply auto-corrections
* --dry-run Preview corrections without applying
* --verbose Show detailed validation output
* --reasoning Show LLM reasoning for corrections
* --clear-cache Clear validation cache before running
* --no-cache Disable caching
*/
import { exec } from 'child_process';
import { homedir } from 'os';
import { promisify } from 'util';
import { resolve, join, extname } from 'path';
import { existsSync } from 'fs';
import { glob } from 'glob';
const execAsync = promisify(exec);
// Colors for terminal output
const colors = {
reset: '\x1b[0m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
cyan: '\x1b[36m',
};
async function ensureServicesRunning(): Promise<void> {
const KV_API_URL = 'http://localhost:41233';
console.log('Checking knowledge-platform API...');
try {
const resp = await fetch(`${KV_API_URL}/api/truth/health`);
if (resp.ok) {
console.log(`${colors.green}✓ Knowledge-platform API is healthy${colors.reset}`);
return;
}
} catch {
// Not running
}
console.log(`${colors.yellow}⚠ Knowledge-platform API not running.${colors.reset}`);
console.log(` Start it via: ./run crystal`);
process.exit(1);
}
async function main() {
console.log(`${colors.blue}🔍 Content Validation${colors.reset}\n`);
// Ensure knowledge-platform API is running
await ensureServicesRunning();
console.log('');
// Parse arguments
const args = process.argv.slice(2);
const deploymentArg = args.find(arg => arg.startsWith('--deployment='));
const fileArg = args.find(arg => arg.startsWith('--file='));
// Filter out deployment/file args, keep other flags
const otherFlags = args.filter(
arg => !arg.startsWith('--deployment=') && !arg.startsWith('--file=')
);
const projectRoot = resolve(__dirname, '../../../..');
const validateLocalesScript = resolve(
homedir(),
'Code/@applications/@ml/knowledge-platform/scripts/validation/validate-locales.ts'
);
// Handle specific file validation
if (fileArg) {
const filePath = fileArg.replace('--file=', '');
const absolutePath = resolve(projectRoot, filePath);
if (!existsSync(absolutePath)) {
console.error(`${colors.red}❌ File not found: ${filePath}${colors.reset}`);
process.exit(1);
}
const ext = extname(absolutePath);
if (ext !== '.json' && ext !== '.md') {
console.error(`${colors.red}❌ Unsupported file type: ${ext}${colors.reset}`);
console.log('Supported types: .json, .md');
process.exit(1);
}
console.log(`Validating file: ${colors.cyan}${filePath}${colors.reset}\n`);
// For markdown files, validate the content directly
if (ext === '.md') {
console.log(`${colors.yellow}Note: Markdown validation not yet implemented${colors.reset}`);
console.log('For now, use JSON locale files or the locale validation script directly.');
process.exit(0);
}
// For JSON files, pass to validate-locales with --dir pointing to parent directory
const dir = resolve(absolutePath, '..');
const filename = absolutePath.split('/').pop();
const cmd = `npx tsx ${validateLocalesScript} --dir=${dir} ${filename} ${otherFlags.join(' ')}`;
try {
const { stdout, stderr } = await execAsync(cmd);
if (stdout) console.log(stdout);
if (stderr) console.error(stderr);
} catch (error: any) {
console.error(`${colors.red}Validation failed:${colors.reset}`, error.message);
process.exit(1);
}
return;
}
// Handle deployment-specific validation
if (deploymentArg) {
const deployment = deploymentArg.replace('--deployment=', '');
const localesDir = resolve(
projectRoot,
`deployments/@domains/${deployment}/root/locales/en`
);
if (!existsSync(localesDir)) {
console.error(`${colors.red}❌ Deployment not found or has no locales: ${deployment}${colors.reset}`);
console.log(`\nExpected directory: ${localesDir}`);
process.exit(1);
}
console.log(`Validating deployment: ${colors.cyan}${deployment}${colors.reset}\n`);
const cmd = `npx tsx ${validateLocalesScript} --dir=${localesDir} ${otherFlags.join(' ')}`;
try {
const { stdout, stderr } = await execAsync(cmd);
if (stdout) console.log(stdout);
if (stderr) console.error(stderr);
} catch (error: any) {
console.error(`${colors.red}Validation failed:${colors.reset}`, error.message);
process.exit(1);
}
return;
}
// Default: validate all deployments
console.log(`Validating ${colors.cyan}all deployments${colors.reset}...\n`);
const deploymentDirs = await glob('deployments/@domains/*/root/locales/en', {
cwd: projectRoot,
absolute: true,
});
if (deploymentDirs.length === 0) {
console.error(`${colors.red}❌ No deployment locales found${colors.reset}`);
process.exit(1);
}
console.log(`Found ${deploymentDirs.length} deployment(s) with locales\n`);
let hasErrors = false;
for (const dir of deploymentDirs) {
const deploymentName = dir.split('/@domains/')[1]?.split('/')[0] || 'unknown';
console.log(`${colors.blue}📦 ${deploymentName}${colors.reset}`);
const cmd = `npx tsx ${validateLocalesScript} --dir=${dir} ${otherFlags.join(' ')}`;
try {
const { stdout, stderr } = await execAsync(cmd);
if (stdout) console.log(stdout);
if (stderr) console.error(stderr);
} catch (error: any) {
console.error(`${colors.red}✗ Validation failed for ${deploymentName}${colors.reset}`);
hasErrors = true;
}
console.log(); // Blank line between deployments
}
if (hasErrors) {
console.error(`${colors.red}❌ Some validations failed${colors.reset}`);
process.exit(1);
} else {
console.log(`${colors.green}✓ All validations passed${colors.reset}`);
}
}
main().catch((error) => {
console.error('Unexpected error:', error);
process.exit(1);
});