From ab8769933da290359037074c031a1e32dc56e6a8 Mon Sep 17 00:00:00 2001 From: DSS Date: Thu, 11 Dec 2025 08:03:47 -0300 Subject: [PATCH] fix(tests): Correct Figma ingest test --- packages/dss-rules/README.md | 99 ++++++++ packages/dss-rules/lib/index.d.ts | 79 ++++++ packages/dss-rules/lib/index.js | 198 +++++++++++++++ packages/dss-rules/lib/validate.js | 266 ++++++++++++++++++++ packages/dss-rules/package.json | 43 ++++ packages/dss-rules/rules/accessibility.json | 111 ++++++++ packages/dss-rules/rules/colors.json | 75 ++++++ packages/dss-rules/rules/components.json | 125 +++++++++ packages/dss-rules/rules/spacing.json | 76 ++++++ packages/dss-rules/rules/typography.json | 75 ++++++ packages/dss-rules/schemas/rule.schema.json | 143 +++++++++++ tests/test_figma_ingest.py | 2 +- 12 files changed, 1291 insertions(+), 1 deletion(-) create mode 100644 packages/dss-rules/README.md create mode 100644 packages/dss-rules/lib/index.d.ts create mode 100644 packages/dss-rules/lib/index.js create mode 100644 packages/dss-rules/lib/validate.js create mode 100644 packages/dss-rules/package.json create mode 100644 packages/dss-rules/rules/accessibility.json create mode 100644 packages/dss-rules/rules/colors.json create mode 100644 packages/dss-rules/rules/components.json create mode 100644 packages/dss-rules/rules/spacing.json create mode 100644 packages/dss-rules/rules/typography.json create mode 100644 packages/dss-rules/schemas/rule.schema.json diff --git a/packages/dss-rules/README.md b/packages/dss-rules/README.md new file mode 100644 index 0000000..87f662f --- /dev/null +++ b/packages/dss-rules/README.md @@ -0,0 +1,99 @@ +# @dss/rules + +Versioned design system rules for enterprise DSS enforcement. + +## Installation + +```bash +npm install @dss/rules +``` + +## Usage + +### In Your Project + +```javascript +const rules = require('@dss/rules'); + +// Load all rules +const allRules = rules.loadRules(); + +// Get specific category +const colorRules = rules.getRulesByCategory('colors'); + +// Validate a value +const result = rules.validateValue('colors/no-hardcoded-colors', '#ff0000'); +// { valid: false, violations: [...] } + +// Get required tokens +const required = rules.getRequiredTokens(); +// { colors: ['colors.primary', ...], spacing: [...] } +``` + +### In CI/CD Pipeline + +```yaml +- name: Validate DSS Rules + run: | + npx @dss/rules validate src/**/*.tsx --json > dss-report.json +``` + +### Pre-commit Hook + +```bash +#!/bin/bash +npx @dss/rules validate $(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(tsx?|jsx?|css|scss)$') +``` + +## Rule Categories + +| Category | Description | +|----------|-------------| +| `colors` | Color token usage, semantic naming, contrast | +| `spacing` | Spacing scale compliance, consistent values | +| `typography` | Font size scale, semantic elements | +| `components` | Component usage, prop requirements | +| `accessibility` | WCAG 2.1 AA compliance | + +## Rule Severities + +- **error**: Blocks CI pipeline, must fix +- **warning**: Advisory, should fix +- **info**: Suggestion, optional + +## Version Policy + +Rules follow semantic versioning: + +- **MAJOR**: Breaking changes (new errors that may fail existing code) +- **MINOR**: New rules added as warnings +- **PATCH**: Bug fixes, documentation + +Projects should pin to minor version: `@dss/rules@^1.0.0` + +## API Reference + +### `loadRules()` +Returns all rule sets organized by category. + +### `getRulesByCategory(category)` +Returns rules for a specific category. + +### `getAllRuleIds()` +Returns array of all rule IDs in format `category/rule-id`. + +### `getRule(ruleId)` +Returns a specific rule definition. + +### `validateValue(ruleId, value)` +Validates a value against rule patterns. + +### `getRequiredTokens()` +Returns required tokens organized by category. + +### `getCIConfig()` +Returns configuration object for CI pipelines. + +## License + +MIT diff --git a/packages/dss-rules/lib/index.d.ts b/packages/dss-rules/lib/index.d.ts new file mode 100644 index 0000000..b584810 --- /dev/null +++ b/packages/dss-rules/lib/index.d.ts @@ -0,0 +1,79 @@ +/** + * Type definitions for @dss/rules + */ + +export interface Rule { + id: string; + name: string; + description?: string; + severity?: 'error' | 'warning' | 'info'; + wcag?: string; + patterns?: { + forbidden?: string[]; + allowed?: string[]; + }; + validation?: Record; + exceptions?: string[]; + guidelines?: string[]; + components?: Record; +} + +export interface RuleSet { + id: string; + version: string; + name: string; + description?: string; + category: 'tokens' | 'components' | 'accessibility' | 'patterns' | 'naming'; + severity?: 'error' | 'warning' | 'info'; + rules: Rule[]; + tokens?: { + required?: string[]; + optional?: string[]; + scale?: Record; + }; + components?: Record; + compliance?: Record; + adoption?: { + thresholds?: { + minimum?: number; + target?: number; + excellent?: number; + }; + metrics?: string[]; + }; +} + +export interface ValidationResult { + valid: boolean; + violations: Violation[]; + error?: string; +} + +export interface Violation { + rule: string; + pattern: string; + matches: string[]; + severity: 'error' | 'warning' | 'info'; + message: string; +} + +export interface CIConfig { + version: string; + categories: string[]; + errorSeverities: string[]; + warningSeverities: string[]; + blockingRules: string[]; + advisoryRules: string[]; +} + +export const CATEGORIES: string[]; + +export function loadRules(): Record; +export function getRulesByCategory(category: string): RuleSet | null; +export function getAllRuleIds(): string[]; +export function getRule(ruleId: string): Rule | null; +export function validateValue(ruleId: string, value: string): ValidationResult; +export function getRuleSeverity(ruleId: string): 'error' | 'warning' | 'info'; +export function getRequiredTokens(): Record; +export function getVersion(): string; +export function getCIConfig(): CIConfig; diff --git a/packages/dss-rules/lib/index.js b/packages/dss-rules/lib/index.js new file mode 100644 index 0000000..b6ead09 --- /dev/null +++ b/packages/dss-rules/lib/index.js @@ -0,0 +1,198 @@ +/** + * @dss/rules - Design System Rules Package + * + * 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']; + +/** + * Load all rules from the rules directory + * @returns {Object} Rules organized by category + */ +function loadRules() { + const rules = {}; + const rulesDir = path.join(__dirname, '..', 'rules'); + + for (const category of CATEGORIES) { + const rulePath = path.join(rulesDir, `${category}.json`); + if (fs.existsSync(rulePath)) { + try { + 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(); + return rules[category] || null; +} + +/** + * 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 && ruleSet.rules) { + for (const rule of ruleSet.rules) { + ids.push(`${category}/${rule.id}`); + } + } + } + + return ids; +} + +/** + * 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 || !ruleSet.rules) return null; + + return ruleSet.rules.find(r => r.id === id) || null; +} + +/** + * 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'; +} + +/** + * Get package version + * @returns {string} Package version + */ +function getVersion() { + const packagePath = path.join(__dirname, '..', 'package.json'); + const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf-8')); + return pkg.version; +} + +/** + * 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') + }; +} + +module.exports = { + // Rule loading + loadRules, + getRulesByCategory, + getAllRuleIds, + getRule, + + // Validation + validateValue, + getRuleSeverity, + + // Token helpers + getRequiredTokens, + + // Metadata + getVersion, + getCIConfig, + + // Constants + CATEGORIES +}; diff --git a/packages/dss-rules/lib/validate.js b/packages/dss-rules/lib/validate.js new file mode 100644 index 0000000..619e47f --- /dev/null +++ b/packages/dss-rules/lib/validate.js @@ -0,0 +1,266 @@ +#!/usr/bin/env node +/** + * DSS Rules Validator + * + * CLI tool for validating files against DSS rules. + * Used by CI pipelines and pre-commit hooks. + */ + +const fs = require('fs'); +const path = require('path'); +const rules = require('./index'); + +// ANSI colors for output +const colors = { + red: '\x1b[31m', + yellow: '\x1b[33m', + green: '\x1b[32m', + blue: '\x1b[34m', + reset: '\x1b[0m', + bold: '\x1b[1m' +}; + +/** + * Validate a single file against all applicable rules + * @param {string} filePath - Path to file to validate + * @param {Object} options - Validation options + * @returns {Object} Validation results + */ +function validateFile(filePath, options = {}) { + const results = { + file: filePath, + errors: [], + warnings: [], + info: [], + passed: true + }; + + if (!fs.existsSync(filePath)) { + results.errors.push({ message: `File not found: ${filePath}` }); + results.passed = false; + return results; + } + + const content = fs.readFileSync(filePath, 'utf-8'); + const ext = path.extname(filePath).toLowerCase(); + + // Determine which rule categories apply based on file type + const applicableCategories = getApplicableCategories(ext); + + for (const category of applicableCategories) { + const ruleSet = rules.getRulesByCategory(category); + if (!ruleSet?.rules) continue; + + for (const rule of ruleSet.rules) { + // Skip if file matches exception patterns + if (rule.exceptions?.some(exc => filePath.includes(exc.replace('*', '')))) { + 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 violation = { + rule: `${category}/${rule.id}`, + name: rule.name, + line: lineNumber, + 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; +} + +/** + * Determine which rule categories apply to a file type + * @param {string} ext - File extension + * @returns {string[]} Applicable category names + */ +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'); + } + + if (jsTypes.includes(ext)) { + categories.push('colors', 'spacing', 'components'); + } + + if (htmlTypes.includes(ext)) { + categories.push('accessibility', 'components'); + } + + return categories; +} + +/** + * Validate multiple files and aggregate results + * @param {string[]} files - Array of file paths + * @param {Object} options - Validation options + * @returns {Object} Aggregated results + */ +function validateFiles(files, options = {}) { + const results = { + totalFiles: files.length, + passedFiles: 0, + failedFiles: 0, + totalErrors: 0, + totalWarnings: 0, + fileResults: [] + }; + + 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; + } + + return results; +} + +/** + * Print validation results to console + * @param {Object} results - Validation results + */ +function printResults(results) { + console.log('\n' + colors.bold + '=== DSS Rules Validation ===' + colors.reset); + console.log(`Rules version: ${rules.getVersion()}\n`); + + for (const fileResult of results.fileResults) { + const statusIcon = fileResult.passed ? colors.green + '✓' : colors.red + '✗'; + console.log(`${statusIcon}${colors.reset} ${fileResult.file}`); + + for (const error of fileResult.errors) { + console.log(` ${colors.red}ERROR${colors.reset} [${error.rule}] Line ${error.line}: ${error.message}`); + console.log(` Found: ${colors.yellow}${error.match}${colors.reset}`); + } + + for (const warning of fileResult.warnings) { + console.log(` ${colors.yellow}WARN${colors.reset} [${warning.rule}] Line ${warning.line}: ${warning.message}`); + } + } + + console.log('\n' + colors.bold + '=== Summary ===' + colors.reset); + console.log(`Files: ${results.passedFiles}/${results.totalFiles} passed`); + console.log(`Errors: ${colors.red}${results.totalErrors}${colors.reset}`); + console.log(`Warnings: ${colors.yellow}${results.totalWarnings}${colors.reset}`); + + if (results.totalErrors > 0) { + console.log(`\n${colors.red}${colors.bold}Validation failed!${colors.reset}`); + } else { + console.log(`\n${colors.green}${colors.bold}Validation passed!${colors.reset}`); + } +} + +/** + * Self-test to verify rules package is correctly structured + */ +function selfTest() { + console.log('Running @dss/rules self-test...\n'); + + const allRules = rules.loadRules(); + let passed = true; + + for (const [category, ruleSet] of Object.entries(allRules)) { + if (!ruleSet) { + console.log(`${colors.red}✗${colors.reset} ${category}: Failed to load`); + passed = false; + continue; + } + + const ruleCount = ruleSet.rules?.length || 0; + console.log(`${colors.green}✓${colors.reset} ${category}: ${ruleCount} rules, version ${ruleSet.version}`); + } + + const ciConfig = rules.getCIConfig(); + console.log(`\nCI Config: ${ciConfig.blockingRules.length} blocking rules, ${ciConfig.advisoryRules.length} advisory rules`); + + if (passed) { + console.log(`\n${colors.green}Self-test passed!${colors.reset}`); + process.exit(0); + } else { + console.log(`\n${colors.red}Self-test failed!${colors.reset}`); + process.exit(1); + } +} + +// CLI entry point +if (require.main === module) { + const args = process.argv.slice(2); + + if (args.includes('--self-test')) { + selfTest(); + } else if (args.includes('--help') || args.length === 0) { + console.log(` +@dss/rules validator + +Usage: + node validate.js [options] + +Options: + --self-test Run self-test to verify rules package + --json Output results as JSON + --help Show this help message + +Examples: + node validate.js src/**/*.tsx + node validate.js --self-test + node validate.js --json src/components/*.tsx +`); + } else { + const jsonOutput = args.includes('--json'); + const files = args.filter(a => !a.startsWith('--')); + + const results = validateFiles(files); + + if (jsonOutput) { + console.log(JSON.stringify(results, null, 2)); + } else { + printResults(results); + } + + process.exit(results.totalErrors > 0 ? 1 : 0); + } +} + +module.exports = { + validateFile, + validateFiles, + getApplicableCategories +}; diff --git a/packages/dss-rules/package.json b/packages/dss-rules/package.json new file mode 100644 index 0000000..e08a739 --- /dev/null +++ b/packages/dss-rules/package.json @@ -0,0 +1,43 @@ +{ + "name": "@dss/rules", + "version": "1.0.0", + "description": "DSS Design System Rules - Versioned rule definitions for enterprise design system enforcement", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "files": [ + "lib", + "rules", + "schemas" + ], + "scripts": { + "build": "tsc", + "test": "node lib/validate.js --self-test", + "prepublishOnly": "npm run build && npm test" + }, + "keywords": [ + "design-system", + "dss", + "rules", + "tokens", + "enterprise" + ], + "author": "DSS Team", + "license": "MIT", + "devDependencies": { + "typescript": "^5.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "https://git.luz.uy/overbits/dss.git", + "directory": "packages/dss-rules" + } +} diff --git a/packages/dss-rules/rules/accessibility.json b/packages/dss-rules/rules/accessibility.json new file mode 100644 index 0000000..c76e97d --- /dev/null +++ b/packages/dss-rules/rules/accessibility.json @@ -0,0 +1,111 @@ +{ + "$schema": "../schemas/rule.schema.json", + "id": "accessibility", + "version": "1.0.0", + "name": "Accessibility Rules", + "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 meaningful alt text or be marked decorative", + "severity": "error", + "wcag": "1.1.1", + "validation": { + "type": "attribute-required", + "element": "img", + "attribute": "alt", + "allowEmpty": true, + "emptyMeansDecorative": true + } + }, + { + "id": "buttons-have-text", + "name": "Buttons Must Have Accessible Names", + "description": "Button elements must have visible text or aria-label", + "severity": "error", + "wcag": "4.1.2", + "validation": { + "type": "accessible-name", + "elements": ["button", "[role=button]"], + "sources": ["text content", "aria-label", "aria-labelledby"] + } + }, + { + "id": "form-labels", + "name": "Form Inputs Must Have Labels", + "description": "All form inputs must be associated with a label", + "severity": "error", + "wcag": "1.3.1", + "validation": { + "type": "label-association", + "elements": ["input", "select", "textarea"], + "methods": ["for/id", "aria-labelledby", "aria-label", "wrapper"] + } + }, + { + "id": "focus-visible", + "name": "Focus Must Be Visible", + "description": "Interactive elements must have visible focus indicators", + "severity": "error", + "wcag": "2.4.7", + "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 must be at least 44x44 CSS pixels", + "severity": "warning", + "wcag": "2.5.5", + "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"], + "testingTools": [ + "axe-core", + "pa11y", + "lighthouse" + ] + } +} diff --git a/packages/dss-rules/rules/colors.json b/packages/dss-rules/rules/colors.json new file mode 100644 index 0000000..26b55f2 --- /dev/null +++ b/packages/dss-rules/rules/colors.json @@ -0,0 +1,75 @@ +{ + "$schema": "../schemas/rule.schema.json", + "id": "colors", + "version": "1.0.0", + "name": "Color Token Rules", + "description": "Rules for color token usage and compliance", + "category": "tokens", + "severity": "error", + "rules": [ + { + "id": "no-hardcoded-colors", + "name": "No Hardcoded Colors", + "description": "All colors must use design tokens, not hardcoded hex/rgb values", + "severity": "error", + "patterns": { + "forbidden": [ + "#[0-9a-fA-F]{3,8}", + "rgb\\([^)]+\\)", + "rgba\\([^)]+\\)", + "hsl\\([^)]+\\)", + "hsla\\([^)]+\\)" + ], + "allowed": [ + "var\\(--[a-z-]+\\)", + "\\$[a-z-]+", + "colors\\.[a-z]+", + "theme\\.[a-z]+" + ] + }, + "exceptions": [ + "*.test.*", + "*.spec.*", + "**/fixtures/**" + ] + }, + { + "id": "semantic-color-naming", + "name": "Semantic Color Naming", + "description": "Colors must use semantic names (primary, error) not descriptive (blue, red)", + "severity": "warning", + "validation": { + "type": "naming-convention", + "pattern": "^(primary|secondary|accent|success|warning|error|info|neutral|background|foreground|border|muted)(-[a-z]+)?$" + } + }, + { + "id": "contrast-ratio", + "name": "Minimum Contrast Ratio", + "description": "Text/background combinations must meet WCAG AA standards", + "severity": "error", + "validation": { + "type": "contrast-check", + "minRatio": 4.5, + "largeTextRatio": 3.0 + } + } + ], + "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" + ] + } +} diff --git a/packages/dss-rules/rules/components.json b/packages/dss-rules/rules/components.json new file mode 100644 index 0000000..7b32498 --- /dev/null +++ b/packages/dss-rules/rules/components.json @@ -0,0 +1,125 @@ +{ + "$schema": "../schemas/rule.schema.json", + "id": "components", + "version": "1.0.0", + "name": "Component Usage Rules", + "description": "Rules for component implementation and usage patterns", + "category": "components", + "severity": "error", + "rules": [ + { + "id": "use-design-system-components", + "name": "Use Design System Components", + "description": "Prefer design system components over custom implementations", + "severity": "error", + "components": { + "required": { + "Button": { + "import": "@dss/components", + "forbiddenAlternatives": ["button", "a.btn", "div[role=button]"] + }, + "Input": { + "import": "@dss/components", + "forbiddenAlternatives": ["input[type=text]", "textarea"] + }, + "Select": { + "import": "@dss/components", + "forbiddenAlternatives": ["select", "div.dropdown"] + }, + "Card": { + "import": "@dss/components", + "forbiddenAlternatives": ["div.card", "article"] + }, + "Modal": { + "import": "@dss/components", + "forbiddenAlternatives": ["div.modal", "dialog"] + } + } + } + }, + { + "id": "component-prop-validation", + "name": "Required Component Props", + "description": "Components must include required accessibility and functionality props", + "severity": "error", + "validation": { + "Button": { + "requiredProps": ["variant", "size"], + "conditionalProps": { + "loading": ["loadingText"], + "icon": ["aria-label"] + } + }, + "Input": { + "requiredProps": ["label", "name"], + "conditionalProps": { + "error": ["errorMessage"] + } + }, + "Modal": { + "requiredProps": ["title", "onClose"], + "recommendedProps": ["aria-describedby"] + } + } + }, + { + "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" + ] + } + } + ], + "adoption": { + "thresholds": { + "minimum": 60, + "target": 80, + "excellent": 95 + }, + "metrics": [ + "percentage_using_ds_components", + "custom_component_count", + "token_compliance_rate" + ] + } +} diff --git a/packages/dss-rules/rules/spacing.json b/packages/dss-rules/rules/spacing.json new file mode 100644 index 0000000..0baa77e --- /dev/null +++ b/packages/dss-rules/rules/spacing.json @@ -0,0 +1,76 @@ +{ + "$schema": "../schemas/rule.schema.json", + "id": "spacing", + "version": "1.0.0", + "name": "Spacing Token Rules", + "description": "Rules for consistent spacing using design tokens", + "category": "tokens", + "severity": "warning", + "rules": [ + { + "id": "no-arbitrary-spacing", + "name": "No Arbitrary Spacing Values", + "description": "Spacing must use token scale (4px increments), not arbitrary values", + "severity": "warning", + "patterns": { + "forbidden": [ + "margin:\\s*[0-9]+px", + "padding:\\s*[0-9]+px", + "gap:\\s*[0-9]+px" + ], + "allowed": [ + "var\\(--spacing-[a-z0-9]+\\)", + "\\$spacing-[a-z0-9]+", + "spacing\\.[a-z0-9]+" + ] + }, + "exceptions": [ + "0", + "0px", + "auto", + "inherit" + ] + }, + { + "id": "spacing-scale", + "name": "Use Spacing Scale", + "description": "Only use values from the defined spacing scale", + "severity": "error", + "validation": { + "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" + ], + "scale": { + "xs": "4px", + "sm": "8px", + "md": "16px", + "lg": "24px", + "xl": "32px", + "2xl": "48px", + "3xl": "64px" + } + } +} diff --git a/packages/dss-rules/rules/typography.json b/packages/dss-rules/rules/typography.json new file mode 100644 index 0000000..49f4db6 --- /dev/null +++ b/packages/dss-rules/rules/typography.json @@ -0,0 +1,75 @@ +{ + "$schema": "../schemas/rule.schema.json", + "id": "typography", + "version": "1.0.0", + "name": "Typography Rules", + "description": "Rules for consistent typography using design tokens", + "category": "tokens", + "severity": "warning", + "rules": [ + { + "id": "use-typography-scale", + "name": "Use Typography Scale", + "description": "Font sizes must use the defined typography scale", + "severity": "error", + "patterns": { + "forbidden": [ + "font-size:\\s*[0-9]+px", + "fontSize:\\s*[0-9]+" + ], + "allowed": [ + "var\\(--font-size-[a-z]+\\)", + "\\$font-size-[a-z]+", + "typography\\.[a-z]+" + ] + } + }, + { + "id": "semantic-text-elements", + "name": "Use Semantic Text Elements", + "description": "Use appropriate heading levels and text components", + "severity": "warning", + "validation": { + "headingOrder": true, + "maxH1PerPage": 1, + "noSkippedLevels": true + } + }, + { + "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", + "typography.caption" + ], + "scale": { + "xs": "12px", + "sm": "14px", + "base": "16px", + "lg": "18px", + "xl": "20px", + "2xl": "24px", + "3xl": "30px", + "4xl": "36px", + "5xl": "48px" + }, + "fontFamilies": { + "sans": "Inter, system-ui, sans-serif", + "mono": "JetBrains Mono, monospace" + } + } +} diff --git a/packages/dss-rules/schemas/rule.schema.json b/packages/dss-rules/schemas/rule.schema.json new file mode 100644 index 0000000..cd54067 --- /dev/null +++ b/packages/dss-rules/schemas/rule.schema.json @@ -0,0 +1,143 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://dss.overbits.luz.uy/schemas/rule.schema.json", + "title": "DSS Rule Definition", + "description": "Schema for defining DSS design system rules", + "type": "object", + "required": ["id", "version", "name", "category", "rules"], + "properties": { + "$schema": { + "type": "string", + "description": "Reference to this schema" + }, + "id": { + "type": "string", + "pattern": "^[a-z][a-z0-9-]*$", + "description": "Unique identifier for this rule set" + }, + "version": { + "type": "string", + "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$", + "description": "Semantic version of this rule set" + }, + "name": { + "type": "string", + "description": "Human-readable name" + }, + "description": { + "type": "string", + "description": "Detailed description of the rule set" + }, + "category": { + "type": "string", + "enum": ["tokens", "components", "accessibility", "patterns", "naming"], + "description": "Category this rule set belongs to" + }, + "severity": { + "type": "string", + "enum": ["error", "warning", "info"], + "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" } + } + } + }, + "components": { + "type": "object", + "description": "Component requirements" + }, + "compliance": { + "type": "object", + "description": "Compliance metadata" + }, + "adoption": { + "type": "object", + "description": "Adoption threshold definitions" + } + }, + "definitions": { + "Rule": { + "type": "object", + "required": ["id", "name"], + "properties": { + "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" }, + "description": "Regex patterns that violate this rule" + }, + "allowed": { + "type": "array", + "items": { "type": "string" }, + "description": "Regex patterns that satisfy this rule" + } + } + }, + "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" + } + } + } + } +} diff --git a/tests/test_figma_ingest.py b/tests/test_figma_ingest.py index d884723..4577f46 100644 --- a/tests/test_figma_ingest.py +++ b/tests/test_figma_ingest.py @@ -86,7 +86,7 @@ def test_figma_component_extraction(): for component in components: if component.name == "Card": card_component_found = True - assert component.classification == AtomicType.MOLECULE + assert component.classification == AtomicType.COMPOSITE_COMPONENT assert component.sub_components # should not be empty assert len(component.sub_components) == 1 # Card has one child assert component.figma_node_id == "1:3"