meld
Version:
Meld: A template language for LLM prompts
564 lines (490 loc) • 18.2 kB
text/typescript
import { stateLogger as logger } from '@core/utils/logger.js';
import type { IStateTrackingService, StateMetadata, StateRelationship } from './IStateTrackingService.js';
import { v4 as uuidv4 } from 'uuid';
/**
* @package
* Implementation of the state tracking service.
*
* @remarks
* Provides state instance tracking, relationship management, and metadata storage.
* Uses UUIDs for state identification and maintains relationship graphs.
*/
export class StateTrackingService implements IStateTrackingService {
private states: Map<string, StateMetadata>;
private relationships: Map<string, StateRelationship[]>;
private contextBoundaries: ContextBoundary[] = [];
private variableCrossings: VariableCrossing[] = [];
constructor() {
this.states = new Map();
this.relationships = new Map();
}
registerState(metadata: Partial<StateMetadata> & { id?: string }): string {
// Use provided ID or generate a new one
const stateId = metadata.id || uuidv4();
if (this.states.has(stateId)) {
// Update existing state metadata
const existingMetadata = this.states.get(stateId)!;
this.states.set(stateId, {
...existingMetadata,
...metadata,
id: stateId
});
} else {
// Create new state metadata
this.states.set(stateId, {
id: stateId,
source: metadata.source || 'implicit',
parentId: metadata.parentId,
filePath: metadata.filePath,
transformationEnabled: metadata.transformationEnabled || false,
createdAt: Date.now()
});
}
return stateId;
}
getStateMetadata(stateId: string): StateMetadata | undefined {
return this.states.get(stateId);
}
addRelationship(sourceId: string, targetId: string, type: 'parent-child' | 'merge-source' | 'merge-target'): void {
logger.debug('Adding relationship:', {
operation: 'addRelationship',
sourceId,
targetId,
type,
sourceState: this.states.get(sourceId),
targetState: this.states.get(targetId),
sourceRelationships: this.relationships.get(sourceId),
targetRelationships: this.relationships.get(targetId)
});
// Ensure both states exist
if (!this.states.has(sourceId)) {
logger.debug('Creating missing source state', { sourceId });
this.registerState({ id: sourceId });
}
if (!this.states.has(targetId)) {
logger.debug('Creating missing target state', { targetId });
this.registerState({ id: targetId });
}
// Initialize relationships arrays if they don't exist
if (!this.relationships.has(sourceId)) {
logger.debug('Initializing source relationships array', { sourceId });
this.relationships.set(sourceId, []);
}
if (!this.relationships.has(targetId)) {
logger.debug('Initializing target relationships array', { targetId });
this.relationships.set(targetId, []);
}
// Get the current relationships
const relationships = this.relationships.get(sourceId)!;
logger.debug('Current relationships before adding new one:', {
sourceId,
targetId,
type,
existingRelationships: relationships
});
// Check if this exact relationship already exists
const existingRelationship = relationships.find(rel =>
rel.targetId === targetId && rel.type === type
);
// Add the new relationship if it doesn't exist
if (!existingRelationship) {
relationships.push({ targetId, type });
logger.debug('Added new relationship:', {
sourceId,
targetId,
type,
updatedRelationships: relationships
});
// For parent-child relationships, update the child's metadata
if (type === 'parent-child') {
const targetState = this.states.get(targetId);
if (targetState) {
const oldParentId = targetState.parentId;
targetState.parentId = sourceId;
this.states.set(targetId, targetState);
logger.debug('Updated child state metadata for parent-child:', {
childId: targetId,
oldParentId,
newParentId: sourceId,
updatedMetadata: targetState
});
}
}
// For merge operations, we need to handle both source and target relationships
if (type === 'merge-source' || type === 'merge-target') {
const sourceState = this.states.get(sourceId);
const targetState = this.states.get(targetId);
logger.debug('Processing merge relationship:', {
type,
sourceState,
targetState,
sourceStateParentId: sourceState?.parentId,
targetStateParentId: targetState?.parentId
});
if (sourceState && targetState) {
if (type === 'merge-source') {
const oldParentId = targetState.parentId;
targetState.parentId = sourceId;
this.states.set(targetId, targetState);
logger.debug('Updated target state metadata for merge-source:', {
targetId,
oldParentId,
newParentId: sourceId,
updatedMetadata: targetState
});
} else if (type === 'merge-target') {
const targetParentId = targetState.parentId;
if (targetParentId) {
const oldParentId = sourceState.parentId;
sourceState.parentId = targetParentId;
this.states.set(sourceId, sourceState);
logger.debug('Updated source state metadata for merge-target:', {
sourceId,
oldParentId,
newParentId: targetParentId,
updatedMetadata: sourceState
});
}
}
}
}
}
logger.debug('Final state after relationship operation:', {
sourceId,
targetId,
type,
sourceState: this.states.get(sourceId),
targetState: this.states.get(targetId),
sourceRelationships: this.relationships.get(sourceId),
targetRelationships: this.relationships.get(targetId)
});
}
getRelationships(stateId: string): StateRelationship[] {
return this.relationships.get(stateId) || [];
}
getParentState(stateId: string): string | undefined {
const metadata = this.states.get(stateId);
return metadata?.parentId;
}
getChildStates(stateId: string): string[] {
const relationships = this.relationships.get(stateId) || [];
return relationships
.filter(r => r.type === 'parent-child' || r.type === 'merge-source')
.map(r => r.targetId);
}
hasState(stateId: string): boolean {
return this.states.has(stateId);
}
getAllStates(): StateMetadata[] {
return Array.from(this.states.values());
}
getStateLineage(stateId: string, visited: Set<string> = new Set()): string[] {
logger.debug('Getting state lineage:', {
operation: 'getStateLineage',
stateId,
visitedStates: Array.from(visited),
currentState: this.states.get(stateId)
});
if (!this.states.has(stateId)) {
logger.debug('State not found, returning empty lineage', { stateId });
return [];
}
// If we've seen this state before, return empty array to prevent cycles
if (visited.has(stateId)) {
logger.debug('State already visited, preventing cycle', { stateId });
return [];
}
// Mark this state as visited
visited.add(stateId);
logger.debug('Marked state as visited', {
stateId,
visitedStates: Array.from(visited)
});
// Get the state's metadata
const metadata = this.states.get(stateId)!;
logger.debug('Retrieved state metadata', {
stateId,
metadata,
relationships: this.relationships.get(stateId) || []
});
// Get parent's lineage first (recursively)
let parentLineage: string[] = [];
if (metadata.parentId) {
parentLineage = this.getStateLineage(metadata.parentId, visited);
logger.debug('Retrieved parent lineage', {
stateId,
parentId: metadata.parentId,
parentLineage,
parentState: this.states.get(metadata.parentId)
});
}
// Check for merge relationships
const relationships = this.relationships.get(stateId) || [];
const mergeTargets = relationships
.filter(rel => rel.type === 'merge-target')
.map(rel => rel.targetId);
logger.debug('Found merge target relationships', {
stateId,
relationships,
mergeTargets,
mergeTargetStates: mergeTargets.map(id => this.states.get(id))
});
// Get lineage from merge targets AND their parents
const mergeLineages = mergeTargets.flatMap(targetId => {
logger.debug('Processing merge target', {
stateId,
targetId,
targetState: this.states.get(targetId),
targetRelationships: this.relationships.get(targetId)
});
if (visited.has(targetId)) {
logger.debug('Merge target already visited, skipping', { targetId });
return [];
}
const targetState = this.states.get(targetId);
if (!targetState) {
logger.debug('Merge target state not found', { targetId });
return [];
}
// Include target's parent in lineage
const targetParentId = targetState.parentId;
logger.debug('Processing merge target parent', {
targetId,
targetParentId,
targetParentState: targetParentId ? this.states.get(targetParentId) : undefined,
targetParentRelationships: targetParentId ? this.relationships.get(targetParentId) : undefined
});
if (targetParentId && !visited.has(targetParentId)) {
// Get parent's lineage first
const parentLineage = this.getStateLineage(targetParentId, visited);
// Then get target's lineage
const targetLineage = this.getStateLineage(targetId, visited);
logger.debug('Combined merge target lineages', {
targetId,
parentLineage,
targetLineage,
combined: [...new Set([...parentLineage, ...targetLineage])]
});
// Combine them, ensuring no duplicates
return [...new Set([...parentLineage, ...targetLineage])];
}
// If no parent, just get target's lineage
const targetLineage = this.getStateLineage(targetId, visited);
logger.debug('Got merge target lineage (no parent)', {
targetId,
targetLineage
});
return targetLineage;
});
logger.debug('Processed all merge lineages', {
stateId,
mergeLineages,
flattenedMergeLineages: mergeLineages.flat()
});
// Combine parent lineage with merge target lineages
const combinedLineage = [...parentLineage];
logger.debug('Starting lineage combination', {
stateId,
initialCombinedLineage: combinedLineage
});
// Ensure we're working with arrays, not strings
const flattenedMergeLineages = mergeLineages.flat();
logger.debug('Flattened merge lineages', {
stateId,
flattenedMergeLineages
});
// Add each ID from the flattened merge lineages
for (const id of flattenedMergeLineages) {
if (!combinedLineage.includes(id)) {
combinedLineage.push(id);
logger.debug('Added ID to combined lineage', {
stateId,
addedId: id,
updatedCombinedLineage: combinedLineage
});
}
}
// Add current state to the lineage
if (!combinedLineage.includes(stateId)) {
combinedLineage.push(stateId);
logger.debug('Added current state to lineage', {
stateId,
finalCombinedLineage: combinedLineage
});
}
logger.debug('Final lineage result', {
stateId,
parentLineage,
mergeLineages: flattenedMergeLineages,
combinedLineage,
relationships: this.relationships.get(stateId)
});
return combinedLineage;
}
getStateDescendants(stateId: string, visited: Set<string> = new Set()): string[] {
if (!this.states.has(stateId)) {
return [];
}
// If we've seen this state before, return empty array to prevent cycles
if (visited.has(stateId)) {
return [];
}
// Mark this state as visited
visited.add(stateId);
// Get all relationships where this state is the parent
const childRelationships = this.relationships.get(stateId) || [];
// Get immediate children
const children = childRelationships
.filter(rel => rel.type === 'parent-child' || rel.type === 'merge-source')
.map(rel => rel.targetId);
// Get descendants of each child
const descendantArrays = children.map(childId =>
this.getStateDescendants(childId, visited)
);
// Combine immediate children with their descendants
return [...children, ...descendantArrays.flat()];
}
/**
* Track context boundary creation during import or embed operations.
* @param sourceStateId - The parent/source state ID
* @param targetStateId - The child/target state ID
* @param boundaryType - The type of boundary (import or embed)
* @param filePath - The file path associated with the boundary
*/
trackContextBoundary(
sourceStateId: string,
targetStateId: string,
boundaryType: 'import' | 'embed',
filePath?: string
): void {
if (!this.hasState(sourceStateId) || !this.hasState(targetStateId)) {
console.warn(`Cannot track context boundary: One or both states not found (${sourceStateId}, ${targetStateId})`);
return;
}
// Record the context boundary
this.contextBoundaries.push({
sourceStateId,
targetStateId,
boundaryType,
filePath,
createdAt: Date.now()
});
// Also make sure we have the parent-child relationship recorded
this.addRelationship(sourceStateId, targetStateId, 'parent-child');
}
/**
* Track variable copying between contexts.
* @param sourceStateId - The source state ID
* @param targetStateId - The target state ID
* @param variableName - The name of the variable being copied
* @param variableType - The type of variable
* @param alias - Optional alias for the variable in the target context
*/
trackVariableCrossing(
sourceStateId: string,
targetStateId: string,
variableName: string,
variableType: 'text' | 'data' | 'path' | 'command',
alias?: string
): void {
if (!this.hasState(sourceStateId) || !this.hasState(targetStateId)) {
console.warn(`Cannot track variable crossing: One or both states not found (${sourceStateId}, ${targetStateId})`);
return;
}
this.variableCrossings.push({
sourceStateId,
targetStateId,
variableName,
variableType,
alias,
timestamp: Date.now()
});
}
/**
* Get all context boundaries for visualization.
* @returns Array of context boundary information
*/
getContextBoundaries(): ContextBoundary[] {
return [...this.contextBoundaries];
}
/**
* Get variable crossings for a specific state.
* @param stateId - The state ID to get variable crossings for
* @returns Array of variable crossing information
*/
getVariableCrossings(stateId: string): VariableCrossing[] {
return this.variableCrossings.filter(
crossing => crossing.sourceStateId === stateId || crossing.targetStateId === stateId
);
}
/**
* Get all state relationships of a specific type.
* @param type - The type of relationship to get
* @returns Array of relationships
*/
getRelationshipsByType(type: 'parent-child' | 'merge-source' | 'merge-target'): StateRelationshipInfo[] {
const results: StateRelationshipInfo[] = [];
this.states.forEach((_, sourceId) => {
const relationships = this.getRelationships(sourceId);
relationships
.filter(rel => rel.type === type)
.forEach(rel => {
// Find the timestamp for when this relationship was created
const createdAt = this.findRelationshipTimestamp(sourceId, rel.targetId) || Date.now();
results.push({
sourceId,
targetId: rel.targetId,
type: rel.type,
createdAt
});
});
});
return results;
}
/**
* Generate context hierarchy information for a specific state and its descendants.
* @param rootStateId - The root state to start from
* @returns Context hierarchy information
*/
getContextHierarchy(rootStateId: string): ContextHierarchyInfo {
// Get all descendants plus the root state itself
const descendants = this.getStateDescendants(rootStateId);
const stateIds = [rootStateId, ...descendants];
// Get relevant states
const states = stateIds.map(id => this.states.get(id)).filter(Boolean) as StateMetadata[];
// Get context boundaries that involve these states
const boundaries = this.contextBoundaries.filter(
boundary => stateIds.includes(boundary.sourceStateId) && stateIds.includes(boundary.targetStateId)
);
// Get variable crossings that involve these states
const variableCrossings = this.variableCrossings.filter(
crossing => stateIds.includes(crossing.sourceStateId) && stateIds.includes(crossing.targetStateId)
);
return {
rootStateId,
states,
boundaries,
variableCrossings
};
}
/**
* Helper to find the timestamp when a relationship was created
* @private
*/
private findRelationshipTimestamp(sourceId: string, targetId: string): number | undefined {
// Check context boundaries first as they're most likely to have accurate timestamps
const contextBoundary = this.contextBoundaries.find(
b => b.sourceStateId === sourceId && b.targetStateId === targetId
);
if (contextBoundary) {
return contextBoundary.createdAt;
}
// If no direct match found, try to infer from state metadata
const sourceState = this.states.get(sourceId);
const targetState = this.states.get(targetId);
if (targetState && targetState.createdAt) {
return targetState.createdAt;
}
return sourceState?.createdAt;
}
}