Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 09:02:33 +08:00
commit 0c40192593
82 changed files with 18699 additions and 0 deletions

View File

@@ -0,0 +1,325 @@
# Fluxwing Validator
Deterministic validation for uxscii components and screens.
## Two Modes of Operation
### Interactive Mode (User Invocation)
When users say "Validate my components", the skill:
1. Presents menu (everything/components/screens/custom)
2. Runs appropriate validators
3. Shows minimal summary with optional details
**Example:**
```
✓ 12/14 components valid
✗ 2/14 components failed
Show error details? (yes/no)
```
### Direct Mode (Skill-to-Skill)
Other skills call validator scripts directly:
```bash
node {SKILL_ROOT}/../fluxwing-validator/validate-component.js \
./fluxwing/components/button.uxm \
{SKILL_ROOT}/schemas/uxm-component.schema.json
```
Output: Always verbose (full errors, warnings, stats)
Exit codes: 0 = valid, 1 = errors, 2 = invalid args
---
## Overview
This skill contains validation scripts that can be called from Claude Code skills to validate `.uxm` component files against JSON Schema with additional uxscii-specific checks.
**Key features:**
-**Deterministic** - Locked dependencies via `package-lock.json`
-**Zero setup** - `node_modules` bundled in repository
-**Fast** - ~80ms execution time with ajv
-**Comprehensive** - Schema validation + uxscii-specific rules
-**Structured output** - JSON and human-readable formats
## Installation
This skill is bundled with fluxwing-skills plugin.
**Plugin install:**
```bash
/plugin install fluxwing-skills
```
**Script install (development):**
```bash
./scripts/install.sh
```
No npm install required - dependencies bundled with skill.
## Files
```
skills/fluxwing-validator/
├── SKILL.md # Interactive validation workflow
├── validate-component.js # Component validator
├── validate-screen.js # Screen validator
├── validate-batch.js # Batch validator
├── test-validator.js # Component test suite
├── test-screen-validator.js # Screen test suite
├── package.json # Dependencies (ajv 8.12.0)
├── package-lock.json # Locked versions
├── node_modules/ # Bundled dependencies (~7.4 MB)
└── README.md # This file
```
## Usage
### From Command Line
```bash
# From project root
node skills/fluxwing-validator/validate-component.js \
./fluxwing/components/component.uxm \
skills/fluxwing-component-creator/schemas/uxm-component.schema.json
# JSON output (for programmatic use)
node skills/fluxwing-validator/validate-component.js \
./fluxwing/components/component.uxm \
skills/fluxwing-component-creator/schemas/uxm-component.schema.json \
--json
```
### From Other Skills (using SKILL_ROOT)
```bash
# In SKILL.md - Direct mode validation
node {SKILL_ROOT}/../fluxwing-validator/validate-component.js \
./fluxwing/components/button.uxm \
{SKILL_ROOT}/schemas/uxm-component.schema.json
```
### Exit Codes
- `0` - Component is valid
- `1` - Validation errors found
- `2` - Missing dependencies or invalid arguments
## Validation Checks
### 1. JSON Schema Validation
Validates against `uxm-component.schema.json` using ajv (Draft 7):
- Required fields (id, type, version, metadata, props, ascii)
- Field types and formats
- Enum constraints (type, category, fidelity)
- Pattern matching (id format, version format)
- Array and object structures
### 2. ASCII Template File
- Checks that corresponding `.md` file exists
- Validates path: `component.uxm``component.md`
### 3. Template Variables
- Extracts `{{variables}}` from `.md` template
- Compares against `ascii.variables` in `.uxm`
- Warns if variables in `.md` are not defined in `.uxm`
- Supports both formats:
- Array of strings: `["text", "value"]`
- Array of objects: `[{"name": "text", "type": "string"}]`
### 4. Accessibility
For interactive components (with `behavior.interactions`):
- Warns if missing `behavior.accessibility.role`
- Warns if `focusable` is not set
### 5. ASCII Dimensions
- Warns if `ascii.width` > 120 (terminal width limit)
- Warns if `ascii.height` > 50 (single viewport limit)
### 6. State Properties
- Warns if states exist but have no properties defined
## Output Format
### Human-Readable
```
✓ Valid: primary-button
Type: button
Version: 1.0.0
States: 4
Props: 5
Warnings: 1
1. Interactive component should have ARIA role
Location: behavior → accessibility → role
```
### JSON
```json
{
"valid": true,
"errors": [],
"warnings": [
{
"path": ["behavior", "accessibility", "role"],
"message": "Interactive component should have ARIA role",
"type": "accessibility"
}
],
"stats": {
"id": "primary-button",
"type": "button",
"version": "1.0.0",
"states": 4,
"props": 5,
"interactive": true,
"hasAccessibility": true
}
}
```
## Dependencies
Only 6 packages, 2.7 MB total:
- `ajv@8.12.0` - JSON Schema validator (industry standard)
- `ajv-formats@2.1.1` - Format validation (date-time, etc.)
- Supporting packages (fast-deep-equal, fast-uri, json-schema-traverse, require-from-string)
**All dependencies are bundled in `node_modules/` and checked into git.**
## Node.js Version
- **Minimum:** Node.js 14.0.0
- **Recommended:** Node.js 18+ or 20+ (LTS)
- **Tested:** Node.js 18, 20, 22
## Testing
```bash
# Run test suite against bundled templates
npm test
# Or directly
node test-validator.js
```
Tests validate components from:
- `skills/fluxwing-component-creator/templates/*.uxm`
## Development
### Updating Validation Logic
1. Edit `validate-component.js`
2. Test changes: `npm test`
3. Commit (no build step needed)
### Updating Dependencies
```bash
npm update
npm audit fix
npm test
git add package-lock.json node_modules/
git commit -m "Update dependencies"
```
### Adding New Validators
Create new scripts following the same pattern:
- `validate-screen.js` - For screen validation
- `validate-variable.js` - For variable substitution
## Why Node.js?
Compared to Python or native binaries:
**Advantages:**
- ✅ No build step (unlike Go/Rust/Deno compiled)
- ✅ Readable source code (easier debugging)
- ✅ Small diffs (text, not binaries)
- ✅ Likely installed (most developers have Node.js)
- ✅ Mature ecosystem (ajv is industry standard)
- ✅ Fast enough (~80ms including startup)
**Tradeoffs:**
- ⚠️ Requires Node.js runtime (not zero-dependency)
- ⚠️ Slower than native (~80ms vs ~5ms for Go)
## Integration Examples
### Component Creator Skill
```bash
# After creating component (from SKILL.md)
node {SKILL_ROOT}/../fluxwing-validator/validate-component.js \
./fluxwing/components/${COMPONENT_ID}.uxm \
{SKILL_ROOT}/schemas/uxm-component.schema.json
if [ $? -eq 0 ]; then
echo "✓ Component validated successfully"
else
echo "✗ Validation failed - see errors above"
exit 1
fi
```
### Pre-commit Hook
```bash
# .claude/hooks/pre-commit.sh
for uxm_file in fluxwing/components/*.uxm; do
node skills/fluxwing-validator/validate-component.js \
"$uxm_file" \
skills/fluxwing-component-creator/schemas/uxm-component.schema.json \
|| exit 1
done
```
## Troubleshooting
### "ajv libraries not found"
The validator couldn't find the ajv dependency. This should never happen since `node_modules/` is bundled, but if it does:
```bash
cd skills/fluxwing-validator
npm install
```
### "Component file not found"
Check that the path to the `.uxm` file is correct. Paths are relative to the current working directory.
### "Invalid JSON"
The `.uxm` file has syntax errors. Use a JSON validator to find the issue:
```bash
cat component.uxm | jq .
```
## Future Enhancements
- [x] Screen validation (`validate-screen.js`)
- [x] Batch validation (`validate-batch.js`)
- [ ] Variable substitution validation
- [ ] Watch mode for development
- [ ] Integration with CI/CD pipelines
- [ ] VSCode extension integration
## License
MIT

View File

@@ -0,0 +1,354 @@
---
name: fluxwing-validator
description: Validate uxscii components and screens against schema with interactive menu or direct script calls
version: 1.0.0
author: Fluxwing Team
allowed-tools: Read, Bash, AskUserQuestion, TodoWrite
---
# Fluxwing Validator
Validate uxscii components and screens against JSON Schema with interactive workflows.
## Overview
This skill provides two modes of operation:
1. **Interactive Mode**: User invocation with menu and minimal output
2. **Direct Mode**: Script calls from other skills with verbose output
## When to Use This Skill
**User says:**
- "Validate my components"
- "Check if everything is valid"
- "Run validation on my screens"
- "Validate the project"
**Other skills:** Call validator scripts directly (see Technical Reference below)
## Interactive Validation Workflow
### Step 1: Present Validation Options
Use AskUserQuestion to present menu:
```
What would you like to validate?
```
**Options:**
1. **Everything in this project** - Validates all components and screens
2. **Just components** - Validates `./fluxwing/components/*.uxm`
3. **Just screens** - Validates `./fluxwing/screens/*.uxm`
4. **Let me specify a file or pattern** - Custom path/glob pattern
### Step 2: Check What Exists
Before running validation, check if directories exist:
```bash
# Check for components
test -d ./fluxwing/components
# Check for screens
test -d ./fluxwing/screens
```
**If neither exists:**
- Inform user: "No components or screens found. Create some first!"
- Exit cleanly
**If option 4 (custom) selected:**
- Ask user for the pattern/file path
- Validate it's not empty
- Proceed with user-provided pattern
### Step 3: Run Validation
Based on user selection:
**Option 1: Everything**
```bash
# Validate components
node {SKILL_ROOT}/validate-batch.js \
"./fluxwing/components/*.uxm" \
"{SKILL_ROOT}/../fluxwing-component-creator/schemas/uxm-component.schema.json" \
--json
# Validate screens
node {SKILL_ROOT}/validate-batch.js \
"./fluxwing/screens/*.uxm" \
"{SKILL_ROOT}/../fluxwing-component-creator/schemas/uxm-component.schema.json" \
--screens \
--json
```
**Option 2: Just components**
```bash
node {SKILL_ROOT}/validate-batch.js \
"./fluxwing/components/*.uxm" \
"{SKILL_ROOT}/../fluxwing-component-creator/schemas/uxm-component.schema.json" \
--json
```
**Option 3: Just screens**
```bash
node {SKILL_ROOT}/validate-batch.js \
"./fluxwing/screens/*.uxm" \
"{SKILL_ROOT}/../fluxwing-component-creator/schemas/uxm-component.schema.json" \
--screens \
--json
```
**Option 4: Custom pattern**
```bash
# Use user-provided pattern
node {SKILL_ROOT}/validate-batch.js \
"${userPattern}" \
"{SKILL_ROOT}/../fluxwing-component-creator/schemas/uxm-component.schema.json" \
--json
```
### Step 4: Parse Results and Show Minimal Summary
Parse JSON output from validator to extract:
- `total`: Total files validated
- `passed`: Number of valid files
- `failed`: Number of failed files
- `warnings`: Total warning count
**Display minimal summary:**
```
✓ 12/14 components valid
✗ 2/14 components failed
⚠ 3 warnings total
```
**If all passed:**
```
✓ All 14 components valid
⚠ 3 warnings
```
**If everything failed:**
```
✗ All 14 components failed
```
**If nothing to validate:**
```
No files found matching pattern
```
### Step 5: Ask About Details
Use AskUserQuestion to ask:
```
Show error details?
```
**Options:**
1. **Yes** - Display full validation output
2. **No** - Clean exit
### Step 6: Display Details (if requested)
If user selects "Yes", show full validation output including:
**Failed files section:**
```
Failed Files:
✗ broken-button (./fluxwing/components/broken-button.uxm)
Errors: 2
1. must have required property 'fidelity'
2. ASCII template file not found
✗ old-card (./fluxwing/components/old-card.uxm)
Errors: 1
1. invalid version format
```
**Passed with warnings section:**
```
Passed with Warnings:
✓ login-screen (2 warnings)
✓ dashboard (1 warning)
```
**Fully passed section (optional, only if not too many):**
```
Fully Passed:
✓ primary-button
✓ secondary-button
✓ email-input
...
```
## Edge Cases
### No fluxwing directory exists
```
No components or screens found. Create some first!
```
### Empty directories
```
✓ 0/0 components valid
```
### Invalid glob pattern (option 4)
```
No files found matching pattern: ${pattern}
```
### Validation script fails to execute
```
Error: Cannot execute validator. Node.js required.
```
## Technical Reference (For Other Skills)
### Direct Script Calls
Other skills (component-creator, screen-scaffolder) call validator scripts directly:
**Validate single component:**
```bash
node {SKILL_ROOT}/../fluxwing-validator/validate-component.js \
./fluxwing/components/button.uxm \
{SKILL_ROOT}/schemas/uxm-component.schema.json
```
**Validate single screen:**
```bash
node {SKILL_ROOT}/../fluxwing-validator/validate-screen.js \
./fluxwing/screens/login-screen.uxm \
{SKILL_ROOT}/schemas/uxm-component.schema.json
```
**Batch validate:**
```bash
node {SKILL_ROOT}/../fluxwing-validator/validate-batch.js \
"./fluxwing/components/*.uxm" \
{SKILL_ROOT}/schemas/uxm-component.schema.json
```
**Output modes:**
- Default: Human-readable (verbose, full errors/warnings)
- `--json`: Machine-readable JSON
**Exit codes:**
- `0`: All files valid
- `1`: One or more files invalid
- `2`: Missing dependencies or invalid arguments
### Validator Scripts
**Available scripts:**
- `validate-component.js` - Validate single component file
- `validate-screen.js` - Validate single screen file (with screen-specific checks)
- `validate-batch.js` - Validate multiple files with glob patterns
- `test-validator.js` - Test component templates
- `test-screen-validator.js` - Test screen templates
### npm Scripts (for testing)
```bash
cd {SKILL_ROOT}
# Run tests
npm test # Test component templates
npm run test:screens # Test screen templates
# Batch validation
npm run validate:components # Validate all components
npm run validate:screens # Validate all screens
npm run validate:all # Validate everything
```
## Example Interactions
### Example 1: Validate Everything
**User:** "Validate my components"
**Skill:**
```
What would you like to validate?
1. Everything in this project
2. Just components
3. Just screens
4. Let me specify a file or pattern
```
**User selects:** Option 1
**Skill runs validation and shows:**
```
✓ 12/14 components valid
✗ 2/14 components failed
⚠ 3 warnings total
✓ 2/2 screens valid
⚠ 1 warning
Show error details?
```
**User:** "yes"
**Skill shows full error details for failed files**
### Example 2: Validate Specific Pattern
**User:** "Validate my components"
**Skill:** (presents menu)
**User selects:** Option 4 (custom pattern)
**Skill:** "What file or pattern would you like to validate?"
**User:** "./fluxwing/components/*-button.uxm"
**Skill validates and shows:**
```
✓ 3/3 files valid
Show error details?
```
## Implementation Notes
**Parse JSON output:**
```javascript
const result = JSON.parse(bashOutput);
const total = result.total;
const passed = result.passed;
const failed = result.failed;
const warnings = result.warnings;
```
**Summary formatting:**
- Show passed/failed ratio for quick scan
- Highlight failures prominently
- Warnings shown but not intrusive
- Clean, minimal output by default
**Error detail formatting:**
- Group by status (failed, warnings, passed)
- Show file path and error count
- Display first 2-3 errors per file
- Indicate if more errors exist
---
**Skill Status:** Ready for use
**Version:** 1.0.0

View File

@@ -0,0 +1,23 @@
{
"name": "fluxwing-validators",
"version": "1.0.0",
"description": "Deterministic Node.js validators for uxscii components and screens",
"main": "validate-component.js",
"scripts": {
"test": "node test-validator.js",
"test:screens": "node test-screen-validator.js",
"validate:components": "node validate-batch.js '../fluxwing/components/*.uxm' '../skills/fluxwing-component-creator/schemas/uxm-component.schema.json'",
"validate:screens": "node validate-batch.js '../fluxwing/screens/*.uxm' '../skills/fluxwing-component-creator/schemas/uxm-component.schema.json' --screens",
"validate:all": "npm run validate:components && npm run validate:screens"
},
"engines": {
"node": ">=14.0.0"
},
"dependencies": {
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
"glob": "^10.3.10"
},
"author": "Fluxwing",
"license": "MIT"
}

View File

@@ -0,0 +1,89 @@
#!/usr/bin/env node
/**
* Test suite for screen validator
* Tests bundled screen templates against the schema
*/
const fs = require('fs');
const path = require('path');
const { validateScreen } = require('./validate-screen.js');
// ANSI color codes
const GREEN = '\x1b[32m';
const RED = '\x1b[31m';
const YELLOW = '\x1b[33m';
const RESET = '\x1b[0m';
console.log('Testing Fluxwing Screen Validator\n');
console.log('==================================================\n');
// Test configuration
const schemaPath = path.join(__dirname, '..', 'fluxwing-component-creator', 'schemas', 'uxm-component.schema.json');
const templatesDir = path.join(__dirname, '..', 'fluxwing-screen-scaffolder', 'templates');
// Find all .uxm files in templates directory
const screenFiles = fs.readdirSync(templatesDir)
.filter(file => file.endsWith('.uxm'))
.map(file => path.join(templatesDir, file));
if (screenFiles.length === 0) {
console.log(`${YELLOW}⚠ No screen templates found in ${templatesDir}${RESET}`);
process.exit(0);
}
// Test results
const results = {
passed: 0,
failed: 0,
warnings: 0
};
// Run tests
for (const screenFile of screenFiles) {
const screenName = path.basename(screenFile, '.uxm');
console.log(`Testing: ${screenName}`);
try {
const result = validateScreen(screenFile, schemaPath);
if (result.valid) {
console.log(` ${GREEN}✓ PASS${RESET} - ${screenName}`);
results.passed++;
if (result.warnings.length > 0) {
console.log(` (${result.warnings.length} warnings)`);
results.warnings += result.warnings.length;
}
} else {
console.log(` ${RED}✗ FAIL${RESET} - ${screenName}`);
console.log(` Errors: ${result.errors.length}`);
results.failed++;
// Show first few errors
result.errors.slice(0, 3).forEach((error, i) => {
console.log(` ${i + 1}. ${error.message}`);
});
if (result.errors.length > 3) {
console.log(` ... and ${result.errors.length - 3} more errors`);
}
}
} catch (error) {
console.log(` ${RED}✗ ERROR${RESET} - ${screenName}`);
console.log(` ${error.message}`);
results.failed++;
}
console.log('');
}
// Print summary
console.log('==================================================');
console.log(`Results: ${GREEN}${results.passed} passed${RESET}, ${results.failed > 0 ? RED : ''}${results.failed} failed${RESET}`);
if (results.warnings > 0) {
console.log(`Warnings: ${YELLOW}${results.warnings} total${RESET}`);
}
// Exit with appropriate code
process.exit(results.failed > 0 ? 1 : 0);

View File

@@ -0,0 +1,68 @@
#!/usr/bin/env node
/**
* Test script for validate-component.js
* Tests against bundled template components
*/
const { validateComponent } = require('./validate-component.js');
const path = require('path');
const fs = require('fs');
// Test components from templates
const testComponents = [
'../fluxwing-component-creator/templates/primary-button.uxm',
'../fluxwing-component-creator/templates/email-input.uxm',
'../fluxwing-component-creator/templates/card.uxm',
'../fluxwing-component-creator/templates/badge.uxm',
'../fluxwing-component-creator/templates/alert.uxm'
];
const schemaPath = path.join(__dirname, '../fluxwing-component-creator/schemas/uxm-component.schema.json');
console.log('Testing Fluxwing Component Validator\n');
console.log('='.repeat(50));
console.log('');
let passed = 0;
let failed = 0;
for (const componentRelPath of testComponents) {
const componentPath = path.join(__dirname, componentRelPath);
const componentName = path.basename(componentPath);
if (!fs.existsSync(componentPath)) {
console.log(`⊘ SKIP: ${componentName} (file not found)`);
continue;
}
console.log(`Testing: ${componentName}`);
try {
const result = validateComponent(componentPath, schemaPath);
if (result.valid) {
passed++;
console.log(` ✓ PASS - ${result.stats.id}`);
if (result.warnings.length > 0) {
console.log(` (${result.warnings.length} warnings)`);
}
} else {
failed++;
console.log(` ✗ FAIL - ${result.errors.length} errors`);
result.errors.slice(0, 2).forEach(err => {
console.log(`${err.message}`);
});
}
} catch (error) {
failed++;
console.log(` ✗ ERROR: ${error.message}`);
}
console.log('');
}
console.log('='.repeat(50));
console.log(`Results: ${passed} passed, ${failed} failed`);
console.log('');
process.exit(failed > 0 ? 1 : 0);

View File

@@ -0,0 +1,208 @@
#!/usr/bin/env node
/**
* Batch validation for multiple .uxm files
* Validates multiple component or screen files in one command
*
* Usage:
* node validate-batch.js "./fluxwing/components/FILE.uxm" <schema.json>
* node validate-batch.js "./fluxwing/screens/FILE.uxm" <schema.json> --json
* node validate-batch.js "./fluxwing/ALL/FILE.uxm" <schema.json> --screens
*
* Options:
* --json Output results as JSON
* --screens Use screen validator instead of component validator
*
* Exit codes:
* 0 - All files valid
* 1 - One or more files invalid
* 2 - Missing dependencies or invalid arguments
*/
const fs = require('fs');
const path = require('path');
// Check for glob dependency
let glob;
try {
glob = require('glob');
} catch (error) {
console.error('Error: glob library not found');
console.error('Install with: npm install glob');
process.exit(2);
}
// Import validators
const { validateComponent } = require('./validate-component.js');
const { validateScreen } = require('./validate-screen.js');
// ANSI color codes
const GREEN = '\x1b[32m';
const RED = '\x1b[31m';
const YELLOW = '\x1b[33m';
const BLUE = '\x1b[34m';
const RESET = '\x1b[0m';
/**
* Validate multiple files matching a glob pattern
* @param {string} pattern - Glob pattern for files to validate
* @param {string} schemaPath - Path to JSON schema file
* @param {Object} options - Validation options
* @returns {Object} Batch validation results
*/
function validateBatch(pattern, schemaPath, options = {}) {
const useScreenValidator = options.screens || false;
const validator = useScreenValidator ? validateScreen : validateComponent;
// Find all matching files
const files = glob.sync(pattern);
if (files.length === 0) {
return {
total: 0,
passed: 0,
failed: 0,
warnings: 0,
files: [],
message: `No files found matching pattern: ${pattern}`
};
}
const results = {
total: files.length,
passed: 0,
failed: 0,
warnings: 0,
files: []
};
// Validate each file
for (const file of files) {
const result = validator(file, schemaPath);
const fileResult = {
file: file,
id: result.stats?.id || path.basename(file, '.uxm'),
valid: result.valid,
errors: result.errors.length,
warnings: result.warnings.length,
errorDetails: result.errors,
warningDetails: result.warnings
};
results.files.push(fileResult);
if (result.valid) {
results.passed++;
} else {
results.failed++;
}
results.warnings += result.warnings.length;
}
return results;
}
/**
* Print human-readable batch results
* @param {Object} results - Batch validation results
*/
function printHumanReadable(results) {
if (results.total === 0) {
console.log(`${YELLOW}${results.message}${RESET}`);
return;
}
console.log(`${BLUE}Batch Validation Results${RESET}\n`);
console.log(`Total files: ${results.total}`);
console.log(`${GREEN}Passed: ${results.passed}${RESET}`);
if (results.failed > 0) {
console.log(`${RED}Failed: ${results.failed}${RESET}`);
}
if (results.warnings > 0) {
console.log(`${YELLOW}Total warnings: ${results.warnings}${RESET}`);
}
console.log('');
// Show failed files
const failedFiles = results.files.filter(f => !f.valid);
if (failedFiles.length > 0) {
console.log(`${RED}Failed Files:${RESET}\n`);
failedFiles.forEach(file => {
console.log(` ${RED}${RESET} ${file.id} (${file.file})`);
console.log(` Errors: ${file.errors}`);
// Show first 2 errors
file.errorDetails.slice(0, 2).forEach((error, i) => {
console.log(` ${i + 1}. ${error.message}`);
});
if (file.errors > 2) {
console.log(` ... and ${file.errors - 2} more errors`);
}
console.log('');
});
}
// Show passed files with warnings
const passedWithWarnings = results.files.filter(f => f.valid && f.warnings > 0);
if (passedWithWarnings.length > 0) {
console.log(`${YELLOW}Passed with Warnings:${RESET}\n`);
passedWithWarnings.forEach(file => {
console.log(` ${GREEN}${RESET} ${file.id} ${YELLOW}(${file.warnings} warnings)${RESET}`);
});
console.log('');
}
// Show fully passed files (compact)
const fullPassed = results.files.filter(f => f.valid && f.warnings === 0);
if (fullPassed.length > 0) {
console.log(`${GREEN}Fully Passed:${RESET}\n`);
fullPassed.forEach(file => {
console.log(` ${GREEN}${RESET} ${file.id}`);
});
}
}
/**
* Print JSON results
* @param {Object} results - Batch validation results
*/
function printJSON(results) {
console.log(JSON.stringify(results, null, 2));
}
// CLI interface
if (require.main === module) {
const args = process.argv.slice(2);
if (args.length < 2) {
console.error('Usage: node validate-batch.js <pattern> <schema.json> [--json] [--screens]');
console.error('');
console.error('Examples:');
console.error(' node validate-batch.js "./fluxwing/components/*.uxm" schema.json');
console.error(' node validate-batch.js "./fluxwing/**/*.uxm" schema.json --json');
console.error(' node validate-batch.js "./fluxwing/screens/*.uxm" schema.json --screens');
process.exit(2);
}
const pattern = args[0];
const schemaPath = args[1];
const jsonOutput = args.includes('--json');
const useScreens = args.includes('--screens');
const results = validateBatch(pattern, schemaPath, { screens: useScreens });
if (jsonOutput) {
printJSON(results);
} else {
printHumanReadable(results);
}
process.exit(results.failed > 0 ? 1 : 0);
}
module.exports = { validateBatch };

View File

@@ -0,0 +1,359 @@
#!/usr/bin/env node
/**
* Fast, deterministic component validation using JSON Schema (ajv).
* Validates .uxm component files against schema with uxscii-specific checks.
*
* Usage:
* node validate-component.js <component.uxm> <schema.json>
* node validate-component.js <component.uxm> <schema.json> --json
*
* Exit codes:
* 0 - Valid component
* 1 - Validation errors found
* 2 - Missing dependencies or invalid arguments
*/
const fs = require('fs');
const path = require('path');
// Check for ajv dependency
let Ajv, addFormats;
try {
Ajv = require('ajv');
addFormats = require('ajv-formats');
} catch (error) {
console.error('Error: ajv libraries not found');
console.error('Install with: npm install ajv ajv-formats');
process.exit(2);
}
/**
* Validate a .uxm component file against the schema
* @param {string} uxmFilePath - Path to .uxm file
* @param {string} schemaPath - Path to JSON schema file
* @returns {Object} Validation result object
*/
function validateComponent(uxmFilePath, schemaPath) {
const result = {
valid: true,
errors: [],
warnings: [],
stats: {}
};
// Load component file
let component;
try {
const uxmContent = fs.readFileSync(uxmFilePath, 'utf8');
component = JSON.parse(uxmContent);
} catch (error) {
if (error.code === 'ENOENT') {
result.valid = false;
result.errors.push({
path: [],
message: `Component file not found: ${uxmFilePath}`,
type: 'file_not_found'
});
return result;
}
if (error instanceof SyntaxError) {
result.valid = false;
result.errors.push({
path: [],
message: `Invalid JSON: ${error.message}`,
type: 'json_error'
});
return result;
}
throw error;
}
// Load schema file
let schema;
try {
const schemaContent = fs.readFileSync(schemaPath, 'utf8');
schema = JSON.parse(schemaContent);
} catch (error) {
if (error.code === 'ENOENT') {
result.valid = false;
result.errors.push({
path: [],
message: `Schema file not found: ${schemaPath}`,
type: 'file_not_found'
});
return result;
}
throw error;
}
// Validate against JSON schema
const ajv = new Ajv({ allErrors: true, strict: false });
addFormats(ajv);
const validate = ajv.compile(schema);
const valid = validate(component);
if (!valid) {
for (const error of validate.errors) {
result.errors.push({
path: error.instancePath.split('/').filter(p => p),
message: error.message,
type: 'schema_violation',
details: error.params
});
}
result.valid = false;
}
// uxscii-specific validation checks
performUxsciiChecks(component, uxmFilePath, result);
// Collect stats
result.stats = {
id: component.id,
type: component.type,
version: component.version,
states: component.behavior?.states?.length || 0,
props: Object.keys(component.props || {}).length,
interactive: component.behavior?.interactions?.length > 0,
hasAccessibility: !!component.behavior?.accessibility
};
return result;
}
/**
* Perform uxscii-specific validation checks
* @param {Object} component - Parsed component object
* @param {string} uxmFilePath - Path to .uxm file
* @param {Object} result - Result object to populate
*/
function performUxsciiChecks(component, uxmFilePath, result) {
// Check 1: ASCII template file exists
const mdFilePath = uxmFilePath.replace('.uxm', '.md');
let mdContent = null;
try {
mdContent = fs.readFileSync(mdFilePath, 'utf8');
} catch (error) {
if (error.code === 'ENOENT') {
result.valid = false;
result.errors.push({
path: ['ascii', 'templateFile'],
message: `ASCII template file not found: ${mdFilePath}`,
type: 'missing_file'
});
return; // Can't do variable checks without .md file
}
}
// Check 2: Template variables match
if (mdContent) {
checkTemplateVariables(component, mdContent, result);
}
// Check 3: Accessibility requirements for interactive components
checkAccessibility(component, result);
// Check 4: ASCII dimensions
checkAsciiDimensions(component, result);
// Check 5: States have properties
checkStates(component, result);
}
/**
* Check that template variables in .md match those defined in .uxm
* @param {Object} component - Component object
* @param {string} mdContent - Content of .md file
* @param {Object} result - Result object
*/
function checkTemplateVariables(component, mdContent, result) {
// Extract {{variables}} from markdown template
const varPattern = /\{\{(\w+)\}\}/g;
const mdVars = new Set();
let match;
while ((match = varPattern.exec(mdContent)) !== null) {
mdVars.add(match[1]);
}
// Get variables from .uxm (handle both array of strings and array of objects)
const asciiVars = component.ascii?.variables || [];
const uxmVars = new Set();
if (asciiVars.length > 0) {
if (typeof asciiVars[0] === 'object') {
// Array of objects format: [{"name": "text", "type": "string"}]
asciiVars.forEach(v => {
if (v && typeof v === 'object' && v.name) {
uxmVars.add(v.name);
}
});
} else {
// Array of strings format: ["text", "value"]
asciiVars.forEach(v => uxmVars.add(v));
}
}
// Check for variables in .md but not defined in .uxm
const missing = [...mdVars].filter(v => !uxmVars.has(v));
if (missing.length > 0) {
result.warnings.push({
path: ['ascii', 'variables'],
message: `Variables in .md but not defined in .uxm: ${missing.sort().join(', ')}`,
type: 'variable_mismatch'
});
}
}
/**
* Check accessibility requirements
* @param {Object} component - Component object
* @param {Object} result - Result object
*/
function checkAccessibility(component, result) {
const hasInteractions = component.behavior?.interactions?.length > 0;
if (hasInteractions) {
const accessibility = component.behavior?.accessibility || {};
if (!accessibility.role) {
result.warnings.push({
path: ['behavior', 'accessibility', 'role'],
message: 'Interactive component should have ARIA role',
type: 'accessibility'
});
}
if (!accessibility.focusable) {
result.warnings.push({
path: ['behavior', 'accessibility', 'focusable'],
message: 'Interactive component should be focusable',
type: 'accessibility'
});
}
}
}
/**
* Check ASCII dimensions are within recommended limits
* @param {Object} component - Component object
* @param {Object} result - Result object
*/
function checkAsciiDimensions(component, result) {
const ascii = component.ascii || {};
const width = ascii.width || 0;
const height = ascii.height || 0;
if (width > 120) {
result.warnings.push({
path: ['ascii', 'width'],
message: `Width ${width} exceeds recommended max of 120`,
type: 'dimensions'
});
}
if (height > 50) {
result.warnings.push({
path: ['ascii', 'height'],
message: `Height ${height} exceeds recommended max of 50`,
type: 'dimensions'
});
}
}
/**
* Check that states have properties defined
* @param {Object} component - Component object
* @param {Object} result - Result object
*/
function checkStates(component, result) {
const states = component.behavior?.states || [];
for (const state of states) {
if (!state.properties || Object.keys(state.properties).length === 0) {
result.warnings.push({
path: ['behavior', 'states', state.name || 'unknown'],
message: `State '${state.name || 'unknown'}' has no properties defined`,
type: 'incomplete_state'
});
}
}
}
/**
* Print results in human-readable format
* @param {Object} result - Validation result
*/
function printHumanReadable(result) {
if (result.valid) {
console.log(`✓ Valid: ${result.stats.id}`);
console.log(` Type: ${result.stats.type}`);
console.log(` Version: ${result.stats.version}`);
console.log(` States: ${result.stats.states}`);
console.log(` Props: ${result.stats.props}`);
if (result.warnings.length > 0) {
console.log(`\n Warnings: ${result.warnings.length}`);
result.warnings.forEach((warning, i) => {
const pathStr = warning.path.join(' → ') || 'root';
console.log(` ${i + 1}. ${warning.message}`);
console.log(` Location: ${pathStr}`);
});
}
} else {
console.log(`✗ Validation Failed`);
console.log('');
const maxErrors = 5;
result.errors.slice(0, maxErrors).forEach((error, i) => {
const pathStr = error.path.join(' → ') || 'root';
console.log(` Error ${i + 1}: ${error.message}`);
console.log(` Location: ${pathStr}`);
console.log('');
});
if (result.errors.length > maxErrors) {
console.log(` ... and ${result.errors.length - maxErrors} more errors`);
}
}
}
/**
* Main entry point
*/
function main() {
const args = process.argv.slice(2);
if (args.length < 2) {
console.log('Usage: validate-component.js <component.uxm> <schema.json> [--json]');
console.log('');
console.log('Validates a uxscii component against the JSON schema.');
console.log('Returns JSON with validation results and exits 0 if valid, 1 if invalid.');
console.log('');
console.log('Options:');
console.log(' --json Output results as JSON instead of human-readable format');
process.exit(2);
}
const uxmFile = args[0];
const schemaFile = args[1];
const jsonOutput = args.includes('--json');
const result = validateComponent(uxmFile, schemaFile);
if (jsonOutput) {
console.log(JSON.stringify(result, null, 2));
} else {
printHumanReadable(result);
}
process.exit(result.valid ? 0 : 1);
}
// Run if called directly
if (require.main === module) {
main();
}
module.exports = { validateComponent };

View File

@@ -0,0 +1,222 @@
#!/usr/bin/env node
/**
* Screen validation using component validator + screen-specific checks.
* Validates .uxm screen files with additional checks for rendered examples and composed components.
*
* Usage:
* node validate-screen.js <screen.uxm> <schema.json>
* node validate-screen.js <screen.uxm> <schema.json> --json
*
* Exit codes:
* 0 - Valid screen
* 1 - Validation errors found
* 2 - Missing dependencies or invalid arguments
*/
const fs = require('fs');
const path = require('path');
// Import component validator
const { validateComponent } = require('./validate-component.js');
/**
* Validate a .uxm screen file with screen-specific checks
* @param {string} uxmFilePath - Path to .uxm file
* @param {string} schemaPath - Path to JSON schema file
* @returns {Object} Validation result object
*/
function validateScreen(uxmFilePath, schemaPath) {
// 1. Run standard component validation
const result = validateComponent(uxmFilePath, schemaPath);
// If component validation failed completely, return early
if (!fs.existsSync(uxmFilePath)) {
return result;
}
// Load screen data for screen-specific checks
let screen;
try {
const uxmContent = fs.readFileSync(uxmFilePath, 'utf8');
screen = JSON.parse(uxmContent);
} catch (error) {
// Already handled by validateComponent
return result;
}
// 2. Screen-specific checks
checkRenderedFile(uxmFilePath, result);
checkComposedComponents(screen, uxmFilePath, result);
return result;
}
/**
* Check if .rendered.md file exists (recommended for screens)
* @param {string} uxmFilePath - Path to .uxm file
* @param {Object} result - Result object to populate
*/
function checkRenderedFile(uxmFilePath, result) {
const renderedFilePath = uxmFilePath.replace('.uxm', '.rendered.md');
if (!fs.existsSync(renderedFilePath)) {
result.warnings.push({
path: ['screen'],
message: `Rendered example file recommended for screens: ${renderedFilePath}`,
type: 'missing_rendered',
severity: 'info'
});
}
}
/**
* Check if components referenced in screen exist
* @param {Object} screen - Parsed screen object
* @param {string} uxmFilePath - Path to .uxm file
* @param {Object} result - Result object to populate
*/
function checkComposedComponents(screen, uxmFilePath, result) {
// Extract component references from screen
const composedComponents = extractComponentReferences(screen);
if (composedComponents.length === 0) {
return; // No composed components to check
}
// Determine base path for component lookups
const screenDir = path.dirname(uxmFilePath);
const projectRoot = findProjectRoot(screenDir);
const componentsDir = path.join(projectRoot, 'fluxwing', 'components');
for (const componentId of composedComponents) {
const componentPath = path.join(componentsDir, `${componentId}.uxm`);
if (!fs.existsSync(componentPath)) {
result.warnings.push({
path: ['composed'],
message: `Referenced component not found: ${componentId} (expected at ${componentPath})`,
type: 'missing_component',
severity: 'warning'
});
}
}
}
/**
* Extract component IDs referenced in screen
* @param {Object} screen - Parsed screen object
* @returns {Array<string>} Array of component IDs
*/
function extractComponentReferences(screen) {
const componentIds = new Set();
// Check if screen extends another component
if (screen.extends) {
componentIds.add(screen.extends);
}
// Check slots for component references
if (screen.slots) {
for (const slot of Object.values(screen.slots)) {
if (slot.component) {
componentIds.add(slot.component);
}
if (Array.isArray(slot.components)) {
slot.components.forEach(c => componentIds.add(c));
}
}
}
// Check ASCII template content for component references (basic pattern matching)
// Pattern: {{component:component-id}}
const mdFilePath = screen.ascii?.templateFile;
if (mdFilePath) {
try {
const screenDir = path.dirname(screen.id);
const mdContent = fs.readFileSync(mdFilePath, 'utf8');
const componentPattern = /\{\{component:([a-z0-9-]+)\}\}/g;
let match;
while ((match = componentPattern.exec(mdContent)) !== null) {
componentIds.add(match[1]);
}
} catch (error) {
// Template file not found - already reported by component validator
}
}
return Array.from(componentIds);
}
/**
* Find project root directory (containing fluxwing/ directory)
* @param {string} startDir - Starting directory
* @returns {string} Project root path
*/
function findProjectRoot(startDir) {
let currentDir = startDir;
while (currentDir !== path.dirname(currentDir)) {
const fluxwingDir = path.join(currentDir, 'fluxwing');
if (fs.existsSync(fluxwingDir)) {
return currentDir;
}
currentDir = path.dirname(currentDir);
}
// Fallback to current working directory
return process.cwd();
}
// CLI interface
if (require.main === module) {
const args = process.argv.slice(2);
if (args.length < 2) {
console.error('Usage: node validate-screen.js <screen.uxm> <schema.json> [--json]');
process.exit(2);
}
const uxmFilePath = args[0];
const schemaPath = args[1];
const jsonOutput = args.includes('--json');
const result = validateScreen(uxmFilePath, schemaPath);
if (jsonOutput) {
console.log(JSON.stringify(result, null, 2));
} else {
if (result.valid) {
console.log(`✓ Valid: ${result.stats.id || 'screen'}`);
console.log(` Type: ${result.stats.type || 'unknown'}`);
console.log(` Version: ${result.stats.version || 'unknown'}`);
console.log(` States: ${result.stats.states || 0}`);
console.log(` Props: ${result.stats.props || 0}`);
if (result.warnings.length > 0) {
console.log(`\n⚠ Warnings:`);
result.warnings.forEach((warning, i) => {
console.log(`\n Warning ${i + 1}: ${warning.message}`);
if (warning.path.length > 0) {
console.log(` Location: ${warning.path.join(' → ')}`);
}
});
}
} else {
console.error('✗ Validation Failed\n');
result.errors.forEach((error, i) => {
console.error(` Error ${i + 1}: ${error.message}`);
if (error.path.length > 0) {
console.error(` Location: ${error.path.join(' → ')}`);
}
});
if (result.errors.length > 8) {
console.error(`\n ... and ${result.errors.length - 8} more errors`);
}
}
}
process.exit(result.valid ? 0 : 1);
}
module.exports = { validateScreen };