mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
* feat: add three viewing modes for app specification Introduces View, Edit, and Source modes for the spec page: - View: Clean read-only display with cards, badges, and accordions - Edit: Structured form-based editor for all spec fields - Source: Raw XML editor for advanced users Also adds @automaker/spec-parser shared package for XML parsing between server and client. * fix: address PR review feedback - Replace array index keys with stable UUIDs in array-field-editor, features-section, and roadmap-section components - Replace regex-based XML parsing with fast-xml-parser for robustness - Simplify renderContent logic in spec-view by removing dead code paths * fix: convert git+ssh URLs to https in package-lock.json * fix: address PR review feedback for spec visualiser - Remove unused RefreshCw import from spec-view.tsx - Add explicit parsedSpec check in renderContent for robustness - Hide save button in view mode since it's read-only - Remove GripVertical drag handles since drag-and-drop is not implemented - Rename Map imports to MapIcon to avoid shadowing global Map - Escape tagName in xml-utils.ts RegExp functions for safety - Add aria-label attributes for accessibility on mode tabs * fix: address additional PR review feedback - Fix Textarea controlled/uncontrolled warning with default value - Preserve IDs in useEffect sync to avoid unnecessary remounts - Consolidate lucide-react imports - Add JSDoc note about tag attributes limitation in xml-utils.ts - Remove redundant disabled prop from SpecModeTabs
144 lines
5.5 KiB
TypeScript
144 lines
5.5 KiB
TypeScript
/**
|
|
* Validation utilities for SpecOutput objects.
|
|
*/
|
|
|
|
import type { SpecOutput } from '@automaker/types';
|
|
|
|
/**
|
|
* Validation result containing errors if any.
|
|
*/
|
|
export interface ValidationResult {
|
|
valid: boolean;
|
|
errors: string[];
|
|
}
|
|
|
|
/**
|
|
* Validate a SpecOutput object for required fields and data integrity.
|
|
*
|
|
* @param spec - The SpecOutput object to validate
|
|
* @returns ValidationResult with errors if validation fails
|
|
*/
|
|
export function validateSpec(spec: SpecOutput | null | undefined): ValidationResult {
|
|
const errors: string[] = [];
|
|
|
|
if (!spec) {
|
|
return { valid: false, errors: ['Spec is null or undefined'] };
|
|
}
|
|
|
|
// Required string fields
|
|
if (!spec.project_name || typeof spec.project_name !== 'string') {
|
|
errors.push('project_name is required and must be a string');
|
|
} else if (spec.project_name.trim().length === 0) {
|
|
errors.push('project_name cannot be empty');
|
|
}
|
|
|
|
if (!spec.overview || typeof spec.overview !== 'string') {
|
|
errors.push('overview is required and must be a string');
|
|
} else if (spec.overview.trim().length === 0) {
|
|
errors.push('overview cannot be empty');
|
|
}
|
|
|
|
// Required array fields
|
|
if (!Array.isArray(spec.technology_stack)) {
|
|
errors.push('technology_stack is required and must be an array');
|
|
} else if (spec.technology_stack.length === 0) {
|
|
errors.push('technology_stack must have at least one item');
|
|
} else if (spec.technology_stack.some((t) => typeof t !== 'string' || t.trim() === '')) {
|
|
errors.push('technology_stack items must be non-empty strings');
|
|
}
|
|
|
|
if (!Array.isArray(spec.core_capabilities)) {
|
|
errors.push('core_capabilities is required and must be an array');
|
|
} else if (spec.core_capabilities.length === 0) {
|
|
errors.push('core_capabilities must have at least one item');
|
|
} else if (spec.core_capabilities.some((c) => typeof c !== 'string' || c.trim() === '')) {
|
|
errors.push('core_capabilities items must be non-empty strings');
|
|
}
|
|
|
|
// Implemented features
|
|
if (!Array.isArray(spec.implemented_features)) {
|
|
errors.push('implemented_features is required and must be an array');
|
|
} else {
|
|
spec.implemented_features.forEach((f, i) => {
|
|
if (!f.name || typeof f.name !== 'string' || f.name.trim() === '') {
|
|
errors.push(`implemented_features[${i}].name is required and must be a non-empty string`);
|
|
}
|
|
if (!f.description || typeof f.description !== 'string') {
|
|
errors.push(`implemented_features[${i}].description is required and must be a string`);
|
|
}
|
|
if (f.file_locations !== undefined) {
|
|
if (!Array.isArray(f.file_locations)) {
|
|
errors.push(`implemented_features[${i}].file_locations must be an array if provided`);
|
|
} else if (f.file_locations.some((loc) => typeof loc !== 'string' || loc.trim() === '')) {
|
|
errors.push(`implemented_features[${i}].file_locations items must be non-empty strings`);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Optional array fields
|
|
if (spec.additional_requirements !== undefined) {
|
|
if (!Array.isArray(spec.additional_requirements)) {
|
|
errors.push('additional_requirements must be an array if provided');
|
|
} else if (spec.additional_requirements.some((r) => typeof r !== 'string' || r.trim() === '')) {
|
|
errors.push('additional_requirements items must be non-empty strings');
|
|
}
|
|
}
|
|
|
|
if (spec.development_guidelines !== undefined) {
|
|
if (!Array.isArray(spec.development_guidelines)) {
|
|
errors.push('development_guidelines must be an array if provided');
|
|
} else if (spec.development_guidelines.some((g) => typeof g !== 'string' || g.trim() === '')) {
|
|
errors.push('development_guidelines items must be non-empty strings');
|
|
}
|
|
}
|
|
|
|
// Implementation roadmap
|
|
if (spec.implementation_roadmap !== undefined) {
|
|
if (!Array.isArray(spec.implementation_roadmap)) {
|
|
errors.push('implementation_roadmap must be an array if provided');
|
|
} else {
|
|
const validStatuses = ['completed', 'in_progress', 'pending'];
|
|
spec.implementation_roadmap.forEach((r, i) => {
|
|
if (!r.phase || typeof r.phase !== 'string' || r.phase.trim() === '') {
|
|
errors.push(
|
|
`implementation_roadmap[${i}].phase is required and must be a non-empty string`
|
|
);
|
|
}
|
|
if (!r.status || !validStatuses.includes(r.status)) {
|
|
errors.push(
|
|
`implementation_roadmap[${i}].status must be one of: ${validStatuses.join(', ')}`
|
|
);
|
|
}
|
|
if (!r.description || typeof r.description !== 'string') {
|
|
errors.push(`implementation_roadmap[${i}].description is required and must be a string`);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
return { valid: errors.length === 0, errors };
|
|
}
|
|
|
|
/**
|
|
* Check if XML content appears to be a valid spec XML (basic structure check).
|
|
* This is a quick check, not a full validation.
|
|
*
|
|
* @param xmlContent - The XML content to check
|
|
* @returns true if the content appears to be valid spec XML
|
|
*/
|
|
export function isValidSpecXml(xmlContent: string): boolean {
|
|
if (!xmlContent || typeof xmlContent !== 'string') {
|
|
return false;
|
|
}
|
|
|
|
// Check for essential elements
|
|
const hasRoot = xmlContent.includes('<project_specification>');
|
|
const hasProjectName = /<project_name>[\s\S]*?<\/project_name>/.test(xmlContent);
|
|
const hasOverview = /<overview>[\s\S]*?<\/overview>/.test(xmlContent);
|
|
const hasTechStack = /<technology_stack>[\s\S]*?<\/technology_stack>/.test(xmlContent);
|
|
const hasCapabilities = /<core_capabilities>[\s\S]*?<\/core_capabilities>/.test(xmlContent);
|
|
|
|
return hasRoot && hasProjectName && hasOverview && hasTechStack && hasCapabilities;
|
|
}
|