gitvan
Version:
Autonomic Git-native development automation platform with AI-powered workflows
385 lines (327 loc) • 9.9 kB
JavaScript
// src/workflow/WorkflowParser.mjs
// Parser and validator for workflow definitions in Turtle format
// Loads workflow definitions and validates their structure
import { useGraph } from "../composables/graph.mjs";
/**
* Workflow parser that loads and validates Turtle workflow definitions
*/
export class WorkflowParser {
/**
* @param {object} options
* @param {object} [options.logger] - Logger instance
*/
constructor(options = {}) {
this.logger = options.logger || console;
}
/**
* Parse a workflow definition from Turtle data
* @param {object} turtle - useTurtle instance
* @param {string} workflowId - The ID of the workflow to parse
* @returns {Promise<object|null>} Parsed workflow or null if not found
*/
async parseWorkflow(turtle, workflowId) {
this.logger.info(`🔍 Looking for workflow: ${workflowId}`);
try {
// Find the workflow hook
const hooks = turtle.getHooks();
const workflowHook = hooks.find((hook) => hook.id === workflowId);
if (!workflowHook) {
this.logger.warn(`⚠️ Workflow not found: ${workflowId}`);
return null;
}
this.logger.info(`🔍 Found workflow hook: ${workflowHook.title}`);
// Parse workflow steps
const steps = await this._parseWorkflowSteps(turtle, workflowHook);
// Validate workflow structure
await this._validateWorkflow(steps);
const workflow = {
id: workflowId,
title: workflowHook.title,
predicate: workflowHook.pred,
steps: steps,
metadata: {
parsedAt: new Date().toISOString(),
stepCount: steps.length,
},
};
this.logger.info(
`✅ Parsed workflow: ${workflowId} with ${steps.length} steps`
);
return workflow;
} catch (error) {
this.logger.error(`❌ Failed to parse workflow: ${workflowId}`, error);
throw new Error(`Workflow parsing failed: ${error.message}`);
}
}
/**
* Parse workflow steps from the hook's pipelines
* @private
*/
async _parseWorkflowSteps(turtle, workflowHook) {
const steps = [];
const stepIds = new Set();
for (const pipelineNode of workflowHook.pipelines) {
const pipelineSteps = turtle.getPipelineSteps(pipelineNode);
for (const stepNode of pipelineSteps) {
const step = await this._parseStep(turtle, stepNode);
if (step && !stepIds.has(step.id)) {
stepIds.add(step.id);
steps.push(step);
}
}
}
return steps;
}
/**
* Parse a single workflow step
* @private
*/
async _parseStep(turtle, stepNode) {
try {
const stepId = this._extractStepId(stepNode);
const stepType = this._extractStepType(turtle, stepNode);
const config = await this._extractStepConfig(turtle, stepNode);
const dependencies = this._extractDependencies(turtle, stepNode);
return {
id: stepId,
type: stepType,
config: config,
dependsOn: dependencies,
node: stepNode,
};
} catch (error) {
this.logger.warn(`⚠️ Failed to parse step: ${error.message}`);
return null;
}
}
/**
* Extract step ID from step node
* @private
*/
_extractStepId(stepNode) {
// Extract ID from the node URI or use a generated one
if (stepNode && stepNode.value) {
const uri = stepNode.value;
const parts = uri.split("/");
return parts[parts.length - 1] || uri;
}
return `step_${Date.now()}`;
}
/**
* Extract step type from step node
* @private
*/
_extractStepType(turtle, stepNode) {
const GV = "https://gitvan.dev/ontology#";
// Check for specific step types
if (turtle.isA(stepNode, GV + "SparqlStep")) {
return "sparql";
} else if (turtle.isA(stepNode, GV + "TemplateStep")) {
return "template";
} else if (turtle.isA(stepNode, GV + "FileStep")) {
return "file";
} else if (turtle.isA(stepNode, GV + "HttpStep")) {
return "http";
} else if (turtle.isA(stepNode, GV + "GitStep")) {
return "git";
}
return "unknown";
}
/**
* Extract step configuration
* @private
*/
async _extractStepConfig(turtle, stepNode) {
const GV = "https://gitvan.dev/ontology#";
const config = {};
// Extract SPARQL query
const queryText = await turtle.getQueryText(stepNode);
if (queryText) {
config.query = queryText;
}
// Extract template
const templateText = await turtle.getTemplateText(stepNode);
if (templateText) {
config.template = templateText;
}
// Extract file path
const filePath = turtle.getOne(stepNode, GV + "filePath");
if (filePath) {
config.filePath = filePath.value;
}
// Extract HTTP configuration
const httpUrl = turtle.getOne(stepNode, GV + "httpUrl");
if (httpUrl) {
config.httpUrl = httpUrl.value;
}
const httpMethod = turtle.getOne(stepNode, GV + "httpMethod");
if (httpMethod) {
config.httpMethod = httpMethod.value;
}
// Extract Git configuration
const gitCommand = turtle.getOne(stepNode, GV + "gitCommand");
if (gitCommand) {
config.gitCommand = gitCommand.value;
}
// Extract input/output mappings
const inputMapping = turtle.getOne(stepNode, GV + "inputMapping");
if (inputMapping) {
config.inputMapping = inputMapping.value;
}
const outputMapping = turtle.getOne(stepNode, GV + "outputMapping");
if (outputMapping) {
config.outputMapping = outputMapping.value;
}
// Extract timeout
const timeout = turtle.getOne(stepNode, GV + "timeout");
if (timeout) {
config.timeout = parseInt(timeout.value);
}
return config;
}
/**
* Extract step dependencies
* @private
*/
_extractDependencies(turtle, stepNode) {
const GV = "https://gitvan.dev/ontology#";
const dependencies = [];
const dependsOnList = turtle.getOne(stepNode, GV + "dependsOn");
if (dependsOnList) {
const dependsOnItems = turtle.readList(dependsOnList);
for (const item of dependsOnItems) {
if (item && item.value) {
dependencies.push(item.value);
}
}
}
return dependencies;
}
/**
* Validate workflow structure
* @private
*/
async _validateWorkflow(steps) {
this.logger.info(`🔍 Validating workflow structure`);
// Check for duplicate step IDs
const stepIds = steps.map((step) => step.id);
const uniqueIds = new Set(stepIds);
if (stepIds.length !== uniqueIds.size) {
throw new Error("Duplicate step IDs found in workflow");
}
// Check for circular dependencies
this._validateDependencies(steps);
// Check for required configurations
for (const step of steps) {
this._validateStepConfig(step);
}
this.logger.info(`✅ Workflow validation passed`);
}
/**
* Validate step dependencies for cycles
* @private
*/
_validateDependencies(steps) {
const stepMap = new Map(steps.map((step) => [step.id, step]));
for (const step of steps) {
const visited = new Set();
const visiting = new Set();
if (this._hasCycle(step.id, stepMap, visited, visiting)) {
throw new Error(
`Circular dependency detected involving step: ${step.id}`
);
}
}
}
/**
* Check for cycles in dependency graph
* @private
*/
_hasCycle(stepId, stepMap, visited, visiting) {
if (visiting.has(stepId)) {
return true; // Cycle detected
}
if (visited.has(stepId)) {
return false; // Already processed
}
visiting.add(stepId);
const step = stepMap.get(stepId);
if (step && step.dependsOn) {
for (const depId of step.dependsOn) {
if (this._hasCycle(depId, stepMap, visited, visiting)) {
return true;
}
}
}
visiting.delete(stepId);
visited.add(stepId);
return false;
}
/**
* Validate step configuration
* @private
*/
_validateStepConfig(step) {
switch (step.type) {
case "sparql":
if (!step.config.query) {
throw new Error(`SPARQL step ${step.id} missing query configuration`);
}
break;
case "template":
if (!step.config.template) {
throw new Error(
`Template step ${step.id} missing template configuration`
);
}
break;
case "file":
if (!step.config.filePath) {
throw new Error(
`File step ${step.id} missing filePath configuration`
);
}
break;
case "http":
if (!step.config.httpUrl) {
throw new Error(`HTTP step ${step.id} missing httpUrl configuration`);
}
break;
case "git":
if (!step.config.gitCommand) {
throw new Error(
`Git step ${step.id} missing gitCommand configuration`
);
}
break;
}
}
/**
* Get all available workflow IDs
* @param {object} turtle - useTurtle instance
* @returns {Promise<Array<string>>} List of workflow IDs
*/
async getWorkflowIds(turtle) {
const hooks = turtle.getHooks();
return hooks.map((hook) => hook.id);
}
/**
* Get workflow metadata without full parsing
* @param {object} turtle - useTurtle instance
* @param {string} workflowId - Workflow ID
* @returns {Promise<object|null>} Workflow metadata or null
*/
async getWorkflowMetadata(turtle, workflowId) {
const hooks = turtle.getHooks();
const workflowHook = hooks.find((hook) => hook.id === workflowId);
if (!workflowHook) {
return null;
}
return {
id: workflowId,
title: workflowHook.title,
predicate: workflowHook.pred,
pipelineCount: workflowHook.pipelines.length,
};
}
}