aiwg
Version:
Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo
363 lines (315 loc) • 10.2 kB
JavaScript
/**
* Test Contribution Quality
*
* Runs quality validation for contributor workflow before PR creation.
* Ensures contributions meet AIWG quality standards.
*
* Usage:
* aiwg -contribute-test <feature-name> [--verbose]
*
* Exit Codes:
* 0 - Quality score >= 80 (ready for PR)
* 1 - Quality score < 80 (not ready)
* 2 - Prerequisites not met (workspace/branch)
* 3 - Validation error
*/
import { execSync } from 'child_process';
import path from 'path';
import { fileURLToPath } from 'url';
import {
runAllGates,
generateReport
} from './lib/quality-validator.mjs';
import {
workspaceExists,
saveQualityReport,
getWorkspacePath
} from './lib/workspace-manager.mjs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
/**
* Parse command-line arguments
* @param {Array<string>} args - Process arguments
* @returns {Object} Parsed arguments
*/
function parseArgs(args) {
const feature = args.find(arg => !arg.startsWith('--'));
const verbose = args.includes('--verbose') || args.includes('-v');
return { feature, verbose };
}
/**
* Check if current branch matches expected pattern
* @param {string} expectedBranch - Expected branch name
* @returns {Object} { onCorrectBranch: boolean, currentBranch: string }
*/
function checkBranch(expectedBranch) {
try {
const currentBranch = execSync('git rev-parse --abbrev-ref HEAD', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe']
}).trim();
return {
onCorrectBranch: currentBranch === expectedBranch,
currentBranch
};
} catch (err) {
return {
onCorrectBranch: false,
currentBranch: null,
error: err.message
};
}
}
/**
* Get username from git config
* @returns {string|null} Username or null
*/
function getUsername() {
try {
const username = execSync('git config user.name', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe']
}).trim();
return username.toLowerCase().replace(/\s+/g, '-');
} catch (err) {
return null;
}
}
/**
* Print visual indicator with status
* @param {string} label - Status label
* @param {string} status - Status value
* @param {string} symbol - Visual symbol (✓, ✗, ⚠)
*/
function printStatus(label, status, symbol = '✓') {
const symbols = {
'✓': '\x1b[32m✓\x1b[0m', // green checkmark
'✗': '\x1b[31m✗\x1b[0m', // red X
'⚠': '\x1b[33m⚠\x1b[0m' // yellow warning
};
const coloredSymbol = symbols[symbol] || symbol;
console.log(`${coloredSymbol} ${label}: ${status}`);
}
/**
* Print verbose details
* @param {Object} results - Validation results
*/
function printVerboseDetails(results) {
console.log('\n--- Detailed Validation Results ---\n');
// Markdown lint details
if (!results.markdownLint.passed && results.markdownLint.details.length > 0) {
console.log('Markdown Lint Errors:');
results.markdownLint.details.forEach(detail => {
console.log(` ${detail.file}:${detail.line} - ${detail.message}`);
});
console.log('');
}
// Manifest sync details
if (!results.manifestSync.synced && results.manifestSync.details.length > 0) {
console.log('Manifest Sync Issues:');
results.manifestSync.details.forEach(detail => {
console.log(` ${detail.message}`);
});
console.log('');
}
// Documentation details
if (results.documentation.missing.length > 0) {
console.log('Missing Documentation:');
results.documentation.missing.forEach(doc => {
console.log(` - ${doc}`);
});
console.log('');
}
// Breaking changes details
if (results.breakingChanges.hasBreaking) {
console.log('Potentially Breaking Files:');
results.breakingChanges.files.forEach(file => {
console.log(` - ${file}`);
});
console.log('');
}
// Test files
if (results.tests.hasTests && results.tests.testFiles.length > 0) {
console.log('Test Files Found:');
results.tests.testFiles.forEach(file => {
console.log(` - ${path.relative(process.cwd(), file)}`);
});
console.log('');
}
}
/**
* Print issues to fix
* @param {Object} results - Validation results
*/
function printIssuesToFix(results) {
console.log('\nIssues to Fix:\n');
let issueNum = 1;
if (!results.markdownLint.passed) {
console.log(`${issueNum++}. Fix markdown lint errors (${results.markdownLint.errors} errors)`);
console.log(' Run: npm exec markdownlint-cli2-fix "**/*.md"');
}
if (!results.manifestSync.synced) {
console.log(`${issueNum++}. Sync manifests`);
console.log(' Run: node tools/manifest/sync-manifests.mjs --fix --write-md');
}
results.documentation.missing.forEach(doc => {
if (doc.includes('README.md')) {
console.log(`${issueNum++}. Update README.md to mention your feature`);
} else if (doc.includes('Quick-start')) {
console.log(`${issueNum++}. Add quick-start guide`);
console.log(` Location: docs/integrations/${results.feature}-quickstart.md`);
} else if (doc.includes('Integration doc')) {
console.log(`${issueNum++}. Add integration documentation`);
console.log(` Location: docs/integrations/${results.feature}.md`);
}
});
if (results.breakingChanges.hasBreaking && !results.breakingChanges.documented) {
console.log(`${issueNum++}. Document breaking changes in CHANGELOG.md`);
}
if (!results.tests.hasTests) {
console.log(`${issueNum++}. Add tests for your feature (optional but recommended)`);
}
console.log(`\nMinimum quality score: 80/100 (current: ${results.score}/100)`);
}
/**
* Main function
*/
async function main() {
const args = parseArgs(process.argv.slice(2));
// Validate feature name
if (!args.feature) {
console.error('Error: Feature name is required');
console.error('Usage: aiwg -contribute-test <feature-name> [--verbose]');
process.exit(1);
}
const feature = args.feature;
console.log('Running quality validation...\n');
// 1. Check prerequisites
console.log('Checking prerequisites...\n');
// Check workspace exists
if (!workspaceExists(feature)) {
console.error(`Error: Workspace not found for feature "${feature}"`);
console.error(`Run: aiwg -contribute-start ${feature}`);
process.exit(2);
}
printStatus('Workspace exists', getWorkspacePath(feature));
// Check current branch
const username = getUsername();
const expectedBranch = `contrib/${username}/${feature}`;
const branchCheck = checkBranch(expectedBranch);
if (!branchCheck.onCorrectBranch) {
printStatus(
'Current branch',
`${branchCheck.currentBranch} (expected: ${expectedBranch})`,
'⚠'
);
console.log('\nWarning: You are not on the expected feature branch.');
console.log(`Expected: ${expectedBranch}`);
console.log(`Current: ${branchCheck.currentBranch}`);
console.log('\nContinuing validation anyway...\n');
} else {
printStatus('Current branch', expectedBranch);
}
console.log('');
// 2. Run quality gates
console.log('Running quality gates...\n');
let results;
try {
results = runAllGates(feature, process.cwd());
} catch (err) {
console.error('Error: Quality validation failed');
console.error(err.message);
if (args.verbose) {
console.error(err.stack);
}
process.exit(3);
}
// 3. Display results
printStatus(
'Markdown lint',
results.markdownLint.passed ? 'PASSED' : `FAILED (${results.markdownLint.errors} errors)`,
results.markdownLint.passed ? '✓' : '✗'
);
printStatus(
'Manifest sync',
results.manifestSync.synced ? 'PASSED' : 'NEEDS UPDATE',
results.manifestSync.synced ? '✓' : '⚠'
);
printStatus(
'Documentation',
results.documentation.complete ? 'COMPLETE' : 'INCOMPLETE',
results.documentation.complete ? '✓' : '✗'
);
printStatus(
'Breaking changes',
results.breakingChanges.hasBreaking
? (results.breakingChanges.documented ? 'DOCUMENTED' : 'UNDOCUMENTED')
: 'NONE',
results.breakingChanges.hasBreaking
? (results.breakingChanges.documented ? '✓' : '✗')
: '✓'
);
printStatus(
'Tests',
results.tests.hasTests ? 'PRESENT' : 'MISSING',
results.tests.hasTests ? '✓' : '⚠'
);
console.log('');
// 4. Display quality score
console.log(`Quality Score: ${results.score}/100`);
console.log(`Status: ${results.passed ? '\x1b[32m✅ READY for PR creation\x1b[0m' : '\x1b[31m❌ NOT READY\x1b[0m'}`);
// 5. Show verbose details if requested
if (args.verbose) {
printVerboseDetails(results);
}
// 6. Show issues to fix if not passing
if (!results.passed) {
printIssuesToFix(results);
}
// 7. Save quality report to workspace
console.log('');
try {
const saveResult = saveQualityReport(feature, {
score: results.score,
passed: results.passed,
timestamp: new Date().toISOString(),
gates: {
markdownLint: {
passed: results.markdownLint.passed,
errors: results.markdownLint.errors
},
manifestSync: {
synced: results.manifestSync.synced,
outOfSync: results.manifestSync.outOfSync
},
documentation: {
complete: results.documentation.complete,
missing: results.documentation.missing
},
breakingChanges: {
hasBreaking: results.breakingChanges.hasBreaking,
documented: results.breakingChanges.documented
},
tests: {
hasTests: results.tests.hasTests,
testFiles: results.tests.testFiles.length
}
}
});
if (saveResult.success) {
console.log(`Quality report saved: ${getWorkspacePath(feature)}/quality.json`);
} else {
console.warn(`Warning: Failed to save quality report: ${saveResult.error}`);
}
} catch (err) {
console.warn(`Warning: Failed to save quality report: ${err.message}`);
}
// 8. Exit with appropriate code
process.exit(results.passed ? 0 : 1);
}
// Run main function
main().catch(err => {
console.error('Fatal error:', err.message);
process.exit(3);
});