meld
Version:
Meld: A template language for LLM prompts
1,365 lines (1,148 loc) • 46.7 kB
text/typescript
import { IStateVisualizationService, VisualizationConfig, StateMetrics, NodeStyle, EdgeStyle } from './IStateVisualizationService';
import { IStateHistoryService } from '../StateHistoryService/IStateHistoryService';
import { IStateTrackingService, StateMetadata, StateRelationship } from '../StateTrackingService/IStateTrackingService';
/**
* @package
* Implementation of state visualization service.
*/
export class StateVisualizationService implements IStateVisualizationService {
constructor(
private historyService: IStateHistoryService,
private trackingService: IStateTrackingService,
) {}
private generateMermaidGraph(nodes: Map<string, StateMetadata>, edges: StateRelationship[], config: VisualizationConfig): string {
const lines: string[] = ['graph TD;'];
// Add nodes with styling
nodes.forEach((metadata, id) => {
const style = this.getNodeStyle(metadata, config);
const styleStr = `style="${style.shape},${style.color}"`;
const label = config.includeMetadata
? `${id}[${metadata.source}${metadata.filePath ? `\\n${metadata.filePath}` : ''}]`
: `${id}[${metadata.source}]`;
lines.push(` ${label} ${styleStr};`);
});
// Add edges with styling
edges.forEach(edge => {
const style = this.getEdgeStyle(edge, config);
const styleStr = `style="${style.style},${style.color}"`;
const label = edge.type;
lines.push(` ${edge.targetId} -->|${label}| ${edge.type} ${styleStr};`);
});
return lines.join('\n');
}
private generateDotGraph(nodes: Map<string, StateMetadata>, edges: StateRelationship[], config: VisualizationConfig): string {
const lines: string[] = ['digraph G {'];
// Add nodes with styling
nodes.forEach((metadata, id) => {
const style = this.getNodeStyle(metadata, config);
const label = config.includeMetadata
? `${id}\\n${metadata.source}${metadata.filePath ? `\\n${metadata.filePath}` : ''}`
: `${id}\\n${metadata.source}`;
const attrs = [
`label="${label}"`,
`shape="${style.shape}"`,
`color="${style.color}"`,
];
if (style.tooltip) {
attrs.push(`tooltip="${style.tooltip}"`);
}
lines.push(` "${id}" [${attrs.join(',')}];`);
});
// Add edges with styling
edges.forEach(edge => {
const style = this.getEdgeStyle(edge, config);
const attrs = [
`style="${style.style}"`,
`color="${style.color}"`,
`label="${edge.type}"`,
];
if (style.tooltip) {
attrs.push(`tooltip="${style.tooltip}"`);
}
lines.push(` "${edge.targetId}" -> "${edge.type}" [${attrs.join(',')}];`);
});
lines.push('}');
return lines.join('\n');
}
private getNodeStyle(metadata: StateMetadata, config: VisualizationConfig): NodeStyle {
if (config.styleNodes) {
return config.styleNodes(metadata);
}
// Default styling based on state type
const style: NodeStyle = {
shape: 'box',
color: '#000000',
};
switch (metadata.source) {
case 'new':
style.color = '#4CAF50';
break;
case 'clone':
style.color = '#2196F3';
break;
case 'merge':
style.shape = 'diamond';
style.color = '#9C27B0';
break;
case 'implicit':
style.color = '#757575';
break;
}
return style;
}
private getEdgeStyle(relationship: StateRelationship, config: VisualizationConfig): EdgeStyle {
if (config.styleEdges) {
return config.styleEdges(relationship);
}
// Default styling based on relationship type
const style: EdgeStyle = {
style: 'solid',
color: '#000000',
};
switch (relationship.type) {
case 'parent-child':
style.style = 'solid';
break;
case 'merge-source':
style.style = 'dashed';
break;
case 'merge-target':
style.style = 'dotted';
break;
}
return style;
}
public generateHierarchyView(rootStateId: string, config: VisualizationConfig): string {
// Validate format first
if (!['mermaid', 'dot', 'json'].includes(config.format)) {
throw new Error(`Unsupported format: ${config.format}`);
}
const lineage = this.trackingService.getStateLineage(rootStateId);
const descendants = this.trackingService.getStateDescendants(rootStateId);
const allStateIds = new Set([...lineage, ...descendants]);
// Build nodes and edges
const nodes = new Map<string, StateMetadata>();
const edges: StateRelationship[] = [];
// Collect all states and their relationships
allStateIds.forEach(stateId => {
// Get state metadata from history
const operations = this.historyService.getOperationHistory(stateId);
const createOp = operations.find(op => op.type === 'create');
if (createOp && createOp.metadata) {
nodes.set(stateId, createOp.metadata);
}
// Get relationships from tracking service
const stateLineage = this.trackingService.getStateLineage(stateId);
if (stateLineage.length > 1) {
const parentIndex = stateLineage.indexOf(stateId) - 1;
if (parentIndex >= 0) {
edges.push({
targetId: stateId,
type: 'parent-child',
});
}
}
});
// Generate visualization in requested format
switch (config.format) {
case 'mermaid':
return this.generateMermaidGraph(nodes, edges, config);
case 'dot':
return this.generateDotGraph(nodes, edges, config);
case 'json':
return JSON.stringify({
nodes: Array.from(nodes.entries()).map(([id, metadata]) => ({
id,
...metadata,
})),
edges,
}, null, 2);
default:
throw new Error(`Unsupported format: ${config.format}`);
}
}
public generateTransitionDiagram(stateId: string, config: VisualizationConfig): string {
const transformations = this.historyService.getTransformationChain(stateId);
if (transformations.length === 0) {
return '';
}
switch (config.format) {
case 'mermaid':
return this.generateMermaidTransitionDiagram(transformations, config);
case 'dot':
return this.generateDotTransitionDiagram(transformations, config);
case 'json':
return JSON.stringify(transformations, null, 2);
default:
throw new Error(`Unsupported format: ${config.format}`);
}
}
private generateMermaidTransitionDiagram(transformations: StateTransformation[], config: VisualizationConfig): string {
const lines: string[] = ['graph LR;'];
// Helper to format value display
const formatValue = (value: unknown): string => {
if (typeof value === 'object' && value !== null) {
return Object.entries(value as Record<string, unknown>)
.map(([key, val]) => {
if (Array.isArray(val)) {
return `${key}: [${val.join(',')}]`;
} else if (typeof val === 'object' && val !== null) {
return Object.entries(val as Record<string, unknown>)
.map(([k, v]) => Array.isArray(v) ? `${key}.${k}: [${v.join(',')}]` : `${key}.${k}: ${v}`)
.join('\\n');
}
return `${key}: ${val}`;
})
.join('\\n');
}
return String(value);
};
// Add nodes and transitions
transformations.forEach((transform, index) => {
const beforeId = `state_${index}`;
const afterId = `state_${index + 1}`;
// Add before state
const beforeLabel = formatValue(transform.before);
lines.push(` ${beforeId}["${beforeLabel}"];`);
// Add after state
const afterLabel = formatValue(transform.after);
lines.push(` ${afterId}["${afterLabel}"];`);
// Add transition with timestamp first for better readability
const transitionLabel = config.includeTimestamps
? `${transform.timestamp} ${transform.operation}`
: transform.operation;
lines.push(` ${beforeId} -->|${transitionLabel}| ${afterId};`);
// Add styling
const style = this.getNodeStyle({ source: transform.source } as StateMetadata, config);
lines.push(` style ${beforeId} fill:${style.color};`);
lines.push(` style ${afterId} fill:${style.color};`);
});
return lines.join('\n');
}
private generateDotTransitionDiagram(transformations: StateTransformation[], config: VisualizationConfig): string {
const lines: string[] = ['digraph G {'];
lines.push(' rankdir=LR;'); // Left to right layout
// Helper to format value display
const formatValue = (value: unknown): string => {
if (typeof value === 'object' && value !== null) {
return Object.entries(value as Record<string, unknown>)
.map(([key, val]) => {
if (Array.isArray(val)) {
return `${key}: [${val.join(',')}]`;
} else if (typeof val === 'object' && val !== null) {
return Object.entries(val as Record<string, unknown>)
.map(([k, v]) => `${key}.${k}: ${v}`)
.join('\\n');
}
return `${key}: ${val}`;
})
.join('\\n');
}
return String(value);
};
// Add nodes and transitions
transformations.forEach((transform, index) => {
const beforeId = `state_${index}`;
const afterId = `state_${index + 1}`;
// Add before state
const beforeLabel = formatValue(transform.before);
const beforeStyle = this.getNodeStyle({ source: transform.source } as StateMetadata, config);
lines.push(` "${beforeId}" [label="${beforeLabel}",shape="${beforeStyle.shape}",color="${beforeStyle.color}"];`);
// Add after state
const afterLabel = formatValue(transform.after);
const afterStyle = this.getNodeStyle({ source: transform.source } as StateMetadata, config);
lines.push(` "${afterId}" [label="${afterLabel}",shape="${afterStyle.shape}",color="${afterStyle.color}"];`);
// Add transition
const transitionLabel = config.includeTimestamps
? `${transform.operation}\\n${transform.timestamp}`
: transform.operation;
lines.push(` "${beforeId}" -> "${afterId}" [label="${transitionLabel}"];`);
});
lines.push('}');
return lines.join('\n');
}
public generateRelationshipGraph(stateIds: string[], config: VisualizationConfig): string {
if (!['mermaid', 'dot', 'json'].includes(config.format)) {
throw new Error(`Unsupported format: ${config.format}`);
}
// Collect all states and their relationships
const nodes = new Map<string, StateMetadata>();
const edges: StateRelationship[] = [];
const processedStates = new Set<string>();
// Helper to process a state and its relationships
const processState = (stateId: string) => {
if (processedStates.has(stateId)) return;
processedStates.add(stateId);
// Get state metadata from history
const operations = this.historyService.getOperationHistory(stateId);
const createOrMergeOp = operations.find(op => op.type === 'create' || op.type === 'merge');
if (createOrMergeOp?.metadata) {
nodes.set(stateId, createOrMergeOp.metadata);
}
// Get lineage relationships
const lineage = this.trackingService.getStateLineage(stateId);
if (lineage.length > 1) {
for (let i = 1; i < lineage.length; i++) {
edges.push({
targetId: lineage[i],
sourceId: lineage[i - 1],
type: 'parent-child',
});
}
}
// Get merge relationships
const mergeOps = operations.filter(op => op.type === 'merge');
mergeOps.forEach(op => {
if (op.parentId) {
edges.push({
sourceId: op.parentId,
targetId: stateId,
type: 'merge-source',
});
// Also process the parent state if we haven't yet
processState(op.parentId);
}
});
// Process descendants
const descendants = this.trackingService.getStateDescendants(stateId);
descendants.forEach(descendantId => processState(descendantId));
};
// Process all requested states
stateIds.forEach(stateId => processState(stateId));
// Generate visualization in requested format
switch (config.format) {
case 'mermaid':
return this.generateMermaidRelationshipGraph(nodes, edges, config);
case 'dot':
return this.generateDotRelationshipGraph(nodes, edges, config);
case 'json':
return JSON.stringify({
nodes: Array.from(nodes.entries()).map(([id, metadata]) => ({
id,
...metadata,
})),
edges,
}, null, 2);
default:
throw new Error(`Unsupported format: ${config.format}`);
}
}
private generateMermaidRelationshipGraph(
nodes: Map<string, StateMetadata>,
edges: StateRelationship[],
config: VisualizationConfig
): string {
const lines: string[] = ['graph TD;'];
// Add nodes with styling
nodes.forEach((metadata, id) => {
const style = this.getNodeStyle(metadata, config);
const label = config.includeMetadata
? `${id}[${metadata.source}${metadata.filePath ? `\\n${metadata.filePath}` : ''}]`
: `${id}[${metadata.source}]`;
lines.push(` ${label};`);
lines.push(` style ${id} fill:${style.color},stroke:${style.color},stroke-width:2px,${style.shape};`);
});
// Add edges with styling
edges.forEach(edge => {
const style = this.getEdgeStyle(edge, config);
const sourceId = edge.sourceId || 'unknown';
const label = config.includeMetadata ? edge.type : '';
lines.push(` ${sourceId} -->|${label}| ${edge.targetId};`);
lines.push(` linkStyle ${lines.length - 2} stroke:${style.color},stroke-width:2px,${style.style};`);
});
return lines.join('\n');
}
private generateDotRelationshipGraph(
nodes: Map<string, StateMetadata>,
edges: StateRelationship[],
config: VisualizationConfig
): string {
const lines: string[] = ['digraph G {'];
lines.push(' rankdir=TB;'); // Top to bottom layout
// Add nodes with styling
nodes.forEach((metadata, id) => {
const style = this.getNodeStyle(metadata, config);
const label = config.includeMetadata
? `${id}\\n${metadata.source}${metadata.filePath ? `\\n${metadata.filePath}` : ''}`
: `${id}\\n${metadata.source}`;
const attrs = [
`label="${label}"`,
`shape="${style.shape}"`,
`color="${style.color}"`,
`style="filled"`,
`fillcolor="${style.color}22"`, // Add transparency to fill color
];
if (style.tooltip) {
attrs.push(`tooltip="${style.tooltip}"`);
}
lines.push(` "${id}" [${attrs.join(',')}];`);
});
// Add edges with styling
edges.forEach(edge => {
const style = this.getEdgeStyle(edge, config);
const sourceId = edge.sourceId || 'unknown';
const attrs = [
`style="${style.style}"`,
`color="${style.color}"`,
`penwidth=2`,
];
if (config.includeMetadata) {
attrs.push(`label="${edge.type}"`);
}
if (style.tooltip) {
attrs.push(`tooltip="${style.tooltip}"`);
}
lines.push(` "${sourceId}" -> "${edge.targetId}" [${attrs.join(',')}];`);
});
lines.push('}');
return lines.join('\n');
}
public generateTimeline(stateIds: string[], config: VisualizationConfig): string {
const operations = stateIds.flatMap(id => this.historyService.getOperationHistory(id));
operations.sort((a, b) => a.timestamp - b.timestamp);
if (operations.length === 0) {
return '';
}
switch (config.format) {
case 'mermaid':
return this.generateMermaidTimeline(operations, config);
case 'dot':
return this.generateDotTimeline(operations, config);
case 'json':
return JSON.stringify(operations, null, 2);
default:
throw new Error(`Unsupported format: ${config.format}`);
}
}
private generateMermaidTimeline(operations: StateOperation[], config: VisualizationConfig): string {
const lines: string[] = [
'gantt',
' dateFormat X',
' axisFormat %s',
'',
];
// Group operations by state
const stateGroups = new Map<string, StateOperation[]>();
operations.forEach(op => {
if (!stateGroups.has(op.stateId)) {
stateGroups.set(op.stateId, []);
}
stateGroups.get(op.stateId)!.push(op);
});
// Add sections for each state
stateGroups.forEach((stateOps, stateId) => {
lines.push(` section ${stateId}`);
stateOps.forEach((op, index) => {
const duration = index < stateOps.length - 1 ? stateOps[index + 1].timestamp - op.timestamp : 1000;
const taskId = `${stateId}_${op.type}_${op.timestamp}`;
const label = config.includeTimestamps
? `${op.type} (${op.timestamp})`
: op.type;
lines.push(` ${label} :${taskId}, ${op.timestamp}, ${duration}ms`);
});
lines.push('');
});
return lines.join('\n');
}
private generateDotTimeline(operations: StateOperation[], config: VisualizationConfig): string {
const lines: string[] = ['digraph G {'];
lines.push(' rankdir=LR;');
// Add nodes for each operation
operations.forEach((op, index) => {
const label = config.includeTimestamps
? `${op.type}\\n${op.timestamp}`
: op.type;
lines.push(` "op_${index}" [label="${label}"];`);
// Add edge to next operation if it exists
if (index < operations.length - 1) {
lines.push(` "op_${index}" -> "op_${index + 1}";`);
}
});
lines.push('}');
return lines.join('\n');
}
public getMetrics(timeRange?: { start: number; end: number }): StateMetrics {
// Get all operations within time range
const operations = this.historyService.queryHistory({
timeRange,
});
// Calculate metrics
const metrics: StateMetrics = {
totalStates: 0,
statesByType: {},
averageTransformationsPerState: 0,
maxTransformationChainLength: 0,
averageChildrenPerState: 0,
maxTreeDepth: 0,
operationFrequency: {},
};
if (operations.length === 0) {
return metrics;
}
// Count unique states and their types
const stateIds = new Set<string>();
const stateTypes = new Map<string, number>();
const transformationsPerState = new Map<string, number>();
const operationCounts = new Map<string, number>();
operations.forEach(op => {
// Count states
stateIds.add(op.stateId);
// Count state types
if ((op.type === 'create' || op.type === 'merge') && op.source) {
stateTypes.set(op.source, (stateTypes.get(op.source) || 0) + 1);
}
// Count transformations per state
if (op.type === 'transform') {
transformationsPerState.set(op.stateId, (transformationsPerState.get(op.stateId) || 0) + 1);
}
// Count operation frequencies
operationCounts.set(op.type, (operationCounts.get(op.type) || 0) + 1);
});
// Calculate tree depth metrics
const stateLineages = Array.from(stateIds)
.map(id => this.trackingService.getStateLineage(id))
.filter(lineage => lineage && lineage.length > 0); // Filter out undefined or empty lineages
const maxDepth = stateLineages.length > 0
? Math.max(...stateLineages.map(lineage => lineage.length))
: 0;
// Calculate children per state
const childrenCounts = new Map<string, number>();
stateLineages.forEach(lineage => {
if (lineage.length > 1) {
const parentId = lineage[lineage.length - 2];
childrenCounts.set(parentId, (childrenCounts.get(parentId) || 0) + 1);
}
});
// Set metrics
metrics.totalStates = stateIds.size;
metrics.statesByType = Object.fromEntries(stateTypes);
metrics.averageTransformationsPerState = stateIds.size > 0
? Array.from(transformationsPerState.values()).reduce((a, b) => a + b, 0) / stateIds.size
: 0;
metrics.maxTransformationChainLength = transformationsPerState.size > 0
? Math.max(...Array.from(transformationsPerState.values()))
: 0;
metrics.averageChildrenPerState = childrenCounts.size > 0
? Array.from(childrenCounts.values()).reduce((a, b) => a + b, 0) / childrenCounts.size
: 0;
metrics.maxTreeDepth = maxDepth;
metrics.operationFrequency = Object.fromEntries(operationCounts);
return metrics;
}
public exportStateGraph(config: VisualizationConfig): string {
// TODO: Implement complete graph export
return '';
}
/**
* Generate a context hierarchy visualization showing context boundaries
* @param rootStateId - The root state to start visualization from
* @param config - Context visualization configuration
* @returns Context hierarchy visualization in the specified format
*/
public visualizeContextHierarchy(rootStateId: string, config: ContextVisualizationConfig): string {
// Get the hierarchy information from the tracking service
const hierarchyInfo = this.trackingService.getContextHierarchy(rootStateId);
// Generate visualization based on the format
switch (config.format) {
case 'mermaid':
return this.generateMermaidContextHierarchy(hierarchyInfo, config);
case 'dot':
return this.generateDotContextHierarchy(hierarchyInfo, config);
case 'json':
return JSON.stringify(hierarchyInfo, null, 2);
default:
throw new Error(`Unsupported format: ${config.format}`);
}
}
/**
* Generate a variable propagation visualization showing how variables move across contexts
* @param variableName - The name of the variable to track propagation for
* @param rootStateId - Optional root state to limit visualization scope
* @param config - Context visualization configuration
* @returns Variable propagation visualization in the specified format
*/
public visualizeVariablePropagation(variableName: string, rootStateId?: string, config?: ContextVisualizationConfig): string {
const defaultConfig: ContextVisualizationConfig = {
format: 'mermaid',
includeVars: true,
filterToRelevantVars: true,
includeTimestamps: true,
includeFilePaths: true
};
const mergedConfig = { ...defaultConfig, ...config };
// Get all states or limit to the subtree from rootStateId
let states: StateMetadata[] = [];
if (rootStateId) {
const hierarchyInfo = this.trackingService.getContextHierarchy(rootStateId);
states = hierarchyInfo.states;
} else {
states = this.trackingService.getAllStates();
}
// Get all variable crossings for the specified variable
const allCrossings = states.flatMap(state =>
this.trackingService.getVariableCrossings(state.id)
);
// Filter to just the specified variable
const variableCrossings = allCrossings.filter(
crossing => crossing.variableName === variableName
);
// If there are no crossings, return a simple message
if (variableCrossings.length === 0) {
return `// No variable crossings found for variable "${variableName}"`;
}
// Generate visualization based on the format
switch (mergedConfig.format) {
case 'mermaid':
return this.generateMermaidVariablePropagation(variableName, states, variableCrossings, mergedConfig);
case 'dot':
return this.generateDotVariablePropagation(variableName, states, variableCrossings, mergedConfig);
case 'json':
return JSON.stringify({ variableName, states, crossings: variableCrossings }, null, 2);
default:
throw new Error(`Unsupported format: ${mergedConfig.format}`);
}
}
/**
* Generate a combined context and variable flow visualization
* @param rootStateId - The root state to start visualization from
* @param config - Context visualization configuration
* @returns Combined context and variable flow visualization
*/
public visualizeContextsAndVariableFlow(rootStateId: string, config: ContextVisualizationConfig): string {
// Get the hierarchy information from the tracking service
const hierarchyInfo = this.trackingService.getContextHierarchy(rootStateId);
// Generate visualization based on the format
switch (config.format) {
case 'mermaid':
return this.generateMermaidContextsAndFlow(hierarchyInfo, config);
case 'dot':
return this.generateDotContextsAndFlow(hierarchyInfo, config);
case 'json':
return JSON.stringify(hierarchyInfo, null, 2);
default:
throw new Error(`Unsupported format: ${config.format}`);
}
}
/**
* Generate a resolution path timeline visualization for a specific variable
* @param variableName - The name of the variable to track resolution for
* @param rootStateId - Optional root state to limit visualization scope
* @param config - Context visualization configuration
* @returns Resolution path timeline visualization
*/
public visualizeResolutionPathTimeline(variableName: string, rootStateId?: string, config?: ContextVisualizationConfig): string {
const defaultConfig: ContextVisualizationConfig = {
format: 'mermaid',
includeVars: true,
includeTimestamps: true,
includeFilePaths: true
};
const mergedConfig = { ...defaultConfig, ...config };
// Get all states or limit to the subtree from rootStateId
let states: StateMetadata[] = [];
if (rootStateId) {
const hierarchyInfo = this.trackingService.getContextHierarchy(rootStateId);
states = hierarchyInfo.states;
} else {
states = this.trackingService.getAllStates();
}
// Get all variable crossings for the specified variable
const allCrossings = states.flatMap(state =>
this.trackingService.getVariableCrossings(state.id)
);
// Filter to just the specified variable
const variableCrossings = allCrossings.filter(
crossing => crossing.variableName === variableName
);
// If there are no crossings, return a simple message
if (variableCrossings.length === 0) {
return `// No variable crossings found for variable "${variableName}"`;
}
// Generate visualization based on the format
switch (mergedConfig.format) {
case 'mermaid':
return this.generateMermaidResolutionTimeline(variableName, states, variableCrossings, mergedConfig);
case 'dot':
return this.generateDotResolutionTimeline(variableName, states, variableCrossings, mergedConfig);
case 'json':
return JSON.stringify({ variableName, states, crossings: variableCrossings }, null, 2);
default:
throw new Error(`Unsupported format: ${mergedConfig.format}`);
}
}
/**
* Generate a Mermaid diagram for context hierarchy
* @private
*/
private generateMermaidContextHierarchy(hierarchyInfo: ContextHierarchyInfo, config: ContextVisualizationConfig): string {
const { states, boundaries } = hierarchyInfo;
let mermaid = 'graph TD\n';
// Add states as nodes
states.forEach(state => {
const label = this.formatStateLabel(state, config);
mermaid += ` ${state.id}["${label}"]\n`;
mermaid += ` style ${state.id} ${this.getContextNodeStyle(state, config)}\n`;
});
// Add boundaries as edges
boundaries.forEach(boundary => {
const style = this.getContextBoundaryStyle(boundary, config);
let label = '';
if (config.includeBoundaryTypes) {
label = ` |${boundary.boundaryType}|`;
}
mermaid += ` ${boundary.sourceStateId} --> ${boundary.targetStateId}${label}\n`;
});
// Add variable crossings if requested
if (config.includeVars && hierarchyInfo.variableCrossings.length > 0) {
mermaid += '\n %% Variable crossings\n';
hierarchyInfo.variableCrossings.forEach(crossing => {
const sourceNodeId = crossing.sourceStateId;
const targetNodeId = crossing.targetStateId;
let label = `${crossing.variableName}`;
if (crossing.alias && crossing.alias !== crossing.variableName) {
label += ` as ${crossing.alias}`;
}
mermaid += ` ${sourceNodeId} -. "${label}" .-> ${targetNodeId}\n`;
});
}
return mermaid;
}
/**
* Generate a DOT diagram for context hierarchy
* @private
*/
private generateDotContextHierarchy(hierarchyInfo: ContextHierarchyInfo, config: ContextVisualizationConfig): string {
const { states, boundaries } = hierarchyInfo;
let dot = 'digraph ContextHierarchy {\n';
dot += ' rankdir=TB;\n';
dot += ' node [shape=box, style=filled, fontname="Arial"];\n';
// Add states as nodes
states.forEach(state => {
const label = this.formatStateLabel(state, config);
dot += ` "${state.id}" [label="${label}" ${this.getContextNodeStyleDot(state, config)}];\n`;
});
// Add boundaries as edges
boundaries.forEach(boundary => {
let label = '';
if (config.includeBoundaryTypes) {
label = `label="${boundary.boundaryType}"`;
}
dot += ` "${boundary.sourceStateId}" -> "${boundary.targetStateId}" [${label} ${this.getContextBoundaryStyleDot(boundary, config)}];\n`;
});
// Add variable crossings if requested
if (config.includeVars && hierarchyInfo.variableCrossings.length > 0) {
dot += '\n // Variable crossings\n';
hierarchyInfo.variableCrossings.forEach(crossing => {
const sourceNodeId = crossing.sourceStateId;
const targetNodeId = crossing.targetStateId;
let label = `${crossing.variableName}`;
if (crossing.alias && crossing.alias !== crossing.variableName) {
label += ` as ${crossing.alias}`;
}
dot += ` "${sourceNodeId}" -> "${targetNodeId}" [label="${label}", style=dashed, color=blue];\n`;
});
}
dot += '}\n';
return dot;
}
/**
* Generate a Mermaid diagram for variable propagation
* @private
*/
private generateMermaidVariablePropagation(
variableName: string,
states: StateMetadata[],
crossings: VariableCrossing[],
config: ContextVisualizationConfig
): string {
// Create a map for quick state lookup
const stateMap = new Map<string, StateMetadata>();
states.forEach(state => stateMap.set(state.id, state));
let mermaid = `graph TD\n %% Variable propagation for "${variableName}"\n`;
// Add states involved in crossings
const involvedStateIds = new Set<string>();
crossings.forEach(crossing => {
involvedStateIds.add(crossing.sourceStateId);
involvedStateIds.add(crossing.targetStateId);
});
// Add states as nodes
Array.from(involvedStateIds).forEach(stateId => {
const state = stateMap.get(stateId);
if (state) {
const label = this.formatStateLabel(state, config);
mermaid += ` ${state.id}["${label}"]\n`;
mermaid += ` style ${state.id} ${this.getContextNodeStyle(state, config)}\n`;
}
});
// Add crossings as edges
crossings.forEach(crossing => {
const sourceNodeId = crossing.sourceStateId;
const targetNodeId = crossing.targetStateId;
let label = variableName;
if (crossing.alias && crossing.alias !== variableName) {
label += ` as ${crossing.alias}`;
}
let edge = '';
if (crossing.variableType === 'text') {
edge = ` -. "${label} (text)" .-> `;
} else if (crossing.variableType === 'data') {
edge = ` -. "${label} (data)" .-> `;
} else if (crossing.variableType === 'path') {
edge = ` -. "${label} (path)" .-> `;
} else {
edge = ` -. "${label}" .-> `;
}
mermaid += ` ${sourceNodeId}${edge}${targetNodeId}\n`;
});
return mermaid;
}
/**
* Generate a DOT diagram for variable propagation
* @private
*/
private generateDotVariablePropagation(
variableName: string,
states: StateMetadata[],
crossings: VariableCrossing[],
config: ContextVisualizationConfig
): string {
// Create a map for quick state lookup
const stateMap = new Map<string, StateMetadata>();
states.forEach(state => stateMap.set(state.id, state));
let dot = `digraph VariablePropagation {\n // Variable propagation for "${variableName}"\n`;
dot += ' rankdir=TB;\n';
dot += ' node [shape=box, style=filled, fontname="Arial"];\n';
// Add states involved in crossings
const involvedStateIds = new Set<string>();
crossings.forEach(crossing => {
involvedStateIds.add(crossing.sourceStateId);
involvedStateIds.add(crossing.targetStateId);
});
// Add states as nodes
Array.from(involvedStateIds).forEach(stateId => {
const state = stateMap.get(stateId);
if (state) {
const label = this.formatStateLabel(state, config);
dot += ` "${state.id}" [label="${label}" ${this.getContextNodeStyleDot(state, config)}];\n`;
}
});
// Add crossings as edges
crossings.forEach(crossing => {
const sourceNodeId = crossing.sourceStateId;
const targetNodeId = crossing.targetStateId;
let label = variableName;
if (crossing.alias && crossing.alias !== variableName) {
label += ` as ${crossing.alias}`;
}
if (crossing.variableType) {
label += ` (${crossing.variableType})`;
}
dot += ` "${sourceNodeId}" -> "${targetNodeId}" [label="${label}", style=dashed, color=blue];\n`;
});
dot += '}\n';
return dot;
}
/**
* Generate a Mermaid diagram for combined context and variable flow
* @private
*/
private generateMermaidContextsAndFlow(hierarchyInfo: ContextHierarchyInfo, config: ContextVisualizationConfig): string {
const { states, boundaries, variableCrossings } = hierarchyInfo;
let mermaid = 'graph TD\n';
// Add states as nodes
states.forEach(state => {
const label = this.formatStateLabel(state, config);
mermaid += ` ${state.id}["${label}"]\n`;
mermaid += ` style ${state.id} ${this.getContextNodeStyle(state, config)}\n`;
});
// Add boundaries as edges
boundaries.forEach(boundary => {
const style = this.getContextBoundaryStyle(boundary, config);
let label = '';
if (config.includeBoundaryTypes) {
label = ` |${boundary.boundaryType}|`;
}
mermaid += ` ${boundary.sourceStateId} --> ${boundary.targetStateId}${label}\n`;
});
// Group variable crossings by variable name
const crossingsByVariable = new Map<string, VariableCrossing[]>();
variableCrossings.forEach(crossing => {
if (!crossingsByVariable.has(crossing.variableName)) {
crossingsByVariable.set(crossing.variableName, []);
}
crossingsByVariable.get(crossing.variableName)!.push(crossing);
});
// Add variable crossings grouped by variable
if (config.includeVars && variableCrossings.length > 0) {
mermaid += '\n %% Variable flows\n';
crossingsByVariable.forEach((crossings, variableName) => {
mermaid += ` %% Flow for variable "${variableName}"\n`;
crossings.forEach(crossing => {
const sourceNodeId = crossing.sourceStateId;
const targetNodeId = crossing.targetStateId;
let label = variableName;
if (crossing.alias && crossing.alias !== variableName) {
label += ` as ${crossing.alias}`;
}
mermaid += ` ${sourceNodeId} -. "${label}" .-> ${targetNodeId}\n`;
});
});
}
return mermaid;
}
/**
* Generate a DOT diagram for combined context and variable flow
* @private
*/
private generateDotContextsAndFlow(hierarchyInfo: ContextHierarchyInfo, config: ContextVisualizationConfig): string {
const { states, boundaries, variableCrossings } = hierarchyInfo;
let dot = 'digraph ContextsAndVariableFlow {\n';
dot += ' rankdir=TB;\n';
dot += ' node [shape=box, style=filled, fontname="Arial"];\n';
// Add states as nodes
states.forEach(state => {
const label = this.formatStateLabel(state, config);
dot += ` "${state.id}" [label="${label}" ${this.getContextNodeStyleDot(state, config)}];\n`;
});
// Add boundaries as edges
boundaries.forEach(boundary => {
let label = '';
if (config.includeBoundaryTypes) {
label = `label="${boundary.boundaryType}"`;
}
dot += ` "${boundary.sourceStateId}" -> "${boundary.targetStateId}" [${label} ${this.getContextBoundaryStyleDot(boundary, config)}];\n`;
});
// Group variable crossings by variable name
const crossingsByVariable = new Map<string, VariableCrossing[]>();
variableCrossings.forEach(crossing => {
if (!crossingsByVariable.has(crossing.variableName)) {
crossingsByVariable.set(crossing.variableName, []);
}
crossingsByVariable.get(crossing.variableName)!.push(crossing);
});
// Add variable crossings grouped by variable
if (config.includeVars && variableCrossings.length > 0) {
dot += '\n // Variable flows\n';
crossingsByVariable.forEach((crossings, variableName) => {
dot += ` // Flow for variable "${variableName}"\n`;
crossings.forEach(crossing => {
const sourceNodeId = crossing.sourceStateId;
const targetNodeId = crossing.targetStateId;
let label = variableName;
if (crossing.alias && crossing.alias !== variableName) {
label += ` as ${crossing.alias}`;
}
dot += ` "${sourceNodeId}" -> "${targetNodeId}" [label="${label}", style=dashed, color=blue];\n`;
});
});
}
dot += '}\n';
return dot;
}
/**
* Generate a Mermaid timeline diagram for variable resolution
* @private
*/
private generateMermaidResolutionTimeline(
variableName: string,
states: StateMetadata[],
crossings: VariableCrossing[],
config: ContextVisualizationConfig
): string {
// Create a map for quick state lookup
const stateMap = new Map<string, StateMetadata>();
states.forEach(state => stateMap.set(state.id, state));
// Sort crossings by timestamp
const sortedCrossings = [...crossings].sort((a, b) => a.timestamp - b.timestamp);
let mermaid = `gantt\n title Resolution Timeline for "${variableName}"\n`;
mermaid += ` dateFormat X\n`;
mermaid += ` axisFormat %s\n`;
// Define context sections
const contextIds = new Set<string>();
crossings.forEach(crossing => {
contextIds.add(crossing.sourceStateId);
contextIds.add(crossing.targetStateId);
});
// Add context sections
Array.from(contextIds).forEach(stateId => {
const state = stateMap.get(stateId);
if (state) {
const contextName = state.filePath ? `${state.id} (${state.filePath})` : state.id;
mermaid += ` section ${contextName}\n`;
// Find all crossings where this state is involved
const relevantCrossings = sortedCrossings.filter(
crossing => crossing.sourceStateId === stateId || crossing.targetStateId === stateId
);
if (relevantCrossings.length === 0) {
mermaid += ` No crossings : 0, 0\n`;
} else {
relevantCrossings.forEach(crossing => {
const direction = crossing.sourceStateId === stateId ? 'out' : 'in';
const otherStateId = direction === 'out' ? crossing.targetStateId : crossing.sourceStateId;
const description = `${direction === 'out' ? 'Export to' : 'Import from'} ${otherStateId}`;
// For timelines, use the timestamp directly
const timestamp = crossing.timestamp;
const duration = 10; // Small fixed duration for visibility
mermaid += ` ${description} : ${timestamp}, ${timestamp + duration}\n`;
});
}
}
});
return mermaid;
}
/**
* Generate a DOT timeline diagram for variable resolution
* @private
*/
private generateDotResolutionTimeline(
variableName: string,
states: StateMetadata[],
crossings: VariableCrossing[],
config: ContextVisualizationConfig
): string {
// Create a map for quick state lookup
const stateMap = new Map<string, StateMetadata>();
states.forEach(state => stateMap.set(state.id, state));
// Sort crossings by timestamp
const sortedCrossings = [...crossings].sort((a, b) => a.timestamp - b.timestamp);
let dot = `digraph ResolutionTimeline {\n label="Resolution Timeline for "${variableName}"\n`;
dot += ' rankdir=LR;\n';
dot += ' node [shape=box, style=filled, fontname="Arial"];\n';
// Create a timeline node for each crossing
sortedCrossings.forEach((crossing, index) => {
const sourceState = stateMap.get(crossing.sourceStateId);
const targetState = stateMap.get(crossing.targetStateId);
const sourceLabel = sourceState?.filePath ? `${crossing.sourceStateId} (${sourceState.filePath})` : crossing.sourceStateId;
const targetLabel = targetState?.filePath ? `${crossing.targetStateId} (${targetState.filePath})` : crossing.targetStateId;
const eventTime = new Date(crossing.timestamp).toISOString().replace('T', ' ').substring(0, 19);
const label = `${eventTime}\\n${variableName}${crossing.alias ? ` as ${crossing.alias}` : ''}`;
dot += ` "event${index}" [label="${label}", shape=circle, color=lightblue];\n`;
// Add edge from source to event
dot += ` "${crossing.sourceStateId}" -> "event${index}" [label="export", style=dashed];\n`;
// Add edge from event to target
dot += ` "event${index}" -> "${crossing.targetStateId}" [label="import", style=dashed];\n`;
// Add node labels if first occurrence
if (!dot.includes(`"${crossing.sourceStateId}" [`)) {
dot += ` "${crossing.sourceStateId}" [label="${sourceLabel}", ${this.getContextNodeStyleDot(sourceState!, config)}];\n`;
}
if (!dot.includes(`"${crossing.targetStateId}" [`)) {
dot += ` "${crossing.targetStateId}" [label="${targetLabel}", ${this.getContextNodeStyleDot(targetState!, config)}];\n`;
}
});
// Add timeline constraint
if (sortedCrossings.length > 1) {
dot += '\n // Timeline ordering\n';
dot += ' { rank=same; ';
for (let i = 0; i < sortedCrossings.length; i++) {
dot += `"event${i}" `;
}
dot += '}\n';
// Add invisible edges for timeline ordering
for (let i = 0; i < sortedCrossings.length - 1; i++) {
dot += ` "event${i}" -> "event${i+1}" [style=invis];\n`;
}
}
dot += '}\n';
return dot;
}
/**
* Format a state label for visualization
* @private
*/
private formatStateLabel(state: StateMetadata, config: ContextVisualizationConfig): string {
let label = state.id;
if (config.includeFilePaths && state.filePath) {
label += `\\n${state.filePath}`;
}
if (config.includeTimestamps && state.createdAt) {
const date = new Date(state.createdAt);
label += `\\n${date.toISOString().replace('T', ' ').substring(0, 19)}`;
}
return label;
}
/**
* Get Mermaid style string for a context node
* @private
*/
private getContextNodeStyle(state: StateMetadata, config: ContextVisualizationConfig): string {
let style = '';
// Style based on state source
switch (state.source) {
case 'new':
style = 'fill:#e1f5fe,stroke:#01579b';
break;
case 'clone':
style = 'fill:#fff9c4,stroke:#fbc02d';
break;
case 'child':
style = 'fill:#c8e6c9,stroke:#388e3c';
break;
case 'merge':
style = 'fill:#f8bbd0,stroke:#c2185b';
break;
case 'implicit':
style = 'fill:#d1c4e9,stroke:#512da8';
break;
default:
style = 'fill:#f5f5f5,stroke:#616161';
}
return style;
}
/**
* Get DOT style string for a context node
* @private
*/
private getContextNodeStyleDot(state: StateMetadata, config: ContextVisualizationConfig): string {
let fillColor = '';
let strokeColor = '';
// Style based on state source
switch (state.source) {
case 'new':
fillColor = '#e1f5fe';
strokeColor = '#01579b';
break;
case 'clone':
fillColor = '#fff9c4';
strokeColor = '#fbc02d';
break;
case 'child':
fillColor = '#c8e6c9';
strokeColor = '#388e3c';
break;
case 'merge':
fillColor = '#f8bbd0';
strokeColor = '#c2185b';
break;
case 'implicit':
fillColor = '#d1c4e9';
strokeColor = '#512da8';
break;
default:
fillColor = '#f5f5f5';
strokeColor = '#616161';
}
return `fillcolor="${fillColor}", color="${strokeColor}"`;
}
/**
* Get Mermaid style for a context boundary edge
* @private
*/
private getContextBoundaryStyle(boundary: ContextBoundary, config: ContextVisualizationConfig): string {
if (boundary.boundaryType === 'import') {
return 'stroke:#388e3c,stroke-width:2';
} else {
return 'stroke:#01579b,stroke-width:2';
}
}
/**
* Get DOT style for a context boundary edge
* @private
*/
private getContextBoundaryStyleDot(boundary: ContextBoundary, config: ContextVisualizationConfig): string {
if (boundary.boundaryType === 'import') {
return 'color="#388e3c", penwidth=2';
} else {
return 'color="#01579b", penwidth=2';
}
}
}