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

This reverts commit 9dbd56271e.
This commit is contained in:
DSS
2025-12-11 09:59:45 -03:00
parent 9dbd56271e
commit 44cea9443b
27 changed files with 397 additions and 3887 deletions

View File

@@ -1,361 +0,0 @@
#!/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

@@ -1,489 +0,0 @@
#!/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);
});

View File

@@ -1,23 +1,19 @@
/**
* @dss/rules - Design System Rules Package
*
* Versioned rule definitions for enterprise design system enforcement.
* Pull-based distribution via npm for consistent rule versions across projects.
* Provides versioned rule definitions for enterprise design system enforcement.
* Pull-based distribution via npm for consistent rule versions across 60+ projects.
*/
const fs = require('fs');
const path = require('path');
// Rule categories
const CATEGORIES = ['colors', 'spacing', 'typography', 'components', 'accessibility'];
// Match dss-ignore in various comment styles
// - // dss-ignore (JS/TS line comment)
// - /* dss-ignore */ (CSS/JS block comment)
// - # dss-ignore (Python/YAML/Shell comment)
const IGNORE_PATTERN = /\/\/\s*dss-ignore(-next-line)?|\/\*\s*dss-ignore(-next-line)?\s*\*\/|#\s*dss-ignore(-next-line)?/;
const SKIP_COMMIT_PATTERN = /\[dss-skip\]/;
/**
* Load all rules from the rules directory
* @returns {Object} Rules organized by category
*/
function loadRules() {
const rules = {};
@@ -27,18 +23,22 @@ function loadRules() {
const rulePath = path.join(rulesDir, `${category}.json`);
if (fs.existsSync(rulePath)) {
try {
rules[category] = JSON.parse(fs.readFileSync(rulePath, 'utf-8'));
const content = fs.readFileSync(rulePath, 'utf-8');
rules[category] = JSON.parse(content);
} catch (error) {
console.error(`Failed to load rules for ${category}:`, error.message);
rules[category] = null;
}
}
}
return rules;
}
/**
* Get rules for a specific category
* @param {string} category - Rule category (colors, spacing, etc.)
* @returns {Object|null} Rule definitions or null if not found
*/
function getRulesByCategory(category) {
const rules = loadRules();
@@ -47,244 +47,112 @@ function getRulesByCategory(category) {
/**
* Get all rule IDs across all categories
* @returns {string[]} Array of rule IDs in format "category/rule-id"
*/
function getAllRuleIds() {
const rules = loadRules();
const ids = [];
for (const [category, ruleSet] of Object.entries(rules)) {
if (ruleSet?.rules) {
if (ruleSet && ruleSet.rules) {
for (const rule of ruleSet.rules) {
ids.push(`${category}/${rule.id}`);
}
}
}
return ids;
}
/**
* Get a specific rule by full ID (category/rule-id)
* Get a specific rule by its full ID
* @param {string} ruleId - Full rule ID in format "category/rule-id"
* @returns {Object|null} Rule definition or null
*/
function getRule(ruleId) {
const [category, id] = ruleId.split('/');
const ruleSet = getRulesByCategory(category);
if (!ruleSet?.rules) return null;
if (!ruleSet || !ruleSet.rules) return null;
return ruleSet.rules.find(r => r.id === id) || null;
}
/**
* Get rule severity
* Validate a value against rule patterns
* @param {string} ruleId - Full rule ID
* @param {string} value - Value to validate
* @returns {Object} Validation result {valid, violations}
*/
function validateValue(ruleId, value) {
const rule = getRule(ruleId);
if (!rule) {
return { valid: true, violations: [], error: `Rule not found: ${ruleId}` };
}
const violations = [];
// Check forbidden patterns
if (rule.patterns?.forbidden) {
for (const pattern of rule.patterns.forbidden) {
const regex = new RegExp(pattern, 'gi');
const matches = value.match(regex);
if (matches) {
violations.push({
rule: ruleId,
pattern,
matches,
severity: rule.severity || 'warning',
message: `Found forbidden pattern: ${matches.join(', ')}`
});
}
}
}
return {
valid: violations.length === 0,
violations
};
}
/**
* Get required tokens from all rule sets
* @returns {Object} Required tokens organized by category
*/
function getRequiredTokens() {
const rules = loadRules();
const required = {};
for (const [category, ruleSet] of Object.entries(rules)) {
if (ruleSet?.tokens?.required) {
required[category] = ruleSet.tokens.required;
}
}
return required;
}
/**
* Get severity level for a rule
* @param {string} ruleId - Full rule ID
* @returns {string} Severity level (error, warning, info)
*/
function getRuleSeverity(ruleId) {
const rule = getRule(ruleId);
if (!rule) return 'warning';
// Rule-specific severity overrides category default
if (rule.severity) return rule.severity;
// Fall back to category default
const [category] = ruleId.split('/');
const ruleSet = getRulesByCategory(category);
return ruleSet?.severity || 'warning';
}
/**
* Check if a line has dss-ignore comment
*/
function isLineIgnored(lines, lineNumber) {
if (lineNumber <= 0 || lineNumber > lines.length) return false;
const currentLine = lines[lineNumber - 1];
const previousLine = lineNumber > 1 ? lines[lineNumber - 2] : '';
// Check current line for inline ignore (on same line as violation)
if (IGNORE_PATTERN.test(currentLine)) return true;
// Check previous line for dss-ignore-next-line OR standalone dss-ignore
// A standalone /* dss-ignore */ on its own line ignores the next line
if (/dss-ignore-next-line/.test(previousLine)) return true;
// Check if previous line is ONLY a dss-ignore comment (standalone)
// This handles: /* dss-ignore */ on its own line
const standaloneIgnore = /^\s*(\/\*\s*dss-ignore\s*\*\/|\/\/\s*dss-ignore|#\s*dss-ignore)\s*$/;
if (standaloneIgnore.test(previousLine)) return true;
return false;
}
/**
* Validate file content against rules with dss-ignore support
*/
function validateContent(content, filePath, options = {}) {
const results = {
file: filePath,
errors: [],
warnings: [],
info: [],
ignored: [],
passed: true
};
const lines = content.split('\n');
const ext = path.extname(filePath).toLowerCase();
const applicableCategories = getApplicableCategories(ext);
for (const category of applicableCategories) {
const ruleSet = getRulesByCategory(category);
if (!ruleSet?.rules) continue;
for (const rule of ruleSet.rules) {
// Skip if file matches exception patterns
if (rule.exceptions?.some(exc => {
// Handle glob-like patterns more carefully
// *.test.* should only match filenames like "foo.test.js", not paths containing "test"
if (exc.startsWith('**/')) {
// Directory pattern: **/fixtures/** -> match any path containing /fixtures/
const dirName = exc.replace(/^\*\*\//, '').replace(/\/\*\*$/, '');
return filePath.includes(`/${dirName}/`);
} else if (exc.includes('/')) {
// Path pattern
const pattern = exc.replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*');
return new RegExp(pattern).test(filePath);
} else if (exc.startsWith('*.') || exc.endsWith('.*')) {
// Filename extension pattern: *.test.* matches only the basename
const basename = path.basename(filePath);
const pattern = '^' + exc.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$';
return new RegExp(pattern).test(basename);
} else {
// Simple value exception (like "transparent", "inherit")
return false; // These are value exceptions, not file exceptions
}
})) continue;
// Check forbidden patterns
if (rule.patterns?.forbidden) {
for (const pattern of rule.patterns.forbidden) {
try {
const regex = new RegExp(pattern, 'gm');
let match;
while ((match = regex.exec(content)) !== null) {
const lineNumber = content.substring(0, match.index).split('\n').length;
const column = match.index - content.lastIndexOf('\n', match.index - 1);
// Check if this line is ignored
if (isLineIgnored(lines, lineNumber)) {
results.ignored.push({
rule: `${category}/${rule.id}`,
line: lineNumber,
column,
match: match[0]
});
continue;
}
const violation = {
rule: `${category}/${rule.id}`,
name: rule.name,
file: filePath,
line: lineNumber,
column,
match: match[0],
message: rule.description || `Violation of ${rule.name}`
};
const severity = rule.severity || ruleSet.severity || 'warning';
if (severity === 'error') {
results.errors.push(violation);
results.passed = false;
} else if (severity === 'warning') {
results.warnings.push(violation);
} else {
results.info.push(violation);
}
}
} catch (e) {
// Invalid regex, skip
}
}
}
}
}
return results;
}
/**
* Validate a file from disk
*/
function validateFile(filePath, options = {}) {
if (!fs.existsSync(filePath)) {
return {
file: filePath,
errors: [{ message: `File not found: ${filePath}` }],
warnings: [],
info: [],
ignored: [],
passed: false
};
}
const content = fs.readFileSync(filePath, 'utf-8');
return validateContent(content, filePath, options);
}
/**
* Determine applicable rule categories based on file extension
*/
function getApplicableCategories(ext) {
const cssTypes = ['.css', '.scss', '.sass', '.less', '.styl'];
const jsTypes = ['.js', '.jsx', '.ts', '.tsx', '.vue', '.svelte'];
const htmlTypes = ['.html', '.htm', '.vue', '.svelte', '.jsx', '.tsx'];
const categories = [];
if (cssTypes.includes(ext)) categories.push('colors', 'spacing', 'typography', 'accessibility');
if (jsTypes.includes(ext)) categories.push('colors', 'spacing', 'components');
if (htmlTypes.includes(ext)) categories.push('accessibility', 'components');
return [...new Set(categories)];
}
/**
* Validate multiple files
*/
function validateFiles(files, options = {}) {
const results = {
totalFiles: files.length,
passedFiles: 0,
failedFiles: 0,
totalErrors: 0,
totalWarnings: 0,
totalIgnored: 0,
fileResults: [],
rulesVersion: getVersion()
};
for (const file of files) {
const fileResult = validateFile(file, options);
results.fileResults.push(fileResult);
if (fileResult.passed) {
results.passedFiles++;
} else {
results.failedFiles++;
}
results.totalErrors += fileResult.errors.length;
results.totalWarnings += fileResult.warnings.length;
results.totalIgnored += fileResult.ignored.length;
}
return results;
}
/**
* Get required tokens from all rule sets
*/
function getRequiredTokens() {
const rules = loadRules();
const required = {};
for (const [category, ruleSet] of Object.entries(rules)) {
if (ruleSet?.tokens?.required) {
required[category] = ruleSet.tokens.required;
}
}
return required;
}
/**
* Get package version
* @returns {string} Package version
*/
function getVersion() {
const packagePath = path.join(__dirname, '..', 'package.json');
@@ -293,95 +161,38 @@ function getVersion() {
}
/**
* Check if commit message contains skip flag
*/
function shouldSkipValidation(commitMessage) {
return SKIP_COMMIT_PATTERN.test(commitMessage);
}
/**
* Get CI configuration
* Export configuration for CI/CD integration
* @returns {Object} Configuration object for CI pipelines
*/
function getCIConfig() {
return {
version: getVersion(),
categories: CATEGORIES,
errorSeverities: ['error'],
warningSeverities: ['warning'],
blockingRules: getAllRuleIds().filter(id => getRuleSeverity(id) === 'error'),
advisoryRules: getAllRuleIds().filter(id => getRuleSeverity(id) !== 'error'),
skipPattern: '[dss-skip]'
advisoryRules: getAllRuleIds().filter(id => getRuleSeverity(id) !== 'error')
};
}
/**
* Compare against baseline to find new violations only
*/
function compareWithBaseline(current, baseline) {
if (!baseline) return current;
const baselineViolations = new Set(
baseline.fileResults?.flatMap(f =>
[...f.errors, ...f.warnings].map(v => `${v.file}:${v.rule}:${v.line}`)
) || []
);
const newResults = {
...current,
newErrors: [],
newWarnings: [],
existingErrors: [],
existingWarnings: []
};
for (const fileResult of current.fileResults) {
for (const error of fileResult.errors) {
const key = `${error.file}:${error.rule}:${error.line}`;
if (baselineViolations.has(key)) {
newResults.existingErrors.push(error);
} else {
newResults.newErrors.push(error);
}
}
for (const warning of fileResult.warnings) {
const key = `${warning.file}:${warning.rule}:${warning.line}`;
if (baselineViolations.has(key)) {
newResults.existingWarnings.push(warning);
} else {
newResults.newWarnings.push(warning);
}
}
}
return newResults;
}
module.exports = {
// Rule loading
loadRules,
getRulesByCategory,
getAllRuleIds,
getRule,
getRuleSeverity,
// Validation
validateContent,
validateFile,
validateFiles,
isLineIgnored,
getApplicableCategories,
// Baseline comparison
compareWithBaseline,
// CI helpers
getCIConfig,
shouldSkipValidation,
validateValue,
getRuleSeverity,
// Token helpers
getRequiredTokens,
// Metadata
getVersion,
CATEGORIES,
IGNORE_PATTERN,
SKIP_COMMIT_PATTERN
getCIConfig,
// Constants
CATEGORIES
};

View File

@@ -1,23 +1,17 @@
{
"name": "@dss/rules",
"version": "1.0.0",
"description": "DSS Design System Rules - Versioned rule definitions for enterprise enforcement",
"description": "DSS Design System Rules - Versioned rule definitions for enterprise design system enforcement",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"bin": {
"dss-rules": "bin/cli.js",
"dss-init": "bin/init.js"
},
"files": [
"lib",
"bin",
"rules",
"schemas",
"templates"
"schemas"
],
"scripts": {
"build": "tsc",
"test": "node bin/cli.js --self-test",
"test": "node lib/validate.js --self-test",
"prepublishOnly": "npm run build && npm test"
},
"keywords": [
@@ -25,14 +19,16 @@
"dss",
"rules",
"tokens",
"enterprise",
"linting"
"enterprise"
],
"author": "DSS Team",
"license": "MIT",
"devDependencies": {
"typescript": "^5.0.0"
},
"peerDependencies": {
"ajv": "^8.0.0"
},
"engines": {
"node": ">=18.0.0"
},

View File

@@ -3,20 +3,22 @@
"id": "accessibility",
"version": "1.0.0",
"name": "Accessibility Rules",
"description": "WCAG 2.1 AA compliance rules (token-based, not computed)",
"description": "WCAG 2.1 AA compliance rules for accessible design",
"category": "accessibility",
"severity": "error",
"rules": [
{
"id": "images-have-alt",
"name": "Images Must Have Alt Text",
"description": "All img elements must have alt attribute",
"description": "All img elements must have meaningful alt text or be marked decorative",
"severity": "error",
"wcag": "1.1.1",
"validation": {
"type": "attribute-required",
"element": "img",
"attribute": "alt"
"attribute": "alt",
"allowEmpty": true,
"emptyMeansDecorative": true
}
},
{
@@ -27,7 +29,8 @@
"wcag": "4.1.2",
"validation": {
"type": "accessible-name",
"elements": ["button", "[role=button]"]
"elements": ["button", "[role=button]"],
"sources": ["text content", "aria-label", "aria-labelledby"]
}
},
{
@@ -38,38 +41,71 @@
"wcag": "1.3.1",
"validation": {
"type": "label-association",
"elements": ["input", "select", "textarea"]
"elements": ["input", "select", "textarea"],
"methods": ["for/id", "aria-labelledby", "aria-label", "wrapper"]
}
},
{
"id": "no-focus-outline-none",
"name": "Do Not Remove Focus Outline",
"description": "Never use outline: none on focusable elements",
"id": "focus-visible",
"name": "Focus Must Be Visible",
"description": "Interactive elements must have visible focus indicators",
"severity": "error",
"wcag": "2.4.7",
"patterns": {
"forbidden": [
"outline:\\s*none",
"outline:\\s*0(?![0-9])",
":focus\\s*\\{[^}]*outline:\\s*none"
]
"validation": {
"type": "focus-style",
"minContrastRatio": 3.0,
"forbiddenPatterns": ["outline: none", "outline: 0", ":focus { outline: none }"]
}
},
{
"id": "color-not-only",
"name": "Color Not Only Indicator",
"description": "Information must not be conveyed by color alone",
"severity": "warning",
"wcag": "1.4.1",
"guidelines": [
"Error states need icon + color + text",
"Links in text need underline or other indicator",
"Status indicators need icon or pattern"
]
},
{
"id": "touch-target-size",
"name": "Minimum Touch Target Size",
"description": "Interactive elements should be at least 44x44 CSS pixels",
"description": "Interactive elements must be at least 44x44 CSS pixels",
"severity": "warning",
"wcag": "2.5.5",
"guidelines": [
"Use Button component which ensures minimum size",
"Ensure clickable areas have sufficient padding"
]
"validation": {
"type": "size-check",
"minWidth": 44,
"minHeight": 44,
"elements": ["button", "a", "[role=button]", "input[type=checkbox]", "input[type=radio]"]
}
},
{
"id": "keyboard-navigation",
"name": "Keyboard Navigation",
"description": "All functionality must be accessible via keyboard",
"severity": "error",
"wcag": "2.1.1",
"validation": {
"type": "keyboard-accessible",
"requirements": [
"All interactive elements focusable",
"No keyboard traps",
"Logical tab order",
"Skip links for navigation"
]
}
}
],
"compliance": {
"level": "AA",
"standards": ["WCAG 2.1"],
"note": "Computed checks (contrast ratio) require runtime analysis"
"testingTools": [
"axe-core",
"pa11y",
"lighthouse"
]
}
}

View File

@@ -10,11 +10,11 @@
{
"id": "no-hardcoded-colors",
"name": "No Hardcoded Colors",
"description": "Colors must use design tokens, not hardcoded hex/rgb values",
"description": "All colors must use design tokens, not hardcoded hex/rgb values",
"severity": "error",
"patterns": {
"forbidden": [
"#[0-9a-fA-F]{3,8}(?![0-9a-fA-F])",
"#[0-9a-fA-F]{3,8}",
"rgb\\([^)]+\\)",
"rgba\\([^)]+\\)",
"hsl\\([^)]+\\)",
@@ -27,7 +27,11 @@
"theme\\.[a-z]+"
]
},
"exceptions": ["*.test.*", "*.spec.*", "**/fixtures/**", "transparent", "inherit", "currentColor"]
"exceptions": [
"*.test.*",
"*.spec.*",
"**/fixtures/**"
]
},
{
"id": "semantic-color-naming",
@@ -52,7 +56,20 @@
}
],
"tokens": {
"required": ["colors.primary", "colors.secondary", "colors.background", "colors.foreground", "colors.border", "colors.error", "colors.success", "colors.warning"],
"optional": ["colors.muted", "colors.accent", "colors.info"]
"required": [
"colors.primary",
"colors.secondary",
"colors.background",
"colors.foreground",
"colors.border",
"colors.error",
"colors.success",
"colors.warning"
],
"optional": [
"colors.muted",
"colors.accent",
"colors.info"
]
}
}

View File

@@ -10,7 +10,7 @@
{
"id": "use-design-system-components",
"name": "Use Design System Components",
"description": "Prefer design system components over native HTML or custom implementations",
"description": "Prefer design system components over custom implementations",
"severity": "error",
"components": {
"required": {
@@ -44,12 +44,17 @@
"severity": "error",
"validation": {
"Button": {
"requiredProps": ["variant"],
"conditionalProps": { "loading": ["loadingText"], "icon": ["aria-label"] }
"requiredProps": ["variant", "size"],
"conditionalProps": {
"loading": ["loadingText"],
"icon": ["aria-label"]
}
},
"Input": {
"requiredProps": ["label", "name"],
"conditionalProps": { "error": ["errorMessage"] }
"conditionalProps": {
"error": ["errorMessage"]
}
},
"Modal": {
"requiredProps": ["title", "onClose"],
@@ -57,15 +62,52 @@
}
}
},
{
"id": "component-composition",
"name": "Component Composition Patterns",
"description": "Follow recommended composition patterns for complex UIs",
"severity": "info",
"patterns": {
"forms": {
"structure": ["Form", "FormField", "Input/Select", "Button"],
"guidelines": [
"Wrap inputs in FormField for consistent labeling",
"Use Form component for validation handling",
"Place submit button inside Form"
]
},
"lists": {
"structure": ["List", "ListItem"],
"guidelines": [
"Use semantic list components for accessibility",
"Implement virtualization for 50+ items"
]
},
"navigation": {
"structure": ["Nav", "NavItem", "NavLink"],
"guidelines": [
"Use Nav component for main navigation",
"Implement active state handling"
]
}
}
},
{
"id": "no-inline-styles",
"name": "No Inline Styles on Components",
"description": "Components should use className/variant props, not style attribute",
"severity": "warning",
"patterns": {
"forbidden": ["style={{", "style={"]
},
"exceptions": ["dynamic positioning", "animations", "calculated values"]
"forbidden": [
"style={{",
"style={{"
],
"exceptions": [
"dynamic positioning",
"animations",
"calculated values"
]
}
}
],
"adoption": {
@@ -74,6 +116,10 @@
"target": 80,
"excellent": 95
},
"metrics": ["percentage_using_ds_components", "custom_component_count", "token_compliance_rate"]
"metrics": [
"percentage_using_ds_components",
"custom_component_count",
"token_compliance_rate"
]
}
}

View File

@@ -10,7 +10,7 @@
{
"id": "no-arbitrary-spacing",
"name": "No Arbitrary Spacing Values",
"description": "Spacing must use token scale, not arbitrary pixel values",
"description": "Spacing must use token scale (4px increments), not arbitrary values",
"severity": "warning",
"patterns": {
"forbidden": [
@@ -24,7 +24,12 @@
"spacing\\.[a-z0-9]+"
]
},
"exceptions": ["0", "0px", "auto", "inherit"]
"exceptions": [
"0",
"0px",
"auto",
"inherit"
]
},
{
"id": "spacing-scale",
@@ -35,10 +40,29 @@
"type": "scale-check",
"allowedValues": [0, 4, 8, 12, 16, 20, 24, 32, 40, 48, 64, 80, 96, 128]
}
},
{
"id": "consistent-component-spacing",
"name": "Component Internal Spacing",
"description": "Components should use consistent internal spacing patterns",
"severity": "info",
"guidelines": [
"Use spacing.xs (4px) for tight groupings",
"Use spacing.sm (8px) for related elements",
"Use spacing.md (16px) for section separation",
"Use spacing.lg (24px) for major sections",
"Use spacing.xl (32px+) for page-level separation"
]
}
],
"tokens": {
"required": ["spacing.xs", "spacing.sm", "spacing.md", "spacing.lg", "spacing.xl"],
"required": [
"spacing.xs",
"spacing.sm",
"spacing.md",
"spacing.lg",
"spacing.xl"
],
"scale": {
"xs": "4px",
"sm": "8px",

View File

@@ -10,16 +10,15 @@
{
"id": "use-typography-scale",
"name": "Use Typography Scale",
"description": "Font sizes must use the defined typography scale tokens",
"description": "Font sizes must use the defined typography scale",
"severity": "error",
"patterns": {
"forbidden": [
"font-size:\\s*[0-9]+px",
"fontSize:\\s*[0-9]+",
"fontSize:\\s*'[0-9]+px'"
"fontSize:\\s*[0-9]+"
],
"allowed": [
"var\\(--font-size-[a-z0-9]+\\)",
"var\\(--font-size-[a-z]+\\)",
"\\$font-size-[a-z]+",
"typography\\.[a-z]+"
]
@@ -37,25 +36,26 @@
}
},
{
"id": "no-font-family-override",
"name": "No Font Family Override",
"description": "Font families should use design system tokens",
"severity": "warning",
"patterns": {
"forbidden": [
"font-family:\\s*['\"][^'\"]+['\"]",
"fontFamily:\\s*['\"][^'\"]+['\"]"
],
"allowed": [
"var\\(--font-[a-z]+\\)",
"\\$font-[a-z]+",
"fonts\\.[a-z]+"
]
}
"id": "line-height-consistency",
"name": "Consistent Line Heights",
"description": "Line heights should match the typography scale",
"severity": "info",
"guidelines": [
"Use lineHeight.tight (1.25) for headings",
"Use lineHeight.normal (1.5) for body text",
"Use lineHeight.relaxed (1.75) for long-form content"
]
}
],
"tokens": {
"required": ["typography.h1", "typography.h2", "typography.h3", "typography.body", "typography.small"],
"required": [
"typography.h1",
"typography.h2",
"typography.h3",
"typography.body",
"typography.small",
"typography.caption"
],
"scale": {
"xs": "12px",
"sm": "14px",
@@ -64,7 +64,12 @@
"xl": "20px",
"2xl": "24px",
"3xl": "30px",
"4xl": "36px"
"4xl": "36px",
"5xl": "48px"
},
"fontFamilies": {
"sans": "Inter, system-ui, sans-serif",
"mono": "JetBrains Mono, monospace"
}
}
}

View File

@@ -1,118 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://dss.overbits.luz.uy/schemas/ds.config.schema.json",
"title": "DSS Project Configuration",
"description": "Configuration schema for DSS-enabled projects",
"type": "object",
"required": ["name", "rules"],
"properties": {
"$schema": {
"type": "string"
},
"name": {
"type": "string",
"description": "Project name"
},
"version": {
"type": "string",
"description": "Project version"
},
"rules": {
"type": "object",
"required": ["package"],
"properties": {
"package": {
"type": "string",
"description": "Rules package name (e.g., @dss/rules)"
},
"version": {
"type": "string",
"description": "Semver version constraint"
},
"overrides": {
"type": "object",
"description": "Rule-specific overrides",
"additionalProperties": {
"type": "object",
"properties": {
"severity": {
"enum": ["error", "warning", "info", "off"]
},
"enabled": {
"type": "boolean"
}
}
}
}
}
},
"analysis": {
"type": "object",
"properties": {
"include": {
"type": "array",
"items": { "type": "string" },
"description": "Glob patterns for files to analyze"
},
"exclude": {
"type": "array",
"items": { "type": "string" },
"description": "Glob patterns for files to exclude"
},
"output": {
"type": "string",
"description": "Output path for analysis graph"
}
}
},
"metrics": {
"type": "object",
"properties": {
"upload": {
"type": "boolean",
"description": "Whether to upload metrics to dashboard"
},
"dashboardUrl": {
"type": "string",
"format": "uri",
"description": "Dashboard API endpoint"
},
"projectId": {
"type": "string",
"description": "Project identifier in dashboard"
}
}
},
"ci": {
"type": "object",
"properties": {
"blocking": {
"type": "boolean",
"description": "Whether errors block the pipeline"
},
"skipPattern": {
"type": "string",
"description": "Pattern in commit message to skip validation"
},
"baselineBranch": {
"type": "string",
"description": "Branch to compare against for new violations"
}
}
},
"tokens": {
"type": "object",
"description": "Token configuration",
"properties": {
"source": {
"type": "string",
"description": "Path to token definitions"
},
"format": {
"enum": ["css", "scss", "json", "js"],
"description": "Token file format"
}
}
}
}
}

View File

@@ -7,44 +7,74 @@
"required": ["id", "version", "name", "category", "rules"],
"properties": {
"$schema": {
"type": "string"
"type": "string",
"description": "Reference to this schema"
},
"id": {
"type": "string",
"pattern": "^[a-z][a-z0-9-]*$"
"pattern": "^[a-z][a-z0-9-]*$",
"description": "Unique identifier for this rule set"
},
"version": {
"type": "string",
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$",
"description": "Semantic version of this rule set"
},
"name": {
"type": "string"
"type": "string",
"description": "Human-readable name"
},
"description": {
"type": "string"
"type": "string",
"description": "Detailed description of the rule set"
},
"category": {
"type": "string",
"enum": ["tokens", "components", "accessibility", "patterns", "naming"]
"enum": ["tokens", "components", "accessibility", "patterns", "naming"],
"description": "Category this rule set belongs to"
},
"severity": {
"type": "string",
"enum": ["error", "warning", "info"],
"default": "warning"
"default": "warning",
"description": "Default severity for rules in this set"
},
"rules": {
"type": "array",
"items": {
"$ref": "#/definitions/Rule"
}
},
"description": "Individual rules in this set"
},
"tokens": {
"type": "object",
"description": "Token requirements and definitions",
"properties": {
"required": { "type": "array", "items": { "type": "string" } },
"optional": { "type": "array", "items": { "type": "string" } },
"scale": { "type": "object", "additionalProperties": { "type": "string" } }
"required": {
"type": "array",
"items": { "type": "string" }
},
"optional": {
"type": "array",
"items": { "type": "string" }
},
"scale": {
"type": "object",
"additionalProperties": { "type": "string" }
}
}
},
"components": {
"type": "object",
"description": "Component requirements"
},
"compliance": {
"type": "object",
"description": "Compliance metadata"
},
"adoption": {
"type": "object",
"description": "Adoption threshold definitions"
}
},
"definitions": {
@@ -52,21 +82,61 @@
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" },
"name": { "type": "string" },
"description": { "type": "string" },
"severity": { "type": "string", "enum": ["error", "warning", "info"] },
"wcag": { "type": "string" },
"id": {
"type": "string",
"pattern": "^[a-z][a-z0-9-]*$",
"description": "Unique rule identifier"
},
"name": {
"type": "string",
"description": "Human-readable rule name"
},
"description": {
"type": "string",
"description": "What this rule checks for"
},
"severity": {
"type": "string",
"enum": ["error", "warning", "info"],
"description": "Rule severity (overrides set default)"
},
"wcag": {
"type": "string",
"description": "WCAG criterion reference if applicable"
},
"patterns": {
"type": "object",
"properties": {
"forbidden": { "type": "array", "items": { "type": "string" } },
"allowed": { "type": "array", "items": { "type": "string" } }
"forbidden": {
"type": "array",
"items": { "type": "string" },
"description": "Regex patterns that violate this rule"
},
"allowed": {
"type": "array",
"items": { "type": "string" },
"description": "Regex patterns that satisfy this rule"
}
}
},
"validation": { "type": "object" },
"exceptions": { "type": "array", "items": { "type": "string" } },
"guidelines": { "type": "array", "items": { "type": "string" } }
"validation": {
"type": "object",
"description": "Validation configuration"
},
"exceptions": {
"type": "array",
"items": { "type": "string" },
"description": "File patterns or values to exclude"
},
"guidelines": {
"type": "array",
"items": { "type": "string" },
"description": "Human-readable guidelines for this rule"
},
"components": {
"type": "object",
"description": "Component-specific rule configuration"
}
}
}
}

View File

@@ -1,23 +0,0 @@
{
"$schema": "https://dss.overbits.luz.uy/schemas/ds.config.schema.json",
"name": "{{PROJECT_NAME}}",
"version": "1.0.0",
"rules": {
"package": "@dss/rules",
"version": "^1.0.0"
},
"analysis": {
"include": ["src/**/*.{ts,tsx,js,jsx,css,scss}"],
"exclude": ["**/node_modules/**", "**/*.test.*", "**/*.spec.*"],
"output": ".dss/analysis_graph.json"
},
"metrics": {
"upload": true,
"dashboardUrl": "https://dss.overbits.luz.uy/api/metrics"
},
"ci": {
"blocking": true,
"skipPattern": "[dss-skip]",
"baselineBranch": "main"
}
}

View File

@@ -1 +0,0 @@
# This folder is created by DSS initialization

View File

@@ -1,122 +0,0 @@
name: DSS Design System Validation
on:
push:
branches: ['*']
pull_request:
branches: [main, develop]
env:
DSS_MODE: ci
DSS_DASHBOARD_URL: ${{ vars.DSS_DASHBOARD_URL || 'https://dss.overbits.luz.uy/api/metrics' }}
jobs:
dss-validate:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- 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] in commit message
id: skip-check
run: |
COMMIT_MSG=$(git log -1 --pretty=%B)
if echo "$COMMIT_MSG" | grep -q "\[dss-skip\]"; then
echo "skip=true" >> $GITHUB_OUTPUT
echo "::warning::DSS validation skipped via [dss-skip] flag"
else
echo "skip=false" >> $GITHUB_OUTPUT
fi
- name: Run DSS Rules Validation
if: steps.skip-check.outputs.skip != 'true'
id: validate
run: |
# Run validation and capture output
npx dss-rules --ci --json src/ > dss-report.json 2>&1 || true
# Check results
ERRORS=$(jq '.totalErrors' dss-report.json)
WARNINGS=$(jq '.totalWarnings' dss-report.json)
echo "errors=$ERRORS" >> $GITHUB_OUTPUT
echo "warnings=$WARNINGS" >> $GITHUB_OUTPUT
# Print summary
echo "## DSS Validation Results" >> $GITHUB_STEP_SUMMARY
echo "- Errors: $ERRORS" >> $GITHUB_STEP_SUMMARY
echo "- Warnings: $WARNINGS" >> $GITHUB_STEP_SUMMARY
if [ "$ERRORS" -gt 0 ]; then
echo "::error::DSS validation failed with $ERRORS errors"
exit 1
fi
- name: Check for version drift
if: steps.skip-check.outputs.skip != 'true'
run: |
CURRENT_VERSION=$(npm list @dss/rules --json 2>/dev/null | jq -r '.dependencies["@dss/rules"].version // "unknown"')
LATEST_VERSION=$(npm view @dss/rules version 2>/dev/null || echo "unknown")
if [ "$CURRENT_VERSION" != "$LATEST_VERSION" ] && [ "$LATEST_VERSION" != "unknown" ]; then
echo "::warning::@dss/rules version drift detected: using $CURRENT_VERSION, latest is $LATEST_VERSION"
fi
- name: Upload metrics to dashboard
if: steps.skip-check.outputs.skip != 'true' && always()
run: |
if [ -f dss-report.json ]; then
# Extract metrics for upload
jq '{
project: "${{ github.repository }}",
branch: "${{ github.ref_name }}",
commit: "${{ github.sha }}",
timestamp: now | todate,
metrics: {
totalFiles: .totalFiles,
passedFiles: .passedFiles,
failedFiles: .failedFiles,
totalErrors: .totalErrors,
totalWarnings: .totalWarnings,
rulesVersion: .rulesVersion
},
fileResults: [.fileResults[] | {
file: .file,
errors: (.errors | length),
warnings: (.warnings | length),
violations: [.errors[], .warnings[] | {
rule: .rule,
line: .line,
column: .column
}]
}]
}' dss-report.json > metrics-payload.json
# Upload to dashboard (non-blocking)
curl -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${{ secrets.DSS_API_TOKEN }}" \
-d @metrics-payload.json \
"$DSS_DASHBOARD_URL/upload" \
--fail-with-body || echo "::warning::Failed to upload metrics to dashboard"
fi
- name: Upload validation report artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: dss-validation-report
path: dss-report.json
retention-days: 30

View File

@@ -1,152 +0,0 @@
# DSS Design System Validation - GitHub Actions
# Generated by @dss/rules init
#
# This workflow validates design system compliance and uploads metrics
# to the DSS dashboard for portfolio-wide visibility.
#
# Required Secrets:
# DSS_DASHBOARD_URL: URL to DSS metrics API (e.g., https://dss.example.com)
# DSS_API_TOKEN: Authentication token for metrics upload
name: DSS Validate
on:
push:
branches: [main, master, develop]
pull_request:
branches: [main, master]
env:
NODE_VERSION: '20'
jobs:
validate:
name: Design System Validation
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for baseline comparison
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
# Check for break-glass [dss-skip] in commit message
- name: Check for [dss-skip]
id: skip-check
run: |
COMMIT_MSG=$(git log -1 --pretty=%B)
if echo "$COMMIT_MSG" | grep -q '\[dss-skip\]'; then
echo "skip=true" >> $GITHUB_OUTPUT
echo "::warning::DSS validation skipped via [dss-skip] commit message"
echo "::warning::Commit: $(git log -1 --pretty='%h %s')"
else
echo "skip=false" >> $GITHUB_OUTPUT
fi
# Check @dss/rules version drift
- name: Check rules version
if: steps.skip-check.outputs.skip != 'true'
run: |
INSTALLED=$(npm list @dss/rules --json 2>/dev/null | jq -r '.dependencies["@dss/rules"].version // "not-installed"')
LATEST=$(npm view @dss/rules version 2>/dev/null || echo "unknown")
echo "Installed @dss/rules: $INSTALLED"
echo "Latest @dss/rules: $LATEST"
if [ "$INSTALLED" != "$LATEST" ] && [ "$LATEST" != "unknown" ]; then
echo "::warning::@dss/rules is outdated ($INSTALLED vs $LATEST). Consider updating."
fi
# Run DSS validation
- name: Run DSS validation
if: steps.skip-check.outputs.skip != 'true'
id: validate
run: |
# Run validation with CI mode (strict, JSON output)
npm run dss:validate:ci || echo "validation_failed=true" >> $GITHUB_OUTPUT
# Extract summary for PR comment
if [ -f .dss/results.json ]; then
ERRORS=$(jq -r '.metrics.totalErrors // 0' .dss/results.json)
WARNINGS=$(jq -r '.metrics.totalWarnings // 0' .dss/results.json)
SCORE=$(jq -r '.metrics.adoptionScore // 0' .dss/results.json)
echo "errors=$ERRORS" >> $GITHUB_OUTPUT
echo "warnings=$WARNINGS" >> $GITHUB_OUTPUT
echo "score=$SCORE" >> $GITHUB_OUTPUT
fi
# Upload metrics to DSS dashboard
- name: Upload metrics to dashboard
if: steps.skip-check.outputs.skip != 'true' && always()
continue-on-error: true
run: |
if [ ! -f .dss/results.json ]; then
echo "No results file found, skipping upload"
exit 0
fi
# Add git metadata to results
jq --arg branch "${{ github.ref_name }}" \
--arg commit "${{ github.sha }}" \
--arg repo "${{ github.repository }}" \
'. + {branch: $branch, commit: $commit, project: $repo}' \
.dss/results.json > .dss/upload.json
curl -X POST "${DSS_DASHBOARD_URL}/api/metrics/upload" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${DSS_API_TOKEN}" \
-d @.dss/upload.json \
--fail --silent --show-error
env:
DSS_DASHBOARD_URL: ${{ secrets.DSS_DASHBOARD_URL }}
DSS_API_TOKEN: ${{ secrets.DSS_API_TOKEN }}
# Comment on PR with results
- name: Comment on PR
if: github.event_name == 'pull_request' && steps.skip-check.outputs.skip != 'true'
uses: actions/github-script@v7
with:
script: |
const errors = '${{ steps.validate.outputs.errors }}' || '0';
const warnings = '${{ steps.validate.outputs.warnings }}' || '0';
const score = '${{ steps.validate.outputs.score }}' || 'N/A';
const status = errors === '0' ? '✅' : '❌';
const body = `## ${status} DSS Validation Results
| Metric | Value |
|--------|-------|
| Adoption Score | ${score}% |
| Errors | ${errors} |
| Warnings | ${warnings} |
${errors !== '0' ? '⚠️ Please fix design system violations before merging.' : '🎉 All design system checks passed!'}
---
*Powered by @dss/rules*`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: body
});
# Fail if validation errors (authoritative enforcement)
- name: Check validation result
if: steps.skip-check.outputs.skip != 'true'
run: |
if [ "${{ steps.validate.outputs.validation_failed }}" = "true" ]; then
echo "::error::DSS validation failed with errors. Please fix violations."
exit 1
fi

View File

@@ -1,9 +0,0 @@
# DSS Design System (generated files - do not commit)
.dss/analysis_graph.json
.dss/cache/
.dss/metrics.json
.dss/baseline.json
# Keep config and schema
!.dss/ds.config.json
!.dss/.gitkeep

View File

@@ -1,126 +0,0 @@
# DSS Design System Validation - GitLab CI
# Generated by @dss/rules init
#
# This workflow validates design system compliance and uploads metrics
# to the DSS dashboard for portfolio-wide visibility.
#
# Required Variables:
# DSS_DASHBOARD_URL: URL to DSS metrics API (e.g., https://dss.example.com)
# DSS_API_TOKEN: Authentication token for metrics upload
stages:
- validate
variables:
NODE_VERSION: "20"
.node-cache:
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
- .npm/
dss-validate:
stage: validate
image: node:${NODE_VERSION}
extends: .node-cache
script:
# Install dependencies
- npm ci --cache .npm --prefer-offline
# Check for break-glass [dss-skip] in commit message
- |
COMMIT_MSG=$(git log -1 --pretty=%B)
if echo "$COMMIT_MSG" | grep -q '\[dss-skip\]'; then
echo "⚠️ DSS validation skipped via [dss-skip] commit message"
echo "Commit: $(git log -1 --pretty='%h %s')"
exit 0
fi
# Check @dss/rules version drift
- |
INSTALLED=$(npm list @dss/rules --json 2>/dev/null | jq -r '.dependencies["@dss/rules"].version // "not-installed"')
LATEST=$(npm view @dss/rules version 2>/dev/null || echo "unknown")
echo "Installed @dss/rules: $INSTALLED"
echo "Latest @dss/rules: $LATEST"
if [ "$INSTALLED" != "$LATEST" ] && [ "$LATEST" != "unknown" ]; then
echo "⚠️ @dss/rules is outdated ($INSTALLED vs $LATEST). Consider updating."
fi
# Run DSS validation
- npm run dss:validate:ci || VALIDATION_FAILED=true
# Upload metrics to dashboard
- |
if [ -f .dss/results.json ]; then
jq --arg branch "$CI_COMMIT_REF_NAME" \
--arg commit "$CI_COMMIT_SHA" \
--arg repo "$CI_PROJECT_PATH" \
'. + {branch: $branch, commit: $commit, project: $repo}' \
.dss/results.json > .dss/upload.json
curl -X POST "${DSS_DASHBOARD_URL}/api/metrics/upload" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${DSS_API_TOKEN}" \
-d @.dss/upload.json \
--fail --silent --show-error || echo "⚠️ Failed to upload metrics (non-blocking)"
fi
# Fail if validation errors
- |
if [ "$VALIDATION_FAILED" = "true" ]; then
echo "❌ DSS validation failed with errors. Please fix violations."
exit 1
fi
artifacts:
when: always
paths:
- .dss/results.json
expire_in: 1 week
reports:
codequality: .dss/results.json
rules:
- if: $CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "master" || $CI_COMMIT_BRANCH == "develop"
- if: $CI_MERGE_REQUEST_IID
# Optional: MR comment with results (requires GITLAB_TOKEN with API access)
dss-mr-comment:
stage: validate
image: curlimages/curl:latest
needs:
- job: dss-validate
artifacts: true
script:
- |
if [ ! -f .dss/results.json ]; then
echo "No results file, skipping MR comment"
exit 0
fi
ERRORS=$(jq -r '.metrics.totalErrors // 0' .dss/results.json)
WARNINGS=$(jq -r '.metrics.totalWarnings // 0' .dss/results.json)
SCORE=$(jq -r '.metrics.adoptionScore // 0' .dss/results.json)
if [ "$ERRORS" = "0" ]; then
STATUS="✅"
MESSAGE="🎉 All design system checks passed!"
else
STATUS="❌"
MESSAGE="⚠️ Please fix design system violations before merging."
fi
BODY="## $STATUS DSS Validation Results\n\n| Metric | Value |\n|--------|-------|\n| Adoption Score | ${SCORE}% |\n| Errors | $ERRORS |\n| Warnings | $WARNINGS |\n\n$MESSAGE\n\n---\n*Powered by @dss/rules*"
curl --request POST \
--header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \
--header "Content-Type: application/json" \
--data "{\"body\": \"$BODY\"}" \
"${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/merge_requests/${CI_MERGE_REQUEST_IID}/notes" \
|| echo "⚠️ Failed to post MR comment (non-blocking)"
rules:
- if: $CI_MERGE_REQUEST_IID && $GITLAB_TOKEN
allow_failure: true