feat: Enterprise DSS architecture implementation
Some checks failed
DSS Project Analysis / dss-context-update (push) Has been cancelled

Complete implementation of enterprise design system validation:

Phase 1 - @dss/rules npm package:
- CLI with validate and init commands
- 16 rules across 5 categories (colors, spacing, typography, components, a11y)
- dss-ignore support (inline and next-line)
- Break-glass [dss-skip] for emergency merges
- CI workflow templates (Gitea, GitHub, GitLab)

Phase 2 - Metrics dashboard:
- FastAPI metrics API with SQLite storage
- Portfolio-wide metrics aggregation
- Project drill-down with file:line:column violations
- Trend charts and history tracking

Phase 3 - Local analysis cache:
- LocalAnalysisCache for offline-capable validation
- Mode detection (LOCAL/REMOTE/CI)
- Stale cache warnings with recommendations

Phase 4 - Project onboarding:
- dss-init command for project setup
- Creates ds.config.json, .dss/ folder structure
- Updates .gitignore and package.json scripts
- Optional CI workflow setup

Architecture decisions:
- No commit-back: CI uploads to dashboard, not git
- Three-tier: Dashboard (read-only) → CI (authoritative) → Local (advisory)
- Pull-based rules via npm for version control

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
DSS
2025-12-11 09:41:36 -03:00
parent ab8769933d
commit 9dbd56271e
27 changed files with 3888 additions and 398 deletions

View File

@@ -0,0 +1,361 @@
#!/usr/bin/env node
/**
* DSS Rules CLI
*
* Command-line tool for validating files against DSS design system rules.
* Used by CI pipelines, pre-commit hooks, and local development.
*/
const fs = require('fs');
const path = require('path');
const { glob } = require('glob');
const rules = require('../lib/index');
// ANSI colors
const c = {
red: '\x1b[31m',
yellow: '\x1b[33m',
green: '\x1b[32m',
blue: '\x1b[34m',
cyan: '\x1b[36m',
dim: '\x1b[2m',
reset: '\x1b[0m',
bold: '\x1b[1m'
};
/**
* Parse command line arguments
*/
function parseArgs(args) {
const options = {
files: [],
json: false,
baseline: null,
strict: false,
quiet: false,
help: false,
selfTest: false,
version: false,
ciMode: false,
fetchBaseline: null
};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === '--json') options.json = true;
else if (arg === '--strict') options.strict = true;
else if (arg === '--quiet' || arg === '-q') options.quiet = true;
else if (arg === '--help' || arg === '-h') options.help = true;
else if (arg === '--self-test') options.selfTest = true;
else if (arg === '--version' || arg === '-v') options.version = true;
else if (arg === '--ci') options.ciMode = true;
else if (arg === '--baseline' && args[i + 1]) {
options.baseline = args[++i];
}
else if (arg === '--fetch-baseline' && args[i + 1]) {
options.fetchBaseline = args[++i];
}
else if (!arg.startsWith('-')) {
options.files.push(arg);
}
}
return options;
}
/**
* Print help message
*/
function printHelp() {
console.log(`
${c.bold}@dss/rules${c.reset} - Design System Rules Validator
${c.bold}Usage:${c.reset}
dss-rules <command> [options]
${c.bold}Commands:${c.reset}
init Initialize DSS in a new project
validate Validate files against design system rules (default)
${c.bold}Validate Options:${c.reset}
-h, --help Show this help message
-v, --version Show version
--json Output results as JSON
--quiet, -q Minimal output (errors only)
--strict Treat warnings as errors
--ci CI mode (auto-detects baseline, version drift warnings)
--baseline <file> Compare against baseline JSON to show only new violations
--fetch-baseline <url> Fetch baseline from dashboard API
--self-test Verify rules package installation
${c.bold}Init Options:${c.reset}
--force, -f Overwrite existing configuration
--ci <platform> Set up CI workflow (gitea, github, gitlab)
--yes, -y Skip interactive prompts, use defaults
${c.bold}Examples:${c.reset}
dss-rules init # Initialize new project
dss-rules init --ci github # Initialize with GitHub Actions
dss-rules validate src/**/*.tsx # Validate files
dss-rules --ci --strict src/ # CI mode validation
dss-rules --json src/ > report.json # JSON output
${c.bold}Ignore Comments:${c.reset}
// dss-ignore Ignore current line
// dss-ignore-next-line Ignore next line
/* dss-ignore */ Block ignore (CSS)
${c.bold}Skip CI Validation:${c.reset}
git commit -m "fix: hotfix [dss-skip]"
${c.bold}Exit Codes:${c.reset}
0 All checks passed
1 Validation errors found
2 Configuration error
`);
}
/**
* Print version info
*/
function printVersion() {
console.log(`@dss/rules v${rules.getVersion()}`);
const config = rules.getCIConfig();
console.log(` ${config.blockingRules.length} blocking rules`);
console.log(` ${config.advisoryRules.length} advisory rules`);
}
/**
* Run self-test
*/
function selfTest() {
console.log(`${c.bold}Running @dss/rules self-test...${c.reset}\n`);
const allRules = rules.loadRules();
let passed = true;
let totalRules = 0;
for (const [category, ruleSet] of Object.entries(allRules)) {
if (!ruleSet) {
console.log(`${c.red}${c.reset} ${category}: Failed to load`);
passed = false;
continue;
}
const count = ruleSet.rules?.length || 0;
totalRules += count;
console.log(`${c.green}${c.reset} ${category}: ${count} rules (v${ruleSet.version})`);
}
const config = rules.getCIConfig();
console.log(`\n${c.bold}Summary:${c.reset}`);
console.log(` Package version: ${config.version}`);
console.log(` Total rules: ${totalRules}`);
console.log(` Blocking (error): ${config.blockingRules.length}`);
console.log(` Advisory (warning): ${config.advisoryRules.length}`);
if (passed) {
console.log(`\n${c.green}${c.bold}Self-test passed!${c.reset}`);
process.exit(0);
} else {
console.log(`\n${c.red}${c.bold}Self-test failed!${c.reset}`);
process.exit(2);
}
}
/**
* Expand glob patterns to file list
*/
async function expandGlobs(patterns) {
const files = [];
for (const pattern of patterns) {
if (pattern.includes('*')) {
const matches = await glob(pattern, { nodir: true });
files.push(...matches);
} else if (fs.existsSync(pattern)) {
const stat = fs.statSync(pattern);
if (stat.isDirectory()) {
const dirFiles = await glob(`${pattern}/**/*.{js,jsx,ts,tsx,css,scss,vue,svelte}`, { nodir: true });
files.push(...dirFiles);
} else {
files.push(pattern);
}
}
}
return [...new Set(files)];
}
/**
* Print validation results
*/
function printResults(results, options) {
if (!options.quiet) {
console.log(`\n${c.bold}=== DSS Rules Validation ===${c.reset}`);
console.log(`${c.dim}Rules version: ${results.rulesVersion}${c.reset}\n`);
}
for (const fileResult of results.fileResults) {
if (fileResult.errors.length === 0 && fileResult.warnings.length === 0) {
if (!options.quiet) {
console.log(`${c.green}${c.reset} ${fileResult.file}`);
}
continue;
}
const icon = fileResult.passed ? c.yellow + '⚠' : c.red + '✗';
console.log(`${icon}${c.reset} ${fileResult.file}`);
for (const error of fileResult.errors) {
console.log(` ${c.red}ERROR${c.reset} [${error.rule}] ${error.line}:${error.column}`);
console.log(` ${error.message}`);
console.log(` ${c.dim}Found: ${c.yellow}${error.match}${c.reset}`);
}
for (const warning of fileResult.warnings) {
if (!options.quiet) {
console.log(` ${c.yellow}WARN${c.reset} [${warning.rule}] ${warning.line}:${warning.column}`);
console.log(` ${warning.message}`);
}
}
if (fileResult.ignored.length > 0 && !options.quiet) {
console.log(` ${c.dim}(${fileResult.ignored.length} ignored)${c.reset}`);
}
}
// Summary
console.log(`\n${c.bold}=== Summary ===${c.reset}`);
console.log(`Files: ${results.passedFiles}/${results.totalFiles} passed`);
console.log(`Errors: ${c.red}${results.totalErrors}${c.reset}`);
console.log(`Warnings: ${c.yellow}${results.totalWarnings}${c.reset}`);
if (results.totalIgnored > 0) {
console.log(`Ignored: ${c.dim}${results.totalIgnored}${c.reset}`);
}
// New violations if baseline comparison
if (results.newErrors !== undefined) {
console.log(`\n${c.bold}New violations:${c.reset} ${results.newErrors.length} errors, ${results.newWarnings.length} warnings`);
console.log(`${c.dim}Existing:${c.reset} ${results.existingErrors.length} errors, ${results.existingWarnings.length} warnings`);
}
// Final status
if (results.totalErrors > 0) {
console.log(`\n${c.red}${c.bold}Validation failed!${c.reset}`);
} else if (results.totalWarnings > 0 && options.strict) {
console.log(`\n${c.yellow}${c.bold}Validation failed (strict mode)!${c.reset}`);
} else {
console.log(`\n${c.green}${c.bold}Validation passed!${c.reset}`);
}
}
/**
* Check for version drift and warn
*/
async function checkVersionDrift(options) {
if (!options.ciMode) return;
try {
// Check if there's a newer version available
const currentVersion = rules.getVersion();
// In real implementation, would check npm registry
// For now, just show the current version
console.log(`${c.cyan}[CI]${c.reset} Using @dss/rules v${currentVersion}`);
} catch (e) {
// Ignore version check errors
}
}
/**
* Load baseline for comparison
*/
function loadBaseline(baselinePath) {
if (!baselinePath) return null;
try {
if (fs.existsSync(baselinePath)) {
return JSON.parse(fs.readFileSync(baselinePath, 'utf-8'));
}
} catch (e) {
console.error(`${c.yellow}Warning: Could not load baseline: ${e.message}${c.reset}`);
}
return null;
}
/**
* Main entry point
*/
async function main() {
const args = process.argv.slice(2);
// Check for init command first
if (args[0] === 'init') {
// Delegate to init script
require('./init');
return;
}
// Check for validate command (default)
const validateArgs = args[0] === 'validate' ? args.slice(1) : args;
const options = parseArgs(validateArgs);
if (options.help) {
printHelp();
process.exit(0);
}
if (options.version) {
printVersion();
process.exit(0);
}
if (options.selfTest) {
selfTest();
return;
}
if (options.files.length === 0) {
console.error(`${c.red}Error: No files specified${c.reset}`);
console.log('Run with --help for usage information');
process.exit(2);
}
// Check version drift in CI mode
await checkVersionDrift(options);
// Expand globs
const files = await expandGlobs(options.files);
if (files.length === 0) {
console.error(`${c.yellow}Warning: No files matched the patterns${c.reset}`);
process.exit(0);
}
// Run validation
let results = rules.validateFiles(files);
// Compare with baseline if provided
const baseline = loadBaseline(options.baseline);
if (baseline) {
results = rules.compareWithBaseline(results, baseline);
}
// Output
if (options.json) {
console.log(JSON.stringify(results, null, 2));
} else {
printResults(results, options);
}
// Exit code
if (results.totalErrors > 0) {
process.exit(1);
}
if (options.strict && results.totalWarnings > 0) {
process.exit(1);
}
process.exit(0);
}
main().catch(err => {
console.error(`${c.red}Error: ${err.message}${c.reset}`);
process.exit(2);
});

View File

@@ -0,0 +1,489 @@
#!/usr/bin/env node
/**
* DSS Project Initialization CLI
*
* Sets up a new project for DSS validation:
* - Creates ds.config.json
* - Sets up .dss/ folder with .gitignore
* - Configures package.json scripts
* - Optionally sets up CI workflow
*
* Usage:
* npx @dss/rules init
* npx @dss/rules init --ci gitea
* npx @dss/rules init --force
*/
const fs = require('fs');
const path = require('path');
const readline = require('readline');
const PACKAGE_VERSION = require('../package.json').version;
// Template paths
const TEMPLATES_DIR = path.join(__dirname, '..', 'templates');
// Default config template
const DEFAULT_CONFIG = {
"$schema": "https://dss.overbits.luz.uy/schemas/ds.config.json",
"project": {
"id": "",
"name": "",
"description": "Design system validation for this project"
},
"extends": {
"skin": "classic"
},
"validation": {
"rules": ["colors", "spacing", "typography", "components", "accessibility"],
"severity": {
"colors": "error",
"spacing": "warning",
"typography": "warning",
"components": "error",
"accessibility": "warning"
}
},
"overrides": {
"tokens": {}
}
};
// CI platform configurations
const CI_PLATFORMS = {
gitea: {
name: 'Gitea Actions',
template: 'gitea-workflow.yml',
destDir: '.gitea/workflows',
destFile: 'dss-validate.yml'
},
github: {
name: 'GitHub Actions',
template: 'github-workflow.yml',
destDir: '.github/workflows',
destFile: 'dss-validate.yml'
},
gitlab: {
name: 'GitLab CI',
template: 'gitlab-ci.yml',
destDir: '',
destFile: '.gitlab-ci.yml'
}
};
async function main() {
const args = process.argv.slice(2);
const options = parseArgs(args);
console.log('\n🎨 DSS Project Initialization\n');
console.log(`Version: ${PACKAGE_VERSION}`);
console.log('─'.repeat(40) + '\n');
const projectRoot = process.cwd();
// Check if already initialized
const configPath = path.join(projectRoot, 'ds.config.json');
if (fs.existsSync(configPath) && !options.force) {
console.log('⚠️ Project already initialized (ds.config.json exists)');
console.log(' Use --force to reinitialize\n');
process.exit(1);
}
// Interactive mode if not all options provided
const config = options.interactive
? await interactiveSetup(projectRoot)
: await autoSetup(projectRoot, options);
// 1. Create ds.config.json
console.log('📝 Creating ds.config.json...');
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
console.log(' ✓ Configuration file created\n');
// 2. Create .dss/ folder structure
console.log('📁 Setting up .dss/ folder...');
setupDssFolder(projectRoot);
console.log(' ✓ Local cache folder ready\n');
// 3. Update .gitignore
console.log('📋 Updating .gitignore...');
updateGitignore(projectRoot);
console.log(' ✓ .gitignore updated\n');
// 4. Add npm scripts
console.log('📦 Adding npm scripts...');
addNpmScripts(projectRoot);
console.log(' ✓ Package.json updated\n');
// 5. Setup CI if requested
if (options.ci) {
console.log(`🔧 Setting up ${CI_PLATFORMS[options.ci]?.name || options.ci} CI...`);
setupCI(projectRoot, options.ci);
console.log(' ✓ CI workflow created\n');
}
// Success message
console.log('─'.repeat(40));
console.log('\n✅ DSS initialization complete!\n');
console.log('Next steps:');
console.log(' 1. Review ds.config.json and customize rules');
console.log(' 2. Run: npx dss-rules validate');
console.log(' 3. Fix any violations found\n');
if (!options.ci) {
console.log('💡 Tip: Set up CI validation with:');
console.log(' npx @dss/rules init --ci gitea\n');
}
}
function parseArgs(args) {
const options = {
force: false,
ci: null,
interactive: true,
projectId: null,
projectName: null
};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === '--force' || arg === '-f') {
options.force = true;
} else if (arg === '--ci') {
options.ci = args[++i] || 'gitea';
} else if (arg === '--yes' || arg === '-y') {
options.interactive = false;
} else if (arg === '--id') {
options.projectId = args[++i];
} else if (arg === '--name') {
options.projectName = args[++i];
} else if (arg === '--help' || arg === '-h') {
showHelp();
process.exit(0);
}
}
return options;
}
function showHelp() {
console.log(`
DSS Project Initialization
Usage:
npx @dss/rules init [options]
Options:
--force, -f Overwrite existing configuration
--ci <platform> Set up CI workflow (gitea, github, gitlab)
--yes, -y Skip interactive prompts, use defaults
--id <id> Project ID (default: directory name)
--name <name> Project display name
--help, -h Show this help message
Examples:
npx @dss/rules init
npx @dss/rules init --ci gitea
npx @dss/rules init -y --ci github
npx @dss/rules init --id my-app --name "My Application"
`);
}
async function interactiveSetup(projectRoot) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const question = (prompt) => new Promise(resolve => rl.question(prompt, resolve));
try {
const dirName = path.basename(projectRoot);
const projectId = await question(`Project ID [${dirName}]: `) || dirName;
const projectName = await question(`Project Name [${projectId}]: `) || projectId;
const skin = await question('Base skin [classic]: ') || 'classic';
rl.close();
const config = { ...DEFAULT_CONFIG };
config.project.id = projectId;
config.project.name = projectName;
config.extends.skin = skin;
return config;
} catch (e) {
rl.close();
throw e;
}
}
async function autoSetup(projectRoot, options) {
const dirName = path.basename(projectRoot);
const config = { ...DEFAULT_CONFIG };
config.project.id = options.projectId || dirName;
config.project.name = options.projectName || config.project.id;
return config;
}
function setupDssFolder(projectRoot) {
const dssDir = path.join(projectRoot, '.dss');
const cacheDir = path.join(dssDir, 'cache');
// Create directories
if (!fs.existsSync(dssDir)) {
fs.mkdirSync(dssDir, { recursive: true });
}
if (!fs.existsSync(cacheDir)) {
fs.mkdirSync(cacheDir, { recursive: true });
}
// Create .gitignore in .dss/
const gitignorePath = path.join(dssDir, '.gitignore');
const gitignoreContent = `# DSS local cache - do not commit
*
!.gitignore
`;
fs.writeFileSync(gitignorePath, gitignoreContent);
// Create metadata.json
const metadataPath = path.join(dssDir, 'metadata.json');
const metadata = {
initialized_at: new Date().toISOString(),
rules_version: PACKAGE_VERSION,
last_updated: null
};
fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
}
function updateGitignore(projectRoot) {
const gitignorePath = path.join(projectRoot, '.gitignore');
// Entries to add
const entries = [
'',
'# DSS local analysis cache',
'.dss/',
'!.dss/.gitignore'
];
let content = '';
if (fs.existsSync(gitignorePath)) {
content = fs.readFileSync(gitignorePath, 'utf-8');
// Check if already configured
if (content.includes('.dss/')) {
console.log(' (already configured)');
return;
}
}
// Append entries
const newContent = content + entries.join('\n') + '\n';
fs.writeFileSync(gitignorePath, newContent);
}
function addNpmScripts(projectRoot) {
const packagePath = path.join(projectRoot, 'package.json');
if (!fs.existsSync(packagePath)) {
console.log(' (no package.json found, skipping)');
return;
}
try {
const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf-8'));
// Add scripts
pkg.scripts = pkg.scripts || {};
if (!pkg.scripts['dss:validate']) {
pkg.scripts['dss:validate'] = 'dss-rules validate';
}
if (!pkg.scripts['dss:validate:ci']) {
pkg.scripts['dss:validate:ci'] = 'dss-rules validate --ci --strict --json > .dss/results.json';
}
if (!pkg.scripts['dss:baseline']) {
pkg.scripts['dss:baseline'] = 'dss-rules validate --baseline';
}
fs.writeFileSync(packagePath, JSON.stringify(pkg, null, 2) + '\n');
} catch (e) {
console.log(` ⚠️ Failed to update package.json: ${e.message}`);
}
}
function setupCI(projectRoot, platform) {
const ciConfig = CI_PLATFORMS[platform];
if (!ciConfig) {
console.log(` ⚠️ Unknown CI platform: ${platform}`);
console.log(` Supported: ${Object.keys(CI_PLATFORMS).join(', ')}`);
return;
}
// Read template
const templatePath = path.join(TEMPLATES_DIR, ciConfig.template);
if (!fs.existsSync(templatePath)) {
// Create a default template inline
const template = getDefaultCITemplate(platform);
writeCIFile(projectRoot, ciConfig, template);
return;
}
const template = fs.readFileSync(templatePath, 'utf-8');
writeCIFile(projectRoot, ciConfig, template);
}
function writeCIFile(projectRoot, ciConfig, content) {
const destDir = path.join(projectRoot, ciConfig.destDir);
const destPath = path.join(destDir, ciConfig.destFile);
// Create directory
if (ciConfig.destDir && !fs.existsSync(destDir)) {
fs.mkdirSync(destDir, { recursive: true });
}
fs.writeFileSync(destPath, content);
}
function getDefaultCITemplate(platform) {
if (platform === 'github') {
return `# DSS Design System Validation
name: DSS Validate
on:
push:
branches: [main, master, develop]
pull_request:
branches: [main, master]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Check for [dss-skip]
id: skip-check
run: |
if git log -1 --pretty=%B | grep -q '\\[dss-skip\\]'; then
echo "skip=true" >> $GITHUB_OUTPUT
echo "⚠️ DSS validation skipped via [dss-skip] commit message"
else
echo "skip=false" >> $GITHUB_OUTPUT
fi
- name: Run DSS validation
if: steps.skip-check.outputs.skip != 'true'
run: npm run dss:validate:ci
- name: Upload metrics to dashboard
if: steps.skip-check.outputs.skip != 'true'
run: |
curl -X POST "\${DSS_DASHBOARD_URL}/api/metrics/upload" \\
-H "Content-Type: application/json" \\
-H "Authorization: Bearer \${DSS_API_TOKEN}" \\
-d @.dss/results.json
env:
DSS_DASHBOARD_URL: \${{ secrets.DSS_DASHBOARD_URL }}
DSS_API_TOKEN: \${{ secrets.DSS_API_TOKEN }}
`;
}
if (platform === 'gitlab') {
return `# DSS Design System Validation
stages:
- validate
dss-validate:
stage: validate
image: node:20
script:
- npm ci
- |
if git log -1 --pretty=%B | grep -q '\\[dss-skip\\]'; then
echo "⚠️ DSS validation skipped via [dss-skip] commit message"
exit 0
fi
- npm run dss:validate:ci
- |
curl -X POST "\${DSS_DASHBOARD_URL}/api/metrics/upload" \\
-H "Content-Type: application/json" \\
-H "Authorization: Bearer \${DSS_API_TOKEN}" \\
-d @.dss/results.json
only:
- main
- master
- develop
- merge_requests
`;
}
// Default to gitea template (most similar to the one in templates/)
return `# DSS Design System Validation
name: DSS Validate
on:
push:
branches: [main, master, develop]
pull_request:
branches: [main, master]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Check for [dss-skip]
id: skip-check
run: |
if git log -1 --pretty=%B | grep -q '\\[dss-skip\\]'; then
echo "skip=true" >> \$GITHUB_OUTPUT
echo "::warning::DSS validation skipped via [dss-skip] commit message"
else
echo "skip=false" >> \$GITHUB_OUTPUT
fi
- name: Run DSS validation
if: steps.skip-check.outputs.skip != 'true'
run: npm run dss:validate:ci
- name: Upload metrics to dashboard
if: steps.skip-check.outputs.skip != 'true' && always()
run: |
curl -X POST "\${DSS_DASHBOARD_URL}/api/metrics/upload" \\
-H "Content-Type: application/json" \\
-H "Authorization: Bearer \${DSS_API_TOKEN}" \\
-d @.dss/results.json
env:
DSS_DASHBOARD_URL: \${{ secrets.DSS_DASHBOARD_URL }}
DSS_API_TOKEN: \${{ secrets.DSS_API_TOKEN }}
`;
}
// Run main
main().catch(err => {
console.error('\n❌ Initialization failed:', err.message);
process.exit(1);
});