UNPKG

prompt-version-manager

Version:

Centralized prompt management system for Human Behavior AI agents

262 lines 9.75 kB
"use strict"; /** * Merge functionality for PVM TypeScript - handles merging branches with conflict resolution. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.RecursiveMerge = exports.ThreeWayMerge = exports.MergeStrategy = void 0; exports.mergeBranches = mergeBranches; exports.formatConflictMarkers = formatConflictMarkers; const objects_1 = require("../storage/objects"); const exceptions_1 = require("../core/exceptions"); class MergeStrategy { storage; constructor(storage) { this.storage = storage; } } exports.MergeStrategy = MergeStrategy; class ThreeWayMerge extends MergeStrategy { async mergeTrees(baseTreeHash, oursTreeHash, theirsTreeHash) { // Load all three trees const oursTree = await this.loadTree(oursTreeHash); const theirsTree = await this.loadTree(theirsTreeHash); const baseTree = baseTreeHash ? await this.loadTree(baseTreeHash) : {}; // Find all unique filenames const allFiles = new Set([ ...Object.keys(oursTree), ...Object.keys(theirsTree), ...Object.keys(baseTree) ]); const mergedFiles = {}; const conflicts = []; for (const filename of allFiles) { const baseHash = baseTree[filename]; const oursHash = oursTree[filename]; const theirsHash = theirsTree[filename]; // Determine the merge result for this file const mergeResult = await this.mergeFile(filename, baseHash, oursHash, theirsHash); if (mergeResult === null) { // File was deleted in both branches or only existed in base continue; } else if (typeof mergeResult === 'object') { conflicts.push(mergeResult); } else { // Successfully merged mergedFiles[filename] = mergeResult; } } if (conflicts.length > 0) { return { conflicts, mergedFiles, isFastForward: false, hasConflicts: true }; } // Create merged tree const mergedTree = new objects_1.TreeObject(mergedFiles); const mergedTreeHash = await this.storage.storeObject(mergedTree); return { mergedTreeHash, conflicts: [], mergedFiles, isFastForward: false, hasConflicts: false }; } async loadTree(treeHash) { try { const treeObj = await this.storage.loadObject(treeHash); if (!(treeObj instanceof objects_1.TreeObject)) { throw new exceptions_1.StorageError(`Object ${treeHash} is not a tree`); } return treeObj.entries; } catch (error) { if (error instanceof exceptions_1.ObjectNotFoundError) { return {}; } throw error; } } async mergeFile(filename, baseHash, oursHash, theirsHash) { // Case 1: File unchanged if (oursHash === theirsHash) { return oursHash || null; } // Case 2: File only changed in ours if (baseHash === theirsHash) { return oursHash || null; } // Case 3: File only changed in theirs if (baseHash === oursHash) { return theirsHash || null; } // Case 4: File added in both branches with same content if (!baseHash && oursHash && theirsHash && oursHash === theirsHash) { return oursHash; } // Case 5: File deleted in both branches if (!oursHash && !theirsHash) { return null; } // Case 6: File deleted in one branch but modified in other if ((!oursHash && theirsHash !== baseHash) || (!theirsHash && oursHash !== baseHash)) { // This is a conflict - deleted vs modified return await this.createConflict(filename, baseHash, oursHash, theirsHash); } // Case 7: File modified differently in both branches if (oursHash !== theirsHash) { // Try content-level merge for prompts const merged = await this.tryPromptMerge(filename, baseHash, oursHash, theirsHash); if (merged) { return merged; } // Otherwise, it's a conflict return await this.createConflict(filename, baseHash, oursHash, theirsHash); } // Default: keep ours return oursHash || null; } async createConflict(filename, baseHash, oursHash, theirsHash) { const baseContent = baseHash ? await this.loadPromptContent(baseHash) : undefined; const oursContent = oursHash ? await this.loadPromptContent(oursHash) : undefined; const theirsContent = theirsHash ? await this.loadPromptContent(theirsHash) : undefined; return { filename, baseHash, oursHash, theirsHash, baseContent, oursContent, theirsContent }; } async loadPromptContent(promptHash) { try { const promptObj = await this.storage.loadObject(promptHash); if (promptObj instanceof objects_1.PromptVersionObject) { // Return the messages as a string for now const lines = []; for (const msg of promptObj.promptVersion.messages) { lines.push(`## ${msg.role.toUpperCase()}`); lines.push(msg.content); if (msg.metadata) { lines.push(`Metadata: ${JSON.stringify(msg.metadata)}`); } lines.push(''); } return lines.join('\n'); } return `[Binary content: ${promptHash}]`; } catch (error) { return `[Failed to load: ${promptHash}]`; } } async tryPromptMerge(_filename, _baseHash, _oursHash, _theirsHash) { // For now, return null to indicate we can't auto-merge // In the future, this could implement smart prompt merging // based on message-level changes return null; } } exports.ThreeWayMerge = ThreeWayMerge; class RecursiveMerge extends ThreeWayMerge { async findMergeBase(commit1Hash, commit2Hash) { // Get all ancestors of both commits const ancestors1 = await this.getAllAncestors(commit1Hash); const ancestors2 = await this.getAllAncestors(commit2Hash); // Find common ancestors const commonAncestors = new Set(); for (const ancestor of ancestors1) { if (ancestors2.has(ancestor)) { commonAncestors.add(ancestor); } } if (commonAncestors.size === 0) { return undefined; } // Find the most recent common ancestor // (simplified - in Git this uses commit timestamps and graph distance) // For now, just return the first one found return Array.from(commonAncestors)[0]; } async getAllAncestors(commitHash) { const ancestors = new Set(); const toVisit = [commitHash]; while (toVisit.length > 0) { const current = toVisit.shift(); if (ancestors.has(current)) { continue; } ancestors.add(current); try { const commitObj = await this.storage.loadObject(current); if (commitObj instanceof objects_1.CommitObject) { toVisit.push(...commitObj.commit.parentHashes); } } catch (error) { // Skip if we can't load the commit continue; } } return ancestors; } } exports.RecursiveMerge = RecursiveMerge; async function mergeBranches(storage, currentCommitHash, mergeCommitHash, strategy = 'recursive') { // Select merge strategy let merger; if (strategy === 'recursive') { merger = new RecursiveMerge(storage); } else { merger = new ThreeWayMerge(storage); } // Find merge base let mergeBase; if (merger instanceof RecursiveMerge) { mergeBase = await merger.findMergeBase(currentCommitHash, mergeCommitHash); } // Load commits and their trees const currentCommit = await storage.loadObject(currentCommitHash); const mergeCommit = await storage.loadObject(mergeCommitHash); if (!(currentCommit instanceof objects_1.CommitObject) || !(mergeCommit instanceof objects_1.CommitObject)) { throw new exceptions_1.StorageError('Invalid commit objects'); } let baseTreeHash; if (mergeBase) { const baseCommit = await storage.loadObject(mergeBase); if (baseCommit instanceof objects_1.CommitObject) { baseTreeHash = baseCommit.commit.treeHash; } } // Perform the merge const result = await merger.mergeTrees(baseTreeHash, currentCommit.commit.treeHash, mergeCommit.commit.treeHash); return result; } function formatConflictMarkers(conflict) { const lines = []; lines.push('<<<<<<< HEAD'); if (conflict.oursContent) { lines.push(conflict.oursContent); } else { lines.push('[DELETED]'); } lines.push('======='); if (conflict.theirsContent) { lines.push(conflict.theirsContent); } else { lines.push('[DELETED]'); } lines.push(`>>>>>>> ${conflict.theirsHash?.substring(0, 7) || 'theirs'}`); return lines.join('\n'); } //# sourceMappingURL=merge.js.map