@trinity-method/cli
Version:
Trinity Method SDK - Structured AI-assisted development framework for Claude Code
440 lines (391 loc) • 14.2 kB
JavaScript
/**
* Codebase Metrics Collection Utility
* Trinity Method SDK - Hybrid Audit System
*
* Collects scriptable metrics from codebase without semantic analysis.
* Designed for cross-platform compatibility (Node.js, Flutter, React, Python, Rust).
*/
import fs from 'fs-extra';
import path from 'path';
import { execSync } from 'child_process';
import { glob } from 'glob';
/**
* Main entry point for metrics collection
* @param {string} sourceDir - Source code directory (src/, lib/, app/)
* @param {string} framework - Detected framework (Node.js, Flutter, React, Python, Rust)
* @returns {Object} Collected metrics
*/
async function collectCodebaseMetrics(sourceDir, framework) {
console.log(` Collecting metrics from ${sourceDir} (${framework})...`);
const metrics = {
// Code Quality Metrics
todoCount: 0,
todoComments: 0,
fixmeComments: 0,
hackComments: 0,
consoleStatements: 0,
commentedCodeBlocks: 0,
// File Complexity Metrics
totalFiles: 0,
filesOver500: 0,
filesOver1000: 0,
filesOver3000: 0,
avgFileLength: 0,
largestFiles: [],
// Dependency Metrics
dependencies: {},
dependencyCount: 0,
devDependencies: {},
devDependencyCount: 0,
// Git Metrics
commitCount: 0,
contributors: 0,
lastCommitDate: 'Unknown',
// Framework-Specific
frameworkVersion: 'Unknown',
packageManager: 'Unknown',
};
try {
// Code Quality Metrics
metrics.todoComments = await countPattern(sourceDir, /\/\/\s*TODO|#\s*TODO/gi);
metrics.fixmeComments = await countPattern(sourceDir, /\/\/\s*FIXME|#\s*FIXME/gi);
metrics.hackComments = await countPattern(sourceDir, /\/\/\s*HACK|#\s*HACK/gi);
metrics.todoCount = metrics.todoComments + metrics.fixmeComments + metrics.hackComments;
metrics.consoleStatements = await countPattern(sourceDir, /console\.(log|warn|error|debug|info)/gi);
metrics.commentedCodeBlocks = await countCommentedCode(sourceDir);
// File Complexity Metrics
const fileStats = await analyzeFileComplexity(sourceDir);
metrics.totalFiles = fileStats.totalFiles;
metrics.filesOver500 = fileStats.filesOver500;
metrics.filesOver1000 = fileStats.filesOver1000;
metrics.filesOver3000 = fileStats.filesOver3000;
metrics.avgFileLength = fileStats.avgFileLength;
metrics.largestFiles = fileStats.largestFiles;
// Dependency Metrics
const deps = await parseDependencies(framework);
metrics.dependencies = deps.dependencies;
metrics.dependencyCount = deps.dependencyCount;
metrics.devDependencies = deps.devDependencies;
metrics.devDependencyCount = deps.devDependencyCount;
// Git Metrics (optional)
try {
metrics.commitCount = await getCommitCount();
metrics.contributors = await getContributorCount();
metrics.lastCommitDate = await getLastCommitDate();
} catch (error) {
// Git not available or not a git repo
console.log(' Git metrics unavailable (not a git repository)');
}
// Framework-Specific
metrics.frameworkVersion = await detectFrameworkVersion(framework);
metrics.packageManager = await detectPackageManager();
} catch (error) {
console.error(' Error collecting metrics:', error.message);
throw error;
}
return metrics;
}
/**
* Count occurrences of a pattern in source files
* @param {string} dir - Directory to search
* @param {RegExp} pattern - Pattern to match
* @returns {number} Count of matches
*/
async function countPattern(dir, pattern) {
if (!await fs.pathExists(dir)) {
return 0;
}
try {
// Get all source files
const files = glob.sync(`${dir}/**/*.{js,jsx,ts,tsx,dart,py,rs}`, {
ignore: ['**/node_modules/**', '**/build/**', '**/.dart_tool/**', '**/dist/**', '**/__pycache__/**']
});
let count = 0;
for (const file of files) {
const content = await fs.readFile(file, 'utf8');
const matches = content.match(pattern);
if (matches) {
count += matches.length;
}
}
return count;
} catch (error) {
console.error(` Error counting pattern: ${error.message}`);
return 0;
}
}
/**
* Count commented code blocks (heuristic: 3+ consecutive comment lines)
* @param {string} dir - Directory to search
* @returns {number} Estimated count of commented code blocks
*/
async function countCommentedCode(dir) {
if (!await fs.pathExists(dir)) {
return 0;
}
try {
const files = glob.sync(`${dir}/**/*.{js,jsx,ts,tsx,dart,py,rs}`, {
ignore: ['**/node_modules/**', '**/build/**', '**/.dart_tool/**', '**/dist/**', '**/__pycache__/**']
});
let blockCount = 0;
for (const file of files) {
const content = await fs.readFile(file, 'utf8');
const lines = content.split('\n');
let consecutiveComments = 0;
for (const line of lines) {
const trimmed = line.trim();
if (trimmed.startsWith('//') || trimmed.startsWith('#')) {
consecutiveComments++;
if (consecutiveComments === 3) {
blockCount++;
}
} else {
consecutiveComments = 0;
}
}
}
return blockCount;
} catch (error) {
console.error(` Error counting commented code: ${error.message}`);
return 0;
}
}
/**
* Analyze file complexity metrics
* @param {string} dir - Directory to analyze
* @returns {Object} File complexity metrics
*/
async function analyzeFileComplexity(dir) {
if (!await fs.pathExists(dir)) {
return {
totalFiles: 0,
filesOver500: 0,
filesOver1000: 0,
filesOver3000: 0,
avgFileLength: 0,
largestFiles: []
};
}
try {
const files = glob.sync(`${dir}/**/*.{js,jsx,ts,tsx,dart,py,rs}`, {
ignore: ['**/node_modules/**', '**/build/**', '**/.dart_tool/**', '**/dist/**', '**/__pycache__/**']
});
const fileSizes = [];
let filesOver500 = 0;
let filesOver1000 = 0;
let filesOver3000 = 0;
let totalLines = 0;
for (const file of files) {
const content = await fs.readFile(file, 'utf8');
const lineCount = content.split('\n').length;
totalLines += lineCount;
fileSizes.push({ file: path.relative(dir, file), lines: lineCount });
if (lineCount > 500) filesOver500++;
if (lineCount > 1000) filesOver1000++;
if (lineCount > 3000) filesOver3000++;
}
// Sort by size and get top 10
fileSizes.sort((a, b) => b.lines - a.lines);
const largestFiles = fileSizes.slice(0, 10);
return {
totalFiles: files.length,
filesOver500,
filesOver1000,
filesOver3000,
avgFileLength: files.length > 0 ? Math.round(totalLines / files.length) : 0,
largestFiles
};
} catch (error) {
console.error(` Error analyzing file complexity: ${error.message}`);
return {
totalFiles: 0,
filesOver500: 0,
filesOver1000: 0,
filesOver3000: 0,
avgFileLength: 0,
largestFiles: []
};
}
}
/**
* Parse dependencies from framework-specific files
* @param {string} framework - Framework name
* @returns {Object} Dependencies and counts
*/
async function parseDependencies(framework) {
const result = {
dependencies: {},
dependencyCount: 0,
devDependencies: {},
devDependencyCount: 0
};
try {
if (framework === 'Node.js' || framework === 'React' || framework === 'Next.js') {
// Parse package.json
if (await fs.pathExists('package.json')) {
const pkg = await fs.readJson('package.json');
result.dependencies = pkg.dependencies || {};
result.devDependencies = pkg.devDependencies || {};
result.dependencyCount = Object.keys(result.dependencies).length;
result.devDependencyCount = Object.keys(result.devDependencies).length;
}
} else if (framework === 'Flutter') {
// Parse pubspec.yaml
if (await fs.pathExists('pubspec.yaml')) {
const yaml = await fs.readFile('pubspec.yaml', 'utf8');
const depMatch = yaml.match(/dependencies:\s*\n((?: \w+:.*\n)*)/);
const devDepMatch = yaml.match(/dev_dependencies:\s*\n((?: \w+:.*\n)*)/);
if (depMatch) {
const deps = depMatch[1].split('\n').filter(line => line.trim().length > 0);
result.dependencyCount = deps.length;
deps.forEach(dep => {
const [name] = dep.trim().split(':');
result.dependencies[name] = 'latest';
});
}
if (devDepMatch) {
const devDeps = devDepMatch[1].split('\n').filter(line => line.trim().length > 0);
result.devDependencyCount = devDeps.length;
devDeps.forEach(dep => {
const [name] = dep.trim().split(':');
result.devDependencies[name] = 'latest';
});
}
}
} else if (framework === 'Python') {
// Parse requirements.txt
if (await fs.pathExists('requirements.txt')) {
const reqs = await fs.readFile('requirements.txt', 'utf8');
const deps = reqs.split('\n').filter(line => line.trim().length > 0 && !line.startsWith('#'));
result.dependencyCount = deps.length;
deps.forEach(dep => {
const [name] = dep.split(/[=<>]/);
result.dependencies[name.trim()] = 'latest';
});
}
} else if (framework === 'Rust') {
// Parse Cargo.toml
if (await fs.pathExists('Cargo.toml')) {
const toml = await fs.readFile('Cargo.toml', 'utf8');
const depMatch = toml.match(/\[dependencies\]\s*\n((?:\w+\s*=.*\n)*)/);
const devDepMatch = toml.match(/\[dev-dependencies\]\s*\n((?:\w+\s*=.*\n)*)/);
if (depMatch) {
const deps = depMatch[1].split('\n').filter(line => line.trim().length > 0);
result.dependencyCount = deps.length;
deps.forEach(dep => {
const [name] = dep.trim().split(/\s*=/);
result.dependencies[name] = 'latest';
});
}
if (devDepMatch) {
const devDeps = devDepMatch[1].split('\n').filter(line => line.trim().length > 0);
result.devDependencyCount = devDeps.length;
devDeps.forEach(dep => {
const [name] = dep.trim().split(/\s*=/);
result.devDependencies[name] = 'latest';
});
}
}
}
} catch (error) {
console.error(` Error parsing dependencies: ${error.message}`);
}
return result;
}
/**
* Get total commit count
* @returns {number} Commit count
*/
async function getCommitCount() {
try {
const count = execSync('git rev-list --count HEAD', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] });
return parseInt(count.trim(), 10);
} catch (error) {
return 0;
}
}
/**
* Get contributor count
* @returns {number} Contributor count
*/
async function getContributorCount() {
try {
const output = execSync('git shortlog -sn --all', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] });
const lines = output.trim().split('\n').filter(line => line.length > 0);
return lines.length;
} catch (error) {
return 1;
}
}
/**
* Get last commit date
* @returns {string} Last commit date (ISO format)
*/
async function getLastCommitDate() {
try {
const date = execSync('git log -1 --format=%cI', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] });
return date.trim();
} catch (error) {
return 'Unknown';
}
}
/**
* Detect framework version
* @param {string} framework - Framework name
* @returns {string} Version string
*/
async function detectFrameworkVersion(framework) {
try {
if (framework === 'Node.js' || framework === 'React' || framework === 'Next.js') {
if (await fs.pathExists('package.json')) {
const pkg = await fs.readJson('package.json');
// Try framework-specific version first
if (framework === 'React' && pkg.dependencies?.react) {
return pkg.dependencies.react.replace(/[\^~]/, '');
}
if (framework === 'Next.js' && pkg.dependencies?.next) {
return pkg.dependencies.next.replace(/[\^~]/, '');
}
// Fall back to Node.js version
const nodeVersion = execSync('node --version', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] });
return nodeVersion.trim();
}
} else if (framework === 'Flutter') {
if (await fs.pathExists('pubspec.yaml')) {
const yaml = await fs.readFile('pubspec.yaml', 'utf8');
const match = yaml.match(/sdk:\s*["']>=?(\d+\.\d+\.\d+)/);
if (match) return match[1];
}
} else if (framework === 'Python') {
const version = execSync('python --version 2>&1', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] });
return version.trim().replace('Python ', '');
} else if (framework === 'Rust') {
const version = execSync('rustc --version', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] });
return version.trim().replace('rustc ', '');
}
} catch (error) {
// Framework version detection failed
}
return 'Unknown';
}
/**
* Detect package manager
* @returns {string} Package manager name
*/
async function detectPackageManager() {
if (await fs.pathExists('package-lock.json')) return 'npm';
if (await fs.pathExists('yarn.lock')) return 'yarn';
if (await fs.pathExists('pnpm-lock.yaml')) return 'pnpm';
if (await fs.pathExists('pubspec.yaml')) return 'pub';
if (await fs.pathExists('requirements.txt')) return 'pip';
if (await fs.pathExists('Cargo.toml')) return 'cargo';
return 'Unknown';
}
export {
collectCodebaseMetrics,
countPattern,
analyzeFileComplexity,
parseDependencies,
detectFrameworkVersion,
detectPackageManager
};