prompt-version-manager
Version:
Centralized prompt management system for Human Behavior AI agents
262 lines • 9.75 kB
JavaScript
;
/**
* 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