format-for-composer-v1.1.0.js•22.7 kB
#!/usr/bin/env node
/**
* Format for Composer Tool v1.1.0 - FAIL-FAST RELIABILITY
* Transforms validated lesson data into EXACT EuConquisto Composer JSON format with strict validation
* @version 1.1.0 (January 20, 2025)
* @status ENHANCED - Fail-fast validation with comprehensive error reporting
* @reference JIT workflow step 5 of 7
* @enhancement Strict validation, no fallbacks, detailed troubleshooting
*/
export class ComposerFormatterV110 {
constructor() {
this.processingStartTime = null;
this.transformationLog = [];
this.validationErrors = [];
this.supportedWidgetTypes = this.getSupportedWidgetTypes();
this.requiredComposerFields = ['version', 'metadata', 'interface', 'structure'];
}
/**
* Main formatting entry point with FAIL-FAST validation
* @param {Object} validatedLessonData - Validated lesson data with metadata and widgets
* @returns {Object} Composer JSON format OR detailed error information
*/
async formatForComposer(validatedLessonData) {
this.processingStartTime = Date.now();
this.transformationLog = [];
this.validationErrors = [];
try {
console.error('[FORMAT_FOR_COMPOSER_V110] Starting fail-fast formatting transformation');
// FAIL-FAST VALIDATION PHASE
const validationResult = this.validateValidatedLessonDataComprehensively(validatedLessonData);
if (!validationResult.valid) {
throw new Error(`Composer Formatting validation failed:\n\n${validationResult.errors.join('\n\n')}\n\nReceived input structure: ${JSON.stringify(this.getInputSummary(validatedLessonData), null, 2)}`);
}
const { metadata, widgets } = validationResult.normalizedData;
console.error(`[FORMAT_FOR_COMPOSER_V110] Validation passed - Formatting lesson: "${metadata.topic}"`);
console.error(`[FORMAT_FOR_COMPOSER_V110] Widgets to transform: ${widgets.length}`);
// COMPOSER STRUCTURE CREATION PHASE
const composition = this.createComposerStructure(metadata);
// WIDGET TRANSFORMATION PHASE
for (let index = 0; index < widgets.length; index++) {
const widget = widgets[index];
console.error(`[FORMAT_FOR_COMPOSER_V110] Transforming widget ${index + 1}/${widgets.length}: ${widget.type}`);
const transformedWidget = this.transformWidgetStrict(widget, index, widgets.length, metadata);
if (transformedWidget) {
composition.structure.push(transformedWidget);
this.logTransformation(`WIDGET_${index + 1}`, widget.type, 'transformed successfully');
} else {
throw new Error(`Failed to transform widget ${index + 1} (${widget.type}). Transformation returned null.`);
}
}
// FINAL VALIDATION PHASE
this.validateComposerOutputStrict(composition);
const processingTime = Date.now() - this.processingStartTime;
console.error(`[FORMAT_FOR_COMPOSER_V110] Success - ${composition.structure.length} widgets transformed in ${processingTime}ms`);
return {
success: true,
data: {
composerJSON: composition,
transformationSummary: {
totalWidgets: composition.structure.length,
processingTime: processingTime,
formatCompliance: "EuConquisto Composer API v1.1",
readyForSave: true
},
qualityMetrics: {
transformationSuccess: true,
widgetIntegrity: "100%",
formatValidation: "passed"
},
nextStepGuidance: {
readyForSave: true,
saveTool: "save_composition_direct_api",
note: "Composer JSON is fully formatted and ready for API save"
}
},
metadata: {
version: "1.1.0",
timestamp: new Date().toISOString(),
processingTime: processingTime,
failFastValidation: "passed",
widgetsTransformed: composition.structure.length
}
};
} catch (error) {
console.error('[FORMAT_FOR_COMPOSER_V110] FAIL-FAST ERROR:', error.message);
return {
success: false,
error: {
code: 'COMPOSER_FORMATTING_FAILED',
message: error.message,
timestamp: new Date().toISOString(),
processingTime: Date.now() - this.processingStartTime,
failFast: true,
developmentMode: true,
troubleshooting: {
requiredInputStructure: {
validatedLessonData: {
metadata: {
topic: "string - lesson title",
gradeLevel: "string - target grade level",
subject: "string - subject area",
duration: "string (optional)"
},
widgets: [
{
id: "string - unique identifier",
type: "string - supported widget type",
content: "object - widget-specific content"
}
]
}
},
transformationPhases: [
"1. Input validation and normalization",
"2. Composer structure creation",
"3. Widget-by-widget transformation",
"4. Final Composer JSON validation",
"5. Output quality verification"
],
supportedWidgetTypes: this.supportedWidgetTypes,
commonIssues: [
"Invalid widget types not supported by Composer",
"Missing or malformed widget content",
"Metadata fields missing or invalid",
"Widget transformation errors",
"Composer JSON structure validation failures"
],
debugSteps: [
"1. Check if validatedLessonData is properly structured",
"2. Verify all widget types are supported",
"3. Ensure widget content follows expected format",
"4. Check metadata completeness",
"5. Validate transformation output structure"
]
}
}
};
}
}
/**
* COMPREHENSIVE FAIL-FAST VALIDATION for validated lesson data
*/
validateValidatedLessonDataComprehensively(validatedLessonData) {
const errors = [];
// Basic input validation
if (!validatedLessonData) {
errors.push("❌ MISSING INPUT ERROR:\nvalidatedLessonData is required.\nThis should come from the validate_lesson_data tool output.\nExpected: { metadata: {...}, widgets: [...] }");
return { valid: false, errors };
}
if (typeof validatedLessonData !== 'object' || validatedLessonData === null) {
errors.push(`❌ INPUT TYPE ERROR:\nvalidatedLessonData must be an object, received: ${typeof validatedLessonData}\nValue: ${JSON.stringify(validatedLessonData, null, 2)}`);
return { valid: false, errors };
}
// Handle nested data structure (from validate_lesson_data output)
let actualData = validatedLessonData;
if (validatedLessonData.data && validatedLessonData.data.validatedLessonData) {
console.error('[FORMAT_FOR_COMPOSER_V110] Extracting data from nested structure');
actualData = validatedLessonData.data.validatedLessonData;
} else if (validatedLessonData.validatedLessonData) {
console.error('[FORMAT_FOR_COMPOSER_V110] Extracting data from wrapped structure');
actualData = validatedLessonData.validatedLessonData;
}
// Metadata validation
if (!actualData.metadata) {
errors.push("❌ MISSING METADATA ERROR:\nmetadata object is required in validated lesson data.\nMetadata should contain lesson information (topic, gradeLevel, subject).\nCheck validate_lesson_data tool output.");
return { valid: false, errors };
}
if (typeof actualData.metadata !== 'object' || actualData.metadata === null) {
errors.push(`❌ METADATA TYPE ERROR:\nmetadata must be an object, received: ${typeof actualData.metadata}\nValue: ${JSON.stringify(actualData.metadata)}`);
return { valid: false, errors };
}
// Required metadata fields
const requiredMetadataFields = ['topic', 'gradeLevel', 'subject'];
const missingMetadataFields = requiredMetadataFields.filter(field => !actualData.metadata[field]);
if (missingMetadataFields.length > 0) {
errors.push(`❌ INCOMPLETE METADATA ERROR:\nRequired metadata fields missing: ${missingMetadataFields.join(', ')}\nRequired fields: ${requiredMetadataFields.join(', ')}\nReceived metadata: ${JSON.stringify(actualData.metadata, null, 2)}`);
return { valid: false, errors };
}
// Widgets validation
if (!actualData.widgets) {
errors.push("❌ MISSING WIDGETS ERROR:\nwidgets array is required in validated lesson data.\nWidgets should contain the lesson content structure.\nCheck validate_lesson_data tool output.");
return { valid: false, errors };
}
if (!Array.isArray(actualData.widgets)) {
errors.push(`❌ WIDGETS TYPE ERROR:\nwidgets must be an array, received: ${typeof actualData.widgets}\nValue: ${JSON.stringify(actualData.widgets)}`);
return { valid: false, errors };
}
if (actualData.widgets.length === 0) {
errors.push("❌ EMPTY WIDGETS ERROR:\nwidgets array cannot be empty.\nAt least one widget is required for Composer formatting.\nEnsure validate_lesson_data completed successfully.");
return { valid: false, errors };
}
// Individual widget validation
actualData.widgets.forEach((widget, index) => {
const widgetErrors = this.validateWidgetForTransformation(widget, index);
if (widgetErrors.length > 0) {
errors.push(`❌ WIDGET ${index + 1} TRANSFORMATION ERROR:\n${widgetErrors.join('\n')}\nWidget: ${JSON.stringify(widget, null, 2)}`);
}
});
if (errors.length > 0) {
return { valid: false, errors };
}
console.error(`[FORMAT_FOR_COMPOSER_V110] Validation passed - normalized data extracted`);
return {
valid: true,
normalizedData: {
metadata: actualData.metadata,
widgets: actualData.widgets
},
errors: []
};
}
/**
* VALIDATE widget for transformation readiness
*/
validateWidgetForTransformation(widget, index) {
const errors = [];
// Basic structure
if (!widget || typeof widget !== 'object') {
errors.push(`Widget must be an object, received: ${typeof widget}`);
return errors;
}
// Required fields
if (!widget.id || typeof widget.id !== 'string' || widget.id.trim().length === 0) {
errors.push(`Widget ID is required and must be a non-empty string`);
}
if (!widget.type || typeof widget.type !== 'string') {
errors.push(`Widget type is required and must be a string`);
} else if (!this.supportedWidgetTypes.includes(widget.type)) {
errors.push(`Unsupported widget type for Composer: ${widget.type}\nSupported types: ${this.supportedWidgetTypes.join(', ')}`);
}
if (!widget.content || typeof widget.content !== 'object' || widget.content === null) {
errors.push(`Widget content is required and must be an object`);
}
return errors;
}
/**
* CREATE Composer structure with metadata and interface
*/
createComposerStructure(metadata) {
const composition = {
version: "1.1",
metadata: {
title: metadata.topic || "Educational Lesson",
description: metadata.description || `Lesson about ${metadata.topic} for ${metadata.gradeLevel}`,
thumb: metadata.thumb || null,
tags: metadata.tags || [metadata.subject, metadata.gradeLevel].filter(Boolean)
},
interface: {
content_language: "pt_br",
index_option: "buttons",
font_family: "Lato",
show_summary: "disabled",
finish_btn: "disabled"
},
structure: []
};
console.error(`[FORMAT_FOR_COMPOSER_V110] Composer structure created for: "${metadata.topic}"`);
return composition;
}
/**
* STRICT widget transformation with comprehensive error handling
*/
transformWidgetStrict(widget, index, totalWidgets, metadata) {
try {
const baseType = widget.type.split('-')[0];
let transformedWidget = null;
switch (baseType) {
case 'head':
transformedWidget = this.transformHeadWidget(widget, index);
break;
case 'text':
transformedWidget = this.transformTextWidget(widget, index);
break;
case 'quiz':
transformedWidget = this.transformQuizWidget(widget, index);
break;
case 'flashcards':
transformedWidget = this.transformFlashcardsWidget(widget, index);
break;
case 'image':
transformedWidget = this.transformImageWidget(widget, index);
break;
case 'list':
transformedWidget = this.transformListWidget(widget, index);
break;
case 'video':
transformedWidget = this.transformVideoWidget(widget, index);
break;
case 'hotspots':
transformedWidget = this.transformHotspotsWidget(widget, index);
break;
default:
throw new Error(`Unsupported widget type for transformation: ${widget.type}`);
}
if (!transformedWidget) {
throw new Error(`Transformation function returned null for widget type: ${widget.type}`);
}
// Validate transformed widget structure
this.validateTransformedWidget(transformedWidget, widget.type);
return transformedWidget;
} catch (error) {
throw new Error(`Widget transformation failed for ${widget.type} at index ${index}: ${error.message}`);
}
}
/**
* VALIDATE transformed widget structure
*/
validateTransformedWidget(transformedWidget, originalType) {
if (!transformedWidget || typeof transformedWidget !== 'object') {
throw new Error(`Transformed widget is not a valid object for type: ${originalType}`);
}
if (!transformedWidget.id || typeof transformedWidget.id !== 'string') {
throw new Error(`Transformed widget missing valid ID for type: ${originalType}`);
}
if (!transformedWidget.type || typeof transformedWidget.type !== 'string') {
throw new Error(`Transformed widget missing valid type for type: ${originalType}`);
}
if (!transformedWidget.content || typeof transformedWidget.content !== 'object') {
throw new Error(`Transformed widget missing valid content for type: ${originalType}`);
}
}
/**
* Transform head widget to Composer format
*/
transformHeadWidget(widget, index) {
if (!widget.content.title) {
throw new Error("Head widget content.title is required");
}
return {
id: widget.id,
type: "head-1",
content: {
title: widget.content.title,
subtitle: widget.content.subtitle || ""
}
};
}
/**
* Transform text widget to Composer format
*/
transformTextWidget(widget, index) {
if (!widget.content.text) {
throw new Error("Text widget content.text is required");
}
return {
id: widget.id,
type: "text-1",
content: {
text: widget.content.text,
title: widget.content.title || ""
}
};
}
/**
* Transform quiz widget to Composer format
*/
transformQuizWidget(widget, index) {
if (!widget.content.questions || !Array.isArray(widget.content.questions)) {
throw new Error("Quiz widget content.questions array is required");
}
if (widget.content.questions.length === 0) {
throw new Error("Quiz widget must have at least one question");
}
const transformedQuestions = widget.content.questions.map((q, qIndex) => {
if (!q.question || !q.options || !Array.isArray(q.options) || typeof q.correct !== 'number') {
throw new Error(`Quiz question ${qIndex + 1} missing required fields (question, options, correct)`);
}
return {
question: q.question,
options: q.options,
correct: q.correct
};
});
return {
id: widget.id,
type: "quiz-1",
content: {
questions: transformedQuestions
}
};
}
/**
* Transform flashcards widget to Composer format
*/
transformFlashcardsWidget(widget, index) {
if (!widget.content.cards || !Array.isArray(widget.content.cards)) {
throw new Error("Flashcards widget content.cards array is required");
}
if (widget.content.cards.length === 0) {
throw new Error("Flashcards widget must have at least one card");
}
const transformedCards = widget.content.cards.map((card, cIndex) => {
if (!card.front || !card.back) {
throw new Error(`Flashcard ${cIndex + 1} missing required fields (front, back)`);
}
return {
front: card.front,
back: card.back
};
});
return {
id: widget.id,
type: "flashcards-1",
content: {
cards: transformedCards
}
};
}
/**
* Transform image widget to Composer format
*/
transformImageWidget(widget, index) {
if (!widget.content.src) {
throw new Error("Image widget content.src is required");
}
return {
id: widget.id,
type: "image-1",
content: {
src: widget.content.src,
alt: widget.content.alt || "Educational image",
caption: widget.content.caption || ""
}
};
}
/**
* Transform list widget to Composer format
*/
transformListWidget(widget, index) {
if (!widget.content.items || !Array.isArray(widget.content.items)) {
throw new Error("List widget content.items array is required");
}
if (widget.content.items.length === 0) {
throw new Error("List widget must have at least one item");
}
return {
id: widget.id,
type: "list-1",
content: {
items: widget.content.items,
title: widget.content.title || ""
}
};
}
/**
* Transform video widget to Composer format
*/
transformVideoWidget(widget, index) {
if (!widget.content.src) {
throw new Error("Video widget content.src is required");
}
return {
id: widget.id,
type: "video-1",
content: {
src: widget.content.src,
title: widget.content.title || "",
description: widget.content.description || ""
}
};
}
/**
* Transform hotspots widget to Composer format
*/
transformHotspotsWidget(widget, index) {
if (!widget.content.image || !widget.content.hotspots) {
throw new Error("Hotspots widget requires content.image and content.hotspots");
}
return {
id: widget.id,
type: "hotspots-1",
content: {
image: widget.content.image,
hotspots: widget.content.hotspots
}
};
}
/**
* VALIDATE final Composer output structure
*/
validateComposerOutputStrict(composition) {
const errors = [];
// Check required fields
this.requiredComposerFields.forEach(field => {
if (!composition[field]) {
errors.push(`Composer JSON missing required field: ${field}`);
}
});
// Validate structure array
if (!Array.isArray(composition.structure)) {
errors.push("Composer JSON structure must be an array");
} else if (composition.structure.length === 0) {
errors.push("Composer JSON structure cannot be empty");
}
// Validate each widget in structure
composition.structure.forEach((widget, index) => {
if (!widget.id || !widget.type || !widget.content) {
errors.push(`Widget ${index + 1} in structure missing required fields (id, type, content)`);
}
});
if (errors.length > 0) {
throw new Error(`Composer JSON validation failed:\n${errors.join('\n')}`);
}
console.error(`[FORMAT_FOR_COMPOSER_V110] Final Composer JSON validation passed`);
}
/**
* Log transformation step
*/
logTransformation(widgetId, widgetType, result) {
this.transformationLog.push({
widgetId,
widgetType,
result,
timestamp: new Date().toISOString()
});
}
/**
* Get input summary for error reporting
*/
getInputSummary(input) {
if (!input) return "null";
if (typeof input !== 'object') return typeof input;
const summary = {
hasMetadata: !!input.metadata,
hasWidgets: !!input.widgets,
hasNestedData: !!(input.data || input.validatedLessonData)
};
if (input.metadata) {
summary.metadataKeys = Object.keys(input.metadata);
}
if (input.widgets) {
summary.widgetsCount = Array.isArray(input.widgets) ? input.widgets.length : 'not array';
}
return summary;
}
/**
* Get supported widget types for Composer
*/
getSupportedWidgetTypes() {
return [
'head-1', 'head-2',
'text-1', 'text-2',
'quiz-1', 'quiz-2',
'flashcards-1', 'flashcards-2',
'image-1', 'image-2',
'video-1', 'video-2',
'list-1', 'list-2',
'hotspots-1', 'hotspots-2'
];
}
}
/**
* Create and export the tool instance for JIT server integration
*/
export function createComposerFormatterV110() {
const formatter = new ComposerFormatterV110();
return {
name: 'format_for_composer_v110',
description: 'STEP 5 v1.1.0: Transform validated lesson data into exact EuConquisto Composer JSON format with fail-fast validation. Ensures perfect API compliance with detailed error reporting.',
inputSchema: {
type: 'object',
properties: {
validatedLessonData: {
type: 'object',
description: 'Validated lesson data from validate_lesson_data tool output',
properties: {
metadata: {
type: 'object',
properties: {
topic: { type: 'string' },
gradeLevel: { type: 'string' },
subject: { type: 'string' }
},
required: ['topic', 'gradeLevel', 'subject']
},
widgets: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'string' },
type: { type: 'string' },
content: { type: 'object' }
},
required: ['id', 'type', 'content']
}
}
},
required: ['metadata', 'widgets']
}
},
required: ['validatedLessonData']
},
handler: async (input) => {
return await formatter.formatForComposer(input.validatedLessonData);
}
};
}