fix: add ID-based fallback matching to prevent ghost resurrection (bd-ncwo)

Root cause: The merge driver matched issues by IssueKey (ID+CreatedAt+CreatedBy).
When timestamp precision differed (e.g., with/without nanoseconds), the same
issue was treated as two different issues, causing both tombstone and closed
versions to appear in the merge result.

Fix: Added ID-based fallback matching in merge3WayWithTTL. When key-based
matching fails but the same ID exists in the other side, use that for merging.
Also track processed IDs to prevent duplicates.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-16 22:35:29 -08:00
parent 0ce039429d
commit fcd29f5ea4
2 changed files with 201 additions and 2 deletions

View File

@@ -298,7 +298,7 @@ func merge3Way(base, left, right []Issue) ([]Issue, []string) {
// merge3WayWithTTL performs a 3-way merge with configurable tombstone TTL.
// This is the core merge function that handles tombstone semantics.
func merge3WayWithTTL(base, left, right []Issue, ttl time.Duration) ([]Issue, []string) {
// Build maps for quick lookup
// Build maps for quick lookup by IssueKey
baseMap := make(map[IssueKey]Issue)
for _, issue := range base {
baseMap[makeKey(issue)] = issue
@@ -314,8 +314,22 @@ func merge3WayWithTTL(base, left, right []Issue, ttl time.Duration) ([]Issue, []
rightMap[makeKey(issue)] = issue
}
// Track which issues we've processed
// bd-ncwo: Also build ID-based maps for fallback matching
// This handles cases where the same issue has slightly different CreatedAt/CreatedBy
// (e.g., due to timestamp precision differences between systems)
leftByID := make(map[string]Issue)
for _, issue := range left {
leftByID[issue.ID] = issue
}
rightByID := make(map[string]Issue)
for _, issue := range right {
rightByID[issue.ID] = issue
}
// Track which issues we've processed (by both key and ID)
processed := make(map[IssueKey]bool)
processedIDs := make(map[string]bool) // bd-ncwo: track processed IDs to avoid duplicates
var result []Issue
var conflicts []string
@@ -341,6 +355,43 @@ func merge3WayWithTTL(base, left, right []Issue, ttl time.Duration) ([]Issue, []
leftIssue, inLeft := leftMap[key]
rightIssue, inRight := rightMap[key]
// bd-ncwo: ID-based fallback matching for tombstone preservation
// If key doesn't match but same ID exists in the other side, use that
if !inLeft && inRight {
if fallback, found := leftByID[rightIssue.ID]; found {
leftIssue = fallback
inLeft = true
// Mark the fallback's key as processed to avoid duplicate
processed[makeKey(fallback)] = true
}
}
if !inRight && inLeft {
if fallback, found := rightByID[leftIssue.ID]; found {
rightIssue = fallback
inRight = true
// Mark the fallback's key as processed to avoid duplicate
processed[makeKey(fallback)] = true
}
}
// bd-ncwo: Check if we've already processed this ID (via a different key)
currentID := key.ID
if currentID == "" {
if inLeft {
currentID = leftIssue.ID
} else if inRight {
currentID = rightIssue.ID
} else if inBase {
currentID = baseIssue.ID
}
}
if currentID != "" && processedIDs[currentID] {
continue
}
if currentID != "" {
processedIDs[currentID] = true
}
// Determine tombstone status
leftTombstone := inLeft && IsTombstone(leftIssue)
rightTombstone := inRight && IsTombstone(rightIssue)